From 7d3cb26611e51b52c3fc888c97870960a41d2d51 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 17 Jul 2024 10:55:08 -0700 Subject: [PATCH 001/260] Remove unused `AccountId` from `ChainDataType` --- bittensor/chain_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/chain_data.py b/bittensor/chain_data.py index e68658edb..029cb2982 100644 --- a/bittensor/chain_data.py +++ b/bittensor/chain_data.py @@ -333,7 +333,6 @@ class ChainDataType(Enum): IPInfo = 7 SubnetHyperparameters = 8 ScheduledColdkeySwapInfo = 9 - AccountId = 10 def from_scale_encoding( From c44ac68d84d570f725c8a1c9580748feb0219eed Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 17 Jul 2024 10:56:18 -0700 Subject: [PATCH 002/260] Update `decode_hex_identity_dict` function in commands utils. --- bittensor/utils/wallet_utils.py | 53 +++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/bittensor/utils/wallet_utils.py b/bittensor/utils/wallet_utils.py index 39218c33f..60c07e105 100644 --- a/bittensor/utils/wallet_utils.py +++ b/bittensor/utils/wallet_utils.py @@ -2,26 +2,27 @@ # Copyright © 2021 Yuma Rao # Copyright © 2022 Opentensor Foundation # Copyright © 2023 Opentensor Technologies Inc - +# # 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. -from substrateinterface.utils import ss58 from typing import Union, Optional -from .. import __ss58_format__ from substrateinterface import Keypair as Keypair +from substrateinterface.utils import ss58 + +from .. import __ss58_format__ def get_ss58_format(ss58_address: str) -> int: @@ -155,14 +156,54 @@ def create_identity_dict( def decode_hex_identity_dict(info_dictionary): + """ + Decodes hex-encoded strings in a dictionary. + + This function traverses the given dictionary, identifies hex-encoded strings, and decodes them into readable strings. It handles nested dictionaries and lists within the dictionary. + + Args: + info_dictionary (dict): The dictionary containing hex-encoded strings to decode. + + Returns: + dict: The dictionary with decoded strings. + + Examples: + input_dict = { + ... "name": {"value": "0x6a6f686e"}, + ... "additional": [ + ... [{"data": "0x64617461"}] + ... ] + ... } + decode_hex_identity_dict(input_dict) + {'name': 'john', 'additional': [('data', 'data')]} + """ + + def get_decoded(data: str): + """Decodes a hex-encoded string.""" + try: + return bytes.fromhex(data[2:]).decode() + except UnicodeDecodeError: + print(f"Could not decode: {key}: {item}") + for key, value in info_dictionary.items(): if isinstance(value, dict): item = list(value.values())[0] if isinstance(item, str) and item.startswith("0x"): try: - info_dictionary[key] = bytes.fromhex(item[2:]).decode() + info_dictionary[key] = get_decoded(item) except UnicodeDecodeError: print(f"Could not decode: {key}: {item}") else: info_dictionary[key] = item + if key == "additional": + additional = [] + for item in value: + additional.append( + tuple( + get_decoded(data=next(iter(sub_item.values()))) + for sub_item in item + ) + ) + info_dictionary[key] = additional + return info_dictionary From 9118c00ca6f98eba96e50bca74c0de04fe4caebf Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 17 Jul 2024 10:57:11 -0700 Subject: [PATCH 003/260] Update `DelegatesDetails` class and `get_delegates_details` --- bittensor/commands/utils.py | 77 ++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/bittensor/commands/utils.py b/bittensor/commands/utils.py index 661cd818c..e9d3b9178 100644 --- a/bittensor/commands/utils.py +++ b/bittensor/commands/utils.py @@ -1,30 +1,31 @@ # The MIT License (MIT) # Copyright © 2021 Yuma Rao - +# # 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 sys import os +import sys +from dataclasses import dataclass +from typing import Any, List, Dict, Optional, Tuple + +from rich.prompt import Confirm, PromptBase + import bittensor -import requests -from bittensor.utils.registration import torch -from bittensor.utils.balance import Balance from bittensor.utils import U64_NORMALIZED_FLOAT, U16_NORMALIZED_FLOAT -from typing import List, Dict, Any, Optional, Tuple -from rich.prompt import Confirm, PromptBase -from dataclasses import dataclass +from bittensor.utils.balance import Balance +from bittensor.utils.registration import torch from . import defaults console = bittensor.__console__ @@ -244,40 +245,38 @@ def normalize_hyperparameters( @dataclass class DelegatesDetails: - name: str - url: str - description: str - signature: str + display: str + additional: List[Tuple[str, str]] + web: str + legal: Optional[str] = None + riot: Optional[str] = None + email: Optional[str] = None + pgp_fingerprint: Optional[str] = None + image: Optional[str] = None + twitter: Optional[str] = None @classmethod - def from_json(cls, json: Dict[str, any]) -> "DelegatesDetails": + def from_chain_data(cls, data: Dict[str, Any]) -> "DelegatesDetails": return cls( - name=json["name"], - url=json["url"], - description=json["description"], - signature=json["signature"], + display=data.get("display", ""), + additional=data.get("additional", []), + web=data.get("web", ""), + legal=data.get("legal"), + riot=data.get("riot"), + email=data.get("email"), + pgp_fingerprint=data.get("pgp_fingerprint"), + image=data.get("image"), + twitter=data.get("image"), ) -def _get_delegates_details_from_github( - requests_get, url: str -) -> Dict[str, DelegatesDetails]: - response = requests_get(url) - - if response.status_code == 200: - all_delegates: Dict[str, Any] = response.json() - all_delegates_details = {} - for delegate_hotkey, delegates_details in all_delegates.items(): - all_delegates_details[delegate_hotkey] = DelegatesDetails.from_json( - delegates_details - ) - return all_delegates_details - else: - return {} - - -def get_delegates_details(url: str) -> Optional[Dict[str, DelegatesDetails]]: +def get_delegates_details( + subtensor: "bittensor.subtensor", +) -> Optional[Dict[str, DelegatesDetails]]: try: - return _get_delegates_details_from_github(requests.get, url) - except Exception: + return subtensor.get_delegate_identities() + except Exception as error: + bittensor.logging.exception( + f"Unable to get Delegates Identities. Error: {error}" + ) return None # Fail silently From 6b9cdb320319227b1a7b1b31fac000d374904487 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 17 Jul 2024 11:04:07 -0700 Subject: [PATCH 004/260] Add `get_delegate_identities` method to Subtensor class --- bittensor/subtensor.py | 45 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index d1ebac9df..4627cdd65 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -1,15 +1,15 @@ # The MIT License (MIT) # Copyright © 2021 Yuma Rao # Copyright © 2023 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 @@ -56,6 +56,7 @@ IPInfo, custom_rpc_type_registry, ) +from .commands.utils import DelegatesDetails from .errors import IdentityError, NominationError, StakeError, TakeError from .extrinsics.commit_weights import ( commit_weights_extrinsic, @@ -2727,7 +2728,7 @@ def _do_set_root_weights( Tuple[bool, Optional[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 on the root network. + trust in other neurons based on observed performance and contributions to the root network. """ @retry(delay=2, tries=3, backoff=2, max_delay=4, logger=_logger) @@ -2756,7 +2757,7 @@ def make_substrate_call_with_retry(): ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalziation or inclusion." + return True, "Not waiting for finalization or inclusion." response.process_events() if response.is_success: @@ -2877,6 +2878,40 @@ def make_substrate_call_with_retry() -> bool: return make_substrate_call_with_retry() + def get_delegate_identities(self, block: Optional[int] = None) -> Dict[str, DelegatesDetails]: + """ + Retrieves the identities of all delegates from the blockchain. + + This method makes a substrate call to retrieve the identities of all delegates and retries the call up to three times with exponential backoff in case of failures. + + Args: + block (Optional[int], optional): The block number to retrieve the delegate identities from. If ``None``, the latest block is used. Default is ``None``. + + Returns: + Dict[str, DelegatesDetails]: A dictionary where the keys are delegate SS58 addresses and the values are DelegatesDetails objects containing the details of each delegate. + """ + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + def make_substrate_call_with_retry() -> "QueryMapResult": + return self.substrate.query_map( + module="Registry", + storage_function="IdentityOf", + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), + ) + + identities_info = make_substrate_call_with_retry() + + result = { + ss58_address.value: DelegatesDetails.from_chain_data( + bittensor.utils.wallet_utils.decode_hex_identity_dict( + identity.value["info"] + ) + ) + for ss58_address, identity in identities_info + } + return result + # Make some commitment on-chain about arbitrary data. def commit(self, wallet, netuid: int, data: str): """ From ef9a8c51327934d2024c9fc6d51e09ea15d276a5 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 17 Jul 2024 15:58:42 -0700 Subject: [PATCH 005/260] Update `bittensor.commands.utils.get_delegates_details` usage across the bittensor. --- bittensor/commands/delegates.py | 62 ++++++++++++++++----------------- bittensor/commands/inspect.py | 8 ++--- bittensor/commands/network.py | 6 ++-- bittensor/commands/root.py | 6 ++-- bittensor/commands/senate.py | 22 +++++------- bittensor/commands/stake.py | 10 +++--- 6 files changed, 49 insertions(+), 65 deletions(-) diff --git a/bittensor/commands/delegates.py b/bittensor/commands/delegates.py index 4d03b289e..4f6e63e81 100644 --- a/bittensor/commands/delegates.py +++ b/bittensor/commands/delegates.py @@ -46,7 +46,7 @@ def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: def show_delegates_lite( - delegates_lite: List["bittensor.DelegateInfoLite"], width: Optional[int] = None + subtensor: "bittensor.subtensor", delegates_lite: List["bittensor.DelegateInfoLite"], width: Optional[int] = None ): """ This method is a lite version of the :func:`show_delegates`. This method displays a formatted table of Bittensor network delegates with detailed statistics to the console. @@ -57,6 +57,7 @@ def show_delegates_lite( This helper function is not intended to be used directly in user code unless specifically required. Args: + subtensor (bittensor.subtensor): The instance of the subtensor class. delegates_lite (List[bittensor.DelegateInfoLite]): A list of delegate information objects to be displayed. width (Optional[int]): The width of the console output table. Defaults to ``None``, which will make the table expand to the maximum width of the console. @@ -83,9 +84,7 @@ def show_delegates_lite( the table in the console. """ - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) - ) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) if registered_delegate_info is None: bittensor.__console__.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" @@ -120,14 +119,17 @@ def show_delegates_lite( table.add_column("[overline white]Desc", style="rgb(50,163,219)") for i, d in enumerate(delegates_lite): + delegate_name = "" + delegate_url = "" + delegate_description = "" if d.delegate_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[d.delegate_ss58].name - delegate_url = registered_delegate_info[d.delegate_ss58].url - delegate_description = registered_delegate_info[d.delegate_ss58].description - else: - delegate_name = "" - delegate_url = "" - delegate_description = "" + delegate = registered_delegate_info[d.delegate_ss58] + + delegate_name = delegate.display + delegate_url = delegate.web + + if len(delegate.additional) > 0 and len(delegate.additional[0]) == 2: + delegate_description = delegate.additional[0][1] table.add_row( # `INDEX` column @@ -153,6 +155,7 @@ def show_delegates_lite( # Uses rich console to pretty print a table of delegates. def show_delegates( + subtensor: "bittensor.subtensor", delegates: List["bittensor.DelegateInfo"], prev_delegates: Optional[List["bittensor.DelegateInfo"]], width: Optional[int] = None, @@ -167,6 +170,7 @@ def show_delegates( to be used directly in user code unless specifically required. Args: + subtensor (bittensor.subtensor): The instance of the subtensor class. delegates (List[bittensor.DelegateInfo]): A list of delegate information objects to be displayed. prev_delegates (Optional[List[bittensor.DelegateInfo]]): A list of delegate information objects from a previous state, used to calculate changes in stake. Defaults to ``None``. width (Optional[int]): The width of the console output table. Defaults to ``None``, which will make the table expand to the maximum width of the console. @@ -206,9 +210,7 @@ def show_delegates( for prev_delegate in prev_delegates: prev_delegates_dict[prev_delegate.hotkey_ss58] = prev_delegate - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) - ) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) if registered_delegate_info is None: bittensor.__console__.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" @@ -265,16 +267,17 @@ def show_delegates( ), bittensor.Balance.from_rao(0), # default to 0 if no owner stake. ) + + delegate_name = "" + delegate_url = "" + delegate_description = "" + if delegate.hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[delegate.hotkey_ss58].name - delegate_url = registered_delegate_info[delegate.hotkey_ss58].url - delegate_description = registered_delegate_info[ - delegate.hotkey_ss58 - ].description - else: - delegate_name = "" - delegate_url = "" - delegate_description = "" + delegate_ = registered_delegate_info[delegate.hotkey_ss58] + delegate_name = delegate_.display + delegate_url = delegate_.web + if len(delegate_.additional) > 0 and len(delegate_.additional[1]) == 2: + delegate_description = delegate_.additional[0][1] if delegate.hotkey_ss58 in prev_delegates_dict: prev_stake = prev_delegates_dict[delegate.hotkey_ss58].total_stake @@ -431,7 +434,7 @@ def check_config(config: "bittensor.config"): sys.exit(1) delegates.sort(key=lambda delegate: delegate.total_stake, reverse=True) - show_delegates(delegates, prev_delegates=prev_delegates) + show_delegates(subtensor=subtensor, delegates=delegates, prev_delegates=prev_delegates) delegate_index = Prompt.ask("Enter delegate index") config.delegate_ss58key = str(delegates[int(delegate_index)].hotkey_ss58) console.print( @@ -576,7 +579,7 @@ def check_config(config: "bittensor.config"): sys.exit(1) delegates.sort(key=lambda delegate: delegate.total_stake, reverse=True) - show_delegates(delegates, prev_delegates=prev_delegates) + show_delegates(subtensor=subtensor, delegates=delegates, prev_delegates=prev_delegates) delegate_index = Prompt.ask("Enter delegate index") config.delegate_ss58key = str(delegates[int(delegate_index)].hotkey_ss58) console.print( @@ -681,10 +684,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ":warning: [yellow]Could not fetch delegates history[/yellow]" ) - show_delegates( - delegates, - prev_delegates=prev_delegates, - width=cli.config.get("width", None), + show_delegates(subtensor=subtensor, delegates=delegates, prev_delegates=prev_delegates, width=cli.config.get("width", None), ) @staticmethod @@ -948,9 +948,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): delegates.sort(key=lambda delegate: delegate[0].total_stake, reverse=True) total_delegated += sum(my_delegates.values()) - registered_delegate_info: Optional[DelegatesDetails] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) - ) + registered_delegate_info: Optional[DelegatesDetails] = get_delegates_details(subtensor=subtensor) if registered_delegate_info is None: bittensor.__console__.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" diff --git a/bittensor/commands/inspect.py b/bittensor/commands/inspect.py index 4ef0e84c4..884bc3967 100644 --- a/bittensor/commands/inspect.py +++ b/bittensor/commands/inspect.py @@ -138,9 +138,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) bittensor.logging.debug(f"Netuids to check: {netuids}") - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) - ) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) if registered_delegate_info is None: bittensor.__console__.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" @@ -190,7 +188,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): table.add_row(wallet.name, str(cold_balance), "", "", "", "", "", "", "") for dele, staked in delegates: if dele.hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[dele.hotkey_ss58].name + delegate_name = registered_delegate_info[dele.hotkey_ss58].display else: delegate_name = dele.hotkey_ss58 table.add_row( @@ -249,7 +247,7 @@ def check_config(config: "bittensor.config"): wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) - if config.netuids != [] and config.netuids != None: + if config.netuids != [] and config.netuids is not None: if not isinstance(config.netuids, list): config.netuids = [int(config.netuids)] else: diff --git a/bittensor/commands/network.py b/bittensor/commands/network.py index 3564bc534..51cc014dd 100644 --- a/bittensor/commands/network.py +++ b/bittensor/commands/network.py @@ -247,9 +247,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): rows = [] total_neurons = 0 - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - url=bittensor.__delegates_details_url__ - ) + delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) for subnet in subnets: total_neurons += subnet.max_n @@ -262,7 +260,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): str(subnet.tempo), f"{subnet.burn!s:8.8}", str(bittensor.utils.formatting.millify(subnet.difficulty)), - f"{delegate_info[subnet.owner_ss58].name if subnet.owner_ss58 in delegate_info else subnet.owner_ss58}", + f"{delegate_info[subnet.owner_ss58].display if subnet.owner_ss58 in delegate_info else subnet.owner_ss58}", ) ) table = Table( diff --git a/bittensor/commands/root.py b/bittensor/commands/root.py index 5607921b1..0e8bb9ecf 100644 --- a/bittensor/commands/root.py +++ b/bittensor/commands/root.py @@ -148,9 +148,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): root_neurons: typing.List[bittensor.NeuronInfoLite] = subtensor.neurons_lite( netuid=0 ) - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - url=bittensor.__delegates_details_url__ - ) + delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) table = Table(show_footer=False) table.title = "[white]Root Network" @@ -191,7 +189,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): table.add_row( str(neuron_data.uid), ( - delegate_info[neuron_data.hotkey].name + delegate_info[neuron_data.hotkey].display if neuron_data.hotkey in delegate_info else "" ), diff --git a/bittensor/commands/senate.py b/bittensor/commands/senate.py index 03a73cde5..97b5596f2 100644 --- a/bittensor/commands/senate.py +++ b/bittensor/commands/senate.py @@ -70,9 +70,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) senate_members = subtensor.get_senate_members() - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - url=bittensor.__delegates_details_url__ - ) + delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) table = Table(show_footer=False) table.title = "[white]Senate" @@ -93,7 +91,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): for ss58_address in senate_members: table.add_row( ( - delegate_info[ss58_address].name + delegate_info[ss58_address].display if ss58_address in delegate_info else "" ), @@ -143,14 +141,14 @@ def format_call_data(call_data: "bittensor.ProposalCallData") -> str: def display_votes( - vote_data: "bittensor.ProposalVoteData", delegate_info: "bittensor.DelegateInfo" + vote_data: "bittensor.ProposalVoteData", delegate_info: "Dict[str, DelegatesDetails]" ) -> str: vote_list = list() for address in vote_data["ayes"]: vote_list.append( "{}: {}".format( - delegate_info[address].name if address in delegate_info else address, + delegate_info[address].display if address in delegate_info else address, "[bold green]Aye[/bold green]", ) ) @@ -158,7 +156,7 @@ def display_votes( for address in vote_data["nays"]: vote_list.append( "{}: {}".format( - delegate_info[address].name if address in delegate_info else address, + delegate_info[address].display if address in delegate_info else address, "[bold red]Nay[/bold red]", ) ) @@ -211,9 +209,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): senate_members = subtensor.get_senate_members() proposals = subtensor.get_proposals() - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) - ) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) table = Table(show_footer=False) table.title = ( @@ -338,13 +334,11 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): return proposal_vote_data = subtensor.get_vote_data(proposal_hash) - if proposal_vote_data == None: + if proposal_vote_data is None: console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") return - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) - ) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) table = Table(show_footer=False) table.title = "[white]Votes for Proposal {}".format(proposal_hash) diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index 1bc2cf278..b8fd306fe 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -376,14 +376,12 @@ def run(cli: "bittensor.cli"): @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Show all stake accounts.""" - if cli.config.get("all", d=False) == True: + """Show all stake accounts.""" + if cli.config.get("all", d=False) is True: wallets = _get_coldkey_wallets_for_path(cli.config.wallet.path) else: wallets = [bittensor.wallet(config=cli.config)] - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) - ) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) def get_stake_accounts( wallet, subtensor @@ -466,7 +464,7 @@ def get_stakes_from_delegates( for nom in dele.nominators: if nom[0] == wallet.coldkeypub.ss58_address: delegate_name = ( - registered_delegate_info[dele.hotkey_ss58].name + registered_delegate_info[dele.hotkey_ss58].display if dele.hotkey_ss58 in registered_delegate_info else dele.hotkey_ss58 ) From 7058bfbf0625a3e18baa247fc82d81f91dbf3541 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 17 Jul 2024 16:25:37 -0700 Subject: [PATCH 006/260] ruff --- bittensor/commands/delegates.py | 30 +++++++++++++++++++++++------- bittensor/commands/inspect.py | 4 +++- bittensor/commands/network.py | 4 +++- bittensor/commands/root.py | 4 +++- bittensor/commands/senate.py | 15 +++++++++++---- bittensor/commands/stake.py | 4 +++- bittensor/subtensor.py | 5 ++++- tests/unit_tests/test_subtensor.py | 4 ++++ 8 files changed, 54 insertions(+), 16 deletions(-) diff --git a/bittensor/commands/delegates.py b/bittensor/commands/delegates.py index 4f6e63e81..ab1a89b44 100644 --- a/bittensor/commands/delegates.py +++ b/bittensor/commands/delegates.py @@ -46,7 +46,9 @@ def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: def show_delegates_lite( - subtensor: "bittensor.subtensor", delegates_lite: List["bittensor.DelegateInfoLite"], width: Optional[int] = None + subtensor: "bittensor.subtensor", + delegates_lite: List["bittensor.DelegateInfoLite"], + width: Optional[int] = None, ): """ This method is a lite version of the :func:`show_delegates`. This method displays a formatted table of Bittensor network delegates with detailed statistics to the console. @@ -84,7 +86,9 @@ def show_delegates_lite( the table in the console. """ - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( + get_delegates_details(subtensor=subtensor) + ) if registered_delegate_info is None: bittensor.__console__.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" @@ -210,7 +214,9 @@ def show_delegates( for prev_delegate in prev_delegates: prev_delegates_dict[prev_delegate.hotkey_ss58] = prev_delegate - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( + get_delegates_details(subtensor=subtensor) + ) if registered_delegate_info is None: bittensor.__console__.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" @@ -434,7 +440,9 @@ def check_config(config: "bittensor.config"): sys.exit(1) delegates.sort(key=lambda delegate: delegate.total_stake, reverse=True) - show_delegates(subtensor=subtensor, delegates=delegates, prev_delegates=prev_delegates) + show_delegates( + subtensor=subtensor, delegates=delegates, prev_delegates=prev_delegates + ) delegate_index = Prompt.ask("Enter delegate index") config.delegate_ss58key = str(delegates[int(delegate_index)].hotkey_ss58) console.print( @@ -579,7 +587,9 @@ def check_config(config: "bittensor.config"): sys.exit(1) delegates.sort(key=lambda delegate: delegate.total_stake, reverse=True) - show_delegates(subtensor=subtensor, delegates=delegates, prev_delegates=prev_delegates) + show_delegates( + subtensor=subtensor, delegates=delegates, prev_delegates=prev_delegates + ) delegate_index = Prompt.ask("Enter delegate index") config.delegate_ss58key = str(delegates[int(delegate_index)].hotkey_ss58) console.print( @@ -684,7 +694,11 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ":warning: [yellow]Could not fetch delegates history[/yellow]" ) - show_delegates(subtensor=subtensor, delegates=delegates, prev_delegates=prev_delegates, width=cli.config.get("width", None), + show_delegates( + subtensor=subtensor, + delegates=delegates, + prev_delegates=prev_delegates, + width=cli.config.get("width", None), ) @staticmethod @@ -948,7 +962,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): delegates.sort(key=lambda delegate: delegate[0].total_stake, reverse=True) total_delegated += sum(my_delegates.values()) - registered_delegate_info: Optional[DelegatesDetails] = get_delegates_details(subtensor=subtensor) + registered_delegate_info: Optional[DelegatesDetails] = ( + get_delegates_details(subtensor=subtensor) + ) if registered_delegate_info is None: bittensor.__console__.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" diff --git a/bittensor/commands/inspect.py b/bittensor/commands/inspect.py index 884bc3967..ef32a7694 100644 --- a/bittensor/commands/inspect.py +++ b/bittensor/commands/inspect.py @@ -138,7 +138,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) bittensor.logging.debug(f"Netuids to check: {netuids}") - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( + get_delegates_details(subtensor=subtensor) + ) if registered_delegate_info is None: bittensor.__console__.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" diff --git a/bittensor/commands/network.py b/bittensor/commands/network.py index 51cc014dd..56afecb90 100644 --- a/bittensor/commands/network.py +++ b/bittensor/commands/network.py @@ -247,7 +247,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): rows = [] total_neurons = 0 - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) + delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( + subtensor=subtensor + ) for subnet in subnets: total_neurons += subnet.max_n diff --git a/bittensor/commands/root.py b/bittensor/commands/root.py index 0e8bb9ecf..35da7b1eb 100644 --- a/bittensor/commands/root.py +++ b/bittensor/commands/root.py @@ -148,7 +148,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): root_neurons: typing.List[bittensor.NeuronInfoLite] = subtensor.neurons_lite( netuid=0 ) - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) + delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( + subtensor=subtensor + ) table = Table(show_footer=False) table.title = "[white]Root Network" diff --git a/bittensor/commands/senate.py b/bittensor/commands/senate.py index 97b5596f2..4b9183652 100644 --- a/bittensor/commands/senate.py +++ b/bittensor/commands/senate.py @@ -70,7 +70,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) senate_members = subtensor.get_senate_members() - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) + delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( + subtensor=subtensor + ) table = Table(show_footer=False) table.title = "[white]Senate" @@ -141,7 +143,8 @@ def format_call_data(call_data: "bittensor.ProposalCallData") -> str: def display_votes( - vote_data: "bittensor.ProposalVoteData", delegate_info: "Dict[str, DelegatesDetails]" + vote_data: "bittensor.ProposalVoteData", + delegate_info: "Dict[str, DelegatesDetails]", ) -> str: vote_list = list() @@ -209,7 +212,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): senate_members = subtensor.get_senate_members() proposals = subtensor.get_proposals() - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( + get_delegates_details(subtensor=subtensor) + ) table = Table(show_footer=False) table.title = ( @@ -338,7 +343,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") return - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( + get_delegates_details(subtensor=subtensor) + ) table = Table(show_footer=False) table.title = "[white]Votes for Proposal {}".format(proposal_hash) diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index b8fd306fe..a560f63cb 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -381,7 +381,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): wallets = _get_coldkey_wallets_for_path(cli.config.wallet.path) else: wallets = [bittensor.wallet(config=cli.config)] - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details(subtensor=subtensor) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( + get_delegates_details(subtensor=subtensor) + ) def get_stake_accounts( wallet, subtensor diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index 4627cdd65..cdcf46a72 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -2878,7 +2878,9 @@ def make_substrate_call_with_retry() -> bool: return make_substrate_call_with_retry() - def get_delegate_identities(self, block: Optional[int] = None) -> Dict[str, DelegatesDetails]: + def get_delegate_identities( + self, block: Optional[int] = None + ) -> Dict[str, DelegatesDetails]: """ Retrieves the identities of all delegates from the blockchain. @@ -2890,6 +2892,7 @@ def get_delegate_identities(self, block: Optional[int] = None) -> Dict[str, Dele Returns: Dict[str, DelegatesDetails]: A dictionary where the keys are delegate SS58 addresses and the values are DelegatesDetails objects containing the details of each delegate. """ + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) def make_substrate_call_with_retry() -> "QueryMapResult": return self.substrate.query_map( diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 731285c22..caabd051b 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -2315,3 +2315,7 @@ def test_get_remaining_arbitration_period_happy(subtensor, mocker): ) # if we change the methods logic in the future we have to be make sure the returned type is correct assert result == 1800 # 2000 - 200 + + +def test_get_delegate_identities(subtensor, mocker): + pass From 0f9b6f30a83c55045b728e5909dd29aec9f6c265 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 17 Jul 2024 16:34:46 -0700 Subject: [PATCH 007/260] Add test for `bittensor.utils.wallet_utils.decode_hex_identity_dict` --- bittensor/utils/wallet_utils.py | 6 +- tests/unit_tests/utils/test_wallet_utils.py | 66 +++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/utils/test_wallet_utils.py diff --git a/bittensor/utils/wallet_utils.py b/bittensor/utils/wallet_utils.py index 60c07e105..e7fda95ad 100644 --- a/bittensor/utils/wallet_utils.py +++ b/bittensor/utils/wallet_utils.py @@ -17,7 +17,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from typing import Union, Optional +from typing import Any, Dict, Union, Optional from substrateinterface import Keypair as Keypair from substrateinterface.utils import ss58 @@ -155,7 +155,7 @@ def create_identity_dict( } -def decode_hex_identity_dict(info_dictionary): +def decode_hex_identity_dict(info_dictionary) -> Dict[str, Any]: """ Decodes hex-encoded strings in a dictionary. @@ -178,7 +178,7 @@ def decode_hex_identity_dict(info_dictionary): {'name': 'john', 'additional': [('data', 'data')]} """ - def get_decoded(data: str): + def get_decoded(data: str) -> str: """Decodes a hex-encoded string.""" try: return bytes.fromhex(data[2:]).decode() diff --git a/tests/unit_tests/utils/test_wallet_utils.py b/tests/unit_tests/utils/test_wallet_utils.py new file mode 100644 index 000000000..b807f9aab --- /dev/null +++ b/tests/unit_tests/utils/test_wallet_utils.py @@ -0,0 +1,66 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao +# Copyright © 2022 Opentensor Foundation +# Copyright © 2023 Opentensor Technologies Inc +# +# 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. + +from bittensor.utils.wallet_utils import decode_hex_identity_dict + + +def test_decode_hex_identity_dict(): + """Tests the decode_hex_identity_dict function to ensure it correctly decodes a hex-encoded identity dictionary.""" + # Prep + fake_info_dictionary = { + "additional": [ + ( + {"Raw11": "0x6465736372697074696f6e"}, + { + "Raw64": "0x46616b65206465736372697074696f6e" + }, + ) + ], + "display": {"Raw17": "0x46616b654e616d65"}, + "legal": "None", + "web": {"Raw22": "0x687474703a2f2f7777772e626c61626c612d746573742e636f6d"}, + "riot": "None", + "email": "None", + "pgp_fingerprint": None, + "image": "None", + "twitter": "None", + } + + expected_result = { + "additional": [ + ("description", "Fake description") + ], + "display": "FakeName", + "legal": "None", + "web": "http://www.blabla-test.com", + "riot": "None", + "email": "None", + "pgp_fingerprint": None, + "image": "None", + "twitter": "None", + } + + # Call + + result = decode_hex_identity_dict(fake_info_dictionary) + + # Assertions + assert result == expected_result + + From 5eef3ad634ea877d787a51c0d428a0c1920f6bc0 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 17 Jul 2024 16:59:33 -0700 Subject: [PATCH 008/260] Add test for `bittensor.commands.utils.get_delegates_details` --- tests/unit_tests/commands/test_utils.py | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/unit_tests/commands/test_utils.py diff --git a/tests/unit_tests/commands/test_utils.py b/tests/unit_tests/commands/test_utils.py new file mode 100644 index 000000000..41bb53a3b --- /dev/null +++ b/tests/unit_tests/commands/test_utils.py @@ -0,0 +1,58 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao +# Copyright © 2022 Opentensor Foundation +# Copyright © 2023 Opentensor Technologies Inc +# +# 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 pytest +from bittensor.commands.utils import get_delegates_details +import bittensor + + +def test_get_delegates_details_success(mocker): + """Tests the get_delegates_details function to ensure it successfully retrieves delegate details.""" + # Prep + mocked_sub = mocker.MagicMock() + mocker.patch("bittensor.subtensor", return_value=mocked_sub) + + # Call + + fake_subtensor = bittensor.subtensor() + result = get_delegates_details(fake_subtensor) + + # Assertions + mocked_sub.get_delegate_identities.assert_called_once() + assert result == mocked_sub.get_delegate_identities.return_value + + +def test_get_delegates_details_error(mocker): + """Tests the get_delegates_details function to ensure it handles errors correctly.""" + # Prep + test_error_message = "Test exception" + mocked_sub = mocker.MagicMock() + mocked_sub.get_delegate_identities.side_effect = Exception(test_error_message) + + mocker.patch("bittensor.subtensor", return_value=mocked_sub) + mocker.patch("bittensor.logging.exception") + + # Call + fake_subtensor = bittensor.subtensor() + result = get_delegates_details(fake_subtensor) + + # Assertions + mocked_sub.get_delegate_identities.assert_called_once() + bittensor.logging.exception.assert_called_once_with(f"Unable to get Delegates Identities. Error: {test_error_message}") + assert result is None From 94532b855d25e77ec460ef71c53981d89cb31ddf Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 17 Jul 2024 17:31:11 -0700 Subject: [PATCH 009/260] Add test for `bittensor.subtensor.Subtensor.get_delegate_identities` --- tests/unit_tests/commands/test_utils.py | 4 +- tests/unit_tests/test_subtensor.py | 67 +++++++++++++++++++-- tests/unit_tests/utils/test_wallet_utils.py | 10 +-- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/tests/unit_tests/commands/test_utils.py b/tests/unit_tests/commands/test_utils.py index 41bb53a3b..7edd1c663 100644 --- a/tests/unit_tests/commands/test_utils.py +++ b/tests/unit_tests/commands/test_utils.py @@ -54,5 +54,7 @@ def test_get_delegates_details_error(mocker): # Assertions mocked_sub.get_delegate_identities.assert_called_once() - bittensor.logging.exception.assert_called_once_with(f"Unable to get Delegates Identities. Error: {test_error_message}") + bittensor.logging.exception.assert_called_once_with( + f"Unable to get Delegates Identities. Error: {test_error_message}" + ) assert result is None diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index caabd051b..69a139122 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -17,7 +17,9 @@ # Standard Lib import argparse +import dataclasses import unittest.mock as mock +from typing import Any from unittest.mock import MagicMock # 3rd Party @@ -25,14 +27,15 @@ # Application import bittensor +from bittensor import subtensor_module +from bittensor.chain_data import SubnetHyperparameters +from bittensor.commands import utils +from bittensor.commands.utils import normalize_hyperparameters from bittensor.subtensor import ( Subtensor, _logger, - Balance, ) -from bittensor.chain_data import SubnetHyperparameters -from bittensor.commands.utils import normalize_hyperparameters -from bittensor import subtensor_module +from bittensor.utils import wallet_utils from bittensor.utils.balance import Balance U16_MAX = 65535 @@ -2318,4 +2321,58 @@ def test_get_remaining_arbitration_period_happy(subtensor, mocker): def test_get_delegate_identities(subtensor, mocker): - pass + """Tests the get_delegate_identities method to ensure it correctly retrieves and decodes delegate identities.""" + # Preps + + subtensor.substrate = mocker.MagicMock() + fake_info_dictionary = { + "additional": [ + ( + {"Raw11": "0x6465736372697074696f6e"}, + {"Raw64": "0x46616b65206465736372697074696f6e"}, + ) + ], + "display": {"Raw17": "0x46616b654e616d65"}, + "legal": "None", + "web": {"Raw22": "0x687474703a2f2f7777772e626c61626c612d746573742e636f6d"}, + "riot": "None", + "email": "None", + "pgp_fingerprint": None, + "image": "None", + "twitter": "None", + } + + expected_return = { + "additional": [("description", "Fake description")], + "display": "FakeName", + "legal": "None", + "web": "http://www.blabla-test.com", + "riot": "None", + "email": "None", + "pgp_fingerprint": None, + "image": "None", + "twitter": "None", + } + + @dataclasses.dataclass + class FakeQueryReturn: + value: Any + + fake_value = {"info": fake_info_dictionary} + mocked_query_map = [ + [FakeQueryReturn("fake_address"), FakeQueryReturn(fake_value)], + ] + + mocker.patch.object(subtensor.substrate, "query_map", return_value=mocked_query_map) + spy_from_chain_data = mocker.spy(utils.DelegatesDetails, "from_chain_data") + spy_decode_hex_identity_dict = mocker.spy(wallet_utils, "decode_hex_identity_dict") + + # Calls + result = subtensor.get_delegate_identities() + + # Assertions + spy_decode_hex_identity_dict.assert_called_once_with(fake_info_dictionary) + spy_from_chain_data.assert_called_once_with(expected_return) + assert result == { + "fake_address": utils.DelegatesDetails.from_chain_data(expected_return) + } diff --git a/tests/unit_tests/utils/test_wallet_utils.py b/tests/unit_tests/utils/test_wallet_utils.py index b807f9aab..fccb15dbd 100644 --- a/tests/unit_tests/utils/test_wallet_utils.py +++ b/tests/unit_tests/utils/test_wallet_utils.py @@ -27,9 +27,7 @@ def test_decode_hex_identity_dict(): "additional": [ ( {"Raw11": "0x6465736372697074696f6e"}, - { - "Raw64": "0x46616b65206465736372697074696f6e" - }, + {"Raw64": "0x46616b65206465736372697074696f6e"}, ) ], "display": {"Raw17": "0x46616b654e616d65"}, @@ -43,9 +41,7 @@ def test_decode_hex_identity_dict(): } expected_result = { - "additional": [ - ("description", "Fake description") - ], + "additional": [("description", "Fake description")], "display": "FakeName", "legal": "None", "web": "http://www.blabla-test.com", @@ -62,5 +58,3 @@ def test_decode_hex_identity_dict(): # Assertions assert result == expected_result - - From 2efdd06c154b0e9a01079fa3d81a2c99e9c85f8c Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 13:29:10 -0700 Subject: [PATCH 010/260] Part1: Removing content related to the wallet. Start use the pip installable package. --- bittensor/__init__.py | 23 +- bittensor/btlogging/loggingmachine.py | 24 +- bittensor/config.py | 415 --------- bittensor/errors.py | 107 +-- bittensor/keyfile.py | 862 ----------------- bittensor/mock/subtensor_mock.py | 50 +- bittensor/wallet.py | 873 ------------------ requirements/prod.txt | 3 + tests/integration_tests/test_cli.py | 144 ++- .../unit_tests/extrinsics/test_delegation.py | 37 +- tests/unit_tests/extrinsics/test_network.py | 25 +- .../unit_tests/extrinsics/test_prometheus.py | 25 +- .../extrinsics/test_registration.py | 27 +- tests/unit_tests/extrinsics/test_serving.py | 38 +- tests/unit_tests/test_keyfile.py | 626 ------------- tests/unit_tests/test_wallet.py | 517 ----------- 16 files changed, 266 insertions(+), 3530 deletions(-) delete mode 100644 bittensor/config.py delete mode 100644 bittensor/keyfile.py delete mode 100644 bittensor/wallet.py delete mode 100644 tests/unit_tests/test_keyfile.py delete mode 100644 tests/unit_tests/test_wallet.py diff --git a/bittensor/__init__.py b/bittensor/__init__.py index f153ccc06..f3fb844eb 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -1,21 +1,20 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022-2023 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 os import warnings @@ -277,7 +276,6 @@ def debug(on: bool = True): IdentityError, InternalServerError, InvalidRequestNameError, - KeyFileError, MetadataError, NominationError, NotDelegateError, @@ -295,9 +293,11 @@ def debug(on: bool = True): UnstakeError, ) +from bittensor_wallet.errors import KeyFileError + from substrateinterface import Keypair # noqa: F401 -from .config import InvalidConfigFile, DefaultConfig, config, T -from .keyfile import ( +from bittensor_wallet.config import InvalidConfigFile, DefaultConfig, Config as config, T +from bittensor_wallet.keyfile import ( serialized_keypair_to_keyfile_data, deserialize_keypair_from_keyfile_data, validate_password, @@ -311,10 +311,9 @@ def debug(on: bool = True): encrypt_keyfile_data, get_coldkey_password_from_environment, decrypt_keyfile_data, - keyfile, - Mockkeyfile, + Keyfile as keyfile ) -from .wallet import display_mnemonic_msg, wallet +from bittensor_wallet.wallet import display_mnemonic_msg, Wallet as wallet from .utils import ( ss58_to_vec_u8, diff --git a/bittensor/btlogging/loggingmachine.py b/bittensor/btlogging/loggingmachine.py index b36019a6a..b28357158 100644 --- a/bittensor/btlogging/loggingmachine.py +++ b/bittensor/btlogging/loggingmachine.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 OpenTensor Foundation - +# 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 @@ -31,9 +31,9 @@ from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler from typing import NamedTuple +from bittensor_wallet.config import Config from statemachine import State, StateMachine -import bittensor.config from bittensor.btlogging.defines import ( BITTENSOR_LOGGER_NAME, DATE_FORMAT, @@ -42,8 +42,8 @@ DEFAULT_MAX_ROTATING_LOG_FILE_SIZE, TRACE_LOG_FORMAT, ) -from bittensor.btlogging.format import BtFileFormatter, BtStreamFormatter -from bittensor.btlogging.helpers import all_loggers +from .format import BtFileFormatter, BtStreamFormatter +from .helpers import all_loggers class LoggingConfig(NamedTuple): @@ -89,7 +89,7 @@ class LoggingMachine(StateMachine): | Disabled.to(Disabled) ) - def __init__(self, config: bittensor.config, name: str = BITTENSOR_LOGGER_NAME): + def __init__(self, config: "Config", name: str = BITTENSOR_LOGGER_NAME): # basics super(LoggingMachine, self).__init__() self._queue = mp.Queue(-1) @@ -429,7 +429,7 @@ def get_level(self) -> int: """Returns Logging level.""" return self._logger.level - def check_config(self, config: bittensor.config): + def check_config(self, config: "Config"): assert config.logging def help(self): @@ -475,7 +475,7 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): pass @classmethod - def config(cls) -> bittensor.config: + def config(cls) -> "Config": """Get config from the argument parser. Return: @@ -483,11 +483,11 @@ def config(cls) -> bittensor.config: """ parser = argparse.ArgumentParser() cls.add_args(parser) - return bittensor.config(parser, args=[]) + return Config(parser, args=[]) def __call__( self, - config: bittensor.config = None, + config: "Config" = None, debug: bool = None, trace: bool = None, record_log: bool = None, diff --git a/bittensor/config.py b/bittensor/config.py deleted file mode 100644 index 59ad4451b..000000000 --- a/bittensor/config.py +++ /dev/null @@ -1,415 +0,0 @@ -""" -Implementation of the config class, which manages the configuration of different Bittensor modules. -""" - -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 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 os -import sys -import yaml -import copy -from copy import deepcopy -from munch import DefaultMunch -from typing import List, Optional, Dict, Any, TypeVar, Type -import argparse - - -class InvalidConfigFile(Exception): - """In place of YAMLError""" - - pass - - -class config(DefaultMunch): - """ - Implementation of the config class, which manages the configuration of different Bittensor modules. - """ - - __is_set: Dict[str, bool] - - r""" Translates the passed parser into a nested Bittensor config. - - Args: - parser (argparse.ArgumentParser): - Command line parser object. - strict (bool): - If ``true``, the command line arguments are strictly parsed. - args (list of str): - Command line arguments. - default (Optional[Any]): - Default value for the Config. Defaults to ``None``. - This default will be returned for attributes that are undefined. - Returns: - config (bittensor.config): - Nested config object created from parser arguments. - """ - - def __init__( - self, - parser: argparse.ArgumentParser = None, - args: Optional[List[str]] = None, - strict: bool = False, - default: Optional[Any] = None, - ) -> None: - super().__init__(default) - - self["__is_set"] = {} - - if parser == None: - return None - - # Optionally add config specific arguments - try: - parser.add_argument( - "--config", - type=str, - help="If set, defaults are overridden by passed file.", - ) - except: - # this can fail if --config has already been added. - pass - - try: - parser.add_argument( - "--strict", - action="store_true", - help="""If flagged, config will check that only exact arguments have been set.""", - default=False, - ) - except: - # this can fail if --strict has already been added. - pass - - try: - parser.add_argument( - "--no_version_checking", - action="store_true", - help="Set ``true`` to stop cli version checking.", - default=False, - ) - except: - # this can fail if --no_version_checking has already been added. - pass - - try: - parser.add_argument( - "--no_prompt", - dest="no_prompt", - action="store_true", - help="Set ``true`` to stop cli from prompting the user.", - default=False, - ) - except: - # this can fail if --no_version_checking has already been added. - pass - - # Get args from argv if not passed in. - if args == None: - args = sys.argv[1:] - - # Check for missing required arguments before proceeding - missing_required_args = self.__check_for_missing_required_args(parser, args) - if missing_required_args: - # Handle missing required arguments gracefully - raise ValueError( - f"Missing required arguments: {', '.join(missing_required_args)}" - ) - - # 1.1 Optionally load defaults if the --config is set. - try: - config_file_path = ( - str(os.getcwd()) - + "/" - + vars(parser.parse_known_args(args)[0])["config"] - ) - except Exception as e: - config_file_path = None - - # Parse args not strict - config_params = config.__parse_args__(args=args, parser=parser, strict=False) - - # 2. Optionally check for --strict - ## strict=True when passed in OR when --strict is set - strict = config_params.strict or strict - - if config_file_path != None: - config_file_path = os.path.expanduser(config_file_path) - try: - with open(config_file_path) as f: - params_config = yaml.safe_load(f) - print("Loading config defaults from: {}".format(config_file_path)) - parser.set_defaults(**params_config) - except Exception as e: - print("Error in loading: {} using default parser settings".format(e)) - - # 2. Continue with loading in params. - params = config.__parse_args__(args=args, parser=parser, strict=strict) - - _config = self - - # Splits params and add to config - config.__split_params__(params=params, _config=_config) - - # Make the is_set map - _config["__is_set"] = {} - - ## Reparse args using default of unset - parser_no_defaults = copy.deepcopy(parser) - - # Only command as the arg, else no args - default_param_args = ( - [_config.get("command")] - if _config.get("command") != None and _config.get("subcommand") == None - else [] - ) - if _config.get("command") != None and _config.get("subcommand") != None: - default_param_args = [_config.get("command"), _config.get("subcommand")] - - ## Get all args by name - default_params = parser.parse_args(args=default_param_args) - - all_default_args = default_params.__dict__.keys() | [] - ## Make a dict with keys as args and values as argparse.SUPPRESS - defaults_as_suppress = {key: argparse.SUPPRESS for key in all_default_args} - ## Set the defaults to argparse.SUPPRESS, should remove them from the namespace - parser_no_defaults.set_defaults(**defaults_as_suppress) - parser_no_defaults._defaults.clear() # Needed for quirk of argparse - - ### Check for subparsers and do the same - if parser_no_defaults._subparsers != None: - for action in parser_no_defaults._subparsers._actions: - # Should only be the "command" subparser action - if isinstance(action, argparse._SubParsersAction): - # Set the defaults to argparse.SUPPRESS, should remove them from the namespace - # Each choice is the keyword for a command, we need to set the defaults for each of these - ## Note: we also need to clear the _defaults dict for each, this is a quirk of argparse - cmd_parser: argparse.ArgumentParser - for cmd_parser in action.choices.values(): - # If this choice is also a subparser, set defaults recursively - if cmd_parser._subparsers: - for action in cmd_parser._subparsers._actions: - # Should only be the "command" subparser action - if isinstance(action, argparse._SubParsersAction): - cmd_parser: argparse.ArgumentParser - for cmd_parser in action.choices.values(): - cmd_parser.set_defaults(**defaults_as_suppress) - cmd_parser._defaults.clear() # Needed for quirk of argparse - else: - cmd_parser.set_defaults(**defaults_as_suppress) - cmd_parser._defaults.clear() # Needed for quirk of argparse - - ## Reparse the args, but this time with the defaults as argparse.SUPPRESS - params_no_defaults = config.__parse_args__( - args=args, parser=parser_no_defaults, strict=strict - ) - - ## Diff the params and params_no_defaults to get the is_set map - _config["__is_set"] = { - arg_key: True - for arg_key in [ - k - for k, _ in filter( - lambda kv: kv[1] != argparse.SUPPRESS, - params_no_defaults.__dict__.items(), - ) - ] - } - - @staticmethod - def __split_params__(params: argparse.Namespace, _config: "config"): - # Splits params on dot syntax i.e neuron.axon_port and adds to _config - for arg_key, arg_val in params.__dict__.items(): - split_keys = arg_key.split(".") - head = _config - keys = split_keys - while len(keys) > 1: - if ( - hasattr(head, keys[0]) and head[keys[0]] != None - ): # Needs to be Config - head = getattr(head, keys[0]) - keys = keys[1:] - else: - head[keys[0]] = config() - head = head[keys[0]] - keys = keys[1:] - if len(keys) == 1: - head[keys[0]] = arg_val - - @staticmethod - def __parse_args__( - args: List[str], parser: argparse.ArgumentParser = None, strict: bool = False - ) -> argparse.Namespace: - """Parses the passed args use the passed parser. - - Args: - args (List[str]): - List of arguments to parse. - parser (argparse.ArgumentParser): - Command line parser object. - strict (bool): - If ``true``, the command line arguments are strictly parsed. - Returns: - Namespace: - Namespace object created from parser arguments. - """ - if not strict: - params, unrecognized = parser.parse_known_args(args=args) - params_list = list(params.__dict__) - # bug within argparse itself, does not correctly set value for boolean flags - for unrec in unrecognized: - if unrec.startswith("--") and unrec[2:] in params_list: - # Set the missing boolean value to true - setattr(params, unrec[2:], True) - else: - params = parser.parse_args(args=args) - - return params - - def __deepcopy__(self, memo) -> "config": - _default = self.__default__ - - config_state = self.__getstate__() - config_copy = config() - memo[id(self)] = config_copy - - config_copy.__setstate__(config_state) - config_copy.__default__ = _default - - config_copy["__is_set"] = deepcopy(self["__is_set"], memo) - - return config_copy - - def __repr__(self) -> str: - return self.__str__() - - @staticmethod - def _remove_private_keys(d): - if "__parser" in d: - d.pop("__parser", None) - if "__is_set" in d: - d.pop("__is_set", None) - for k, v in list(d.items()): - if isinstance(v, dict): - config._remove_private_keys(v) - return d - - def __str__(self) -> str: - # remove the parser and is_set map from the visible config - visible = copy.deepcopy(self.toDict()) - visible.pop("__parser", None) - visible.pop("__is_set", None) - cleaned = config._remove_private_keys(visible) - return "\n" + yaml.dump(cleaned, sort_keys=False) - - def copy(self) -> "config": - return copy.deepcopy(self) - - def to_string(self, items) -> str: - """Get string from items""" - return "\n" + yaml.dump(items.toDict()) - - def update_with_kwargs(self, kwargs): - """Add config to self""" - for key, val in kwargs.items(): - self[key] = val - - @classmethod - def _merge(cls, a, b): - """Merge two configurations recursively. - If there is a conflict, the value from the second configuration will take precedence. - """ - for key in b: - if key in a: - if isinstance(a[key], dict) and isinstance(b[key], dict): - a[key] = cls._merge(a[key], b[key]) - else: - a[key] = b[key] - else: - a[key] = b[key] - return a - - def merge(self, b): - """ - Merges the current config with another config. - - Args: - b: Another config to merge. - """ - self = self._merge(self, b) - - @classmethod - def merge_all(cls, configs: List["config"]) -> "config": - """ - Merge all configs in the list into one config. - If there is a conflict, the value from the last configuration in the list will take precedence. - - Args: - configs (list of config): - List of configs to be merged. - - Returns: - config: - Merged config object. - """ - result = cls() - for cfg in configs: - result.merge(cfg) - return result - - def is_set(self, param_name: str) -> bool: - """ - Returns a boolean indicating whether the parameter has been set or is still the default. - """ - if param_name not in self.get("__is_set"): - return False - else: - return self.get("__is_set")[param_name] - - def __check_for_missing_required_args( - self, parser: argparse.ArgumentParser, args: List[str] - ) -> List[str]: - required_args = self.__get_required_args_from_parser(parser) - missing_args = [arg for arg in required_args if not any(arg in s for s in args)] - return missing_args - - @staticmethod - def __get_required_args_from_parser(parser: argparse.ArgumentParser) -> List[str]: - required_args = [] - for action in parser._actions: - if action.required: - # Prefix the argument with '--' if it's a long argument, or '-' if it's short - prefix = "--" if len(action.dest) > 1 else "-" - required_args.append(prefix + action.dest) - return required_args - - -T = TypeVar("T", bound="DefaultConfig") - - -class DefaultConfig(config): - """ - A Config with a set of default values. - """ - - @classmethod - def default(cls: Type[T]) -> T: - """ - Get default config. - """ - raise NotImplementedError("Function default is not implemented.") diff --git a/bittensor/errors.py b/bittensor/errors.py index b8366ee68..b80883459 100644 --- a/bittensor/errors.py +++ b/bittensor/errors.py @@ -1,19 +1,20 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Foundation - +# 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. + from __future__ import annotations import typing @@ -23,99 +24,63 @@ class ChainError(BaseException): - r"""Base error for any chain related errors.""" - - pass + """Base error for any chain related errors.""" class ChainConnectionError(ChainError): - r"""Error for any chain connection related errors.""" - - pass + """Error for any chain connection related errors.""" class ChainTransactionError(ChainError): - r"""Error for any chain transaction related errors.""" - - pass + """Error for any chain transaction related errors.""" class ChainQueryError(ChainError): - r"""Error for any chain query related errors.""" - - pass + """Error for any chain query related errors.""" class StakeError(ChainTransactionError): - r"""Error raised when a stake transaction fails.""" - - pass + """Error raised when a stake transaction fails.""" class UnstakeError(ChainTransactionError): - r"""Error raised when an unstake transaction fails.""" - - pass + """Error raised when an unstake transaction fails.""" class IdentityError(ChainTransactionError): - r"""Error raised when an identity transaction fails.""" - - pass + """Error raised when an identity transaction fails.""" class NominationError(ChainTransactionError): - r"""Error raised when a nomination transaction fails.""" - - pass + """Error raised when a nomination transaction fails.""" class TakeError(ChainTransactionError): - r"""Error raised when a increase / decrease take transaction fails.""" - - pass + """Error raised when a increase / decrease take transaction fails.""" class TransferError(ChainTransactionError): - r"""Error raised when a transfer transaction fails.""" - - pass + """Error raised when a transfer transaction fails.""" class RegistrationError(ChainTransactionError): - r"""Error raised when a neuron registration transaction fails.""" - - pass + """Error raised when a neuron registration transaction fails.""" class NotRegisteredError(ChainTransactionError): - r"""Error raised when a neuron is not registered, and the transaction requires it to be.""" - - pass + """Error raised when a neuron is not registered, and the transaction requires it to be.""" class NotDelegateError(StakeError): - r"""Error raised when a hotkey you are trying to stake to is not a delegate.""" - - pass - - -class KeyFileError(Exception): - """Error thrown when the keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid.""" - - pass + """Error raised when a hotkey you are trying to stake to is not a delegate.""" class MetadataError(ChainTransactionError): - r"""Error raised when metadata commitment transaction fails.""" - - pass + """Error raised when metadata commitment transaction fails.""" class InvalidRequestNameError(Exception): - r"""This exception is raised when the request name is invalid. Ususally indicates a broken URL.""" - - pass + """This exception is raised when the request name is invalid. Usually indicates a broken URL.""" class SynapseException(Exception): @@ -128,51 +93,35 @@ def __init__( class UnknownSynapseError(SynapseException): - r"""This exception is raised when the request name is not found in the Axon's forward_fns dictionary.""" - - pass + """This exception is raised when the request name is not found in the Axon's forward_fns dictionary.""" class SynapseParsingError(Exception): - r"""This exception is raised when the request headers are unable to be parsed into the synapse type.""" - - pass + """This exception is raised when the request headers are unable to be parsed into the synapse type.""" class NotVerifiedException(SynapseException): - r"""This exception is raised when the request is not verified.""" - - pass + """This exception is raised when the request is not verified.""" class BlacklistedException(SynapseException): - r"""This exception is raised when the request is blacklisted.""" - - pass + """This exception is raised when the request is blacklisted.""" class PriorityException(SynapseException): - r"""This exception is raised when the request priority is not met.""" - - pass + """This exception is raised when the request priority is not met.""" class PostProcessException(SynapseException): - r"""This exception is raised when the response headers cannot be updated.""" - - pass + """This exception is raised when the response headers cannot be updated.""" class RunException(SynapseException): - r"""This exception is raised when the requested function cannot be executed. Indicates a server error.""" - - pass + """This exception is raised when the requested function cannot be executed. Indicates a server error.""" class InternalServerError(SynapseException): - r"""This exception is raised when the requested function fails on the server. Indicates a server error.""" - - pass + """This exception is raised when the requested function fails on the server. Indicates a server error.""" class SynapseDendriteNoneException(SynapseException): diff --git a/bittensor/keyfile.py b/bittensor/keyfile.py deleted file mode 100644 index f1b2ad622..000000000 --- a/bittensor/keyfile.py +++ /dev/null @@ -1,862 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 os -import base64 -import json -import stat -import getpass -import bittensor -from bittensor.errors import KeyFileError -from typing import Optional -from pathlib import Path - -from ansible_vault import Vault -from ansible.parsing.vault import AnsibleVaultError -from cryptography.exceptions import InvalidSignature, InvalidKey -from cryptography.fernet import Fernet, InvalidToken -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from nacl import pwhash, secret -from password_strength import PasswordPolicy -from substrateinterface.utils.ss58 import ss58_encode -from termcolor import colored -from rich.prompt import Confirm - - -NACL_SALT = b"\x13q\x83\xdf\xf1Z\t\xbc\x9c\x90\xb5Q\x879\xe9\xb1" - - -def serialized_keypair_to_keyfile_data(keypair: "bittensor.Keypair") -> bytes: - """Serializes keypair object into keyfile data. - - Args: - keypair (bittensor.Keypair): The keypair object to be serialized. - Returns: - data (bytes): Serialized keypair data. - """ - json_data = { - "accountId": "0x" + keypair.public_key.hex() if keypair.public_key else None, - "publicKey": "0x" + keypair.public_key.hex() if keypair.public_key else None, - "privateKey": "0x" + keypair.private_key.hex() if keypair.private_key else None, - "secretPhrase": keypair.mnemonic if keypair.mnemonic else None, - "secretSeed": ( - "0x" - + ( - keypair.seed_hex - if isinstance(keypair.seed_hex, str) - else keypair.seed_hex.hex() - ) - if keypair.seed_hex - else None - ), - "ss58Address": keypair.ss58_address if keypair.ss58_address else None, - } - data = json.dumps(json_data).encode() - return data - - -def deserialize_keypair_from_keyfile_data(keyfile_data: bytes) -> "bittensor.Keypair": - """Deserializes Keypair object from passed keyfile data. - - Args: - keyfile_data (bytes): The keyfile data as bytes to be loaded. - Returns: - keypair (bittensor.Keypair): The Keypair loaded from bytes. - Raises: - KeyFileError: Raised if the passed bytes cannot construct a keypair object. - """ - keyfile_data = keyfile_data.decode() - try: - keyfile_dict = dict(json.loads(keyfile_data)) - except: - string_value = str(keyfile_data) - if string_value[:2] == "0x": - string_value = ss58_encode(string_value) - keyfile_dict = { - "accountId": None, - "publicKey": None, - "privateKey": None, - "secretPhrase": None, - "secretSeed": None, - "ss58Address": string_value, - } - else: - raise bittensor.KeyFileError( - "Keypair could not be created from keyfile data: {}".format( - string_value - ) - ) - - if "secretSeed" in keyfile_dict and keyfile_dict["secretSeed"] is not None: - return bittensor.Keypair.create_from_seed(keyfile_dict["secretSeed"]) - - elif "secretPhrase" in keyfile_dict and keyfile_dict["secretPhrase"] is not None: - return bittensor.Keypair.create_from_mnemonic( - mnemonic=keyfile_dict["secretPhrase"] - ) - - elif keyfile_dict.get("privateKey", None) is not None: - # May have the above dict keys also, but we want to preserve the first two - return bittensor.Keypair.create_from_private_key( - keyfile_dict["privateKey"], ss58_format=bittensor.__ss58_format__ - ) - - if "ss58Address" in keyfile_dict and keyfile_dict["ss58Address"] is not None: - return bittensor.Keypair(ss58_address=keyfile_dict["ss58Address"]) - - else: - raise bittensor.KeyFileError( - "Keypair could not be created from keyfile data: {}".format(keyfile_dict) - ) - - -def validate_password(password: str) -> bool: - """Validates the password against a password policy. - - Args: - password (str): The password to verify. - Returns: - valid (bool): ``True`` if the password meets validity requirements. - """ - policy = PasswordPolicy.from_names(strength=0.20, entropybits=10, length=6) - if not password: - return False - tested_pass = policy.password(password) - result = tested_pass.test() - if len(result) > 0: - print( - colored( - "Password not strong enough. Try increasing the length of the password or the password complexity" - ) - ) - return False - password_verification = getpass.getpass("Retype your password: ") - if password != password_verification: - print("Passwords do not match") - return False - return True - - -def ask_password_to_encrypt() -> str: - """Prompts the user to enter a password for key encryption. - - Returns: - password (str): The valid password entered by the user. - """ - valid = False - while not valid: - password = getpass.getpass("Specify password for key encryption: ") - valid = validate_password(password) - return password - - -def keyfile_data_is_encrypted_nacl(keyfile_data: bytes) -> bool: - """Returns true if the keyfile data is NaCl encrypted. - - Args: - keyfile_data ( bytes, required ): - Bytes to validate. - Returns: - is_nacl (bool): - ``True`` if data is ansible encrypted. - """ - return keyfile_data[: len("$NACL")] == b"$NACL" - - -def keyfile_data_is_encrypted_ansible(keyfile_data: bytes) -> bool: - """Returns true if the keyfile data is ansible encrypted. - - Args: - keyfile_data (bytes): The bytes to validate. - Returns: - is_ansible (bool): True if the data is ansible encrypted. - """ - return keyfile_data[:14] == b"$ANSIBLE_VAULT" - - -def keyfile_data_is_encrypted_legacy(keyfile_data: bytes) -> bool: - """Returns true if the keyfile data is legacy encrypted. - Args: - keyfile_data (bytes): The bytes to validate. - Returns: - is_legacy (bool): ``True`` if the data is legacy encrypted. - """ - return keyfile_data[:6] == b"gAAAAA" - - -def keyfile_data_is_encrypted(keyfile_data: bytes) -> bool: - """Returns ``true`` if the keyfile data is encrypted. - - Args: - keyfile_data (bytes): The bytes to validate. - Returns: - is_encrypted (bool): ``True`` if the data is encrypted. - """ - return ( - keyfile_data_is_encrypted_nacl(keyfile_data) - or keyfile_data_is_encrypted_ansible(keyfile_data) - or keyfile_data_is_encrypted_legacy(keyfile_data) - ) - - -def keyfile_data_encryption_method(keyfile_data: bytes) -> bool: - """Returns ``true`` if the keyfile data is encrypted. - - Args: - keyfile_data ( bytes, required ): - Bytes to validate - Returns: - encryption_method (bool): - ``True`` if data is encrypted. - """ - - if keyfile_data_is_encrypted_nacl(keyfile_data): - return "NaCl" - elif keyfile_data_is_encrypted_ansible(keyfile_data): - return "Ansible Vault" - elif keyfile_data_is_encrypted_legacy(keyfile_data): - return "legacy" - - -def legacy_encrypt_keyfile_data(keyfile_data: bytes, password: str = None) -> bytes: - password = ask_password_to_encrypt() if password is None else password - console = bittensor.__console__ - with console.status( - ":exclamation_mark: Encrypting key with legacy encrpytion method..." - ): - vault = Vault(password) - return vault.vault.encrypt(keyfile_data) - - -def encrypt_keyfile_data(keyfile_data: bytes, password: str = None) -> bytes: - """Encrypts the passed keyfile data using ansible vault. - - Args: - keyfile_data (bytes): The bytes to encrypt. - password (str, optional): The password used to encrypt the data. If ``None``, asks for user input. - Returns: - encrypted_data (bytes): The encrypted data. - """ - password = bittensor.ask_password_to_encrypt() if password is None else password - password = bytes(password, "utf-8") - kdf = pwhash.argon2i.kdf - key = kdf( - secret.SecretBox.KEY_SIZE, - password, - NACL_SALT, - opslimit=pwhash.argon2i.OPSLIMIT_SENSITIVE, - memlimit=pwhash.argon2i.MEMLIMIT_SENSITIVE, - ) - box = secret.SecretBox(key) - encrypted = box.encrypt(keyfile_data) - return b"$NACL" + encrypted - - -def get_coldkey_password_from_environment(coldkey_name: str) -> Optional[str]: - """Retrieves the cold key password from the environment variables. - - Args: - coldkey_name (str): The name of the cold key. - Returns: - password (str): The password retrieved from the environment variables, or ``None`` if not found. - """ - envs = { - normalized_env_name: env_value - for env_name, env_value in os.environ.items() - if (normalized_env_name := env_name.upper()).startswith("BT_COLD_PW_") - } - return envs.get(f"BT_COLD_PW_{coldkey_name.replace('-', '_').upper()}") - - -def decrypt_keyfile_data( - keyfile_data: bytes, password: str = None, coldkey_name: Optional[str] = None -) -> bytes: - """Decrypts the passed keyfile data using ansible vault. - - Args: - keyfile_data (bytes): The bytes to decrypt. - password (str, optional): The password used to decrypt the data. If ``None``, asks for user input. - coldkey_name (str, optional): The name of the cold key. If provided, retrieves the password from environment variables. - Returns: - decrypted_data (bytes): The decrypted data. - Raises: - KeyFileError: Raised if the file is corrupted or if the password is incorrect. - """ - if coldkey_name is not None and password is None: - password = get_coldkey_password_from_environment(coldkey_name) - - try: - password = ( - getpass.getpass("Enter password to unlock key: ") - if password is None - else password - ) - console = bittensor.__console__ - with console.status(":key: Decrypting key..."): - # NaCl SecretBox decrypt. - if keyfile_data_is_encrypted_nacl(keyfile_data): - password = bytes(password, "utf-8") - kdf = pwhash.argon2i.kdf - key = kdf( - secret.SecretBox.KEY_SIZE, - password, - NACL_SALT, - opslimit=pwhash.argon2i.OPSLIMIT_SENSITIVE, - memlimit=pwhash.argon2i.MEMLIMIT_SENSITIVE, - ) - box = secret.SecretBox(key) - decrypted_keyfile_data = box.decrypt(keyfile_data[len("$NACL") :]) - # Ansible decrypt. - elif keyfile_data_is_encrypted_ansible(keyfile_data): - vault = Vault(password) - try: - decrypted_keyfile_data = vault.load(keyfile_data) - except AnsibleVaultError: - raise bittensor.KeyFileError("Invalid password") - # Legacy decrypt. - elif keyfile_data_is_encrypted_legacy(keyfile_data): - __SALT = ( - b"Iguesscyborgslikemyselfhaveatendencytobeparanoidaboutourorigins" - ) - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - salt=__SALT, - length=32, - iterations=10000000, - backend=default_backend(), - ) - key = base64.urlsafe_b64encode(kdf.derive(password.encode())) - cipher_suite = Fernet(key) - decrypted_keyfile_data = cipher_suite.decrypt(keyfile_data) - # Unknown. - else: - raise bittensor.KeyFileError( - "keyfile data: {} is corrupt".format(keyfile_data) - ) - - except (InvalidSignature, InvalidKey, InvalidToken): - raise bittensor.KeyFileError("Invalid password") - - if not isinstance(decrypted_keyfile_data, bytes): - decrypted_keyfile_data = json.dumps(decrypted_keyfile_data).encode() - return decrypted_keyfile_data - - -class keyfile: - """Defines an interface for a substrate interface keypair stored on device.""" - - def __init__(self, path: str): - self.path = os.path.expanduser(path) - self.name = Path(self.path).parent.stem - - def __str__(self): - if not self.exists_on_device(): - return "keyfile (empty, {})>".format(self.path) - if self.is_encrypted(): - return "Keyfile ({} encrypted, {})>".format( - keyfile_data_encryption_method(self._read_keyfile_data_from_file()), - self.path, - ) - else: - return "keyfile (decrypted, {})>".format(self.path) - - def __repr__(self): - return self.__str__() - - @property - def keypair(self) -> "bittensor.Keypair": - """Returns the keypair from path, decrypts data if the file is encrypted. - - Returns: - keypair (bittensor.Keypair): The keypair stored under the path. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, writable, corrupted, or if the password is incorrect. - """ - return self.get_keypair() - - @property - def data(self) -> bytes: - """Returns the keyfile data under path. - - Returns: - keyfile_data (bytes): The keyfile data stored under the path. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, or writable. - """ - return self._read_keyfile_data_from_file() - - @property - def keyfile_data(self) -> bytes: - """Returns the keyfile data under path. - - Returns: - keyfile_data (bytes): The keyfile data stored under the path. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, or writable. - """ - return self._read_keyfile_data_from_file() - - def set_keypair( - self, - keypair: "bittensor.Keypair", - encrypt: bool = True, - overwrite: bool = False, - password: str = None, - ): - """Writes the keypair to the file and optionally encrypts data. - - Args: - keypair (bittensor.Keypair): The keypair to store under the path. - encrypt (bool, optional): If ``True``, encrypts the file under the path. Default is ``True``. - overwrite (bool, optional): If ``True``, forces overwrite of the current file. Default is ``False``. - password (str, optional): The password used to encrypt the file. If ``None``, asks for user input. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, writable, or if the password is incorrect. - """ - self.make_dirs() - keyfile_data = serialized_keypair_to_keyfile_data(keypair) - if encrypt: - keyfile_data = bittensor.encrypt_keyfile_data(keyfile_data, password) - self._write_keyfile_data_to_file(keyfile_data, overwrite=overwrite) - - def get_keypair(self, password: str = None) -> "bittensor.Keypair": - """Returns the keypair from the path, decrypts data if the file is encrypted. - - Args: - password (str, optional): The password used to decrypt the file. If ``None``, asks for user input. - Returns: - keypair (bittensor.Keypair): The keypair stored under the path. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, writable, corrupted, or if the password is incorrect. - """ - keyfile_data = self._read_keyfile_data_from_file() - if keyfile_data_is_encrypted(keyfile_data): - decrypted_keyfile_data = decrypt_keyfile_data( - keyfile_data, password, coldkey_name=self.name - ) - else: - decrypted_keyfile_data = keyfile_data - return deserialize_keypair_from_keyfile_data(decrypted_keyfile_data) - - def make_dirs(self): - """Creates directories for the path if they do not exist.""" - directory = os.path.dirname(self.path) - if not os.path.exists(directory): - os.makedirs(directory) - - def exists_on_device(self) -> bool: - """Returns ``True`` if the file exists on the device. - - Returns: - on_device (bool): ``True`` if the file is on the device. - """ - if not os.path.isfile(self.path): - return False - return True - - def is_readable(self) -> bool: - """Returns ``True`` if the file under path is readable. - - Returns: - readable (bool): ``True`` if the file is readable. - """ - if not self.exists_on_device(): - return False - if not os.access(self.path, os.R_OK): - return False - return True - - def is_writable(self) -> bool: - """Returns ``True`` if the file under path is writable. - - Returns: - writable (bool): ``True`` if the file is writable. - """ - if os.access(self.path, os.W_OK): - return True - return False - - def is_encrypted(self) -> bool: - """Returns ``True`` if the file under path is encrypted. - - Returns: - encrypted (bool): ``True`` if the file is encrypted. - """ - if not self.exists_on_device(): - return False - if not self.is_readable(): - return False - return keyfile_data_is_encrypted(self._read_keyfile_data_from_file()) - - def _may_overwrite(self) -> bool: - """Asks the user if it is okay to overwrite the file. - - Returns: - may_overwrite (bool): ``True`` if the user allows overwriting the file. - """ - choice = input("File {} already exists. Overwrite? (y/N) ".format(self.path)) - return choice == "y" - - def check_and_update_encryption( - self, print_result: bool = True, no_prompt: bool = False - ): - """Check the version of keyfile and update if needed. - - Args: - print_result (bool): - Print the checking result or not. - no_prompt (bool): - Skip if no prompt. - Raises: - KeyFileError: - Raised if the file does not exists, is not readable, writable. - Returns: - result (bool): - Return ``True`` if the keyfile is the most updated with nacl, else ``False``. - """ - if not self.exists_on_device(): - if print_result: - bittensor.__console__.print(f"Keyfile does not exist. {self.path}") - return False - if not self.is_readable(): - if print_result: - bittensor.__console__.print(f"Keyfile is not redable. {self.path}") - return False - if not self.is_writable(): - if print_result: - bittensor.__console__.print(f"Keyfile is not writable. {self.path}") - return False - - update_keyfile = False - if not no_prompt: - keyfile_data = self._read_keyfile_data_from_file() - - # If the key is not nacl encrypted. - if keyfile_data_is_encrypted( - keyfile_data - ) and not keyfile_data_is_encrypted_nacl(keyfile_data): - terminate = False - bittensor.__console__.print( - f"You may update the keyfile to improve the security for storing your keys.\nWhile the key and the password stays the same, it would require providing your password once.\n:key:{self}\n" - ) - update_keyfile = Confirm.ask("Update keyfile?") - if update_keyfile: - stored_mnemonic = False - while not stored_mnemonic: - bittensor.__console__.print( - f"\nPlease make sure you have the mnemonic stored in case an error occurs during the transfer.", - style="white on red", - ) - stored_mnemonic = Confirm.ask("Have you stored the mnemonic?") - if not stored_mnemonic and not Confirm.ask( - "You must proceed with a stored mnemonic, retry and continue this keyfile update?" - ): - terminate = True - break - - decrypted_keyfile_data = None - while decrypted_keyfile_data == None and not terminate: - try: - password = getpass.getpass( - "\nEnter password to update keyfile: " - ) - decrypted_keyfile_data = decrypt_keyfile_data( - keyfile_data, coldkey_name=self.name, password=password - ) - except KeyFileError: - if not Confirm.ask( - "Invalid password, retry and continue this keyfile update?" - ): - terminate = True - break - - if not terminate: - encrypted_keyfile_data = encrypt_keyfile_data( - decrypted_keyfile_data, password=password - ) - self._write_keyfile_data_to_file( - encrypted_keyfile_data, overwrite=True - ) - - if print_result or update_keyfile: - keyfile_data = self._read_keyfile_data_from_file() - if not keyfile_data_is_encrypted(keyfile_data): - if print_result: - bittensor.__console__.print( - f"\nKeyfile is not encrypted. \n:key: {self}" - ) - return False - elif keyfile_data_is_encrypted_nacl(keyfile_data): - if print_result: - bittensor.__console__.print( - f"\n:white_heavy_check_mark: Keyfile is updated. \n:key: {self}" - ) - return True - else: - if print_result: - bittensor.__console__.print( - f'\n:cross_mark: Keyfile is outdated, please update with "btcli wallet update" \n:key: {self}' - ) - return False - return False - - def encrypt(self, password: str = None): - """Encrypts the file under the path. - - Args: - password (str, optional): The password for encryption. If ``None``, asks for user input. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, or writable. - """ - if not self.exists_on_device(): - raise bittensor.KeyFileError( - "Keyfile at: {} does not exist".format(self.path) - ) - if not self.is_readable(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not readable".format(self.path) - ) - if not self.is_writable(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not writable".format(self.path) - ) - keyfile_data = self._read_keyfile_data_from_file() - if not keyfile_data_is_encrypted(keyfile_data): - as_keypair = deserialize_keypair_from_keyfile_data(keyfile_data) - keyfile_data = serialized_keypair_to_keyfile_data(as_keypair) - keyfile_data = encrypt_keyfile_data(keyfile_data, password) - self._write_keyfile_data_to_file(keyfile_data, overwrite=True) - - def decrypt(self, password: str = None): - """Decrypts the file under the path. - - Args: - password (str, optional): The password for decryption. If ``None``, asks for user input. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, writable, corrupted, or if the password is incorrect. - """ - if not self.exists_on_device(): - raise bittensor.KeyFileError( - "Keyfile at: {} does not exist".format(self.path) - ) - if not self.is_readable(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not readable".format(self.path) - ) - if not self.is_writable(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not writable".format(self.path) - ) - keyfile_data = self._read_keyfile_data_from_file() - if keyfile_data_is_encrypted(keyfile_data): - keyfile_data = decrypt_keyfile_data( - keyfile_data, password, coldkey_name=self.name - ) - as_keypair = deserialize_keypair_from_keyfile_data(keyfile_data) - keyfile_data = serialized_keypair_to_keyfile_data(as_keypair) - self._write_keyfile_data_to_file(keyfile_data, overwrite=True) - - def _read_keyfile_data_from_file(self) -> bytes: - """Reads the keyfile data from the file. - - Returns: - keyfile_data (bytes): The keyfile data stored under the path. - Raises: - KeyFileError: Raised if the file does not exist or is not readable. - """ - if not self.exists_on_device(): - raise bittensor.KeyFileError( - "Keyfile at: {} does not exist".format(self.path) - ) - if not self.is_readable(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not readable".format(self.path) - ) - with open(self.path, "rb") as file: - data = file.read() - return data - - def _write_keyfile_data_to_file(self, keyfile_data: bytes, overwrite: bool = False): - """Writes the keyfile data to the file. - - Args: - keyfile_data (bytes): The byte data to store under the path. - overwrite (bool, optional): If ``True``, overwrites the data without asking for permission from the user. Default is ``False``. - Raises: - KeyFileError: Raised if the file is not writable or the user responds No to the overwrite prompt. - """ - # Check overwrite. - if self.exists_on_device() and not overwrite: - if not self._may_overwrite(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not writable".format(self.path) - ) - with open(self.path, "wb") as keyfile: - keyfile.write(keyfile_data) - # Set file permissions. - os.chmod(self.path, stat.S_IRUSR | stat.S_IWUSR) - - -class Mockkeyfile: - """ - The Mockkeyfile is a mock object representing a keyfile that does not exist on the device. - - It is designed for use in testing scenarios and simulations where actual filesystem operations are not required. - The keypair stored in the Mockkeyfile is treated as non-encrypted and the data is stored as a serialized string. - """ - - def __init__(self, path: str): - """ - Initializes a Mockkeyfile object. - - Args: - path (str): The path of the mock keyfile. - """ - self.path = path - self._mock_keypair = None - self._mock_data = None - - def __str__(self): - """ - Returns a string representation of the Mockkeyfile. The representation will indicate if the keyfile is empty, encrypted, or decrypted. - - Returns: - str: The string representation of the Mockkeyfile. - """ - return f"Mockkeyfile({self.path})" - - def __repr__(self): - """ - Returns a string representation of the Mockkeyfile, same as :func:`__str__()`. - - Returns: - str: The string representation of the Mockkeyfile. - """ - return self.__str__() - - @property - def keypair(self): - """ - Returns the mock keypair stored in the keyfile. - - Returns: - bittensor.Keypair: The mock keypair. - """ - return self._mock_keypair - - @property - def data(self): - """ - Returns the serialized keypair data stored in the keyfile. - - Returns: - bytes: The serialized keypair data. - """ - return self._mock_data - - def set_keypair(self, keypair, encrypt=True, overwrite=False, password=None): - """ - Sets the mock keypair in the keyfile. The ``encrypt`` and ``overwrite`` parameters are ignored. - - Args: - keypair (bittensor.Keypair): The mock keypair to be set. - encrypt (bool, optional): Ignored in this context. Defaults to ``True``. - overwrite (bool, optional): Ignored in this context. Defaults to ``False``. - password (str, optional): Ignored in this context. Defaults to ``None``. - """ - self._mock_keypair = keypair - self._mock_data = None # You may need to serialize the keypair here - - def get_keypair(self, password=None): - """ - Returns the mock keypair stored in the keyfile. The ``password`` parameter is ignored. - - Args: - password (str, optional): Ignored in this context. Defaults to ``None``. - - Returns: - bittensor.Keypair: The mock keypair stored in the keyfile. - """ - return self._mock_keypair - - def make_dirs(self): - """ - Creates the directories for the mock keyfile. Does nothing in this class, since no actual filesystem operations are needed. - """ - pass - - def exists_on_device(self): - """ - Returns ``True`` indicating that the mock keyfile exists on the device (although it is not created on the actual file system). - - Returns: - bool: Always returns ``True`` for Mockkeyfile. - """ - return True - - def is_readable(self): - """ - Returns ``True`` indicating that the mock keyfile is readable (although it is not read from the actual file system). - - Returns: - bool: Always returns ``True`` for Mockkeyfile. - """ - return True - - def is_writable(self): - """ - Returns ``True`` indicating that the mock keyfile is writable (although it is not written to the actual file system). - - Returns: - bool: Always returns ``True`` for Mockkeyfile. - """ - return True - - def is_encrypted(self): - """ - Returns ``False`` indicating that the mock keyfile is not encrypted. - - Returns: - bool: Always returns ``False`` for Mockkeyfile. - """ - return False - - def encrypt(self, password=None): - """ - Raises a ValueError since encryption is not supported for the mock keyfile. - - Args: - password (str, optional): Ignored in this context. Defaults to ``None``. - - Raises: - ValueError: Always raises this exception for Mockkeyfile. - """ - raise ValueError("Cannot encrypt a Mockkeyfile") - - def decrypt(self, password=None): - """ - Returns without doing anything since the mock keyfile is not encrypted. - - Args: - password (str, optional): Ignored in this context. Defaults to ``None``. - """ - pass - - def check_and_update_encryption(self, no_prompt=None, print_result=False): - return diff --git a/bittensor/mock/subtensor_mock.py b/bittensor/mock/subtensor_mock.py index 5c2c3b42d..4787fa0ce 100644 --- a/bittensor/mock/subtensor_mock.py +++ b/bittensor/mock/subtensor_mock.py @@ -1,30 +1,31 @@ # The MIT License (MIT) -# Copyright © 2022-2023 Opentensor Foundation - +# 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. +from abc import abstractclassmethod +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, Dict, List, Optional, Tuple, TypedDict, Union +from typing import Any, Dict, List, Optional, Tuple, Union +from typing import TypedDict from unittest.mock import MagicMock -from dataclasses import dataclass -from abc import abstractclassmethod -from collections.abc import Mapping -from hashlib import sha256 -from ..wallet import wallet +from bittensor_wallet import Wallet from ..chain_data import ( NeuronInfo, @@ -40,9 +41,6 @@ from ..utils.balance import Balance from ..utils.registration import POWSolution -from typing import TypedDict - - # Mock Testing Constant __GLOBAL_MOCK_STATE__ = {} @@ -578,7 +576,7 @@ def _handle_type_default(self, name: str, params: List[object]) -> object: return defaults_mapping.get(name, None) - def commit(self, wallet: "wallet", netuid: int, data: str) -> None: + def commit(self, wallet: "Wallet", netuid: int, data: str) -> None: uid = self.get_uid_for_hotkey_on_subnet( hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, @@ -1007,7 +1005,7 @@ def neurons_lite( # Extrinsics def _do_delegation( self, - wallet: "wallet", + wallet: "Wallet", delegate_ss58: str, amount: "Balance", wait_for_inclusion: bool = True, @@ -1030,7 +1028,7 @@ def _do_delegation( def _do_undelegation( self, - wallet: "wallet", + wallet: "Wallet", delegate_ss58: str, amount: "Balance", wait_for_inclusion: bool = True, @@ -1051,7 +1049,7 @@ def _do_undelegation( def _do_nominate( self, - wallet: "wallet", + wallet: "Wallet", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: @@ -1071,13 +1069,13 @@ def _do_nominate( return True def get_transfer_fee( - self, wallet: "wallet", dest: str, value: Union["Balance", float, int] + self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] ) -> "Balance": return Balance(700) def _do_transfer( self, - wallet: "wallet", + wallet: "Wallet", dest: str, transfer_balance: "Balance", wait_for_inclusion: bool = True, @@ -1110,7 +1108,7 @@ def _do_transfer( def _do_pow_register( self, netuid: int, - wallet: "wallet", + wallet: "Wallet", pow_result: "POWSolution", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, @@ -1132,7 +1130,7 @@ def _do_pow_register( def _do_burned_register( self, netuid: int, - wallet: "wallet", + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> Tuple[bool, Optional[str]]: @@ -1162,7 +1160,7 @@ def _do_burned_register( def _do_stake( self, - wallet: "wallet", + wallet: "Wallet", hotkey_ss58: str, amount: "Balance", wait_for_inclusion: bool = True, @@ -1233,7 +1231,7 @@ def _do_stake( def _do_unstake( self, - wallet: "wallet", + wallet: "Wallet", hotkey_ss58: str, amount: "Balance", wait_for_inclusion: bool = True, @@ -1440,7 +1438,7 @@ def query_subnet_info(name: str) -> Optional[object]: def _do_serve_prometheus( self, - wallet: "wallet", + wallet: "Wallet", call_params: "PrometheusServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, @@ -1449,7 +1447,7 @@ def _do_serve_prometheus( def _do_set_weights( self, - wallet: "wallet", + wallet: "Wallet", netuid: int, uids: int, vals: List[int], @@ -1461,7 +1459,7 @@ def _do_set_weights( def _do_serve_axon( self, - wallet: "wallet", + wallet: "Wallet", call_params: "AxonServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, diff --git a/bittensor/wallet.py b/bittensor/wallet.py deleted file mode 100644 index 28da5d865..000000000 --- a/bittensor/wallet.py +++ /dev/null @@ -1,873 +0,0 @@ -"""Implementation of the wallet class, which manages balances with staking and transfer. Also manages hotkey and coldkey.""" - -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 copy -import os -from typing import Dict, Optional, Tuple, Union, overload - -from substrateinterface import Keypair -from termcolor import colored - -import bittensor -from bittensor.utils import is_valid_bittensor_address_or_public_key - - -def display_mnemonic_msg(keypair: Keypair, key_type: str): - """ - Display the mnemonic and a warning message to keep the mnemonic safe. - - Args: - keypair (Keypair): Keypair object. - key_type (str): Type of the key (coldkey or hotkey). - """ - mnemonic = keypair.mnemonic - mnemonic_green = colored(mnemonic, "green") - print( - colored( - "\nIMPORTANT: Store this mnemonic in a secure (preferable offline place), as anyone " - "who has possession of this mnemonic can use it to regenerate the key and access your tokens. \n", - "red", - ) - ) - print("The mnemonic to the new {} is:\n\n{}\n".format(key_type, mnemonic_green)) - print( - "You can use the mnemonic to recreate the key in case it gets lost. The command to use to regenerate the key using this mnemonic is:" - ) - print("btcli w regen_{} --mnemonic {}".format(key_type, mnemonic)) - print("") - - -class wallet: - """ - The wallet class in the Bittensor framework handles wallet functionality, crucial for participating in the Bittensor network. - - It manages two types of keys: coldkey and hotkey, each serving different purposes in network operations. Each wallet contains a coldkey and a hotkey. - - The coldkey is the user's primary key for holding stake in their wallet and is the only way that users - can access Tao. Coldkeys can hold tokens and should be encrypted on your device. - - The coldkey is the primary key used for securing the wallet's stake in the Bittensor network (Tao) and - is critical for financial transactions like staking and unstaking tokens. It's recommended to keep the - coldkey encrypted and secure, as it holds the actual tokens. - - The hotkey, in contrast, is used for operational tasks like subscribing to and setting weights in the - network. It's linked to the coldkey through the metagraph and does not directly hold tokens, thereby - offering a safer way to interact with the network during regular operations. - - Args: - name (str): The name of the wallet, used to identify it among possibly multiple wallets. - path (str): File system path where wallet keys are stored. - hotkey_str (str): String identifier for the hotkey. - _hotkey, _coldkey, _coldkeypub (bittensor.Keypair): Internal representations of the hotkey and coldkey. - - Methods: - create_if_non_existent, create, recreate: Methods to handle the creation of wallet keys. - get_coldkey, get_hotkey, get_coldkeypub: Methods to retrieve specific keys. - set_coldkey, set_hotkey, set_coldkeypub: Methods to set or update keys. - hotkey_file, coldkey_file, coldkeypub_file: Properties that return respective key file objects. - regenerate_coldkey, regenerate_hotkey, regenerate_coldkeypub: Methods to regenerate keys from different sources. - config, help, add_args: Utility methods for configuration and assistance. - - The wallet class is a fundamental component for users to interact securely with the Bittensor network, facilitating both operational tasks and transactions involving value transfer across the network. - - Example Usage:: - - # Create a new wallet with default coldkey and hotkey names - my_wallet = wallet() - - # Access hotkey and coldkey - hotkey = my_wallet.get_hotkey() - coldkey = my_wallet.get_coldkey() - - # Set a new coldkey - my_wallet.new_coldkey(n_words=24) # number of seed words to use - - # Update wallet hotkey - my_wallet.set_hotkey(new_hotkey) - - # Print wallet details - print(my_wallet) - - # Access coldkey property, must use password to unlock - my_wallet.coldkey - """ - - @classmethod - def config(cls) -> "bittensor.config": - """ - Get config from the argument parser. - - Returns: - bittensor.config: Config object. - """ - parser = argparse.ArgumentParser() - cls.add_args(parser) - return bittensor.config(parser, args=[]) - - @classmethod - def help(cls): - """ - Print help to stdout. - """ - parser = argparse.ArgumentParser() - cls.add_args(parser) - print(cls.__new__.__doc__) - parser.print_help() - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): - """ - Accept specific arguments from parser. - - Args: - parser (argparse.ArgumentParser): Argument parser object. - prefix (str): Argument prefix. - """ - prefix_str = "" if prefix is None else prefix + "." - try: - default_name = os.getenv("BT_WALLET_NAME") or "default" - default_hotkey = os.getenv("BT_WALLET_NAME") or "default" - default_path = os.getenv("BT_WALLET_PATH") or "~/.bittensor/wallets/" - parser.add_argument( - "--" + prefix_str + "wallet.name", - required=False, - default=default_name, - help="The name of the wallet to unlock for running bittensor " - "(name mock is reserved for mocking this wallet)", - ) - parser.add_argument( - "--" + prefix_str + "wallet.hotkey", - required=False, - default=default_hotkey, - help="The name of the wallet's hotkey.", - ) - parser.add_argument( - "--" + prefix_str + "wallet.path", - required=False, - default=default_path, - help="The path to your bittensor wallets", - ) - except argparse.ArgumentError as e: - pass - - def __init__( - self, - name: str = None, - hotkey: str = None, - path: str = None, - config: "bittensor.config" = None, - ): - r""" - Initialize the bittensor wallet object containing a hot and coldkey. - - Args: - name (str, optional): The name of the wallet to unlock for running bittensor. Defaults to ``default``. - hotkey (str, optional): The name of hotkey used to running the miner. Defaults to ``default``. - path (str, optional): The path to your bittensor wallets. Defaults to ``~/.bittensor/wallets/``. - config (bittensor.config, optional): bittensor.wallet.config(). Defaults to ``None``. - """ - # Fill config from passed args using command line defaults. - if config is None: - config = wallet.config() - self.config = copy.deepcopy(config) - self.config.wallet.name = name or self.config.wallet.get( - "name", bittensor.defaults.wallet.name - ) - self.config.wallet.hotkey = hotkey or self.config.wallet.get( - "hotkey", bittensor.defaults.wallet.hotkey - ) - self.config.wallet.path = path or self.config.wallet.get( - "path", bittensor.defaults.wallet.path - ) - - self.name = self.config.wallet.name - self.path = self.config.wallet.path - self.hotkey_str = self.config.wallet.hotkey - - self._hotkey = None - self._coldkey = None - self._coldkeypub = None - - def __str__(self): - """ - Returns the string representation of the Wallet object. - - Returns: - str: The string representation. - """ - return "wallet({}, {}, {})".format(self.name, self.hotkey_str, self.path) - - def __repr__(self): - """ - Returns the string representation of the wallet object. - - Returns: - str: The string representation. - """ - return self.__str__() - - def create_if_non_existent( - self, coldkey_use_password: bool = True, hotkey_use_password: bool = False - ) -> "wallet": - """ - Checks for existing coldkeypub and hotkeys, and creates them if non-existent. - - Args: - coldkey_use_password (bool, optional): Whether to use a password for coldkey. Defaults to ``True``. - hotkey_use_password (bool, optional): Whether to use a password for hotkey. Defaults to ``False``. - - Returns: - wallet: The wallet object. - """ - return self.create(coldkey_use_password, hotkey_use_password) - - def create( - self, coldkey_use_password: bool = True, hotkey_use_password: bool = False - ) -> "wallet": - """ - Checks for existing coldkeypub and hotkeys, and creates them if non-existent. - - Args: - coldkey_use_password (bool, optional): Whether to use a password for coldkey. Defaults to ``True``. - hotkey_use_password (bool, optional): Whether to use a password for hotkey. Defaults to ``False``. - - Returns: - wallet: The wallet object. - """ - # ---- Setup Wallet. ---- - if ( - not self.coldkey_file.exists_on_device() - and not self.coldkeypub_file.exists_on_device() - ): - self.create_new_coldkey(n_words=12, use_password=coldkey_use_password) - if not self.hotkey_file.exists_on_device(): - self.create_new_hotkey(n_words=12, use_password=hotkey_use_password) - return self - - def recreate( - self, coldkey_use_password: bool = True, hotkey_use_password: bool = False - ) -> "wallet": - """ - Checks for existing coldkeypub and hotkeys and creates them if non-existent. - - Args: - coldkey_use_password (bool, optional): Whether to use a password for coldkey. Defaults to ``True``. - hotkey_use_password (bool, optional): Whether to use a password for hotkey. Defaults to ``False``. - - Returns: - wallet: The wallet object. - """ - # ---- Setup Wallet. ---- - self.create_new_coldkey(n_words=12, use_password=coldkey_use_password) - self.create_new_hotkey(n_words=12, use_password=hotkey_use_password) - return self - - @property - def hotkey_file(self) -> "bittensor.keyfile": - """ - Property that returns the hotkey file. - - Returns: - bittensor.keyfile: The hotkey file. - """ - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - hotkey_path = os.path.join(wallet_path, "hotkeys", self.hotkey_str) - return bittensor.keyfile(path=hotkey_path) - - @property - def coldkey_file(self) -> "bittensor.keyfile": - """ - Property that returns the coldkey file. - - Returns: - bittensor.keyfile: The coldkey file. - """ - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkey_path = os.path.join(wallet_path, "coldkey") - return bittensor.keyfile(path=coldkey_path) - - @property - def coldkeypub_file(self) -> "bittensor.keyfile": - """ - Property that returns the coldkeypub file. - - Returns: - bittensor.keyfile: The coldkeypub file. - """ - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkeypub_path = os.path.join(wallet_path, "coldkeypub.txt") - return bittensor.keyfile(path=coldkeypub_path) - - def set_hotkey( - self, - keypair: "bittensor.Keypair", - encrypt: bool = False, - overwrite: bool = False, - ) -> "bittensor.keyfile": - """ - Sets the hotkey for the wallet. - - Args: - keypair (bittensor.Keypair): The hotkey keypair. - encrypt (bool, optional): Whether to encrypt the hotkey. Defaults to ``False``. - overwrite (bool, optional): Whether to overwrite an existing hotkey. Defaults to ``False``. - - Returns: - bittensor.keyfile: The hotkey file. - """ - self._hotkey = keypair - self.hotkey_file.set_keypair(keypair, encrypt=encrypt, overwrite=overwrite) - - def set_coldkeypub( - self, - keypair: "bittensor.Keypair", - encrypt: bool = False, - overwrite: bool = False, - ) -> "bittensor.keyfile": - """ - Sets the coldkeypub for the wallet. - - Args: - keypair (bittensor.Keypair): The coldkeypub keypair. - encrypt (bool, optional): Whether to encrypt the coldkeypub. Defaults to ``False``. - overwrite (bool, optional): Whether to overwrite an existing coldkeypub. Defaults to ``False``. - - Returns: - bittensor.keyfile: The coldkeypub file. - """ - self._coldkeypub = bittensor.Keypair(ss58_address=keypair.ss58_address) - self.coldkeypub_file.set_keypair( - self._coldkeypub, encrypt=encrypt, overwrite=overwrite - ) - - def set_coldkey( - self, - keypair: "bittensor.Keypair", - encrypt: bool = True, - overwrite: bool = False, - ) -> "bittensor.keyfile": - """ - Sets the coldkey for the wallet. - - Args: - keypair (bittensor.Keypair): The coldkey keypair. - encrypt (bool, optional): Whether to encrypt the coldkey. Defaults to ``True``. - overwrite (bool, optional): Whether to overwrite an existing coldkey. Defaults to ``False``. - - Returns: - bittensor.keyfile: The coldkey file. - """ - self._coldkey = keypair - self.coldkey_file.set_keypair( - self._coldkey, encrypt=encrypt, overwrite=overwrite - ) - - def get_coldkey(self, password: str = None) -> "bittensor.Keypair": - """ - Gets the coldkey from the wallet. - - Args: - password (str, optional): The password to decrypt the coldkey. Defaults to ``None``. - - Returns: - bittensor.Keypair: The coldkey keypair. - """ - return self.coldkey_file.get_keypair(password=password) - - def get_hotkey(self, password: str = None) -> "bittensor.Keypair": - """ - Gets the hotkey from the wallet. - - Args: - password (str, optional): The password to decrypt the hotkey. Defaults to ``None``. - - Returns: - bittensor.Keypair: The hotkey keypair. - """ - return self.hotkey_file.get_keypair(password=password) - - def get_coldkeypub(self, password: str = None) -> "bittensor.Keypair": - """ - Gets the coldkeypub from the wallet. - - Args: - password (str, optional): The password to decrypt the coldkeypub. Defaults to ``None``. - - Returns: - bittensor.Keypair: The coldkeypub keypair. - """ - return self.coldkeypub_file.get_keypair(password=password) - - @property - def hotkey(self) -> "bittensor.Keypair": - r"""Loads the hotkey from wallet.path/wallet.name/hotkeys/wallet.hotkey or raises an error. - - Returns: - hotkey (Keypair): - hotkey loaded from config arguments. - Raises: - KeyFileError: Raised if the file is corrupt of non-existent. - CryptoKeyError: Raised if the user enters an incorrec password for an encrypted keyfile. - """ - if self._hotkey == None: - self._hotkey = self.hotkey_file.keypair - return self._hotkey - - @property - def coldkey(self) -> "bittensor.Keypair": - r"""Loads the hotkey from wallet.path/wallet.name/coldkey or raises an error. - - Returns: - coldkey (Keypair): coldkey loaded from config arguments. - Raises: - KeyFileError: Raised if the file is corrupt of non-existent. - CryptoKeyError: Raised if the user enters an incorrec password for an encrypted keyfile. - """ - if self._coldkey == None: - self._coldkey = self.coldkey_file.keypair - return self._coldkey - - @property - def coldkeypub(self) -> "bittensor.Keypair": - r"""Loads the coldkeypub from wallet.path/wallet.name/coldkeypub.txt or raises an error. - - Returns: - coldkeypub (Keypair): coldkeypub loaded from config arguments. - Raises: - KeyFileError: Raised if the file is corrupt of non-existent. - CryptoKeyError: Raised if the user enters an incorrect password for an encrypted keyfile. - """ - if self._coldkeypub == None: - self._coldkeypub = self.coldkeypub_file.keypair - return self._coldkeypub - - def create_coldkey_from_uri( - self, - uri: str, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates coldkey from suri string, optionally encrypts it with the user-provided password. - - Args: - uri: (str, required): - URI string to use i.e., ``/Alice`` or ``/Bob``. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the coldkey under the same path ``//coldkey``. - Returns: - wallet (bittensor.wallet): - This object with newly created coldkey. - """ - keypair = Keypair.create_from_uri(uri) - if not suppress: - display_mnemonic_msg(keypair, "coldkey") - self.set_coldkey(keypair, encrypt=use_password, overwrite=overwrite) - self.set_coldkeypub(keypair, overwrite=overwrite) - return self - - def create_hotkey_from_uri( - self, - uri: str, - use_password: bool = False, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates hotkey from suri string, optionally encrypts it with the user-provided password. - - Args: - uri: (str, required): - URI string to use i.e., ``/Alice`` or ``/Bob`` - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the hotkey under the same path ``//hotkeys/``. - Returns: - wallet (bittensor.wallet): - This object with newly created hotkey. - """ - keypair = Keypair.create_from_uri(uri) - if not suppress: - display_mnemonic_msg(keypair, "hotkey") - self.set_hotkey(keypair, encrypt=use_password, overwrite=overwrite) - return self - - def new_coldkey( - self, - n_words: int = 12, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates a new coldkey, optionally encrypts it with the user-provided password and saves to disk. - - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the coldkey under the same path ``//coldkey``. - Returns: - wallet (bittensor.wallet): - This object with newly created coldkey. - """ - self.create_new_coldkey(n_words, use_password, overwrite, suppress) - - def create_new_coldkey( - self, - n_words: int = 12, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates a new coldkey, optionally encrypts it with the user-provided password and saves to disk. - - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the coldkey under the same path ``//coldkey``. - Returns: - wallet (bittensor.wallet): - This object with newly created coldkey. - """ - mnemonic = Keypair.generate_mnemonic(n_words) - keypair = Keypair.create_from_mnemonic(mnemonic) - if not suppress: - display_mnemonic_msg(keypair, "coldkey") - self.set_coldkey(keypair, encrypt=use_password, overwrite=overwrite) - self.set_coldkeypub(keypair, overwrite=overwrite) - return self - - def new_hotkey( - self, - n_words: int = 12, - use_password: bool = False, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates a new hotkey, optionally encrypts it with the user-provided password and saves to disk. - - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the hotkey under the same path ``//hotkeys/``. - Returns: - wallet (bittensor.wallet): - This object with newly created hotkey. - """ - self.create_new_hotkey(n_words, use_password, overwrite, suppress) - - def create_new_hotkey( - self, - n_words: int = 12, - use_password: bool = False, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates a new hotkey, optionally encrypts it with the user-provided password and saves to disk. - - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Will this operation overwrite the hotkey under the same path //hotkeys/ - Returns: - wallet (bittensor.wallet): - This object with newly created hotkey. - """ - mnemonic = Keypair.generate_mnemonic(n_words) - keypair = Keypair.create_from_mnemonic(mnemonic) - if not suppress: - display_mnemonic_msg(keypair, "hotkey") - self.set_hotkey(keypair, encrypt=use_password, overwrite=overwrite) - return self - - def regenerate_coldkeypub( - self, - ss58_address: Optional[str] = None, - public_key: Optional[Union[str, bytes]] = None, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Regenerates the coldkeypub from the passed ``ss58_address`` or public_key and saves the file. Requires either ``ss58_address`` or public_key to be passed. - - Args: - ss58_address: (str, optional): - Address as ``ss58`` string. - public_key: (str | bytes, optional): - Public key as hex string or bytes. - overwrite (bool, optional) (default: False): - Determins if this operation overwrites the coldkeypub (if exists) under the same path ``//coldkeypub``. - Returns: - wallet (bittensor.wallet): - Newly re-generated wallet with coldkeypub. - - """ - if ss58_address is None and public_key is None: - raise ValueError("Either ss58_address or public_key must be passed") - - if not is_valid_bittensor_address_or_public_key( - ss58_address if ss58_address is not None else public_key - ): - raise ValueError( - f"Invalid {'ss58_address' if ss58_address is not None else 'public_key'}" - ) - - if ss58_address is not None: - ss58_format = bittensor.utils.get_ss58_format(ss58_address) - keypair = Keypair( - ss58_address=ss58_address, - public_key=public_key, - ss58_format=ss58_format, - ) - else: - keypair = Keypair( - ss58_address=ss58_address, - public_key=public_key, - ss58_format=bittensor.__ss58_format__, - ) - - # No need to encrypt the public key - self.set_coldkeypub(keypair, overwrite=overwrite) - - return self - - # Short name for regenerate_coldkeypub - regen_coldkeypub = regenerate_coldkeypub - - @overload - def regenerate_coldkey( - self, - mnemonic: Optional[Union[list, str]] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - @overload - def regenerate_coldkey( - self, - seed: Optional[str] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - @overload - def regenerate_coldkey( - self, - json: Optional[Tuple[Union[str, Dict], str]] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - def regenerate_coldkey( - self, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - **kwargs, - ) -> "wallet": - """Regenerates the coldkey from the passed mnemonic or seed, or JSON encrypts it with the user's password and saves the file. - - Args: - mnemonic: (Union[list, str], optional): - Key mnemonic as list of words or string space separated words. - seed: (str, optional): - Seed as hex string. - json: (Tuple[Union[str, Dict], str], optional): - Restore from encrypted JSON backup as ``(json_data: Union[str, Dict], passphrase: str)`` - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the coldkey under the same path ``//coldkey``. - Returns: - wallet (bittensor.wallet): - This object with newly created coldkey. - - Note: - Uses priority order: ``mnemonic > seed > json``. - - """ - if len(kwargs) == 0: - raise ValueError("Must pass either mnemonic, seed, or json") - - # Get from kwargs - mnemonic = kwargs.get("mnemonic", None) - seed = kwargs.get("seed", None) - json = kwargs.get("json", None) - - if mnemonic is None and seed is None and json is None: - raise ValueError("Must pass either mnemonic, seed, or json") - if mnemonic is not None: - if isinstance(mnemonic, str): - mnemonic = mnemonic.split() - elif isinstance(mnemonic, list) and len(mnemonic) == 1: - mnemonic = mnemonic[0].split() - if len(mnemonic) not in [12, 15, 18, 21, 24]: - raise ValueError( - "Mnemonic has invalid size. This should be 12,15,18,21 or 24 words" - ) - keypair = Keypair.create_from_mnemonic( - " ".join(mnemonic), ss58_format=bittensor.__ss58_format__ - ) - if not suppress: - display_mnemonic_msg(keypair, "coldkey") - elif seed is not None: - keypair = Keypair.create_from_seed( - seed, ss58_format=bittensor.__ss58_format__ - ) - else: - # json is not None - if ( - not isinstance(json, tuple) - or len(json) != 2 - or not isinstance(json[0], (str, dict)) - or not isinstance(json[1], str) - ): - raise ValueError( - "json must be a tuple of (json_data: str | Dict, passphrase: str)" - ) - - json_data, passphrase = json - keypair = Keypair.create_from_encrypted_json( - json_data, passphrase, ss58_format=bittensor.__ss58_format__ - ) - - self.set_coldkey(keypair, encrypt=use_password, overwrite=overwrite) - self.set_coldkeypub(keypair, overwrite=overwrite) - return self - - # Short name for regenerate_coldkey - regen_coldkey = regenerate_coldkey - - @overload - def regenerate_hotkey( - self, - mnemonic: Optional[Union[list, str]] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - @overload - def regenerate_hotkey( - self, - seed: Optional[str] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - @overload - def regenerate_hotkey( - self, - json: Optional[Tuple[Union[str, Dict], str]] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - def regenerate_hotkey( - self, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - **kwargs, - ) -> "wallet": - """Regenerates the hotkey from passed mnemonic or seed, encrypts it with the user's password and saves the file. - - Args: - mnemonic: (Union[list, str], optional): - Key mnemonic as list of words or string space separated words. - seed: (str, optional): - Seed as hex string. - json: (Tuple[Union[str, Dict], str], optional): - Restore from encrypted JSON backup as ``(json_data: Union[str, Dict], passphrase: str)``. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determies if this operation overwrites the hotkey under the same path ``//hotkeys/``. - Returns: - wallet (bittensor.wallet): - This object with newly created hotkey. - """ - if len(kwargs) == 0: - raise ValueError("Must pass either mnemonic, seed, or json") - - # Get from kwargs - mnemonic = kwargs.get("mnemonic", None) - seed = kwargs.get("seed", None) - json = kwargs.get("json", None) - - if mnemonic is None and seed is None and json is None: - raise ValueError("Must pass either mnemonic, seed, or json") - if mnemonic is not None: - if isinstance(mnemonic, str): - mnemonic = mnemonic.split() - elif isinstance(mnemonic, list) and len(mnemonic) == 1: - mnemonic = mnemonic[0].split() - if len(mnemonic) not in [12, 15, 18, 21, 24]: - raise ValueError( - "Mnemonic has invalid size. This should be 12,15,18,21 or 24 words" - ) - keypair = Keypair.create_from_mnemonic( - " ".join(mnemonic), ss58_format=bittensor.__ss58_format__ - ) - if not suppress: - display_mnemonic_msg(keypair, "hotkey") - elif seed is not None: - keypair = Keypair.create_from_seed( - seed, ss58_format=bittensor.__ss58_format__ - ) - else: - # json is not None - if ( - not isinstance(json, tuple) - or len(json) != 2 - or not isinstance(json[0], (str, dict)) - or not isinstance(json[1], str) - ): - raise ValueError( - "json must be a tuple of (json_data: str | Dict, passphrase: str)" - ) - - json_data, passphrase = json - keypair = Keypair.create_from_encrypted_json( - json_data, passphrase, ss58_format=bittensor.__ss58_format__ - ) - - self.set_hotkey(keypair, encrypt=use_password, overwrite=overwrite) - return self - - # Short name for regenerate_hotkey - regen_hotkey = regenerate_hotkey diff --git a/requirements/prod.txt b/requirements/prod.txt index 1b46e7961..d9caabaa2 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -32,3 +32,6 @@ termcolor tqdm uvicorn wheel + +# temporary btwallet dependence from repo directly instead of pypi +git+https://github.com/opentensor/btwallet.git@main#egg=bittensor-wallet diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py index 6fe1acf3b..5ec03f6ee 100644 --- a/tests/integration_tests/test_cli.py +++ b/tests/integration_tests/test_cli.py @@ -1,34 +1,31 @@ # The MIT License (MIT) -# Copyright © 2022 Yuma Rao -# Copyright © 2022-2023 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 contextlib -from copy import deepcopy import os import random -import shutil +import unittest +from copy import deepcopy from types import SimpleNamespace from typing import Dict -import unittest from unittest.mock import MagicMock, patch import pytest +from bittensor_wallet import Wallet +from bittensor_wallet.mock import get_mock_keypair, get_mock_wallet as generate_wallet import bittensor from bittensor import Balance @@ -36,14 +33,7 @@ from bittensor.commands.identity import SetIdentityCommand from bittensor.commands.wallets import _get_coldkey_ss58_addresses_for_path from bittensor.mock import MockSubtensor -from bittensor.wallet import wallet as Wallet -from tests.helpers import ( - is_running_in_circleci, - MockConsole, - _get_mock_keypair, - _get_mock_wallet as generate_wallet, -) - +from tests.helpers import is_running_in_circleci, MockConsole _subtensor_mock: MockSubtensor = MockSubtensor() @@ -52,16 +42,12 @@ def setUpModule(): _subtensor_mock.reset() _subtensor_mock.create_subnet(netuid=1) - _subtensor_mock.create_subnet(netuid=2) - _subtensor_mock.create_subnet(netuid=3) # Set diff 0 _subtensor_mock.set_difficulty(netuid=1, difficulty=0) - _subtensor_mock.set_difficulty(netuid=2, difficulty=0) - _subtensor_mock.set_difficulty(netuid=3, difficulty=0) @@ -119,7 +105,7 @@ def test_overview(self, _): mock_hotkeys = ["hk0", "hk1", "hk2", "hk3", "hk4"] - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -127,7 +113,7 @@ def test_overview(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), coldkeypub_file=MagicMock( exists_on_device=MagicMock(return_value=True) # Wallet exists ), @@ -227,7 +213,7 @@ def test_overview_not_in_first_subnet(self, _): mock_hotkeys = ["hk0", "hk1", "hk2", "hk3", "hk4"] - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -235,7 +221,7 @@ def test_overview_not_in_first_subnet(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), coldkeypub_file=MagicMock( exists_on_device=MagicMock(return_value=True) # Wallet exists ), @@ -465,7 +451,7 @@ def test_unstake_with_specific_hotkeys(self, _): "hk2": Balance.from_float(12.2), } - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -473,7 +459,7 @@ def test_unstake_with_specific_hotkeys(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(config.hotkeys) ] @@ -541,7 +527,7 @@ def test_unstake_with_all_hotkeys(self, _): "hk2": Balance.from_float(12.2), } - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -549,7 +535,7 @@ def test_unstake_with_all_hotkeys(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(list(mock_stakes.keys())) ] @@ -620,7 +606,7 @@ def test_unstake_with_exclude_hotkeys_from_all(self, _): "hk2": Balance.from_float(12.2), } - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -628,7 +614,7 @@ def test_unstake_with_exclude_hotkeys_from_all(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(list(mock_stakes.keys())) ] @@ -706,7 +692,7 @@ def test_unstake_with_multiple_hotkeys_max_stake(self, _): "hk2": Balance.from_float(12.2), } - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -714,7 +700,7 @@ def test_unstake_with_multiple_hotkeys_max_stake(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(list(mock_stakes.keys())) ] @@ -798,10 +784,10 @@ def test_unstake_with_thresholds(self, _): mock_wallets = [ SimpleNamespace( name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), + coldkey=get_mock_keypair(idx, self.id()), + coldkeypub=get_mock_keypair(idx, self.id()), hotkey_str="hk{}".format(idx), # doesn't matter - hotkey=_get_mock_keypair(idx + 100, self.id()), # doesn't matter + hotkey=get_mock_keypair(idx + 100, self.id()), # doesn't matter ) for idx, wallet_name in enumerate(wallet_names) ] @@ -912,7 +898,7 @@ def test_unstake_all(self, _): mock_stakes: Dict[str, Balance] = {"hk0": Balance.from_float(10.0)} - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -920,7 +906,7 @@ def test_unstake_all(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(config.hotkeys) ] @@ -980,7 +966,7 @@ def test_stake_with_specific_hotkeys(self, _): mock_balance = Balance.from_float(22.2) - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -988,7 +974,7 @@ def test_stake_with_specific_hotkeys(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(config.hotkeys) ] @@ -1054,7 +1040,7 @@ def test_stake_with_all_hotkeys(self, _): mock_balance = Balance.from_float(22.0) - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -1062,7 +1048,7 @@ def test_stake_with_all_hotkeys(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(mock_hotkeys) ] @@ -1152,7 +1138,7 @@ def test_stake_with_exclude_hotkeys_from_all(self, _): mock_balance = Balance.from_float(25.0) - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -1160,7 +1146,7 @@ def test_stake_with_exclude_hotkeys_from_all(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(mock_hotkeys) ] @@ -1258,7 +1244,7 @@ def test_stake_with_multiple_hotkeys_max_stake(self, _): "hk2": Balance.from_float(0.0), } - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -1266,7 +1252,7 @@ def test_stake_with_multiple_hotkeys_max_stake(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(config.hotkeys) ] @@ -1365,7 +1351,7 @@ def test_stake_with_multiple_hotkeys_max_stake_not_enough_balance(self, _): mock_balance = Balance.from_float(15.0 * 2) # Not enough for all hotkeys - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -1373,7 +1359,7 @@ def test_stake_with_multiple_hotkeys_max_stake_not_enough_balance(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(config.hotkeys) ] @@ -1458,7 +1444,7 @@ def test_stake_with_single_hotkey_max_stake(self, _): mock_balance = Balance.from_float(15.0 * 3) - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -1466,7 +1452,7 @@ def test_stake_with_single_hotkey_max_stake(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(config.hotkeys) ] @@ -1546,7 +1532,7 @@ def test_stake_with_single_hotkey_max_stake_not_enough_balance(self, _): mock_balance = Balance.from_float(1.0) # Not enough balance to do max - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -1554,7 +1540,7 @@ def test_stake_with_single_hotkey_max_stake_not_enough_balance(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(config.hotkeys) ] @@ -1639,7 +1625,7 @@ def test_stake_with_single_hotkey_max_stake_enough_stake(self, _): "hk0": Balance.from_float(config.max_stake * 2) } - mock_coldkey_kp = _get_mock_keypair(0, self.id()) + mock_coldkey_kp = get_mock_keypair(0, self.id()) mock_wallets = [ SimpleNamespace( @@ -1647,7 +1633,7 @@ def test_stake_with_single_hotkey_max_stake_enough_stake(self, _): coldkey=mock_coldkey_kp, coldkeypub=mock_coldkey_kp, hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), + hotkey=get_mock_keypair(idx + 100, self.id()), ) for idx, hk in enumerate(config.hotkeys) ] @@ -1743,10 +1729,10 @@ def test_stake_with_thresholds(self, _): mock_wallets = [ SimpleNamespace( name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), + coldkey=get_mock_keypair(idx, self.id()), + coldkeypub=get_mock_keypair(idx, self.id()), hotkey_str="hk{}".format(idx), # doesn't matter - hotkey=_get_mock_keypair(idx + 100, self.id()), # doesn't matter + hotkey=get_mock_keypair(idx + 100, self.id()), # doesn't matter ) for idx, wallet_name in enumerate(wallet_names) ] @@ -1857,10 +1843,10 @@ def test_nominate(self, _): mock_wallet = SimpleNamespace( name="w0", - coldkey=_get_mock_keypair(0, self.id()), - coldkeypub=_get_mock_keypair(0, self.id()), + coldkey=get_mock_keypair(0, self.id()), + coldkeypub=get_mock_keypair(0, self.id()), hotkey_str="hk0", - hotkey=_get_mock_keypair(0 + 100, self.id()), + hotkey=get_mock_keypair(0 + 100, self.id()), ) # Register mock wallet and give it a balance @@ -1921,10 +1907,10 @@ def test_delegate_stake(self, _): for idx_hk, hk in enumerate(list(mock_balances[wallet_name].keys())): wallet = SimpleNamespace( name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), + coldkey=get_mock_keypair(idx, self.id()), + coldkeypub=get_mock_keypair(idx, self.id()), hotkey_str=hk, - hotkey=_get_mock_keypair(idx * 100 + idx_hk, self.id()), + hotkey=get_mock_keypair(idx * 100 + idx_hk, self.id()), ) mock_wallets.append(wallet) @@ -2007,10 +1993,10 @@ def test_undelegate_stake(self, _): for idx_hk, hk in enumerate(list(mock_balances[wallet_name].keys())): wallet = SimpleNamespace( name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), + coldkey=get_mock_keypair(idx, self.id()), + coldkeypub=get_mock_keypair(idx, self.id()), hotkey_str=hk, - hotkey=_get_mock_keypair(idx * 100 + idx_hk, self.id()), + hotkey=get_mock_keypair(idx * 100 + idx_hk, self.id()), ) mock_wallets.append(wallet) @@ -2104,8 +2090,8 @@ def test_transfer(self, _): for idx, wallet_name in enumerate(list(mock_balances.keys())): wallet = SimpleNamespace( name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), + coldkey=get_mock_keypair(idx, self.id()), + coldkeypub=get_mock_keypair(idx, self.id()), ) mock_wallets.append(wallet) @@ -2172,8 +2158,8 @@ def test_transfer_not_enough_balance(self, _): for idx, wallet_name in enumerate(list(mock_balances.keys())): wallet = SimpleNamespace( name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), + coldkey=get_mock_keypair(idx, self.id()), + coldkeypub=get_mock_keypair(idx, self.id()), ) mock_wallets.append(wallet) @@ -2240,7 +2226,7 @@ def test_register(self, _): config.subcommand = "register" config.no_prompt = True - mock_wallet = generate_wallet(hotkey=_get_mock_keypair(100, self.id())) + mock_wallet = generate_wallet(hotkey=get_mock_keypair(100, self.id())) # Give the wallet some balance for burning success, err = _subtensor_mock.force_set_balance( @@ -2271,7 +2257,7 @@ def test_pow_register(self, _): config.pow_register.update_interval = 50_000 config.no_prompt = True - mock_wallet = generate_wallet(hotkey=_get_mock_keypair(100, self.id())) + mock_wallet = generate_wallet(hotkey=get_mock_keypair(100, self.id())) class MockException(Exception): pass @@ -2302,7 +2288,7 @@ def test_stake(self, _): subtensor = bittensor.subtensor(config) - mock_wallet = generate_wallet(hotkey=_get_mock_keypair(100, self.id())) + mock_wallet = generate_wallet(hotkey=get_mock_keypair(100, self.id())) # Register the hotkey and give it some balance _subtensor_mock.force_register_neuron( @@ -2345,8 +2331,8 @@ def test_metagraph(self, _): def register_mock_neuron(i: int) -> int: mock_nn.append( SimpleNamespace( - hotkey=_get_mock_keypair(i + 100, self.id()).ss58_address, - coldkey=_get_mock_keypair(i, self.id()).ss58_address, + hotkey=get_mock_keypair(i + 100, self.id()).ss58_address, + coldkey=get_mock_keypair(i, self.id()).ss58_address, balance=Balance.from_rao(random.randint(0, 2**45)).rao, stake=Balance.from_rao(random.randint(0, 2**45)).rao, ) @@ -2453,8 +2439,8 @@ def test_delegate(self, _): """ Test delegate add command """ - mock_wallet = generate_wallet(hotkey=_get_mock_keypair(100, self.id())) - delegate_wallet = generate_wallet(hotkey=_get_mock_keypair(100 + 1, self.id())) + mock_wallet = generate_wallet(hotkey=get_mock_keypair(100, self.id())) + delegate_wallet = generate_wallet(hotkey=get_mock_keypair(100 + 1, self.id())) # register the wallet _ = _subtensor_mock.force_register_neuron( diff --git a/tests/unit_tests/extrinsics/test_delegation.py b/tests/unit_tests/extrinsics/test_delegation.py index 42dcf4e70..ac107413c 100644 --- a/tests/unit_tests/extrinsics/test_delegation.py +++ b/tests/unit_tests/extrinsics/test_delegation.py @@ -1,19 +1,38 @@ -import pytest +# 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. + from unittest.mock import MagicMock, patch -from bittensor.subtensor import Subtensor -from bittensor.wallet import wallet as Wallet -from bittensor.utils.balance import Balance -from bittensor.extrinsics.delegation import ( - nominate_extrinsic, - delegate_extrinsic, - undelegate_extrinsic, -) + +import pytest +from bittensor_wallet.wallet import Wallet + from bittensor.errors import ( NominationError, NotDelegateError, NotRegisteredError, StakeError, ) +from bittensor.extrinsics.delegation import ( + nominate_extrinsic, + delegate_extrinsic, + undelegate_extrinsic, +) +from bittensor.subtensor import Subtensor +from bittensor.utils.balance import Balance @pytest.fixture diff --git a/tests/unit_tests/extrinsics/test_network.py b/tests/unit_tests/extrinsics/test_network.py index 67df030ff..fb516d4ae 100644 --- a/tests/unit_tests/extrinsics/test_network.py +++ b/tests/unit_tests/extrinsics/test_network.py @@ -1,11 +1,30 @@ -import pytest +# 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. + from unittest.mock import MagicMock, patch -from bittensor.subtensor import Subtensor -from bittensor.wallet import wallet as Wallet + +import pytest +from bittensor_wallet import Wallet + from bittensor.extrinsics.network import ( set_hyperparameter_extrinsic, register_subnetwork_extrinsic, ) +from bittensor.subtensor import Subtensor # Mock the bittensor and related modules to avoid real network calls and wallet operations diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index 7d9c975fb..8eca832d7 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -1,9 +1,28 @@ -import pytest +# 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. + from unittest.mock import MagicMock, patch + +import pytest +from bittensor_wallet import Wallet + import bittensor -from bittensor.subtensor import Subtensor -from bittensor.wallet import wallet as Wallet from bittensor.extrinsics.prometheus import prometheus_extrinsic +from bittensor.subtensor import Subtensor # Mocking the bittensor and networking modules diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index 49805f0cf..aed33553d 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -1,8 +1,25 @@ -import pytest +# 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. + from unittest.mock import MagicMock, patch -from bittensor.subtensor import Subtensor -from bittensor.wallet import wallet as Wallet -from bittensor.utils.registration import POWSolution + +import pytest +from bittensor_wallet import Wallet + from bittensor.extrinsics.registration import ( MaxSuccessException, MaxAttemptsException, @@ -10,6 +27,8 @@ burned_register_extrinsic, register_extrinsic, ) +from bittensor.subtensor import Subtensor +from bittensor.utils.registration import POWSolution # Mocking external dependencies diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 7aa3ebf5b..7d8c75e35 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -1,35 +1,53 @@ -import pytest +# 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. from unittest.mock import MagicMock, patch -from bittensor.subtensor import Subtensor -from bittensor.wallet import wallet as Wallet + +import pytest +from bittensor_wallet import Wallet + from bittensor.axon import axon as Axon from bittensor.extrinsics.serving import ( serve_extrinsic, publish_metadata, serve_axon_extrinsic, ) +from bittensor.subtensor import Subtensor @pytest.fixture -def mock_subtensor(): - mock_subtensor = MagicMock(spec=Subtensor) +def mock_subtensor(mocker): + mock_subtensor = mocker.MagicMock(spec=Subtensor) mock_subtensor.network = "test_network" - mock_subtensor.substrate = MagicMock() + mock_subtensor.substrate = mocker.MagicMock() return mock_subtensor @pytest.fixture -def mock_wallet(): - wallet = MagicMock(spec=Wallet) +def mock_wallet(mocker): + wallet = mocker.MagicMock(spec=Wallet) wallet.hotkey.ss58_address = "hotkey_address" wallet.coldkeypub.ss58_address = "coldkey_address" return wallet @pytest.fixture -def mock_axon(mock_wallet): - axon = MagicMock(spec=Axon) +def mock_axon(mock_wallet, mocker): + axon = mocker.MagicMock(spec=Axon) axon.wallet = mock_wallet() axon.external_port = 9221 return axon diff --git a/tests/unit_tests/test_keyfile.py b/tests/unit_tests/test_keyfile.py deleted file mode 100644 index 8db105c3b..000000000 --- a/tests/unit_tests/test_keyfile.py +++ /dev/null @@ -1,626 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2022 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 os -import json -import time -import pytest -import shutil -import bittensor -import unittest.mock as mock -from scalecodec import ScaleBytes -from substrateinterface import Keypair, KeypairType -from substrateinterface.constants import DEV_PHRASE -from substrateinterface.exceptions import ConfigurationError -from bip39 import bip39_validate - -from bittensor import get_coldkey_password_from_environment - - -def test_generate_mnemonic(): - """ - Test the generation of a mnemonic and its validation. - """ - mnemonic = Keypair.generate_mnemonic() - assert bip39_validate(mnemonic) == True - - -def test_invalid_mnemonic(): - """ - Test the validation of an invalid mnemonic. - """ - mnemonic = "This is an invalid mnemonic" - assert bip39_validate(mnemonic) == False - - -def test_create_sr25519_keypair(): - """ - Test the creation of a sr25519 keypair from a mnemonic and verify the SS58 address. - """ - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - keypair = Keypair.create_from_mnemonic(mnemonic, ss58_format=0) - assert keypair.ss58_address == "16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2" - - -def test_only_provide_ss58_address(): - """ - Test the creation of a keypair with only the SS58 address provided. - """ - keypair = Keypair(ss58_address="16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2") - - assert ( - f"0x{keypair.public_key.hex()}" - == "0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446" - ) - - -def test_only_provide_public_key(): - """ - Test the creation of a keypair with only the public key provided. - """ - keypair = Keypair( - public_key="0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446", - ss58_format=0, - ) - - assert keypair.ss58_address == "16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2" - - -def test_provide_no_ss58_address_and_public_key(): - """ - Test the creation of a keypair without providing SS58 address and public key. - """ - with pytest.raises(ValueError): - Keypair() - - -def test_incorrect_private_key_length_sr25519(): - """ - Test the creation of a keypair with an incorrect private key length for sr25519. - """ - with pytest.raises(ValueError): - Keypair( - private_key="0x23", - ss58_address="16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2", - ) - - -def test_incorrect_public_key(): - """ - Test the creation of a keypair with an incorrect public key. - """ - with pytest.raises(ValueError): - Keypair(public_key="0x23") - - -def test_sign_and_verify(): - """ - Test the signing and verification of a message using a keypair. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("Test1231223123123") - assert keypair.verify("Test1231223123123", signature) == True - - -def test_sign_and_verify_hex_data(): - """ - Test the signing and verification of hex data using a keypair. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("0x1234") - assert keypair.verify("0x1234", signature) == True - - -def test_sign_and_verify_scale_bytes(): - """ - Test the signing and verification of ScaleBytes data using a keypair. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - data = ScaleBytes("0x1234") - signature = keypair.sign(data) - assert keypair.verify(data, signature) == True - - -def test_sign_missing_private_key(): - """ - Test signing a message with a keypair that is missing the private key. - """ - keypair = Keypair(ss58_address="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY") - with pytest.raises(ConfigurationError): - keypair.sign("0x1234") - - -def test_sign_unsupported_crypto_type(): - """ - Test signing a message with an unsupported crypto type. - """ - keypair = Keypair.create_from_private_key( - ss58_address="16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2", - private_key="0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e", - crypto_type=3, - ) - with pytest.raises(ConfigurationError): - keypair.sign("0x1234") - - -def test_verify_unsupported_crypto_type(): - """ - Test verifying a signature with an unsupported crypto type. - """ - keypair = Keypair.create_from_private_key( - ss58_address="16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2", - private_key="0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e", - crypto_type=3, - ) - with pytest.raises(ConfigurationError): - keypair.verify("0x1234", "0x1234") - - -def test_sign_and_verify_incorrect_signature(): - """ - Test verifying an incorrect signature for a signed message. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = "0x4c291bfb0bb9c1274e86d4b666d13b2ac99a0bacc04a4846fb8ea50bda114677f83c1f164af58fc184451e5140cc8160c4de626163b11451d3bbb208a1889f8a" - assert keypair.verify("Test1231223123123", signature) == False - - -def test_sign_and_verify_invalid_signature(): - """ - Test verifying an invalid signature format for a signed message. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = "Test" - with pytest.raises(TypeError): - keypair.verify("Test1231223123123", signature) - - -def test_sign_and_verify_invalid_message(): - """ - Test verifying a signature against an incorrect message. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("Test1231223123123") - assert keypair.verify("OtherMessage", signature) == False - - -def test_create_ed25519_keypair(): - """ - Test the creation of an ed25519 keypair from a mnemonic and verify the SS58 address. - """ - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - keypair = Keypair.create_from_mnemonic( - mnemonic, ss58_format=0, crypto_type=KeypairType.ED25519 - ) - assert keypair.ss58_address == "16dYRUXznyhvWHS1ktUENGfNAEjCawyDzHRtN9AdFnJRc38h" - - -def test_sign_and_verify_ed25519(): - """ - Test the signing and verification of a message using an ed25519 keypair. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=KeypairType.ED25519) - signature = keypair.sign("Test1231223123123") - assert keypair.verify("Test1231223123123", signature) == True - - -def test_sign_and_verify_invalid_signature_ed25519(): - """ - Test verifying an incorrect signature for a message signed with an ed25519 keypair. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=KeypairType.ED25519) - signature = "0x4c291bfb0bb9c1274e86d4b666d13b2ac99a0bacc04a4846fb8ea50bda114677f83c1f164af58fc184451e5140cc8160c4de626163b11451d3bbb208a1889f8a" - assert keypair.verify("Test1231223123123", signature) == False - - -def test_unsupport_crypto_type(): - """ - Test creating a keypair with an unsupported crypto type. - """ - with pytest.raises(ValueError): - Keypair.create_from_seed( - seed_hex="0xda3cf5b1e9144931?a0f0db65664aab662673b099415a7f8121b7245fb0be4143", - crypto_type=2, - ) - - -def test_create_keypair_from_private_key(): - """ - Test creating a keypair from a private key and verify the public key. - """ - keypair = Keypair.create_from_private_key( - ss58_address="16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2", - private_key="0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e", - ) - assert ( - f"0x{keypair.public_key.hex()}" - == "0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446" - ) - - -def test_hdkd_hard_path(): - """ - Test hierarchical deterministic key derivation with a hard derivation path. - """ - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - derivation_address = "5FEiH8iuDUw271xbqWTWuB6WrDjv5dnCeDX1CyHubAniXDNN" - derivation_path = "//Alice" - derived_keypair = Keypair.create_from_uri(mnemonic + derivation_path) - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_soft_path(): - """ - Test hierarchical deterministic key derivation with a soft derivation path. - """ - derivation_address = "5GNXbA46ma5dg19GXdiKi5JH3mnkZ8Yea3bBtZAvj7t99P9i" - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - derived_keypair = Keypair.create_from_uri(f"{mnemonic}/Alice") - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_default_to_dev_mnemonic(): - """ - Test hierarchical deterministic key derivation with a default development mnemonic. - """ - derivation_address = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" - derivation_path = "//Alice" - derived_keypair = Keypair.create_from_uri(derivation_path) - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_nested_hard_soft_path(): - """ - Test hierarchical deterministic key derivation with a nested hard and soft derivation path. - """ - derivation_address = "5CJGwWiKXSE16WJaxBdPZhWqUYkotgenLUALv7ZvqQ4TXeqf" - derivation_path = "//Bob/test" - derived_keypair = Keypair.create_from_uri(derivation_path) - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_nested_soft_hard_path(): - """ - Test hierarchical deterministic key derivation with a nested soft and hard derivation path. - """ - derivation_address = "5Cwc8tShrshDJUp1P1M21dKUTcYQpV9GcfSa4hUBNmMdV3Cx" - derivation_path = "/Bob//test" - derived_keypair = Keypair.create_from_uri(derivation_path) - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_path_gt_32_bytes(): - """ - Test hierarchical deterministic key derivation with a derivation path longer than 32 bytes. - """ - derivation_address = "5GR5pfZeNs1uQiSWVxZaQiZou3wdZiX894eqgvfNfHbEh7W2" - derivation_path = "//PathNameLongerThan32BytesWhichShouldBeHashed" - derived_keypair = Keypair.create_from_uri(derivation_path) - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_unsupported_password(): - """ - Test hierarchical deterministic key derivation with an unsupported password. - """ - - with pytest.raises(NotImplementedError): - Keypair.create_from_uri(f"{DEV_PHRASE}///test") - - -def create_keyfile(root_path): - """ - Creates a keyfile object with two keypairs: alice and bob. - - Args: - root_path (str): The root path for the keyfile. - - Returns: - bittensor.keyfile: The created keyfile object. - """ - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - - mnemonic = bittensor.Keypair.generate_mnemonic(12) - alice = bittensor.Keypair.create_from_mnemonic(mnemonic) - keyfile.set_keypair( - alice, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - - bob = bittensor.Keypair.create_from_uri("/Bob") - keyfile.set_keypair( - bob, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - - return keyfile - - -@pytest.fixture(scope="session") -def keyfile_setup_teardown(): - root_path = f"/tmp/pytest{time.time()}" - os.makedirs(root_path, exist_ok=True) - - create_keyfile(root_path) - - yield root_path - - shutil.rmtree(root_path) - - -def test_create(keyfile_setup_teardown): - """ - Test case for creating a keyfile and performing various operations on it. - """ - root_path = keyfile_setup_teardown - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - - mnemonic = bittensor.Keypair.generate_mnemonic(12) - alice = bittensor.Keypair.create_from_mnemonic(mnemonic) - keyfile.set_keypair( - alice, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - assert keyfile.is_readable() - assert keyfile.is_writable() - assert keyfile.is_encrypted() - keyfile.decrypt(password="thisisafakepassword") - assert not keyfile.is_encrypted() - keyfile.encrypt(password="thisisafakepassword") - assert keyfile.is_encrypted() - str(keyfile) - keyfile.decrypt(password="thisisafakepassword") - assert not keyfile.is_encrypted() - str(keyfile) - - assert ( - keyfile.get_keypair(password="thisisafakepassword").ss58_address - == alice.ss58_address - ) - assert ( - keyfile.get_keypair(password="thisisafakepassword").private_key - == alice.private_key - ) - assert ( - keyfile.get_keypair(password="thisisafakepassword").public_key - == alice.public_key - ) - - bob = bittensor.Keypair.create_from_uri("/Bob") - keyfile.set_keypair( - bob, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - assert ( - keyfile.get_keypair(password="thisisafakepassword").ss58_address - == bob.ss58_address - ) - assert ( - keyfile.get_keypair(password="thisisafakepassword").public_key == bob.public_key - ) - - repr(keyfile) - - -def test_legacy_coldkey(keyfile_setup_teardown): - """ - Test case for legacy cold keyfile. - """ - root_path = keyfile_setup_teardown - legacy_filename = os.path.join(root_path, "coldlegacy_keyfile") - keyfile = bittensor.keyfile(path=legacy_filename) - keyfile.make_dirs() - keyfile_data = b"0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - with open(legacy_filename, "wb") as keyfile_obj: - keyfile_obj.write(keyfile_data) - assert keyfile.keyfile_data == keyfile_data - keyfile.encrypt(password="this is the fake password") - keyfile.decrypt(password="this is the fake password") - expected_decryption = { - "accountId": "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f", - "publicKey": "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f", - "privateKey": None, - "secretPhrase": None, - "secretSeed": None, - "ss58Address": "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm", - } - for key, value in expected_decryption.items(): - value_str = f'"{value}"' if value is not None else "null" - assert f'"{key}": {value_str}'.encode() in keyfile.keyfile_data - - assert ( - keyfile.get_keypair().ss58_address - == "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - ) - assert ( - f"0x{keyfile.get_keypair().public_key.hex()}" - == "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - ) - - -def test_validate_password(): - """ - Test case for the validate_password function. - - This function tests the behavior of the validate_password function from the bittensor.keyfile module. - It checks various scenarios to ensure that the function correctly validates passwords. - """ - from bittensor.keyfile import validate_password - - assert validate_password(None) == False - assert validate_password("passw0rd") == False - assert validate_password("123456789") == False - with mock.patch("getpass.getpass", return_value="biTTensor"): - assert validate_password("biTTensor") == True - with mock.patch("getpass.getpass", return_value="biTTenso"): - assert validate_password("biTTensor") == False - - -def test_decrypt_keyfile_data_legacy(): - """ - Test case for decrypting legacy keyfile data. - - This test case verifies that the `decrypt_keyfile_data` function correctly decrypts - encrypted data using a legacy encryption scheme. - - The test generates a key using a password and encrypts a sample data. Then, it decrypts - the encrypted data using the same password and asserts that the decrypted data matches - the original data. - """ - import base64 - - from cryptography.fernet import Fernet - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC - - from bittensor.keyfile import decrypt_keyfile_data - - __SALT = b"Iguesscyborgslikemyselfhaveatendencytobeparanoidaboutourorigins" - - def __generate_key(password): - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - salt=__SALT, - length=32, - iterations=10000000, - backend=default_backend(), - ) - key = base64.urlsafe_b64encode(kdf.derive(password.encode())) - return key - - pw = "fakepasssword238947239" - data = b"encrypt me!" - key = __generate_key(pw) - cipher_suite = Fernet(key) - encrypted_data = cipher_suite.encrypt(data) - - decrypted_data = decrypt_keyfile_data(encrypted_data, pw) - assert decrypted_data == data - - -def test_user_interface(): - """ - Test the user interface for asking password to encrypt. - - This test case uses the `ask_password_to_encrypt` function from the `bittensor.keyfile` module. - It mocks the `getpass.getpass` function to simulate user input of passwords. - The expected result is that the `ask_password_to_encrypt` function returns the correct password. - """ - from bittensor.keyfile import ask_password_to_encrypt - - with mock.patch( - "getpass.getpass", - side_effect=["pass", "password", "asdury3294y", "asdury3294y"], - ): - assert ask_password_to_encrypt() == "asdury3294y" - - -def test_overwriting(keyfile_setup_teardown): - """ - Test case for overwriting a keypair in the keyfile. - """ - root_path = keyfile_setup_teardown - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - alice = bittensor.Keypair.create_from_uri("/Alice") - keyfile.set_keypair( - alice, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - bob = bittensor.Keypair.create_from_uri("/Bob") - - with pytest.raises(bittensor.KeyFileError) as pytest_wrapped_e: - with mock.patch("builtins.input", return_value="n"): - keyfile.set_keypair( - bob, encrypt=True, overwrite=False, password="thisisafakepassword" - ) - - -def test_serialized_keypair_to_keyfile_data(keyfile_setup_teardown): - """ - Test case for serializing a keypair to keyfile data. - - This test case verifies that the `serialized_keypair_to_keyfile_data` function correctly - serializes a keypair to keyfile data. It then deserializes the keyfile data and asserts - that the deserialized keypair matches the original keypair. - """ - from bittensor.keyfile import serialized_keypair_to_keyfile_data - - root_path = keyfile_setup_teardown - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - - mnemonic = bittensor.Keypair.generate_mnemonic(12) - keypair = bittensor.Keypair.create_from_mnemonic(mnemonic) - - keyfile.set_keypair( - keypair, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - keypair_data = serialized_keypair_to_keyfile_data(keypair) - decoded_keypair_data = json.loads(keypair_data.decode()) - - assert decoded_keypair_data["secretPhrase"] == keypair.mnemonic - assert decoded_keypair_data["ss58Address"] == keypair.ss58_address - assert decoded_keypair_data["publicKey"] == f"0x{keypair.public_key.hex()}" - assert decoded_keypair_data["accountId"] == f"0x{keypair.public_key.hex()}" - - -def test_deserialize_keypair_from_keyfile_data(keyfile_setup_teardown): - """ - Test case for deserializing a keypair from keyfile data. - - This test case verifies that the `deserialize_keypair_from_keyfile_data` function correctly - deserializes keyfile data to a keypair. It first serializes a keypair to keyfile data and - then deserializes the keyfile data to a keypair. It then asserts that the deserialized keypair - matches the original keypair. - """ - from bittensor.keyfile import serialized_keypair_to_keyfile_data - from bittensor.keyfile import deserialize_keypair_from_keyfile_data - - root_path = keyfile_setup_teardown - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - - mnemonic = bittensor.Keypair.generate_mnemonic(12) - keypair = bittensor.Keypair.create_from_mnemonic(mnemonic) - - keyfile.set_keypair( - keypair, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - keypair_data = serialized_keypair_to_keyfile_data(keypair) - deserialized_keypair = deserialize_keypair_from_keyfile_data(keypair_data) - - assert deserialized_keypair.ss58_address == keypair.ss58_address - assert deserialized_keypair.public_key == keypair.public_key - assert deserialized_keypair.private_key == keypair.private_key - - -def test_get_coldkey_password_from_environment(monkeypatch): - password_by_wallet = { - "WALLET": "password", - "my_wallet": "password2", - "my-wallet": "password2", - } - - monkeypatch.setenv("bt_cold_pw_wallet", password_by_wallet["WALLET"]) - monkeypatch.setenv("BT_COLD_PW_My_Wallet", password_by_wallet["my_wallet"]) - - for wallet, password in password_by_wallet.items(): - assert get_coldkey_password_from_environment(wallet) == password - - assert get_coldkey_password_from_environment("non_existent_wallet") is None diff --git a/tests/unit_tests/test_wallet.py b/tests/unit_tests/test_wallet.py deleted file mode 100644 index 0d0466e34..000000000 --- a/tests/unit_tests/test_wallet.py +++ /dev/null @@ -1,517 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2022 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 json -import time -import pytest -import random -import re -import bittensor -from bittensor.errors import KeyFileError -from rich.prompt import Confirm -from ansible_vault import Vault -from unittest.mock import patch - - -def legacy_encrypt_keyfile_data(keyfile_data: bytes, password: str = None) -> bytes: - console = bittensor.__console__ - with console.status(":locked_with_key: Encrypting key..."): - vault = Vault(password) - return vault.vault.encrypt(keyfile_data) - - -def create_wallet(default_updated_password): - # create an nacl wallet - wallet = bittensor.wallet( - name=f"mock-{str(time.time())}", - path="/tmp/tests_wallets/do_not_use", - ) - with patch.object( - bittensor, - "ask_password_to_encrypt", - return_value=default_updated_password, - ): - wallet.create() - assert "NaCl" in str(wallet.coldkey_file) - - return wallet - - -def create_legacy_wallet(default_legacy_password=None, legacy_password=None): - def _legacy_encrypt_keyfile_data(*args, **kwargs): - args = { - k: v - for k, v in zip( - legacy_encrypt_keyfile_data.__code__.co_varnames[: len(args)], - args, - ) - } - kwargs = {**args, **kwargs} - kwargs["password"] = legacy_password - return legacy_encrypt_keyfile_data(**kwargs) - - legacy_wallet = bittensor.wallet( - name=f"mock-legacy-{str(time.time())}", - path="/tmp/tests_wallets/do_not_use", - ) - legacy_password = ( - default_legacy_password if legacy_password == None else legacy_password - ) - - # create a legacy ansible wallet - with patch.object( - bittensor, - "encrypt_keyfile_data", - new=_legacy_encrypt_keyfile_data, - # new = TestWalletUpdate.legacy_encrypt_keyfile_data, - ): - legacy_wallet.create() - assert "Ansible" in str(legacy_wallet.coldkey_file) - - return legacy_wallet - - -@pytest.fixture -def wallet_update_setup(): - # Setup the default passwords and wallets - default_updated_password = "nacl_password" - default_legacy_password = "ansible_password" - empty_wallet = bittensor.wallet( - name=f"mock-empty-{str(time.time())}", - path="/tmp/tests_wallets/do_not_use", - ) - legacy_wallet = create_legacy_wallet( - default_legacy_password=default_legacy_password - ) - wallet = create_wallet(default_updated_password) - - return { - "default_updated_password": default_updated_password, - "default_legacy_password": default_legacy_password, - "empty_wallet": empty_wallet, - "legacy_wallet": legacy_wallet, - "wallet": wallet, - } - - -def test_encrypt_and_decrypt(): - """Test message can be encrypted and decrypted successfully with ansible/nacl.""" - json_data = { - "address": "This is the address.", - "id": "This is the id.", - "key": "This is the key.", - } - message = json.dumps(json_data).encode() - - # encrypt and decrypt with nacl - encrypted_message = bittensor.encrypt_keyfile_data(message, "password") - decrypted_message = bittensor.decrypt_keyfile_data(encrypted_message, "password") - assert decrypted_message == message - assert bittensor.keyfile_data_is_encrypted(encrypted_message) - assert not bittensor.keyfile_data_is_encrypted(decrypted_message) - assert not bittensor.keyfile_data_is_encrypted_ansible(decrypted_message) - assert bittensor.keyfile_data_is_encrypted_nacl(encrypted_message) - - # encrypt and decrypt with legacy ansible - encrypted_message = legacy_encrypt_keyfile_data(message, "password") - decrypted_message = bittensor.decrypt_keyfile_data(encrypted_message, "password") - assert decrypted_message == message - assert bittensor.keyfile_data_is_encrypted(encrypted_message) - assert not bittensor.keyfile_data_is_encrypted(decrypted_message) - assert not bittensor.keyfile_data_is_encrypted_nacl(decrypted_message) - assert bittensor.keyfile_data_is_encrypted_ansible(encrypted_message) - - -def test_check_and_update_encryption_not_updated(wallet_update_setup): - """Test for a few cases where wallet should not be updated. - 1. When the wallet is already updated. - 2. When it is the hotkey. - 3. When the wallet is empty. - 4. When the wallet is legacy but no prompt to ask for password. - 5. When the password is wrong. - """ - wallet = wallet_update_setup["wallet"] - empty_wallet = wallet_update_setup["empty_wallet"] - legacy_wallet = wallet_update_setup["legacy_wallet"] - default_legacy_password = wallet_update_setup["default_legacy_password"] - # test the checking with no rewriting needs to be done. - with patch("bittensor.encrypt_keyfile_data") as encrypt: - # self.wallet is already the most updated with nacl encryption. - assert wallet.coldkey_file.check_and_update_encryption() - - # hotkey_file is not encrypted, thus do not need to be updated. - assert not wallet.hotkey_file.check_and_update_encryption() - - # empty_wallet has not been created, thus do not need to be updated. - assert not empty_wallet.coldkey_file.check_and_update_encryption() - - # legacy wallet cannot be updated without asking for password form prompt. - assert not legacy_wallet.coldkey_file.check_and_update_encryption( - no_prompt=True - ) - - # Wrong password - legacy_wallet = create_legacy_wallet( - default_legacy_password=default_legacy_password - ) - with patch("getpass.getpass", return_value="wrong_password"), patch.object( - Confirm, "ask", return_value=False - ): - assert not legacy_wallet.coldkey_file.check_and_update_encryption() - - # no renewal has been done in this test. - assert not encrypt.called - - -def test_check_and_update_excryption(wallet_update_setup, legacy_wallet=None): - """Test for the alignment of the updated VS old wallet. - 1. Same coldkey_file data. - 2. Same coldkey path. - 3. Same hotkey_file data. - 4. Same hotkey path. - 5. same password. - - Read the updated wallet in 2 ways. - 1. Directly as the output of check_and_update_encryption() - 2. Read from file using the same coldkey and hotkey name - """ - default_legacy_password = wallet_update_setup["default_legacy_password"] - - def check_new_coldkey_file(keyfile): - new_keyfile_data = keyfile._read_keyfile_data_from_file() - new_decrypted_keyfile_data = bittensor.decrypt_keyfile_data( - new_keyfile_data, legacy_password - ) - new_path = legacy_wallet.coldkey_file.path - - assert old_coldkey_file_data != None - assert new_keyfile_data != None - assert not old_coldkey_file_data == new_keyfile_data - assert bittensor.keyfile_data_is_encrypted_ansible(old_coldkey_file_data) - assert bittensor.keyfile_data_is_encrypted_nacl(new_keyfile_data) - assert not bittensor.keyfile_data_is_encrypted_nacl(old_coldkey_file_data) - assert not bittensor.keyfile_data_is_encrypted_ansible(new_keyfile_data) - assert old_decrypted_coldkey_file_data == new_decrypted_keyfile_data - assert new_path == old_coldkey_path - - def check_new_hotkey_file(keyfile): - new_keyfile_data = keyfile._read_keyfile_data_from_file() - new_path = legacy_wallet.hotkey_file.path - - assert old_hotkey_file_data == new_keyfile_data - assert new_path == old_hotkey_path - assert not bittensor.keyfile_data_is_encrypted(new_keyfile_data) - - if legacy_wallet == None: - legacy_password = f"PASSword-{random.randint(0, 10000)}" - legacy_wallet = create_legacy_wallet(legacy_password=legacy_password) - - else: - legacy_password = default_legacy_password - - # get old cold keyfile data - old_coldkey_file_data = legacy_wallet.coldkey_file._read_keyfile_data_from_file() - old_decrypted_coldkey_file_data = bittensor.decrypt_keyfile_data( - old_coldkey_file_data, legacy_password - ) - old_coldkey_path = legacy_wallet.coldkey_file.path - - # get old hot keyfile data - old_hotkey_file_data = legacy_wallet.hotkey_file._read_keyfile_data_from_file() - old_hotkey_path = legacy_wallet.hotkey_file.path - - # update legacy_wallet from ansible to nacl - with patch("getpass.getpass", return_value=legacy_password), patch.object( - Confirm, "ask", return_value=True - ): - legacy_wallet.coldkey_file.check_and_update_encryption() - - # get new keyfile data from the same legacy wallet - check_new_coldkey_file(legacy_wallet.coldkey_file) - check_new_hotkey_file(legacy_wallet.hotkey_file) - - # get new keyfile data from wallet name - updated_legacy_wallet = bittensor.wallet( - name=legacy_wallet.name, - hotkey=legacy_wallet.hotkey_str, - path="/tmp/tests_wallets/do_not_use", - ) - check_new_coldkey_file(updated_legacy_wallet.coldkey_file) - check_new_hotkey_file(updated_legacy_wallet.hotkey_file) - - # def test_password_retain(self): - # [tick] test the same password works - # [tick] try to read using the same hotkey/coldkey name - # [tick] test the same keyfile data could be retained - # [tick] test what if a wrong password was inserted - # [no need] try to read from the new file path - # [tick] test the old and new encrypted is not the same - # [tick] test that the hotkeys are not affected - - -@pytest.fixture -def mock_wallet(): - wallet = bittensor.wallet( - name=f"mock-{str(time.time())}", - hotkey=f"mock-{str(time.time())}", - path="/tmp/tests_wallets/do_not_use", - ) - wallet.create_new_coldkey(use_password=False, overwrite=True, suppress=True) - wallet.create_new_hotkey(use_password=False, overwrite=True, suppress=True) - - return wallet - - -def test_regen_coldkeypub_from_ss58_addr(mock_wallet): - """Test the `regenerate_coldkeypub` method of the wallet class, which regenerates the cold key pair from an SS58 address. - It checks whether the `set_coldkeypub` method is called with the expected arguments, and verifies that the generated key pair's SS58 address matches the input SS58 address. - It also tests the behavior when an invalid SS58 address is provided, raising a `ValueError` as expected. - """ - ss58_address = "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - with patch.object(mock_wallet, "set_coldkeypub") as mock_set_coldkeypub: - mock_wallet.regenerate_coldkeypub( - ss58_address=ss58_address, overwrite=True, suppress=True - ) - - mock_set_coldkeypub.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkeypub.call_args_list[0][0][0] - assert keypair.ss58_address == ss58_address - - ss58_address_bad = ( - "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zx" # 1 character short - ) - with pytest.raises(ValueError): - mock_wallet.regenerate_coldkeypub( - ss58_address=ss58_address_bad, overwrite=True, suppress=True - ) - - -def test_regen_coldkeypub_from_hex_pubkey_str(mock_wallet): - """Test the `regenerate_coldkeypub` method of the wallet class, which regenerates the cold key pair from a hex public key string. - It checks whether the `set_coldkeypub` method is called with the expected arguments, and verifies that the generated key pair's public key matches the input public key. - It also tests the behavior when an invalid public key string is provided, raising a `ValueError` as expected. - """ - pubkey_str = "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - with patch.object(mock_wallet, "set_coldkeypub") as mock_set_coldkeypub: - mock_wallet.regenerate_coldkeypub( - public_key=pubkey_str, overwrite=True, suppress=True - ) - - mock_set_coldkeypub.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkeypub.call_args_list[0][0][0] - assert "0x" + keypair.public_key.hex() == pubkey_str - - pubkey_str_bad = "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512" # 1 character short - with pytest.raises(ValueError): - mock_wallet.regenerate_coldkeypub( - ss58_address=pubkey_str_bad, overwrite=True, suppress=True - ) - - -def test_regen_coldkeypub_from_hex_pubkey_bytes(mock_wallet): - """Test the `regenerate_coldkeypub` method of the wallet class, which regenerates the cold key pair from a hex public key byte string. - It checks whether the `set_coldkeypub` method is called with the expected arguments, and verifies that the generated key pair's public key matches the input public key. - """ - pubkey_str = "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - pubkey_bytes = bytes.fromhex(pubkey_str[2:]) # Remove 0x from beginning - with patch.object(mock_wallet, "set_coldkeypub") as mock_set_coldkeypub: - mock_wallet.regenerate_coldkeypub( - public_key=pubkey_bytes, overwrite=True, suppress=True - ) - - mock_set_coldkeypub.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkeypub.call_args_list[0][0][0] - assert keypair.public_key == pubkey_bytes - - -def test_regen_coldkeypub_no_pubkey(mock_wallet): - """Test the `regenerate_coldkeypub` method of the wallet class when no public key is provided. - It verifies that a `ValueError` is raised when neither a public key nor an SS58 address is provided. - """ - with pytest.raises(ValueError): - # Must provide either public_key or ss58_address - mock_wallet.regenerate_coldkeypub( - ss58_address=None, public_key=None, overwrite=True, suppress=True - ) - - -def test_regen_coldkey_from_hex_seed_str(mock_wallet): - """Test the `regenerate_coldkey` method of the wallet class, which regenerates the cold key pair from a hex seed string. - It checks whether the `set_coldkey` method is called with the expected arguments, and verifies that the generated key pair's seed and SS58 address match the input seed and the expected SS58 address. - It also tests the behavior when an invalid seed string is provided, raising a `ValueError` as expected. - """ - ss58_addr = "5D5cwd8DX6ij7nouVcoxDuWtJfiR1BnzCkiBVTt7DU8ft5Ta" - seed_str = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f7636" - with patch.object(mock_wallet, "set_coldkey") as mock_set_coldkey: - mock_wallet.regenerate_coldkey(seed=seed_str, overwrite=True, suppress=True) - - mock_set_coldkey.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkey.call_args_list[0][0][0] - seed_hex = ( - keypair.seed_hex - if isinstance(keypair.seed_hex, str) - else keypair.seed_hex.hex() - ) - - assert re.match( - rf"(0x|){seed_str[2:]}", seed_hex - ), "The seed_hex does not match the expected pattern" - assert ( - keypair.ss58_address == ss58_addr - ) # Check that the ss58 address is correct - - seed_str_bad = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f763" # 1 character short - with pytest.raises(ValueError): - mock_wallet.regenerate_coldkey(seed=seed_str_bad, overwrite=True, suppress=True) - - -def test_regen_hotkey_from_hex_seed_str(mock_wallet): - """Test the `regenerate_coldkey` method of the wallet class, which regenerates the cold key pair from a hex seed string. - It checks whether the `set_coldkey` method is called with the expected arguments, and verifies that the generated key pair's seed and SS58 address match the input seed and the expected SS58 address. - It also tests the behavior when an invalid seed string is provided, raising a `ValueError` as expected. - """ - ss58_addr = "5D5cwd8DX6ij7nouVcoxDuWtJfiR1BnzCkiBVTt7DU8ft5Ta" - seed_str = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f7636" - with patch.object(mock_wallet, "set_hotkey") as mock_set_hotkey: - mock_wallet.regenerate_hotkey(seed=seed_str, overwrite=True, suppress=True) - - mock_set_hotkey.assert_called_once() - keypair: bittensor.Keypair = mock_set_hotkey.call_args_list[0][0][0] - - seed_hex = ( - keypair.seed_hex - if isinstance(keypair.seed_hex, str) - else keypair.seed_hex.hex() - ) - - pattern = rf"(0x|){seed_str[2:]}" - assert re.match( - pattern, seed_hex - ), f"The seed_hex '{seed_hex}' does not match the expected pattern '{pattern}'" - assert ( - keypair.ss58_address == ss58_addr - ) # Check that the ss58 address is correct - - seed_str_bad = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f763" # 1 character short - with pytest.raises(ValueError): - mock_wallet.regenerate_hotkey(seed=seed_str_bad, overwrite=True, suppress=True) - - -@pytest.mark.parametrize( - "mnemonic, expected_exception", - [ - # Input is in a string format - ( - "fiscal prevent noise record smile believe quote front weasel book axis legal", - None, - ), - # Input is in a list format (acquired by encapsulating mnemonic arg in a string "" in the cli) - ( - [ - "fiscal prevent noise record smile believe quote front weasel book axis legal" - ], - None, - ), - # Input is in a full list format (aquired by pasting mnemonic arg simply w/o quotes in cli) - ( - [ - "fiscal", - "prevent", - "noise", - "record", - "smile", - "believe", - "quote", - "front", - "weasel", - "book", - "axis", - "legal", - ], - None, - ), - # Incomplete mnemonic - ("word1 word2 word3", ValueError), - # No mnemonic added - (None, ValueError), - ], - ids=[ - "string-format", - "list-format-thru-string", - "list-format", - "incomplete-mnemonic", - "no-mnemonic", - ], -) -def test_regen_coldkey_mnemonic(mock_wallet, mnemonic, expected_exception): - """Test the `regenerate_coldkey` method of the wallet class, which regenerates the cold key pair from a mnemonic. - We test different input formats of mnemonics and check if the function works as expected. - """ - with patch.object(mock_wallet, "set_coldkey") as mock_set_coldkey, patch.object( - mock_wallet, "set_coldkeypub" - ) as mock_set_coldkeypub: - if expected_exception: - with pytest.raises(expected_exception): - mock_wallet.regenerate_coldkey( - mnemonic=mnemonic, overwrite=True, suppress=True - ) - else: - mock_wallet.regenerate_coldkey(mnemonic=mnemonic) - mock_set_coldkey.assert_called_once() - mock_set_coldkeypub.assert_called_once() - - -@pytest.mark.parametrize( - "overwrite, user_input, expected_exception", - [ - (True, None, None), # Test with overwrite=True, no user input needed - (False, "n", True), # Test with overwrite=False and user says no, KeyFileError - (False, "y", None), # Test with overwrite=False and user says yes - ], -) -def test_regen_coldkey_overwrite_functionality( - mock_wallet, overwrite, user_input, expected_exception -): - """Test the `regenerate_coldkey` method of the wallet class, emphasizing on the overwrite functionality""" - ss58_addr = "5D5cwd8DX6ij7nouVcoxDuWtJfiR1BnzCkiBVTt7DU8ft5Ta" - seed_str = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f7636" - - with patch.object(mock_wallet, "set_coldkey") as mock_set_coldkey, patch( - "builtins.input", return_value=user_input - ): - if expected_exception: - with pytest.raises(KeyFileError): - mock_wallet.regenerate_coldkey( - seed=seed_str, overwrite=overwrite, suppress=True - ) - else: - mock_wallet.regenerate_coldkey( - seed=seed_str, overwrite=overwrite, suppress=True - ) - mock_set_coldkey.assert_called_once() - keypair = mock_set_coldkey.call_args_list[0][0][0] - seed_hex = ( - keypair.seed_hex - if isinstance(keypair.seed_hex, str) - else keypair.seed_hex.hex() - ) - assert re.match( - rf"(0x|){seed_str[2:]}", seed_hex - ), "The seed_hex does not match the expected pattern" - assert ( - keypair.ss58_address == ss58_addr - ), "The SS58 address does not match the expected address" From 41a4a56ba1b542c6acc68a43bab6f13f53f1e24a Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 13:31:33 -0700 Subject: [PATCH 011/260] Fix requirements --- requirements/prod.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements/prod.txt b/requirements/prod.txt index d9caabaa2..e52499a38 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -33,5 +33,4 @@ tqdm uvicorn wheel -# temporary btwallet dependence from repo directly instead of pypi git+https://github.com/opentensor/btwallet.git@main#egg=bittensor-wallet From f16796e62d1a99dd256427ebf9c0e1d4ec242c42 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 13:42:48 -0700 Subject: [PATCH 012/260] Fix mypy checker + ruff --- bittensor/__init__.py | 9 +++++++-- bittensor/axon.py | 27 +++++++++++++++------------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index f3fb844eb..dcb035d81 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -296,7 +296,12 @@ def debug(on: bool = True): from bittensor_wallet.errors import KeyFileError from substrateinterface import Keypair # noqa: F401 -from bittensor_wallet.config import InvalidConfigFile, DefaultConfig, Config as config, T +from bittensor_wallet.config import ( + InvalidConfigFile, + DefaultConfig, + Config as config, + T, +) from bittensor_wallet.keyfile import ( serialized_keypair_to_keyfile_data, deserialize_keypair_from_keyfile_data, @@ -311,7 +316,7 @@ def debug(on: bool = True): encrypt_keyfile_data, get_coldkey_password_from_environment, decrypt_keyfile_data, - Keyfile as keyfile + Keyfile as keyfile, ) from bittensor_wallet.wallet import display_mnemonic_msg, Wallet as wallet diff --git a/bittensor/axon.py b/bittensor/axon.py index 55db8bcea..13c60cdbd 100644 --- a/bittensor/axon.py +++ b/bittensor/axon.py @@ -333,31 +333,31 @@ def __init__( "max_workers", bittensor.defaults.axon.max_workers ) axon.check_config(config) - self.config = config # type: ignore [method-assign] + self.config = config # type: ignore # Get wallet or use default. self.wallet = wallet or bittensor.wallet() # Build axon objects. self.uuid = str(uuid.uuid1()) - self.ip = self.config.axon.ip - self.port = self.config.axon.port + self.ip = self.config.axon.ip # type: ignore + self.port = self.config.axon.port # type: ignore self.external_ip = ( - self.config.axon.external_ip - if self.config.axon.external_ip is not None + self.config.axon.external_ip # type: ignore + if self.config.axon.external_ip is not None # type: ignore else bittensor.utils.networking.get_external_ip() ) self.external_port = ( - self.config.axon.external_port - if self.config.axon.external_port is not None - else self.config.axon.port + self.config.axon.external_port # type: ignore + if self.config.axon.external_port is not None # type: ignore + else self.config.axon.port # type: ignore ) - self.full_address = str(self.config.axon.ip) + ":" + str(self.config.axon.port) + self.full_address = str(self.config.axon.ip) + ":" + str(self.config.axon.port) # type: ignore self.started = False # Build middleware self.thread_pool = bittensor.PriorityThreadPoolExecutor( - max_workers=self.config.axon.max_workers + max_workers=self.config.axon.max_workers # type: ignore ) self.nonces: Dict[str, int] = {} @@ -372,9 +372,12 @@ def __init__( self.app = FastAPI() log_level = "trace" if bittensor.logging.__trace_on__ else "critical" self.fast_config = uvicorn.Config( - self.app, host="0.0.0.0", port=self.config.axon.port, log_level=log_level + self.app, + host="0.0.0.0", + port=self.config.axon.port, # type: ignore + log_level=log_level, # type: ignore ) - self.fast_server = FastAPIThreadedServer(config=self.fast_config) + self.fast_server = FastAPIThreadedServer(config=self.fast_config) # type: ignore self.router = APIRouter() self.app.include_router(self.router) From d5d71b710d37c0658302e5c92c019e036d831d53 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 15:16:45 -0700 Subject: [PATCH 013/260] Delete cli related e2e tests --- tests/e2e_tests/__init__.py | 0 tests/e2e_tests/conftest.py | 85 --- tests/e2e_tests/multistep/__init__.py | 0 tests/e2e_tests/multistep/test_axon.py | 107 ---- tests/e2e_tests/multistep/test_dendrite.py | 161 ------ tests/e2e_tests/multistep/test_emissions.py | 276 --------- tests/e2e_tests/multistep/test_incentive.py | 242 -------- .../e2e_tests/multistep/test_last_tx_block.py | 51 -- tests/e2e_tests/subcommands/__init__.py | 0 .../subcommands/delegation/__init__.py | 0 .../delegation/test_set_delegate_take.py | 56 -- .../subcommands/hyperparams/__init__.py | 0 .../hyperparams/test_liquid_alpha.py | 275 --------- .../subcommands/register/__init__.py | 0 .../subcommands/register/test_swap_hotkey.py | 541 ------------------ tests/e2e_tests/subcommands/root/__init__.py | 0 .../root/test_root_delegate_list.py | 19 - .../test_root_register_add_member_senate.py | 131 ----- .../subcommands/root/test_root_senate_vote.py | 40 -- .../root/test_root_view_proposal.py | 64 --- tests/e2e_tests/subcommands/stake/__init__.py | 0 .../stake/test_stake_add_remove.py | 65 --- .../subcommands/stake/test_stake_show.py | 49 -- .../e2e_tests/subcommands/subnet/__init__.py | 0 .../subcommands/subnet/test_metagraph.py | 108 ---- .../e2e_tests/subcommands/wallet/__init__.py | 0 .../subcommands/wallet/test_faucet.py | 86 --- .../subcommands/wallet/test_transfer.py | 31 - .../wallet/test_wallet_creations.py | 489 ---------------- .../e2e_tests/subcommands/weights/__init__.py | 0 .../weights/test_commit_weights.py | 241 -------- tests/e2e_tests/utils.py | 214 ------- 32 files changed, 3331 deletions(-) delete mode 100644 tests/e2e_tests/__init__.py delete mode 100644 tests/e2e_tests/conftest.py delete mode 100644 tests/e2e_tests/multistep/__init__.py delete mode 100644 tests/e2e_tests/multistep/test_axon.py delete mode 100644 tests/e2e_tests/multistep/test_dendrite.py delete mode 100644 tests/e2e_tests/multistep/test_emissions.py delete mode 100644 tests/e2e_tests/multistep/test_incentive.py delete mode 100644 tests/e2e_tests/multistep/test_last_tx_block.py delete mode 100644 tests/e2e_tests/subcommands/__init__.py delete mode 100644 tests/e2e_tests/subcommands/delegation/__init__.py delete mode 100644 tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py delete mode 100644 tests/e2e_tests/subcommands/hyperparams/__init__.py delete mode 100644 tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py delete mode 100644 tests/e2e_tests/subcommands/register/__init__.py delete mode 100644 tests/e2e_tests/subcommands/register/test_swap_hotkey.py delete mode 100644 tests/e2e_tests/subcommands/root/__init__.py delete mode 100644 tests/e2e_tests/subcommands/root/test_root_delegate_list.py delete mode 100644 tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py delete mode 100644 tests/e2e_tests/subcommands/root/test_root_senate_vote.py delete mode 100644 tests/e2e_tests/subcommands/root/test_root_view_proposal.py delete mode 100644 tests/e2e_tests/subcommands/stake/__init__.py delete mode 100644 tests/e2e_tests/subcommands/stake/test_stake_add_remove.py delete mode 100644 tests/e2e_tests/subcommands/stake/test_stake_show.py delete mode 100644 tests/e2e_tests/subcommands/subnet/__init__.py delete mode 100644 tests/e2e_tests/subcommands/subnet/test_metagraph.py delete mode 100644 tests/e2e_tests/subcommands/wallet/__init__.py delete mode 100644 tests/e2e_tests/subcommands/wallet/test_faucet.py delete mode 100644 tests/e2e_tests/subcommands/wallet/test_transfer.py delete mode 100644 tests/e2e_tests/subcommands/wallet/test_wallet_creations.py delete mode 100644 tests/e2e_tests/subcommands/weights/__init__.py delete mode 100644 tests/e2e_tests/subcommands/weights/test_commit_weights.py delete mode 100644 tests/e2e_tests/utils.py diff --git a/tests/e2e_tests/__init__.py b/tests/e2e_tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py deleted file mode 100644 index 4575ff298..000000000 --- a/tests/e2e_tests/conftest.py +++ /dev/null @@ -1,85 +0,0 @@ -import logging -import os -import re -import shlex -import signal -import subprocess -import time - -import pytest -from substrateinterface import SubstrateInterface - -from tests.e2e_tests.utils import ( - clone_or_update_templates, - install_templates, - uninstall_templates, - template_path, -) - -logging.basicConfig(level=logging.INFO) - - -# Fixture for setting up and tearing down a localnet.sh chain between tests -@pytest.fixture(scope="function") -def local_chain(request): - param = request.param if hasattr(request, "param") else None - # Get the environment variable for the script path - script_path = os.getenv("LOCALNET_SH_PATH") - - if not script_path: - # Skip the test if the localhost.sh path is not set - 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 - args = "" if param is None else f"{param}" - - # compile commands to send to process - cmds = shlex.split(f"{script_path} {args}") - # Start new node process - process = subprocess.Popen( - cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid - ) - - # Pattern match indicates node is compiled and ready - pattern = re.compile(r"Imported #1") - - # install neuron templates - logging.info("downloading and installing neuron templates from github") - templates_dir = clone_or_update_templates() - install_templates(templates_dir) - - timestamp = int(time.time()) - - def wait_for_node_start(process, pattern): - for line in process.stdout: - print(line.strip()) - # 10 min as timeout - if int(time.time()) - timestamp > 10 * 60: - print("Subtensor not started in time") - break - if pattern.search(line): - print("Node started!") - break - - wait_for_node_start(process, pattern) - - # Run the test, passing in substrate interface - yield SubstrateInterface(url="ws://127.0.0.1:9945") - - # Terminate the process group (includes all child processes) - os.killpg(os.getpgid(process.pid), signal.SIGTERM) - - # Give some time for the process to terminate - time.sleep(1) - - # If the process is not terminated, send SIGKILL - if process.poll() is None: - os.killpg(os.getpgid(process.pid), signal.SIGKILL) - - # Ensure the process has terminated - process.wait() - - # uninstall templates - logging.info("uninstalling neuron templates") - uninstall_templates(template_path) diff --git a/tests/e2e_tests/multistep/__init__.py b/tests/e2e_tests/multistep/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/multistep/test_axon.py b/tests/e2e_tests/multistep/test_axon.py deleted file mode 100644 index a91c0076c..000000000 --- a/tests/e2e_tests/multistep/test_axon.py +++ /dev/null @@ -1,107 +0,0 @@ -import asyncio -import sys - -import pytest - -import bittensor -from bittensor.utils import networking -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, -) -from tests.e2e_tests.utils import ( - setup_wallet, - template_path, - templates_repo, -) - -""" -Test the axon mechanism. - -Verify that: -* axon is registered on network as a miner -* ip -* type -* port - -are set correctly, and that the miner is currently running - -""" - - -@pytest.mark.asyncio -async def test_axon(local_chain): - netuid = 1 - # Register root as Alice - alice_keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # Verify subnet created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [netuid]).serialize() - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) - - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - - # validate one miner with ip of none - old_axon = metagraph.axons[0] - - assert len(metagraph.axons) == 1 - assert old_axon.hotkey == alice_keypair.ss58_address - assert old_axon.coldkey == alice_keypair.ss58_address - assert old_axon.ip == "0.0.0.0" - assert old_axon.port == 0 - assert old_axon.ip_type == 0 - - # register miner - # "python neurons/miner.py --netuid 1 --subtensor.chain_endpoint ws://localhost:9945 --wallet.name wallet.name --wallet.hotkey wallet.hotkey.ss58_address" - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/miner.py"', - "--no_prompt", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - wallet.path, - "--wallet.name", - wallet.name, - "--wallet.hotkey", - "default", - ] - ) - - axon_process = await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph to refresh with latest data - - # refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - updated_axon = metagraph.axons[0] - external_ip = networking.get_external_ip() - - assert len(metagraph.axons) == 1 - assert updated_axon.ip == external_ip - assert updated_axon.ip_type == networking.ip_version(external_ip) - assert updated_axon.port == 8091 - assert updated_axon.hotkey == alice_keypair.ss58_address - assert updated_axon.coldkey == alice_keypair.ss58_address diff --git a/tests/e2e_tests/multistep/test_dendrite.py b/tests/e2e_tests/multistep/test_dendrite.py deleted file mode 100644 index 44e05d9fb..000000000 --- a/tests/e2e_tests/multistep/test_dendrite.py +++ /dev/null @@ -1,161 +0,0 @@ -import asyncio -import logging -import sys - -import pytest - -import bittensor -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, - StakeCommand, - RootRegisterCommand, - RootSetBoostCommand, -) -from tests.e2e_tests.utils import ( - setup_wallet, - template_path, - templates_repo, - wait_epoch, -) - - -logging.basicConfig(level=logging.INFO) - -""" -Test the dendrites mechanism. - -Verify that: -* dendrite is registered on network as a validator -* stake successfully -* validator permit is set - -""" - - -@pytest.mark.asyncio -async def test_dendrite(local_chain): - netuid = 1 - # Register root as Alice - the subnet owner - alice_keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # Verify subnet created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [netuid]).serialize() - - bob_keypair, exec_command, wallet_path = setup_wallet("//Bob") - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) - - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # assert one neuron is Bob - assert len(subtensor.neurons(netuid=netuid)) == 1 - neuron = metagraph.neurons[0] - assert neuron.hotkey == bob_keypair.ss58_address - assert neuron.coldkey == bob_keypair.ss58_address - - # assert stake is 0 - assert neuron.stake.tao == 0 - - # Stake to become to top neuron after the first epoch - exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - ], - ) - - # refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - neuron = metagraph.neurons[0] - # assert stake is 10000 - assert neuron.stake.tao == 10_000.0 - - # assert neuron is not validator - assert neuron.active is True - assert neuron.validator_permit is False - assert neuron.validator_trust == 0.0 - assert neuron.pruning_score == 0 - - # register validator from template - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/validator.py"', - "--no_prompt", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - wallet.path, - "--wallet.name", - wallet.name, - "--wallet.hotkey", - "default", - ] - ) - - # run validator in the background - dendrite_process = await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - - # register validator with root network - exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--netuid", - str(netuid), - ], - ) - - exec_command( - RootSetBoostCommand, - [ - "root", - "boost", - "--netuid", - str(netuid), - "--increase", - "1", - ], - ) - # get current block, wait until next epoch - await wait_epoch(subtensor, netuid=netuid) - - # refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - - # refresh validator neuron - neuron = metagraph.neurons[0] - - assert len(metagraph.neurons) == 1 - assert neuron.active is True - assert neuron.validator_permit is True - assert neuron.hotkey == bob_keypair.ss58_address - assert neuron.coldkey == bob_keypair.ss58_address diff --git a/tests/e2e_tests/multistep/test_emissions.py b/tests/e2e_tests/multistep/test_emissions.py deleted file mode 100644 index ebbccd3e1..000000000 --- a/tests/e2e_tests/multistep/test_emissions.py +++ /dev/null @@ -1,276 +0,0 @@ -import asyncio -import logging -import sys - -import pytest - -import bittensor -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, - StakeCommand, - RootRegisterCommand, - RootSetBoostCommand, - SubnetSudoCommand, - RootSetWeightsCommand, - SetTakeCommand, -) -from tests.e2e_tests.utils import ( - setup_wallet, - template_path, - templates_repo, - wait_epoch, -) - -logging.basicConfig(level=logging.INFO) - -""" -Test the emissions mechanism. - -Verify that for the miner: -* trust -* rank -* consensus -* incentive -* emission -are updated with proper values after an epoch has passed. - -For the validator verify that: -* validator_permit -* validator_trust -* dividends -* stake -are updated with proper values after an epoch has passed. - -""" - - -@pytest.mark.asyncio -async def test_emissions(local_chain): - netuid = 1 - # Register root as Alice - the subnet owner and validator - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - # Verify subnet created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [netuid]).serialize() - - # Register Bob as miner - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - - # Register Alice as neuron to the subnet - alice_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) - - # Register Bob as neuron to the subnet - bob_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - # assert two neurons are in network - assert len(subtensor.neurons(netuid=netuid)) == 2 - - # Alice to stake to become to top neuron after the first epoch - alice_exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # register Alice as validator - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/validator.py"', - "--no_prompt", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - alice_wallet.path, - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - # run validator in the background - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - await asyncio.sleep(5) - - # register validator with root network - alice_exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--netuid", - str(netuid), - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - await wait_epoch(subtensor, netuid=netuid) - - alice_exec_command( - RootSetBoostCommand, - [ - "root", - "boost", - "--netuid", - str(netuid), - "--increase", - "1000", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # register Bob as miner - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/miner.py"', - "--no_prompt", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - bob_wallet.path, - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - await wait_epoch(subtensor) - - logging.warning("Setting root set weights") - alice_exec_command( - RootSetWeightsCommand, - [ - "root", - "weights", - "--netuid", - str(netuid), - "--weights", - "0.3", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # Set delegate take for Alice - alice_exec_command(SetTakeCommand, ["r", "set_take", "--take", "0.15"]) - - # Lower the rate limit - alice_exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - str(netuid), - "--wallet.name", - alice_wallet.name, - "--param", - "weights_rate_limit", - "--value", - "1", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # wait epoch until for emissions to get distributed - await wait_epoch(subtensor) - - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - - # refresh metagraph - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # get current emissions and validate that Alice has gotten tao - weights = [(0, [(0, 65535), (1, 65535)])] - assert subtensor.weights(netuid=netuid) == weights - - neurons = subtensor.neurons(netuid=netuid) - bob = neurons[1] - alice = neurons[0] - - assert bob.emission > 0 - assert bob.consensus == 1 - assert bob.incentive == 1 - assert bob.rank == 1 - assert bob.trust == 1 - - assert alice.emission > 0 - assert alice.bonds == [(1, 65535)] - assert alice.dividends == 1 - assert alice.stake.tao > 10000 # assert an increase in stake - assert alice.validator_permit is True - assert alice.validator_trust == 1 - - assert alice.weights == [(0, 65535), (1, 65535)] - - assert ( - subtensor.get_emission_value_by_subnet(netuid=netuid) > 0 - ) # emission on this subnet is strictly greater than 0 diff --git a/tests/e2e_tests/multistep/test_incentive.py b/tests/e2e_tests/multistep/test_incentive.py deleted file mode 100644 index 39bd60cb7..000000000 --- a/tests/e2e_tests/multistep/test_incentive.py +++ /dev/null @@ -1,242 +0,0 @@ -import asyncio -import logging -import sys - -import pytest - -import bittensor -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, - StakeCommand, - RootRegisterCommand, - RootSetBoostCommand, -) -from tests.e2e_tests.utils import ( - setup_wallet, - template_path, - templates_repo, - wait_epoch, -) - -logging.basicConfig(level=logging.INFO) - -""" -Test the incentive mechanism. - -Verify that for the miner: -* trust -* rank -* consensus -* incentive -are updated with proper values after an epoch has passed. - -For the validator verify that: -* validator_permit -* validator_trust -* dividends -* stake -are updated with proper values after an epoch has passed. - -""" - - -@pytest.mark.asyncio -async def test_incentive(local_chain): - netuid = 1 - # Register root as Alice - the subnet owner and validator - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - # Verify subnet created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [netuid]).serialize() - - # Register Bob as miner - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - - # Register Alice as neuron to the subnet - alice_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) - - # Register Bob as neuron to the subnet - bob_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - # assert two neurons are in network - assert len(subtensor.neurons(netuid=netuid)) == 2 - - # Alice to stake to become to top neuron after the first epoch - alice_exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - ], - ) - - # register Bob as miner - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/miner.py"', - "--no_prompt", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - bob_wallet.path, - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - - miner_process = await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph to refresh with latest data - - # register Alice as validator - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/validator.py"', - "--no_prompt", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - alice_wallet.path, - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - # run validator in the background - - validator_process = await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - - # register validator with root network - alice_exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--netuid", - str(netuid), - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - ], - ) - - alice_exec_command( - RootSetBoostCommand, - [ - "root", - "boost", - "--netuid", - str(netuid), - "--increase", - "100", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - ], - ) - - # get latest metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - - # get current emissions - bob_neuron = metagraph.neurons[1] - assert bob_neuron.incentive == 0 - assert bob_neuron.consensus == 0 - assert bob_neuron.rank == 0 - assert bob_neuron.trust == 0 - - alice_neuron = metagraph.neurons[0] - assert alice_neuron.validator_permit is False - assert alice_neuron.dividends == 0 - assert alice_neuron.stake.tao == 10_000.0 - assert alice_neuron.validator_trust == 0 - - # wait until next epoch - await wait_epoch(subtensor) - - # for some reason the weights do not get set through the template. Set weight manually. - alice_wallet = bittensor.wallet() - alice_wallet._hotkey = alice_keypair - subtensor._do_set_weights( - wallet=alice_wallet, - uids=[1], - vals=[65535], - netuid=netuid, - version_key=0, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - # wait epoch until weight go into effect - await wait_epoch(subtensor) - - # refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - - # get current emissions and validate that Alice has gotten tao - bob_neuron = metagraph.neurons[1] - assert bob_neuron.incentive == 1 - assert bob_neuron.consensus == 1 - assert bob_neuron.rank == 1 - assert bob_neuron.trust == 1 - - alice_neuron = metagraph.neurons[0] - assert alice_neuron.validator_permit is True - assert alice_neuron.dividends == 1 - assert alice_neuron.stake.tao == 10_000.0 - assert alice_neuron.validator_trust == 1 diff --git a/tests/e2e_tests/multistep/test_last_tx_block.py b/tests/e2e_tests/multistep/test_last_tx_block.py deleted file mode 100644 index 5bc475921..000000000 --- a/tests/e2e_tests/multistep/test_last_tx_block.py +++ /dev/null @@ -1,51 +0,0 @@ -from bittensor.commands.root import RootRegisterCommand -from bittensor.commands.delegates import NominateCommand -from bittensor.commands.network import RegisterSubnetworkCommand -from bittensor.commands.register import RegisterCommand -from ..utils import setup_wallet - - -# Automated testing for take related tests described in -# https://discord.com/channels/799672011265015819/1176889736636407808/1236057424134144152 -def test_takes(local_chain): - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RootRegisterCommand, ["root", "register"]) - - # Create subnet 1 and verify created successfully - assert not (local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize()) - - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]) - - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register and nominate Bob - keypair, exec_command, wallet = setup_wallet("//Bob") - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlock", [keypair.ss58_address] - ).serialize() - == 0 - ) - - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address] - ).serialize() - == 0 - ) - exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) - exec_command(NominateCommand, ["root", "nominate"]) - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlock", [keypair.ss58_address] - ).serialize() - > 0 - ) - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address] - ).serialize() - > 0 - ) diff --git a/tests/e2e_tests/subcommands/__init__.py b/tests/e2e_tests/subcommands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/delegation/__init__.py b/tests/e2e_tests/subcommands/delegation/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py b/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py deleted file mode 100644 index cefb150f7..000000000 --- a/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py +++ /dev/null @@ -1,56 +0,0 @@ -from bittensor.commands.delegates import SetTakeCommand, NominateCommand -from bittensor.commands.network import RegisterSubnetworkCommand -from bittensor.commands.register import RegisterCommand -from bittensor.commands.root import RootRegisterCommand - -from tests.e2e_tests.utils import setup_wallet - - -def test_set_delegate_increase_take(local_chain): - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RootRegisterCommand, ["root", "register"]) - - # Create subnet 1 and verify created successfully - assert not (local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize()) - - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]) - - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register and nominate Bob - keypair, exec_command, wallet = setup_wallet("//Bob") - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlock", [keypair.ss58_address] - ).serialize() - == 0 - ) - - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address] - ).serialize() - == 0 - ) - exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) - exec_command(NominateCommand, ["root", "nominate"]) - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlock", [keypair.ss58_address] - ).serialize() - > 0 - ) - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address] - ).serialize() - > 0 - ) - - # Set delegate take for Bob - exec_command(SetTakeCommand, ["r", "set_take", "--take", "0.15"]) - assert local_chain.query( - "SubtensorModule", "Delegates", [keypair.ss58_address] - ).value == int(0.15 * 65535) diff --git a/tests/e2e_tests/subcommands/hyperparams/__init__.py b/tests/e2e_tests/subcommands/hyperparams/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py b/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py deleted file mode 100644 index cf2522b78..000000000 --- a/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py +++ /dev/null @@ -1,275 +0,0 @@ -import bittensor -from bittensor.commands import ( - RegisterCommand, - StakeCommand, - RegisterSubnetworkCommand, - SubnetSudoCommand, -) -from tests.e2e_tests.utils import setup_wallet - -""" -Test the liquid alpha weights mechanism. - -Verify that: -* it can get enabled -* liquid alpha values cannot be set before the feature flag is set -* after feature flag, you can set alpha_high -* after feature flag, you can set alpha_low -""" - - -def test_liquid_alpha_enabled(local_chain, capsys): - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # hyperparameter values - alpha_values = "6553, 53083" - - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - # Stake to become to top neuron after the first epoch - exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "100000", - ], - ) - - # Assert liquid alpha disabled - subtensor = bittensor.subtensor(network="ws://localhost:9945") - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False - ), "Liquid alpha is enabled by default" - - # Attempt to set alpha high/low while disabled (should fail) - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=list(map(int, alpha_values.split(","))), - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None - output = capsys.readouterr().out - assert ( - "❌ Failed: Subtensor returned `LiquidAlphaDisabled (Module)` error. This means: \n`Attempting to set alpha high/low while disabled`" - in output - ) - - # Enable Liquid Alpha - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "liquid_alpha_enabled", - "--value", - "True", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - assert subtensor.get_subnet_hyperparameters( - netuid=1 - ).liquid_alpha_enabled, "Failed to enable liquid alpha" - - output = capsys.readouterr().out - assert "✅ Hyper parameter liquid_alpha_enabled changed to True" in output - - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "alpha_values", - "--value", - "87, 54099", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 54099 - ), "Failed to set alpha high" - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 87 - ), "Failed to set alpha low" - - u16_max = 65535 - # Set alpha high too low - alpha_high_too_low = ( - u16_max * 4 // 5 - ) - 1 # One less than the minimum acceptable value - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=[6553, alpha_high_too_low], - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None - output = capsys.readouterr().out - assert ( - "❌ Failed: Subtensor returned `AlphaHighTooLow (Module)` error. This means: \n`Alpha high is too low: alpha_high > 0.8`" - in output - ) - - alpha_high_too_high = u16_max + 1 # One more than the max acceptable value - try: - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=[6553, alpha_high_too_high], - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None, "Expected not to be able to set alpha value above u16" - except Exception as e: - assert str(e) == "65536 out of range for u16", f"Unexpected error: {e}" - - # Set alpha low too low - alpha_low_too_low = 0 - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=[alpha_low_too_low, 53083], - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None - output = capsys.readouterr().out - assert ( - "❌ Failed: Subtensor returned `AlphaLowOutOfRange (Module)` error. This means: \n`Alpha low is out of range: alpha_low > 0 && alpha_low < 0.8`" - in output - ) - - # Set alpha low too high - alpha_low_too_high = ( - u16_max * 4 // 5 - ) + 1 # One more than the maximum acceptable value - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=[alpha_low_too_high, 53083], - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None - output = capsys.readouterr().out - assert ( - "❌ Failed: Subtensor returned `AlphaLowOutOfRange (Module)` error. This means: \n`Alpha low is out of range: alpha_low > 0 && alpha_low < 0.8`" - in output - ) - - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "alpha_values", - "--value", - alpha_values, - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 53083 - ), "Failed to set alpha high" - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 6553 - ), "Failed to set alpha low" - - output = capsys.readouterr().out - assert "✅ Hyper parameter alpha_values changed to [6553.0, 53083.0]" in output - - # Disable Liquid Alpha - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "liquid_alpha_enabled", - "--value", - "False", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False - ), "Failed to disable liquid alpha" - - output = capsys.readouterr().out - assert "✅ Hyper parameter liquid_alpha_enabled changed to False" in output - - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=list(map(int, alpha_values.split(","))), - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None - output = capsys.readouterr().out - assert ( - "❌ Failed: Subtensor returned `LiquidAlphaDisabled (Module)` error. This means: \n`Attempting to set alpha high/low while disabled`" - in output - ) diff --git a/tests/e2e_tests/subcommands/register/__init__.py b/tests/e2e_tests/subcommands/register/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/register/test_swap_hotkey.py b/tests/e2e_tests/subcommands/register/test_swap_hotkey.py deleted file mode 100644 index b32d0cf11..000000000 --- a/tests/e2e_tests/subcommands/register/test_swap_hotkey.py +++ /dev/null @@ -1,541 +0,0 @@ -import asyncio -import sys -import logging -import uuid - -import pytest - -import bittensor -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, - SwapHotkeyCommand, - StakeCommand, - RootRegisterCommand, - NewHotkeyCommand, - ListCommand, -) -from tests.e2e_tests.utils import ( - setup_wallet, - template_path, - templates_repo, - wait_interval, -) - -logging.basicConfig(level=logging.INFO) - -""" -Test the swap_hotkey mechanism. - -Verify that: -* Alice - neuron is registered on network as a validator -* Bob - neuron is registered on network as a miner -* Swap hotkey of Alice via BTCLI -* verify that the hotkey is swapped -* verify that stake hotkey, delegates hotkey, UIDS and prometheus hotkey is swapped -""" - - -@pytest.mark.asyncio -async def test_swap_hotkey_validator_owner(local_chain): - # Register root as Alice - the subnet owner and validator - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register Bob as miner - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - - alice_old_hotkey_address = alice_wallet.hotkey.ss58_address - - # Register Alice as neuron to the subnet - alice_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - # Register Bob as neuron to the subnet - bob_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - # assert two neurons are in network - assert len(subtensor.neurons(netuid=1)) == 2 - - # register Bob as miner - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/miner.py"', - "--no_prompt", - "--netuid", - "1", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - bob_wallet.path, - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph to refresh with latest data - - # register Alice as validator - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/validator.py"', - "--no_prompt", - "--netuid", - "1", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - alice_wallet.path, - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - # run validator in the background - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - - # register validator with root network - alice_exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--netuid", - "1", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - ], - ) - - # Alice to stake to become to top neuron after the first epoch - alice_exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - ], - ) - - # get latest metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # assert alice has old hotkey - alice_neuron = metagraph.neurons[0] - - # get current number of hotkeys - wallet_tree = alice_exec_command(ListCommand, ["w", "list"], "get_tree") - num_hotkeys = len(wallet_tree.children[0].children) - - assert alice_neuron.coldkey == "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" - assert alice_neuron.hotkey == alice_old_hotkey_address - assert ( - alice_neuron.stake_dict["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"].tao - == 10000.0 - ) - assert alice_neuron.hotkey == alice_neuron.coldkey - assert alice_neuron.hotkey == subtensor.get_all_subnets_info()[1].owner_ss58 - assert alice_neuron.coldkey == subtensor.get_hotkey_owner(alice_old_hotkey_address) - assert subtensor.is_hotkey_delegate(alice_neuron.hotkey) is True - assert ( - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=alice_neuron.hotkey, netuid=1 - ) - is True - ) - assert ( - subtensor.get_uid_for_hotkey_on_subnet( - hotkey_ss58=alice_neuron.hotkey, netuid=1 - ) - == alice_neuron.uid - ) - if num_hotkeys > 1: - logging.info(f"You have {num_hotkeys} hotkeys for Alice.") - - # generate new guid name for hotkey - new_hotkey_name = str(uuid.uuid4()) - - # create a new hotkey - alice_exec_command( - NewHotkeyCommand, - [ - "w", - "new_hotkey", - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - new_hotkey_name, - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # wait rate limit, until we are allowed to change hotkeys - rate_limit = subtensor.tx_rate_limit() - curr_block = subtensor.get_current_block() - await wait_interval(rate_limit + curr_block + 1, subtensor) - - # swap hotkey - alice_exec_command( - SwapHotkeyCommand, - [ - "w", - "swap_hotkey", - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - alice_wallet.hotkey_str, - "--wallet.hotkey_b", - new_hotkey_name, - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # get latest metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # assert Alice has new hotkey - alice_neuron = metagraph.neurons[0] - wallet_tree = alice_exec_command(ListCommand, ["w", "list"], "get_tree") - new_num_hotkeys = len(wallet_tree.children[0].children) - - assert ( - alice_neuron.coldkey == "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" - ) # cold key didnt change - assert alice_neuron.hotkey != alice_old_hotkey_address - assert alice_neuron.hotkey != alice_neuron.coldkey - assert ( - alice_neuron.coldkey == subtensor.get_all_subnets_info()[1].owner_ss58 - ) # new hotkey address is subnet owner - assert alice_neuron.coldkey != subtensor.get_hotkey_owner( - alice_old_hotkey_address - ) # old key is NOT owner - assert alice_neuron.coldkey == subtensor.get_hotkey_owner( - alice_neuron.hotkey - ) # new key is owner - assert ( - subtensor.is_hotkey_delegate(alice_neuron.hotkey) is True - ) # new key is delegate - assert ( # new key is registered on subnet - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=alice_neuron.hotkey, netuid=1 - ) - is True - ) - assert ( # old key is NOT registered on subnet - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=alice_old_hotkey_address, netuid=1 - ) - is False - ) - assert ( # uid is unchanged - subtensor.get_uid_for_hotkey_on_subnet( - hotkey_ss58=alice_neuron.hotkey, netuid=1 - ) - == alice_neuron.uid - ) - assert new_num_hotkeys == num_hotkeys + 1 - - -""" -Test the swap_hotkey mechanism. - -Verify that: -* Alice - neuron is registered on network as a validator -* Bob - neuron is registered on network as a miner -* Swap hotkey of Bob via BTCLI -* verify that the hotkey is swapped -* verify that stake hotkey, delegates hotkey, UIDS and prometheus hotkey is swapped -""" - - -@pytest.mark.asyncio -async def test_swap_hotkey_miner(local_chain): - # Register root as Alice - the subnet owner and validator - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register Bob as miner - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - - bob_old_hotkey_address = bob_wallet.hotkey.ss58_address - - # Register Alice as neuron to the subnet - alice_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - # Register Bob as neuron to the subnet - bob_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - # assert two neurons are in network - assert len(subtensor.neurons(netuid=1)) == 2 - - # register Bob as miner - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/miner.py"', - "--no_prompt", - "--netuid", - "1", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - bob_wallet.path, - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - # register Alice as validator - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/validator.py"', - "--no_prompt", - "--netuid", - "1", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - alice_wallet.path, - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - # run validator in the background - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - - # register validator with root network - alice_exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--netuid", - "1", - ], - ) - - # Alice to stake to become to top neuron after the first epoch - alice_exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - ], - ) - - # get latest metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # assert bob has old hotkey - bob_neuron = metagraph.neurons[1] - - # get current number of hotkeys - wallet_tree = bob_exec_command(ListCommand, ["w", "list"], "get_tree") - num_hotkeys = len(wallet_tree.children[0].children) - - assert bob_neuron.coldkey == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - assert bob_neuron.hotkey == bob_old_hotkey_address - assert bob_neuron.hotkey == bob_neuron.coldkey - assert bob_neuron.coldkey == subtensor.get_hotkey_owner(bob_old_hotkey_address) - assert subtensor.is_hotkey_delegate(bob_neuron.hotkey) is False - assert ( - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=bob_neuron.hotkey, netuid=1 - ) - is True - ) - assert ( - subtensor.get_uid_for_hotkey_on_subnet(hotkey_ss58=bob_neuron.hotkey, netuid=1) - == bob_neuron.uid - ) - if num_hotkeys > 1: - logging.info(f"You have {num_hotkeys} hotkeys for Bob.") - - # generate new guid name for hotkey - new_hotkey_name = str(uuid.uuid4()) - - # create a new hotkey - bob_exec_command( - NewHotkeyCommand, - [ - "w", - "new_hotkey", - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - new_hotkey_name, - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # wait rate limit, until we are allowed to change hotkeys - rate_limit = subtensor.tx_rate_limit() - curr_block = subtensor.get_current_block() - await wait_interval(rate_limit + curr_block + 1, subtensor) - - # swap hotkey - bob_exec_command( - SwapHotkeyCommand, - [ - "w", - "swap_hotkey", - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - bob_wallet.hotkey_str, - "--wallet.hotkey_b", - new_hotkey_name, - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # get latest metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # assert bob has new hotkey - bob_neuron = metagraph.neurons[1] - wallet_tree = alice_exec_command(ListCommand, ["w", "list"], "get_tree") - new_num_hotkeys = len(wallet_tree.children[0].children) - - assert ( - bob_neuron.coldkey == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - ) # cold key didn't change - assert bob_neuron.hotkey != bob_old_hotkey_address - assert bob_neuron.hotkey != bob_neuron.coldkey - assert bob_neuron.coldkey == subtensor.get_hotkey_owner( - bob_neuron.hotkey - ) # new key is owner - assert ( - subtensor.is_hotkey_delegate(bob_neuron.hotkey) is False - ) # new key is delegate ?? - assert ( # new key is registered on subnet - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=bob_neuron.hotkey, netuid=1 - ) - is True - ) - assert ( # old key is NOT registered on subnet - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=bob_old_hotkey_address, netuid=1 - ) - is False - ) - assert ( # uid is unchanged - subtensor.get_uid_for_hotkey_on_subnet(hotkey_ss58=bob_neuron.hotkey, netuid=1) - == bob_neuron.uid - ) - assert new_num_hotkeys == num_hotkeys + 1 diff --git a/tests/e2e_tests/subcommands/root/__init__.py b/tests/e2e_tests/subcommands/root/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/root/test_root_delegate_list.py b/tests/e2e_tests/subcommands/root/test_root_delegate_list.py deleted file mode 100644 index a3a9240f0..000000000 --- a/tests/e2e_tests/subcommands/root/test_root_delegate_list.py +++ /dev/null @@ -1,19 +0,0 @@ -from bittensor.commands.delegates import ListDelegatesCommand -from ...utils import setup_wallet - - -# delegate seems hard code the network config -def test_root_delegate_list(local_chain, capsys): - alice_keypair, exec_command, wallet = setup_wallet("//Alice") - - # 1200 hardcoded block gap - exec_command( - ListDelegatesCommand, - ["root", "list_delegates"], - ) - - captured = capsys.readouterr() - lines = captured.out.splitlines() - - # the command print too many lines - assert len(lines) > 200 diff --git a/tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py b/tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py deleted file mode 100644 index 26e227c4e..000000000 --- a/tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py +++ /dev/null @@ -1,131 +0,0 @@ -import bittensor -from bittensor.commands import ( - RegisterSubnetworkCommand, - RegisterCommand, - StakeCommand, - NominateCommand, - SetTakeCommand, - RootRegisterCommand, -) -from bittensor.commands.senate import SenateCommand -from ...utils import setup_wallet - - -def test_root_register_add_member_senate(local_chain, capsys): - # Register root as Alice - the subnet owner - alice_keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - # Stake to become to top neuron after the first epoch - exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - ], - ) - - exec_command(NominateCommand, ["root", "nominate"]) - - exec_command(SetTakeCommand, ["r", "set_take", "--take", "0.8"]) - - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - # Query local chain for senate members - members = local_chain.query("SenateMembers", "Members").serialize() - assert len(members) == 3 - - # Assert subtensor has 3 senate members - subtensor = bittensor.subtensor(network="ws://localhost:9945") - sub_senate = len(subtensor.get_senate_members()) - assert ( - sub_senate == 3 - ), f"Root senate expected 3 members but found {sub_senate} instead." - - # Execute command and capture output - exec_command( - SenateCommand, - ["root", "senate"], - ) - - captured = capsys.readouterr() - lines = captured.out.splitlines() - - # assert output is graph Titling "Senate" with names and addresses - assert "Senate" in lines[17].strip().split() - assert "NAME" in lines[18].strip().split() - assert "ADDRESS" in lines[18].strip().split() - assert ( - "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL" in lines[19].strip().split() - ) - assert ( - "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" in lines[20].strip().split() - ) - assert ( - "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" in lines[21].strip().split() - ) - - exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--wallet.hotkey", - "default", - "--wallet.name", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - # sudo_call_add_senate_member(local_chain, wallet) - - members = local_chain.query("SenateMembers", "Members").serialize() - assert len(members) == 4 - - # Assert subtensor has 4 senate members - subtensor = bittensor.subtensor(network="ws://localhost:9945") - sub_senate = len(subtensor.get_senate_members()) - assert ( - sub_senate == 4 - ), f"Root senate expected 3 members but found {sub_senate} instead." - - exec_command( - SenateCommand, - ["root", "senate"], - ) - - captured = capsys.readouterr() - lines = captured.out.splitlines() - - # assert output is graph Titling "Senate" with names and addresses - assert "Senate" in lines[2].strip().split() - assert "NAME" in lines[3].strip().split() - assert "ADDRESS" in lines[3].strip().split() - assert ( - "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL" in lines[4].strip().split() - ) - assert ( - "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" in lines[5].strip().split() - ) - assert ( - "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" in lines[6].strip().split() - ) - assert ( - "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" in lines[7].strip().split() - ) diff --git a/tests/e2e_tests/subcommands/root/test_root_senate_vote.py b/tests/e2e_tests/subcommands/root/test_root_senate_vote.py deleted file mode 100644 index 1e938080b..000000000 --- a/tests/e2e_tests/subcommands/root/test_root_senate_vote.py +++ /dev/null @@ -1,40 +0,0 @@ -from bittensor.commands.senate import VoteCommand -from bittensor.commands.root import RootRegisterCommand - -from ...utils import ( - setup_wallet, - call_add_proposal, -) - - -def test_root_senate_vote(local_chain, capsys, monkeypatch): - keypair, exec_command, wallet = setup_wallet("//Alice") - monkeypatch.setattr("rich.prompt.Confirm.ask", lambda self: True) - - exec_command( - RootRegisterCommand, - ["root", "register"], - ) - - members = local_chain.query("Triumvirate", "Members") - proposals = local_chain.query("Triumvirate", "Proposals").serialize() - - assert len(members) == 3 - assert len(proposals) == 0 - - call_add_proposal(local_chain, wallet) - - proposals = local_chain.query("Triumvirate", "Proposals").serialize() - - assert len(proposals) == 1 - proposal_hash = proposals[0] - - exec_command( - VoteCommand, - ["root", "senate_vote", "--proposal", proposal_hash], - ) - - voting = local_chain.query("Triumvirate", "Voting", [proposal_hash]).serialize() - - assert len(voting["ayes"]) == 1 - assert voting["ayes"][0] == wallet.hotkey.ss58_address diff --git a/tests/e2e_tests/subcommands/root/test_root_view_proposal.py b/tests/e2e_tests/subcommands/root/test_root_view_proposal.py deleted file mode 100644 index 05bfe8fa3..000000000 --- a/tests/e2e_tests/subcommands/root/test_root_view_proposal.py +++ /dev/null @@ -1,64 +0,0 @@ -from bittensor.commands.senate import ProposalsCommand - -from ...utils import ( - setup_wallet, - call_add_proposal, -) -import bittensor - - -def test_root_view_proposal(local_chain, capsys): - keypair, exec_command, wallet = setup_wallet("//Alice") - - proposals = local_chain.query("Triumvirate", "Proposals").serialize() - - assert len(proposals) == 0 - - call_add_proposal(local_chain, wallet) - - proposals = local_chain.query("Triumvirate", "Proposals").serialize() - - assert len(proposals) == 1 - - exec_command( - ProposalsCommand, - ["root", "proposals"], - ) - - simulated_output = [ - "📡 Syncing with chain: local ...", - " Proposals Active Proposals: 1 Senate Size: 3 ", - "HASH C…", - "0x78b8a348690f565efe3730cd8189f7388c0a896b6fd090276639c9130c0eba47 r…", - " \x00) ", - " ", - ] - - captured = capsys.readouterr() - lines = captured.out.splitlines() - for line in lines: - bittensor.logging.info(line) - - # Assert that the length of the lines is as expected - assert len(lines) == 6 - - # Check each line for expected content - assert ( - lines[0] == "📡 Syncing with chain: local ..." - ), f"Expected '📡 Syncing with chain: local ...', got {lines[0]}" - assert ( - lines[1].strip() - == "Proposals Active Proposals: 1 Senate Size: 3" - ), f"Expected 'Proposals Active Proposals: 1 Senate Size: 3', got {lines[1].strip()}" - assert ( - lines[2].strip().startswith("HASH") - ), f"Expected line starting with 'HASH', got {lines[2].strip()}" - assert ( - lines[3] - .strip() - .startswith( - "0x78b8a348690f565efe3730cd8189f7388c0a896b6fd090276639c9130c0eba47" - ) - ), f"Expected line starting with '0x78b8a348690f565efe3730cd8189f7388c0a896b6fd090276639c9130c0eba47', got {lines[3].strip()}" - assert lines[4].strip() == "\x00)", f"Expected '\x00)', got {lines[4].strip()}" - assert lines[5].strip() == "", f"Expected empty line, got {lines[5].strip()}" diff --git a/tests/e2e_tests/subcommands/stake/__init__.py b/tests/e2e_tests/subcommands/stake/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/stake/test_stake_add_remove.py b/tests/e2e_tests/subcommands/stake/test_stake_add_remove.py deleted file mode 100644 index 20e4f22af..000000000 --- a/tests/e2e_tests/subcommands/stake/test_stake_add_remove.py +++ /dev/null @@ -1,65 +0,0 @@ -from bittensor.commands.stake import StakeCommand -from bittensor.commands.unstake import UnStakeCommand -from bittensor.commands.network import RegisterSubnetworkCommand -from bittensor.commands.register import RegisterCommand -from ...utils import ( - setup_wallet, - sudo_call_set_network_limit, - sudo_call_set_target_stakes_per_interval, -) - - -def test_stake_add(local_chain): - alice_keypair, exec_command, wallet = setup_wallet("//Alice") - assert sudo_call_set_network_limit(local_chain, wallet) - assert sudo_call_set_target_stakes_per_interval(local_chain, wallet) - - assert not (local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize()) - - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]) - - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlock", [wallet.hotkey.ss58_address] - ).serialize() - == 0 - ) - - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlockDelegateTake", [wallet.hotkey.ss58_address] - ).serialize() - == 0 - ) - - exec_command(RegisterCommand, ["s", "register", "--neduid", "1"]) - - assert ( - local_chain.query( - "SubtensorModule", "TotalHotkeyStake", [wallet.hotkey.ss58_address] - ).serialize() - == 0 - ) - - stake_amount = 2 - exec_command(StakeCommand, ["stake", "add", "--amount", str(stake_amount)]) - exact_stake = local_chain.query( - "SubtensorModule", "TotalHotkeyStake", [wallet.hotkey.ss58_address] - ).serialize() - withdraw_loss = 1_000_000 - stake_amount_in_rao = stake_amount * 1_000_000_000 - - assert stake_amount_in_rao - withdraw_loss < exact_stake <= stake_amount_in_rao - - # we can test remove after set the stake rate limit larger than 1 - remove_amount = 1 - exec_command(UnStakeCommand, ["stake", "remove", "--amount", str(remove_amount)]) - assert ( - local_chain.query( - "SubtensorModule", "TotalHotkeyStake", [wallet.hotkey.ss58_address] - ).serialize() - == exact_stake - remove_amount * 1_000_000_000 - ) diff --git a/tests/e2e_tests/subcommands/stake/test_stake_show.py b/tests/e2e_tests/subcommands/stake/test_stake_show.py deleted file mode 100644 index b3c434e7b..000000000 --- a/tests/e2e_tests/subcommands/stake/test_stake_show.py +++ /dev/null @@ -1,49 +0,0 @@ -from bittensor.commands.stake import StakeShow -from ...utils import setup_wallet - - -def test_stake_show(local_chain, capsys): - keypair, exec_command, wallet = setup_wallet("//Alice") - - # Execute the command - exec_command(StakeShow, ["stake", "show"]) - captured = capsys.readouterr() - lines = captured.out.split("\n") - - # Ensure there are enough lines - assert len(lines) >= 5, "Output has fewer than 5 lines." - - # Check the header line - header = lines[0] - assert "Coldkey" in header, "Header missing 'Coldkey'." - assert "Balance" in header, "Header missing 'Balance'." - assert "Account" in header, "Header missing 'Account'." - assert "Stake" in header, "Header missing 'Stake'." - assert "Rate" in header, "Header missing 'Rate'." - - # Check the first line of data - values1 = lines[1].strip().split() - assert values1[0] == "default", f"Expected 'default', got {values1[0]}." - assert ( - values1[1].replace("τ", "") == "1000000.000000" - ), f"Expected '1000000.000000', got {values1[1]}." - - # Check the second line of data - values2 = lines[2].strip().split() - assert values2[0] == "default", f"Expected 'default', got {values2[0]}." - assert ( - values2[1].replace("τ", "") == "0.000000" - ), f"Expected '0.000000', got {values2[1]}." - assert values2[2] == "0/d", f"Expected '0/d', got {values2[2]}." - - # Check the third line of data - values3 = lines[3].strip().split() - assert ( - values3[0].replace("τ", "") == "1000000.00000" - ), f"Expected '1000000.00000', got {values3[0]}." - assert ( - values3[1].replace("τ", "") == "0.00000" - ), f"Expected '0.00000', got {values3[1]}." - assert ( - values3[2].replace("τ", "") == "0.00000/d" - ), f"Expected '0.00000/d', got {values3[2]}." diff --git a/tests/e2e_tests/subcommands/subnet/__init__.py b/tests/e2e_tests/subcommands/subnet/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/subnet/test_metagraph.py b/tests/e2e_tests/subcommands/subnet/test_metagraph.py deleted file mode 100644 index c9334f8ab..000000000 --- a/tests/e2e_tests/subcommands/subnet/test_metagraph.py +++ /dev/null @@ -1,108 +0,0 @@ -import bittensor -from bittensor.commands import ( - MetagraphCommand, - RegisterCommand, - RegisterSubnetworkCommand, -) -from tests.e2e_tests.utils import setup_wallet - -""" -Test the metagraph command before and after registering neurons. - -Verify that: -* Metagraph gets displayed -* Initially empty -------------------------- -* Register 2 neurons one by one -* Ensure both are visible in metagraph -""" - - -def test_metagraph_command(local_chain, capsys): - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - metagraph = subtensor.metagraph(netuid=1) - - # Assert metagraph is empty - assert len(metagraph.uids) == 0 - - # Execute btcli metagraph command - exec_command(MetagraphCommand, ["subnet", "metagraph", "--netuid", "1"]) - - captured = capsys.readouterr() - lines = captured.out.splitlines() - - # Assert metagraph is printed for netuid 1 - assert "Metagraph: net: local:1" in lines[2] - - # Register Bob as neuron to the subnet - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - bob_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - captured = capsys.readouterr() - lines = captured.out.splitlines() - - # Assert neuron was registered - assert "✅ Registered" in lines[3] - - # Refresh the metagraph - metagraph = subtensor.metagraph(netuid=1) - - # Assert metagraph has registered neuron - assert len(metagraph.uids) == 1 - assert metagraph.hotkeys[0] == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - # Execute btcli metagraph command - exec_command(MetagraphCommand, ["subnet", "metagraph", "--netuid", "1"]) - - captured = capsys.readouterr() - - # Assert the neuron is registered and displayed - assert "Metagraph: net: local:1" and "N: 1/1" in captured.out - - # Register Dave as neuron to the subnet - dave_keypair, dave_exec_command, dave_wallet = setup_wallet("//Dave") - dave_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - captured = capsys.readouterr() - lines = captured.out.splitlines() - - # Assert neuron was registered - assert "✅ Registered" in lines[3] - - # Refresh the metagraph - metagraph = subtensor.metagraph(netuid=1) - - # Assert metagraph has registered neuron - assert len(metagraph.uids) == 2 - assert metagraph.hotkeys[1] == "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" - - # Execute btcli metagraph command - exec_command(MetagraphCommand, ["subnet", "metagraph", "--netuid", "1"]) - - captured = capsys.readouterr() - - # Assert the neuron is registered and displayed - assert "Metagraph: net: local:1" and "N: 2/2" in captured.out diff --git a/tests/e2e_tests/subcommands/wallet/__init__.py b/tests/e2e_tests/subcommands/wallet/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/wallet/test_faucet.py b/tests/e2e_tests/subcommands/wallet/test_faucet.py deleted file mode 100644 index bb51bd916..000000000 --- a/tests/e2e_tests/subcommands/wallet/test_faucet.py +++ /dev/null @@ -1,86 +0,0 @@ -import pytest - -import bittensor -from bittensor import logging -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, - RunFaucetCommand, -) -from tests.e2e_tests.utils import ( - setup_wallet, -) - - -@pytest.mark.skip -@pytest.mark.parametrize("local_chain", [False], indirect=True) -def test_faucet(local_chain): - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--no_prompt", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # verify current balance - wallet_balance = subtensor.get_balance(keypair.ss58_address) - assert wallet_balance.tao == 998999.0 - - # run faucet 3 times - for i in range(3): - logging.info(f"faucet run #:{i + 1}") - try: - exec_command( - RunFaucetCommand, - [ - "wallet", - "faucet", - "--wallet.name", - wallet.name, - "--wallet.hotkey", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - logging.info( - f"wallet balance is {subtensor.get_balance(keypair.ss58_address).tao} tao" - ) - except SystemExit as e: - logging.warning( - "Block not generated fast enough to be within 3 block seconds window." - ) - # Handle the SystemExit exception - assert e.code == 1 # Assert that the exit code is 1 - except Exception as e: - logging.warning(f"Unexpected exception occurred on faucet: {e}") - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - new_wallet_balance = subtensor.get_balance(keypair.ss58_address) - # verify balance increase - assert wallet_balance.tao < new_wallet_balance.tao diff --git a/tests/e2e_tests/subcommands/wallet/test_transfer.py b/tests/e2e_tests/subcommands/wallet/test_transfer.py deleted file mode 100644 index 83b096258..000000000 --- a/tests/e2e_tests/subcommands/wallet/test_transfer.py +++ /dev/null @@ -1,31 +0,0 @@ -from bittensor.commands.transfer import TransferCommand -from ...utils import setup_wallet - - -# Example test using the local_chain fixture -def test_transfer(local_chain): - keypair, exec_command, wallet = setup_wallet("//Alice") - - acc_before = local_chain.query("System", "Account", [keypair.ss58_address]) - exec_command( - TransferCommand, - [ - "wallet", - "transfer", - "--amount", - "2", - "--dest", - "5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx", - ], - ) - acc_after = local_chain.query("System", "Account", [keypair.ss58_address]) - - expected_transfer = 2_000_000_000 - tolerance = 200_000 # Tx fee tolerance - - actual_difference = ( - acc_before.value["data"]["free"] - acc_after.value["data"]["free"] - ) - assert ( - expected_transfer <= actual_difference <= expected_transfer + tolerance - ), f"Expected transfer with tolerance: {expected_transfer} <= {actual_difference} <= {expected_transfer + tolerance}" diff --git a/tests/e2e_tests/subcommands/wallet/test_wallet_creations.py b/tests/e2e_tests/subcommands/wallet/test_wallet_creations.py deleted file mode 100644 index b13447378..000000000 --- a/tests/e2e_tests/subcommands/wallet/test_wallet_creations.py +++ /dev/null @@ -1,489 +0,0 @@ -import os -import re -import time -from typing import Dict, Optional, Tuple - -from bittensor.commands.list import ListCommand -from bittensor.commands.wallets import ( - NewColdkeyCommand, - NewHotkeyCommand, - RegenColdkeyCommand, - RegenColdkeypubCommand, - RegenHotkeyCommand, - WalletCreateCommand, -) -from bittensor.subtensor import subtensor - -from ...utils import setup_wallet - -""" -Verify commands: - -* btcli w list -* btcli w create -* btcli w new_coldkey -* btcli w new_hotkey -* btcli w regen_coldkey -* btcli w regen_coldkeypub -* btcli w regen_hotkey -""" - - -def verify_wallet_dir( - base_path: str, - wallet_name: str, - hotkey_name: Optional[str] = None, - coldkeypub_name: Optional[str] = None, -) -> Tuple[bool, str]: - """ - Verifies the existence of wallet directory, coldkey, and optionally the hotkey. - - Args: - base_path (str): The base directory path where wallets are stored. - wallet_name (str): The name of the wallet directory to verify. - hotkey_name (str, optional): The name of the hotkey file to verify. If None, - only the wallet and coldkey file are checked. - coldkeypub_name (str, optional): The name of the coldkeypub file to verify. If None - only the wallet and coldkey is checked - - Returns: - tuple: Returns a tuple containing a boolean and a message. The boolean is True if - all checks pass, otherwise False. - """ - wallet_path = os.path.join(base_path, wallet_name) - - # Check if wallet directory exists - if not os.path.isdir(wallet_path): - return False, f"Wallet directory {wallet_name} not found in {base_path}" - - # Check if coldkey file exists - coldkey_path = os.path.join(wallet_path, "coldkey") - if not os.path.isfile(coldkey_path): - return False, f"Coldkey file not found in {wallet_name}" - - # Check if coldkeypub exists - if coldkeypub_name: - coldkeypub_path = os.path.join(wallet_path, coldkeypub_name) - if not os.path.isfile(coldkeypub_path): - return False, f"Coldkeypub file not found in {wallet_name}" - - # Check if hotkey directory and file exists - if hotkey_name: - hotkeys_path = os.path.join(wallet_path, "hotkeys") - if not os.path.isdir(hotkeys_path): - return False, f"Hotkeys directory not found in {wallet_name}" - - hotkey_file_path = os.path.join(hotkeys_path, hotkey_name) - if not os.path.isfile(hotkey_file_path): - return ( - False, - f"Hotkey file {hotkey_name} not found in {wallet_name}/hotkeys", - ) - - return True, f"Wallet {wallet_name} verified successfully" - - -def verify_key_pattern(output: str, wallet_name: str) -> Optional[str]: - """ - Verifies that a specific wallet key pattern exists in the output text. - - Args: - output (str): The string output where the wallet key should be verified. - wallet_name (str): The name of the wallet to search for in the output. - - Raises: - AssertionError: If the wallet key pattern is not found, or if the key does not - start with '5', or if the key is not exactly 48 characters long. - """ - split_output = output.splitlines() - pattern = rf"{wallet_name}\s*\((5[A-Za-z0-9]{{47}})\)" - found = False - - # Traverse each line to find instance of the pattern - for line in split_output: - match = re.search(pattern, line) - if match: - # Assert key starts with '5' - assert match.group(1).startswith( - "5" - ), f"{wallet_name} should start with '5'" - # Assert length of key is 48 characters - assert ( - len(match.group(1)) == 48 - ), f"Key for {wallet_name} should be 48 characters long" - found = True - return match.group(1) - - # If no match is found in any line, raise an assertion error - assert found, f"{wallet_name} not found in wallet list" - return None - - -def extract_ss58_address(output: str, wallet_name: str) -> str: - """ - Extracts the ss58 address from the given output for a specified wallet. - - Args: - output (str): The captured output. - wallet_name (str): The name of the wallet. - - Returns: - str: ss58 address. - """ - pattern = rf"{wallet_name}\s*\((5[A-Za-z0-9]{{47}})\)" - lines = output.splitlines() - for line in lines: - match = re.search(pattern, line) - if match: - return match.group(1) # Return the ss58 address - - raise ValueError(f"ss58 address not found for wallet {wallet_name}") - - -def extract_mnemonics_from_commands(output: str) -> Dict[str, Optional[str]]: - """ - Extracts mnemonics of coldkeys & hotkeys from the given output for a specified wallet. - - Args: - output (str): The captured output. - - Returns: - dict: A dictionary keys 'coldkey' and 'hotkey', each containing their mnemonics. - """ - mnemonics: Dict[str, Optional[str]] = {"coldkey": None, "hotkey": None} - lines = output.splitlines() - - # Regex pattern to capture the mnemonic - pattern = re.compile(r"btcli w regen_(coldkey|hotkey) --mnemonic ([a-z ]+)") - - for line in lines: - line = line.strip().lower() - match = pattern.search(line) - if match: - key_type = match.group(1) # 'coldkey' or 'hotkey' - mnemonic_phrase = match.group(2).strip() - mnemonics[key_type] = mnemonic_phrase - - return mnemonics - - -def test_wallet_creations(local_chain: subtensor, capsys): - """ - Test the creation and verification of wallet keys and directories in the Bittensor network. - - Steps: - 1. List existing wallets and verify the default setup. - 2. Create a new wallet with both coldkey and hotkey, verify their presence in the output, - and check their physical existence. - 3. Create a new coldkey and verify both its display in the command line output and its physical file. - 4. Create a new hotkey for an existing coldkey, verify its display in the command line output, - and check for both coldkey and hotkey files. - - Raises: - AssertionError: If any of the checks or verifications fail - """ - - wallet_path_name = "//Alice" - base_path = f"/tmp/btcli-e2e-wallet-{wallet_path_name.strip('/')}" - keypair, exec_command, wallet = setup_wallet(wallet_path_name) - - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - - captured = capsys.readouterr() - # Assert the coldkey and hotkey are present in the display with keys - assert "default" and "└── default" in captured.out - wallet_status, message = verify_wallet_dir( - base_path, "default", hotkey_name="default" - ) - assert wallet_status, message - - # ----------------------------- - # Command 1: - # ----------------------------- - # Create a new wallet (coldkey + hotkey) - exec_command( - WalletCreateCommand, - [ - "wallet", - "create", - "--wallet.name", - "new_wallet", - "--wallet.hotkey", - "new_hotkey", - "--no_password", - "--overwrite_coldkey", - "--overwrite_hotkey", - "--no_prompt", - "--wallet.path", - base_path, - ], - ) - - captured = capsys.readouterr() - - # List the wallets - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - - captured = capsys.readouterr() - - # Verify coldkey "new_wallet" is displayed with key - verify_key_pattern(captured.out, "new_wallet") - - # Verify hotkey "new_hotkey" is displayed with key - verify_key_pattern(captured.out, "new_hotkey") - - # Physically verify "new_wallet" and "new_hotkey" are present - wallet_status, message = verify_wallet_dir( - base_path, "new_wallet", hotkey_name="new_hotkey" - ) - assert wallet_status, message - - # ----------------------------- - # Command 2: - # ----------------------------- - # Create a new wallet (coldkey) - exec_command( - NewColdkeyCommand, - [ - "wallet", - "new_coldkey", - "--wallet.name", - "new_coldkey", - "--no_password", - "--no_prompt", - "--overwrite_coldkey", - "--wallet.path", - base_path, - ], - ) - - captured = capsys.readouterr() - - # List the wallets - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - - captured = capsys.readouterr() - - # Verify coldkey "new_coldkey" is displayed with key - verify_key_pattern(captured.out, "new_coldkey") - - # Physically verify "new_coldkey" is present - wallet_status, message = verify_wallet_dir(base_path, "new_coldkey") - assert wallet_status, message - - # ----------------------------- - # Command 3: - # ----------------------------- - # Create a new hotkey for alice_new_coldkey wallet - exec_command( - NewHotkeyCommand, - [ - "wallet", - "new_hotkey", - "--wallet.name", - "new_coldkey", - "--wallet.hotkey", - "new_hotkey", - "--no_prompt", - "--overwrite_hotkey", - "--wallet.path", - base_path, - ], - ) - - captured = capsys.readouterr() - - # List the wallets - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - captured = capsys.readouterr() - - # Verify hotkey "alice_new_hotkey" is displyed with key - verify_key_pattern(captured.out, "new_hotkey") - - # Physically verify "alice_new_coldkey" and "alice_new_hotkey" are present - wallet_status, message = verify_wallet_dir( - base_path, "new_coldkey", hotkey_name="new_hotkey" - ) - assert wallet_status, message - - -def test_wallet_regen(local_chain: subtensor, capsys): - """ - Test the regeneration of coldkeys, hotkeys, and coldkeypub files using mnemonics or ss58 address. - - Steps: - 1. List existing wallets and verify the default setup. - 2. Regenerate the coldkey using the mnemonics and verify using mod time. - 3. Regenerate the coldkeypub using ss58 address and verify using mod time - 4. Regenerate the hotkey using mnemonics and verify using mod time. - - Raises: - AssertionError: If any of the checks or verifications fail - """ - wallet_path_name = "//Bob" - base_path = f"/tmp/btcli-e2e-wallet-{wallet_path_name.strip('/')}" - keypair, exec_command, wallet = setup_wallet(wallet_path_name) - - # Create a new wallet (coldkey + hotkey) - exec_command( - WalletCreateCommand, - [ - "wallet", - "create", - "--wallet.name", - "new_wallet", - "--wallet.hotkey", - "new_hotkey", - "--no_password", - "--overwrite_coldkey", - "--overwrite_hotkey", - "--no_prompt", - "--wallet.path", - base_path, - ], - ) - - captured = capsys.readouterr() - mnemonics = extract_mnemonics_from_commands(captured.out) - - wallet_status, message = verify_wallet_dir( - base_path, - "new_wallet", - hotkey_name="new_hotkey", - coldkeypub_name="coldkeypub.txt", - ) - assert wallet_status, message # Ensure wallet exists - - # ----------------------------- - # Command 1: - # ----------------------------- - - coldkey_path = os.path.join(base_path, "new_wallet", "coldkey") - initial_coldkey_mod_time = os.path.getmtime(coldkey_path) - - exec_command( - RegenColdkeyCommand, - [ - "wallet", - "regen_coldkey", - "--wallet.name", - "new_wallet", - "--wallet.path", - base_path, - "--no_prompt", - "--overwrite_coldkey", - "--mnemonic", - mnemonics["coldkey"], - "--no_password", - ], - ) - - # Wait a bit to ensure file system updates modification time - time.sleep(1) - - new_coldkey_mod_time = os.path.getmtime(coldkey_path) - - assert ( - initial_coldkey_mod_time != new_coldkey_mod_time - ), "Coldkey file was not regenerated as expected" - - # ----------------------------- - # Command 2: - # ----------------------------- - - coldkeypub_path = os.path.join(base_path, "new_wallet", "coldkeypub.txt") - initial_coldkeypub_mod_time = os.path.getmtime(coldkeypub_path) - - # List the wallets - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - captured = capsys.readouterr() - ss58_address = extract_ss58_address(captured.out, "new_wallet") - - exec_command( - RegenColdkeypubCommand, - [ - "wallet", - "regen_coldkeypub", - "--wallet.name", - "new_wallet", - "--wallet.path", - base_path, - "--no_prompt", - "--overwrite_coldkeypub", - "--ss58_address", - ss58_address, - ], - ) - - # Wait a bit to ensure file system updates modification time - time.sleep(1) - - new_coldkeypub_mod_time = os.path.getmtime(coldkeypub_path) - - assert ( - initial_coldkeypub_mod_time != new_coldkeypub_mod_time - ), "Coldkeypub file was not regenerated as expected" - - # ----------------------------- - # Command 3: - # ----------------------------- - - hotkey_path = os.path.join(base_path, "new_wallet", "hotkeys", "new_hotkey") - initial_hotkey_mod_time = os.path.getmtime(hotkey_path) - - exec_command( - RegenHotkeyCommand, - [ - "wallet", - "regen_hotkey", - "--no_prompt", - "--overwrite_hotkey", - "--wallet.name", - "new_wallet", - "--wallet.hotkey", - "new_hotkey", - "--wallet.path", - base_path, - "--mnemonic", - mnemonics["hotkey"], - ], - ) - - # Wait a bit to ensure file system updates modification time - time.sleep(1) - - new_hotkey_mod_time = os.path.getmtime(hotkey_path) - - assert ( - initial_hotkey_mod_time != new_hotkey_mod_time - ), "Hotkey file was not regenerated as expected" diff --git a/tests/e2e_tests/subcommands/weights/__init__.py b/tests/e2e_tests/subcommands/weights/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/weights/test_commit_weights.py b/tests/e2e_tests/subcommands/weights/test_commit_weights.py deleted file mode 100644 index 01725f26b..000000000 --- a/tests/e2e_tests/subcommands/weights/test_commit_weights.py +++ /dev/null @@ -1,241 +0,0 @@ -import re - -import numpy as np -import asyncio -import pytest - -import bittensor -import bittensor.utils.weight_utils as weight_utils -from bittensor.commands import ( - RegisterCommand, - StakeCommand, - RegisterSubnetworkCommand, - CommitWeightCommand, - RevealWeightCommand, - SubnetSudoCommand, -) -from tests.e2e_tests.utils import setup_wallet, wait_interval - -""" -Test the Commit/Reveal weights mechanism. - -Verify that: -* Weights are commited -* weights are hashed with salt ---- after an epoch --- -* weights are un-hashed with salt -* weights are properly revealed - -""" - - -@pytest.mark.asyncio -async def test_commit_and_reveal_weights(local_chain): - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # define values - weights = 0.1 - uid = 0 - salt = "18, 179, 107, 0, 165, 211, 141, 197" - - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - # Stake to become to top neuron after the first epoch - exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "100000", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # Enable Commit Reveal - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "commit_reveal_weights_enabled", - "--value", - "True", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - assert subtensor.get_subnet_hyperparameters( - netuid=1 - ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" - - # Lower the interval - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "commit_reveal_weights_interval", - "--value", - "370", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).commit_reveal_weights_interval - == 370 - ), "Failed to set commit/reveal interval" - - # Lower the rate limit - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "weights_rate_limit", - "--value", - "0", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).weights_rate_limit == 0 - ), "Failed to set commit/reveal rate limit" - - # Configure the CLI arguments for the CommitWeightCommand - exec_command( - CommitWeightCommand, - [ - "wt", - "commit", - "--no_prompt", - "--netuid", - "1", - "--uids", - str(uid), - "--weights", - str(weights), - "--salt", - str(salt), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - "/tmp/btcli-wallet", - ], - ) - - weight_commits = subtensor.query_module( - module="SubtensorModule", - name="WeightCommits", - params=[1, wallet.hotkey.ss58_address], - ) - - # Assert that the committed weights are set correctly - assert weight_commits.value is not None, "Weight commit not found in storage" - commit_hash, commit_block = weight_commits.value - assert commit_block > 0, f"Invalid block number: {commit_block}" - - # Query the WeightCommitRevealInterval storage map - weight_commit_reveal_interval = subtensor.query_module( - module="SubtensorModule", name="WeightCommitRevealInterval", params=[1] - ) - interval = weight_commit_reveal_interval.value - assert interval > 0, "Invalid WeightCommitRevealInterval" - - # Wait until the reveal block range - await wait_interval(interval, subtensor) - - # Configure the CLI arguments for the RevealWeightCommand - exec_command( - RevealWeightCommand, - [ - "wt", - "reveal", - "--no_prompt", - "--netuid", - "1", - "--uids", - str(uid), - "--weights", - str(weights), - "--salt", - str(salt), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - "/tmp/btcli-wallet", - ], - ) - - # Query the Weights storage map - revealed_weights = subtensor.query_module( - module="SubtensorModule", - name="Weights", - params=[1, uid], # netuid and uid - ) - - # Assert that the revealed weights are set correctly - assert revealed_weights.value is not None, "Weight reveal not found in storage" - - uid_list = [int(x) for x in re.split(r"[ ,]+", str(uid))] - uids = np.array(uid_list, dtype=np.int64) - weight_list = [float(x) for x in re.split(r"[ ,]+", str(weights))] - weights_array = np.array(weight_list, dtype=np.float32) - weight_uids, expected_weights = weight_utils.convert_weights_and_uids_for_emit( - uids, weights_array - ) - assert ( - expected_weights[0] == revealed_weights.value[0][1] - ), f"Incorrect revealed weights. Expected: {expected_weights[0]}, Actual: {revealed_weights.value[0][1]}" diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py deleted file mode 100644 index 5a4adc6c9..000000000 --- a/tests/e2e_tests/utils.py +++ /dev/null @@ -1,214 +0,0 @@ -import logging -import asyncio -import os -import shutil -import subprocess -import sys -import time -from typing import List - -from substrateinterface import SubstrateInterface - -import bittensor -from bittensor import Keypair - -template_path = os.getcwd() + "/neurons/" -templates_repo = "templates repository" - - -def setup_wallet(uri: str): - keypair = Keypair.create_from_uri(uri) - wallet_path = "/tmp/btcli-e2e-wallet-{}".format(uri.strip("/")) - wallet = bittensor.wallet(path=wallet_path) - wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True) - wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) - wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True) - - def exec_command(command, extra_args: List[str], function: str = "run"): - parser = bittensor.cli.__create_parser__() - args = extra_args + [ - "--no_prompt", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - wallet_path, - ] - logging.info(f'executing command: {command} {" ".join(args)}') - config = bittensor.config( - parser=parser, - args=args, - ) - cli_instance = bittensor.cli(config) - # Dynamically call the specified function on the command - result = getattr(command, function)(cli_instance) - return result - - return keypair, exec_command, wallet - - -def sudo_call_set_network_limit( - substrate: SubstrateInterface, wallet: bittensor.wallet -) -> bool: - inner_call = substrate.compose_call( - call_module="AdminUtils", - call_function="sudo_set_network_rate_limit", - call_params={"rate_limit": 1}, - ) - call = substrate.compose_call( - call_module="Sudo", - call_function="sudo", - call_params={"call": inner_call}, - ) - - extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - response.process_events() - return response.is_success - - -def sudo_call_set_target_stakes_per_interval( - substrate: SubstrateInterface, wallet: bittensor.wallet -) -> bool: - inner_call = substrate.compose_call( - call_module="AdminUtils", - call_function="sudo_set_target_stakes_per_interval", - call_params={"target_stakes_per_interval": 100}, - ) - call = substrate.compose_call( - call_module="Sudo", - call_function="sudo", - call_params={"call": inner_call}, - ) - - extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - response.process_events() - return response.is_success - - -def call_add_proposal(substrate: SubstrateInterface, wallet: bittensor.wallet) -> bool: - proposal_call = substrate.compose_call( - call_module="System", - call_function="remark", - call_params={"remark": [0]}, - ) - call = substrate.compose_call( - call_module="Triumvirate", - call_function="propose", - call_params={ - "proposal": proposal_call, - "length_bound": 100_000, - "duration": 100_000_000, - }, - ) - - extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - response.process_events() - return response.is_success - - -async def wait_epoch(subtensor, netuid=1): - q_tempo = [ - v.value - for [k, v] in subtensor.query_map_subtensor("Tempo") - if k.value == netuid - ] - if len(q_tempo) == 0: - raise Exception("could not determine tempo") - tempo = q_tempo[0] - logging.info(f"tempo = {tempo}") - await wait_interval(tempo, subtensor, netuid) - - -async def wait_interval(tempo, subtensor, netuid=1): - interval = tempo + 1 - current_block = subtensor.get_current_block() - last_epoch = current_block - 1 - (current_block + netuid + 1) % interval - next_tempo_block_start = last_epoch + interval - last_reported = None - while current_block < next_tempo_block_start: - await asyncio.sleep( - 1 - ) # Wait for 1 second before checking the block number again - current_block = subtensor.get_current_block() - if last_reported is None or current_block - last_reported >= 10: - last_reported = current_block - print( - f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" - ) - logging.info( - f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" - ) - - -def clone_or_update_templates(): - specific_commit = None - install_dir = template_path - repo_mapping = { - templates_repo: "https://github.com/opentensor/bittensor-subnet-template.git", - } - os.makedirs(install_dir, exist_ok=True) - os.chdir(install_dir) - - for repo, git_link in repo_mapping.items(): - if not os.path.exists(repo): - print(f"\033[94mCloning {repo}...\033[0m") - subprocess.run(["git", "clone", git_link, repo], check=True) - else: - print(f"\033[94mUpdating {repo}...\033[0m") - os.chdir(repo) - subprocess.run(["git", "pull"], check=True) - os.chdir("..") - - # here for pulling specific commit versions of repo - if specific_commit: - os.chdir(templates_repo) - print( - f"\033[94mChecking out commit {specific_commit} in {templates_repo}...\033[0m" - ) - subprocess.run(["git", "checkout", specific_commit], check=True) - os.chdir("..") - - return install_dir + templates_repo + "/" - - -def install_templates(install_dir): - subprocess.check_call([sys.executable, "-m", "pip", "install", install_dir]) - - -def uninstall_templates(install_dir): - # uninstall templates - subprocess.check_call( - [sys.executable, "-m", "pip", "uninstall", "bittensor_subnet_template", "-y"] - ) - # delete everything in directory - shutil.rmtree(install_dir) - - -async def write_output_log_to_file(name, stream): - log_file = f"{name}.log" - with open(log_file, "a") as f: - while True: - line = await stream.readline() - if not line: - break - f.write(line.decode()) - f.flush() From 7b770a86597dc9cdc4517cba75c01d4ba1f3d33a Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 15:56:02 -0700 Subject: [PATCH 014/260] License fix --- bittensor/__init__.py | 2 +- bittensor/btlogging/loggingmachine.py | 2 +- bittensor/errors.py | 2 +- bittensor/mock/subtensor_mock.py | 2 +- tests/integration_tests/test_cli.py | 2 +- tests/unit_tests/extrinsics/test_delegation.py | 2 +- tests/unit_tests/extrinsics/test_network.py | 2 +- tests/unit_tests/extrinsics/test_prometheus.py | 2 +- tests/unit_tests/extrinsics/test_registration.py | 2 +- tests/unit_tests/extrinsics/test_serving.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index dcb035d81..6f6bbc1c7 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2024 OpenTensor Foundation +# 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 diff --git a/bittensor/btlogging/loggingmachine.py b/bittensor/btlogging/loggingmachine.py index b28357158..8bc94fde3 100644 --- a/bittensor/btlogging/loggingmachine.py +++ b/bittensor/btlogging/loggingmachine.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2024 OpenTensor Foundation +# 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 diff --git a/bittensor/errors.py b/bittensor/errors.py index b80883459..65c6c66e6 100644 --- a/bittensor/errors.py +++ b/bittensor/errors.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2024 OpenTensor Foundation +# 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 diff --git a/bittensor/mock/subtensor_mock.py b/bittensor/mock/subtensor_mock.py index 4787fa0ce..63cfc891d 100644 --- a/bittensor/mock/subtensor_mock.py +++ b/bittensor/mock/subtensor_mock.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2024 OpenTensor Foundation +# 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 diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py index 5ec03f6ee..263c7c1b9 100644 --- a/tests/integration_tests/test_cli.py +++ b/tests/integration_tests/test_cli.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2024 OpenTensor Foundation +# 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 diff --git a/tests/unit_tests/extrinsics/test_delegation.py b/tests/unit_tests/extrinsics/test_delegation.py index ac107413c..729f5baf8 100644 --- a/tests/unit_tests/extrinsics/test_delegation.py +++ b/tests/unit_tests/extrinsics/test_delegation.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2024 OpenTensor Foundation +# 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 diff --git a/tests/unit_tests/extrinsics/test_network.py b/tests/unit_tests/extrinsics/test_network.py index fb516d4ae..ede01bf5f 100644 --- a/tests/unit_tests/extrinsics/test_network.py +++ b/tests/unit_tests/extrinsics/test_network.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2024 OpenTensor Foundation +# 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 diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index 8eca832d7..237e2051b 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2024 OpenTensor Foundation +# 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 diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index aed33553d..b06f8b5ec 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2024 OpenTensor Foundation +# 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 diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 7d8c75e35..3269397a2 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2024 OpenTensor Foundation +# 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 From 0948bb470c60d5ec8ca2287662503f82c831d111 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 16:40:55 -0700 Subject: [PATCH 015/260] Rebase to `feat/roman/btsdk` --- bittensor/__init__.py | 2 +- bittensor/extrinsics/delegation.py | 2 +- bittensor/extrinsics/root.py | 2 +- bittensor/extrinsics/set_weights.py | 2 +- bittensor/subtensor.py | 2 +- bittensor/threadpool.py | 2 +- bittensor/{ => utils}/btlogging/__init__.py | 10 +++++----- bittensor/{ => utils}/btlogging/defines.py | 8 ++++---- bittensor/{ => utils}/btlogging/format.py | 8 ++++---- bittensor/{ => utils}/btlogging/helpers.py | 8 ++++---- bittensor/{ => utils}/btlogging/loggingmachine.py | 4 +++- tests/unit_tests/test_logging.py | 6 +++--- 12 files changed, 29 insertions(+), 27 deletions(-) rename bittensor/{ => utils}/btlogging/__init__.py (93%) rename bittensor/{ => utils}/btlogging/defines.py (96%) rename bittensor/{ => utils}/btlogging/format.py (99%) rename bittensor/{ => utils}/btlogging/helpers.py (98%) rename bittensor/{ => utils}/btlogging/loggingmachine.py (99%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 6f6bbc1c7..de1056f1f 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -361,7 +361,7 @@ def debug(on: bool = True): from .subtensor import Subtensor as subtensor from .cli import cli as cli, COMMANDS as ALL_COMMANDS -from .btlogging import logging +from bittensor.utils.btlogging import logging from .metagraph import metagraph as metagraph from .threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor diff --git a/bittensor/extrinsics/delegation.py b/bittensor/extrinsics/delegation.py index 54bdb5273..ad9b95302 100644 --- a/bittensor/extrinsics/delegation.py +++ b/bittensor/extrinsics/delegation.py @@ -28,7 +28,7 @@ from rich.prompt import Confirm from typing import Union, Optional from bittensor.utils.balance import Balance -from bittensor.btlogging.defines import BITTENSOR_LOGGER_NAME +from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME logger = logging.getLogger(BITTENSOR_LOGGER_NAME) diff --git a/bittensor/extrinsics/root.py b/bittensor/extrinsics/root.py index 8a7e9e386..05570813c 100644 --- a/bittensor/extrinsics/root.py +++ b/bittensor/extrinsics/root.py @@ -25,7 +25,7 @@ from rich.prompt import Confirm from typing import Union, List import bittensor.utils.weight_utils as weight_utils -from bittensor.btlogging.defines import BITTENSOR_LOGGER_NAME +from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME from bittensor.utils.registration import torch, legacy_torch_api_compat logger = logging.getLogger(BITTENSOR_LOGGER_NAME) diff --git a/bittensor/extrinsics/set_weights.py b/bittensor/extrinsics/set_weights.py index dc3052d0a..db5e4a739 100644 --- a/bittensor/extrinsics/set_weights.py +++ b/bittensor/extrinsics/set_weights.py @@ -24,7 +24,7 @@ from rich.prompt import Confirm from typing import Union, Tuple import bittensor.utils.weight_utils as weight_utils -from bittensor.btlogging.defines import BITTENSOR_LOGGER_NAME +from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME from bittensor.utils.registration import torch, use_torch logger = logging.getLogger(BITTENSOR_LOGGER_NAME) diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index 75484fd69..edc37efd0 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -39,7 +39,7 @@ from substrateinterface.exceptions import SubstrateRequestException import bittensor -from bittensor.btlogging import logging as _logger +from bittensor.utils.btlogging import logging as _logger from bittensor.utils import torch, weight_utils, format_error_message from .chain_data import ( DelegateInfoLite, diff --git a/bittensor/threadpool.py b/bittensor/threadpool.py index 3e49786ff..bd7aaa63d 100644 --- a/bittensor/threadpool.py +++ b/bittensor/threadpool.py @@ -20,7 +20,7 @@ from typing import Callable from concurrent.futures import _base -from bittensor.btlogging.defines import BITTENSOR_LOGGER_NAME +from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME # Workers are created as daemon threads. This is done to allow the interpreter # to exit when there are still idle threads in a ThreadPoolExecutor's thread diff --git a/bittensor/btlogging/__init__.py b/bittensor/utils/btlogging/__init__.py similarity index 93% rename from bittensor/btlogging/__init__.py rename to bittensor/utils/btlogging/__init__.py index 6bf6d2bf3..6b02c51ba 100644 --- a/bittensor/btlogging/__init__.py +++ b/bittensor/utils/btlogging/__init__.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -22,7 +22,7 @@ formatters to ensure consistent logging throughout the project. """ -from bittensor.btlogging.loggingmachine import LoggingMachine +from .loggingmachine import LoggingMachine logging = LoggingMachine(LoggingMachine.config()) diff --git a/bittensor/btlogging/defines.py b/bittensor/utils/btlogging/defines.py similarity index 96% rename from bittensor/btlogging/defines.py rename to bittensor/utils/btlogging/defines.py index c87177ffd..9e1dada25 100644 --- a/bittensor/btlogging/defines.py +++ b/bittensor/utils/btlogging/defines.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 OpenTensor Foundation - +# 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 diff --git a/bittensor/btlogging/format.py b/bittensor/utils/btlogging/format.py similarity index 99% rename from bittensor/btlogging/format.py rename to bittensor/utils/btlogging/format.py index 5f2c8cb86..70629128f 100644 --- a/bittensor/btlogging/format.py +++ b/bittensor/utils/btlogging/format.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 OpenTensor Foundation - +# 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 diff --git a/bittensor/btlogging/helpers.py b/bittensor/utils/btlogging/helpers.py similarity index 98% rename from bittensor/btlogging/helpers.py rename to bittensor/utils/btlogging/helpers.py index 532c1f716..892df1052 100644 --- a/bittensor/btlogging/helpers.py +++ b/bittensor/utils/btlogging/helpers.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 OpenTensor Foundation - +# 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 diff --git a/bittensor/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py similarity index 99% rename from bittensor/btlogging/loggingmachine.py rename to bittensor/utils/btlogging/loggingmachine.py index 8bc94fde3..fc6be6558 100644 --- a/bittensor/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -34,7 +34,9 @@ from bittensor_wallet.config import Config from statemachine import State, StateMachine -from bittensor.btlogging.defines import ( +from bittensor_wallet.config import Config + +from .defines import ( BITTENSOR_LOGGER_NAME, DATE_FORMAT, DEFAULT_LOG_BACKUP_COUNT, diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 1822fc86e..068bb39bd 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -2,9 +2,9 @@ import multiprocessing import logging as stdlogging from unittest.mock import MagicMock, patch -from bittensor.btlogging import LoggingMachine -from bittensor.btlogging.defines import DEFAULT_LOG_FILE_NAME, BITTENSOR_LOGGER_NAME -from bittensor.btlogging.loggingmachine import LoggingConfig +from bittensor.utils.btlogging import LoggingMachine +from bittensor.utils.btlogging.defines import DEFAULT_LOG_FILE_NAME, BITTENSOR_LOGGER_NAME +from bittensor.utils.btlogging.loggingmachine import LoggingConfig @pytest.fixture(autouse=True, scope="session") From e0b3faefa2f7e803a2711f75b05e68da903ec4a4 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 16:50:41 -0700 Subject: [PATCH 016/260] Fix test --- tests/unit_tests/test_logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 068bb39bd..1ac772481 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -75,7 +75,7 @@ def test_state_transitions(logging_machine, mock_config): Test state transitions and the associated logging level changes. """ config, log_file_path = mock_config - with patch("bittensor.btlogging.loggingmachine.all_loggers") as mocked_all_loggers: + with patch("bittensor.utils.btlogging.loggingmachine.all_loggers") as mocked_all_loggers: # mock the main bittensor logger, identified by its `name` field mocked_bt_logger = MagicMock() mocked_bt_logger.name = BITTENSOR_LOGGER_NAME From 82a3e203b51c16f445d86b61e148cb9c74d4bc38 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 19:22:56 -0700 Subject: [PATCH 017/260] Temporary placement of cli and related content in btcli (for manual testing during development) --- bittensor/__init__.py | 125 +++++++++-------- bittensor/btcli/__init__.py | 0 bittensor/{ => btcli}/cli.py | 0 bittensor/{ => btcli}/commands/__init__.py | 0 .../commands/check_coldkey_swap.py | 5 +- bittensor/{ => btcli}/commands/delegates.py | 12 +- bittensor/{ => btcli}/commands/identity.py | 0 bittensor/{ => btcli}/commands/inspect.py | 27 ++-- bittensor/{ => btcli}/commands/list.py | 11 +- bittensor/{ => btcli}/commands/metagraph.py | 12 +- bittensor/{ => btcli}/commands/misc.py | 11 +- bittensor/{ => btcli}/commands/network.py | 21 +-- bittensor/{ => btcli}/commands/overview.py | 25 ++-- bittensor/{ => btcli}/commands/register.py | 20 +-- bittensor/{ => btcli}/commands/root.py | 11 +- bittensor/{ => btcli}/commands/senate.py | 19 +-- bittensor/{ => btcli}/commands/stake.py | 13 +- bittensor/{ => btcli}/commands/transfer.py | 12 +- bittensor/{ => btcli}/commands/unstake.py | 21 +-- bittensor/{ => btcli}/commands/utils.py | 19 +-- bittensor/{ => btcli}/commands/wallets.py | 4 +- bittensor/{ => btcli}/commands/weights.py | 0 bittensor/core/__init__.py | 0 bittensor/extrinsics/network.py | 11 +- bittensor/mock/keyfile_mock.py | 90 ------------- bittensor/mock/wallet_mock.py | 127 ------------------ bittensor/settings.py | 17 +++ tests/helpers/helpers.py | 22 +-- tests/integration_tests/test_cli.py | 20 +-- .../integration_tests/test_cli_no_network.py | 24 ++-- tests/unit_tests/test_overview.py | 8 +- tests/unit_tests/test_subtensor.py | 21 +-- 32 files changed, 260 insertions(+), 448 deletions(-) create mode 100644 bittensor/btcli/__init__.py rename bittensor/{ => btcli}/cli.py (100%) rename bittensor/{ => btcli}/commands/__init__.py (100%) rename bittensor/{ => btcli}/commands/check_coldkey_swap.py (98%) rename bittensor/{ => btcli}/commands/delegates.py (99%) rename bittensor/{ => btcli}/commands/identity.py (100%) rename bittensor/{ => btcli}/commands/inspect.py (99%) rename bittensor/{ => btcli}/commands/list.py (98%) rename bittensor/{ => btcli}/commands/metagraph.py (99%) rename bittensor/{ => btcli}/commands/misc.py (98%) rename bittensor/{ => btcli}/commands/network.py (99%) rename bittensor/{ => btcli}/commands/overview.py (99%) rename bittensor/{ => btcli}/commands/register.py (99%) rename bittensor/{ => btcli}/commands/root.py (99%) rename bittensor/{ => btcli}/commands/senate.py (99%) rename bittensor/{ => btcli}/commands/stake.py (99%) rename bittensor/{ => btcli}/commands/transfer.py (98%) rename bittensor/{ => btcli}/commands/unstake.py (99%) rename bittensor/{ => btcli}/commands/utils.py (99%) rename bittensor/{ => btcli}/commands/wallets.py (99%) rename bittensor/{ => btcli}/commands/weights.py (100%) create mode 100644 bittensor/core/__init__.py delete mode 100644 bittensor/mock/keyfile_mock.py delete mode 100644 bittensor/mock/wallet_mock.py create mode 100644 bittensor/settings.py diff --git a/bittensor/__init__.py b/bittensor/__init__.py index de1056f1f..68b0ff8dc 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -37,67 +37,6 @@ nest_asyncio.apply() - -# Bittensor code and protocol version. -__version__ = "7.3.0" - -_version_split = __version__.split(".") -__version_info__ = tuple(int(part) for part in _version_split) -_version_int_base = 1000 -assert max(__version_info__) < _version_int_base - -__version_as_int__: int = sum( - e * (_version_int_base**i) for i, e in enumerate(reversed(__version_info__)) -) -assert __version_as_int__ < 2**31 # fits in int32 -__new_signature_version__ = 360 - -# Rich console. -__console__ = Console() -__use_console__ = True - -# Remove overdue locals in debug training. -install(show_locals=False) - - -def __getattr__(name): - if name == "version_split": - warnings.warn( - "version_split is deprecated and will be removed in future versions. Use __version__ instead.", - DeprecationWarning, - ) - return _version_split - raise AttributeError(f"module {__name__} has no attribute {name}") - - -def turn_console_off(): - global __use_console__ - global __console__ - from io import StringIO - - __use_console__ = False - __console__ = Console(file=StringIO(), stderr=False) - - -def turn_console_on(): - global __use_console__ - global __console__ - __use_console__ = True - __console__ = Console() - - -turn_console_off() - - -# Logging helpers. -def trace(on: bool = True): - logging.set_trace(on) - - -def debug(on: bool = True): - logging.set_debug(on) - - # Substrate chain block time (seconds). __blocktime__ = 12 @@ -352,6 +291,27 @@ def debug(on: bool = True): ProposalVoteData, ) +# Bittensor code and protocol version. +__version__ = "7.3.0" + +_version_split = __version__.split(".") +__version_info__ = tuple(int(part) for part in _version_split) +_version_int_base = 1000 +assert max(__version_info__) < _version_int_base + +__version_as_int__: int = sum( + e * (_version_int_base**i) for i, e in enumerate(reversed(__version_info__)) +) +assert __version_as_int__ < 2**31 # fits in int32 +__new_signature_version__ = 360 + +# Rich console. +__console__ = Console() +__use_console__ = True + +# Remove overdue locals in debug training. +install(show_locals=False) + # Allows avoiding name spacing conflicts and continue access to the `subtensor` module with `subtensor_module` name from . import subtensor as subtensor_module @@ -360,7 +320,7 @@ def debug(on: bool = True): from .subtensor import Subtensor from .subtensor import Subtensor as subtensor -from .cli import cli as cli, COMMANDS as ALL_COMMANDS +from .btcli.cli import cli as cli, COMMANDS as ALL_COMMANDS from bittensor.utils.btlogging import logging from .metagraph import metagraph as metagraph from .threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor @@ -371,10 +331,7 @@ def debug(on: bool = True): from .axon import axon as axon from .dendrite import dendrite as dendrite -from .mock.keyfile_mock import MockKeyfile as MockKeyfile from .mock.subtensor_mock import MockSubtensor as MockSubtensor -from .mock.wallet_mock import MockWallet as MockWallet - from .subnets import SubnetsAPI as SubnetsAPI configs = [ @@ -385,3 +342,41 @@ def debug(on: bool = True): logging.get_config(), ] defaults = config.merge_all(configs) + + +def __getattr__(name): + if name == "version_split": + warnings.warn( + "version_split is deprecated and will be removed in future versions. Use __version__ instead.", + DeprecationWarning, + ) + return _version_split + raise AttributeError(f"module {__name__} has no attribute {name}") + + +def turn_console_off(): + global __use_console__ + global __console__ + from io import StringIO + + __use_console__ = False + __console__ = Console(file=StringIO(), stderr=False) + + +def turn_console_on(): + global __use_console__ + global __console__ + __use_console__ = True + __console__ = Console() + + +# Logging helpers. +def trace(on: bool = True): + logging.set_trace(on) + + +def debug(on: bool = True): + logging.set_debug(on) + + +turn_console_off() diff --git a/bittensor/btcli/__init__.py b/bittensor/btcli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bittensor/cli.py b/bittensor/btcli/cli.py similarity index 100% rename from bittensor/cli.py rename to bittensor/btcli/cli.py diff --git a/bittensor/commands/__init__.py b/bittensor/btcli/commands/__init__.py similarity index 100% rename from bittensor/commands/__init__.py rename to bittensor/btcli/commands/__init__.py diff --git a/bittensor/commands/check_coldkey_swap.py b/bittensor/btcli/commands/check_coldkey_swap.py similarity index 98% rename from bittensor/commands/check_coldkey_swap.py rename to bittensor/btcli/commands/check_coldkey_swap.py index 2b003e828..008ab610b 100644 --- a/bittensor/commands/check_coldkey_swap.py +++ b/bittensor/btcli/commands/check_coldkey_swap.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao +# 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 @@ -17,13 +17,14 @@ import argparse +from rich.console import Console from rich.prompt import Prompt import bittensor from bittensor.utils.formatting import convert_blocks_to_time from . import defaults -console = bittensor.__console__ +console = Console() def fetch_arbitration_stats(subtensor, wallet): diff --git a/bittensor/commands/delegates.py b/bittensor/btcli/commands/delegates.py similarity index 99% rename from bittensor/commands/delegates.py rename to bittensor/btcli/commands/delegates.py index 4d03b289e..909b66c1f 100644 --- a/bittensor/commands/delegates.py +++ b/bittensor/btcli/commands/delegates.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 OpenTensor Foundation - +# 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 @@ -20,7 +20,7 @@ import sys from typing import List, Dict, Optional -from rich.console import Text +from rich.console import Text, Console from rich.prompt import Prompt, FloatPrompt, Confirm from rich.table import Table from substrateinterface.exceptions import SubstrateRequestException @@ -42,7 +42,7 @@ def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: return wallets -console = bittensor.__console__ +console = Console() def show_delegates_lite( diff --git a/bittensor/commands/identity.py b/bittensor/btcli/commands/identity.py similarity index 100% rename from bittensor/commands/identity.py rename to bittensor/btcli/commands/identity.py diff --git a/bittensor/commands/inspect.py b/bittensor/btcli/commands/inspect.py similarity index 99% rename from bittensor/commands/inspect.py rename to bittensor/btcli/commands/inspect.py index 4ef0e84c4..4abcc733b 100644 --- a/bittensor/commands/inspect.py +++ b/bittensor/btcli/commands/inspect.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -16,10 +16,16 @@ # DEALINGS IN THE SOFTWARE. import argparse -import bittensor -from tqdm import tqdm -from rich.table import Table +import os +from typing import List, Tuple, Optional, Dict + +from rich.console import Console from rich.prompt import Prompt +from rich.table import Table +from tqdm import tqdm + +import bittensor +from . import defaults from .utils import ( get_delegates_details, DelegatesDetails, @@ -27,13 +33,8 @@ get_all_wallets_for_path, filter_netuids_by_registered_hotkeys, ) -from . import defaults -console = bittensor.__console__ - -import os -import bittensor -from typing import List, Tuple, Optional, Dict +console = Console() def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: diff --git a/bittensor/commands/list.py b/bittensor/btcli/commands/list.py similarity index 98% rename from bittensor/commands/list.py rename to bittensor/btcli/commands/list.py index b2946efff..6d5ccec8a 100644 --- a/bittensor/commands/list.py +++ b/bittensor/btcli/commands/list.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -19,9 +19,10 @@ import argparse import bittensor from rich import print +from rich.console import Console from rich.tree import Tree -console = bittensor.__console__ +console = Console() class ListCommand: diff --git a/bittensor/commands/metagraph.py b/bittensor/btcli/commands/metagraph.py similarity index 99% rename from bittensor/commands/metagraph.py rename to bittensor/btcli/commands/metagraph.py index 79fa48b78..1814053ce 100644 --- a/bittensor/commands/metagraph.py +++ b/bittensor/btcli/commands/metagraph.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -17,13 +17,13 @@ import argparse +from rich.console import Console from rich.table import Table import bittensor - from .utils import check_netuid_set -console = bittensor.__console__ # type: ignore +console = Console() class MetagraphCommand: diff --git a/bittensor/commands/misc.py b/bittensor/btcli/commands/misc.py similarity index 98% rename from bittensor/commands/misc.py rename to bittensor/btcli/commands/misc.py index ded1c7804..6254ff112 100644 --- a/bittensor/commands/misc.py +++ b/bittensor/btcli/commands/misc.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -18,10 +18,11 @@ import os import argparse import bittensor +from rich.console import Console from rich.prompt import Prompt from rich.table import Table -console = bittensor.__console__ +console = Console() class UpdateCommand: diff --git a/bittensor/commands/network.py b/bittensor/btcli/commands/network.py similarity index 99% rename from bittensor/commands/network.py rename to bittensor/btcli/commands/network.py index 3564bc534..1715100cb 100644 --- a/bittensor/commands/network.py +++ b/bittensor/btcli/commands/network.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -16,20 +16,23 @@ # DEALINGS IN THE SOFTWARE. import argparse -import bittensor -from . import defaults # type: ignore +from typing import List, Optional, Dict, Union, Tuple + +from rich.console import Console from rich.prompt import Prompt from rich.table import Table -from typing import List, Optional, Dict, Union, Tuple + +import bittensor +from . import defaults # type: ignore +from .identity import SetIdentityCommand from .utils import ( get_delegates_details, DelegatesDetails, check_netuid_set, normalize_hyperparameters, ) -from .identity import SetIdentityCommand -console = bittensor.__console__ +console = Console() class RegisterSubnetworkCommand: diff --git a/bittensor/commands/overview.py b/bittensor/btcli/commands/overview.py similarity index 99% rename from bittensor/commands/overview.py rename to bittensor/btcli/commands/overview.py index b572847e4..7a439e2fc 100644 --- a/bittensor/commands/overview.py +++ b/bittensor/btcli/commands/overview.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -16,24 +16,27 @@ # DEALINGS IN THE SOFTWARE. import argparse -import bittensor -from tqdm import tqdm -from concurrent.futures import ProcessPoolExecutor from collections import defaultdict +from concurrent.futures import ProcessPoolExecutor +from typing import List, Optional, Dict, Tuple + from fuzzywuzzy import fuzz from rich.align import Align -from rich.table import Table +from rich.console import Console from rich.prompt import Prompt -from typing import List, Optional, Dict, Tuple +from rich.table import Table +from tqdm import tqdm + +import bittensor +from . import defaults from .utils import ( get_hotkey_wallets_for_wallet, get_coldkey_wallets_for_path, get_all_wallets_for_path, filter_netuids_by_registered_hotkeys, ) -from . import defaults -console = bittensor.__console__ +console = Console() class OverviewCommand: diff --git a/bittensor/commands/register.py b/bittensor/btcli/commands/register.py similarity index 99% rename from bittensor/commands/register.py rename to bittensor/btcli/commands/register.py index a5a14773a..02f717edb 100644 --- a/bittensor/commands/register.py +++ b/bittensor/btcli/commands/register.py @@ -1,30 +1,32 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 sys import argparse -import bittensor -from rich.prompt import Prompt, Confirm -from .utils import check_netuid_set, check_for_cuda_reg_config +import sys from copy import deepcopy +from rich.console import Console +from rich.prompt import Prompt, Confirm + +import bittensor from . import defaults +from .utils import check_netuid_set, check_for_cuda_reg_config -console = bittensor.__console__ +console = Console() class RegisterCommand: diff --git a/bittensor/commands/root.py b/bittensor/btcli/commands/root.py similarity index 99% rename from bittensor/commands/root.py rename to bittensor/btcli/commands/root.py index 5607921b1..ed4994d0d 100644 --- a/bittensor/commands/root.py +++ b/bittensor/btcli/commands/root.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -21,13 +21,14 @@ import numpy as np import bittensor from typing import List, Optional, Dict +from rich.console import Console from rich.prompt import Prompt from rich.table import Table from .utils import get_delegates_details, DelegatesDetails from . import defaults -console = bittensor.__console__ +console = Console() class RootRegisterCommand: diff --git a/bittensor/commands/senate.py b/bittensor/btcli/commands/senate.py similarity index 99% rename from bittensor/commands/senate.py rename to bittensor/btcli/commands/senate.py index 03a73cde5..d96e19adc 100644 --- a/bittensor/commands/senate.py +++ b/bittensor/btcli/commands/senate.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -17,14 +17,17 @@ import argparse -import bittensor +from typing import Optional, Dict + +from rich.console import Console from rich.prompt import Prompt, Confirm from rich.table import Table -from typing import Optional, Dict -from .utils import get_delegates_details, DelegatesDetails + +import bittensor from . import defaults +from .utils import get_delegates_details, DelegatesDetails -console = bittensor.__console__ +console = Console() class SenateCommand: diff --git a/bittensor/commands/stake.py b/bittensor/btcli/commands/stake.py similarity index 99% rename from bittensor/commands/stake.py rename to bittensor/btcli/commands/stake.py index 1bc2cf278..f391e1e5a 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/btcli/commands/stake.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -20,20 +20,21 @@ import sys from typing import List, Union, Optional, Dict, Tuple +from rich.console import Console from rich.prompt import Confirm, Prompt from rich.table import Table from tqdm import tqdm import bittensor from bittensor.utils.balance import Balance +from . import defaults from .utils import ( get_hotkey_wallets_for_wallet, get_delegates_details, DelegatesDetails, ) -from . import defaults -console = bittensor.__console__ +console = Console() class StakeCommand: diff --git a/bittensor/commands/transfer.py b/bittensor/btcli/commands/transfer.py similarity index 98% rename from bittensor/commands/transfer.py rename to bittensor/btcli/commands/transfer.py index 24c6e7840..c7644683f 100644 --- a/bittensor/commands/transfer.py +++ b/bittensor/btcli/commands/transfer.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation - +# 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 @@ -19,10 +18,11 @@ import sys import argparse import bittensor +from rich.console import Console from rich.prompt import Prompt from . import defaults -console = bittensor.__console__ +console = Console() class TransferCommand: diff --git a/bittensor/commands/unstake.py b/bittensor/btcli/commands/unstake.py similarity index 99% rename from bittensor/commands/unstake.py rename to bittensor/btcli/commands/unstake.py index 87d13aab9..d7c843c86 100644 --- a/bittensor/commands/unstake.py +++ b/bittensor/btcli/commands/unstake.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -16,15 +16,18 @@ # DEALINGS IN THE SOFTWARE. import sys -import bittensor -from tqdm import tqdm +from typing import List, Union, Optional, Tuple + +from rich.console import Console from rich.prompt import Confirm, Prompt +from tqdm import tqdm + +import bittensor from bittensor.utils.balance import Balance -from typing import List, Union, Optional, Tuple -from .utils import get_hotkey_wallets_for_wallet from . import defaults +from .utils import get_hotkey_wallets_for_wallet -console = bittensor.__console__ +console = Console() class UnStakeCommand: diff --git a/bittensor/commands/utils.py b/bittensor/btcli/commands/utils.py similarity index 99% rename from bittensor/commands/utils.py rename to bittensor/btcli/commands/utils.py index 661cd818c..7ad60791b 100644 --- a/bittensor/commands/utils.py +++ b/bittensor/btcli/commands/utils.py @@ -15,19 +15,22 @@ # 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 sys import os -import bittensor -import requests -from bittensor.utils.registration import torch -from bittensor.utils.balance import Balance -from bittensor.utils import U64_NORMALIZED_FLOAT, U16_NORMALIZED_FLOAT +import sys +from dataclasses import dataclass from typing import List, Dict, Any, Optional, Tuple + +import requests +from rich.console import Console from rich.prompt import Confirm, PromptBase -from dataclasses import dataclass + +import bittensor +from bittensor.utils import U64_NORMALIZED_FLOAT, U16_NORMALIZED_FLOAT +from bittensor.utils.balance import Balance +from bittensor.utils.registration import torch from . import defaults -console = bittensor.__console__ +console = Console() class IntListPrompt(PromptBase): diff --git a/bittensor/commands/wallets.py b/bittensor/btcli/commands/wallets.py similarity index 99% rename from bittensor/commands/wallets.py rename to bittensor/btcli/commands/wallets.py index 15819ece7..5bc180832 100644 --- a/bittensor/commands/wallets.py +++ b/bittensor/btcli/commands/wallets.py @@ -26,7 +26,7 @@ import bittensor -from ..utils import RAOPERTAO +from bittensor.utils import RAOPERTAO from . import defaults @@ -191,7 +191,7 @@ def check_config(config: "bittensor.config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) - if config.ss58_address == None and config.public_key_hex == None: + if config.ss58_address is None and config.public_key_hex is None: prompt_answer = Prompt.ask( "Enter the ss58_address or the public key in hex" ) diff --git a/bittensor/commands/weights.py b/bittensor/btcli/commands/weights.py similarity index 100% rename from bittensor/commands/weights.py rename to bittensor/btcli/commands/weights.py diff --git a/bittensor/core/__init__.py b/bittensor/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bittensor/extrinsics/network.py b/bittensor/extrinsics/network.py index 16cbc0ed2..095d1199b 100644 --- a/bittensor/extrinsics/network.py +++ b/bittensor/extrinsics/network.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -22,8 +21,8 @@ from rich.prompt import Confirm import bittensor +from bittensor.btcli.commands.network import HYPERPARAMS from bittensor.utils import format_error_message -from ..commands.network import HYPERPARAMS def _find_event_attributes_in_extrinsic_receipt( diff --git a/bittensor/mock/keyfile_mock.py b/bittensor/mock/keyfile_mock.py deleted file mode 100644 index e13126cc1..000000000 --- a/bittensor/mock/keyfile_mock.py +++ /dev/null @@ -1,90 +0,0 @@ -# The MIT License (MIT) - -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies - -# 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. - -from bittensor import serialized_keypair_to_keyfile_data, keyfile, Keypair - - -class MockKeyfile(keyfile): - """Defines an interface to a mocked keyfile object (nothing is created on device) keypair is treated as non encrypted and the data is just the string version.""" - - def __init__(self, path: str): - super().__init__(path) - - self._mock_keypair = Keypair.create_from_mnemonic( - mnemonic="arrive produce someone view end scout bargain coil slight festival excess struggle" - ) - self._mock_data = serialized_keypair_to_keyfile_data(self._mock_keypair) - - def __str__(self): - if not self.exists_on_device(): - return "Keyfile (empty, {})>".format(self.path) - if self.is_encrypted(): - return "Keyfile (encrypted, {})>".format(self.path) - else: - return "Keyfile (decrypted, {})>".format(self.path) - - def __repr__(self): - return self.__str__() - - @property - def keypair(self) -> "Keypair": - return self._mock_keypair - - @property - def data(self) -> bytes: - return bytes(self._mock_data) - - @property - def keyfile_data(self) -> bytes: - return bytes(self._mock_data) - - def set_keypair( - self, - keypair: "Keypair", - encrypt: bool = True, - overwrite: bool = False, - password: str = None, - ): - self._mock_keypair = keypair - self._mock_data = serialized_keypair_to_keyfile_data(self._mock_keypair) - - def get_keypair(self, password: str = None) -> "Keypair": - return self._mock_keypair - - def make_dirs(self): - return - - def exists_on_device(self) -> bool: - return True - - def is_readable(self) -> bool: - return True - - def is_writable(self) -> bool: - return True - - def is_encrypted(self) -> bool: - return False - - def encrypt(self, password: str = None): - raise ValueError("Cannot encrypt a mock keyfile") - - def decrypt(self, password: str = None): - return diff --git a/bittensor/mock/wallet_mock.py b/bittensor/mock/wallet_mock.py deleted file mode 100644 index 35179f8c9..000000000 --- a/bittensor/mock/wallet_mock.py +++ /dev/null @@ -1,127 +0,0 @@ -# The MIT License (MIT) - -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies - -# 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 os -import bittensor -from typing import Optional -from Crypto.Hash import keccak - -from .keyfile_mock import MockKeyfile - - -class MockWallet(bittensor.wallet): - """ - Mocked Version of the bittensor wallet class, meant to be used for testing - """ - - def __init__(self, **kwargs): - r"""Init bittensor wallet object containing a hot and coldkey. - Args: - _mock (required=True, default=False): - If true creates a mock wallet with random keys. - """ - super().__init__(**kwargs) - # For mocking. - self._is_mock = True - self._mocked_coldkey_keyfile = None - self._mocked_hotkey_keyfile = None - - @property - def hotkey_file(self) -> "bittensor.keyfile": - if self._is_mock: - if self._mocked_hotkey_keyfile == None: - self._mocked_hotkey_keyfile = MockKeyfile(path="MockedHotkey") - return self._mocked_hotkey_keyfile - else: - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - hotkey_path = os.path.join(wallet_path, "hotkeys", self.hotkey_str) - return bittensor.keyfile(path=hotkey_path) - - @property - def coldkey_file(self) -> "bittensor.keyfile": - if self._is_mock: - if self._mocked_coldkey_keyfile == None: - self._mocked_coldkey_keyfile = MockKeyfile(path="MockedColdkey") - return self._mocked_coldkey_keyfile - else: - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkey_path = os.path.join(wallet_path, "coldkey") - return bittensor.keyfile(path=coldkey_path) - - @property - def coldkeypub_file(self) -> "bittensor.keyfile": - if self._is_mock: - if self._mocked_coldkey_keyfile == None: - self._mocked_coldkey_keyfile = MockKeyfile(path="MockedColdkeyPub") - return self._mocked_coldkey_keyfile - else: - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkeypub_path = os.path.join(wallet_path, "coldkeypub.txt") - return bittensor.keyfile(path=coldkeypub_path) - - -def get_mock_wallet( - coldkey: "bittensor.Keypair" = None, hotkey: "bittensor.Keypair" = None -): - wallet = MockWallet(name="mock_wallet", hotkey="mock", path="/tmp/mock_wallet") - - if not coldkey: - coldkey = bittensor.Keypair.create_from_mnemonic( - bittensor.Keypair.generate_mnemonic() - ) - if not hotkey: - hotkey = bittensor.Keypair.create_from_mnemonic( - bittensor.Keypair.generate_mnemonic() - ) - - wallet.set_coldkey(coldkey, encrypt=False, overwrite=True) - wallet.set_coldkeypub(coldkey, encrypt=False, overwrite=True) - wallet.set_hotkey(hotkey, encrypt=False, overwrite=True) - - return wallet - - -def get_mock_keypair(uid: int, test_name: Optional[str] = None) -> bittensor.Keypair: - """ - Returns a mock keypair from a uid and optional test_name. - If test_name is not provided, the uid is the only seed. - If test_name is provided, the uid is hashed with the test_name to create a unique seed for the test. - """ - if test_name is not None: - hashed_test_name: bytes = keccak.new( - digest_bits=256, data=test_name.encode("utf-8") - ).digest() - hashed_test_name_as_int: int = int.from_bytes( - hashed_test_name, byteorder="big", signed=False - ) - uid = uid + hashed_test_name_as_int - - return bittensor.Keypair.create_from_seed( - seed_hex=int.to_bytes(uid, 32, "big", signed=False), - ss58_format=bittensor.__ss58_format__, - ) - - -def get_mock_hotkey(uid: int) -> str: - return get_mock_keypair(uid).ss58_address - - -def get_mock_coldkey(uid: int) -> str: - return get_mock_keypair(uid).ss58_address diff --git a/bittensor/settings.py b/bittensor/settings.py new file mode 100644 index 000000000..8c6e3a0de --- /dev/null +++ b/bittensor/settings.py @@ -0,0 +1,17 @@ +# 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. + diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index 482f59ce2..e88b92e9d 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -16,16 +16,18 @@ # DEALINGS IN THE SOFTWARE. from typing import Union -from bittensor import Balance, NeuronInfo, AxonInfo, PrometheusInfo, __ss58_format__ -from bittensor.mock.wallet_mock import MockWallet as _MockWallet -from bittensor.mock.wallet_mock import get_mock_coldkey as _get_mock_coldkey -from bittensor.mock.wallet_mock import get_mock_hotkey as _get_mock_hotkey -from bittensor.mock.wallet_mock import get_mock_keypair as _get_mock_keypair -from bittensor.mock.wallet_mock import get_mock_wallet as _get_mock_wallet + +from bittensor_wallet.mock.wallet_mock import MockWallet as _MockWallet +from bittensor_wallet.mock.wallet_mock import get_mock_coldkey as _get_mock_coldkey +from bittensor_wallet.mock.wallet_mock import get_mock_hotkey as _get_mock_hotkey +from bittensor_wallet.mock.wallet_mock import get_mock_keypair as _get_mock_keypair +from bittensor_wallet.mock.wallet_mock import get_mock_wallet as _get_mock_wallet from rich.console import Console from rich.text import Text +from bittensor import Balance, NeuronInfo, AxonInfo, PrometheusInfo + def __mock_wallet_factory__(*args, **kwargs) -> _MockWallet: """Returns a mock wallet object.""" diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py index 263c7c1b9..e2bed67fb 100644 --- a/tests/integration_tests/test_cli.py +++ b/tests/integration_tests/test_cli.py @@ -29,9 +29,9 @@ import bittensor from bittensor import Balance -from bittensor.commands.delegates import _get_coldkey_wallets_for_path -from bittensor.commands.identity import SetIdentityCommand -from bittensor.commands.wallets import _get_coldkey_ss58_addresses_for_path +from bittensor.btcli.commands.delegates import _get_coldkey_wallets_for_path +from bittensor.btcli.commands.identity import SetIdentityCommand +from bittensor.btcli.commands.wallets import _get_coldkey_ss58_addresses_for_path from bittensor.mock import MockSubtensor from tests.helpers import is_running_in_circleci, MockConsole @@ -165,7 +165,7 @@ def mock_get_wallet(*args, **kwargs): mock_console = MockConsole() with patch( - "bittensor.commands.overview.get_hotkey_wallets_for_wallet" + "bittensor.btcli.commands.overview.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets with patch("bittensor.wallet") as mock_create_wallet: @@ -270,7 +270,7 @@ def mock_get_wallet(*args, **kwargs): mock_console = MockConsole() with patch( - "bittensor.commands.overview.get_hotkey_wallets_for_wallet" + "bittensor.btcli.commands.overview.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets with patch("bittensor.wallet") as mock_create_wallet: @@ -561,7 +561,7 @@ def mock_get_wallet(*args, **kwargs): return mock_wallets[0] with patch( - "bittensor.commands.unstake.get_hotkey_wallets_for_wallet" + "bittensor.btcli.commands.unstake.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets with patch("bittensor.wallet") as mock_create_wallet: @@ -640,7 +640,7 @@ def mock_get_wallet(*args, **kwargs): return mock_wallets[0] with patch( - "bittensor.commands.unstake.get_hotkey_wallets_for_wallet" + "bittensor.btcli.commands.unstake.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets with patch("bittensor.wallet") as mock_create_wallet: @@ -728,7 +728,7 @@ def mock_get_wallet(*args, **kwargs): return mock_wallets[0] with patch( - "bittensor.commands.unstake.get_hotkey_wallets_for_wallet" + "bittensor.btcli.commands.unstake.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets with patch("bittensor.wallet") as mock_create_wallet: @@ -1082,7 +1082,7 @@ def mock_get_wallet(*args, **kwargs): with patch("bittensor.wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet with patch( - "bittensor.commands.stake.get_hotkey_wallets_for_wallet" + "bittensor.btcli.commands.stake.get_hotkey_wallets_for_wallet" ) as mock_get_hotkey_wallets_for_wallet: mock_get_hotkey_wallets_for_wallet.return_value = mock_wallets @@ -1178,7 +1178,7 @@ def mock_get_wallet(*args, **kwargs): return mock_wallets[0] with patch( - "bittensor.commands.stake.get_hotkey_wallets_for_wallet" + "bittensor.btcli.commands.stake.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets with patch("bittensor.wallet") as mock_create_wallet: diff --git a/tests/integration_tests/test_cli_no_network.py b/tests/integration_tests/test_cli_no_network.py index e3a3d6a49..2470d1560 100644 --- a/tests/integration_tests/test_cli_no_network.py +++ b/tests/integration_tests/test_cli_no_network.py @@ -485,7 +485,7 @@ def return_mock_sub_3(*args, **kwargs): class TestCLIDefaultsNoNetwork(unittest.TestCase): def test_inspect_prompt_wallet_name(self, _): # Patch command to exit early - with patch("bittensor.commands.inspect.InspectCommand.run", return_value=None): + with patch("bittensor.btcli.commands.inspect.InspectCommand.run", return_value=None): # Test prompt happens when no wallet name is passed with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: cli = bittensor.cli( @@ -533,7 +533,7 @@ def test_inspect_prompt_wallet_name(self, _): def test_overview_prompt_wallet_name(self, _): # Patch command to exit early with patch( - "bittensor.commands.overview.OverviewCommand.run", return_value=None + "bittensor.btcli.commands.overview.OverviewCommand.run", return_value=None ): # Test prompt happens when no wallet name is passed with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: @@ -592,7 +592,7 @@ def test_stake_prompt_wallet_name_and_hotkey_name(self, _): "--all", ] # Patch command to exit early - with patch("bittensor.commands.stake.StakeCommand.run", return_value=None): + with patch("bittensor.btcli.commands.stake.StakeCommand.run", return_value=None): # Test prompt happens when # - wallet name IS NOT passed, AND # - hotkey name IS NOT passed @@ -753,7 +753,7 @@ def test_unstake_prompt_wallet_name_and_hotkey_name(self, _): "--all", ] # Patch command to exit early - with patch("bittensor.commands.unstake.UnStakeCommand.run", return_value=None): + with patch("bittensor.btcli.commands.unstake.UnStakeCommand.run", return_value=None): # Test prompt happens when # - wallet name IS NOT passed, AND # - hotkey name IS NOT passed @@ -916,7 +916,7 @@ def test_delegate_prompt_wallet_name(self, _): ] # Patch command to exit early with patch( - "bittensor.commands.delegates.DelegateStakeCommand.run", return_value=None + "bittensor.btcli.commands.delegates.DelegateStakeCommand.run", return_value=None ): # Test prompt happens when # - wallet name IS NOT passed @@ -977,7 +977,7 @@ def test_undelegate_prompt_wallet_name(self, _): ] # Patch command to exit early with patch( - "bittensor.commands.delegates.DelegateUnstakeCommand.run", return_value=None + "bittensor.btcli.commands.delegates.DelegateUnstakeCommand.run", return_value=None ): # Test prompt happens when # - wallet name IS NOT passed @@ -1035,7 +1035,7 @@ def test_history_prompt_wallet_name(self, _): ] # Patch command to exit early with patch( - "bittensor.commands.wallets.GetWalletHistoryCommand.run", return_value=None + "bittensor.btcli.commands.wallets.GetWalletHistoryCommand.run", return_value=None ): # Test prompt happens when # - wallet name IS NOT passed @@ -1099,7 +1099,7 @@ def test_delegate_prompt_hotkey(self, _): ] delegate_ss58 = _get_mock_coldkey(0) - with patch("bittensor.commands.delegates.show_delegates"): + with patch("bittensor.btcli.commands.delegates.show_delegates"): with patch( "bittensor.subtensor.Subtensor.get_delegates", return_value=[ @@ -1118,7 +1118,7 @@ def test_delegate_prompt_hotkey(self, _): ): # Patch command to exit early with patch( - "bittensor.commands.delegates.DelegateStakeCommand.run", + "bittensor.btcli.commands.delegates.DelegateStakeCommand.run", return_value=None, ): # Test prompt happens when @@ -1186,7 +1186,7 @@ def test_undelegate_prompt_hotkey(self, _): ] delegate_ss58 = _get_mock_coldkey(0) - with patch("bittensor.commands.delegates.show_delegates"): + with patch("bittensor.btcli.commands.delegates.show_delegates"): with patch( "bittensor.subtensor.Subtensor.get_delegates", return_value=[ @@ -1205,7 +1205,7 @@ def test_undelegate_prompt_hotkey(self, _): ): # Patch command to exit early with patch( - "bittensor.commands.delegates.DelegateUnstakeCommand.run", + "bittensor.btcli.commands.delegates.DelegateUnstakeCommand.run", return_value=None, ): # Test prompt happens when @@ -1280,7 +1280,7 @@ def test_vote_command_prompt_proposal_hash(self, _): ): # Patch command to exit early with patch( - "bittensor.commands.senate.VoteCommand.run", + "bittensor.btcli.commands.senate.VoteCommand.run", return_value=None, ): # Test prompt happens when diff --git a/tests/unit_tests/test_overview.py b/tests/unit_tests/test_overview.py index 638ab4df4..0d8d61017 100644 --- a/tests/unit_tests/test_overview.py +++ b/tests/unit_tests/test_overview.py @@ -7,7 +7,7 @@ # Bittensor import bittensor -from bittensor.commands.overview import OverviewCommand +from bittensor.btcli.commands import OverviewCommand from tests.unit_tests.factories.neuron_factory import NeuronInfoLiteFactory @@ -95,13 +95,13 @@ def test_get_total_balance( with patch( "bittensor.wallet", return_value=mock_wallet ) as mock_wallet_constructor, patch( - "bittensor.commands.overview.get_coldkey_wallets_for_path", + "bittensor.btcli.commands.overview.get_coldkey_wallets_for_path", return_value=[mock_wallet] if config_all else [], ), patch( - "bittensor.commands.overview.get_all_wallets_for_path", + "bittensor.btcli.commands.overview.get_all_wallets_for_path", return_value=[mock_wallet], ), patch( - "bittensor.commands.overview.get_hotkey_wallets_for_wallet", + "bittensor.btcli.commands.overview.get_hotkey_wallets_for_wallet", return_value=[mock_wallet], ): # Act diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 731285c22..c1b86db8f 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1,38 +1,31 @@ # The MIT License (MIT) -# Copyright © 2022 Opentensor Foundation - +# 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. -# Standard Lib import argparse import unittest.mock as mock from unittest.mock import MagicMock -# 3rd Party import pytest -# Application import bittensor -from bittensor.subtensor import ( - Subtensor, - _logger, - Balance, -) -from bittensor.chain_data import SubnetHyperparameters -from bittensor.commands.utils import normalize_hyperparameters from bittensor import subtensor_module +from bittensor.btcli.commands.utils import normalize_hyperparameters +from bittensor.chain_data import SubnetHyperparameters +from bittensor.subtensor import Subtensor, _logger from bittensor.utils.balance import Balance U16_MAX = 65535 From ff54d5714db818facbfcd0382d843631eff24783 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 20:08:25 -0700 Subject: [PATCH 018/260] Move constants to their related modules. --- bittensor/axon.py | 3 ++- bittensor/constants.py | 43 ------------------------------- bittensor/dendrite.py | 29 +++++++++++++-------- bittensor/utils/axon_utils.py | 14 +++++----- tests/unit_tests/test_axon.py | 16 ++++-------- tests/unit_tests/test_dendrite.py | 6 +---- 6 files changed, 32 insertions(+), 79 deletions(-) delete mode 100644 bittensor/constants.py diff --git a/bittensor/axon.py b/bittensor/axon.py index 13c60cdbd..2d54e85d4 100644 --- a/bittensor/axon.py +++ b/bittensor/axon.py @@ -45,7 +45,6 @@ import bittensor from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds -from bittensor.constants import V_7_2_0 from bittensor.errors import ( BlacklistedException, InvalidRequestNameError, @@ -60,6 +59,8 @@ from bittensor.threadpool import PriorityThreadPoolExecutor from bittensor.utils import networking +V_7_2_0 = 7002000 + class FastAPIThreadedServer(uvicorn.Server): """ diff --git a/bittensor/constants.py b/bittensor/constants.py deleted file mode 100644 index 74d3dd2e0..000000000 --- a/bittensor/constants.py +++ /dev/null @@ -1,43 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2023 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. - -# Standard Library -import asyncio -from typing import Dict, Type - -# 3rd Party -import aiohttp - - -ALLOWED_DELTA = 4_000_000_000 # Delta of 4 seconds for nonce validation -V_7_2_0 = 7002000 -NANOSECONDS_IN_SECOND = 1_000_000_000 - -#### Dendrite #### -DENDRITE_ERROR_MAPPING: Dict[Type[Exception], tuple] = { - aiohttp.ClientConnectorError: ("503", "Service unavailable"), - asyncio.TimeoutError: ("408", "Request timeout"), - aiohttp.ClientResponseError: (None, "Client response error"), - aiohttp.ClientPayloadError: ("400", "Payload error"), - aiohttp.ClientError: ("500", "Client error"), - aiohttp.ServerTimeoutError: ("504", "Server timeout error"), - aiohttp.ServerDisconnectedError: ("503", "Service disconnected"), - aiohttp.ServerConnectionError: ("503", "Service connection error"), -} - -DENDRITE_DEFAULT_ERROR = ("422", "Failed to parse response") -#### End Dendrite #### diff --git a/bittensor/dendrite.py b/bittensor/dendrite.py index 61aa83663..0058dba7c 100644 --- a/bittensor/dendrite.py +++ b/bittensor/dendrite.py @@ -1,37 +1,44 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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. -# Standard Library from __future__ import annotations + import asyncio import time -from typing import Optional, List, Union, AsyncGenerator, Any import uuid +from typing import Any, AsyncGenerator, Dict, List, Optional, Union, Type -# 3rd Party import aiohttp -# Application import bittensor -from bittensor.constants import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR from bittensor.utils.registration import torch, use_torch +DENDRITE_ERROR_MAPPING: Dict[Type[Exception], tuple] = { + aiohttp.ClientConnectorError: ("503", "Service unavailable"), + asyncio.TimeoutError: ("408", "Request timeout"), + aiohttp.ClientResponseError: (None, "Client response error"), + aiohttp.ClientPayloadError: ("400", "Payload error"), + aiohttp.ClientError: ("500", "Client error"), + aiohttp.ServerTimeoutError: ("504", "Server timeout error"), + aiohttp.ServerDisconnectedError: ("503", "Service disconnected"), + aiohttp.ServerConnectionError: ("503", "Service connection error"), +} +DENDRITE_DEFAULT_ERROR = ("422", "Failed to parse response") + class DendriteMixin: """ diff --git a/bittensor/utils/axon_utils.py b/bittensor/utils/axon_utils.py index 5912f389a..c380f14ae 100644 --- a/bittensor/utils/axon_utils.py +++ b/bittensor/utils/axon_utils.py @@ -1,26 +1,24 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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. - from typing import Optional -from bittensor.constants import ALLOWED_DELTA, NANOSECONDS_IN_SECOND +ALLOWED_DELTA = 4_000_000_000 # Delta of 4 seconds for nonce validation +NANOSECONDS_IN_SECOND = 1_000_000_000 def allowed_nonce_window_ns(current_time_ns: int, synapse_timeout: Optional[float]): diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index c25fc2e54..a1a55bebc 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -1,16 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 @@ -18,7 +16,6 @@ # DEALINGS IN THE SOFTWARE. -# Standard Lib import re import time from dataclasses import dataclass @@ -27,20 +24,17 @@ from unittest import IsolatedAsyncioTestCase from unittest.mock import AsyncMock, MagicMock, patch -# Third Party import netaddr import pytest from starlette.requests import Request from fastapi.testclient import TestClient -# Bittensor import bittensor from bittensor import Synapse, RunException from bittensor.axon import AxonMiddleware from bittensor.axon import axon as Axon -from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds -from bittensor.constants import ALLOWED_DELTA, NANOSECONDS_IN_SECOND +from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds, ALLOWED_DELTA, NANOSECONDS_IN_SECOND def test_attach(): diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index 0146bb778..438b00888 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -17,19 +17,15 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -# Standard Lib import asyncio import typing from unittest.mock import MagicMock, Mock -# Third Party import aiohttp import pytest -# Application import bittensor -from bittensor.constants import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR -from bittensor.dendrite import dendrite as Dendrite +from bittensor.dendrite import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR, dendrite as Dendrite from bittensor.synapse import TerminalInfo from tests.helpers import _get_mock_wallet From cce27b8c7f471ce1eadf35627d708a60384bcfcc Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 20:22:19 -0700 Subject: [PATCH 019/260] Refactoring types.py --- bittensor/types.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/bittensor/types.py b/bittensor/types.py index 8aa9b7cde..5fff1140c 100644 --- a/bittensor/types.py +++ b/bittensor/types.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Technologies Inc - +# 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 @@ -19,9 +19,7 @@ class AxonServeCallParams(TypedDict): - """ - Axon serve chain call parameters. - """ + """Axon serve chain call parameters.""" version: int ip: int @@ -31,10 +29,7 @@ class AxonServeCallParams(TypedDict): class PrometheusServeCallParams(TypedDict): - """ - Prometheus serve chain call parameters. - """ - + """Prometheus serve chain call parameters.""" version: int ip: int port: int From b61adb0da5a3100fd303038cd3367b510d329820 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 20:23:05 -0700 Subject: [PATCH 020/260] Refactoring and typos in errors.py --- bittensor/errors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor/errors.py b/bittensor/errors.py index 65c6c66e6..dea8db9d0 100644 --- a/bittensor/errors.py +++ b/bittensor/errors.py @@ -56,7 +56,7 @@ class NominationError(ChainTransactionError): class TakeError(ChainTransactionError): - """Error raised when a increase / decrease take transaction fails.""" + """Error raised when an increase / decrease take transaction fails.""" class TransferError(ChainTransactionError): @@ -85,7 +85,7 @@ class InvalidRequestNameError(Exception): class SynapseException(Exception): def __init__( - self, message="Synapse Exception", synapse: "bittensor.Synapse" | None = None + self, message="Synapse Exception", synapse: bittensor.Synapse | None = None ): self.message = message self.synapse = synapse @@ -128,7 +128,7 @@ class SynapseDendriteNoneException(SynapseException): def __init__( self, message="Synapse Dendrite is None", - synapse: "bittensor.Synapse" | None = None, + synapse: bittensor.Synapse | None = None, ): self.message = message super().__init__(self.message, synapse) From a82d38eb005f13be778539c9319937152d5e4e1b Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 20:32:52 -0700 Subject: [PATCH 021/260] Refactoring metagraph.py --- bittensor/metagraph.py | 75 +++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/bittensor/metagraph.py b/bittensor/metagraph.py index 8d7e97bcc..26b439095 100644 --- a/bittensor/metagraph.py +++ b/bittensor/metagraph.py @@ -1,34 +1,41 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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. -from abc import ABC, abstractmethod import os import pickle -import numpy as np -from numpy.typing import NDArray -import bittensor +from abc import ABC, abstractmethod from os import listdir from os.path import join from typing import List, Optional, Union, Tuple +import numpy as np +from numpy.typing import NDArray + +# import bittensor +from bittensor import __version__, __version_as_int__ from bittensor.chain_data import AxonInfo from bittensor.utils.registration import torch, use_torch +from bittensor.utils.btlogging import logging +from bittensor.subtensor import Subtensor +from rich.console import Console +from bittensor.utils.weight_utils import convert_weight_uids_and_vals_to_tensor + + +console = Console() METAGRAPH_STATE_DICT_NDARRAY_KEYS = [ "version", @@ -85,7 +92,7 @@ def latest_block_path(dir_path: str) -> str: if block_number > latest_block: latest_block = block_number latest_file_full_path = full_path_filename - except Exception as e: + except Exception: pass if not latest_file_full_path: raise ValueError(f"Metagraph not found at: {dir_path}") @@ -366,9 +373,7 @@ def addresses(self) -> List[str]: return [axon.ip_str() for axon in self.axons] @abstractmethod - def __init__( - self, netuid: int, network: str = "finney", lite: bool = True, sync: bool = True - ): + def __init__(self, netuid: int, network: str = "finney", lite: bool = True, sync: bool = True): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. This method is the entry point for creating a metagraph object, @@ -382,7 +387,6 @@ def __init__( Initializing a metagraph object for the Bittensor network with a specific network UID:: metagraph = metagraph(netuid=123, network="finney", lite=True, sync=True) """ - pass def __str__(self) -> str: """ @@ -395,7 +399,7 @@ def __str__(self) -> str: Example: When printing the metagraph object or using it in a string context, this method is automatically invoked:: - print(metagraph) # Output: "metagraph(netuid:1, n:100, block:500, network:finney)" + print(metagraph) # Output: "metagraph(netuid:1, n:100, block:500, network:finney)" """ return "metagraph(netuid:{}, n:{}, block:{}, network:{})".format( self.netuid, self.n.item(), self.block.item(), self.network @@ -441,7 +445,7 @@ def metadata(self) -> dict: "n": self.n.item(), "block": self.block.item(), "network": self.network, - "version": bittensor.__version__, + "version": __version__, } def state_dict(self): @@ -518,7 +522,7 @@ def sync( ): cur_block = subtensor.get_current_block() # type: ignore if block and block < (cur_block - 300): - bittensor.logging.warning( + logging.warning( "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' network for subtensor and retry." ) @@ -553,7 +557,7 @@ def _initialize_subtensor(self, subtensor): """ if not subtensor: # TODO: Check and test the initialization of the new subtensor - subtensor = bittensor.subtensor(network=self.network) + subtensor = Subtensor(network=self.network) return subtensor def _assign_neurons(self, block, lite, subtensor): @@ -603,7 +607,7 @@ def _create_tensor(data, dtype) -> Union[NDArray, "torch.nn.Parameter"]: else np.array(data, dtype=dtype) ) - def _set_weights_and_bonds(self, subtensor: Optional[bittensor.subtensor] = None): + def _set_weights_and_bonds(self, subtensor: "Optional[Subtensor]" = None): """ Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for processing the raw weight and bond data obtained from the network and converting it into a structured format suitable for the metagraph model. @@ -660,7 +664,7 @@ def _process_weights_or_bonds( # TODO: Validate and test the conversion of uids and values to tensor if attribute == "weights": data_array.append( - bittensor.utils.weight_utils.convert_weight_uids_and_vals_to_tensor( + convert_weight_uids_and_vals_to_tensor( len(self.neurons), list(uids), list(values), # type: ignore @@ -686,7 +690,7 @@ def _process_weights_or_bonds( ) ) if len(data_array) == 0: - bittensor.logging.warning( + logging.warning( f"Empty {attribute}_array on metagraph.sync(). The '{attribute}' tensor is empty." ) return tensor_param @@ -696,7 +700,7 @@ def _set_metagraph_attributes(self, block, subtensor): pass def _process_root_weights( - self, data, attribute: str, subtensor: bittensor.subtensor + self, data, attribute: str, subtensor: "Subtensor" ) -> Union[NDArray, "torch.nn.Parameter"]: """ Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` but is tailored for processing root weights, which have a different structure and significance in the network. @@ -712,10 +716,7 @@ def _process_root_weights( Internal Usage: Used internally to process and set root weights for the metagraph:: - self.root_weights = self._process_root_weights( - raw_root_weights_data, "weights", subtensor - ) - + self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) """ data_array = [] n_subnets = subtensor.get_total_subnets() or 0 @@ -749,7 +750,7 @@ def _process_root_weights( ) ) if len(data_array) == 0: - bittensor.logging.warning( + logging.warning( f"Empty {attribute}_array on metagraph.sync(). The '{attribute}' tensor is empty." ) return tensor_param @@ -783,7 +784,7 @@ def save(self) -> "metagraph": # type: ignore state_dict = self.state_dict() state_dict["axons"] = self.axons torch.save(state_dict, graph_filename) - state_dict = torch.load( + torch.load( graph_filename ) # verifies that the file can be loaded correctly else: @@ -874,7 +875,7 @@ def __init__( self.netuid = netuid self.network = network self.version = torch.nn.Parameter( - torch.tensor([bittensor.__version_as_int__], dtype=torch.int64), + torch.tensor([__version_as_int__], dtype=torch.int64), requires_grad=False, ) self.n: torch.nn.Parameter = torch.nn.Parameter( @@ -948,9 +949,7 @@ def _set_metagraph_attributes(self, block, subtensor): self._set_metagraph_attributes(block, subtensor) """ self.n = self._create_tensor(len(self.neurons), dtype=torch.int64) - self.version = self._create_tensor( - [bittensor.__version_as_int__], dtype=torch.int64 - ) + self.version = self._create_tensor([__version_as_int__], dtype=torch.int64) self.block = self._create_tensor( block if block else subtensor.block, dtype=torch.int64 ) @@ -1047,7 +1046,7 @@ def __init__( self.netuid = netuid self.network = network - self.version = (np.array([bittensor.__version_as_int__], dtype=np.int64),) + self.version = (np.array([__version_as_int__], dtype=np.int64),) self.n = np.array([0], dtype=np.int64) self.block = np.array([0], dtype=np.int64) self.stake = np.array([], dtype=np.float32) @@ -1087,7 +1086,7 @@ def _set_metagraph_attributes(self, block, subtensor): # TODO: Check and test the setting of each attribute self.n = self._create_tensor(len(self.neurons), dtype=np.int64) self.version = self._create_tensor( - [bittensor.__version_as_int__], dtype=np.int64 + [__version_as_int__], dtype=np.int64 ) self.block = self._create_tensor( block if block else subtensor.block, dtype=np.int64 @@ -1139,10 +1138,10 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore with open(graph_filename, "rb") as graph_file: state_dict = pickle.load(graph_file) except pickle.UnpicklingError: - bittensor.__console__.print( + console.print( "Unable to load file. Attempting to restore metagraph using torch." ) - bittensor.__console__.print( + console.print( ":warning:[yellow]Warning:[/yellow] This functionality exists to load " "metagraph state from legacy saves, but will not be supported in the future." ) @@ -1154,7 +1153,7 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore state_dict[key] = state_dict[key].detach().numpy() del real_torch except (RuntimeError, ImportError): - bittensor.__console__.print("Unable to load file. It may be corrupted.") + console.print("Unable to load file. It may be corrupted.") raise self.n = state_dict["n"] From 91df5eb1b6275ad209be6af10e2df4dccf263bf0 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 20:42:40 -0700 Subject: [PATCH 022/260] Refactoring metagraph.py --- bittensor/metagraph.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/bittensor/metagraph.py b/bittensor/metagraph.py index 26b439095..7ac89ad8b 100644 --- a/bittensor/metagraph.py +++ b/bittensor/metagraph.py @@ -24,16 +24,14 @@ import numpy as np from numpy.typing import NDArray +from rich.console import Console -# import bittensor -from bittensor import __version__, __version_as_int__ +from bittensor import __version__, __version_as_int__, __archive_entrypoint__ from bittensor.chain_data import AxonInfo -from bittensor.utils.registration import torch, use_torch -from bittensor.utils.btlogging import logging from bittensor.subtensor import Subtensor -from rich.console import Console -from bittensor.utils.weight_utils import convert_weight_uids_and_vals_to_tensor - +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import torch, use_torch +from bittensor.utils.weight_utils import convert_weight_uids_and_vals_to_tensor, convert_bond_uids_and_vals_to_tensor console = Console() @@ -477,7 +475,7 @@ def sync( self, block: Optional[int] = None, lite: bool = True, - subtensor: Optional["bittensor.subtensor"] = None, + subtensor: Optional["Subtensor"] = None, ): """ Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes @@ -488,7 +486,7 @@ def sync( This allows for historical analysis or specific state examination of the network. lite (bool): If True, a lite version of the metagraph is used for quicker synchronization. This is beneficial when full detail is not necessary, allowing for reduced computational and time overhead. - subtensor (Optional[bittensor.subtensor]): An instance of the subtensor class from Bittensor, providing an + subtensor (Optional[Subtensor]): An instance of the subtensor class from Bittensor, providing an interface to the underlying blockchain data. If provided, this instance is used for data retrieval during synchronization. @@ -517,7 +515,7 @@ def sync( subtensor = self._initialize_subtensor(subtensor) if ( - subtensor.chain_endpoint != bittensor.__archive_entrypoint__ # type: ignore + subtensor.chain_endpoint != __archive_entrypoint__ # type: ignore or subtensor.network != "archive" # type: ignore ): cur_block = subtensor.get_current_block() # type: ignore @@ -672,7 +670,7 @@ def _process_weights_or_bonds( ) else: data_array.append( - bittensor.utils.weight_utils.convert_bond_uids_and_vals_to_tensor( # type: ignore + convert_bond_uids_and_vals_to_tensor( # type: ignore len(self.neurons), list(uids), list(values) ).astype(np.float32) ) @@ -731,7 +729,7 @@ def _process_root_weights( uids, values = zip(*item) # TODO: Validate and test the conversion of uids and values to tensor data_array.append( - bittensor.utils.weight_utils.convert_root_weight_uids_and_vals_to_tensor( # type: ignore + convert_root_weight_uids_and_vals_to_tensor( # type: ignore n_subnets, list(uids), list(values), subnets ) ) From 94b2be9774ffd549c8d1ed653dd51baec00bc030 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 29 Jul 2024 20:48:34 -0700 Subject: [PATCH 023/260] metagraph.py to `bittensor/core` --- bittensor/__init__.py | 2 +- bittensor/{ => core}/metagraph.py | 0 tests/integration_tests/test_metagraph_integration.py | 2 +- tests/unit_tests/test_metagraph.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename bittensor/{ => core}/metagraph.py (100%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 68b0ff8dc..a9c14fd03 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -322,7 +322,7 @@ from .btcli.cli import cli as cli, COMMANDS as ALL_COMMANDS from bittensor.utils.btlogging import logging -from .metagraph import metagraph as metagraph +from bittensor.core.metagraph import metagraph as metagraph from .threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor from .synapse import TerminalInfo, Synapse diff --git a/bittensor/metagraph.py b/bittensor/core/metagraph.py similarity index 100% rename from bittensor/metagraph.py rename to bittensor/core/metagraph.py diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py index 5dbb9ddfc..d5c2043f1 100644 --- a/tests/integration_tests/test_metagraph_integration.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -20,7 +20,7 @@ import torch import os from bittensor.mock import MockSubtensor -from bittensor.metagraph import METAGRAPH_STATE_DICT_NDARRAY_KEYS, get_save_dir +from bittensor.core.metagraph import METAGRAPH_STATE_DICT_NDARRAY_KEYS, get_save_dir _subtensor_mock: MockSubtensor = MockSubtensor() diff --git a/tests/unit_tests/test_metagraph.py b/tests/unit_tests/test_metagraph.py index af0dbdba7..181ee7c9d 100644 --- a/tests/unit_tests/test_metagraph.py +++ b/tests/unit_tests/test_metagraph.py @@ -20,7 +20,7 @@ import numpy as np import bittensor -from bittensor.metagraph import metagraph as Metagraph +from bittensor.core.metagraph import metagraph as Metagraph from unittest.mock import MagicMock From 71f23fbb9d7ceaaa5729e55ba04396e4916c5e10 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 09:11:47 -0700 Subject: [PATCH 024/260] axon.py to `bittensor/core` --- bittensor/__init__.py | 6 ++-- bittensor/{ => core}/axon.py | 38 +++++++++++---------- tests/unit_tests/extrinsics/test_serving.py | 2 +- tests/unit_tests/test_axon.py | 11 +++--- 4 files changed, 28 insertions(+), 29 deletions(-) rename bittensor/{ => core}/axon.py (98%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index a9c14fd03..84f7f023c 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -321,14 +321,14 @@ from .subtensor import Subtensor as subtensor from .btcli.cli import cli as cli, COMMANDS as ALL_COMMANDS -from bittensor.utils.btlogging import logging -from bittensor.core.metagraph import metagraph as metagraph +from .utils.btlogging import logging +from .core.metagraph import metagraph as metagraph from .threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor from .synapse import TerminalInfo, Synapse from .stream import StreamingSynapse from .tensor import tensor, Tensor -from .axon import axon as axon +from bittensor.core.axon import Axon as axon from .dendrite import dendrite as dendrite from .mock.subtensor_mock import MockSubtensor as MockSubtensor diff --git a/bittensor/axon.py b/bittensor/core/axon.py similarity index 98% rename from bittensor/axon.py rename to bittensor/core/axon.py index 2d54e85d4..e9f5ab87d 100644 --- a/bittensor/axon.py +++ b/bittensor/core/axon.py @@ -58,6 +58,8 @@ ) from bittensor.threadpool import PriorityThreadPoolExecutor from bittensor.utils import networking +from bittensor.synapse import Synapse + V_7_2_0 = 7002000 @@ -155,7 +157,7 @@ def stop(self): self.should_exit = True -class axon: +class Axon: """ The ``axon`` class in Bittensor is a fundamental component that serves as the server-side interface for a neuron within the Bittensor network. @@ -320,7 +322,7 @@ def __init__( """ # Build and check config. if config is None: - config = axon.config() + config = Axon.config() config = copy.deepcopy(config) config.axon.ip = ip or config.axon.get("ip", bittensor.defaults.axon.ip) config.axon.port = port or config.axon.get("port", bittensor.defaults.axon.port) @@ -333,7 +335,7 @@ def __init__( config.axon.max_workers = max_workers or config.axon.get( "max_workers", bittensor.defaults.axon.max_workers ) - axon.check_config(config) + Axon.check_config(config) self.config = config # type: ignore # Get wallet or use default. @@ -346,7 +348,7 @@ def __init__( self.external_ip = ( self.config.axon.external_ip # type: ignore if self.config.axon.external_ip is not None # type: ignore - else bittensor.utils.networking.get_external_ip() + else networking.get_external_ip() ) self.external_port = ( self.config.axon.external_port # type: ignore @@ -387,7 +389,7 @@ def __init__( self.app.add_middleware(self.middleware_cls, axon=self) # Attach default forward. - def ping(r: bittensor.Synapse) -> bittensor.Synapse: + def ping(r: Synapse) -> Synapse: return r self.attach( @@ -483,7 +485,7 @@ def verify_custom(synapse: MyCustomSynapse): param_class = first_param.annotation assert issubclass( - param_class, bittensor.Synapse + param_class, Synapse ), "The first argument of forward_fn must inherit from bittensor.Synapse" request_name = param_class.__name__ @@ -565,7 +567,7 @@ def config(cls) -> "bittensor.config": bittensor.config: Configuration object with settings from command-line arguments. """ parser = argparse.ArgumentParser() - axon.add_args(parser) # Add specific axon-related arguments + Axon.add_args(parser) # Add specific axon-related arguments return bittensor.config(parser, args=[]) @classmethod @@ -574,7 +576,7 @@ def help(cls): Prints the help text (list of command-line arguments and their descriptions) to stdout. """ parser = argparse.ArgumentParser() - axon.add_args(parser) # Add specific axon-related arguments + Axon.add_args(parser) # Add specific axon-related arguments print(cls.__new__.__doc__) # Print docstring of the class parser.print_help() # Print parser's help text @@ -708,11 +710,11 @@ def check_config(cls, config: "bittensor.config"): AssertionError: If the axon or external ports are not in range [1024, 65535] """ assert ( - config.axon.port > 1024 and config.axon.port < 65535 + 1024 < config.axon.port < 65535 ), "Axon port must be in range [1024, 65535]" assert config.axon.external_port is None or ( - config.axon.external_port > 1024 and config.axon.external_port < 65535 + 1024 < config.axon.external_port < 65535 ), "External port must be in range [1024, 65535]" def to_string(self): @@ -831,7 +833,7 @@ def serve( subtensor.serve_axon(netuid=netuid, axon=self) return self - async def default_verify(self, synapse: bittensor.Synapse): + async def default_verify(self, synapse: Synapse): """ This method is used to verify the authenticity of a received message using a digital signature. @@ -949,7 +951,7 @@ async def default_verify(self, synapse: bittensor.Synapse): raise SynapseDendriteNoneException(synapse=synapse) -def create_error_response(synapse: bittensor.Synapse): +def create_error_response(synapse: Synapse): if synapse.axon is None: return JSONResponse( status_code=400, @@ -965,11 +967,11 @@ def create_error_response(synapse: bittensor.Synapse): def log_and_handle_error( - synapse: bittensor.Synapse, + synapse: Synapse, exception: Exception, status_code: typing.Optional[int] = None, start_time: typing.Optional[float] = None, -) -> bittensor.Synapse: +) -> Synapse: if isinstance(exception, SynapseException): synapse = exception.synapse or synapse @@ -1086,12 +1088,12 @@ async def dispatch( try: # Set up the synapse from its headers. try: - synapse: bittensor.Synapse = await self.preprocess(request) + synapse: Synapse = await self.preprocess(request) except Exception as exc: if isinstance(exc, SynapseException) and exc.synapse is not None: synapse = exc.synapse else: - synapse = bittensor.Synapse() + synapse = Synapse() raise # Logs the start of the request processing @@ -1154,7 +1156,7 @@ async def dispatch( # Return the response to the requester. return response - async def preprocess(self, request: Request) -> bittensor.Synapse: + async def preprocess(self, request: Request) -> Synapse: """ Performs the initial processing of the incoming request. This method is responsible for extracting relevant information from the request and setting up the Synapse object, which @@ -1437,7 +1439,7 @@ async def run( @classmethod async def synapse_to_response( - cls, synapse: bittensor.Synapse, start_time: float + cls, synapse: "Synapse", start_time: float ) -> JSONResponse: """ Converts the Synapse object into a JSON response with HTTP headers. diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 3269397a2..dd338e55a 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -20,7 +20,7 @@ import pytest from bittensor_wallet import Wallet -from bittensor.axon import axon as Axon +from bittensor.core.axon import Axon from bittensor.extrinsics.serving import ( serve_extrinsic, publish_metadata, diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index a1a55bebc..4293ef03c 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -19,21 +19,18 @@ import re import time from dataclasses import dataclass - from typing import Any from unittest import IsolatedAsyncioTestCase from unittest.mock import AsyncMock, MagicMock, patch import netaddr - import pytest -from starlette.requests import Request from fastapi.testclient import TestClient +from starlette.requests import Request import bittensor from bittensor import Synapse, RunException -from bittensor.axon import AxonMiddleware -from bittensor.axon import axon as Axon +from bittensor.core.axon import Axon, AxonMiddleware from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds, ALLOWED_DELTA, NANOSECONDS_IN_SECOND @@ -115,7 +112,7 @@ def wrong_forward_fn(synapse: NonInheritedSynapse) -> Any: def test_log_and_handle_error(): - from bittensor.axon import log_and_handle_error + from bittensor.core.axon import log_and_handle_error synapse = SynapseMock() @@ -126,7 +123,7 @@ def test_log_and_handle_error(): def test_create_error_response(): - from bittensor.axon import create_error_response + from bittensor.core.axon import create_error_response synapse = SynapseMock() synapse.axon.status_code = 500 From c55dcadec9d24e77bc79cdc1eedaf8a013b74a87 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 10:37:43 -0700 Subject: [PATCH 025/260] error.py and types.py to `bittensor/core` --- bittensor/__init__.py | 2 +- bittensor/core/axon.py | 16 ++++++-------- bittensor/{ => core}/errors.py | 0 bittensor/{ => core}/types.py | 0 bittensor/extrinsics/delegation.py | 11 +++++----- bittensor/extrinsics/serving.py | 11 +++++----- bittensor/extrinsics/staking.py | 22 +++++++++---------- bittensor/extrinsics/unstaking.py | 22 +++++++++---------- bittensor/mock/subtensor_mock.py | 2 +- bittensor/subtensor.py | 13 +++++------ .../unit_tests/extrinsics/test_delegation.py | 2 +- tests/unit_tests/extrinsics/test_staking.py | 19 +++++++++++++++- 12 files changed, 66 insertions(+), 54 deletions(-) rename bittensor/{ => core}/errors.py (100%) rename bittensor/{ => core}/types.py (100%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 84f7f023c..f088ef749 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -206,7 +206,7 @@ }, } -from .errors import ( +from .core.errors import ( BlacklistedException, ChainConnectionError, ChainError, diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index e9f5ab87d..5a002ebb7 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -1,24 +1,22 @@ -"""Create and initialize Axon, which services the forward and backward requests from other neurons.""" - # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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. +"""Create and initialize Axon, which services the forward and backward requests from other neurons.""" + import argparse import asyncio import contextlib @@ -45,7 +43,7 @@ import bittensor from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds -from bittensor.errors import ( +from .errors import ( BlacklistedException, InvalidRequestNameError, NotVerifiedException, diff --git a/bittensor/errors.py b/bittensor/core/errors.py similarity index 100% rename from bittensor/errors.py rename to bittensor/core/errors.py diff --git a/bittensor/types.py b/bittensor/core/types.py similarity index 100% rename from bittensor/types.py rename to bittensor/core/types.py diff --git a/bittensor/extrinsics/delegation.py b/bittensor/extrinsics/delegation.py index ad9b95302..e19f003c3 100644 --- a/bittensor/extrinsics/delegation.py +++ b/bittensor/extrinsics/delegation.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -18,7 +17,7 @@ import logging import bittensor -from ..errors import ( +from ..core.errors import ( NominationError, NotDelegateError, NotRegisteredError, diff --git a/bittensor/extrinsics/serving.py b/bittensor/extrinsics/serving.py index bba5367de..2fefc97d1 100644 --- a/bittensor/extrinsics/serving.py +++ b/bittensor/extrinsics/serving.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -25,7 +24,7 @@ import bittensor import bittensor.utils.networking as net from bittensor.utils import format_error_message -from ..errors import MetadataError +from ..core.errors import MetadataError def serve_extrinsic( diff --git a/bittensor/extrinsics/staking.py b/bittensor/extrinsics/staking.py index 298bb1f0d..699e79fa9 100644 --- a/bittensor/extrinsics/staking.py +++ b/bittensor/extrinsics/staking.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -21,6 +20,7 @@ from time import sleep from typing import List, Union, Optional, Tuple from bittensor.utils.balance import Balance +from ..core.errors import NotDelegateError def _check_threshold_amount( @@ -103,7 +103,7 @@ def add_stake_extrinsic( if not own_hotkey: # This is not the wallet's own hotkey so we are delegating. if not subtensor.is_hotkey_delegate(hotkey_ss58): - raise bittensor.errors.NotDelegateError( + raise NotDelegateError( "Hotkey: {} is not a delegate.".format(hotkey_ss58) ) @@ -228,14 +228,14 @@ def add_stake_extrinsic( ) return False - except bittensor.errors.NotRegisteredError as e: + except bittensor.core.errors.NotRegisteredError as e: bittensor.__console__.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( wallet.hotkey_str ) ) return False - except bittensor.errors.StakeError as e: + except bittensor.core.errors.StakeError as e: bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) return False @@ -435,14 +435,14 @@ def add_stake_multiple_extrinsic( ) continue - except bittensor.errors.NotRegisteredError as e: + except bittensor.core.errors.NotRegisteredError as e: bittensor.__console__.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( hotkey_ss58 ) ) continue - except bittensor.errors.StakeError as e: + except bittensor.core.errors.StakeError as e: bittensor.__console__.print( ":cross_mark: [red]Stake Error: {}[/red]".format(e) ) @@ -510,7 +510,7 @@ def __do_add_stake_single( # We are delegating. # Verify that the hotkey is a delegate. if not subtensor.is_hotkey_delegate(hotkey_ss58=hotkey_ss58): - raise bittensor.errors.NotDelegateError( + raise NotDelegateError( "Hotkey: {} is not a delegate.".format(hotkey_ss58) ) diff --git a/bittensor/extrinsics/unstaking.py b/bittensor/extrinsics/unstaking.py index 105bb145b..89af9cdad 100644 --- a/bittensor/extrinsics/unstaking.py +++ b/bittensor/extrinsics/unstaking.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -21,6 +20,7 @@ from time import sleep from typing import List, Union, Optional from bittensor.utils.balance import Balance +from bittensor.core.errors import NotRegisteredError, StakeError def __do_remove_stake_single( @@ -51,9 +51,9 @@ def __do_remove_stake_single( success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. Raises: - bittensor.errors.StakeError: + bittensor.core.errors.StakeError: If the extrinsic fails to be finalized or included in the block. - bittensor.errors.NotRegisteredError: + bittensor.core.errors.NotRegisteredError: If the hotkey is not registered in any subnets. """ @@ -232,14 +232,14 @@ def unstake_extrinsic( ) return False - except bittensor.errors.NotRegisteredError as e: + except NotRegisteredError as e: bittensor.__console__.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( wallet.hotkey_str ) ) return False - except bittensor.errors.StakeError as e: + except StakeError as e: bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) return False @@ -424,12 +424,12 @@ def unstake_multiple_extrinsic( ) continue - except bittensor.errors.NotRegisteredError as e: + except NotRegisteredError as e: bittensor.__console__.print( ":cross_mark: [red]{} is not registered.[/red]".format(hotkey_ss58) ) continue - except bittensor.errors.StakeError as e: + except StakeError as e: bittensor.__console__.print( ":cross_mark: [red]Stake Error: {}[/red]".format(e) ) diff --git a/bittensor/mock/subtensor_mock.py b/bittensor/mock/subtensor_mock.py index 63cfc891d..1e3bc2a32 100644 --- a/bittensor/mock/subtensor_mock.py +++ b/bittensor/mock/subtensor_mock.py @@ -35,7 +35,7 @@ SubnetInfo, AxonInfo, ) -from ..errors import ChainQueryError +from ..core.errors import ChainQueryError from ..subtensor import Subtensor from ..utils import RAOPERTAO, U16_NORMALIZED_FLOAT from ..utils.balance import Balance diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index edc37efd0..b70aa9ed3 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -55,7 +54,7 @@ IPInfo, custom_rpc_type_registry, ) -from .errors import IdentityError, NominationError, StakeError, TakeError +from .core.errors import IdentityError, NominationError, StakeError, TakeError from .extrinsics.commit_weights import ( commit_weights_extrinsic, reveal_weights_extrinsic, @@ -94,7 +93,7 @@ from .extrinsics.staking import add_stake_extrinsic, add_stake_multiple_extrinsic from .extrinsics.transfer import transfer_extrinsic from .extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic -from .types import AxonServeCallParams, PrometheusServeCallParams +from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams from .utils import ( U16_NORMALIZED_FLOAT, ss58_to_vec_u8, diff --git a/tests/unit_tests/extrinsics/test_delegation.py b/tests/unit_tests/extrinsics/test_delegation.py index 729f5baf8..d7c210e92 100644 --- a/tests/unit_tests/extrinsics/test_delegation.py +++ b/tests/unit_tests/extrinsics/test_delegation.py @@ -20,7 +20,7 @@ import pytest from bittensor_wallet.wallet import Wallet -from bittensor.errors import ( +from bittensor.core.errors import ( NominationError, NotDelegateError, NotRegisteredError, diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py index c3b888520..f4a8838e7 100644 --- a/tests/unit_tests/extrinsics/test_staking.py +++ b/tests/unit_tests/extrinsics/test_staking.py @@ -1,3 +1,20 @@ +# 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 pytest from unittest.mock import patch, MagicMock import bittensor @@ -6,7 +23,7 @@ add_stake_extrinsic, add_stake_multiple_extrinsic, ) -from bittensor.errors import NotDelegateError +from bittensor.core.errors import NotDelegateError # Mocking external dependencies From e9ce941ef4395ffe46f8adb0f6770ed93193ec8e Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 10:48:10 -0700 Subject: [PATCH 026/260] subnets.py to `bittensor/core` --- bittensor/__init__.py | 2 +- bittensor/{ => core}/subnets.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) rename bittensor/{ => core}/subnets.py (95%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index f088ef749..5b920d75d 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -332,7 +332,7 @@ from .dendrite import dendrite as dendrite from .mock.subtensor_mock import MockSubtensor as MockSubtensor -from .subnets import SubnetsAPI as SubnetsAPI +from bittensor.core.subnets import SubnetsAPI as SubnetsAPI configs = [ axon.config(), diff --git a/bittensor/subnets.py b/bittensor/core/subnets.py similarity index 95% rename from bittensor/subnets.py rename to bittensor/core/subnets.py index 836a20dcb..6b174b672 100644 --- a/bittensor/subnets.py +++ b/bittensor/core/subnets.py @@ -1,16 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 From 006e1a5e79bd29242c85cb6b1f47c370a9f27d81 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 10:58:39 -0700 Subject: [PATCH 027/260] synapse.py to `bittensor/core` --- bittensor/__init__.py | 4 ++-- bittensor/core/axon.py | 2 +- bittensor/{ => core}/synapse.py | 9 ++++----- tests/unit_tests/test_dendrite.py | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) rename bittensor/{ => core}/synapse.py (99%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 5b920d75d..a2fe8c675 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -325,10 +325,10 @@ from .core.metagraph import metagraph as metagraph from .threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor -from .synapse import TerminalInfo, Synapse +from .core.synapse import TerminalInfo, Synapse from .stream import StreamingSynapse from .tensor import tensor, Tensor -from bittensor.core.axon import Axon as axon +from .core.axon import Axon as axon from .dendrite import dendrite as dendrite from .mock.subtensor_mock import MockSubtensor as MockSubtensor diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 5a002ebb7..383fd9665 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -56,7 +56,7 @@ ) from bittensor.threadpool import PriorityThreadPoolExecutor from bittensor.utils import networking -from bittensor.synapse import Synapse +from .synapse import Synapse V_7_2_0 = 7002000 diff --git a/bittensor/synapse.py b/bittensor/core/synapse.py similarity index 99% rename from bittensor/synapse.py rename to bittensor/core/synapse.py index 80053f706..cf74be0c8 100644 --- a/bittensor/synapse.py +++ b/bittensor/core/synapse.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation - +# 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 diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index 438b00888..73bf52da0 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -26,7 +26,7 @@ import bittensor from bittensor.dendrite import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR, dendrite as Dendrite -from bittensor.synapse import TerminalInfo +from bittensor.core.synapse import TerminalInfo from tests.helpers import _get_mock_wallet From ccbc28dceb2af0bb8237b48789c14c41bddac758 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 11:34:10 -0700 Subject: [PATCH 028/260] tensor.py to `bittensor/core` --- bittensor/__init__.py | 2 +- bittensor/{ => core}/tensor.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) rename bittensor/{ => core}/tensor.py (99%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index a2fe8c675..825967c99 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -327,7 +327,7 @@ from .core.synapse import TerminalInfo, Synapse from .stream import StreamingSynapse -from .tensor import tensor, Tensor +from .core.tensor import tensor, Tensor from .core.axon import Axon as axon from .dendrite import dendrite as dendrite diff --git a/bittensor/tensor.py b/bittensor/core/tensor.py similarity index 99% rename from bittensor/tensor.py rename to bittensor/core/tensor.py index ab46560d9..3eebd404c 100644 --- a/bittensor/tensor.py +++ b/bittensor/core/tensor.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation - +# 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 From 1c4744a59e55761de6df3e7e4f5e3842818c18ea Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 11:44:44 -0700 Subject: [PATCH 029/260] stream.py to `bittensor/core` --- bittensor/__init__.py | 2 +- bittensor/{ => core}/stream.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename bittensor/{ => core}/stream.py (100%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 825967c99..5abd61c6c 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -326,7 +326,7 @@ from .threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor from .core.synapse import TerminalInfo, Synapse -from .stream import StreamingSynapse +from .core.stream import StreamingSynapse from .core.tensor import tensor, Tensor from .core.axon import Axon as axon from .dendrite import dendrite as dendrite diff --git a/bittensor/stream.py b/bittensor/core/stream.py similarity index 100% rename from bittensor/stream.py rename to bittensor/core/stream.py From a7a39b7ca1bdf1b72a5897a8d35c7637e43f20fe Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 11:57:11 -0700 Subject: [PATCH 030/260] threadpool.py to `bittensor/core` --- bittensor/__init__.py | 5 ++--- bittensor/core/axon.py | 5 +++-- bittensor/{ => core}/threadpool.py | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename bittensor/{ => core}/threadpool.py (100%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 5abd61c6c..74eab704b 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -323,8 +323,7 @@ from .btcli.cli import cli as cli, COMMANDS as ALL_COMMANDS from .utils.btlogging import logging from .core.metagraph import metagraph as metagraph -from .threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor - +from .core.threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor from .core.synapse import TerminalInfo, Synapse from .core.stream import StreamingSynapse from .core.tensor import tensor, Tensor @@ -332,7 +331,7 @@ from .dendrite import dendrite as dendrite from .mock.subtensor_mock import MockSubtensor as MockSubtensor -from bittensor.core.subnets import SubnetsAPI as SubnetsAPI +from .core.subnets import SubnetsAPI as SubnetsAPI configs = [ axon.config(), diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 383fd9665..ad00327fd 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -42,6 +42,7 @@ from substrateinterface import Keypair import bittensor +from bittensor.utils import networking from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds from .errors import ( BlacklistedException, @@ -54,9 +55,9 @@ SynapseParsingError, UnknownSynapseError, ) -from bittensor.threadpool import PriorityThreadPoolExecutor -from bittensor.utils import networking + from .synapse import Synapse +from .threadpool import PriorityThreadPoolExecutor V_7_2_0 = 7002000 diff --git a/bittensor/threadpool.py b/bittensor/core/threadpool.py similarity index 100% rename from bittensor/threadpool.py rename to bittensor/core/threadpool.py From f660bad7e2ef269caa040e4d8278914ca020a3a9 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 11:59:31 -0700 Subject: [PATCH 031/260] dendrite.py to `bittensor/core` --- bittensor/__init__.py | 4 ++-- bittensor/{ => core}/dendrite.py | 0 tests/unit_tests/test_dendrite.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename bittensor/{ => core}/dendrite.py (100%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 74eab704b..c25061f19 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -328,10 +328,10 @@ from .core.stream import StreamingSynapse from .core.tensor import tensor, Tensor from .core.axon import Axon as axon -from .dendrite import dendrite as dendrite +from .core.dendrite import dendrite as dendrite +from .core.subnets import SubnetsAPI as SubnetsAPI from .mock.subtensor_mock import MockSubtensor as MockSubtensor -from .core.subnets import SubnetsAPI as SubnetsAPI configs = [ axon.config(), diff --git a/bittensor/dendrite.py b/bittensor/core/dendrite.py similarity index 100% rename from bittensor/dendrite.py rename to bittensor/core/dendrite.py diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index 73bf52da0..4296242fa 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -25,7 +25,7 @@ import pytest import bittensor -from bittensor.dendrite import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR, dendrite as Dendrite +from bittensor.core.dendrite import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR, dendrite as Dendrite from bittensor.core.synapse import TerminalInfo from tests.helpers import _get_mock_wallet From 9838ed30074dccba415e58153e1439f00e6b6d0c Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 12:16:33 -0700 Subject: [PATCH 032/260] chain_data.py to `bittensor/core` --- bittensor/__init__.py | 2 +- bittensor/{ => core}/chain_data.py | 6 +++--- bittensor/core/metagraph.py | 2 +- bittensor/mock/subtensor_mock.py | 2 +- bittensor/subtensor.py | 2 +- tests/unit_tests/factories/neuron_factory.py | 2 +- tests/unit_tests/test_chain_data.py | 4 ++-- tests/unit_tests/test_subtensor.py | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) rename bittensor/{ => core}/chain_data.py (99%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index c25061f19..498ae5870 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -277,7 +277,7 @@ ) from .utils.balance import Balance as Balance -from .chain_data import ( +from .core.chain_data import ( AxonInfo, NeuronInfo, NeuronInfoLite, diff --git a/bittensor/chain_data.py b/bittensor/core/chain_data.py similarity index 99% rename from bittensor/chain_data.py rename to bittensor/core/chain_data.py index 029cb2982..88fb34537 100644 --- a/bittensor/chain_data.py +++ b/bittensor/core/chain_data.py @@ -31,9 +31,9 @@ from scalecodec.utils.ss58 import ss58_encode import bittensor -from .utils import networking as net, RAOPERTAO, U16_NORMALIZED_FLOAT -from .utils.balance import Balance -from .utils.registration import torch, use_torch +from bittensor.utils import networking as net, RAOPERTAO, U16_NORMALIZED_FLOAT +from bittensor.utils.balance import Balance +from bittensor.utils.registration import torch, use_torch custom_rpc_type_registry = { "types": { diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 7ac89ad8b..a4caafb8f 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -27,7 +27,7 @@ from rich.console import Console from bittensor import __version__, __version_as_int__, __archive_entrypoint__ -from bittensor.chain_data import AxonInfo +from .chain_data import AxonInfo from bittensor.subtensor import Subtensor from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch diff --git a/bittensor/mock/subtensor_mock.py b/bittensor/mock/subtensor_mock.py index 1e3bc2a32..6b70a2ce9 100644 --- a/bittensor/mock/subtensor_mock.py +++ b/bittensor/mock/subtensor_mock.py @@ -27,7 +27,7 @@ from bittensor_wallet import Wallet -from ..chain_data import ( +from bittensor.core.chain_data import ( NeuronInfo, NeuronInfoLite, PrometheusInfo, diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index b70aa9ed3..372fc1058 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -40,7 +40,7 @@ import bittensor from bittensor.utils.btlogging import logging as _logger from bittensor.utils import torch, weight_utils, format_error_message -from .chain_data import ( +from .core.chain_data import ( DelegateInfoLite, NeuronInfo, DelegateInfo, diff --git a/tests/unit_tests/factories/neuron_factory.py b/tests/unit_tests/factories/neuron_factory.py index 4ad70c5dc..f99a084ac 100644 --- a/tests/unit_tests/factories/neuron_factory.py +++ b/tests/unit_tests/factories/neuron_factory.py @@ -1,6 +1,6 @@ import factory -from bittensor.chain_data import AxonInfo, NeuronInfoLite, PrometheusInfo +from bittensor.core.chain_data import AxonInfo, NeuronInfoLite, PrometheusInfo from bittensor.utils.balance import Balance diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index a6474bbee..fc793c0f3 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -1,7 +1,7 @@ import pytest import bittensor import torch -from bittensor.chain_data import AxonInfo, ChainDataType, DelegateInfo, NeuronInfo +from bittensor.core.chain_data import AxonInfo, ChainDataType, DelegateInfo, NeuronInfo SS58_FORMAT = bittensor.__ss58_format__ RAOPERTAO = 10**18 @@ -511,7 +511,7 @@ def test_fix_decoded_values_error_cases( @pytest.fixture def mock_from_scale_encoding(mocker): - return mocker.patch("bittensor.chain_data.from_scale_encoding") + return mocker.patch("bittensor.core.chain_data.from_scale_encoding") @pytest.fixture diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index c1b86db8f..51d5972e5 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -24,7 +24,7 @@ import bittensor from bittensor import subtensor_module from bittensor.btcli.commands.utils import normalize_hyperparameters -from bittensor.chain_data import SubnetHyperparameters +from bittensor.core.chain_data import SubnetHyperparameters from bittensor.subtensor import Subtensor, _logger from bittensor.utils.balance import Balance From 9c952eb46baeae1eec8e0b756d9f23f4b90e439b Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 12:17:03 -0700 Subject: [PATCH 033/260] settings.py to `bittensor/core` --- bittensor/{ => core}/settings.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bittensor/{ => core}/settings.py (100%) diff --git a/bittensor/settings.py b/bittensor/core/settings.py similarity index 100% rename from bittensor/settings.py rename to bittensor/core/settings.py From c9e59a708925ad1b79fc420529295e3bbc55df5b Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 12:18:13 -0700 Subject: [PATCH 034/260] create bittensor.api sub-package --- bittensor/api/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 bittensor/api/__init__.py diff --git a/bittensor/api/__init__.py b/bittensor/api/__init__.py new file mode 100644 index 000000000..e69de29bb From e4c1ab45f319b67705ee321db1148d11ed95b4b8 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 12:47:14 -0700 Subject: [PATCH 035/260] Move subtensor.py to bittensor.core and fix related test --- bittensor/__init__.py | 42 ++++++++------ bittensor/core/axon.py | 4 +- bittensor/core/metagraph.py | 2 +- bittensor/core/subnets.py | 2 +- bittensor/{ => core}/subtensor.py | 56 +++++++++---------- bittensor/core/threadpool.py | 6 +- bittensor/mock/subtensor_mock.py | 10 ++-- .../unit_tests/extrinsics/test_delegation.py | 2 +- tests/unit_tests/extrinsics/test_network.py | 2 +- .../unit_tests/extrinsics/test_prometheus.py | 2 +- .../extrinsics/test_registration.py | 2 +- tests/unit_tests/extrinsics/test_root.py | 2 +- tests/unit_tests/extrinsics/test_serving.py | 2 +- tests/unit_tests/test_subtensor.py | 4 +- 14 files changed, 73 insertions(+), 65 deletions(-) rename bittensor/{ => core}/subtensor.py (99%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 498ae5870..33c8d6b49 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -238,7 +238,7 @@ from bittensor_wallet.config import ( InvalidConfigFile, DefaultConfig, - Config as config, + Config, T, ) from bittensor_wallet.keyfile import ( @@ -257,7 +257,7 @@ decrypt_keyfile_data, Keyfile as keyfile, ) -from bittensor_wallet.wallet import display_mnemonic_msg, Wallet as wallet +from bittensor_wallet.wallet import display_mnemonic_msg, Wallet from .utils import ( ss58_to_vec_u8, @@ -312,35 +312,27 @@ # Remove overdue locals in debug training. install(show_locals=False) -# Allows avoiding name spacing conflicts and continue access to the `subtensor` module with `subtensor_module` name -from . import subtensor as subtensor_module - -# Double import allows using class `Subtensor` by referencing `bittensor.Subtensor` and `bittensor.subtensor`. -# This will be available for a while until we remove reference `bittensor.subtensor` -from .subtensor import Subtensor -from .subtensor import Subtensor as subtensor - from .btcli.cli import cli as cli, COMMANDS as ALL_COMMANDS -from .utils.btlogging import logging +from .core.subtensor import Subtensor from .core.metagraph import metagraph as metagraph from .core.threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor from .core.synapse import TerminalInfo, Synapse from .core.stream import StreamingSynapse from .core.tensor import tensor, Tensor -from .core.axon import Axon as axon +from .core.axon import Axon from .core.dendrite import dendrite as dendrite from .core.subnets import SubnetsAPI as SubnetsAPI - from .mock.subtensor_mock import MockSubtensor as MockSubtensor +from .utils.btlogging import logging configs = [ - axon.config(), - subtensor.config(), + Axon.config(), + Subtensor.config(), PriorityThreadPoolExecutor.config(), - wallet.config(), + Wallet.config(), logging.get_config(), ] -defaults = config.merge_all(configs) +defaults = Config.merge_all(configs) def __getattr__(name): @@ -379,3 +371,19 @@ def debug(on: bool = True): turn_console_off() + +# Backwards compatibility with previous bittensor versions. +from .core.subtensor import Subtensor as subtensor +from .core.axon import Axon as axon +from bittensor_wallet.wallet import Wallet as wallet +from bittensor_wallet.config import Config as config + +__all__ = [ + "axon", + "config", + "subtensor", + "wallet", + "InvalidConfigFile", + "DefaultConfig", + "T" +] diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index ad00327fd..8a9afb243 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -567,7 +567,7 @@ def config(cls) -> "bittensor.config": """ parser = argparse.ArgumentParser() Axon.add_args(parser) # Add specific axon-related arguments - return bittensor.config(parser, args=[]) + return bittensor.Config(parser, args=[]) @classmethod def help(cls): @@ -803,7 +803,7 @@ def stop(self) -> "bittensor.axon": return self def serve( - self, netuid: int, subtensor: Optional[bittensor.subtensor] = None + self, netuid: int, subtensor: Optional["bittensor.subtensor"] = None ) -> "bittensor.axon": """ Serves the Axon on the specified subtensor connection using the configured wallet. This method diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index a4caafb8f..2d9bc45be 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -28,7 +28,7 @@ from bittensor import __version__, __version_as_int__, __archive_entrypoint__ from .chain_data import AxonInfo -from bittensor.subtensor import Subtensor +from .subtensor import Subtensor from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch from bittensor.utils.weight_utils import convert_weight_uids_and_vals_to_tensor, convert_bond_uids_and_vals_to_tensor diff --git a/bittensor/core/subnets.py b/bittensor/core/subnets.py index 6b174b672..8bb79c131 100644 --- a/bittensor/core/subnets.py +++ b/bittensor/core/subnets.py @@ -44,7 +44,7 @@ def process_responses(self, responses: List[Union["bt.Synapse", Any]]) -> Any: async def query_api( self, - axons: Union[bt.axon, List[bt.axon]], + axons: Union["bt.Axon", List["bt.Axon"]], deserialize: Optional[bool] = False, timeout: Optional[int] = 12, **kwargs: Optional[Any], diff --git a/bittensor/subtensor.py b/bittensor/core/subtensor.py similarity index 99% rename from bittensor/subtensor.py rename to bittensor/core/subtensor.py index 372fc1058..a26a72387 100644 --- a/bittensor/subtensor.py +++ b/bittensor/core/subtensor.py @@ -40,7 +40,7 @@ import bittensor from bittensor.utils.btlogging import logging as _logger from bittensor.utils import torch, weight_utils, format_error_message -from .core.chain_data import ( +from .chain_data import ( DelegateInfoLite, NeuronInfo, DelegateInfo, @@ -54,56 +54,56 @@ IPInfo, custom_rpc_type_registry, ) -from .core.errors import IdentityError, NominationError, StakeError, TakeError -from .extrinsics.commit_weights import ( +from .errors import IdentityError, NominationError, StakeError, TakeError +from bittensor.extrinsics.commit_weights import ( commit_weights_extrinsic, reveal_weights_extrinsic, ) -from .extrinsics.delegation import ( +from bittensor.extrinsics.delegation import ( delegate_extrinsic, nominate_extrinsic, undelegate_extrinsic, increase_take_extrinsic, decrease_take_extrinsic, ) -from .extrinsics.network import ( +from bittensor.extrinsics.network import ( register_subnetwork_extrinsic, set_hyperparameter_extrinsic, ) -from .extrinsics.prometheus import prometheus_extrinsic -from .extrinsics.registration import ( +from bittensor.extrinsics.prometheus import prometheus_extrinsic +from bittensor.extrinsics.registration import ( register_extrinsic, burned_register_extrinsic, run_faucet_extrinsic, swap_hotkey_extrinsic, ) -from .extrinsics.root import root_register_extrinsic, set_root_weights_extrinsic -from .extrinsics.senate import ( +from bittensor.extrinsics.root import root_register_extrinsic, set_root_weights_extrinsic +from bittensor.extrinsics.senate import ( register_senate_extrinsic, leave_senate_extrinsic, vote_senate_extrinsic, ) -from .extrinsics.serving import ( +from bittensor.extrinsics.serving import ( serve_extrinsic, serve_axon_extrinsic, publish_metadata, get_metadata, ) -from .extrinsics.set_weights import set_weights_extrinsic -from .extrinsics.staking import add_stake_extrinsic, add_stake_multiple_extrinsic -from .extrinsics.transfer import transfer_extrinsic -from .extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic -from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams -from .utils import ( +from bittensor.extrinsics.set_weights import set_weights_extrinsic +from bittensor.extrinsics.staking import add_stake_extrinsic, add_stake_multiple_extrinsic +from bittensor.extrinsics.transfer import transfer_extrinsic +from bittensor.extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic +from .types import AxonServeCallParams, PrometheusServeCallParams +from bittensor.utils import ( U16_NORMALIZED_FLOAT, ss58_to_vec_u8, U64_NORMALIZED_FLOAT, networking, ) -from .utils.balance import Balance -from .utils.registration import POWSolution -from .utils.registration import legacy_torch_api_compat -from .utils.subtensor import get_subtensor_errors +from bittensor.utils.balance import Balance +from bittensor.utils.registration import POWSolution +from bittensor.utils.registration import legacy_torch_api_compat +from bittensor.utils.subtensor import get_subtensor_errors KEY_NONCE: Dict[str, int] = {} @@ -169,7 +169,7 @@ class Subtensor: def __init__( self, network: Optional[str] = None, - config: Optional[bittensor.config] = None, + config: Optional[bittensor.Config] = None, _mock: bool = False, log_verbose: bool = True, ) -> None: @@ -188,7 +188,7 @@ def __init__( network (str, optional): The network name to connect to (e.g., ``finney``, ``local``). This can also be the chain endpoint (e.g., ``wss://entrypoint-finney.opentensor.ai:443``) and will be correctly parsed into the network and chain endpoint. If not specified, defaults to the main Bittensor network. - config (bittensor.config, optional): Configuration object for the subtensor. If not provided, a default + config (bittensor.Config, optional): Configuration object for the subtensor. If not provided, a default configuration is used. _mock (bool, optional): If set to ``True``, uses a mocked connection for testing purposes. @@ -202,7 +202,7 @@ def __init__( # Argument importance: network > chain_endpoint > config.subtensor.chain_endpoint > config.subtensor.network # Check if network is a config object. (Single argument passed as first positional) - if isinstance(network, bittensor.config): + if isinstance(network, bittensor.Config): if network.subtensor is None: _logger.warning( "If passing a bittensor config object, it must not be empty. Using default subtensor config." @@ -285,17 +285,17 @@ def __repr__(self) -> str: return self.__str__() @staticmethod - def config() -> "bittensor.config": + def config() -> "bittensor.Config": """ Creates and returns a Bittensor configuration object. Returns: - config (bittensor.config): A Bittensor configuration object configured with arguments added by the + config (bittensor.Config): A Bittensor configuration object configured with arguments added by the `subtensor.add_args` method. """ parser = argparse.ArgumentParser() Subtensor.add_args(parser) - return bittensor.config(parser, args=[]) + return bittensor.Config(parser, args=[]) @classmethod def help(cls): @@ -405,7 +405,7 @@ def determine_chain_endpoint_and_network(network: str): return "unknown", network @staticmethod - def setup_config(network: str, config: "bittensor.config"): + def setup_config(network: str, config: "bittensor.Config"): """ Sets up and returns the configuration for the Subtensor network and endpoint. @@ -420,7 +420,7 @@ def setup_config(network: str, config: "bittensor.config"): Args: network (str): The name of the Subtensor network. If None, the network and endpoint will be determined from the `config` object. - config (bittensor.config): The configuration object containing the network and chain endpoint settings. + config (bittensor.Config): The configuration object containing the network and chain endpoint settings. Returns: tuple: A tuple containing the formatted WebSocket endpoint URL and the evaluated network name. diff --git a/bittensor/core/threadpool.py b/bittensor/core/threadpool.py index bd7aaa63d..e3b0d92f1 100644 --- a/bittensor/core/threadpool.py +++ b/bittensor/core/threadpool.py @@ -197,14 +197,14 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): pass @classmethod - def config(cls) -> "bittensor.config": + def config(cls) -> "bittensor.Config": """Get config from the argument parser. - Return: :func:`bittensor.config` object. + Return: :func:`bittensor.Config` object. """ parser = argparse.ArgumentParser() PriorityThreadPoolExecutor.add_args(parser) - return bittensor.config(parser, args=[]) + return bittensor.Config(parser, args=[]) @property def is_empty(self): diff --git a/bittensor/mock/subtensor_mock.py b/bittensor/mock/subtensor_mock.py index 6b70a2ce9..ade11ba7a 100644 --- a/bittensor/mock/subtensor_mock.py +++ b/bittensor/mock/subtensor_mock.py @@ -35,11 +35,11 @@ SubnetInfo, AxonInfo, ) -from ..core.errors import ChainQueryError -from ..subtensor import Subtensor -from ..utils import RAOPERTAO, U16_NORMALIZED_FLOAT -from ..utils.balance import Balance -from ..utils.registration import POWSolution +from bittensor.core.subtensor import Subtensor +from bittensor.core.errors import ChainQueryError +from bittensor.utils import RAOPERTAO, U16_NORMALIZED_FLOAT +from bittensor.utils.balance import Balance +from bittensor.utils.registration import POWSolution # Mock Testing Constant __GLOBAL_MOCK_STATE__ = {} diff --git a/tests/unit_tests/extrinsics/test_delegation.py b/tests/unit_tests/extrinsics/test_delegation.py index d7c210e92..b85087fe1 100644 --- a/tests/unit_tests/extrinsics/test_delegation.py +++ b/tests/unit_tests/extrinsics/test_delegation.py @@ -31,7 +31,7 @@ delegate_extrinsic, undelegate_extrinsic, ) -from bittensor.subtensor import Subtensor +from bittensor.core.subtensor import Subtensor from bittensor.utils.balance import Balance diff --git a/tests/unit_tests/extrinsics/test_network.py b/tests/unit_tests/extrinsics/test_network.py index ede01bf5f..f5196d7b3 100644 --- a/tests/unit_tests/extrinsics/test_network.py +++ b/tests/unit_tests/extrinsics/test_network.py @@ -24,7 +24,7 @@ set_hyperparameter_extrinsic, register_subnetwork_extrinsic, ) -from bittensor.subtensor import Subtensor +from bittensor.core.subtensor import Subtensor # Mock the bittensor and related modules to avoid real network calls and wallet operations diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index 237e2051b..12b88d563 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -22,7 +22,7 @@ import bittensor from bittensor.extrinsics.prometheus import prometheus_extrinsic -from bittensor.subtensor import Subtensor +from bittensor.core.subtensor import Subtensor # Mocking the bittensor and networking modules diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index b06f8b5ec..07c0b8d13 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -27,7 +27,7 @@ burned_register_extrinsic, register_extrinsic, ) -from bittensor.subtensor import Subtensor +from bittensor.core.subtensor import Subtensor from bittensor.utils.registration import POWSolution diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index b801f7b4e..5ee77bd47 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -1,6 +1,6 @@ import pytest from unittest.mock import MagicMock, patch -from bittensor.subtensor import Subtensor +from bittensor.core.subtensor import Subtensor from bittensor.extrinsics.root import ( root_register_extrinsic, set_root_weights_extrinsic, diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index dd338e55a..e2fd5cd9b 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -26,7 +26,7 @@ publish_metadata, serve_axon_extrinsic, ) -from bittensor.subtensor import Subtensor +from bittensor.core.subtensor import Subtensor @pytest.fixture diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 51d5972e5..6973ee53d 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -22,10 +22,10 @@ import pytest import bittensor -from bittensor import subtensor_module +from bittensor.core import subtensor as subtensor_module from bittensor.btcli.commands.utils import normalize_hyperparameters from bittensor.core.chain_data import SubnetHyperparameters -from bittensor.subtensor import Subtensor, _logger +from bittensor.core.subtensor import Subtensor, _logger from bittensor.utils.balance import Balance U16_MAX = 65535 From 13081bfac784916f5a5c89cde7497ee64899e140 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 13:12:11 -0700 Subject: [PATCH 036/260] Move `bittensor.extrinsics` to `bittensor.api.extrinsics` and fix related test. --- bittensor/{ => api}/extrinsics/__init__.py | 8 +++---- .../{ => api}/extrinsics/commit_weights.py | 9 ++++--- bittensor/{ => api}/extrinsics/delegation.py | 2 +- bittensor/{ => api}/extrinsics/network.py | 0 bittensor/{ => api}/extrinsics/prometheus.py | 9 ++++--- .../{ => api}/extrinsics/registration.py | 9 ++++--- bittensor/{ => api}/extrinsics/root.py | 9 ++++--- bittensor/{ => api}/extrinsics/senate.py | 9 ++++--- bittensor/{ => api}/extrinsics/serving.py | 2 +- bittensor/{ => api}/extrinsics/set_weights.py | 9 ++++--- bittensor/{ => api}/extrinsics/staking.py | 2 +- bittensor/{ => api}/extrinsics/transfer.py | 13 +++++----- bittensor/{ => api}/extrinsics/unstaking.py | 0 bittensor/core/subtensor.py | 24 +++++++++---------- tests/integration_tests/test_cli.py | 2 +- .../test_subtensor_integration.py | 4 ++-- .../unit_tests/extrinsics/test_delegation.py | 2 +- tests/unit_tests/extrinsics/test_network.py | 2 +- .../unit_tests/extrinsics/test_prometheus.py | 2 +- .../extrinsics/test_registration.py | 8 +++---- tests/unit_tests/extrinsics/test_root.py | 2 +- tests/unit_tests/extrinsics/test_senate.py | 14 +++++------ tests/unit_tests/extrinsics/test_serving.py | 8 +++---- .../unit_tests/extrinsics/test_set_weights.py | 2 +- tests/unit_tests/extrinsics/test_staking.py | 2 +- tests/unit_tests/extrinsics/test_unstaking.py | 2 +- 26 files changed, 74 insertions(+), 81 deletions(-) rename bittensor/{ => api}/extrinsics/__init__.py (95%) rename bittensor/{ => api}/extrinsics/commit_weights.py (98%) rename bittensor/{ => api}/extrinsics/delegation.py (99%) rename bittensor/{ => api}/extrinsics/network.py (100%) rename bittensor/{ => api}/extrinsics/prometheus.py (98%) rename bittensor/{ => api}/extrinsics/registration.py (99%) rename bittensor/{ => api}/extrinsics/root.py (99%) rename bittensor/{ => api}/extrinsics/senate.py (99%) rename bittensor/{ => api}/extrinsics/serving.py (99%) rename bittensor/{ => api}/extrinsics/set_weights.py (98%) rename bittensor/{ => api}/extrinsics/staking.py (99%) rename bittensor/{ => api}/extrinsics/transfer.py (97%) rename bittensor/{ => api}/extrinsics/unstaking.py (100%) diff --git a/bittensor/extrinsics/__init__.py b/bittensor/api/extrinsics/__init__.py similarity index 95% rename from bittensor/extrinsics/__init__.py rename to bittensor/api/extrinsics/__init__.py index 5780b2ee8..640a13250 100644 --- a/bittensor/extrinsics/__init__.py +++ b/bittensor/api/extrinsics/__init__.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Foundation - +# 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 diff --git a/bittensor/extrinsics/commit_weights.py b/bittensor/api/extrinsics/commit_weights.py similarity index 98% rename from bittensor/extrinsics/commit_weights.py rename to bittensor/api/extrinsics/commit_weights.py index a9192952e..90cb2474c 100644 --- a/bittensor/extrinsics/commit_weights.py +++ b/bittensor/api/extrinsics/commit_weights.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 diff --git a/bittensor/extrinsics/delegation.py b/bittensor/api/extrinsics/delegation.py similarity index 99% rename from bittensor/extrinsics/delegation.py rename to bittensor/api/extrinsics/delegation.py index e19f003c3..e981a0dd6 100644 --- a/bittensor/extrinsics/delegation.py +++ b/bittensor/api/extrinsics/delegation.py @@ -17,7 +17,7 @@ import logging import bittensor -from ..core.errors import ( +from bittensor.core.errors import ( NominationError, NotDelegateError, NotRegisteredError, diff --git a/bittensor/extrinsics/network.py b/bittensor/api/extrinsics/network.py similarity index 100% rename from bittensor/extrinsics/network.py rename to bittensor/api/extrinsics/network.py diff --git a/bittensor/extrinsics/prometheus.py b/bittensor/api/extrinsics/prometheus.py similarity index 98% rename from bittensor/extrinsics/prometheus.py rename to bittensor/api/extrinsics/prometheus.py index 97f7c1771..8828d5a14 100644 --- a/bittensor/extrinsics/prometheus.py +++ b/bittensor/api/extrinsics/prometheus.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 diff --git a/bittensor/extrinsics/registration.py b/bittensor/api/extrinsics/registration.py similarity index 99% rename from bittensor/extrinsics/registration.py rename to bittensor/api/extrinsics/registration.py index e82add838..145c05793 100644 --- a/bittensor/extrinsics/registration.py +++ b/bittensor/api/extrinsics/registration.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 diff --git a/bittensor/extrinsics/root.py b/bittensor/api/extrinsics/root.py similarity index 99% rename from bittensor/extrinsics/root.py rename to bittensor/api/extrinsics/root.py index 05570813c..8da130fa2 100644 --- a/bittensor/extrinsics/root.py +++ b/bittensor/api/extrinsics/root.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 diff --git a/bittensor/extrinsics/senate.py b/bittensor/api/extrinsics/senate.py similarity index 99% rename from bittensor/extrinsics/senate.py rename to bittensor/api/extrinsics/senate.py index 043233996..817a589f5 100644 --- a/bittensor/extrinsics/senate.py +++ b/bittensor/api/extrinsics/senate.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 diff --git a/bittensor/extrinsics/serving.py b/bittensor/api/extrinsics/serving.py similarity index 99% rename from bittensor/extrinsics/serving.py rename to bittensor/api/extrinsics/serving.py index 2fefc97d1..ec6417d34 100644 --- a/bittensor/extrinsics/serving.py +++ b/bittensor/api/extrinsics/serving.py @@ -24,7 +24,7 @@ import bittensor import bittensor.utils.networking as net from bittensor.utils import format_error_message -from ..core.errors import MetadataError +from bittensor.core.errors import MetadataError def serve_extrinsic( diff --git a/bittensor/extrinsics/set_weights.py b/bittensor/api/extrinsics/set_weights.py similarity index 98% rename from bittensor/extrinsics/set_weights.py rename to bittensor/api/extrinsics/set_weights.py index db5e4a739..13845e9d5 100644 --- a/bittensor/extrinsics/set_weights.py +++ b/bittensor/api/extrinsics/set_weights.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 diff --git a/bittensor/extrinsics/staking.py b/bittensor/api/extrinsics/staking.py similarity index 99% rename from bittensor/extrinsics/staking.py rename to bittensor/api/extrinsics/staking.py index 699e79fa9..fe5571e91 100644 --- a/bittensor/extrinsics/staking.py +++ b/bittensor/api/extrinsics/staking.py @@ -20,7 +20,7 @@ from time import sleep from typing import List, Union, Optional, Tuple from bittensor.utils.balance import Balance -from ..core.errors import NotDelegateError +from bittensor.core.errors import NotDelegateError def _check_threshold_amount( diff --git a/bittensor/extrinsics/transfer.py b/bittensor/api/extrinsics/transfer.py similarity index 97% rename from bittensor/extrinsics/transfer.py rename to bittensor/api/extrinsics/transfer.py index 91ef3237e..2a9f76e01 100644 --- a/bittensor/extrinsics/transfer.py +++ b/bittensor/api/extrinsics/transfer.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -20,8 +19,8 @@ from rich.prompt import Confirm from typing import Union -from ..utils.balance import Balance -from ..utils import is_valid_bittensor_address_or_public_key +from bittensor.utils.balance import Balance +from bittensor.utils import is_valid_bittensor_address_or_public_key def transfer_extrinsic( diff --git a/bittensor/extrinsics/unstaking.py b/bittensor/api/extrinsics/unstaking.py similarity index 100% rename from bittensor/extrinsics/unstaking.py rename to bittensor/api/extrinsics/unstaking.py diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index a26a72387..dcb2938cf 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -55,44 +55,44 @@ custom_rpc_type_registry, ) from .errors import IdentityError, NominationError, StakeError, TakeError -from bittensor.extrinsics.commit_weights import ( +from bittensor.api.extrinsics.commit_weights import ( commit_weights_extrinsic, reveal_weights_extrinsic, ) -from bittensor.extrinsics.delegation import ( +from bittensor.api.extrinsics.delegation import ( delegate_extrinsic, nominate_extrinsic, undelegate_extrinsic, increase_take_extrinsic, decrease_take_extrinsic, ) -from bittensor.extrinsics.network import ( +from bittensor.api.extrinsics.network import ( register_subnetwork_extrinsic, set_hyperparameter_extrinsic, ) -from bittensor.extrinsics.prometheus import prometheus_extrinsic -from bittensor.extrinsics.registration import ( +from bittensor.api.extrinsics.prometheus import prometheus_extrinsic +from bittensor.api.extrinsics.registration import ( register_extrinsic, burned_register_extrinsic, run_faucet_extrinsic, swap_hotkey_extrinsic, ) -from bittensor.extrinsics.root import root_register_extrinsic, set_root_weights_extrinsic -from bittensor.extrinsics.senate import ( +from bittensor.api.extrinsics.root import root_register_extrinsic, set_root_weights_extrinsic +from bittensor.api.extrinsics.senate import ( register_senate_extrinsic, leave_senate_extrinsic, vote_senate_extrinsic, ) -from bittensor.extrinsics.serving import ( +from bittensor.api.extrinsics.serving import ( serve_extrinsic, serve_axon_extrinsic, publish_metadata, get_metadata, ) -from bittensor.extrinsics.set_weights import set_weights_extrinsic -from bittensor.extrinsics.staking import add_stake_extrinsic, add_stake_multiple_extrinsic -from bittensor.extrinsics.transfer import transfer_extrinsic -from bittensor.extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic +from bittensor.api.extrinsics.set_weights import set_weights_extrinsic +from bittensor.api.extrinsics.staking import add_stake_extrinsic, add_stake_multiple_extrinsic +from bittensor.api.extrinsics.transfer import transfer_extrinsic +from bittensor.api.extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic from .types import AxonServeCallParams, PrometheusServeCallParams from bittensor.utils import ( U16_NORMALIZED_FLOAT, diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py index e2bed67fb..f935d89ac 100644 --- a/tests/integration_tests/test_cli.py +++ b/tests/integration_tests/test_cli.py @@ -2264,7 +2264,7 @@ class MockException(Exception): with patch("bittensor.wallet", return_value=mock_wallet) as mock_create_wallet: with patch( - "bittensor.extrinsics.registration.POWSolution.is_stale", + "bittensor.api.extrinsics.registration.POWSolution.is_stale", side_effect=MockException, ) as mock_is_stale: with pytest.raises(MockException): diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 407dee848..e5ea7f256 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -769,7 +769,7 @@ def test_registration_failed(self): mock_neuron.is_null = True with patch( - "bittensor.extrinsics.registration.create_pow", return_value=None + "bittensor.api.extrinsics.registration.create_pow", return_value=None ) as mock_create_pow: wallet = _get_mock_wallet( hotkey=_get_mock_keypair(0, self.id()), @@ -821,7 +821,7 @@ class ExitEarly(Exception): mock_create_pow = MagicMock(return_value=MagicMock(is_stale=mock_is_stale)) - with patch("bittensor.extrinsics.registration.create_pow", mock_create_pow): + with patch("bittensor.api.extrinsics.registration.create_pow", mock_create_pow): # should create a pow and check if it is stale # then should create a new pow and check if it is stale # then should enter substrate and exit early because of test diff --git a/tests/unit_tests/extrinsics/test_delegation.py b/tests/unit_tests/extrinsics/test_delegation.py index b85087fe1..1b1200f7a 100644 --- a/tests/unit_tests/extrinsics/test_delegation.py +++ b/tests/unit_tests/extrinsics/test_delegation.py @@ -26,7 +26,7 @@ NotRegisteredError, StakeError, ) -from bittensor.extrinsics.delegation import ( +from bittensor.api.extrinsics.delegation import ( nominate_extrinsic, delegate_extrinsic, undelegate_extrinsic, diff --git a/tests/unit_tests/extrinsics/test_network.py b/tests/unit_tests/extrinsics/test_network.py index f5196d7b3..e4589bc1b 100644 --- a/tests/unit_tests/extrinsics/test_network.py +++ b/tests/unit_tests/extrinsics/test_network.py @@ -20,7 +20,7 @@ import pytest from bittensor_wallet import Wallet -from bittensor.extrinsics.network import ( +from bittensor.api.extrinsics.network import ( set_hyperparameter_extrinsic, register_subnetwork_extrinsic, ) diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index 12b88d563..e4c9d5dfd 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -21,7 +21,7 @@ from bittensor_wallet import Wallet import bittensor -from bittensor.extrinsics.prometheus import prometheus_extrinsic +from bittensor.api.extrinsics.prometheus import prometheus_extrinsic from bittensor.core.subtensor import Subtensor diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py index 07c0b8d13..00addfd5a 100644 --- a/tests/unit_tests/extrinsics/test_registration.py +++ b/tests/unit_tests/extrinsics/test_registration.py @@ -20,7 +20,7 @@ import pytest from bittensor_wallet import Wallet -from bittensor.extrinsics.registration import ( +from bittensor.api.extrinsics.registration import ( MaxSuccessException, MaxAttemptsException, swap_hotkey_extrinsic, @@ -97,7 +97,7 @@ def test_run_faucet_extrinsic_happy_path( "bittensor.utils.registration._solve_for_difficulty_fast", return_value=mock_pow_solution, ) as mock_create_pow, patch("rich.prompt.Confirm.ask", return_value=True): - from bittensor.extrinsics.registration import run_faucet_extrinsic + from bittensor.api.extrinsics.registration import run_faucet_extrinsic # Arrange mock_subtensor.get_balance.return_value = 100 @@ -153,7 +153,7 @@ def test_run_faucet_extrinsic_edge_cases( with patch("torch.cuda.is_available", return_value=torch_cuda_available), patch( "rich.prompt.Confirm.ask", return_value=prompt_response ): - from bittensor.extrinsics.registration import run_faucet_extrinsic + from bittensor.api.extrinsics.registration import run_faucet_extrinsic # Act result = run_faucet_extrinsic( @@ -187,7 +187,7 @@ def test_run_faucet_extrinsic_error_cases( "bittensor.utils.registration.create_pow", side_effect=[mock_pow_solution, exception], ): - from bittensor.extrinsics.registration import run_faucet_extrinsic + from bittensor.api.extrinsics.registration import run_faucet_extrinsic # Act result = run_faucet_extrinsic( diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index 5ee77bd47..7ef91c92b 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import MagicMock, patch from bittensor.core.subtensor import Subtensor -from bittensor.extrinsics.root import ( +from bittensor.api.extrinsics.root import ( root_register_extrinsic, set_root_weights_extrinsic, ) diff --git a/tests/unit_tests/extrinsics/test_senate.py b/tests/unit_tests/extrinsics/test_senate.py index 66849efc5..ecf9c5b17 100644 --- a/tests/unit_tests/extrinsics/test_senate.py +++ b/tests/unit_tests/extrinsics/test_senate.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import MagicMock, patch from bittensor import subtensor, wallet -from bittensor.extrinsics.senate import ( +from bittensor.api.extrinsics.senate import ( leave_senate_extrinsic, register_senate_extrinsic, vote_senate_extrinsic, @@ -55,8 +55,8 @@ def test_register_senate_extrinsic( ): # Arrange with patch( - "bittensor.extrinsics.senate.Confirm.ask", return_value=not prompt - ), patch("bittensor.extrinsics.senate.time.sleep"), patch.object( + "bittensor.api.extrinsics.senate.Confirm.ask", return_value=not prompt + ), patch("bittensor.api.extrinsics.senate.time.sleep"), patch.object( mock_subtensor.substrate, "compose_call" ), patch.object(mock_subtensor.substrate, "create_signed_extrinsic"), patch.object( mock_subtensor.substrate, @@ -148,8 +148,8 @@ def test_vote_senate_extrinsic( proposal_idx = 123 with patch( - "bittensor.extrinsics.senate.Confirm.ask", return_value=not prompt - ), patch("bittensor.extrinsics.senate.time.sleep"), patch.object( + "bittensor.api.extrinsics.senate.Confirm.ask", return_value=not prompt + ), patch("bittensor.api.extrinsics.senate.time.sleep"), patch.object( mock_subtensor.substrate, "compose_call" ), patch.object(mock_subtensor.substrate, "create_signed_extrinsic"), patch.object( mock_subtensor.substrate, @@ -212,8 +212,8 @@ def test_leave_senate_extrinsic( ): # Arrange with patch( - "bittensor.extrinsics.senate.Confirm.ask", return_value=not prompt - ), patch("bittensor.extrinsics.senate.time.sleep"), patch.object( + "bittensor.api.extrinsics.senate.Confirm.ask", return_value=not prompt + ), patch("bittensor.api.extrinsics.senate.time.sleep"), patch.object( mock_subtensor.substrate, "compose_call" ), patch.object(mock_subtensor.substrate, "create_signed_extrinsic"), patch.object( mock_subtensor.substrate, diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index e2fd5cd9b..26f25af0b 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -21,7 +21,7 @@ from bittensor_wallet import Wallet from bittensor.core.axon import Axon -from bittensor.extrinsics.serving import ( +from bittensor.api.extrinsics.serving import ( serve_extrinsic, publish_metadata, serve_axon_extrinsic, @@ -119,7 +119,7 @@ def test_serve_extrinsic_happy_path( ): # Arrange mock_subtensor._do_serve_axon.return_value = (True, "") - with patch("bittensor.extrinsics.serving.Confirm.ask", return_value=True): + with patch("bittensor.api.extrinsics.serving.Confirm.ask", return_value=True): # Act result = serve_extrinsic( mock_subtensor, @@ -176,7 +176,7 @@ def test_serve_extrinsic_edge_cases( ): # Arrange mock_subtensor._do_serve_axon.return_value = (True, "") - with patch("bittensor.extrinsics.serving.Confirm.ask", return_value=True): + with patch("bittensor.api.extrinsics.serving.Confirm.ask", return_value=True): # Act result = serve_extrinsic( mock_subtensor, @@ -233,7 +233,7 @@ def test_serve_extrinsic_error_cases( ): # Arrange mock_subtensor._do_serve_axon.return_value = (False, "Error serving axon") - with patch("bittensor.extrinsics.serving.Confirm.ask", return_value=True): + with patch("bittensor.api.extrinsics.serving.Confirm.ask", return_value=True): # Act result = serve_extrinsic( mock_subtensor, diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 68ce7acae..e25fe46cd 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -2,7 +2,7 @@ import pytest from unittest.mock import MagicMock, patch from bittensor import subtensor, wallet -from bittensor.extrinsics.set_weights import set_weights_extrinsic +from bittensor.api.extrinsics.set_weights import set_weights_extrinsic @pytest.fixture diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py index f4a8838e7..77c15e524 100644 --- a/tests/unit_tests/extrinsics/test_staking.py +++ b/tests/unit_tests/extrinsics/test_staking.py @@ -19,7 +19,7 @@ from unittest.mock import patch, MagicMock import bittensor from bittensor.utils.balance import Balance -from bittensor.extrinsics.staking import ( +from bittensor.api.extrinsics.staking import ( add_stake_extrinsic, add_stake_multiple_extrinsic, ) diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py index 0fa6ba84c..87afc66e6 100644 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ b/tests/unit_tests/extrinsics/test_unstaking.py @@ -4,7 +4,7 @@ from unittest.mock import patch, MagicMock from bittensor.utils.balance import Balance -from bittensor.extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic +from bittensor.api.extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic @pytest.fixture From a23b86115a05e1dabb48cc0da511a55ea05da347 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 15:59:24 -0700 Subject: [PATCH 037/260] Return config.py back to the bittensor --- bittensor/__init__.py | 11 +- bittensor/core/config.py | 408 ++++++++++++++++++++ bittensor/utils/btlogging/loggingmachine.py | 3 +- 3 files changed, 415 insertions(+), 7 deletions(-) create mode 100644 bittensor/core/config.py diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 33c8d6b49..cab608fc4 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -235,7 +235,7 @@ from bittensor_wallet.errors import KeyFileError from substrateinterface import Keypair # noqa: F401 -from bittensor_wallet.config import ( +from .core.config import ( InvalidConfigFile, DefaultConfig, Config, @@ -373,10 +373,10 @@ def debug(on: bool = True): turn_console_off() # Backwards compatibility with previous bittensor versions. -from .core.subtensor import Subtensor as subtensor -from .core.axon import Axon as axon -from bittensor_wallet.wallet import Wallet as wallet -from bittensor_wallet.config import Config as config +subtensor = Subtensor +axon = Axon +wallet = Wallet +config = Config __all__ = [ "axon", @@ -387,3 +387,4 @@ def debug(on: bool = True): "DefaultConfig", "T" ] + diff --git a/bittensor/core/config.py b/bittensor/core/config.py new file mode 100644 index 000000000..a87863b9e --- /dev/null +++ b/bittensor/core/config.py @@ -0,0 +1,408 @@ +# 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. + +"""Implementation of the config class, which manages the configuration of different Bittensor modules.""" + +import argparse +import copy +import os +import sys +from copy import deepcopy +from typing import List, Optional, Dict, Any, TypeVar, Type + +import yaml +from munch import DefaultMunch + + +class InvalidConfigFile(Exception): + """In place of YAMLError""" + + +class Config(DefaultMunch): + """Implementation of the config class, which manages the configuration of different Bittensor modules.""" + + __is_set: Dict[str, bool] + + """ + Translates the passed parser into a nested Bittensor config. + + Args: + parser (argparse.ArgumentParser): + Command line parser object. + strict (bool): + If ``true``, the command line arguments are strictly parsed. + args (list of str): + Command line arguments. + default (Optional[Any]): + Default value for the Config. Defaults to ``None``. + This default will be returned for attributes that are undefined. + Returns: + config (Config): + Nested config object created from parser arguments. + """ + + def __init__( + self, + parser: argparse.ArgumentParser = None, + args: Optional[List[str]] = None, + strict: bool = False, + default: Optional[Any] = None, + ) -> None: + super().__init__(default) + + self["__is_set"] = {} + + if parser is None: + return + + # Optionally add config specific arguments + try: + parser.add_argument( + "--config", + type=str, + help="If set, defaults are overridden by passed file.", + ) + except Exception: + # this can fail if --config has already been added. + pass + + try: + parser.add_argument( + "--strict", + action="store_true", + help="""If flagged, config will check that only exact arguments have been set.""", + default=False, + ) + except Exception: + # this can fail if --strict has already been added. + pass + + try: + parser.add_argument( + "--no_version_checking", + action="store_true", + help="Set ``true`` to stop cli version checking.", + default=False, + ) + except Exception: + # this can fail if --no_version_checking has already been added. + pass + + try: + parser.add_argument( + "--no_prompt", + dest="no_prompt", + action="store_true", + help="Set ``true`` to stop cli from prompting the user.", + default=False, + ) + except Exception: + # this can fail if --no_version_checking has already been added. + pass + + # Get args from argv if not passed in. + if args is None: + args = sys.argv[1:] + + # Check for missing required arguments before proceeding + missing_required_args = self.__check_for_missing_required_args(parser, args) + if missing_required_args: + # Handle missing required arguments gracefully + raise ValueError( + f"Missing required arguments: {', '.join(missing_required_args)}" + ) + + # 1.1 Optionally load defaults if the --config is set. + try: + config_file_path = ( + str(os.getcwd()) + + "/" + + vars(parser.parse_known_args(args)[0])["config"] + ) + except Exception: + config_file_path = None + + # Parse args not strict + config_params = Config.__parse_args__(args=args, parser=parser, strict=False) + + # 2. Optionally check for --strict + # strict=True when passed in OR when --strict is set + strict = config_params.strict or strict + + if config_file_path is not None: + config_file_path = os.path.expanduser(config_file_path) + try: + with open(config_file_path) as f: + params_config = yaml.safe_load(f) + print("Loading config defaults from: {}".format(config_file_path)) + parser.set_defaults(**params_config) + except Exception as e: + print("Error in loading: {} using default parser settings".format(e)) + + # 2. Continue with loading in params. + params = Config.__parse_args__(args=args, parser=parser, strict=strict) + + _config = self + + # Splits params and add to config + Config.__split_params__(params=params, _config=_config) + + # Make the is_set map + _config["__is_set"] = {} + + # Reparse args using default of unset + parser_no_defaults = copy.deepcopy(parser) + + # Only command as the arg, else no args + default_param_args = ( + [_config.get("command")] + if _config.get("command") is not None and _config.get("subcommand") is None + else [] + ) + if _config.get("command") is not None and _config.get("subcommand") is not None: + default_param_args = [_config.get("command"), _config.get("subcommand")] + + # Get all args by name + default_params = parser.parse_args(args=default_param_args) + + all_default_args = default_params.__dict__.keys() | [] + # Make a dict with keys as args and values as argparse.SUPPRESS + defaults_as_suppress = {key: argparse.SUPPRESS for key in all_default_args} + # Set the defaults to argparse.SUPPRESS, should remove them from the namespace + parser_no_defaults.set_defaults(**defaults_as_suppress) + parser_no_defaults._defaults.clear() # Needed for quirk of argparse + + # Check for subparsers and do the same + if parser_no_defaults._subparsers is not None: + for action in parser_no_defaults._subparsers._actions: + # Should only be the "command" subparser action + if isinstance(action, argparse._SubParsersAction): + # Set the defaults to argparse.SUPPRESS, should remove them from the namespace + # Each choice is the keyword for a command, we need to set the defaults for each of these + # Note: we also need to clear the _defaults dict for each, this is a quirk of argparse + cmd_parser: argparse.ArgumentParser + for cmd_parser in action.choices.values(): + # If this choice is also a subparser, set defaults recursively + if cmd_parser._subparsers: + for action in cmd_parser._subparsers._actions: + # Should only be the "command" subparser action + if isinstance(action, argparse._SubParsersAction): + cmd_parser: argparse.ArgumentParser + for cmd_parser in action.choices.values(): + cmd_parser.set_defaults(**defaults_as_suppress) + cmd_parser._defaults.clear() # Needed for quirk of argparse + else: + cmd_parser.set_defaults(**defaults_as_suppress) + cmd_parser._defaults.clear() # Needed for quirk of argparse + + # Reparse the args, but this time with the defaults as argparse.SUPPRESS + params_no_defaults = Config.__parse_args__( + args=args, parser=parser_no_defaults, strict=strict + ) + + # Diff the params and params_no_defaults to get the is_set map + _config["__is_set"] = { + arg_key: True + for arg_key in [ + k + for k, _ in filter( + lambda kv: kv[1] != argparse.SUPPRESS, + params_no_defaults.__dict__.items(), + ) + ] + } + + @staticmethod + def __split_params__(params: argparse.Namespace, _config: "Config"): + # Splits params on dot syntax i.e neuron.axon_port and adds to _config + for arg_key, arg_val in params.__dict__.items(): + split_keys = arg_key.split(".") + head = _config + keys = split_keys + while len(keys) > 1: + if ( + hasattr(head, keys[0]) and head[keys[0]] is not None + ): # Needs to be Config + head = getattr(head, keys[0]) + keys = keys[1:] + else: + head[keys[0]] = Config() + head = head[keys[0]] + keys = keys[1:] + if len(keys) == 1: + head[keys[0]] = arg_val + + @staticmethod + def __parse_args__( + args: List[str], parser: argparse.ArgumentParser = None, strict: bool = False + ) -> argparse.Namespace: + """Parses the passed args use the passed parser. + + Args: + args (List[str]): + List of arguments to parse. + parser (argparse.ArgumentParser): + Command line parser object. + strict (bool): + If ``true``, the command line arguments are strictly parsed. + Returns: + Namespace: + Namespace object created from parser arguments. + """ + if not strict: + params, unrecognized = parser.parse_known_args(args=args) + params_list = list(params.__dict__) + # bug within argparse itself, does not correctly set value for boolean flags + for unrec in unrecognized: + if unrec.startswith("--") and unrec[2:] in params_list: + # Set the missing boolean value to true + setattr(params, unrec[2:], True) + else: + params = parser.parse_args(args=args) + + return params + + def __deepcopy__(self, memo) -> "Config": + _default = self.__default__ + + config_state = self.__getstate__() + config_copy = Config() + memo[id(self)] = config_copy + + config_copy.__setstate__(config_state) + config_copy.__default__ = _default + + config_copy["__is_set"] = deepcopy(self["__is_set"], memo) + + return config_copy + + def __repr__(self) -> str: + return self.__str__() + + @staticmethod + def _remove_private_keys(d): + if "__parser" in d: + d.pop("__parser", None) + if "__is_set" in d: + d.pop("__is_set", None) + for k, v in list(d.items()): + if isinstance(v, dict): + Config._remove_private_keys(v) + return d + + def __str__(self) -> str: + # remove the parser and is_set map from the visible config + visible = copy.deepcopy(self.toDict()) + visible.pop("__parser", None) + visible.pop("__is_set", None) + cleaned = Config._remove_private_keys(visible) + return "\n" + yaml.dump(cleaned, sort_keys=False) + + def copy(self) -> "Config": + return copy.deepcopy(self) + + def to_string(self, items) -> str: + """Get string from items""" + return "\n" + yaml.dump(items.toDict()) + + def update_with_kwargs(self, kwargs): + """Add config to self""" + for key, val in kwargs.items(): + self[key] = val + + @classmethod + def _merge(cls, a, b): + """Merge two configurations recursively. + If there is a conflict, the value from the second configuration will take precedence. + """ + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + a[key] = cls._merge(a[key], b[key]) + else: + a[key] = b[key] + else: + a[key] = b[key] + return a + + def merge(self, b): + """ + Merges the current config with another config. + + Args: + b: Another config to merge. + """ + self._merge(self, b) + + @classmethod + def merge_all(cls, configs: List["Config"]) -> "Config": + """ + Merge all configs in the list into one config. + If there is a conflict, the value from the last configuration in the list will take precedence. + + Args: + configs (list of config): + List of configs to be merged. + + Returns: + config: + Merged config object. + """ + result = cls() + for cfg in configs: + result.merge(cfg) + return result + + def is_set(self, param_name: str) -> bool: + """ + Returns a boolean indicating whether the parameter has been set or is still the default. + """ + if param_name not in self.get("__is_set"): + return False + else: + return self.get("__is_set")[param_name] + + def __check_for_missing_required_args( + self, parser: argparse.ArgumentParser, args: List[str] + ) -> List[str]: + required_args = self.__get_required_args_from_parser(parser) + missing_args = [arg for arg in required_args if not any(arg in s for s in args)] + return missing_args + + @staticmethod + def __get_required_args_from_parser(parser: argparse.ArgumentParser) -> List[str]: + required_args = [] + for action in parser._actions: + if action.required: + # Prefix the argument with '--' if it's a long argument, or '-' if it's short + prefix = "--" if len(action.dest) > 1 else "-" + required_args.append(prefix + action.dest) + return required_args + + +T = TypeVar("T", bound="DefaultConfig") + + +class DefaultConfig(Config): + """A Config with a set of default values.""" + + @classmethod + def default(cls: Type[T]) -> T: + """ + Get default config. + """ + raise NotImplementedError("Function default is not implemented.") diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index fc6be6558..7c7ea3376 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -31,10 +31,9 @@ from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler from typing import NamedTuple -from bittensor_wallet.config import Config from statemachine import State, StateMachine -from bittensor_wallet.config import Config +from bittensor.core.config import Config from .defines import ( BITTENSOR_LOGGER_NAME, From 30e639f48e4a8e3ba3683ebabcfed8ca8e083bc5 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 19:38:08 -0700 Subject: [PATCH 038/260] Move constants from bt/__init__.py to bt/core/settings.py --- bittensor/__init__.py | 295 +++++------------- bittensor/btcli/cli.py | 3 +- bittensor/btcli/commands/identity.py | 6 +- bittensor/btcli/commands/register.py | 7 +- bittensor/btcli/commands/stake.py | 3 +- bittensor/btcli/commands/unstake.py | 3 +- bittensor/btcli/commands/wallets.py | 3 +- bittensor/core/chain_data.py | 35 ++- bittensor/core/metagraph.py | 5 +- bittensor/core/settings.py | 161 ++++++++++ bittensor/core/subtensor.py | 39 ++- bittensor/utils/__init__.py | 14 +- bittensor/utils/balance.py | 5 +- bittensor/utils/version.py | 5 +- bittensor/utils/wallet_utils.py | 6 +- .../test_subtensor_integration.py | 13 +- tests/unit_tests/test_chain_data.py | 19 +- tests/unit_tests/test_metagraph.py | 18 +- tests/unit_tests/test_subtensor.py | 34 +- 19 files changed, 354 insertions(+), 320 deletions(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index cab608fc4..797972fc8 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -14,197 +14,24 @@ # 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. +# Bittensor code and protocol version. + +__version__ = "7.3.0" + +_version_split = __version__.split(".") +__version_info__ = tuple(int(part) for part in _version_split) +_version_int_base = 1000 +assert max(__version_info__) < _version_int_base + +__version_as_int__: int = sum(e * (_version_int_base**i) for i, e in enumerate(reversed(__version_info__))) +assert __version_as_int__ < 2 ** 31 # fits in int32 import os import warnings from rich.console import Console from rich.traceback import install - - -if (NEST_ASYNCIO_ENV := os.getenv("NEST_ASYNCIO")) in ("1", None): - if NEST_ASYNCIO_ENV is None: - warnings.warn( - "NEST_ASYNCIO implicitly set to '1'. In the future, the default value will be '0'." - "If you use `nest_asyncio` make sure to add it explicitly to your project dependencies," - "as it will be removed from `bittensor` package dependencies in the future." - "To silence this warning, explicitly set the environment variable, e.g. `export NEST_ASYNCIO=0`.", - DeprecationWarning, - ) - # Install and apply nest asyncio to allow the async functions - # to run in a .ipynb - import nest_asyncio - - nest_asyncio.apply() - -# Substrate chain block time (seconds). -__blocktime__ = 12 - -# Pip address for versioning -__pipaddress__ = "https://pypi.org/pypi/bittensor/json" - -# Raw GitHub url for delegates registry file -__delegates_details_url__: str = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" - -# Substrate ss58_format -__ss58_format__ = 42 - -# Wallet ss58 address length -__ss58_address_length__ = 48 - -__networks__ = ["local", "finney", "test", "archive"] - -__finney_entrypoint__ = "wss://entrypoint-finney.opentensor.ai:443" - -__finney_test_entrypoint__ = "wss://test.finney.opentensor.ai:443/" - -__archive_entrypoint__ = "wss://archive.chain.opentensor.ai:443/" - -# Needs to use wss:// -__bellagene_entrypoint__ = "wss://parachain.opentensor.ai:443" - -if ( - BT_SUBTENSOR_CHAIN_ENDPOINT := os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") -) is not None: - __local_entrypoint__ = BT_SUBTENSOR_CHAIN_ENDPOINT -else: - __local_entrypoint__ = "ws://127.0.0.1:9944" - -__tao_symbol__: str = chr(0x03C4) - -__rao_symbol__: str = chr(0x03C1) - -# Block Explorers map network to explorer url -# Must all be polkadotjs explorer urls -__network_explorer_map__ = { - "opentensor": { - "local": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", - "endpoint": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", - "finney": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", - }, - "taostats": { - "local": "https://x.taostats.io", - "endpoint": "https://x.taostats.io", - "finney": "https://x.taostats.io", - }, -} - -# --- Type Registry --- -__type_registry__ = { - "types": { - "Balance": "u64", # Need to override default u128 - }, - "runtime_api": { - "NeuronInfoRuntimeApi": { - "methods": { - "get_neuron_lite": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - { - "name": "uid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_neurons_lite": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - } - }, - "StakeInfoRuntimeApi": { - "methods": { - "get_stake_info_for_coldkey": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_stake_info_for_coldkeys": { - "params": [ - { - "name": "coldkey_account_vecs", - "type": "Vec>", - }, - ], - "type": "Vec", - }, - }, - }, - "ValidatorIPRuntimeApi": { - "methods": { - "get_associated_validator_ip_info_for_subnet": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - }, - }, - "SubnetInfoRuntimeApi": { - "methods": { - "get_subnet_hyperparams": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - } - } - }, - "SubnetRegistrationRuntimeApi": { - "methods": {"get_network_registration_cost": {"params": [], "type": "u64"}} - }, - "ColdkeySwapRuntimeApi": { - "methods": { - "get_scheduled_coldkey_swap": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_remaining_arbitration_period": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_coldkey_swap_destinations": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - } - }, - }, -} +from .core import settings from .core.errors import ( BlacklistedException, @@ -232,16 +59,15 @@ UnstakeError, ) -from bittensor_wallet.errors import KeyFileError - +from bittensor_wallet.errors import KeyFileError # noqa: F401 from substrateinterface import Keypair # noqa: F401 -from .core.config import ( +from .core.config import ( # noqa: F401 InvalidConfigFile, DefaultConfig, Config, T, ) -from bittensor_wallet.keyfile import ( +from bittensor_wallet.keyfile import ( # noqa: F401 serialized_keypair_to_keyfile_data, deserialize_keypair_from_keyfile_data, validate_password, @@ -255,9 +81,9 @@ encrypt_keyfile_data, get_coldkey_password_from_environment, decrypt_keyfile_data, - Keyfile as keyfile, + Keyfile, ) -from bittensor_wallet.wallet import display_mnemonic_msg, Wallet +from bittensor_wallet.wallet import display_mnemonic_msg, Wallet # noqa: F401 from .utils import ( ss58_to_vec_u8, @@ -291,27 +117,6 @@ ProposalVoteData, ) -# Bittensor code and protocol version. -__version__ = "7.3.0" - -_version_split = __version__.split(".") -__version_info__ = tuple(int(part) for part in _version_split) -_version_int_base = 1000 -assert max(__version_info__) < _version_int_base - -__version_as_int__: int = sum( - e * (_version_int_base**i) for i, e in enumerate(reversed(__version_info__)) -) -assert __version_as_int__ < 2**31 # fits in int32 -__new_signature_version__ = 360 - -# Rich console. -__console__ = Console() -__use_console__ = True - -# Remove overdue locals in debug training. -install(show_locals=False) - from .btcli.cli import cli as cli, COMMANDS as ALL_COMMANDS from .core.subtensor import Subtensor from .core.metagraph import metagraph as metagraph @@ -324,6 +129,11 @@ from .core.subnets import SubnetsAPI as SubnetsAPI from .mock.subtensor_mock import MockSubtensor as MockSubtensor from .utils.btlogging import logging +from .core.settings import blocktime + +# Raw GitHub url for delegates registry file +__delegates_details_url__: str = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" + configs = [ Axon.config(), @@ -345,6 +155,14 @@ def __getattr__(name): raise AttributeError(f"module {__name__} has no attribute {name}") +# Rich console. +__console__ = Console() +__use_console__ = True + +# Remove overdue locals in debug training. +install(show_locals=False) + + def turn_console_off(): global __use_console__ global __console__ @@ -372,19 +190,48 @@ def debug(on: bool = True): turn_console_off() + +def __apply_nest_asyncio(): + """ + Apply nest_asyncio if the environment variable NEST_ASYNCIO is set to "1" or not set. + If not set, warn the user that the default will change in the future. + """ + nest_asyncio_env = os.getenv("NEST_ASYNCIO") + + if nest_asyncio_env == "1" or nest_asyncio_env is None: + if nest_asyncio_env is None: + warnings.warn( + """NEST_ASYNCIO implicitly set to '1'. In the future, the default value will be '0'. + If you use `nest_asyncio`, make sure to add it explicitly to your project dependencies, + as it will be removed from `bittensor` package dependencies in the future. + To silence this warning, explicitly set the environment variable, e.g. `export NEST_ASYNCIO=0`.""", + DeprecationWarning, + ) + # Install and apply nest asyncio to allow the async functions to run in a .ipynb + import nest_asyncio + nest_asyncio.apply() + + +__apply_nest_asyncio() + # Backwards compatibility with previous bittensor versions. -subtensor = Subtensor axon = Axon -wallet = Wallet config = Config +keyfile = Keyfile +wallet = Wallet +subtensor = Subtensor -__all__ = [ - "axon", - "config", - "subtensor", - "wallet", - "InvalidConfigFile", - "DefaultConfig", - "T" -] - +__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 +__bellagene_entrypoint__ = settings.bellagene_entrypoint +__local_entrypoint__ = settings.local_entrypoint +__tao_symbol__ = settings.tao_symbol +__rao_symbol__ = settings.rao_symbol diff --git a/bittensor/btcli/cli.py b/bittensor/btcli/cli.py index 4a7a47775..423394ac3 100644 --- a/bittensor/btcli/cli.py +++ b/bittensor/btcli/cli.py @@ -15,6 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +from rich.console import Console import sys import shtab import argparse @@ -73,7 +74,7 @@ ) # Create a console instance for CLI display. -console = bittensor.__console__ +console = Console() ALIAS_TO_COMMAND = { "subnets": "subnets", diff --git a/bittensor/btcli/commands/identity.py b/bittensor/btcli/commands/identity.py index 15232c444..373cb8d87 100644 --- a/bittensor/btcli/commands/identity.py +++ b/bittensor/btcli/commands/identity.py @@ -2,7 +2,7 @@ from rich.table import Table from rich.prompt import Prompt from sys import getsizeof - +from ...core.settings import networks import bittensor @@ -155,7 +155,7 @@ def check_config(config: "bittensor.config"): config.subtensor.network = Prompt.ask( "Enter subtensor network", default=bittensor.defaults.subtensor.network, - choices=bittensor.__networks__, + choices=networks, ) ( _, @@ -312,7 +312,7 @@ def check_config(config: "bittensor.config"): config.subtensor.network = Prompt.ask( "Enter subtensor network", default=bittensor.defaults.subtensor.network, - choices=bittensor.__networks__, + choices=networks, ) ( _, diff --git a/bittensor/btcli/commands/register.py b/bittensor/btcli/commands/register.py index 02f717edb..53c12c621 100644 --- a/bittensor/btcli/commands/register.py +++ b/bittensor/btcli/commands/register.py @@ -25,6 +25,7 @@ import bittensor from . import defaults from .utils import check_netuid_set, check_for_cuda_reg_config +from ...core.settings import networks console = Console() @@ -137,7 +138,7 @@ def check_config(config: "bittensor.config"): ): config.subtensor.network = Prompt.ask( "Enter subtensor network", - choices=bittensor.__networks__, + choices=networks, default=defaults.subtensor.network, ) _, endpoint = bittensor.subtensor.determine_chain_endpoint_and_network( @@ -336,7 +337,7 @@ def check_config(config: "bittensor.config"): ): config.subtensor.network = Prompt.ask( "Enter subtensor network", - choices=bittensor.__networks__, + choices=networks, default=defaults.subtensor.network, ) _, endpoint = bittensor.subtensor.determine_chain_endpoint_and_network( @@ -594,7 +595,7 @@ def check_config(config: "bittensor.config"): ): config.subtensor.network = Prompt.ask( "Enter subtensor network", - choices=bittensor.__networks__, + choices=networks, default=defaults.subtensor.network, ) _, endpoint = bittensor.subtensor.determine_chain_endpoint_and_network( diff --git a/bittensor/btcli/commands/stake.py b/bittensor/btcli/commands/stake.py index f391e1e5a..e0250746b 100644 --- a/bittensor/btcli/commands/stake.py +++ b/bittensor/btcli/commands/stake.py @@ -28,6 +28,7 @@ import bittensor from bittensor.utils.balance import Balance from . import defaults +from ...core import settings from .utils import ( get_hotkey_wallets_for_wallet, get_delegates_details, @@ -195,7 +196,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): f"Do you want to stake to the following keys from {wallet.name}:\n" + "".join( [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {bittensor.__tao_symbol__}' if amount else 'All'}[/bold white]\n" + f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {settings.tao_symbol}' if amount else 'All'}[/bold white]\n" for hotkey, amount in zip(final_hotkeys, final_amounts) ] ) diff --git a/bittensor/btcli/commands/unstake.py b/bittensor/btcli/commands/unstake.py index d7c843c86..b56e255b8 100644 --- a/bittensor/btcli/commands/unstake.py +++ b/bittensor/btcli/commands/unstake.py @@ -25,6 +25,7 @@ import bittensor from bittensor.utils.balance import Balance from . import defaults +from ...core import settings from .utils import get_hotkey_wallets_for_wallet console = Console() @@ -273,7 +274,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): f"Do you want to unstake from the following keys to {wallet.name}:\n" + "".join( [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {bittensor.__tao_symbol__}' if amount else 'All'}[/bold white]\n" + f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {settings.tao_symbol}' if amount else 'All'}[/bold white]\n" for hotkey, amount in zip(final_hotkeys, final_amounts) ] ) diff --git a/bittensor/btcli/commands/wallets.py b/bittensor/btcli/commands/wallets.py index 5bc180832..83ed62768 100644 --- a/bittensor/btcli/commands/wallets.py +++ b/bittensor/btcli/commands/wallets.py @@ -28,6 +28,7 @@ from bittensor.utils import RAOPERTAO from . import defaults +from ...core.settings import networks class RegenColdkeyCommand: @@ -932,7 +933,7 @@ def check_config(config: "bittensor.config"): network = Prompt.ask( "Enter network", default=defaults.subtensor.network, - choices=bittensor.__networks__, + choices=networks, ) config.subtensor.network = str(network) ( diff --git a/bittensor/core/chain_data.py b/bittensor/core/chain_data.py index 88fb34537..eacfb3902 100644 --- a/bittensor/core/chain_data.py +++ b/bittensor/core/chain_data.py @@ -1,5 +1,5 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Foundation +# 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 @@ -31,6 +31,7 @@ from scalecodec.utils.ss58 import ss58_encode import bittensor +from .settings import ss58_format from bittensor.utils import networking as net, RAOPERTAO, U16_NORMALIZED_FLOAT from bittensor.utils.balance import Balance from bittensor.utils.registration import torch, use_torch @@ -424,13 +425,13 @@ class NeuronInfo: def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfo": """Fixes the values of the NeuronInfo object.""" neuron_info_decoded["hotkey"] = ss58_encode( - neuron_info_decoded["hotkey"], bittensor.__ss58_format__ + neuron_info_decoded["hotkey"], ss58_format ) neuron_info_decoded["coldkey"] = ss58_encode( - neuron_info_decoded["coldkey"], bittensor.__ss58_format__ + neuron_info_decoded["coldkey"], ss58_format ) stake_dict = { - ss58_encode(coldkey, bittensor.__ss58_format__): Balance.from_rao( + ss58_encode(coldkey, ss58_format): Balance.from_rao( int(stake) ) for coldkey, stake in neuron_info_decoded["stake"] @@ -571,13 +572,13 @@ class NeuronInfoLite: def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfoLite": """Fixes the values of the NeuronInfoLite object.""" neuron_info_decoded["hotkey"] = ss58_encode( - neuron_info_decoded["hotkey"], bittensor.__ss58_format__ + neuron_info_decoded["hotkey"], ss58_format ) neuron_info_decoded["coldkey"] = ss58_encode( - neuron_info_decoded["coldkey"], bittensor.__ss58_format__ + neuron_info_decoded["coldkey"], ss58_format ) stake_dict = { - ss58_encode(coldkey, bittensor.__ss58_format__): Balance.from_rao( + ss58_encode(coldkey, ss58_format): Balance.from_rao( int(stake) ) for coldkey, stake in neuron_info_decoded["stake"] @@ -751,13 +752,13 @@ def fix_decoded_values(cls, decoded: Any) -> "DelegateInfo": return cls( hotkey_ss58=ss58_encode( - decoded["delegate_ss58"], bittensor.__ss58_format__ + decoded["delegate_ss58"], ss58_format ), - owner_ss58=ss58_encode(decoded["owner_ss58"], bittensor.__ss58_format__), + owner_ss58=ss58_encode(decoded["owner_ss58"], ss58_format), take=U16_NORMALIZED_FLOAT(decoded["take"]), nominators=[ ( - ss58_encode(nom[0], bittensor.__ss58_format__), + ss58_encode(nom[0], ss58_format), Balance.from_rao(nom[1]), ) for nom in decoded["nominators"] @@ -823,8 +824,8 @@ class StakeInfo: def fix_decoded_values(cls, decoded: Any) -> "StakeInfo": """Fixes the decoded values.""" return cls( - hotkey_ss58=ss58_encode(decoded["hotkey"], bittensor.__ss58_format__), - coldkey_ss58=ss58_encode(decoded["coldkey"], bittensor.__ss58_format__), + hotkey_ss58=ss58_encode(decoded["hotkey"], ss58_format), + coldkey_ss58=ss58_encode(decoded["coldkey"], ss58_format), stake=Balance.from_rao(decoded["stake"]), ) @@ -855,7 +856,7 @@ def list_of_tuple_from_vec_u8( return {} return { - ss58_encode(address=account_id, ss58_format=bittensor.__ss58_format__): [ + ss58_encode(address=account_id, ss58_format=ss58_format): [ StakeInfo.fix_decoded_values(d) for d in stake_info ] for account_id, stake_info in decoded @@ -945,7 +946,7 @@ def fix_decoded_values(cls, decoded: Dict) -> "SubnetInfo": }, emission_value=decoded["emission_values"], burn=Balance.from_rao(decoded["burn"]), - owner_ss58=ss58_encode(decoded["owner"], bittensor.__ss58_format__), + owner_ss58=ss58_encode(decoded["owner"], ss58_format), ) def to_parameter_dict(self) -> Union[dict[str, Any], "torch.nn.ParameterDict"]: @@ -1163,8 +1164,8 @@ class ScheduledColdkeySwapInfo: def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": """Fixes the decoded values.""" return cls( - old_coldkey=ss58_encode(decoded["old_coldkey"], bittensor.__ss58_format__), - new_coldkey=ss58_encode(decoded["new_coldkey"], bittensor.__ss58_format__), + old_coldkey=ss58_encode(decoded["old_coldkey"], ss58_format), + new_coldkey=ss58_encode(decoded["new_coldkey"], ss58_format), arbitration_block=decoded["arbitration_block"], ) @@ -1200,5 +1201,5 @@ def decode_account_id_list(cls, vec_u8: List[int]) -> Optional[List[str]]: if decoded is None: return None return [ - ss58_encode(account_id, bittensor.__ss58_format__) for account_id in decoded + ss58_encode(account_id, ss58_format) for account_id in decoded ] diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 2d9bc45be..6f526f514 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -26,7 +26,8 @@ from numpy.typing import NDArray from rich.console import Console -from bittensor import __version__, __version_as_int__, __archive_entrypoint__ +from bittensor import __version__, __version_as_int__ +from . import settings from .chain_data import AxonInfo from .subtensor import Subtensor from bittensor.utils.btlogging import logging @@ -515,7 +516,7 @@ def sync( subtensor = self._initialize_subtensor(subtensor) if ( - subtensor.chain_endpoint != __archive_entrypoint__ # type: ignore + subtensor.chain_endpoint != settings.archive_entrypoint # type: ignore or subtensor.network != "archive" # type: ignore ): cur_block = subtensor.get_current_block() # type: ignore diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 8c6e3a0de..2a577fb7b 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -15,3 +15,164 @@ # 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 os + + +# Bittensor networks name +networks = ["local", "finney", "test", "archive"] + +# Bittensor endpoints (Needs to use wss://) +finney_entrypoint = "wss://entrypoint-finney.opentensor.ai:443" +finney_test_entrypoint = "wss://test.finney.opentensor.ai:443/" +archive_entrypoint = "wss://archive.chain.opentensor.ai:443/" +bellagene_entrypoint = "wss://parachain.opentensor.ai:443" +local_entrypoint = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or "ws://127.0.0.1:9944" + +# Currency Symbols Bittensor +tao_symbol: str = chr(0x03C4) +rao_symbol: str = chr(0x03C1) + +# Pip address for versioning +pipaddress = "https://pypi.org/pypi/bittensor/json" + +# Substrate chain block time (seconds). +blocktime = 12 + +# Substrate ss58_format +ss58_format = 42 + +# Wallet ss58 address length +ss58_address_length = 48 + + +# Block Explorers map network to explorer url +# Must all be polkadotjs explorer urls +network_explorer_map = { + "opentensor": { + "local": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", + "endpoint": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", + "finney": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", + }, + "taostats": { + "local": "https://x.taostats.io", + "endpoint": "https://x.taostats.io", + "finney": "https://x.taostats.io", + }, +} + +# --- Type Registry --- +type_registry = { + "types": { + "Balance": "u64", # Need to override default u128 + }, + "runtime_api": { + "NeuronInfoRuntimeApi": { + "methods": { + "get_neuron_lite": { + "params": [ + { + "name": "netuid", + "type": "u16", + }, + { + "name": "uid", + "type": "u16", + }, + ], + "type": "Vec", + }, + "get_neurons_lite": { + "params": [ + { + "name": "netuid", + "type": "u16", + }, + ], + "type": "Vec", + }, + } + }, + "StakeInfoRuntimeApi": { + "methods": { + "get_stake_info_for_coldkey": { + "params": [ + { + "name": "coldkey_account_vec", + "type": "Vec", + }, + ], + "type": "Vec", + }, + "get_stake_info_for_coldkeys": { + "params": [ + { + "name": "coldkey_account_vecs", + "type": "Vec>", + }, + ], + "type": "Vec", + }, + }, + }, + "ValidatorIPRuntimeApi": { + "methods": { + "get_associated_validator_ip_info_for_subnet": { + "params": [ + { + "name": "netuid", + "type": "u16", + }, + ], + "type": "Vec", + }, + }, + }, + "SubnetInfoRuntimeApi": { + "methods": { + "get_subnet_hyperparams": { + "params": [ + { + "name": "netuid", + "type": "u16", + }, + ], + "type": "Vec", + } + } + }, + "SubnetRegistrationRuntimeApi": { + "methods": {"get_network_registration_cost": {"params": [], "type": "u64"}} + }, + "ColdkeySwapRuntimeApi": { + "methods": { + "get_scheduled_coldkey_swap": { + "params": [ + { + "name": "coldkey_account_vec", + "type": "Vec", + }, + ], + "type": "Vec", + }, + "get_remaining_arbitration_period": { + "params": [ + { + "name": "coldkey_account_vec", + "type": "Vec", + }, + ], + "type": "Vec", + }, + "get_coldkey_swap_destinations": { + "params": [ + { + "name": "coldkey_account_vec", + "type": "Vec", + }, + ], + "type": "Vec", + }, + } + }, + }, +} \ No newline at end of file diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index dcb2938cf..aced0c4d1 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -104,6 +104,7 @@ from bittensor.utils.registration import POWSolution from bittensor.utils.registration import legacy_torch_api_compat from bittensor.utils.subtensor import get_subtensor_errors +from . import settings KEY_NONCE: Dict[str, int] = {} @@ -221,7 +222,7 @@ def __init__( if ( self.network == "finney" - or self.chain_endpoint == bittensor.__finney_entrypoint__ + or self.chain_endpoint == settings.finney_entrypoint ) and log_verbose: _logger.info( f"You are connecting to {self.network} network with endpoint {self.chain_endpoint}." @@ -239,10 +240,10 @@ def __init__( try: # Set up params. self.substrate = SubstrateInterface( - ss58_format=bittensor.__ss58_format__, + ss58_format=settings.ss58_format, use_remote_preset=True, url=self.chain_endpoint, - type_registry=bittensor.__type_registry__, + type_registry=settings.type_registry, ) except ConnectionRefusedError: _logger.error( @@ -327,8 +328,8 @@ def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = Non """ prefix_str = "" if prefix is None else f"{prefix}." try: - default_network = bittensor.__networks__[1] - default_chain_endpoint = bittensor.__finney_entrypoint__ + default_network = settings.networks[1] + default_chain_endpoint = settings.finney_entrypoint parser.add_argument( f"--{prefix_str}subtensor.network", @@ -376,29 +377,29 @@ def determine_chain_endpoint_and_network(network: str): if network in ["finney", "local", "test", "archive"]: if network == "finney": # Kiru Finney staging network. - return network, bittensor.__finney_entrypoint__ + return network, settings.finney_entrypoint elif network == "local": - return network, bittensor.__local_entrypoint__ + return network, settings.local_entrypoint elif network == "test": - return network, bittensor.__finney_test_entrypoint__ + return network, settings.finney_test_entrypoint elif network == "archive": - return network, bittensor.__archive_entrypoint__ + return network, settings.archive_entrypoint else: if ( - network == bittensor.__finney_entrypoint__ + network == settings.finney_entrypoint or "entrypoint-finney.opentensor.ai" in network ): - return "finney", bittensor.__finney_entrypoint__ + return "finney", settings.finney_entrypoint elif ( - network == bittensor.__finney_test_entrypoint__ + network == settings.finney_test_entrypoint or "test.finney.opentensor.ai" in network ): - return "test", bittensor.__finney_test_entrypoint__ + return "test", settings.finney_test_entrypoint elif ( - network == bittensor.__archive_entrypoint__ + network == settings.archive_entrypoint or "archive.chain.opentensor.ai" in network ): - return "archive", bittensor.__archive_entrypoint__ + return "archive", settings.archive_entrypoint elif "127.0.0.1" in network or "localhost" in network: return "local", network else: @@ -2712,7 +2713,7 @@ def _do_set_root_weights( Tuple[bool, Optional[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 on the root network. + trust in other neurons based on observed performance and contributions to the root network. """ @retry(delay=2, tries=3, backoff=2, max_delay=4, logger=_logger) @@ -2741,7 +2742,7 @@ def make_substrate_call_with_retry(): ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalziation or inclusion." + return True, "Not waiting for finalization or inclusion." response.process_events() if response.is_success: @@ -3141,9 +3142,7 @@ def query_runtime_api( This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. """ - call_definition = bittensor.__type_registry__["runtime_api"][runtime_api][ # type: ignore - "methods" # type: ignore - ][method] # type: ignore + call_definition = settings.type_registry["runtime_api"][runtime_api]["methods"][method] json_result = self.state_call( method=f"{runtime_api}_{method}", diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 700a65613..4971abc85 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 @@ -26,6 +25,7 @@ from .registration import torch, use_torch from .version import version_checking, check_version, VersionCheckError from .wallet_utils import * # noqa F401 +from ..core.settings import ss58_format RAOPERTAO = 1e9 U16_MAX = 65535 @@ -226,7 +226,7 @@ def get_explorer_url_for_network( def ss58_address_to_bytes(ss58_address: str) -> bytes: """Converts a ss58 address to a bytes object.""" account_id_hex: str = scalecodec.ss58_decode( - ss58_address, bittensor.__ss58_format__ + ss58_address, ss58_format ) return bytes.fromhex(account_id_hex) @@ -247,7 +247,7 @@ def u8_key_to_ss58(u8_key: List[int]) -> str: u8_key (List[int]): The u8-encoded account key. """ # First byte is length, then 32 bytes of key. - return scalecodec.ss58_encode(bytes(u8_key).hex(), bittensor.__ss58_format__) + return scalecodec.ss58_encode(bytes(u8_key).hex(), ss58_format) def hash(content, encoding="utf-8"): diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 63ca6cd5b..bbd205795 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -20,6 +20,7 @@ from typing import Union import bittensor +from ..core import settings class Balance: @@ -35,8 +36,8 @@ class Balance: tao: A float property that gives the balance in tao units. """ - unit: str = bittensor.__tao_symbol__ # This is the tao unit - rao_unit: str = bittensor.__rao_symbol__ # This is the rao unit + unit: str = settings.tao_symbol # This is the tao unit + rao_unit: str = settings.rao_symbol # This is the rao unit rao: int tao: float diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index 2a5aa3cd5..07a222b2f 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -5,6 +5,7 @@ import bittensor import requests +from ..core.settings import pipaddress VERSION_CHECK_THRESHOLD = 86400 @@ -38,10 +39,10 @@ def _get_version_from_file(version_file: Path) -> Optional[str]: def _get_version_from_pypi(timeout: int = 15) -> str: bittensor.logging.debug( - f"Checking latest Bittensor version at: {bittensor.__pipaddress__}" + f"Checking latest Bittensor version at: {pipaddress}" ) try: - response = requests.get(bittensor.__pipaddress__, timeout=timeout) + response = requests.get(pipaddress, timeout=timeout) latest_version = response.json()["info"]["version"] return latest_version except requests.exceptions.RequestException: diff --git a/bittensor/utils/wallet_utils.py b/bittensor/utils/wallet_utils.py index 39218c33f..24c1cb703 100644 --- a/bittensor/utils/wallet_utils.py +++ b/bittensor/utils/wallet_utils.py @@ -20,7 +20,7 @@ from substrateinterface.utils import ss58 from typing import Union, Optional -from .. import __ss58_format__ +from ..core.settings import ss58_format from substrateinterface import Keypair as Keypair @@ -41,7 +41,7 @@ def is_valid_ss58_address(address: str) -> bool: """ try: return ss58.is_valid_ss58_address( - address, valid_ss58_format=__ss58_format__ + address, valid_ss58_format=ss58_format ) or ss58.is_valid_ss58_address( address, valid_ss58_format=42 ) # Default substrate ss58 format (legacy) @@ -70,7 +70,7 @@ def is_valid_ed25519_pubkey(public_key: Union[str, bytes]) -> bool: else: raise ValueError("public_key must be a string or bytes") - keypair = Keypair(public_key=public_key, ss58_format=__ss58_format__) + keypair = Keypair(public_key=public_key, ss58_format=ss58_format) ss58_addr = keypair.ss58_address return ss58_addr is not None diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index e5ea7f256..ecc4ace19 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -35,6 +35,7 @@ _get_mock_keypair, _get_mock_wallet, ) +from bittensor.core import settings class TestSubtensor(unittest.TestCase): @@ -79,8 +80,8 @@ def test_network_overrides(self): # Argument importance: chain_endpoint (arg) > network (arg) > config.subtensor.chain_endpoint > config.subtensor.network config0 = bittensor.subtensor.config() config0.subtensor.network = "finney" - config0.subtensor.chain_endpoint = "wss://finney.subtensor.io" # Should not match bittensor.__finney_entrypoint__ - assert config0.subtensor.chain_endpoint != bittensor.__finney_entrypoint__ + config0.subtensor.chain_endpoint = "wss://finney.subtensor.io" # Should not match bittensor.core.settings.finney_entrypoint + assert config0.subtensor.chain_endpoint != settings.finney_entrypoint config1 = bittensor.subtensor.config() config1.subtensor.network = "local" @@ -94,7 +95,7 @@ def test_network_overrides(self): sub1 = bittensor.subtensor(config=config1, network="local") self.assertEqual( sub1.chain_endpoint, - bittensor.__local_entrypoint__, + settings.local_entrypoint, msg="Explicit network arg should override config.network", ) @@ -102,14 +103,14 @@ def test_network_overrides(self): sub2 = bittensor.subtensor(config=config0) self.assertNotEqual( sub2.chain_endpoint, - bittensor.__finney_entrypoint__, # Here we expect the endpoint corresponding to the network "finney" + settings.finney_entrypoint, # Here we expect the endpoint corresponding to the network "finney" msg="config.network should override config.chain_endpoint", ) sub3 = bittensor.subtensor(config=config1) # Should pick local instead of finney (default) assert sub3.network == "local" - assert sub3.chain_endpoint == bittensor.__local_entrypoint__ + assert sub3.chain_endpoint == settings.local_entrypoint def test_get_current_block(self): block = self.subtensor.get_current_block() @@ -843,7 +844,7 @@ class ExitEarly(Exception): def test_defaults_to_finney(self): sub = bittensor.subtensor() assert sub.network == "finney" - assert sub.chain_endpoint == bittensor.__finney_entrypoint__ + assert sub.chain_endpoint == settings.finney_entrypoint if __name__ == "__main__": diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index fc793c0f3..d9e21735a 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -1,9 +1,24 @@ +# 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 pytest -import bittensor import torch from bittensor.core.chain_data import AxonInfo, ChainDataType, DelegateInfo, NeuronInfo -SS58_FORMAT = bittensor.__ss58_format__ RAOPERTAO = 10**18 diff --git a/tests/unit_tests/test_metagraph.py b/tests/unit_tests/test_metagraph.py index 181ee7c9d..7b39ba0e1 100644 --- a/tests/unit_tests/test_metagraph.py +++ b/tests/unit_tests/test_metagraph.py @@ -1,27 +1,29 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Technologies Inc - +# 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. +from unittest.mock import MagicMock from unittest.mock import Mock -import pytest + import numpy as np -import bittensor +import pytest +import bittensor +from bittensor.core import settings from bittensor.core.metagraph import metagraph as Metagraph -from unittest.mock import MagicMock @pytest.fixture @@ -128,7 +130,7 @@ def test_process_weights_or_bonds(mock_environment): @pytest.fixture def mock_subtensor(): subtensor = MagicMock() - subtensor.chain_endpoint = bittensor.__finney_entrypoint__ + subtensor.chain_endpoint = settings.finney_entrypoint subtensor.network = "finney" subtensor.get_current_block.return_value = 601 return subtensor diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 6973ee53d..daeeeec09 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -22,7 +22,7 @@ import pytest import bittensor -from bittensor.core import subtensor as subtensor_module +from bittensor.core import subtensor as subtensor_module, settings from bittensor.btcli.commands.utils import normalize_hyperparameters from bittensor.core.chain_data import SubnetHyperparameters from bittensor.core.subtensor import Subtensor, _logger @@ -223,40 +223,40 @@ def mock_add_argument(*args, **kwargs): "network, expected_network, expected_endpoint", [ # Happy path tests - ("finney", "finney", bittensor.__finney_entrypoint__), - ("local", "local", bittensor.__local_entrypoint__), - ("test", "test", bittensor.__finney_test_entrypoint__), - ("archive", "archive", bittensor.__archive_entrypoint__), + ("finney", "finney", settings.finney_entrypoint), + ("local", "local", settings.local_entrypoint), + ("test", "test", settings.finney_test_entrypoint), + ("archive", "archive", settings.archive_entrypoint), # Endpoint override tests ( - bittensor.__finney_entrypoint__, + settings.finney_entrypoint, "finney", - bittensor.__finney_entrypoint__, + settings.finney_entrypoint, ), ( "entrypoint-finney.opentensor.ai", "finney", - bittensor.__finney_entrypoint__, + settings.finney_entrypoint, ), ( - bittensor.__finney_test_entrypoint__, + settings.finney_test_entrypoint, "test", - bittensor.__finney_test_entrypoint__, + settings.finney_test_entrypoint, ), ( "test.finney.opentensor.ai", "test", - bittensor.__finney_test_entrypoint__, + settings.finney_test_entrypoint, ), ( - bittensor.__archive_entrypoint__, + settings.archive_entrypoint, "archive", - bittensor.__archive_entrypoint__, + settings.archive_entrypoint, ), ( "archive.chain.opentensor.ai", "archive", - bittensor.__archive_entrypoint__, + settings.archive_entrypoint, ), ("127.0.0.1", "local", "127.0.0.1"), ("localhost", "local", "localhost"), @@ -527,7 +527,7 @@ def test_hyperparameter_normalization( # Mid-value test if is_balance: - numeric_value = float(str(norm_value).lstrip(bittensor.__tao_symbol__)) + numeric_value = float(str(norm_value).lstrip(settings.tao_symbol)) expected_tao = mid_value / 1e9 assert ( numeric_value == expected_tao @@ -541,7 +541,7 @@ def test_hyperparameter_normalization( norm_value = get_normalized_value(normalized, param_name) if is_balance: - numeric_value = float(str(norm_value).lstrip(bittensor.__tao_symbol__)) + numeric_value = float(str(norm_value).lstrip(settings.tao_symbol)) expected_tao = max_value / 1e9 assert ( numeric_value == expected_tao @@ -555,7 +555,7 @@ def test_hyperparameter_normalization( norm_value = get_normalized_value(normalized, param_name) if is_balance: - numeric_value = float(str(norm_value).lstrip(bittensor.__tao_symbol__)) + numeric_value = float(str(norm_value).lstrip(settings.tao_symbol)) expected_tao = zero_value / 1e9 assert ( numeric_value == expected_tao From 087b1ff412b3eb2ddc240985a747062bc8b1d084 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 31 Jul 2024 19:52:14 -0700 Subject: [PATCH 039/260] Fix order import problem for bt/__init__.py --- bittensor/__init__.py | 106 ++++++++++++++---------------- bittensor/btcli/commands/utils.py | 11 ++-- bittensor/core/axon.py | 12 ++-- bittensor/core/dendrite.py | 50 +++++++------- bittensor/core/stream.py | 30 +++++++-- 5 files changed, 112 insertions(+), 97 deletions(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 797972fc8..885f144ea 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -29,10 +29,51 @@ import os import warnings +from bittensor_wallet.errors import KeyFileError # noqa: F401 +from bittensor_wallet.keyfile import ( # noqa: F401 + 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, +) +from bittensor_wallet.wallet import display_mnemonic_msg, Wallet # noqa: F401 from rich.console import Console from rich.traceback import install -from .core import settings +from substrateinterface import Keypair # noqa: F401 +from .btcli.cli import cli as cli, COMMANDS as ALL_COMMANDS +from .core import settings +from .core.axon import Axon +from .core.chain_data import ( + AxonInfo, + NeuronInfo, + NeuronInfoLite, + PrometheusInfo, + DelegateInfo, + StakeInfo, + SubnetInfo, + SubnetHyperparameters, + IPInfo, + ProposalCallData, + ProposalVoteData, +) +from .core.config import ( # noqa: F401 + InvalidConfigFile, + DefaultConfig, + Config, + T, +) +from .core.dendrite import dendrite as dendrite from .core.errors import ( BlacklistedException, ChainConnectionError, @@ -58,33 +99,15 @@ UnknownSynapseError, UnstakeError, ) - -from bittensor_wallet.errors import KeyFileError # noqa: F401 -from substrateinterface import Keypair # noqa: F401 -from .core.config import ( # noqa: F401 - InvalidConfigFile, - DefaultConfig, - Config, - T, -) -from bittensor_wallet.keyfile import ( # noqa: F401 - 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, -) -from bittensor_wallet.wallet import display_mnemonic_msg, Wallet # noqa: F401 - +from .core.metagraph import metagraph as metagraph +from .core.settings import blocktime +from .core.stream import StreamingSynapse +from .core.subnets import SubnetsAPI as SubnetsAPI +from .core.subtensor import Subtensor +from .core.synapse import TerminalInfo, Synapse +from .core.tensor import tensor, Tensor +from .core.threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor +from .mock.subtensor_mock import MockSubtensor as MockSubtensor from .utils import ( ss58_to_vec_u8, unbiased_topk, @@ -101,35 +124,8 @@ hash, wallet_utils, ) - from .utils.balance import Balance as Balance -from .core.chain_data import ( - AxonInfo, - NeuronInfo, - NeuronInfoLite, - PrometheusInfo, - DelegateInfo, - StakeInfo, - SubnetInfo, - SubnetHyperparameters, - IPInfo, - ProposalCallData, - ProposalVoteData, -) - -from .btcli.cli import cli as cli, COMMANDS as ALL_COMMANDS -from .core.subtensor import Subtensor -from .core.metagraph import metagraph as metagraph -from .core.threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor -from .core.synapse import TerminalInfo, Synapse -from .core.stream import StreamingSynapse -from .core.tensor import tensor, Tensor -from .core.axon import Axon -from .core.dendrite import dendrite as dendrite -from .core.subnets import SubnetsAPI as SubnetsAPI -from .mock.subtensor_mock import MockSubtensor as MockSubtensor from .utils.btlogging import logging -from .core.settings import blocktime # Raw GitHub url for delegates registry file __delegates_details_url__: str = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" diff --git a/bittensor/btcli/commands/utils.py b/bittensor/btcli/commands/utils.py index 7ad60791b..3b0270fcf 100644 --- a/bittensor/btcli/commands/utils.py +++ b/bittensor/btcli/commands/utils.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -29,6 +29,7 @@ from bittensor.utils.balance import Balance from bittensor.utils.registration import torch from . import defaults +from ...core.chain_data import SubnetHyperparameters console = Console() @@ -200,7 +201,7 @@ def filter_netuids_by_registered_hotkeys( def normalize_hyperparameters( - subnet: bittensor.SubnetHyperparameters, + subnet: "SubnetHyperparameters", ) -> List[Tuple[str, str, str]]: """ Normalizes the hyperparameters of a subnet. diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 8a9afb243..ccb284e9c 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -55,11 +55,9 @@ SynapseParsingError, UnknownSynapseError, ) - from .synapse import Synapse from .threadpool import PriorityThreadPoolExecutor - V_7_2_0 = 7002000 @@ -1155,7 +1153,7 @@ async def dispatch( # Return the response to the requester. return response - async def preprocess(self, request: Request) -> Synapse: + async def preprocess(self, request: Request) -> "Synapse": """ Performs the initial processing of the incoming request. This method is responsible for extracting relevant information from the request and setting up the Synapse object, which @@ -1223,7 +1221,7 @@ async def preprocess(self, request: Request) -> Synapse: # Return the setup synapse. return synapse - async def verify(self, synapse: bittensor.Synapse): + async def verify(self, synapse: "Synapse"): """ Verifies the authenticity and integrity of the request. This method ensures that the incoming request meets the predefined security and validation criteria. @@ -1280,7 +1278,7 @@ async def verify(self, synapse: bittensor.Synapse): f"Not Verified with error: {str(e)}", synapse=synapse ) - async def blacklist(self, synapse: bittensor.Synapse): + async def blacklist(self, synapse: "Synapse"): """ Checks if the request should be blacklisted. This method ensures that requests from disallowed sources or with malicious intent are blocked from processing. This can be extremely useful for @@ -1336,7 +1334,7 @@ async def blacklist(self, synapse: bittensor.Synapse): f"Forbidden. Key is blacklisted: {reason}.", synapse=synapse ) - async def priority(self, synapse: bittensor.Synapse): + async def priority(self, synapse: "Synapse"): """ Executes the priority function for the request. This method assesses and assigns a priority level to the request, determining its urgency and importance in the processing queue. @@ -1403,7 +1401,7 @@ async def submit_task( async def run( self, - synapse: bittensor.Synapse, + synapse: "Synapse", call_next: RequestResponseEndpoint, request: Request, ) -> Response: diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 0058dba7c..58cb66f3d 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -26,6 +26,8 @@ import bittensor from bittensor.utils.registration import torch, use_torch +from .stream import StreamingSynapse +from .synapse import Synapse DENDRITE_ERROR_MAPPING: Dict[Type[Exception], tuple] = { aiohttp.ClientConnectorError: ("503", "Service unavailable"), @@ -89,21 +91,21 @@ class DendriteMixin: Example with a context manager:: - >>> aysnc with dendrite(wallet = bittensor.wallet()) as d: - >>> print(d) - >>> d( ) # ping axon - >>> d( [] ) # ping multiple - >>> d( bittensor.axon(), bittensor.Synapse ) + aysnc with dendrite(wallet = bittensor.wallet()) as d: + print(d) + d( ) # ping axon + d( [] ) # ping multiple + d( bittensor.axon(), bittensor.Synapse ) However, you are able to safely call :func:`dendrite.query()` without a context manager in a synchronous setting. Example without a context manager:: - >>> d = dendrite(wallet = bittensor.wallet() ) - >>> print(d) - >>> d( ) # ping axon - >>> d( [] ) # ping multiple - >>> d( bittensor.axon(), bittensor.Synapse ) + d = dendrite(wallet = bittensor.wallet() ) + print(d) + d( ) # ping axon + d( [] ) # ping multiple + d( bittensor.axon(), bittensor.Synapse ) """ def __init__( @@ -370,7 +372,7 @@ async def forward( List[Union[bittensor.AxonInfo, bittensor.axon]], Union[bittensor.AxonInfo, bittensor.axon], ], - synapse: bittensor.Synapse = bittensor.Synapse(), + synapse: "Synapse" = Synapse(), timeout: float = 12, deserialize: bool = True, run_async: bool = True, @@ -392,12 +394,12 @@ async def forward( For example:: - >>> ... - >>> wallet = bittensor.wallet() # Initialize a wallet - >>> synapse = bittensor.Synapse(...) # Create a synapse object that contains query data - >>> dendrte = bittensor.dendrite(wallet = wallet) # Initialize a dendrite instance - >>> axons = metagraph.axons # Create a list of axons to query - >>> responses = await dendrite(axons, synapse) # Send the query to all axons and await the responses + ... + wallet = bittensor.wallet() # Initialize a wallet + synapse = bittensor.Synapse(...) # Create a synapse object that contains query data + dendrte = bittensor.dendrite(wallet = wallet) # Initialize a dendrite instance + axons = metagraph.axons # Create a list of axons to query + responses = await dendrite(axons, synapse) # Send the query to all axons and await the responses When querying an Axon that sends back data in chunks using the Dendrite, this function returns an AsyncGenerator that yields each chunk as it is received. The generator can be @@ -405,11 +407,11 @@ async def forward( For example:: - >>> ... - >>> dendrte = bittensor.dendrite(wallet = wallet) - >>> async for chunk in dendrite.forward(axons, synapse, timeout, deserialize, run_async, streaming): - >>> # Process each chunk here - >>> print(chunk) + ... + dendrte = bittensor.dendrite(wallet = wallet) + async for chunk in dendrite.forward(axons, synapse, timeout, deserialize, run_async, streaming): + # Process each chunk here + print(chunk) Args: axons (Union[List[Union['bittensor.AxonInfo', 'bittensor.axon']], Union['bittensor.AxonInfo', 'bittensor.axon']]): @@ -519,7 +521,7 @@ async def single_axon_response( async def call( self, target_axon: Union[bittensor.AxonInfo, bittensor.axon], - synapse: bittensor.Synapse = bittensor.Synapse(), + synapse: "Synapse" = Synapse(), timeout: float = 12.0, deserialize: bool = True, ) -> bittensor.Synapse: @@ -591,7 +593,7 @@ async def call( async def call_stream( self, target_axon: Union[bittensor.AxonInfo, bittensor.axon], - synapse: bittensor.StreamingSynapse = bittensor.Synapse(), # type: ignore + synapse: "StreamingSynapse" = Synapse(), # type: ignore timeout: float = 12.0, deserialize: bool = True, ) -> AsyncGenerator[Any, Any]: diff --git a/bittensor/core/stream.py b/bittensor/core/stream.py index e0dc17c42..df37da557 100644 --- a/bittensor/core/stream.py +++ b/bittensor/core/stream.py @@ -1,11 +1,29 @@ -from aiohttp import ClientResponse -import bittensor +# 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. -from starlette.responses import StreamingResponse as _StreamingResponse -from starlette.types import Send, Receive, Scope +from abc import ABC, abstractmethod from typing import Callable, Awaitable + +from aiohttp import ClientResponse from pydantic import ConfigDict, BaseModel -from abc import ABC, abstractmethod +from starlette.responses import StreamingResponse as _StreamingResponse +from starlette.types import Send, Receive, Scope + +from .synapse import Synapse class BTStreamingResponseModel(BaseModel): @@ -29,7 +47,7 @@ class BTStreamingResponseModel(BaseModel): token_streamer: Callable[[Send], Awaitable[None]] -class StreamingSynapse(bittensor.Synapse, ABC): +class StreamingSynapse(Synapse, ABC): """ The :func:`StreamingSynapse` class is designed to be subclassed for handling streaming responses in the Bittensor network. It provides abstract methods that must be implemented by the subclass to deserialize, process streaming responses, From 702b74e4d9767f1147ec6df5168c6a4a1b6b76e3 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 1 Aug 2024 10:06:40 -0700 Subject: [PATCH 040/260] Remove unused error parser based on metadata --- bittensor/core/subtensor.py | 29 ----- bittensor/utils/subtensor.py | 139 ----------------------- tests/unit_tests/utils/test_subtensor.py | 99 ---------------- 3 files changed, 267 deletions(-) delete mode 100644 bittensor/utils/subtensor.py delete mode 100644 tests/unit_tests/utils/test_subtensor.py diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index aced0c4d1..95f7ef0d4 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -103,7 +103,6 @@ from bittensor.utils.balance import Balance from bittensor.utils.registration import POWSolution from bittensor.utils.registration import legacy_torch_api_compat -from bittensor.utils.subtensor import get_subtensor_errors from . import settings KEY_NONCE: Dict[str, int] = {} @@ -5532,31 +5531,3 @@ def get_block_hash(self, block_id: int) -> str: maintaining the trustworthiness of the blockchain. """ return self.substrate.get_block_hash(block_id=block_id) - - def get_error_info_by_index(self, error_index: int) -> Tuple[str, str]: - """ - Returns the error name and description from the Subtensor error list. - - Args: - error_index (int): The index of the error to retrieve. - - Returns: - Tuple[str, str]: A tuple containing the error name and description from substrate metadata. If the error index is not found, returns ("Unknown Error", "") and logs a warning. - """ - unknown_error = ("Unknown Error", "") - - if not self._subtensor_errors: - self._subtensor_errors = get_subtensor_errors(self.substrate) - - name, description = self._subtensor_errors.get(str(error_index), unknown_error) - - if name == unknown_error[0]: - _logger.warning( - f"Subtensor returned an error with an unknown index: {error_index}" - ) - - return name, description - - -# TODO: remove this after fully migrate `bittensor.subtensor` to `bittensor.Subtensor` in `bittensor/__init__.py` -subtensor = Subtensor diff --git a/bittensor/utils/subtensor.py b/bittensor/utils/subtensor.py deleted file mode 100644 index 484184e77..000000000 --- a/bittensor/utils/subtensor.py +++ /dev/null @@ -1,139 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - -# 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. - -"""Module providing common helper functions for working with Subtensor.""" - -import json -import logging -import os -from typing import Dict, Optional, Union, Any - -from substrateinterface.base import SubstrateInterface - -_logger = logging.getLogger("subtensor.errors_handler") - -_USER_HOME_DIR = os.path.expanduser("~") -_BT_DIR = os.path.join(_USER_HOME_DIR, ".bittensor") -_ERRORS_FILE_PATH = os.path.join(_BT_DIR, "subtensor_errors_map.json") -_ST_BUILD_ID = "subtensor_build_id" - -# Create directory if it doesn't exist -os.makedirs(_BT_DIR, exist_ok=True) - - -# Pallet's typing class `PalletMetadataV14` is defined only at -# https://github.com/polkascan/py-scale-codec/blob/master/scalecodec/type_registry/core.json#L1024 -# A class object is created dynamically at runtime. -# Circleci linter complains about string represented classes like 'PalletMetadataV14'. -def _get_errors_from_pallet(pallet) -> Optional[Dict[str, Dict[str, str]]]: - """Extracts and returns error information from the given pallet metadata. - - Args: - pallet (PalletMetadataV14): The pallet metadata containing error definitions. - - Returns: - dict[str, str]: A dictionary of errors indexed by their IDs. - - Raises: - ValueError: If the pallet does not contain error definitions or the list is empty. - """ - if not hasattr(pallet, "errors") or not pallet.errors: - _logger.warning( - "The pallet does not contain any error definitions or the list is empty." - ) - return None - - return { - str(error["index"]): { - "name": error["name"], - "description": " ".join(error["docs"]), - } - for error in pallet.errors - } - - -def _save_errors_to_cache(uniq_version: str, errors: Dict[str, Dict[str, str]]): - """Saves error details and unique version identifier to a JSON file. - - Args: - uniq_version (str): Unique version identifier for the Subtensor build. - errors (dict[str, str]): Error information to be cached. - """ - data = {_ST_BUILD_ID: uniq_version, "errors": errors} - try: - with open(_ERRORS_FILE_PATH, "w") as json_file: - json.dump(data, json_file, indent=4) - except IOError as e: - _logger.warning(f"Error saving to file: {e}") - - -def _get_errors_from_cache() -> Optional[Dict[str, Dict[str, Dict[str, str]]]]: - """Retrieves and returns the cached error information from a JSON file, if it exists. - - Returns: - A dictionary containing error information. - """ - if not os.path.exists(_ERRORS_FILE_PATH): - return None - - try: - with open(_ERRORS_FILE_PATH, "r") as json_file: - data = json.load(json_file) - except IOError as e: - _logger.warning(f"Error reading from file: {e}") - return None - - return data - - -def get_subtensor_errors( - substrate: SubstrateInterface, -) -> Union[Dict[str, Dict[str, str]], Dict[Any, Any]]: - """Fetches or retrieves cached Subtensor error definitions using metadata. - - Args: - substrate (SubstrateInterface): Instance of SubstrateInterface to access metadata. - - Returns: - dict[str, str]: A dictionary containing error information. - """ - if not substrate.metadata: - substrate.get_metadata() - - cached_errors_map = _get_errors_from_cache() - # TODO: Talk to the Nucleus team about a unique identification for each assembly (subtensor). Before that, use - # the metadata value for `subtensor_build_id` - subtensor_build_id = substrate.metadata[0].value - - if not cached_errors_map or subtensor_build_id != cached_errors_map.get( - _ST_BUILD_ID - ): - pallet = substrate.metadata.get_metadata_pallet("SubtensorModule") - subtensor_errors_map = _get_errors_from_pallet(pallet) - - if not subtensor_errors_map: - return {} - - _save_errors_to_cache( - uniq_version=substrate.metadata[0].value, errors=subtensor_errors_map - ) - _logger.info(f"File {_ERRORS_FILE_PATH} has been updated.") - return subtensor_errors_map - else: - return cached_errors_map.get("errors", {}) diff --git a/tests/unit_tests/utils/test_subtensor.py b/tests/unit_tests/utils/test_subtensor.py deleted file mode 100644 index 1c1220bce..000000000 --- a/tests/unit_tests/utils/test_subtensor.py +++ /dev/null @@ -1,99 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2022 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 json - -import pytest - -import bittensor.utils.subtensor as st_utils - - -class MockPallet: - def __init__(self, errors): - self.errors = errors - - -@pytest.fixture -def pallet_with_errors(): - """Provide a mock pallet with sample errors.""" - return MockPallet( - [ - {"index": 1, "name": "ErrorOne", "docs": ["Description one."]}, - { - "index": 2, - "name": "ErrorTwo", - "docs": ["Description two.", "Continued."], - }, - ] - ) - - -@pytest.fixture -def empty_pallet(): - """Provide a mock pallet with no errors.""" - return MockPallet([]) - - -def test_get_errors_from_pallet_with_errors(pallet_with_errors): - """Ensure errors are correctly parsed from pallet.""" - expected = { - "1": {"name": "ErrorOne", "description": "Description one."}, - "2": {"name": "ErrorTwo", "description": "Description two. Continued."}, - } - assert st_utils._get_errors_from_pallet(pallet_with_errors) == expected - - -def test_get_errors_from_pallet_empty(empty_pallet): - """Test behavior with an empty list of errors.""" - assert st_utils._get_errors_from_pallet(empty_pallet) is None - - -def test_save_errors_to_cache(tmp_path): - """Ensure that errors are correctly saved to a file.""" - test_file = tmp_path / "subtensor_errors_map.json" - errors = {"1": {"name": "ErrorOne", "description": "Description one."}} - st_utils._ERRORS_FILE_PATH = test_file - st_utils._save_errors_to_cache("0x123", errors) - - with open(test_file, "r") as file: - data = json.load(file) - assert data["subtensor_build_id"] == "0x123" - assert data["errors"] == errors - - -def test_get_errors_from_cache(tmp_path): - """Test retrieval of errors from cache.""" - test_file = tmp_path / "subtensor_errors_map.json" - errors = {"1": {"name": "ErrorOne", "description": "Description one."}} - - st_utils._ERRORS_FILE_PATH = test_file - with open(test_file, "w") as file: - json.dump({"subtensor_build_id": "0x123", "errors": errors}, file) - assert st_utils._get_errors_from_cache() == { - "subtensor_build_id": "0x123", - "errors": errors, - } - - -def test_get_errors_no_cache(mocker, empty_pallet): - """Test get_errors function when no cache is available.""" - mocker.patch("bittensor.utils.subtensor._get_errors_from_cache", return_value=None) - mocker.patch("bittensor.utils.subtensor.SubstrateInterface") - substrate_mock = mocker.MagicMock() - substrate_mock.metadata.get_metadata_pallet.return_value = empty_pallet - substrate_mock.metadata[0].value = "0x123" - assert st_utils.get_subtensor_errors(substrate_mock) == {} From d69e6edeeeac2a3e177b3c2e4a4ac43643941c2a Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 1 Aug 2024 10:53:59 -0700 Subject: [PATCH 041/260] Move `delegates_details_url` from bt/__init__.py to settings.py --- bittensor/__init__.py | 7 +++---- bittensor/btcli/commands/delegates.py | 7 ++++--- bittensor/btcli/commands/inspect.py | 3 ++- bittensor/btcli/commands/network.py | 3 ++- bittensor/btcli/commands/root.py | 3 ++- bittensor/btcli/commands/senate.py | 7 ++++--- bittensor/btcli/commands/stake.py | 6 +++--- bittensor/core/settings.py | 3 ++- bittensor/utils/balance.py | 1 - tests/unit_tests/test_subtensor.py | 24 ------------------------ 10 files changed, 22 insertions(+), 42 deletions(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 885f144ea..50f311e3c 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -127,9 +127,6 @@ from .utils.balance import Balance as Balance from .utils.btlogging import logging -# Raw GitHub url for delegates registry file -__delegates_details_url__: str = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" - configs = [ Axon.config(), @@ -223,11 +220,13 @@ def __apply_nest_asyncio(): __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 -__bellagene_entrypoint__ = settings.bellagene_entrypoint __local_entrypoint__ = settings.local_entrypoint + __tao_symbol__ = settings.tao_symbol __rao_symbol__ = settings.rao_symbol diff --git a/bittensor/btcli/commands/delegates.py b/bittensor/btcli/commands/delegates.py index 909b66c1f..257959a36 100644 --- a/bittensor/btcli/commands/delegates.py +++ b/bittensor/btcli/commands/delegates.py @@ -28,6 +28,7 @@ import bittensor from . import defaults +from ...core import settings from .identity import SetIdentityCommand from .utils import get_delegates_details, DelegatesDetails @@ -84,7 +85,7 @@ def show_delegates_lite( """ registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) + get_delegates_details(url=settings.delegates_details_url) ) if registered_delegate_info is None: bittensor.__console__.print( @@ -207,7 +208,7 @@ def show_delegates( prev_delegates_dict[prev_delegate.hotkey_ss58] = prev_delegate registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) + get_delegates_details(url=settings.delegates_details_url) ) if registered_delegate_info is None: bittensor.__console__.print( @@ -949,7 +950,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): total_delegated += sum(my_delegates.values()) registered_delegate_info: Optional[DelegatesDetails] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) + get_delegates_details(url=settings.delegates_details_url) ) if registered_delegate_info is None: bittensor.__console__.print( diff --git a/bittensor/btcli/commands/inspect.py b/bittensor/btcli/commands/inspect.py index 4abcc733b..5ab093bc3 100644 --- a/bittensor/btcli/commands/inspect.py +++ b/bittensor/btcli/commands/inspect.py @@ -26,6 +26,7 @@ import bittensor from . import defaults +from ...core import settings from .utils import ( get_delegates_details, DelegatesDetails, @@ -140,7 +141,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): bittensor.logging.debug(f"Netuids to check: {netuids}") registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) + get_delegates_details(url=settings.delegates_details_url) ) if registered_delegate_info is None: bittensor.__console__.print( diff --git a/bittensor/btcli/commands/network.py b/bittensor/btcli/commands/network.py index 1715100cb..35199b957 100644 --- a/bittensor/btcli/commands/network.py +++ b/bittensor/btcli/commands/network.py @@ -24,6 +24,7 @@ import bittensor from . import defaults # type: ignore +from ...core import settings from .identity import SetIdentityCommand from .utils import ( get_delegates_details, @@ -251,7 +252,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): rows = [] total_neurons = 0 delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - url=bittensor.__delegates_details_url__ + url=settings.delegates_details_url ) for subnet in subnets: diff --git a/bittensor/btcli/commands/root.py b/bittensor/btcli/commands/root.py index ed4994d0d..2aae7e5e3 100644 --- a/bittensor/btcli/commands/root.py +++ b/bittensor/btcli/commands/root.py @@ -27,6 +27,7 @@ from .utils import get_delegates_details, DelegatesDetails from . import defaults +from ...core import settings console = Console() @@ -150,7 +151,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): netuid=0 ) delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - url=bittensor.__delegates_details_url__ + url=settings.delegates_details_url ) table = Table(show_footer=False) diff --git a/bittensor/btcli/commands/senate.py b/bittensor/btcli/commands/senate.py index d96e19adc..6a8e80db7 100644 --- a/bittensor/btcli/commands/senate.py +++ b/bittensor/btcli/commands/senate.py @@ -25,6 +25,7 @@ import bittensor from . import defaults +from ...core import settings from .utils import get_delegates_details, DelegatesDetails console = Console() @@ -74,7 +75,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): senate_members = subtensor.get_senate_members() delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - url=bittensor.__delegates_details_url__ + url=settings.delegates_details_url ) table = Table(show_footer=False) @@ -215,7 +216,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): proposals = subtensor.get_proposals() registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) + get_delegates_details(url=settings.delegates_details_url) ) table = Table(show_footer=False) @@ -346,7 +347,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): return registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) + get_delegates_details(url=settings.delegates_details_url) ) table = Table(show_footer=False) diff --git a/bittensor/btcli/commands/stake.py b/bittensor/btcli/commands/stake.py index e0250746b..cdf7ede04 100644 --- a/bittensor/btcli/commands/stake.py +++ b/bittensor/btcli/commands/stake.py @@ -378,13 +378,13 @@ def run(cli: "bittensor.cli"): @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Show all stake accounts.""" - if cli.config.get("all", d=False) == True: + """Show all stake accounts.""" + if cli.config.get("all", d=False): wallets = _get_coldkey_wallets_for_path(cli.config.wallet.path) else: wallets = [bittensor.wallet(config=cli.config)] registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=bittensor.__delegates_details_url__) + get_delegates_details(url=settings.delegates_details_url) ) def get_stake_accounts( diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 2a577fb7b..9ee6db977 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -25,7 +25,6 @@ finney_entrypoint = "wss://entrypoint-finney.opentensor.ai:443" finney_test_entrypoint = "wss://test.finney.opentensor.ai:443/" archive_entrypoint = "wss://archive.chain.opentensor.ai:443/" -bellagene_entrypoint = "wss://parachain.opentensor.ai:443" local_entrypoint = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or "ws://127.0.0.1:9944" # Currency Symbols Bittensor @@ -44,6 +43,8 @@ # Wallet ss58 address length ss58_address_length = 48 +# Raw GitHub url for delegates registry file +delegates_details_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" # Block Explorers map network to explorer url # Must all be polkadotjs explorer urls diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index bbd205795..f95efbbc7 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -19,7 +19,6 @@ from typing import Union -import bittensor from ..core import settings diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index daeeeec09..f5d3ec227 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -289,39 +289,15 @@ class MockSubstrate: @pytest.fixture def subtensor(substrate): - mock.patch.object( - subtensor_module, - "get_subtensor_errors", - return_value={ - "1": ("ErrorOne", "Description one"), - "2": ("ErrorTwo", "Description two"), - }, - ).start() return Subtensor() -def test_get_error_info_by_index_known_error(subtensor): - name, description = subtensor.get_error_info_by_index(1) - assert name == "ErrorOne" - assert description == "Description one" - - @pytest.fixture def mock_logger(): with mock.patch.object(_logger, "warning") as mock_warning: yield mock_warning -def test_get_error_info_by_index_unknown_error(subtensor, mock_logger): - fake_index = 999 - name, description = subtensor.get_error_info_by_index(fake_index) - assert name == "Unknown Error" - assert description == "" - mock_logger.assert_called_once_with( - f"Subtensor returned an error with an unknown index: {fake_index}" - ) - - # Subtensor()._get_hyperparameter tests def test_hyperparameter_subnet_does_not_exist(subtensor, mocker): """Tests when the subnet does not exist.""" From d17fdad2ddc2808d2e9341540c088e0bb6dd2768 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 1 Aug 2024 16:55:50 -0700 Subject: [PATCH 042/260] Bump up setuptools version --- requirements/prod.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/prod.txt b/requirements/prod.txt index e52499a38..b9cf7aecc 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -26,6 +26,7 @@ retry requests rich scalecodec==1.2.11 +setuptools~=70.0.0 shtab~=1.6.5 substrate-interface~=1.7.9 termcolor From c047b480d8be2909fa8ce93ccad16b28d7d015ba Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 1 Aug 2024 17:50:07 -0700 Subject: [PATCH 043/260] Make version.py bittensor import independent --- bittensor/utils/version.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index 07a222b2f..fbc1e297a 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -3,8 +3,9 @@ import time from packaging.version import Version -import bittensor import requests +from .btlogging import logging +from ..core.settings import __version__ from ..core.settings import pipaddress VERSION_CHECK_THRESHOLD = 86400 @@ -21,24 +22,24 @@ def _get_version_file_path() -> Path: def _get_version_from_file(version_file: Path) -> Optional[str]: try: mtime = version_file.stat().st_mtime - bittensor.logging.debug(f"Found version file, last modified: {mtime}") + logging.debug(f"Found version file, last modified: {mtime}") diff = time.time() - mtime if diff >= VERSION_CHECK_THRESHOLD: - bittensor.logging.debug("Version file expired") + logging.debug("Version file expired") return None return version_file.read_text() except FileNotFoundError: - bittensor.logging.debug("No bitensor version file found") + logging.debug("No bittensor version file found") return None except OSError: - bittensor.logging.exception("Failed to read version file") + logging.exception("Failed to read version file") return None def _get_version_from_pypi(timeout: int = 15) -> str: - bittensor.logging.debug( + logging.debug( f"Checking latest Bittensor version at: {pipaddress}" ) try: @@ -46,7 +47,7 @@ def _get_version_from_pypi(timeout: int = 15) -> str: latest_version = response.json()["info"]["version"] return latest_version except requests.exceptions.RequestException: - bittensor.logging.exception("Failed to get latest version from pypi") + logging.exception("Failed to get latest version from pypi") raise @@ -61,25 +62,25 @@ def get_and_save_latest_version(timeout: int = 15) -> str: try: version_file.write_text(latest_version) except OSError: - bittensor.logging.exception("Failed to save latest version to file") + logging.exception("Failed to save latest version to file") return latest_version def check_version(timeout: int = 15): """ - Check if the current version of Bittensor is up to date with the latest version on PyPi. + Check if the current version of Bittensor is up-to-date with the latest version on PyPi. Raises a VersionCheckError if the version check fails. """ try: latest_version = get_and_save_latest_version(timeout) - if Version(latest_version) > Version(bittensor.__version__): + if Version(latest_version) > Version(__version__): print( "\u001b[33mBittensor Version: Current {}/Latest {}\nPlease update to the latest version at your earliest convenience. " "Run the following command to upgrade:\n\n\u001b[0mpython -m pip install --upgrade bittensor".format( - bittensor.__version__, latest_version + __version__, latest_version ) ) except Exception as e: @@ -101,4 +102,4 @@ def version_checking(timeout: int = 15): try: check_version(timeout) except VersionCheckError: - bittensor.logging.exception("Version check failed") + logging.exception("Version check failed") From a262dd071398a3b1d0e7c5764002bf8e860c6add Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 1 Aug 2024 17:51:51 -0700 Subject: [PATCH 044/260] Update setup.py, replace file with the __version__ --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 49c419724..43cbabe83 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -52,7 +52,7 @@ def read_requirements(path): # loading version from setup.py with codecs.open( - os.path.join(here, "bittensor/__init__.py"), encoding="utf-8" + os.path.join(here, "bittensor/core/settings.py"), encoding="utf-8" ) as init_file: version_match = re.search( r"^__version__ = ['\"]([^'\"]*)['\"]", init_file.read(), re.M From 1b7ceda7e8411ff1f49ae670da5327da089f22e6 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 1 Aug 2024 18:34:16 -0700 Subject: [PATCH 045/260] Update settings.py --- bittensor/core/settings.py | 91 +++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 9ee6db977..60ce69df2 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -15,8 +15,55 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +__version__ = "7.3.0" + import os +import re +from pathlib import Path +from rich.console import Console +from rich.traceback import install + +from munch import munchify + + +# Rich console. +__console__ = Console() +__use_console__ = True + +# Remove overdue locals in debug training. +install(show_locals=False) + + +def turn_console_off(): + global __use_console__ + global __console__ + from io import StringIO + + __use_console__ = False + __console__ = Console(file=StringIO(), stderr=False) + + +def turn_console_on(): + global __use_console__ + global __console__ + __use_console__ = True + __console__ = Console() + + +turn_console_off() + +HOME_DIR = Path.home() +USER_BITTENSOR_DIR = HOME_DIR / ".bittensor" +WALLETS_DIR = USER_BITTENSOR_DIR / "wallets" +MINERS_DIR = USER_BITTENSOR_DIR / "miners" + +DEFAULT_ENDPOINT = "wss://entrypoint-finney.opentensor.ai:443" +DEFAULT_NETWORK = "finney" + +# Create dirs if they don't exist +WALLETS_DIR.mkdir(parents=True, exist_ok=True) +MINERS_DIR.mkdir(parents=True, exist_ok=True) # Bittensor networks name networks = ["local", "finney", "test", "archive"] @@ -176,4 +223,46 @@ } }, }, -} \ No newline at end of file +} + +defaults = Munch = munchify({ + "axon": { + "port": os.getenv("BT_AXON_PORT") or 8091, + "ip": os.getenv("BT_AXON_IP") or "[::]", + "external_port": os.getenv("BT_AXON_EXTERNAL_PORT") or None, + "external_ip": os.getenv("BT_AXON_EXTERNAL_IP") or None, + "max_workers": os.getenv("BT_AXON_MAX_WORKERS") or 10, + }, + "logging": { + "debug": os.getenv("BT_LOGGING_DEBUG") or False, + "trace": os.getenv("BT_LOGGING_TRACE") or False, + "record_log": os.getenv("BT_LOGGING_RECORD_LOG") or False, + "logging_dir": os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR), + }, + "priority": { + "max_workers": os.getenv("BT_PRIORITY_MAX_WORKERS") or 5, + "maxsize": os.getenv("BT_PRIORITY_MAXSIZE") or 10 + }, + "subtensor": { + "chain_endpoint": DEFAULT_ENDPOINT, + "network": DEFAULT_NETWORK, + "_mock": False + }, + "wallet": { + "name": "default", + "hotkey": "default", + "path": str(WALLETS_DIR), + }, +}) + + +# Parsing version without any literals. +__version__ = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) + +version_split = __version__.split(".") +_version_info = tuple(int(part) for part in version_split) +_version_int_base = 1000 +assert max(_version_info) < _version_int_base + +version_as_int: int = sum(e * (_version_int_base**i) for i, e in enumerate(reversed(_version_info))) +assert version_as_int < 2 ** 31 # fits in int32 From 544c4b75494c8692cdc3571d6bf23ae5472ce4a4 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 1 Aug 2024 20:02:04 -0700 Subject: [PATCH 046/260] Make axon independent of bittensor import. Fix axon tests. --- bittensor/core/axon.py | 211 +++++++++++++++------------------- tests/unit_tests/test_axon.py | 47 ++++---- 2 files changed, 116 insertions(+), 142 deletions(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index ccb284e9c..39a8c1dd4 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -23,7 +23,6 @@ import copy import inspect import json -import os import threading import time import traceback @@ -33,6 +32,7 @@ from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple import uvicorn +from bittensor_wallet import Wallet from fastapi import APIRouter, Depends, FastAPI from fastapi.responses import JSONResponse from fastapi.routing import serialize_response @@ -41,10 +41,9 @@ from starlette.responses import Response from substrateinterface import Keypair -import bittensor -from bittensor.utils import networking -from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds -from .errors import ( +from bittensor.core.chain_data import AxonInfo +from bittensor.core.config import Config +from bittensor.core.errors import ( BlacklistedException, InvalidRequestNameError, NotVerifiedException, @@ -55,8 +54,13 @@ SynapseParsingError, UnknownSynapseError, ) -from .synapse import Synapse -from .threadpool import PriorityThreadPoolExecutor +from bittensor.core.settings import defaults, version_as_int +from bittensor.core.subtensor import Subtensor +from bittensor.core.synapse import Synapse, TerminalInfo +from bittensor.core.threadpool import PriorityThreadPoolExecutor +from bittensor.utils import networking +from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds +from bittensor.utils.btlogging import logging V_7_2_0 = 7002000 @@ -103,7 +107,6 @@ def install_signal_handlers(self): """ Overrides the default signal handlers provided by ``uvicorn.Server``. This method is essential to ensure that the signal handling in the threaded server does not interfere with the main application's flow, especially in a complex asynchronous environment like the Axon server. """ - pass @contextlib.contextmanager def run_in_thread(self): @@ -133,8 +136,7 @@ def _wrapper_run(self): def start(self): """ - Starts the FastAPI server in a separate thread if it is not already running. This method sets up the server to handle HTTP requests concurrently, enabling the Axon server to efficiently manage - incoming network requests. + Starts the FastAPI server in a separate thread if it is not already running. This method sets up the server to handle HTTP requests concurrently, enabling the Axon server to efficiently manage incoming network requests. The method ensures that the server starts running in a non-blocking manner, allowing the Axon server to continue its other operations seamlessly. """ @@ -187,30 +189,30 @@ class is designed to be flexible and customizable, allowing users to specify cus import bittensor # Define your custom synapse class - class MySyanpse( bittensor.Synapse ): + class MySynapse( bittensor.Synapse ): input: int = 1 output: int = None # Define a custom request forwarding function using your synapse class - def forward( synapse: MySyanpse ) -> MySyanpse: + def forward( synapse: MySynapse ) -> MySynapse: # Apply custom logic to synapse and return it synapse.output = 2 return synapse # Define a custom request verification function - def verify_my_synapse( synapse: MySyanpse ): + def verify_my_synapse( synapse: MySynapse ): # Apply custom verification logic to synapse # Optionally raise Exception assert synapse.input == 1 ... - # Define a custom request blacklist fucntion - def blacklist_my_synapse( synapse: MySyanpse ) -> bool: + # Define a custom request blacklist function + def blacklist_my_synapse( synapse: MySynapse ) -> bool: # Apply custom blacklist return False ( if non blacklisted ) or True ( if blacklisted ) - # Define a custom request priority fucntion - def prioritize_my_synape( synapse: MySyanpse ) -> float: + # Define a custom request priority function + def prioritize_my_synapse( synapse: MySynapse ) -> float: # Apply custom priority return 1.0 @@ -229,7 +231,7 @@ def prioritize_my_synape( synapse: MySyanpse ) -> float: forward_fn = forward_my_synapse, verify_fn = verify_my_synapse, blacklist_fn = blacklist_my_synapse, - priority_fn = prioritize_my_synape + priority_fn = prioritize_my_synapse ) # Serve and start your axon. @@ -243,12 +245,12 @@ def prioritize_my_synape( synapse: MySyanpse ) -> float: forward_fn = forward_my_synapse, verify_fn = verify_my_synapse, blacklist_fn = blacklist_my_synapse, - priority_fn = prioritize_my_synape + priority_fn = prioritize_my_synapse ).attach( forward_fn = forward_my_synapse_2, verify_fn = verify_my_synapse_2, blacklist_fn = blacklist_my_synapse_2, - priority_fn = prioritize_my_synape_2 + priority_fn = prioritize_my_synapse_2 ).serve( netuid = ... subtensor = ... @@ -287,56 +289,43 @@ def prioritize_my_synape( synapse: MySyanpse ) -> float: Error Handling and Validation The method ensures that the attached functions meet the required signatures, providing error handling to prevent runtime issues. - """ def __init__( self, - wallet: Optional["bittensor.wallet"] = None, - config: Optional["bittensor.config"] = None, + wallet: Optional["Wallet"] = None, + config: Optional["Config"] = None, port: Optional[int] = None, ip: Optional[str] = None, external_ip: Optional[str] = None, external_port: Optional[int] = None, max_workers: Optional[int] = None, ): - r"""Creates a new bittensor.Axon object from passed arguments. + """Creates a new bittensor.Axon object from passed arguments. + Args: - config (:obj:`Optional[bittensor.config]`, `optional`): - bittensor.axon.config() - wallet (:obj:`Optional[bittensor.wallet]`, `optional`): - bittensor wallet with hotkey and coldkeypub. - port (:type:`Optional[int]`, `optional`): - Binding port. - ip (:type:`Optional[str]`, `optional`): - Binding ip. - external_ip (:type:`Optional[str]`, `optional`): - The external ip of the server to broadcast to the network. - external_port (:type:`Optional[int]`, `optional`): - The external port of the server to broadcast to the network. - max_workers (:type:`Optional[int]`, `optional`): - Used to create the threadpool if not passed, specifies the number of active threads servicing requests. + config (:obj:`Optional[bittensor.config]`, `optional`): bittensor.axon.config() + wallet (:obj:`Optional[bittensor.wallet]`, `optional`): bittensor wallet with hotkey and coldkeypub. + port (:type:`Optional[int]`, `optional`): Binding port. + ip (:type:`Optional[str]`, `optional`): Binding ip. + external_ip (:type:`Optional[str]`, `optional`): The external ip of the server to broadcast to the network. + external_port (:type:`Optional[int]`, `optional`): The external port of the server to broadcast to the network. + max_workers (:type:`Optional[int]`, `optional`): Used to create the threadpool if not passed, specifies the number of active threads servicing requests. """ # Build and check config. if config is None: config = Axon.config() - config = copy.deepcopy(config) - config.axon.ip = ip or config.axon.get("ip", bittensor.defaults.axon.ip) - config.axon.port = port or config.axon.get("port", bittensor.defaults.axon.port) - config.axon.external_ip = external_ip or config.axon.get( - "external_ip", bittensor.defaults.axon.external_ip - ) - config.axon.external_port = external_port or config.axon.get( - "external_port", bittensor.defaults.axon.external_port - ) - config.axon.max_workers = max_workers or config.axon.get( - "max_workers", bittensor.defaults.axon.max_workers - ) + config: "Config" = copy.deepcopy(config) + config.axon.ip = ip or defaults.axon.ip + config.axon.port = port or defaults.axon.port + config.axon.external_ip = external_ip or defaults.axon.external_ip + config.axon.external_port = external_port or defaults.axon.external_port + config.axon.max_workers = max_workers or defaults.axon.max_workers Axon.check_config(config) self.config = config # type: ignore # Get wallet or use default. - self.wallet = wallet or bittensor.wallet() + self.wallet = wallet or Wallet() # Build axon objects. self.uuid = str(uuid.uuid1()) @@ -356,7 +345,7 @@ def __init__( self.started = False # Build middleware - self.thread_pool = bittensor.PriorityThreadPoolExecutor( + self.thread_pool = PriorityThreadPoolExecutor( max_workers=self.config.axon.max_workers # type: ignore ) self.nonces: Dict[str, int] = {} @@ -370,7 +359,7 @@ def __init__( # Instantiate FastAPI self.app = FastAPI() - log_level = "trace" if bittensor.logging.__trace_on__ else "critical" + log_level = "trace" if logging.__trace_on__ else "critical" self.fast_config = uvicorn.Config( self.app, host="0.0.0.0", @@ -393,10 +382,10 @@ def ping(r: Synapse) -> Synapse: forward_fn=ping, verify_fn=None, blacklist_fn=None, priority_fn=None ) - def info(self) -> "bittensor.AxonInfo": + def info(self) -> "AxonInfo": """Returns the axon info object associated with this axon.""" - return bittensor.AxonInfo( - version=bittensor.__version_as_int__, + return AxonInfo( + version=version_as_int, ip=self.external_ip, ip_type=networking.ip_version(self.external_ip), port=self.external_port, @@ -413,7 +402,7 @@ def attach( blacklist_fn: Optional[Callable] = None, priority_fn: Optional[Callable] = None, verify_fn: Optional[Callable] = None, - ) -> "bittensor.axon": + ) -> "Axon": """ Attaches custom functions to the Axon server for handling incoming requests. This method enables @@ -423,7 +412,7 @@ def attach( Registers an API endpoint to the FastAPI application router. It uses the name of the first argument of the :func:`forward_fn` function as the endpoint name. - The attach method in the Bittensor framework's axon class is a crucial function for registering + The `attach` method in the Bittensor framework's axon class is a crucial function for registering API endpoints to the Axon's FastAPI application router. This method allows the Axon server to define how it handles incoming requests by attaching functions for forwarding, verifying, blacklisting, and prioritizing requests. It's a key part of customizing the server's behavior @@ -503,8 +492,8 @@ async def endpoint(*args, **kwargs): # Add the endpoint to the router, making it available on both GET and POST methods self.router.add_api_route( - f"/{request_name}", - endpoint, + path=f"/{request_name}", + endpoint=endpoint, methods=["GET", "POST"], dependencies=[Depends(self.verify_body_integrity)], ) @@ -513,8 +502,8 @@ async def endpoint(*args, **kwargs): # Check the signature of blacklist_fn, priority_fn and verify_fn if they are provided expected_params = [ Parameter( - "synapse", - Parameter.POSITIONAL_OR_KEYWORD, + name="synapse", + kind=Parameter.POSITIONAL_OR_KEYWORD, annotation=forward_sig.parameters[ list(forward_sig.parameters)[0] ].annotation, @@ -556,7 +545,7 @@ async def endpoint(*args, **kwargs): return self @classmethod - def config(cls) -> "bittensor.config": + def config(cls) -> "Config": """ Parses the command-line arguments to form a Bittensor configuration object. @@ -565,13 +554,11 @@ def config(cls) -> "bittensor.config": """ parser = argparse.ArgumentParser() Axon.add_args(parser) # Add specific axon-related arguments - return bittensor.Config(parser, args=[]) + return Config(parser, args=[]) @classmethod def help(cls): - """ - Prints the help text (list of command-line arguments and their descriptions) to stdout. - """ + """Prints the help text (list of command-line arguments and their descriptions) to stdout.""" parser = argparse.ArgumentParser() Axon.add_args(parser) # Add specific axon-related arguments print(cls.__new__.__doc__) # Print docstring of the class @@ -591,46 +578,39 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: Optional[str] = None) """ prefix_str = "" if prefix is None else prefix + "." try: - # Get default values from environment variables or use default values - default_axon_port = os.getenv("BT_AXON_PORT") or 8091 - default_axon_ip = os.getenv("BT_AXON_IP") or "[::]" - default_axon_external_port = os.getenv("BT_AXON_EXTERNAL_PORT") or None - default_axon_external_ip = os.getenv("BT_AXON_EXTERNAL_IP") or None - default_axon_max_workers = os.getenv("BT_AXON_MAX_WORERS") or 10 - # Add command-line arguments to the parser parser.add_argument( "--" + prefix_str + "axon.port", type=int, help="The local port this axon endpoint is bound to. i.e. 8091", - default=default_axon_port, + default=defaults.axon.port, ) parser.add_argument( "--" + prefix_str + "axon.ip", type=str, help="""The local ip this axon binds to. ie. [::]""", - default=default_axon_ip, + default=defaults.axon.ip, ) parser.add_argument( "--" + prefix_str + "axon.external_port", type=int, required=False, help="""The public port this axon broadcasts to the network. i.e. 8091""", - default=default_axon_external_port, + default=defaults.axon.external_port, ) parser.add_argument( "--" + prefix_str + "axon.external_ip", type=str, required=False, help="""The external ip this axon broadcasts to the network to. ie. [::]""", - default=default_axon_external_ip, + default=defaults.axon.external_ip, ) parser.add_argument( "--" + prefix_str + "axon.max_workers", type=int, help="""The maximum number connection handler threads working simultaneously on this endpoint. The grpc server distributes new worker threads to service requests up to this number.""", - default=default_axon_max_workers, + default=defaults.axon.max_workers, ) except argparse.ArgumentError: @@ -652,13 +632,10 @@ async def verify_body_integrity(self, request: Request): request (Request): The incoming FastAPI request object containing both headers and the request body. Returns: - dict: Returns the parsed body of the request as a dictionary if all the hash comparisons match, - indicating that the body is intact and has not been tampered with. + dict: Returns the parsed body of the request as a dictionary if all the hash comparisons match, indicating that the body is intact and has not been tampered with. Raises: - JSONResponse: Raises a JSONResponse with a 400 status code if any of the hash comparisons fail, - indicating a potential integrity issue with the incoming request payload. - The response includes the detailed error message specifying which field has a hash mismatch. + JSONResponse: Raises a JSONResponse with a 400 status code if any of the hash comparisons fail, indicating a potential integrity issue with the incoming request payload. The response includes the detailed error message specifying which field has a hash mismatch. This method performs several key functions: @@ -669,11 +646,9 @@ async def verify_body_integrity(self, request: Request): 5. Comparing the recomputed hash with the hash provided in the request headers for verification. Note: - The integrity verification is an essential step in ensuring the security of the data exchange - within the Bittensor network. It helps prevent tampering and manipulation of data during transit, - thereby maintaining the reliability and trust in the network communication. + The integrity verification is an essential step in ensuring the security of the data exchange within the Bittensor network. It helps prevent tampering and manipulation of data during transit, thereby maintaining the reliability and trust in the network communication. """ - # Await and load the request body so we can inspect it + # Await and load the request body, so we can inspect it body = await request.body() request_body = body.decode() if isinstance(body, bytes) else body @@ -696,7 +671,7 @@ async def verify_body_integrity(self, request: Request): return body_dict @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): """ This method checks the configuration for the axon's port and wallet. @@ -715,15 +690,11 @@ def check_config(cls, config: "bittensor.config"): ), "External port must be in range [1024, 65535]" def to_string(self): - """ - Provides a human-readable representation of the AxonInfo for this Axon. - """ + """Provides a human-readable representation of the AxonInfo for this Axon.""" return self.info().to_string() def __str__(self) -> str: - """ - Provides a human-readable representation of the Axon instance. - """ + """Provides a human-readable representation of the Axon instance.""" return "Axon({}, {}, {}, {}, {})".format( self.ip, self.port, @@ -746,7 +717,7 @@ def __del__(self): """ self.stop() - def start(self) -> "bittensor.axon": + def start(self) -> "Axon": """ Starts the Axon server and its underlying FastAPI server thread, transitioning the state of the Axon instance to ``started``. This method initiates the server's ability to accept and process @@ -772,7 +743,7 @@ def start(self) -> "bittensor.axon": self.started = True return self - def stop(self) -> "bittensor.axon": + def stop(self) -> "Axon": """ Stops the Axon server and its underlying GRPC server thread, transitioning the state of the Axon instance to ``stopped``. This method ceases the server's ability to accept new network requests, @@ -800,9 +771,7 @@ def stop(self) -> "bittensor.axon": self.started = False return self - def serve( - self, netuid: int, subtensor: Optional["bittensor.subtensor"] = None - ) -> "bittensor.axon": + def serve(self, netuid: int, subtensor: Optional["Subtensor"] = None) -> "Axon": """ Serves the Axon on the specified subtensor connection using the configured wallet. This method registers the Axon with a specific subnet within the Bittensor network, identified by the ``netuid``. @@ -870,8 +839,7 @@ async def default_verify(self, synapse: Synapse): cryptographic keys can participate in secure communication. Args: - synapse: bittensor.Synapse - bittensor request synapse. + synapse(Synapse): bittensor request synapse. Raises: Exception: If the ``receiver_hotkey`` doesn't match with ``self.receiver_hotkey``. @@ -972,19 +940,19 @@ def log_and_handle_error( if isinstance(exception, SynapseException): synapse = exception.synapse or synapse - bittensor.logging.trace(f"Forward handled exception: {exception}") + logging.trace(f"Forward handled exception: {exception}") else: - bittensor.logging.trace(f"Forward exception: {traceback.format_exc()}") + logging.trace(f"Forward exception: {traceback.format_exc()}") if synapse.axon is None: - synapse.axon = bittensor.TerminalInfo() + synapse.axon = TerminalInfo() # Set the status code of the synapse to the given status code. error_id = str(uuid.uuid4()) error_type = exception.__class__.__name__ # Log the detailed error message for internal use - bittensor.logging.error(f"{error_type}#{error_id}: {exception}") + logging.error(f"{error_type}#{error_id}: {exception}") if not status_code and synapse.axon.status_code != 100: status_code = synapse.axon.status_code @@ -1040,7 +1008,7 @@ class AxonMiddleware(BaseHTTPMiddleware): then handling any postprocessing steps such as response header updating and logging. """ - def __init__(self, app: "AxonMiddleware", axon: "bittensor.axon"): + def __init__(self, app: "AxonMiddleware", axon: "Axon"): """ Initialize the AxonMiddleware class. @@ -1095,11 +1063,11 @@ async def dispatch( # Logs the start of the request processing if synapse.dendrite is not None: - bittensor.logging.trace( + logging.trace( f"axon | <-- | {request.headers.get('content-length', -1)} B | {synapse.name} | {synapse.dendrite.hotkey} | {synapse.dendrite.ip}:{synapse.dendrite.port} | 200 | Success " ) else: - bittensor.logging.trace( + logging.trace( f"axon | <-- | {request.headers.get('content-length', -1)} B | {synapse.name} | None | None | 200 | Success " ) @@ -1118,11 +1086,12 @@ async def dispatch( # Handle errors related to preprocess. except InvalidRequestNameError as e: if synapse.axon is None: - synapse.axon = bittensor.TerminalInfo() + synapse.axon = TerminalInfo() synapse.axon.status_code = 400 synapse.axon.status_message = str(e) synapse = log_and_handle_error(synapse, e, start_time=start_time) response = create_error_response(synapse) + except SynapseException as e: synapse = e.synapse or synapse synapse = log_and_handle_error(synapse, e, start_time=start_time) @@ -1138,15 +1107,15 @@ async def dispatch( # Log the details of the processed synapse, including total size, name, hotkey, IP, port, # status code, and status message, using the debug level of the logger. if synapse.dendrite is not None and synapse.axon is not None: - bittensor.logging.trace( + logging.trace( f"axon | --> | {response.headers.get('content-length', -1)} B | {synapse.name} | {synapse.dendrite.hotkey} | {synapse.dendrite.ip}:{synapse.dendrite.port} | {synapse.axon.status_code} | {synapse.axon.status_message}" ) elif synapse.axon is not None: - bittensor.logging.trace( + logging.trace( f"axon | --> | {response.headers.get('content-length', -1)} B | {synapse.name} | None | None | {synapse.axon.status_code} | {synapse.axon.status_message}" ) else: - bittensor.logging.trace( + logging.trace( f"axon | --> | {response.headers.get('content-length', -1)} B | {synapse.name} | None | None | 200 | Success " ) @@ -1202,7 +1171,7 @@ async def preprocess(self, request: Request) -> "Synapse": # Fills the local axon information into the synapse. synapse.axon.__dict__.update( { - "version": str(bittensor.__version_as_int__), + "version": str(version_as_int), "uuid": str(self.axon.uuid), "nonce": time.time_ns(), "status_code": 100, @@ -1262,7 +1231,7 @@ async def verify(self, synapse: "Synapse"): except Exception as e: # If there was an exception during the verification process, we log that # there was a verification exception. - bittensor.logging.trace(f"Verify exception {str(e)}") + logging.trace(f"Verify exception {str(e)}") # Check if the synapse.axon object exists if synapse.axon is not None: @@ -1319,7 +1288,7 @@ async def blacklist(self, synapse: "Synapse"): ) if blacklisted: # We log that the key or identifier is blacklisted. - bittensor.logging.trace(f"Blacklisted: {blacklisted}, {reason}") + logging.trace(f"Blacklisted: {blacklisted}, {reason}") # Check if the synapse.axon object exists if synapse.axon is not None: @@ -1368,7 +1337,7 @@ async def submit_task( """ loop = asyncio.get_event_loop() future = loop.run_in_executor(executor, lambda: priority) - result = await future + await future return priority, result # If a priority function exists for the request's name @@ -1388,7 +1357,7 @@ async def submit_task( except TimeoutError as e: # If the execution of the priority function exceeds the timeout, # it raises an exception to handle the timeout error. - bittensor.logging.trace(f"TimeoutError: {str(e)}") + logging.trace(f"TimeoutError: {str(e)}") # Set the status code of the synapse to 408 which indicates a timeout error. if synapse.axon is not None: @@ -1420,6 +1389,8 @@ async def run( This method is a critical part of the request lifecycle, where the actual processing of the request takes place, leading to the generation of a response. """ + assert isinstance(synapse, Synapse) + try: # The requested function is executed by calling the 'call_next' function, # passing the original request as an argument. This function processes the request @@ -1428,7 +1399,7 @@ async def run( except Exception as e: # Log the exception for debugging purposes. - bittensor.logging.trace(f"Run exception: {str(e)}") + logging.trace(f"Run exception: {str(e)}") raise # Return the starlet response @@ -1452,7 +1423,7 @@ async def synapse_to_response( properly formatted and contains all necessary information. """ if synapse.axon is None: - synapse.axon = bittensor.TerminalInfo() + synapse.axon = TerminalInfo() if synapse.axon.status_code is None: synapse.axon.status_code = 200 diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index 4293ef03c..314c896b8 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -19,7 +19,7 @@ import re import time from dataclasses import dataclass -from typing import Any +from typing import Any, Tuple from unittest import IsolatedAsyncioTestCase from unittest.mock import AsyncMock, MagicMock, patch @@ -28,44 +28,46 @@ from fastapi.testclient import TestClient from starlette.requests import Request -import bittensor -from bittensor import Synapse, RunException from bittensor.core.axon import Axon, AxonMiddleware +from bittensor.core.errors import RunException +from bittensor.core.settings import version_as_int +from bittensor.core.synapse import Synapse +from bittensor.core.threadpool import PriorityThreadPoolExecutor from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds, ALLOWED_DELTA, NANOSECONDS_IN_SECOND -def test_attach(): +def test_attach_initial(): # Create a mock AxonServer instance - server = bittensor.axon() + server = Axon() # Define the Synapse type - class Synapse(bittensor.Synapse): + class TestSynapse(Synapse): pass # Define the functions with the correct signatures - def forward_fn(synapse: Synapse) -> Any: + def forward_fn(synapse: TestSynapse) -> Any: pass - def blacklist_fn(synapse: Synapse) -> bool: - return True + def blacklist_fn(synapse: TestSynapse) -> Tuple[bool, str]: + return True, "" - def priority_fn(synapse: Synapse) -> float: + def priority_fn(synapse: TestSynapse) -> float: return 1.0 - def verify_fn(synapse: Synapse) -> None: + def verify_fn(synapse: TestSynapse) -> None: pass # Test attaching with correct signatures server.attach(forward_fn, blacklist_fn, priority_fn, verify_fn) # Define functions with incorrect signatures - def wrong_blacklist_fn(synapse: Synapse) -> int: + def wrong_blacklist_fn(synapse: TestSynapse) -> int: return 1 - def wrong_priority_fn(synapse: Synapse) -> int: + def wrong_priority_fn(synapse: TestSynapse) -> int: return 1 - def wrong_verify_fn(synapse: Synapse) -> bool: + def wrong_verify_fn(synapse: TestSynapse) -> bool: return True # Test attaching with incorrect signatures @@ -81,14 +83,14 @@ def wrong_verify_fn(synapse: Synapse) -> bool: def test_attach(): # Create a mock AxonServer instance - server = bittensor.axon() + server = Axon() # Define the Synapse type - class Synapse: + class FakeSynapse: pass # Define a class that inherits from Synapse - class InheritedSynapse(bittensor.Synapse): + class InheritedSynapse(Synapse): pass # Define a function with the correct signature @@ -190,10 +192,10 @@ def __init__(self): self.priority_fns = {} self.forward_fns = {} self.verify_fns = {} - self.thread_pool = bittensor.PriorityThreadPoolExecutor(max_workers=1) + self.thread_pool = PriorityThreadPoolExecutor(max_workers=1) -class SynapseMock(bittensor.Synapse): +class SynapseMock(Synapse): pass @@ -256,6 +258,7 @@ async def test_blacklist_fail(middleware): @pytest.mark.asyncio +@pytest.mark.skip("middleware.priority runs infinitely") async def test_priority_pass(middleware): synapse = SynapseMock() middleware.axon.priority_fns = {"SynapseMock": priority_fn_pass} @@ -464,7 +467,7 @@ def setUp(self): self.mock_axon = MagicMock() self.mock_axon.uuid = "1234" self.mock_axon.forward_class_types = { - "request_name": bittensor.Synapse, + "request_name": Synapse, } self.mock_axon.wallet.hotkey.sign.return_value = bytes.fromhex("aabbccdd") # Create an instance of AxonMiddleware @@ -483,7 +486,7 @@ async def test_preprocess(self): synapse = await self.axon_middleware.preprocess(request) # Check if the preprocess function fills the axon information into the synapse - assert synapse.axon.version == str(bittensor.__version_as_int__) + assert synapse.axon.version == str(version_as_int) assert synapse.axon.uuid == "1234" assert synapse.axon.nonce is not None assert synapse.axon.status_message is None @@ -539,7 +542,7 @@ async def test_unknown_path(self, http_client): ) async def test_ping__no_dendrite(self, http_client): - response = http_client.post_synapse(bittensor.Synapse()) + response = http_client.post_synapse(Synapse()) assert (response.status_code, response.json()) == ( 401, { From 316f7af6f50baebed30b2178981d242a8ea03f56 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 1 Aug 2024 20:42:14 -0700 Subject: [PATCH 047/260] Make chain_data.py independent of bittensor import. Fix chain_data tests. --- bittensor/core/chain_data.py | 11 ++++++----- tests/unit_tests/test_chain_data.py | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bittensor/core/chain_data.py b/bittensor/core/chain_data.py index eacfb3902..0cc5a6e06 100644 --- a/bittensor/core/chain_data.py +++ b/bittensor/core/chain_data.py @@ -30,11 +30,11 @@ from scalecodec.types import GenericCall from scalecodec.utils.ss58 import ss58_encode -import bittensor from .settings import ss58_format from bittensor.utils import networking as net, RAOPERTAO, U16_NORMALIZED_FLOAT from bittensor.utils.balance import Balance from bittensor.utils.registration import torch, use_torch +from bittensor.utils.btlogging import logging custom_rpc_type_registry = { "types": { @@ -254,7 +254,7 @@ def to_string(self) -> str: try: return json.dumps(asdict(self)) except (TypeError, ValueError) as e: - bittensor.logging.error(f"Error converting AxonInfo to string: {e}") + logging.error(f"Error converting AxonInfo to string: {e}") return AxonInfo(0, "", 0, 0, "", "").to_string() @classmethod @@ -277,11 +277,11 @@ def from_string(cls, json_string: str) -> "AxonInfo": data = json.loads(json_string) return cls(**data) except json.JSONDecodeError as e: - bittensor.logging.error(f"Error decoding JSON: {e}") + logging.error(f"Error decoding JSON: {e}") except TypeError as e: - bittensor.logging.error(f"Type error: {e}") + logging.error(f"Type error: {e}") except ValueError as e: - bittensor.logging.error(f"Value error: {e}") + logging.error(f"Value error: {e}") return AxonInfo(0, "", 0, 0, "", "") @classmethod @@ -334,6 +334,7 @@ class ChainDataType(Enum): IPInfo = 7 SubnetHyperparameters = 8 ScheduledColdkeySwapInfo = 9 + AccountId = 10 def from_scale_encoding( diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index d9e21735a..be567e542 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -17,6 +17,7 @@ import pytest import torch + from bittensor.core.chain_data import AxonInfo, ChainDataType, DelegateInfo, NeuronInfo RAOPERTAO = 10**18 @@ -532,7 +533,7 @@ def mock_from_scale_encoding(mocker): @pytest.fixture def mock_fix_decoded_values(mocker): return mocker.patch( - "bittensor.DelegateInfo.fix_decoded_values", side_effect=lambda x: x + "bittensor.core.chain_data.DelegateInfo.fix_decoded_values", side_effect=lambda x: x ) From 23ba67b9800aab1a13246d63e68db03d32e23344 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 2 Aug 2024 08:46:50 -0700 Subject: [PATCH 048/260] Make dendrite.py independent of bittensor import. Fix dendrite tests. --- bittensor/core/dendrite.py | 153 ++++++++++++++---------------- tests/unit_tests/test_dendrite.py | 45 ++++----- 2 files changed, 96 insertions(+), 102 deletions(-) diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 58cb66f3d..eb552e8fa 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -23,11 +23,17 @@ from typing import Any, AsyncGenerator, Dict, List, Optional, Union, Type import aiohttp - -import bittensor +from bittensor_wallet import Wallet +from substrateinterface import Keypair + +from bittensor.core.axon import Axon +from bittensor.core.chain_data import AxonInfo +from bittensor.core.settings import version_as_int +from bittensor.core.stream import StreamingSynapse +from bittensor.core.synapse import Synapse, TerminalInfo +from bittensor.utils import networking +from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch -from .stream import StreamingSynapse -from .synapse import Synapse DENDRITE_ERROR_MAPPING: Dict[Type[Exception], tuple] = { aiohttp.ClientConnectorError: ("503", "Service unavailable"), @@ -48,7 +54,7 @@ class DendriteMixin: In the brain analogy, dendrites receive signals from other neurons (in this case, network servers or axons), and the Dendrite class here is designed - to send requests to those endpoint to recieve inputs. + to send requests to those endpoint to receive inputs. This class includes a wallet or keypair used for signing messages, and methods for making HTTP requests to the network servers. It also provides functionalities such as logging @@ -62,19 +68,19 @@ class DendriteMixin: Methods: __str__(): Returns a string representation of the Dendrite object. __repr__(): Returns a string representation of the Dendrite object, acting as a fallback for __str__(). - query(self, *args, **kwargs) -> Union[bittensor.Synapse, List[bittensor.Synapse]]: + query(self, *args, **kwargs) -> Union[Synapse, List[Synapse]]: Makes synchronous requests to one or multiple target Axons and returns responses. - forward(self, axons, synapse=bittensor.Synapse(), timeout=12, deserialize=True, run_async=True, streaming=False) -> bittensor.Synapse: + forward(self, axons, synapse=Synapse(), timeout=12, deserialize=True, run_async=True, streaming=False) -> Synapse: Asynchronously sends requests to one or multiple Axons and collates their responses. - call(self, target_axon, synapse=bittensor.Synapse(), timeout=12.0, deserialize=True) -> bittensor.Synapse: + call(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> Synapse: Asynchronously sends a request to a specified Axon and processes the response. - call_stream(self, target_axon, synapse=bittensor.Synapse(), timeout=12.0, deserialize=True) -> AsyncGenerator[bittensor.Synapse, None]: + call_stream(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> AsyncGenerator[Synapse, None]: Sends a request to a specified Axon and yields an AsyncGenerator that contains streaming response chunks before finally yielding the filled Synapse as the final element. - preprocess_synapse_for_request(self, target_axon_info, synapse, timeout=12.0) -> bittensor.Synapse: + preprocess_synapse_for_request(self, target_axon_info, synapse, timeout=12.0) -> Synapse: Preprocesses the synapse for making a request, including building headers and signing. process_server_response(self, server_response, json_response, local_synapse): @@ -91,31 +97,31 @@ class DendriteMixin: Example with a context manager:: - aysnc with dendrite(wallet = bittensor.wallet()) as d: + async with dendrite(wallet = bittensor_wallet.Wallet()) as d: print(d) d( ) # ping axon d( [] ) # ping multiple - d( bittensor.axon(), bittensor.Synapse ) + d( Axon(), Synapse ) However, you are able to safely call :func:`dendrite.query()` without a context manager in a synchronous setting. Example without a context manager:: - d = dendrite(wallet = bittensor.wallet() ) + d = dendrite(wallet = bittensor_wallet.Wallet() ) print(d) d( ) # ping axon d( [] ) # ping multiple - d( bittensor.axon(), bittensor.Synapse ) + d( bittensor.core.axon.Axon(), bittensor.core.synapse.Synapse ) """ def __init__( - self, wallet: Optional[Union[bittensor.wallet, bittensor.Keypair]] = None + self, wallet: Optional[Union["Wallet", Keypair]] = None ): """ Initializes the Dendrite object, setting up essential properties. Args: - wallet (Optional[Union['bittensor.wallet', 'bittensor.keypair']], optional): + wallet (Optional[Union['bittensor_wallet.Wallet', 'bittensor.keypair']], optional): The user's wallet or keypair used for signing messages. Defaults to ``None``, in which case a new :func:`bittensor.wallet().hotkey` is generated and used. """ # Initialize the parent class @@ -125,12 +131,12 @@ def __init__( self.uuid = str(uuid.uuid1()) # Get the external IP - self.external_ip = bittensor.utils.networking.get_external_ip() + self.external_ip = networking.get_external_ip() # If a wallet or keypair is provided, use its hotkey. If not, generate a new one. self.keypair = ( - wallet.hotkey if isinstance(wallet, bittensor.wallet) else wallet - ) or bittensor.wallet().hotkey + wallet.hotkey if isinstance(wallet, Wallet) else wallet + ) or Wallet().hotkey self.synapse_history: list = [] @@ -253,14 +259,14 @@ def log_exception(self, exception: Exception): """ error_id = str(uuid.uuid4()) error_type = exception.__class__.__name__ - bittensor.logging.error(f"{error_type}#{error_id}: {exception}") + logging.error(f"{error_type}#{error_id}: {exception}") def process_error_message( self, - synapse: Union[bittensor.Synapse, bittensor.StreamingSynapse], + synapse: Union["Synapse", "StreamingSynapse"], request_name: str, exception: Exception, - ) -> Union[bittensor.Synapse, bittensor.StreamingSynapse]: + ) -> Union["Synapse", "StreamingSynapse"]: """ Handles exceptions that occur during network requests, updating the synapse with appropriate status codes and messages. @@ -273,7 +279,7 @@ def process_error_message( exception: The exception object caught during the request. Returns: - bittensor.Synapse: The updated synapse object with the error status code and message. + Synapse: The updated synapse object with the error status code and message. Note: This method updates the synapse object in-place. @@ -315,7 +321,7 @@ def _log_outgoing_request(self, synapse): Args: synapse: The synapse object representing the request being sent. """ - bittensor.logging.trace( + logging.trace( f"dendrite | --> | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | 0 | Success" ) @@ -330,15 +336,11 @@ def _log_incoming_response(self, synapse): Args: synapse: The synapse object representing the received response. """ - bittensor.logging.trace( + logging.trace( f"dendrite | <-- | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | {synapse.dendrite.status_code} | {synapse.dendrite.status_message}" ) - def query( - self, *args, **kwargs - ) -> List[ - Union[AsyncGenerator[Any, Any], bittensor.Synapse, bittensor.StreamingSynapse] - ]: + def query(self, *args, **kwargs) -> "List[Union[AsyncGenerator[Any, Any], Synapse, StreamingSynapse]]": """ Makes a synchronous request to multiple target Axons and returns the server responses. @@ -347,11 +349,11 @@ def query( Args: axons (Union[List[Union['bittensor.AxonInfo', 'bittensor.axon']], Union['bittensor.AxonInfo', 'bittensor.axon']]): The list of target Axon information. - synapse (bittensor.Synapse, optional): The Synapse object. Defaults to :func:`bittensor.Synapse()`. + synapse (Synapse, optional): The Synapse object. Defaults to :func:`Synapse()`. timeout (float, optional): The request timeout duration in seconds. Defaults to ``12.0`` seconds. Returns: - Union[bittensor.Synapse, List[bittensor.Synapse]]: If a single target axon is provided, returns the response from that axon. If multiple target axons are provided, returns a list of responses from all target axons. + Union[Synapse, List[Synapse]]: If a single target axon is provided, returns the response from that axon. If multiple target axons are provided, returns a list of responses from all target axons. """ result = None try: @@ -360,7 +362,7 @@ def query( except Exception: new_loop = asyncio.new_event_loop() asyncio.set_event_loop(new_loop) - result = loop.run_until_complete(self.forward(*args, **kwargs)) + result = new_loop.run_until_complete(self.forward(*args, **kwargs)) new_loop.close() finally: self.close_session() @@ -368,18 +370,13 @@ def query( async def forward( self, - axons: Union[ - List[Union[bittensor.AxonInfo, bittensor.axon]], - Union[bittensor.AxonInfo, bittensor.axon], - ], + axons: "Union[List[Union[AxonInfo, Axon]], Union[AxonInfo, Axon]]", synapse: "Synapse" = Synapse(), timeout: float = 12, deserialize: bool = True, run_async: bool = True, streaming: bool = False, - ) -> List[ - Union[AsyncGenerator[Any, Any], bittensor.Synapse, bittensor.StreamingSynapse] - ]: + ) -> "List[Union[AsyncGenerator[Any, Any], Synapse, StreamingSynapse]]": """ Asynchronously sends requests to one or multiple Axons and collates their responses. @@ -396,7 +393,7 @@ async def forward( ... wallet = bittensor.wallet() # Initialize a wallet - synapse = bittensor.Synapse(...) # Create a synapse object that contains query data + synapse = Synapse(...) # Create a synapse object that contains query data dendrte = bittensor.dendrite(wallet = wallet) # Initialize a dendrite instance axons = metagraph.axons # Create a list of axons to query responses = await dendrite(axons, synapse) # Send the query to all axons and await the responses @@ -416,14 +413,14 @@ async def forward( Args: axons (Union[List[Union['bittensor.AxonInfo', 'bittensor.axon']], Union['bittensor.AxonInfo', 'bittensor.axon']]): The target Axons to send requests to. Can be a single Axon or a list of Axons. - synapse (bittensor.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`bittensor.Synapse` instance. + synapse (Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. timeout (float, optional): Maximum duration to wait for a response from an Axon in seconds. Defaults to ``12.0``. deserialize (bool, optional): Determines if the received response should be deserialized. Defaults to ``True``. run_async (bool, optional): If ``True``, sends requests concurrently. Otherwise, sends requests sequentially. Defaults to ``True``. streaming (bool, optional): Indicates if the response is expected to be in streaming format. Defaults to ``False``. Returns: - Union[AsyncGenerator, bittensor.Synapse, List[bittensor.Synapse]]: If a single Axon is targeted, returns its response. + Union[AsyncGenerator, Synapse, List[Synapse]]: If a single Axon is targeted, returns its response. If multiple Axons are targeted, returns a list of their responses. """ is_list = True @@ -434,10 +431,10 @@ async def forward( # Check if synapse is an instance of the StreamingSynapse class or if streaming flag is set. is_streaming_subclass = issubclass( - synapse.__class__, bittensor.StreamingSynapse + synapse.__class__, StreamingSynapse ) if streaming != is_streaming_subclass: - bittensor.logging.warning( + logging.warning( f"Argument streaming is {streaming} while issubclass(synapse, StreamingSynapse) is {synapse.__class__.__name__}. This may cause unexpected behavior." ) streaming = is_streaming_subclass or streaming @@ -445,7 +442,7 @@ async def forward( async def query_all_axons( is_stream: bool, ) -> Union[ - AsyncGenerator[Any, Any], bittensor.Synapse, bittensor.StreamingSynapse + AsyncGenerator[Any, Any], Synapse, StreamingSynapse ]: """ Handles the processing of requests to all targeted axons, accommodating both streaming and non-streaming responses. @@ -459,16 +456,14 @@ async def query_all_axons( If ``True``, responses are handled in streaming mode. Returns: - List[Union[AsyncGenerator, bittensor.Synapse, bittensor.StreamingSynapse]]: A list + List[Union[AsyncGenerator, Synapse, bittensor.StreamingSynapse]]: A list containing the responses from each axon. The type of each response depends on the streaming mode and the type of synapse used. """ async def single_axon_response( target_axon, - ) -> Union[ - AsyncGenerator[Any, Any], bittensor.Synapse, bittensor.StreamingSynapse - ]: + ) -> "Union[AsyncGenerator[Any, Any], Synapse, StreamingSynapse]": """ Manages the request and response process for a single axon, supporting both streaming and non-streaming modes. @@ -481,7 +476,7 @@ async def single_axon_response( target_axon: The target axon object to which the request is to be sent. This object contains the necessary information like IP address and port to formulate the request. Returns: - Union[AsyncGenerator, bittensor.Synapse, bittensor.StreamingSynapse]: The response + Union[AsyncGenerator, Synapse, bittensor.StreamingSynapse]: The response from the targeted axon. In streaming mode, an AsyncGenerator is returned, yielding data chunks. In non-streaming mode, a Synapse or StreamingSynapse object is returned containing the response. @@ -520,11 +515,11 @@ async def single_axon_response( async def call( self, - target_axon: Union[bittensor.AxonInfo, bittensor.axon], + target_axon: Union["AxonInfo", "Axon"], synapse: "Synapse" = Synapse(), timeout: float = 12.0, deserialize: bool = True, - ) -> bittensor.Synapse: + ) -> "Synapse": """ Asynchronously sends a request to a specified Axon and processes the response. @@ -534,19 +529,19 @@ async def call( Args: target_axon (Union['bittensor.AxonInfo', 'bittensor.axon']): The target Axon to send the request to. - synapse (bittensor.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`bittensor.Synapse` instance. + synapse (Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. timeout (float, optional): Maximum duration to wait for a response from the Axon in seconds. Defaults to ``12.0``. deserialize (bool, optional): Determines if the received response should be deserialized. Defaults to ``True``. Returns: - bittensor.Synapse: The Synapse object, updated with the response data from the Axon. + Synapse: The Synapse object, updated with the response data from the Axon. """ # Record start time start_time = time.time() target_axon = ( target_axon.info() - if isinstance(target_axon, bittensor.axon) + if isinstance(target_axon, Axon) else target_axon ) @@ -584,7 +579,7 @@ async def call( # Log synapse event history self.synapse_history.append( - bittensor.Synapse.from_headers(synapse.to_headers()) + Synapse.from_headers(synapse.to_headers()) ) # Return the updated synapse object after deserializing if requested @@ -592,11 +587,11 @@ async def call( async def call_stream( self, - target_axon: Union[bittensor.AxonInfo, bittensor.axon], + target_axon: "Union[AxonInfo, Axon]", synapse: "StreamingSynapse" = Synapse(), # type: ignore timeout: float = 12.0, deserialize: bool = True, - ) -> AsyncGenerator[Any, Any]: + ) -> "AsyncGenerator[Any, Any]": """ Sends a request to a specified Axon and yields streaming responses. @@ -607,20 +602,20 @@ async def call_stream( Args: target_axon (Union['bittensor.AxonInfo', 'bittensor.axon']): The target Axon to send the request to. - synapse (bittensor.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`bittensor.Synapse` instance. + synapse (Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. timeout (float, optional): Maximum duration to wait for a response (or a chunk of the response) from the Axon in seconds. Defaults to ``12.0``. deserialize (bool, optional): Determines if each received chunk should be deserialized. Defaults to ``True``. Yields: object: Each yielded object contains a chunk of the arbitrary response data from the Axon. - bittensor.Synapse: After the AsyncGenerator has been exhausted, yields the final filled Synapse. + Synapse: After the AsyncGenerator has been exhausted, yields the final filled Synapse. """ # Record start time start_time = time.time() target_axon = ( target_axon.info() - if isinstance(target_axon, bittensor.axon) + if isinstance(target_axon, Axon) else target_axon ) @@ -666,7 +661,7 @@ async def call_stream( # Log synapse event history self.synapse_history.append( - bittensor.Synapse.from_headers(synapse.to_headers()) + Synapse.from_headers(synapse.to_headers()) ) # Return the updated synapse object after deserializing if requested @@ -677,35 +672,35 @@ async def call_stream( def preprocess_synapse_for_request( self, - target_axon_info: bittensor.AxonInfo, - synapse: bittensor.Synapse, + target_axon_info: "AxonInfo", + synapse: "Synapse", timeout: float = 12.0, - ) -> bittensor.Synapse: + ) -> "Synapse": """ Preprocesses the synapse for making a request. This includes building headers for Dendrite and Axon and signing the request. Args: target_axon_info (bittensor.AxonInfo): The target axon information. - synapse (bittensor.Synapse): The synapse object to be preprocessed. + synapse (Synapse): The synapse object to be preprocessed. timeout (float, optional): The request timeout duration in seconds. Defaults to ``12.0`` seconds. Returns: - bittensor.Synapse: The preprocessed synapse. + Synapse: The preprocessed synapse. """ # Set the timeout for the synapse synapse.timeout = timeout - synapse.dendrite = bittensor.TerminalInfo( + synapse.dendrite = TerminalInfo( ip=self.external_ip, - version=bittensor.__version_as_int__, + version=version_as_int, nonce=time.time_ns(), uuid=self.uuid, hotkey=self.keypair.ss58_address, ) # Build the Axon headers using the target axon's details - synapse.axon = bittensor.TerminalInfo( + synapse.axon = TerminalInfo( ip=target_axon_info.ip, port=target_axon_info.port, hotkey=target_axon_info.hotkey, @@ -721,7 +716,7 @@ def process_server_response( self, server_response: aiohttp.ClientResponse, json_response: dict, - local_synapse: bittensor.Synapse, + local_synapse: Synapse, ): """ Processes the server response, updates the local synapse state with the @@ -730,7 +725,7 @@ def process_server_response( Args: server_response (object): The `aiohttp `_ response object from the server. json_response (dict): The parsed JSON response from the server. - local_synapse (bittensor.Synapse): The local synapse object to be updated. + local_synapse (Synapse): The local synapse object to be updated. Raises: None: But errors in attribute setting are silently ignored. @@ -752,12 +747,12 @@ def process_server_response( else: # If the server responded with an error, update the local synapse state if local_synapse.axon is None: - local_synapse.axon = bittensor.TerminalInfo() + local_synapse.axon = TerminalInfo() local_synapse.axon.status_code = server_response.status local_synapse.axon.status_message = json_response.get("message") # Extract server headers and overwrite None values in local synapse headers - server_headers = bittensor.Synapse.from_headers(server_response.headers) # type: ignore + server_headers = Synapse.from_headers(server_response.headers) # type: ignore # Merge dendrite headers local_synapse.dendrite.__dict__.update( @@ -859,10 +854,8 @@ def __del__(self): BaseModel: Union["torch.nn.Module", object] = torch.nn.Module if use_torch() else object -class dendrite(DendriteMixin, BaseModel): # type: ignore - def __init__( - self, wallet: Optional[Union[bittensor.wallet, bittensor.Keypair]] = None - ): +class Dendrite(DendriteMixin, BaseModel): # type: ignore + def __init__(self, wallet: Optional[Union[Wallet, Keypair]] = None): if use_torch(): torch.nn.Module.__init__(self) DendriteMixin.__init__(self, wallet) @@ -873,4 +866,4 @@ def __init__( async def call(self, *args, **kwargs): return await self.forward(*args, **kwargs) - dendrite.__call__ = call + Dendrite.__call__ = call diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index 4296242fa..4aeb08664 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -24,13 +24,15 @@ import aiohttp import pytest -import bittensor -from bittensor.core.dendrite import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR, dendrite as Dendrite +from bittensor.core.axon import Axon +from bittensor.core.dendrite import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR, Dendrite from bittensor.core.synapse import TerminalInfo from tests.helpers import _get_mock_wallet +from bittensor.core.synapse import Synapse +from bittensor.core.chain_data import AxonInfo -class SynapseDummy(bittensor.Synapse): +class SynapseDummy(Synapse): input: int output: typing.Optional[int] = None @@ -42,10 +44,9 @@ def dummy(synapse: SynapseDummy) -> SynapseDummy: @pytest.fixture def setup_dendrite(): - user_wallet = ( - _get_mock_wallet() - ) # assuming bittensor.wallet() returns a wallet object - dendrite_obj = bittensor.dendrite(user_wallet) + # Assuming bittensor.wallet() returns a wallet object + user_wallet = _get_mock_wallet() + dendrite_obj = Dendrite(user_wallet) return dendrite_obj @@ -56,7 +57,7 @@ def dendrite_obj(setup_dendrite): @pytest.fixture def axon_info(): - return bittensor.AxonInfo( + return AxonInfo( version=1, ip="127.0.0.1", port=666, @@ -68,7 +69,7 @@ def axon_info(): @pytest.fixture(scope="session") def setup_axon(): - axon = bittensor.axon() + axon = Axon() axon.attach(forward_fn=dummy) axon.start() yield axon @@ -77,7 +78,7 @@ def setup_axon(): def test_init(setup_dendrite): dendrite_obj = setup_dendrite - assert isinstance(dendrite_obj, bittensor.dendrite) + assert isinstance(dendrite_obj, Dendrite) assert dendrite_obj.keypair == setup_dendrite.keypair @@ -123,16 +124,16 @@ def __await__(self): def test_dendrite_create_wallet(): - d = bittensor.dendrite(_get_mock_wallet()) - d = bittensor.dendrite(_get_mock_wallet().hotkey) - d = bittensor.dendrite(_get_mock_wallet().coldkeypub) + d = Dendrite(_get_mock_wallet()) + d = Dendrite(_get_mock_wallet().hotkey) + d = Dendrite(_get_mock_wallet().coldkeypub) assert d.__str__() == d.__repr__() @pytest.mark.asyncio async def test_forward_many(): n = 10 - d = bittensor.dendrite(wallet=_get_mock_wallet()) + d = Dendrite(wallet=_get_mock_wallet()) d.call = AsyncMock() axons = [MagicMock() for _ in range(n)] @@ -148,10 +149,10 @@ async def test_forward_many(): def test_pre_process_synapse(): - d = bittensor.dendrite(wallet=_get_mock_wallet()) - s = bittensor.Synapse() + d = Dendrite(wallet=_get_mock_wallet()) + s = Synapse() synapse = d.preprocess_synapse_for_request( - target_axon_info=bittensor.axon(wallet=_get_mock_wallet()).info(), + target_axon_info=Axon(wallet=_get_mock_wallet()).info(), synapse=s, timeout=12, ) @@ -292,7 +293,7 @@ def test_terminal_info_error_cases( @pytest.mark.asyncio async def test_dendrite__call__success_response( - axon_info, dendrite_obj, mock_aioresponse + axon_info, dendrite_obj, mock_aio_response ): input_synapse = SynapseDummy(input=1) expected_synapse = SynapseDummy( @@ -308,7 +309,7 @@ async def test_dendrite__call__success_response( ) ) ) - mock_aioresponse.post( + mock_aio_response.post( f"http://127.0.0.1:666/SynapseDummy", body=expected_synapse.json(), ) @@ -323,13 +324,13 @@ async def test_dendrite__call__success_response( @pytest.mark.asyncio async def test_dendrite__call__handles_http_error_response( - axon_info, dendrite_obj, mock_aioresponse + axon_info, dendrite_obj, mock_aio_response ): status_code = 414 message = "Custom Error" - mock_aioresponse.post( - f"http://127.0.0.1:666/SynapseDummy", + mock_aio_response.post( + "http://127.0.0.1:666/SynapseDummy", status=status_code, payload={"message": message}, ) From 137b929cae04dc65711282302cfc410abdd74cd0 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 2 Aug 2024 09:19:35 -0700 Subject: [PATCH 049/260] Make staking.py independent of bittensor import. Fix staking tests. --- bittensor/api/extrinsics/staking.py | 176 +++++++++----------- tests/unit_tests/extrinsics/test_staking.py | 17 +- 2 files changed, 88 insertions(+), 105 deletions(-) diff --git a/bittensor/api/extrinsics/staking.py b/bittensor/api/extrinsics/staking.py index fe5571e91..c647efce3 100644 --- a/bittensor/api/extrinsics/staking.py +++ b/bittensor/api/extrinsics/staking.py @@ -15,17 +15,16 @@ # 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 bittensor from rich.prompt import Confirm from time import sleep from typing import List, Union, Optional, Tuple +from bittensor.core.errors import NotDelegateError, StakeError, NotRegisteredError +from bittensor.core.settings import __console__ as bt_console from bittensor.utils.balance import Balance -from bittensor.core.errors import NotDelegateError +from bittensor_wallet import Wallet -def _check_threshold_amount( - subtensor: "bittensor.subtensor", stake_balance: Balance -) -> Tuple[bool, Balance]: +def _check_threshold_amount(subtensor, stake_balance: "Balance") -> Tuple[bool, "Balance"]: """ Checks if the new stake balance will be above the minimum required stake threshold. @@ -39,7 +38,7 @@ def _check_threshold_amount( staking balance is below the threshold. The threshold balance required to stake. """ - min_req_stake: Balance = subtensor.get_minimum_required_stake() + min_req_stake: "Balance" = subtensor.get_minimum_required_stake() if min_req_stake > stake_balance: return False, min_req_stake @@ -48,38 +47,30 @@ def _check_threshold_amount( def add_stake_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", hotkey_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, + amount: Optional[Union["Balance", float]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, ) -> bool: - r"""Adds the specified amount of stake to passed hotkey ``uid``. + """Adds the specified amount of stake to passed hotkey ``uid``. Args: - wallet (bittensor.wallet): - Bittensor wallet object. - hotkey_ss58 (Optional[str]): - The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. - amount (Union[Balance, float]): - Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + wallet (Wallet): Bittensor wallet object. + hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. + amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. Raises: - bittensor.errors.NotRegisteredError: - If the wallet is not registered on the chain. - bittensor.errors.NotDelegateError: - If the hotkey is not a delegate on the chain. + bittensor.core.errors.NotRegisteredError: If the wallet is not registered on the chain. + bittensor.core.errors.NotDelegateError: If the hotkey is not a delegate on the chain. """ # Decrypt keys, wallet.coldkey @@ -91,7 +82,7 @@ def add_stake_extrinsic( # Flag to indicate if we are using the wallet's own hotkey. own_hotkey: bool - with bittensor.__console__.status( + with bt_console.status( ":satellite: Syncing with chain: [white]{}[/white] ...".format( subtensor.network ) @@ -101,7 +92,7 @@ def add_stake_extrinsic( hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner if not own_hotkey: - # This is not the wallet's own hotkey so we are delegating. + # This is not the wallet's own hotkey, so we are delegating. if not subtensor.is_hotkey_delegate(hotkey_ss58): raise NotDelegateError( "Hotkey: {} is not a delegate.".format(hotkey_ss58) @@ -118,12 +109,12 @@ def add_stake_extrinsic( # Grab the existential deposit. existential_deposit = subtensor.get_existential_deposit() - # Convert to bittensor.Balance + # Convert to Balance if amount is None: # Stake it all. - staking_balance = bittensor.Balance.from_tao(old_balance.tao) - elif not isinstance(amount, bittensor.Balance): - staking_balance = bittensor.Balance.from_tao(amount) + staking_balance = Balance.from_tao(old_balance.tao) + elif not isinstance(amount, Balance): + staking_balance = Balance.from_tao(amount) else: staking_balance = amount @@ -136,7 +127,7 @@ def add_stake_extrinsic( # Check enough to stake. if staking_balance > old_balance: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Not enough stake[/red]:[bold white]\n balance:{}\n amount: {}\n coldkey: {}[/bold white]".format( old_balance, staking_balance, wallet.name ) @@ -150,7 +141,7 @@ def add_stake_extrinsic( subtensor, new_stake_balance ) if not is_above_threshold: - bittensor.__console__.print( + bt_console.print( f":cross_mark: [red]New stake balance of {new_stake_balance} is below the minimum required nomination stake threshold {threshold}.[/red]" ) return False @@ -174,7 +165,7 @@ def add_stake_extrinsic( return False try: - with bittensor.__console__.status( + with bt_console.status( ":satellite: Staking to: [bold white]{}[/bold white] ...".format( subtensor.network ) @@ -193,10 +184,10 @@ def add_stake_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - with bittensor.__console__.status( + with bt_console.status( ":satellite: Checking Balance on: [white]{}[/white] ...".format( subtensor.network ) @@ -211,40 +202,40 @@ def add_stake_extrinsic( block=block, ) # Get current stake - bittensor.__console__.print( + bt_console.print( "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_balance, new_balance ) ) - bittensor.__console__.print( + bt_console.print( "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_stake, new_stake ) ) return True else: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: Error unknown." ) return False - except bittensor.core.errors.NotRegisteredError as e: - bittensor.__console__.print( + except NotRegisteredError: + bt_console.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( wallet.hotkey_str ) ) return False - except bittensor.core.errors.StakeError as e: - bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) + except StakeError as e: + bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) return False def add_stake_multiple_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", hotkey_ss58s: List[str], - amounts: Optional[List[Union[Balance, float]]] = None, + amounts: Optional[List[Union["Balance", float]]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, @@ -252,7 +243,7 @@ def add_stake_multiple_extrinsic( r"""Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. Args: - wallet (bittensor.wallet): + wallet (bittensor_wallet.Wallet): Bittensor wallet object for the coldkey. hotkey_ss58s (List[str]): List of hotkeys to stake to. @@ -283,7 +274,7 @@ def add_stake_multiple_extrinsic( isinstance(amount, (Balance, float)) for amount in amounts ): raise TypeError( - "amounts must be a [list of bittensor.Balance or float] or None" + "amounts must be a [list of Balance or float] or None" ) if amounts is None: @@ -291,7 +282,7 @@ def add_stake_multiple_extrinsic( else: # Convert to Balance amounts = [ - bittensor.Balance.from_tao(amount) if isinstance(amount, float) else amount + Balance.from_tao(amount) if isinstance(amount, float) else amount for amount in amounts ] @@ -303,7 +294,7 @@ def add_stake_multiple_extrinsic( wallet.coldkey old_stakes = [] - with bittensor.__console__.status( + with bt_console.status( ":satellite: Syncing with chain: [white]{}[/white] ...".format( subtensor.network ) @@ -319,21 +310,21 @@ def add_stake_multiple_extrinsic( ) # Remove existential balance to keep key alive. - ## Keys must maintain a balance of at least 1000 rao to stay alive. + # Keys must maintain a balance of at least 1000 rao to stay alive. total_staking_rao = sum( [amount.rao if amount is not None else 0 for amount in amounts] ) if total_staking_rao == 0: # Staking all to the first wallet. if old_balance.rao > 1000: - old_balance -= bittensor.Balance.from_rao(1000) + old_balance -= Balance.from_rao(1000) elif total_staking_rao < 1000: # Staking less than 1000 rao to the wallets. pass else: # Staking more than 1000 rao to the wallets. - ## Reduce the amount to stake to each wallet to keep the balance above 1000 rao. + # Reduce the amount to stake to each wallet to keep the balance above 1000 rao. percent_reduction = 1 - (1000 / total_staking_rao) amounts = [ Balance.from_tao(amount.tao * percent_reduction) for amount in amounts @@ -344,19 +335,19 @@ def add_stake_multiple_extrinsic( zip(hotkey_ss58s, amounts, old_stakes) ): staking_all = False - # Convert to bittensor.Balance - if amount == None: + # Convert to Balance + if amount is None: # Stake it all. - staking_balance = bittensor.Balance.from_tao(old_balance.tao) + staking_balance = Balance.from_tao(old_balance.tao) staking_all = True else: # Amounts are cast to balance earlier in the function - assert isinstance(amount, bittensor.Balance) + assert isinstance(amount, Balance) staking_balance = amount # Check enough to stake if staking_balance > old_balance: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Not enough balance[/red]: [green]{}[/green] to stake: [blue]{}[/blue] from coldkey: [white]{}[/white]".format( old_balance, staking_balance, wallet.name ) @@ -382,14 +373,14 @@ def add_stake_multiple_extrinsic( wait_for_finalization=wait_for_finalization, ) - if staking_response == True: # If we successfully staked. + if staking_response is True: # If we successfully staked. # We only wait here if we expect finalization. if idx < len(hotkey_ss58s) - 1: # Wait for tx rate limit. tx_rate_limit_blocks = subtensor.tx_rate_limit() if tx_rate_limit_blocks > 0: - bittensor.__console__.print( + bt_console.print( ":hourglass: [yellow]Waiting for tx rate limit: [white]{}[/white] blocks[/yellow]".format( tx_rate_limit_blocks ) @@ -405,7 +396,7 @@ def add_stake_multiple_extrinsic( continue - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) @@ -418,7 +409,7 @@ def add_stake_multiple_extrinsic( new_balance = subtensor.get_balance( wallet.coldkeypub.ss58_address, block=block ) - bittensor.__console__.print( + bt_console.print( "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( hotkey_ss58, old_stake, new_stake ) @@ -430,32 +421,32 @@ def add_stake_multiple_extrinsic( break else: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: Error unknown." ) continue - except bittensor.core.errors.NotRegisteredError as e: - bittensor.__console__.print( + except NotRegisteredError: + bt_console.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( hotkey_ss58 ) ) continue - except bittensor.core.errors.StakeError as e: - bittensor.__console__.print( + except StakeError as e: + bt_console.print( ":cross_mark: [red]Stake Error: {}[/red]".format(e) ) continue if successful_stakes != 0: - with bittensor.__console__.status( + with bt_console.status( ":satellite: Checking Balance on: ([white]{}[/white] ...".format( subtensor.network ) ): new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bittensor.__console__.print( + bt_console.print( "Balance: [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_balance, new_balance ) @@ -466,40 +457,31 @@ def add_stake_multiple_extrinsic( def __do_add_stake_single( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", hotkey_ss58: str, - amount: "bittensor.Balance", + amount: "Balance", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - r""" + """ Executes a stake call to the chain using the wallet and the amount specified. Args: - wallet (bittensor.wallet): - Bittensor wallet object. - hotkey_ss58 (str): - Hotkey to stake to. - amount (bittensor.Balance): - Amount to stake as Bittensor balance object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + hotkey_ss58 (str): Hotkey to stake to. + amount (Balance): Amount to stake as Bittensor balance object. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - Raises: - bittensor.errors.StakeError: - If the extrinsic fails to be finalized or included in the block. - bittensor.errors.NotDelegateError: - If the hotkey is not a delegate. - bittensor.errors.NotRegisteredError: - If the hotkey is not registered in any subnets. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + Raises: + bittensor.core.errors.StakeError: If the extrinsic fails to be finalized or included in the block. + bittensor.core.errors.NotDelegateError: If the hotkey is not a delegate. + bittensor.core.errors.NotRegisteredError: If the hotkey is not registered in any subnets. """ # Decrypt keys, wallet.coldkey diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py index 77c15e524..1b9da07e3 100644 --- a/tests/unit_tests/extrinsics/test_staking.py +++ b/tests/unit_tests/extrinsics/test_staking.py @@ -17,26 +17,27 @@ import pytest from unittest.mock import patch, MagicMock -import bittensor from bittensor.utils.balance import Balance from bittensor.api.extrinsics.staking import ( add_stake_extrinsic, add_stake_multiple_extrinsic, ) from bittensor.core.errors import NotDelegateError +from bittensor.core.subtensor import Subtensor +from bittensor_wallet import Wallet # Mocking external dependencies @pytest.fixture def mock_subtensor(): - mock = MagicMock(spec=bittensor.subtensor) + mock = MagicMock(spec=Subtensor) mock.network = "mock_network" return mock @pytest.fixture def mock_wallet(): - mock = MagicMock(spec=bittensor.wallet) + mock = MagicMock(spec=Wallet) mock.hotkey.ss58_address = "5FHneW46..." mock.coldkeypub.ss58_address = "5Gv8YYFu8..." mock.hotkey_str = "mock_hotkey_str" @@ -46,7 +47,7 @@ def mock_wallet(): @pytest.fixture def mock_other_owner_wallet(): - mock = MagicMock(spec=bittensor.wallet) + mock = MagicMock(spec=Wallet) mock.hotkey.ss58_address = "11HneC46..." mock.coldkeypub.ss58_address = "6Gv9ZZFu8..." mock.hotkey_str = "mock_hotkey_str_other_owner" @@ -127,7 +128,7 @@ def test_add_stake_extrinsic( else: staking_balance = ( Balance.from_tao(amount) - if not isinstance(amount, bittensor.Balance) + if not isinstance(amount, Balance) else amount ) @@ -152,11 +153,11 @@ def test_add_stake_extrinsic( ) as mock_confirm, patch.object( mock_subtensor, "get_minimum_required_stake", - return_value=bittensor.Balance.from_tao(0.01), + return_value=Balance.from_tao(0.01), ), patch.object( mock_subtensor, "get_existential_deposit", - return_value=bittensor.Balance.from_rao(100_000), + return_value=Balance.from_rao(100_000), ): mock_balance = mock_subtensor.get_balance() existential_deposit = mock_subtensor.get_existential_deposit() @@ -464,7 +465,7 @@ def test_add_stake_extrinsic( None, 0, TypeError, - "amounts must be a [list of bittensor.Balance or float] or None", + "amounts must be a [list of Balance or float] or None", ), ], ids=[ From 1b228edf3c21c6a51c73cb7f029c0aa94144ce1f Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 2 Aug 2024 10:18:24 -0700 Subject: [PATCH 050/260] Make subtensor.py independent of bittensor import. Fix subtensor tests. --- bittensor/core/subtensor.py | 501 ++++++++++++++--------------- tests/unit_tests/test_subtensor.py | 35 +- 2 files changed, 252 insertions(+), 284 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 95f7ef0d4..5af6a5977 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -23,11 +23,13 @@ import argparse import copy import socket +import sys import time from typing import List, Dict, Union, Optional, Tuple, TypedDict, Any import numpy as np import scalecodec +from bittensor_wallet import Wallet from numpy.typing import NDArray from retry import retry from scalecodec.base import RuntimeConfiguration @@ -37,24 +39,6 @@ from substrateinterface.base import QueryMapResult, SubstrateInterface, ExtrinsicReceipt from substrateinterface.exceptions import SubstrateRequestException -import bittensor -from bittensor.utils.btlogging import logging as _logger -from bittensor.utils import torch, weight_utils, format_error_message -from .chain_data import ( - DelegateInfoLite, - NeuronInfo, - DelegateInfo, - PrometheusInfo, - SubnetInfo, - SubnetHyperparameters, - StakeInfo, - NeuronInfoLite, - AxonInfo, - ProposalVoteData, - IPInfo, - custom_rpc_type_registry, -) -from .errors import IdentityError, NominationError, StakeError, TakeError from bittensor.api.extrinsics.commit_weights import ( commit_weights_extrinsic, reveal_weights_extrinsic, @@ -93,17 +77,38 @@ from bittensor.api.extrinsics.staking import add_stake_extrinsic, add_stake_multiple_extrinsic from bittensor.api.extrinsics.transfer import transfer_extrinsic from bittensor.api.extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic -from .types import AxonServeCallParams, PrometheusServeCallParams +from bittensor.core import settings +from bittensor.core.axon import Axon +from bittensor.core.chain_data import ( + DelegateInfoLite, + NeuronInfo, + DelegateInfo, + PrometheusInfo, + SubnetInfo, + SubnetHyperparameters, + StakeInfo, + NeuronInfoLite, + AxonInfo, + ProposalVoteData, + IPInfo, + custom_rpc_type_registry, +) +from bittensor.core.config import Config +from bittensor.core.metagraph import Metagraph from bittensor.utils import ( U16_NORMALIZED_FLOAT, ss58_to_vec_u8, U64_NORMALIZED_FLOAT, networking, ) +from bittensor.utils import torch, weight_utils, format_error_message +from bittensor.utils import wallet_utils from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging from bittensor.utils.registration import POWSolution from bittensor.utils.registration import legacy_torch_api_compat -from . import settings +from .errors import IdentityError, NominationError, StakeError, TakeError +from .types import AxonServeCallParams, PrometheusServeCallParams KEY_NONCE: Dict[str, int] = {} @@ -148,7 +153,7 @@ class Subtensor: finney_subtensor.connect_websocket() # Register a new neuron on the network. - wallet = bittensor.wallet(...) # Assuming a wallet instance is created. + wallet = bittensor_wallet.wallet(...) # Assuming a wallet instance is created. success = finney_subtensor.register(wallet=wallet, netuid=netuid) # Set inter-neuronal weights for collaborative learning. @@ -169,7 +174,7 @@ class Subtensor: def __init__( self, network: Optional[str] = None, - config: Optional[bittensor.Config] = None, + config: Optional["Config"] = None, _mock: bool = False, log_verbose: bool = True, ) -> None: @@ -185,15 +190,11 @@ def __init__( instructions on how to run a local subtensor node in the documentation in a subsequent release. Args: - network (str, optional): The network name to connect to (e.g., ``finney``, ``local``). This can also be the - chain endpoint (e.g., ``wss://entrypoint-finney.opentensor.ai:443``) and will be correctly parsed into - the network and chain endpoint. If not specified, defaults to the main Bittensor network. - config (bittensor.Config, optional): Configuration object for the subtensor. If not provided, a default - configuration is used. + network (str, optional): The network name to connect to (e.g., ``finney``, ``local``). This can also be the chain endpoint (e.g., ``wss://entrypoint-finney.opentensor.ai:443``) and will be correctly parsed into the network and chain endpoint. If not specified, defaults to the main Bittensor network. + config (bittensor.core.config.Config, optional): Configuration object for the subtensor. If not provided, a default configuration is used. _mock (bool, optional): If set to ``True``, uses a mocked connection for testing purposes. - This initialization sets up the connection to the specified Bittensor network, allowing for various - blockchain operations such as neuron registration, stake management, and setting weights. + This initialization sets up the connection to the specified Bittensor network, allowing for various blockchain operations such as neuron registration, stake management, and setting weights. """ # Determine config.subtensor.chain_endpoint and config.subtensor.network config. @@ -201,17 +202,6 @@ def __init__( # network. # Argument importance: network > chain_endpoint > config.subtensor.chain_endpoint > config.subtensor.network - # Check if network is a config object. (Single argument passed as first positional) - if isinstance(network, bittensor.Config): - if network.subtensor is None: - _logger.warning( - "If passing a bittensor config object, it must not be empty. Using default subtensor config." - ) - config = None - else: - config = network - network = None - if config is None: config = Subtensor.config() self.config = copy.deepcopy(config) # type: ignore @@ -223,14 +213,14 @@ def __init__( self.network == "finney" or self.chain_endpoint == settings.finney_entrypoint ) and log_verbose: - _logger.info( + logging.info( f"You are connecting to {self.network} network with endpoint {self.chain_endpoint}." ) - _logger.warning( + logging.warning( "We strongly encourage running a local subtensor node whenever possible. " "This increases decentralization and resilience of the network." ) - _logger.warning( + logging.warning( "In a future release, local subtensor will become the default endpoint. " "To get ahead of this change, please run a local subtensor node and point to it." ) @@ -245,29 +235,27 @@ def __init__( type_registry=settings.type_registry, ) except ConnectionRefusedError: - _logger.error( + logging.error( f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint. Exiting...", ) - _logger.info( + logging.info( "You can check if you have connectivity by running this command: nc -vz localhost " f"{self.chain_endpoint.split(':')[2]}" ) - exit(1) + sys.exit(1) # TODO (edu/phil): Advise to run local subtensor and point to dev docs. try: self.substrate.websocket.settimeout(600) - # except: - # bittensor.logging.warning("Could not set websocket timeout.") except AttributeError as e: - _logger.warning(f"AttributeError: {e}") + logging.warning(f"AttributeError: {e}") except TypeError as e: - _logger.warning(f"TypeError: {e}") + logging.warning(f"TypeError: {e}") except (socket.error, OSError) as e: - _logger.warning(f"Socket error: {e}") + logging.warning(f"Socket error: {e}") if log_verbose: - _logger.info( + logging.info( f"Connected to {self.network} network and {self.chain_endpoint}." ) @@ -285,17 +273,17 @@ def __repr__(self) -> str: return self.__str__() @staticmethod - def config() -> "bittensor.Config": + def config() -> "Config": """ Creates and returns a Bittensor configuration object. Returns: - config (bittensor.Config): A Bittensor configuration object configured with arguments added by the + config (bittensor.core.config.Config): A Bittensor configuration object configured with arguments added by the `subtensor.add_args` method. """ parser = argparse.ArgumentParser() Subtensor.add_args(parser) - return bittensor.Config(parser, args=[]) + return Config(parser, args=[]) @classmethod def help(cls): @@ -405,7 +393,7 @@ def determine_chain_endpoint_and_network(network: str): return "unknown", network @staticmethod - def setup_config(network: str, config: "bittensor.Config"): + def setup_config(network: str, config: "Config"): """ Sets up and returns the configuration for the Subtensor network and endpoint. @@ -420,7 +408,7 @@ def setup_config(network: str, config: "bittensor.Config"): Args: network (str): The name of the Subtensor network. If None, the network and endpoint will be determined from the `config` object. - config (bittensor.Config): The configuration object containing the network and chain endpoint settings. + config (bittensor.core.config.Config): The configuration object containing the network and chain endpoint settings. Returns: tuple: A tuple containing the formatted WebSocket endpoint URL and the evaluated network name. @@ -468,7 +456,7 @@ def setup_config(network: str, config: "bittensor.Config"): evaluated_network, evaluated_endpoint, ) = Subtensor.determine_chain_endpoint_and_network( - bittensor.defaults.subtensor.network + settings.defaults.subtensor.network ) return ( @@ -485,7 +473,7 @@ def close(self): ############## def nominate( self, - wallet: "bittensor.wallet", + wallet: "Wallet", wait_for_finalization: bool = False, wait_for_inclusion: bool = True, ) -> bool: @@ -495,9 +483,8 @@ def nominate( to participate in consensus and validation processes. Args: - wallet (bittensor.wallet): The wallet containing the hotkey to be nominated. - wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the - blockchain. + wallet (bittensor_wallet.Wallet): The wallet containing the hotkey to be nominated. + wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the blockchain. wait_for_inclusion (bool, optional): If ``True``, waits until the transaction is included in a block. Returns: @@ -515,9 +502,9 @@ def nominate( def delegate( self, - wallet: "bittensor.wallet", + wallet: "Wallet", delegate_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, + amount: Optional[Union["Balance", float]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, @@ -528,7 +515,7 @@ def delegate( to participate in consensus and validation processes. Args: - wallet (bittensor.wallet): The wallet containing the hotkey to be nominated. + wallet (bittensor_wallet.Wallet): The wallet containing the hotkey to be nominated. delegate_ss58 (Optional[str]): The ``SS58`` address of the delegate neuron. amount (Union[Balance, float]): The amount of TAO to undelegate. wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the @@ -554,7 +541,7 @@ def delegate( def undelegate( self, - wallet: "bittensor.wallet", + wallet: "Wallet", delegate_ss58: Optional[str] = None, amount: Optional[Union[Balance, float]] = None, wait_for_inclusion: bool = True, @@ -566,7 +553,7 @@ def undelegate( reduces the staked amount on another neuron, effectively withdrawing support or speculation. Args: - wallet (bittensor.wallet): The wallet used for the undelegation process. + wallet (bittensor_wallet.Wallet): The wallet used for the undelegation process. delegate_ss58 (Optional[str]): The ``SS58`` address of the delegate neuron. amount (Union[Balance, float]): The amount of TAO to undelegate. wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. @@ -591,7 +578,7 @@ def undelegate( def set_take( self, - wallet: "bittensor.wallet", + wallet: "Wallet", delegate_ss58: Optional[str] = None, take: float = 0.0, wait_for_inclusion: bool = True, @@ -600,7 +587,7 @@ def set_take( """ Set delegate hotkey take Args: - wallet (bittensor.wallet): The wallet containing the hotkey to be nominated. + wallet (bittensor_wallet.Wallet): The wallet containing the hotkey to be nominated. delegate_ss58 (str, optional): Hotkey take (float): Delegate take on subnet ID wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the @@ -627,10 +614,10 @@ def set_take( current_take = int(float(delegate.take) * 65535.0) if takeu16 == current_take: - bittensor.__console__.print("Nothing to do, take hasn't changed") + settings.bt_console.print("Nothing to do, take hasn't changed") return True if current_take is None or current_take < takeu16: - bittensor.__console__.print( + settings.bt_console.print( "Current take is either not set or is lower than the new one. Will use increase_take" ) return increase_take_extrinsic( @@ -642,7 +629,7 @@ def set_take( wait_for_finalization=wait_for_finalization, ) else: - bittensor.__console__.print( + settings.bt_console.print( "Current take is higher than the new one. Will use decrease_take" ) return decrease_take_extrinsic( @@ -656,7 +643,7 @@ def set_take( def send_extrinsic( self, - wallet: "bittensor.wallet", + wallet: "Wallet", module: str, function: str, params: dict, @@ -672,7 +659,7 @@ def send_extrinsic( constructs and submits the extrinsic, handling retries and blockchain communication. Args: - wallet (bittensor.wallet): The wallet associated with the extrinsic. + wallet (bittensor_wallet.Wallet): The wallet associated with the extrinsic. module (str): The module name for the extrinsic. function (str): The function name for the extrinsic. params (dict): The parameters for the extrinsic. @@ -743,14 +730,14 @@ def send_extrinsic( except SubstrateRequestException as e: if "Priority is too low" in e.args[0]["message"]: wait = min(wait_time * attempt, max_wait) - _logger.warning( + logging.warning( f"Priority is too low, retrying with new nonce: {nonce} in {wait} seconds." ) nonce = nonce + 1 time.sleep(wait) continue else: - _logger.error(f"Error sending extrinsic: {e}") + logging.error(f"Error sending extrinsic: {e}") response = None return response @@ -761,11 +748,11 @@ def send_extrinsic( # TODO: still needed? Can't find any usage of this method. def set_weights( self, - wallet: "bittensor.wallet", + wallet: "Wallet", netuid: int, uids: Union[NDArray[np.int64], "torch.LongTensor", list], weights: Union[NDArray[np.float32], "torch.FloatTensor", list], - version_key: int = bittensor.__version_as_int__, + version_key: int = settings.version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, prompt: bool = False, @@ -777,7 +764,7 @@ def set_weights( of Bittensor's decentralized learning architecture. Args: - wallet (bittensor.wallet): The wallet associated with the neuron setting the weights. + 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. @@ -817,7 +804,7 @@ def set_weights( prompt=prompt, ) except Exception as e: - _logger.error(f"Error setting weights: {e}") + logging.error(f"Error setting weights: {e}") finally: retries += 1 @@ -825,11 +812,11 @@ def set_weights( def _do_set_weights( self, - wallet: "bittensor.wallet", + wallet: "Wallet", uids: List[int], vals: List[int], netuid: int, - version_key: int = bittensor.__version_as_int__, + version_key: int = settings.version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> Tuple[bool, Optional[str]]: # (success, error_message) @@ -839,7 +826,7 @@ def _do_set_weights( retries and blockchain communication. Args: - wallet (bittensor.wallet): The wallet associated with the neuron setting the weights. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. uids (List[int]): List of neuron UIDs for which weights are being set. vals (List[int]): List of weight values corresponding to each UID. netuid (int): Unique identifier for the network. @@ -854,7 +841,7 @@ def _do_set_weights( trust in other neurons based on observed performance and contributions. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -894,12 +881,12 @@ def make_substrate_call_with_retry(): ################## def commit_weights( self, - wallet: "bittensor.wallet", + wallet: "Wallet", netuid: int, salt: List[int], uids: Union[NDArray[np.int64], list], weights: Union[NDArray[np.int64], list], - version_key: int = bittensor.__version_as_int__, + version_key: int = settings.version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, prompt: bool = False, @@ -910,7 +897,7 @@ def commit_weights( This action serves as a commitment or snapshot of the neuron's current weight distribution. Args: - wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the weights. netuid (int): The unique identifier of the subnet. salt (List[int]): list of randomly generated integers as salt to generated weighted hash. uids (np.ndarray): NumPy array of neuron UIDs for which weights are being committed. @@ -932,7 +919,7 @@ def commit_weights( success = False message = "No attempt made. Perhaps it is too soon to commit weights!" - _logger.info( + logging.info( "Committing weights with params: netuid={}, uids={}, weights={}, version_key={}".format( netuid, uids, weights, version_key ) @@ -948,7 +935,7 @@ def commit_weights( version_key=version_key, ) - _logger.info("Commit Hash: {}".format(commit_hash)) + logging.info("Commit Hash: {}".format(commit_hash)) while retries < max_retries: try: @@ -964,7 +951,7 @@ def commit_weights( if success: break except Exception as e: - bittensor.logging.error(f"Error committing weights: {e}") + logging.error(f"Error committing weights: {e}") finally: retries += 1 @@ -972,7 +959,7 @@ def commit_weights( def _do_commit_weights( self, - wallet: "bittensor.wallet", + wallet: "Wallet", netuid: int, commit_hash: str, wait_for_inclusion: bool = False, @@ -983,7 +970,7 @@ def _do_commit_weights( This method constructs and submits the transaction, handling retries and blockchain communication. Args: - wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. + 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, optional): Waits for the transaction to be included in a block. @@ -996,7 +983,7 @@ def _do_commit_weights( verifiable record of the neuron's weight distribution at a specific point in time. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -1032,12 +1019,12 @@ def make_substrate_call_with_retry(): ################## def reveal_weights( self, - wallet: "bittensor.wallet", + wallet: "Wallet", netuid: int, uids: Union[NDArray[np.int64], list], weights: Union[NDArray[np.int64], list], salt: Union[NDArray[np.int64], list], - version_key: int = bittensor.__version_as_int__, + version_key: int = settings.version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, prompt: bool = False, @@ -1048,7 +1035,7 @@ def reveal_weights( This action serves as a revelation of the neuron's previously committed weight distribution. Args: - wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. netuid (int): The unique identifier of the subnet. uids (np.ndarray): NumPy array of neuron UIDs for which weights are being revealed. weights (np.ndarray): NumPy array of weight values corresponding to each UID. @@ -1088,7 +1075,7 @@ def reveal_weights( if success: break except Exception as e: - bittensor.logging.error(f"Error revealing weights: {e}") + logging.error(f"Error revealing weights: {e}") finally: retries += 1 @@ -1096,7 +1083,7 @@ def reveal_weights( def _do_reveal_weights( self, - wallet: "bittensor.wallet", + wallet: "Wallet", netuid: int, uids: List[int], values: List[int], @@ -1110,7 +1097,7 @@ def _do_reveal_weights( This method constructs and submits the transaction, handling retries and blockchain communication. Args: - wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. + 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. values (List[int]): List of weight values corresponding to each UID. @@ -1126,7 +1113,7 @@ def _do_reveal_weights( and accountability for the neuron's weight distribution. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -1165,7 +1152,7 @@ def make_substrate_call_with_retry(): ################ def register( self, - wallet: "bittensor.wallet", + wallet: "Wallet", netuid: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, @@ -1185,7 +1172,7 @@ def register( it to stake, set weights, and receive incentives. Args: - wallet (bittensor.wallet): The wallet associated with the neuron to be registered. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered. netuid (int): The unique identifier of the subnet. wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults to `False`. @@ -1227,8 +1214,8 @@ def register( def swap_hotkey( self, - wallet: "bittensor.wallet", - new_wallet: "bittensor.wallet", + wallet: "Wallet", + new_wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, @@ -1240,8 +1227,8 @@ def swap_hotkey( options to wait for inclusion and finalization of the transaction, and to prompt the user for confirmation. Args: - wallet (bittensor.wallet): The wallet whose hotkey is to be swapped. - new_wallet (bittensor.wallet): The new wallet with the hotkey to be set. + wallet (bittensor_wallet.Wallet): The wallet whose hotkey is to be swapped. + new_wallet (bittensor_wallet.Wallet): The new wallet with the hotkey to be set. 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`. @@ -1261,7 +1248,7 @@ def swap_hotkey( def run_faucet( self, - wallet: "bittensor.wallet", + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, @@ -1280,7 +1267,7 @@ def run_faucet( Bittensor network, enabling them to start with a small stake on testnet only. Args: - wallet (bittensor.wallet): The wallet for which the faucet transaction is to be run. + wallet (bittensor_wallet.Wallet): The wallet for which the faucet transaction is to be run. wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults to `False`. wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. @@ -1325,7 +1312,7 @@ def run_faucet( def burned_register( self, - wallet: "bittensor.wallet", + wallet: "Wallet", netuid: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, @@ -1336,7 +1323,7 @@ def burned_register( involves recycling TAO tokens, allowing them to be re-mined by performing work on the network. Args: - wallet (bittensor.wallet): The wallet associated with the neuron to be registered. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered. netuid (int): The unique identifier of the subnet. wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Defaults to `False`. @@ -1359,7 +1346,7 @@ def burned_register( def _do_pow_register( self, netuid: int, - wallet: "bittensor.wallet", + wallet: "Wallet", pow_result: POWSolution, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, @@ -1368,7 +1355,7 @@ def _do_pow_register( Args: netuid (int): The subnet to register on. - wallet (bittensor.wallet): The wallet to register. + wallet (bittensor_wallet.Wallet): The wallet to 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`. @@ -1380,7 +1367,7 @@ def _do_pow_register( message. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): # create extrinsic call call = self.substrate.compose_call( @@ -1421,7 +1408,7 @@ def make_substrate_call_with_retry(): def _do_burned_register( self, netuid: int, - wallet: "bittensor.wallet", + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> Tuple[bool, Optional[str]]: @@ -1433,7 +1420,7 @@ def _do_burned_register( Args: netuid (int): The network unique identifier to register on. - wallet (bittensor.wallet): The wallet to be registered. + 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. @@ -1441,7 +1428,7 @@ def _do_burned_register( Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional error message. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): # create extrinsic call call = self.substrate.compose_call( @@ -1477,8 +1464,8 @@ def make_substrate_call_with_retry(): def _do_swap_hotkey( self, - wallet: "bittensor.wallet", - new_wallet: "bittensor.wallet", + wallet: "Wallet", + new_wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> Tuple[bool, Optional[str]]: @@ -1486,8 +1473,8 @@ def _do_swap_hotkey( Performs a hotkey swap extrinsic call to the Subtensor chain. Args: - wallet (bittensor.wallet): The wallet whose hotkey is to be swapped. - new_wallet (bittensor.wallet): The wallet with the new hotkey to be set. + wallet (bittensor_wallet.Wallet): The wallet whose hotkey is to be swapped. + new_wallet (bittensor_wallet.Wallet): The wallet with the new hotkey to be set. 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`. @@ -1497,7 +1484,7 @@ def _do_swap_hotkey( error message. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): # create extrinsic call call = self.substrate.compose_call( @@ -1536,7 +1523,7 @@ def make_substrate_call_with_retry(): ############ def transfer( self, - wallet: "bittensor.wallet", + wallet: "Wallet", dest: str, amount: Union[Balance, float], wait_for_inclusion: bool = True, @@ -1549,7 +1536,7 @@ def transfer( between neurons. Args: - wallet (bittensor.wallet): The wallet from which funds are being transferred. + wallet (bittensor_wallet.Wallet): The wallet from which funds are being transferred. dest (str): The destination public key address. amount (Union[Balance, float]): The amount of TAO to be transferred. wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. @@ -1573,7 +1560,7 @@ def transfer( ) def get_transfer_fee( - self, wallet: "bittensor.wallet", dest: str, value: Union["Balance", float, int] + self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] ) -> "Balance": """ Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. @@ -1581,7 +1568,7 @@ def get_transfer_fee( network conditions and transaction complexity. Args: - wallet (bittensor.wallet): The wallet from which the transfer is initiated. + wallet (bittensor_wallet.Wallet): The wallet from which the transfer is initiated. dest (str): The ``SS58`` address of the destination account. value (Union[Balance, float, int]): The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao (int) units. @@ -1610,7 +1597,7 @@ def get_transfer_fee( call=call, keypair=wallet.coldkeypub ) except Exception as e: - bittensor.__console__.print( + settings.bt_console.print( ":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n {}[/bold white]".format( e ) @@ -1621,7 +1608,7 @@ def get_transfer_fee( return fee else: fee = Balance.from_rao(int(2e7)) - _logger.error( + logging.error( "To calculate the transaction fee, the value must be Balance, float, or int. Received type: %s. Fee " "is %s", type(value), @@ -1631,7 +1618,7 @@ def get_transfer_fee( def _do_transfer( self, - wallet: "bittensor.wallet", + wallet: "Wallet", dest: str, transfer_balance: "Balance", wait_for_inclusion: bool = True, @@ -1640,19 +1627,19 @@ def _do_transfer( """Sends a transfer extrinsic to the chain. Args: - wallet (:func:`bittensor.wallet`): Wallet object. + wallet (bittensor_wallet.Wallet): Wallet object. dest (str): Destination public key address. - transfer_balance (:func:`Balance`): Amount to transfer. + transfer_balance (bittensor.utils.balance.Balance): Amount to transfer. wait_for_inclusion (bool): If ``true``, waits for inclusion. wait_for_finalization (bool): If ``true``, waits for finalization. + Returns: success (bool): ``True`` if transfer was successful. - block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is - ``True``. + block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is ``True``. error (str): Error message if transfer failed. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="Balances", @@ -1713,7 +1700,7 @@ def get_existential_deposit( ########### def register_subnetwork( self, - wallet: "bittensor.wallet", + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization=True, prompt: bool = False, @@ -1724,7 +1711,7 @@ def register_subnetwork( overall Bittensor network. Args: - wallet (bittensor.wallet): The wallet to be used for registration. + wallet (bittensor_wallet.Wallet): The wallet to be used for registration. 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. prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. @@ -1745,7 +1732,7 @@ def register_subnetwork( def set_hyperparameter( self, - wallet: "bittensor.wallet", + wallet: "Wallet", netuid: int, parameter: str, value, @@ -1759,7 +1746,7 @@ def set_hyperparameter( subnetwork. Args: - wallet (bittensor.wallet): The wallet used for setting the hyperparameter. + wallet (bittensor_wallet.Wallet): The wallet used for setting the hyperparameter. netuid (int): The unique identifier of the subnetwork. parameter (str): The name of the hyperparameter to be set. value: The new value for the hyperparameter. @@ -1789,7 +1776,7 @@ def set_hyperparameter( ########### def serve( self, - wallet: "bittensor.wallet", + wallet: "Wallet", ip: str, port: int, protocol: int, @@ -1805,7 +1792,7 @@ def serve( communication within the network. Args: - wallet (bittensor.wallet): The wallet associated with the neuron being served. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron being served. ip (str): The IP address of the serving neuron. port (int): The port number on which the neuron is serving. protocol (int): The protocol type used by the neuron (e.g., GRPC, HTTP). @@ -1839,7 +1826,7 @@ def serve( def serve_axon( self, netuid: int, - axon: "bittensor.axon", + axon: "Axon", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: @@ -1850,7 +1837,7 @@ def serve_axon( Args: netuid (int): The unique identifier of the subnetwork. - axon (bittensor.Axon): The Axon instance to be registered for serving. + axon (bittensor.core.axon.Axon): The Axon instance to be registered for serving. 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. @@ -1866,7 +1853,7 @@ def serve_axon( def _do_serve_axon( self, - wallet: "bittensor.wallet", + wallet: "Wallet", call_params: AxonServeCallParams, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, @@ -1876,7 +1863,7 @@ def _do_serve_axon( creates and submits a transaction, enabling a neuron's Axon to serve requests on the network. Args: - wallet (bittensor.wallet): The wallet associated with the neuron. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. call_params (AxonServeCallParams): Parameters required for the serve axon call. 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. @@ -1888,7 +1875,7 @@ def _do_serve_axon( enhancing the decentralized computation capabilities of Bittensor. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -1916,7 +1903,7 @@ def make_substrate_call_with_retry(): def serve_prometheus( self, - wallet: "bittensor.wallet", + wallet: "Wallet", port: int, netuid: int, wait_for_inclusion: bool = False, @@ -1933,7 +1920,7 @@ def serve_prometheus( def _do_serve_prometheus( self, - wallet: "bittensor.wallet", + wallet: "Wallet", call_params: PrometheusServeCallParams, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, @@ -1941,7 +1928,7 @@ def _do_serve_prometheus( """ Sends a serve prometheus extrinsic to the chain. Args: - wallet (:func:`bittensor.wallet`): Wallet object. + wallet (:func:`bittensor_wallet.Wallet`): Wallet object. call_params (:func:`PrometheusServeCallParams`): Prometheus serve call parameters. wait_for_inclusion (bool): If ``true``, waits for inclusion. wait_for_finalization (bool): If ``true``, waits for finalization. @@ -1950,7 +1937,7 @@ def _do_serve_prometheus( error (:func:`Optional[str]`): Error message if serve prometheus failed, ``None`` otherwise. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -1978,7 +1965,7 @@ def make_substrate_call_with_retry(): def _do_associate_ips( self, - wallet: "bittensor.wallet", + wallet: "Wallet", ip_info_list: List["IPInfo"], netuid: int, wait_for_inclusion: bool = False, @@ -1988,7 +1975,7 @@ def _do_associate_ips( Sends an associate IPs extrinsic to the chain. Args: - wallet (:func:`bittensor.wallet`): Wallet object. + wallet (bittensor_wallet.Wallet): Wallet object. ip_info_list (:func:`List[IPInfo]`): List of IPInfo objects. netuid (int): Netuid to associate IPs to. wait_for_inclusion (bool): If ``true``, waits for inclusion. @@ -1999,7 +1986,7 @@ def _do_associate_ips( error (:func:`Optional[str]`): Error message if associate IPs failed, None otherwise. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -2033,7 +2020,7 @@ def make_substrate_call_with_retry(): ########### def add_stake( self, - wallet: "bittensor.wallet", + wallet: "Wallet", hotkey_ss58: Optional[str] = None, amount: Optional[Union["Balance", float]] = None, wait_for_inclusion: bool = True, @@ -2046,7 +2033,7 @@ def add_stake( and earn incentives. Args: - wallet (bittensor.wallet): The wallet to be used for staking. + wallet (bittensor_wallet.Wallet): The wallet to be used for staking. hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey associated with the neuron. amount (Union[Balance, float]): The amount of TAO to stake. wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. @@ -2071,7 +2058,7 @@ def add_stake( def add_stake_multiple( self, - wallet: "bittensor.wallet", + wallet: "Wallet", hotkey_ss58s: List[str], amounts: Optional[List[Union["Balance", float]]] = None, wait_for_inclusion: bool = True, @@ -2083,7 +2070,7 @@ def add_stake_multiple( allows for efficient staking across different neurons from a single wallet. Args: - wallet (bittensor.wallet): The wallet used for staking. + wallet (bittensor_wallet.Wallet): The wallet used for staking. hotkey_ss58s (List[str]): List of ``SS58`` addresses of hotkeys to stake to. amounts (List[Union[Balance, float]], optional): Corresponding amounts of TAO to stake for each hotkey. wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. @@ -2108,7 +2095,7 @@ def add_stake_multiple( def _do_stake( self, - wallet: "bittensor.wallet", + wallet: "Wallet", hotkey_ss58: str, amount: "Balance", wait_for_inclusion: bool = True, @@ -2117,18 +2104,20 @@ def _do_stake( """Sends a stake extrinsic to the chain. Args: - wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. + wallet (bittensor_wallet.Wallet): Wallet object that can sign the extrinsic. hotkey_ss58 (str): Hotkey ``ss58`` address to stake to. amount (:func:`Balance`): Amount to stake. wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. wait_for_finalization (bool): If ``true``, waits for finalization before returning. + Returns: success (bool): ``True`` if the extrinsic was successful. + Raises: StakeError: If the extrinsic failed. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -2160,7 +2149,7 @@ def make_substrate_call_with_retry(): ############# def unstake_multiple( self, - wallet: "bittensor.wallet", + wallet: "Wallet", hotkey_ss58s: List[str], amounts: Optional[List[Union["Balance", float]]] = None, wait_for_inclusion: bool = True, @@ -2172,7 +2161,7 @@ def unstake_multiple( efficiently. This function is useful for managing the distribution of stakes across multiple neurons. Args: - wallet (bittensor.wallet): The wallet linked to the coldkey from which the stakes are being withdrawn. + wallet (bittensor_wallet.Wallet): The wallet linked to the coldkey from which the stakes are being withdrawn. hotkey_ss58s (List[str]): A list of hotkey ``SS58`` addresses to unstake from. amounts (List[Union[Balance, float]], optional): The amounts of TAO to unstake from each hotkey. If not provided, unstakes all available stakes. @@ -2198,7 +2187,7 @@ def unstake_multiple( def unstake( self, - wallet: "bittensor.wallet", + wallet: "Wallet", hotkey_ss58: Optional[str] = None, amount: Optional[Union["Balance", float]] = None, wait_for_inclusion: bool = True, @@ -2210,7 +2199,7 @@ def unstake( individual neuron stakes within the Bittensor network. Args: - wallet (bittensor.wallet): The wallet associated with the neuron from which the stake is being removed. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron from which the stake is being removed. hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. amount (Union[Balance, float], optional): The amount of TAO to unstake. If not specified, unstakes all. wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. @@ -2235,7 +2224,7 @@ def unstake( def _do_unstake( self, - wallet: "bittensor.wallet", + wallet: "Wallet", hotkey_ss58: str, amount: "Balance", wait_for_inclusion: bool = True, @@ -2244,7 +2233,7 @@ def _do_unstake( """Sends an unstake extrinsic to the chain. Args: - wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. + wallet (:func:`Wallet`): Wallet object that can sign the extrinsic. hotkey_ss58 (str): Hotkey ``ss58`` address to unstake from. amount (:func:`Balance`): Amount to unstake. wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. @@ -2255,7 +2244,7 @@ def _do_unstake( StakeError: If the extrinsic failed. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -2328,7 +2317,7 @@ def get_remaining_arbitration_period( def register_senate( self, - wallet: "bittensor.wallet", + wallet: "Wallet", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, @@ -2338,7 +2327,7 @@ def register_senate( individual neuron stakes within the Bittensor network. Args: - wallet (bittensor.wallet): The wallet associated with the neuron from which the stake is being removed. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron from which the stake is being removed. 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. prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. @@ -2355,7 +2344,7 @@ def register_senate( def leave_senate( self, - wallet: "bittensor.wallet", + wallet: "Wallet", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, @@ -2365,7 +2354,7 @@ def leave_senate( individual neuron stakes within the Bittensor network. Args: - wallet (bittensor.wallet): The wallet associated with the neuron from which the stake is being removed. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron from which the stake is being removed. 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. prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. @@ -2382,7 +2371,7 @@ def leave_senate( def vote_senate( self, - wallet: "bittensor.wallet", + wallet: "Wallet", proposal_hash: str, proposal_idx: int, vote: bool, @@ -2395,7 +2384,7 @@ def vote_senate( individual neuron stakes within the Bittensor network. Args: - wallet (bittensor.wallet): The wallet associated with the neuron from which the stake is being removed. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron from which the stake is being removed. proposal_hash (str): The hash of the proposal being voted on. proposal_idx (int): The index of the proposal being voted on. vote (bool): The vote to be cast (True for yes, False for no). @@ -2552,11 +2541,9 @@ def get_proposals( block (Optional[int]): The blockchain block number to query the proposals. Returns: - Optional[Dict[str, Tuple[bittensor.ProposalCallData, bittensor.ProposalVoteData]]]: A dictionary mapping - proposal hashes to their corresponding call and vote data, or ``None`` if not available. + Optional[Dict[str, Tuple[bittensor.core.chain_data.ProposalCallData, bittensor.core.chain_data.ProposalVoteData]]]: A dictionary mapping proposal hashes to their corresponding call and vote data, or ``None`` if not available. - This function is integral for analyzing the governance activity on the Bittensor network, - providing a holistic view of the proposals and their impact or potential changes within the network. + This function is integral for analyzing the governance activity on the Bittensor network, providing a holistic view of the proposals and their impact or potential changes within the network. """ proposal_hashes: Optional[List[str]] = self.get_proposal_hashes(block=block) if proposal_hashes is None: @@ -2575,17 +2562,16 @@ def get_proposals( def root_register( self, - wallet: "bittensor.wallet", + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: """ - Registers the neuron associated with the wallet on the root network. This process is integral for - participating in the highest layer of decision-making and governance within the Bittensor network. + Registers the neuron associated with the wallet on the root network. This process is integral for participating in the highest layer of decision-making and governance within the Bittensor network. Args: - wallet (bittensor.wallet): The wallet associated with the neuron to be registered on the root network. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered on the root 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. prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. @@ -2593,8 +2579,7 @@ def root_register( Returns: bool: ``True`` if the registration on the root network is successful, False otherwise. - This function enables neurons to engage in the most critical and influential aspects of the network's - governance, signifying a high level of commitment and responsibility in the Bittensor ecosystem. + This function enables neurons to engage in the most critical and influential aspects of the network's governance, signifying a high level of commitment and responsibility in the Bittensor ecosystem. """ return root_register_extrinsic( subtensor=self, @@ -2606,11 +2591,11 @@ def root_register( def _do_root_register( self, - wallet: "bittensor.wallet", + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> Tuple[bool, Optional[str]]: - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): # create extrinsic call call = self.substrate.compose_call( @@ -2644,7 +2629,7 @@ def make_substrate_call_with_retry(): @legacy_torch_api_compat def root_set_weights( self, - wallet: "bittensor.wallet", + wallet: "Wallet", netuids: Union[NDArray[np.int64], "torch.LongTensor", list], weights: Union[NDArray[np.float32], "torch.FloatTensor", list], version_key: int = 0, @@ -2657,7 +2642,7 @@ def root_set_weights( and interactions of neurons at the root level of the Bittensor network. Args: - wallet (bittensor.wallet): The wallet associated with the neuron setting the weights. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. netuids (Union[NDArray[np.int64], torch.LongTensor, list]): The list of neuron UIDs for which weights are being set. weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The corresponding weights to be set for each @@ -2686,11 +2671,11 @@ def root_set_weights( def _do_set_root_weights( self, - wallet: "bittensor.wallet", + wallet: "Wallet", uids: List[int], vals: List[int], netuid: int = 0, - version_key: int = bittensor.__version_as_int__, + version_key: int = settings.version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, ) -> Tuple[bool, Optional[str]]: # (success, error_message) @@ -2700,7 +2685,7 @@ def _do_set_root_weights( retries and blockchain communication. Args: - wallet (bittensor.wallet): The wallet associated with the neuron setting the weights. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. uids (List[int]): List of neuron UIDs for which weights are being set. vals (List[int]): List of weight values corresponding to each UID. netuid (int): Unique identifier for the network. @@ -2715,7 +2700,7 @@ def _do_set_root_weights( trust in other neurons based on observed performance and contributions to the root network. """ - @retry(delay=2, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=2, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -2781,7 +2766,7 @@ def query_identity( network-specific details, providing insights into the neuron's role and status within the Bittensor network. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry() -> "ScaleType": return self.substrate.query( module="Registry", @@ -2794,13 +2779,13 @@ def make_substrate_call_with_retry() -> "ScaleType": identity_info = make_substrate_call_with_retry() - return bittensor.utils.wallet_utils.decode_hex_identity_dict( + return wallet_utils.decode_hex_identity_dict( identity_info.value["info"] ) def update_identity( self, - wallet: "bittensor.wallet", + wallet: "Wallet", identified: Optional[str] = None, params: Optional[dict] = None, wait_for_inclusion: bool = True, @@ -2815,7 +2800,7 @@ def update_identity( parameters. Args: - wallet (bittensor.wallet): The wallet associated with the neuron whose identity is being updated. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron whose identity is being updated. identified (str, optional): The identified ``SS58`` address of the neuron. Defaults to the wallet's coldkey address. params (dict, optional): A dictionary of parameters to update in the neuron's identity. @@ -2833,10 +2818,10 @@ def update_identity( params = {} if params is None else params - call_params = bittensor.utils.wallet_utils.create_identity_dict(**params) + call_params = wallet_utils.create_identity_dict(**params) call_params["identified"] = identified - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry() -> bool: call = self.substrate.compose_call( call_module="Registry", @@ -2868,7 +2853,7 @@ def commit(self, wallet, netuid: int, data: str): Commits arbitrary data to the Bittensor network by publishing metadata. Args: - wallet (bittensor.wallet): The wallet associated with the neuron committing the data. + 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. """ @@ -2923,7 +2908,7 @@ def query_subtensor( providing valuable insights into the state and dynamics of the Bittensor ecosystem. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry() -> "ScaleType": return self.substrate.query( module="SubtensorModule", @@ -2960,7 +2945,7 @@ def query_map_subtensor( relationships within the Bittensor ecosystem, such as inter-neuronal connections and stake distributions. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): return self.substrate.query_map( module="SubtensorModule", @@ -2994,7 +2979,7 @@ def query_constant( operational parameters. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): return self.substrate.get_constant( module_name=module_name, @@ -3032,7 +3017,7 @@ def query_module( parts of the Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry() -> "ScaleType": return self.substrate.query( module=module, @@ -3071,7 +3056,7 @@ def query_map( modules, offering insights into the network's state and the relationships between its different components. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry() -> "QueryMapResult": return self.substrate.query_map( module=module, @@ -3106,7 +3091,7 @@ def state_call( useful for specific use cases where standard queries are insufficient. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry() -> Dict[Any, Any]: block_hash = None if block is None else self.substrate.get_block_hash(block) @@ -4162,7 +4147,7 @@ def get_all_subnets_info(self, block: Optional[int] = None) -> List[SubnetInfo]: the roles of different subnets, and their unique features. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): block_hash = None if block is None else self.substrate.get_block_hash(block) @@ -4196,7 +4181,7 @@ def get_subnet_info( subnet, including its governance, performance, and role within the broader network. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): block_hash = None if block is None else self.substrate.get_block_hash(block) @@ -4354,7 +4339,7 @@ def get_delegate_by_hotkey( the Bittensor network's consensus and governance structures. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(encoded_hotkey_: List[int]): block_hash = None if block is None else self.substrate.get_block_hash(block) @@ -4391,7 +4376,7 @@ def get_delegates_lite(self, block: Optional[int] = None) -> List[DelegateInfoLi """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): block_hash = None if block is None else self.substrate.get_block_hash(block) @@ -4423,7 +4408,7 @@ def get_delegates(self, block: Optional[int] = None) -> List[DelegateInfo]: """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): block_hash = None if block is None else self.substrate.get_block_hash(block) @@ -4458,7 +4443,7 @@ def get_delegated( involvement in the network's delegation and consensus mechanisms. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(encoded_coldkey_: List[int]): block_hash = None if block is None else self.substrate.get_block_hash(block) @@ -4571,7 +4556,7 @@ def get_minimum_required_stake( Exception: If the substrate call fails after the maximum number of retries. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): return self.substrate.query( module="SubtensorModule", storage_function="NominatorMinRequiredStake" @@ -4786,7 +4771,7 @@ def neuron_has_validator_permit( return getattr(_result, "value", None) def neuron_for_wallet( - self, wallet: "bittensor.wallet", netuid: int, block: Optional[int] = None + self, wallet: "Wallet", netuid: int, block: Optional[int] = None ) -> Optional[NeuronInfo]: """ Retrieves information about a neuron associated with a given wallet on a specific subnet. @@ -4794,7 +4779,7 @@ def neuron_for_wallet( the wallet's hotkey address. Args: - wallet (bittensor.wallet): The wallet associated with the neuron. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. netuid (int): The unique identifier of the subnet. block (Optional[int]): The blockchain block number at which to perform the query. @@ -4830,7 +4815,7 @@ def neuron_for_uid( if uid is None: return NeuronInfo.get_null_neuron() - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): block_hash = None if block is None else self.substrate.get_block_hash(block) params = [netuid, uid] @@ -4961,7 +4946,7 @@ def metagraph( netuid: int, lite: bool = True, block: Optional[int] = None, - ) -> "bittensor.metagraph": # type: ignore + ) -> "metagraph": # type: ignore """ Returns a synced metagraph for a specified subnet within the Bittensor network. The metagraph represents the network's structure, including neuron connections and interactions. @@ -4972,13 +4957,12 @@ def metagraph( block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. Returns: - bittensor.Metagraph: The metagraph representing the subnet's structure and neuron relationships. + bittensor.core.metagraph.Metagraph: The metagraph representing the subnet's structure and neuron relationships. The metagraph is an essential tool for understanding the topology and dynamics of the Bittensor - network's decentralized architecture, particularly in relation to neuron interconnectivity and consensus - processes. + network's decentralized architecture, particularly in relation to neuron interconnectivity and consensus processes. """ - metagraph_ = bittensor.metagraph( + metagraph_ = Metagraph( network=self.network, netuid=netuid, lite=lite, sync=False ) metagraph_.sync(block=block, lite=lite, subtensor=self) @@ -5074,19 +5058,16 @@ def associated_validator_ip_info( self, netuid: int, block: Optional[int] = None ) -> Optional[List["IPInfo"]]: """ - Retrieves the list of all validator IP addresses associated with a specific subnet in the Bittensor - network. This information is crucial for network communication and the identification of validator nodes. + Retrieves the list of all validator IP addresses associated with a specific subnet in the Bittensor network. This information is crucial for network communication and the identification of validator nodes. Args: netuid (int): The network UID of the subnet to query. block (Optional[int]): The blockchain block number for the query. Returns: - Optional[List[IPInfo]]: A list of IPInfo objects for validator nodes in the subnet, or ``None`` if no - validators are associated. + Optional[List[IPInfo]]: A list of IPInfo objects for validator nodes in the subnet, or ``None`` if no validators are associated. - Validator IP information is key for establishing secure and reliable connections within the network, - facilitating consensus and validation processes critical for the network's integrity and performance. + Validator IP information is key for establishing secure and reliable connections within the network, facilitating consensus and validation processes critical for the network's integrity and performance. """ hex_bytes_result = self.query_runtime_api( runtime_api="ValidatorIPRuntimeApi", @@ -5107,8 +5088,7 @@ def associated_validator_ip_info( def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: """ - Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost - represents the amount of Tao that needs to be locked or burned to establish a new subnet. + Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the amount of Tao that needs to be locked or burned to establish a new subnet. Args: block (Optional[int]): The blockchain block number for the query. @@ -5116,8 +5096,7 @@ def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: Returns: int: The burn cost for subnet registration. - The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for - controlling the proliferation of subnets and ensuring their commitment to the network's long-term viability. + The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling the proliferation of subnets and ensuring their commitment to the network's long-term viability. """ lock_cost = self.query_runtime_api( runtime_api="SubnetRegistrationRuntimeApi", @@ -5137,7 +5116,7 @@ def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: def _do_delegation( self, - wallet: "bittensor.wallet", + wallet: "Wallet", delegate_ss58: str, amount: "Balance", wait_for_inclusion: bool = True, @@ -5146,11 +5125,10 @@ def _do_delegation( """ Delegates a specified amount of stake to a delegate's hotkey. - This method sends a transaction to add stake to a delegate's hotkey and retries the call up to three times - with exponential backoff in case of failures. + This method sends a transaction to add stake to a delegate's hotkey and retries the call up to three times with exponential backoff in case of failures. Args: - wallet (bittensor.wallet): The wallet from which the stake will be delegated. + wallet (bittensor_wallet.Wallet): The wallet from which the stake will be delegated. delegate_ss58 (str): The SS58 address of the delegate's hotkey. amount (Balance): The amount of stake to be delegated. wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. @@ -5160,7 +5138,7 @@ def _do_delegation( bool: ``True`` if the delegation is successful, ``False`` otherwise. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -5188,7 +5166,7 @@ def make_substrate_call_with_retry(): def _do_undelegation( self, - wallet: "bittensor.wallet", + wallet: "Wallet", delegate_ss58: str, amount: "Balance", wait_for_inclusion: bool = True, @@ -5197,11 +5175,10 @@ def _do_undelegation( """ Removes a specified amount of stake from a delegate's hotkey. - This method sends a transaction to remove stake from a delegate's hotkey and retries the call up to three times - with exponential backoff in case of failures. + This method sends a transaction to remove stake from a delegate's hotkey and retries the call up to three times with exponential backoff in case of failures. Args: - wallet (bittensor.wallet): The wallet from which the stake will be removed. + wallet (bittensor_wallet.Wallet): The wallet from which the stake will be removed. delegate_ss58 (str): The SS58 address of the delegate's hotkey. amount (Balance): The amount of stake to be removed. wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. @@ -5211,7 +5188,7 @@ def _do_undelegation( bool: ``True`` if the undelegation is successful, ``False`` otherwise. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -5242,18 +5219,17 @@ def make_substrate_call_with_retry(): def _do_nominate( self, - wallet: "bittensor.wallet", + wallet: "Wallet", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: """ Nominates the wallet's hotkey to become a delegate. - This method sends a transaction to nominate the wallet's hotkey to become a delegate and retries the call up to - three times with exponential backoff in case of failures. + This method sends a transaction to nominate the wallet's hotkey to become a delegate and retries the call up to three times with exponential backoff in case of failures. Args: - wallet (bittensor.wallet): The wallet whose hotkey will be nominated. + wallet (bittensor_wallet.Wallet): The wallet whose hotkey will be nominated. wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. @@ -5261,7 +5237,7 @@ def _do_nominate( bool: ``True`` if the nomination is successful, ``False`` otherwise. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -5289,7 +5265,7 @@ def make_substrate_call_with_retry(): def _do_increase_take( self, - wallet: "bittensor.wallet", + wallet: "Wallet", hotkey_ss58: str, take: int, wait_for_inclusion: bool = True, @@ -5298,11 +5274,10 @@ def _do_increase_take( """ Increases the take rate for a delegate's hotkey. - This method sends a transaction to increase the take rate for a delegate's hotkey and retries the call up to - three times with exponential backoff in case of failures. + This method sends a transaction to increase the take rate for a delegate's hotkey and retries the call up to three times with exponential backoff in case of failures. Args: - wallet (bittensor.wallet): The wallet from which the transaction will be signed. + wallet (bittensor_wallet.Wallet): The wallet from which the transaction will be signed. hotkey_ss58 (str): The SS58 address of the delegate's hotkey. take (int): The new take rate to be set. wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. @@ -5344,7 +5319,7 @@ def make_substrate_call_with_retry(): def _do_decrease_take( self, - wallet: "bittensor.wallet", + wallet: "Wallet", hotkey_ss58: str, take: int, wait_for_inclusion: bool = True, @@ -5353,11 +5328,10 @@ def _do_decrease_take( """ Decreases the take rate for a delegate's hotkey. - This method sends a transaction to decrease the take rate for a delegate's hotkey and retries the call up to - three times with exponential backoff in case of failures. + This method sends a transaction to decrease the take rate for a delegate's hotkey and retries the call up to three times with exponential backoff in case of failures. Args: - wallet (bittensor.wallet): The wallet from which the transaction will be signed. + wallet (bittensor_wallet.Wallet): The wallet from which the transaction will be signed. hotkey_ss58 (str): The SS58 address of the delegate's hotkey. take (int): The new take rate to be set. wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. @@ -5403,8 +5377,7 @@ def make_substrate_call_with_retry(): def get_balance(self, address: str, block: Optional[int] = None) -> Balance: """ - Retrieves the token balance of a specific address within the Bittensor network. This function queries - the blockchain to determine the amount of Tao held by a given account. + Retrieves the token balance of a specific address within the Bittensor network. This function queries the blockchain to determine the amount of Tao held by a given account. Args: address (str): The Substrate address in ``ss58`` format. @@ -5413,12 +5386,11 @@ def get_balance(self, address: str, block: Optional[int] = None) -> Balance: Returns: Balance: The account balance at the specified block, represented as a Balance object. - This function is important for monitoring account holdings and managing financial transactions - within the Bittensor ecosystem. It helps in assessing the economic status and capacity of network participants. + This function is important for monitoring account holdings and managing financial transactions within the Bittensor ecosystem. It helps in assessing the economic status and capacity of network participants. """ try: - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): return self.substrate.query( module="System", @@ -5431,7 +5403,7 @@ def make_substrate_call_with_retry(): result = make_substrate_call_with_retry() except RemainingScaleBytesNotEmptyException: - _logger.error( + logging.error( "Received a corrupted message. This likely points to an error with the network or subnet." ) return Balance(1000) @@ -5439,17 +5411,15 @@ def make_substrate_call_with_retry(): def get_current_block(self) -> int: """ - Returns the current block number on the Bittensor blockchain. This function provides the latest block - number, indicating the most recent state of the blockchain. + Returns the current block number on the Bittensor blockchain. This function provides the latest block number, indicating the most recent state of the blockchain. Returns: int: The current chain block number. - Knowing the current block number is essential for querying real-time data and performing time-sensitive - operations on the blockchain. It serves as a reference point for network activities and data synchronization. + Knowing the current block number is essential for querying real-time data and performing time-sensitive operations on the blockchain. It serves as a reference point for network activities and data synchronization. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): return self.substrate.get_block_number(None) # type: ignore @@ -5466,11 +5436,10 @@ def get_balances(self, block: Optional[int] = None) -> Dict[str, Balance]: Returns: Dict[str, Balance]: A dictionary mapping each account's ``ss58`` address to its balance. - This function is valuable for analyzing the overall economic landscape of the Bittensor network, - including the distribution of financial resources and the financial status of network participants. + This function is valuable for analyzing the overall economic landscape of the Bittensor network, including the distribution of financial resources and the financial status of network participants. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): return self.substrate.query_map( module="System", @@ -5526,8 +5495,6 @@ def get_block_hash(self, block_id: int) -> str: Returns: str: The cryptographic hash of the specified block. - The block hash is a fundamental aspect of blockchain technology, providing a secure reference to - each block's data. It is crucial for verifying transactions, ensuring data consistency, and - maintaining the trustworthiness of the blockchain. + The block hash is a fundamental aspect of blockchain technology, providing a secure reference to each block's data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the trustworthiness of the blockchain. """ return self.substrate.get_block_hash(block_id=block_id) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index f5d3ec227..6461b2b74 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -20,12 +20,13 @@ from unittest.mock import MagicMock import pytest +from bittensor_wallet import Wallet -import bittensor -from bittensor.core import subtensor as subtensor_module, settings from bittensor.btcli.commands.utils import normalize_hyperparameters +from bittensor.core import subtensor as subtensor_module, settings +from bittensor.core.axon import Axon from bittensor.core.chain_data import SubnetHyperparameters -from bittensor.core.subtensor import Subtensor, _logger +from bittensor.core.subtensor import Subtensor, logging from bittensor.utils.balance import Balance U16_MAX = 65535 @@ -38,11 +39,11 @@ def test_serve_axon_with_external_ip_set(): mock_serve_axon = MagicMock(return_value=True) - mock_subtensor = MagicMock(spec=bittensor.subtensor, serve_axon=mock_serve_axon) + mock_subtensor = MagicMock(spec=Subtensor, serve_axon=mock_serve_axon) mock_add_insecure_port = mock.MagicMock(return_value=None) mock_wallet = MagicMock( - spec=bittensor.wallet, + spec=Wallet, coldkey=MagicMock(), coldkeypub=MagicMock( # mock ss58 address @@ -53,8 +54,8 @@ def test_serve_axon_with_external_ip_set(): ), ) - mock_config = bittensor.axon.config() - mock_axon_with_external_ip_set = bittensor.axon( + mock_config = Axon.config() + mock_axon_with_external_ip_set = Axon( wallet=mock_wallet, ip=internal_ip, external_ip=external_ip, @@ -85,13 +86,13 @@ def test_serve_axon_with_external_port_set(): mock_serve_axon = MagicMock(return_value=True) mock_subtensor = MagicMock( - spec=bittensor.subtensor, + spec=Subtensor, serve=mock_serve, serve_axon=mock_serve_axon, ) mock_wallet = MagicMock( - spec=bittensor.wallet, + spec=Wallet, coldkey=MagicMock(), coldkeypub=MagicMock( # mock ss58 address @@ -102,9 +103,9 @@ def test_serve_axon_with_external_port_set(): ), ) - mock_config = bittensor.axon.config() + mock_config = Axon.config() - mock_axon_with_external_port_set = bittensor.axon( + mock_axon_with_external_port_set = Axon( wallet=mock_wallet, port=internal_port, external_port=external_port, @@ -134,10 +135,10 @@ class ExitEarly(Exception): def test_stake_multiple(): - mock_amount: bittensor.Balance = bittensor.Balance.from_tao(1.0) + mock_amount: Balance = Balance.from_tao(1.0) mock_wallet = MagicMock( - spec=bittensor.wallet, + spec=Wallet, coldkey=MagicMock(), coldkeypub=MagicMock( # mock ss58 address @@ -159,17 +160,17 @@ def test_stake_multiple(): mock_do_stake = MagicMock(side_effect=ExitEarly) mock_subtensor = MagicMock( - spec=bittensor.subtensor, + spec=Subtensor, network="mock_net", get_balance=MagicMock( - return_value=bittensor.Balance.from_tao(mock_amount.tao + 20.0) + return_value=Balance.from_tao(mock_amount.tao + 20.0) ), # enough balance to stake get_neuron_for_pubkey_and_subnet=MagicMock(return_value=mock_neuron), _do_stake=mock_do_stake, ) with pytest.raises(ExitEarly): - bittensor.subtensor.add_stake_multiple( + Subtensor.add_stake_multiple( mock_subtensor, wallet=mock_wallet, hotkey_ss58s=mock_hotkey_ss58s, @@ -294,7 +295,7 @@ def subtensor(substrate): @pytest.fixture def mock_logger(): - with mock.patch.object(_logger, "warning") as mock_warning: + with mock.patch.object(logging, "warning") as mock_warning: yield mock_warning From ffa30b55d0ce3e18a6acafc50179732e7285fbe5 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 2 Aug 2024 10:28:23 -0700 Subject: [PATCH 051/260] Make prometheus.py independent of bittensor import. Fix prometheus tests. --- bittensor/api/extrinsics/prometheus.py | 64 +++++++++---------- .../unit_tests/extrinsics/test_prometheus.py | 8 +-- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/bittensor/api/extrinsics/prometheus.py b/bittensor/api/extrinsics/prometheus.py index 8828d5a14..dbfd3d92f 100644 --- a/bittensor/api/extrinsics/prometheus.py +++ b/bittensor/api/extrinsics/prometheus.py @@ -15,54 +15,50 @@ # 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 bittensor - import json -import bittensor.utils.networking as net + +from bittensor_wallet import Wallet + +from bittensor.core.settings import version_as_int, bt_console +from bittensor.core.types import PrometheusServeCallParams +from bittensor.utils import networking as net +from bittensor.utils.btlogging import logging def prometheus_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", port: int, netuid: int, ip: int = None, wait_for_inclusion: bool = False, wait_for_finalization=True, ) -> bool: - r"""Subscribes an Bittensor endpoint to the substensor chain. + """Subscribes a Bittensor endpoint to the substensor chain. Args: - subtensor (bittensor.subtensor): - Bittensor subtensor object. - wallet (bittensor.wallet): - Bittensor wallet object. - ip (str): - Endpoint host port i.e., ``192.122.31.4``. - port (int): - Endpoint port number i.e., `9221`. - netuid (int): - Network `uid` to serve on. - 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. + subtensor (bittensor.subtensor): Bittensor subtensor object. + wallet (bittensor.wallet): Bittensor wallet object. + ip (str): Endpoint host port i.e., ``192.122.31.4``. + port (int): Endpoint port number i.e., `9221`. + netuid (int): Network `uid` to serve on. + 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. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. - If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ - # ---- Get external ip ---- + # Get external ip if ip is None: try: external_ip = net.get_external_ip() - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Found external ip: {}[/green]".format( external_ip ) ) - bittensor.logging.success( + logging.success( prefix="External IP", suffix="{}".format(external_ip) ) except Exception as E: @@ -74,14 +70,14 @@ def prometheus_extrinsic( else: external_ip = ip - call_params: "bittensor.PrometheusServeCallParams" = { - "version": bittensor.__version_as_int__, + call_params: "PrometheusServeCallParams" = { + "version": version_as_int, "ip": net.ip_to_int(external_ip), "port": port, "ip_type": net.ip_version(external_ip), } - with bittensor.__console__.status(":satellite: Checking Prometheus..."): + with bt_console.status(":satellite: Checking Prometheus..."): neuron = subtensor.get_neuron_for_pubkey_and_subnet( wallet.hotkey.ss58_address, netuid=netuid ) @@ -93,7 +89,7 @@ def prometheus_extrinsic( } if neuron_up_to_date: - bittensor.__console__.print( + bt_console.print( f":white_heavy_check_mark: [green]Prometheus already Served[/green]\n" f"[green not bold]- Status: [/green not bold] |" f"[green not bold] ip: [/green not bold][white not bold]{net.int_to_ip(neuron.prometheus_info.ip)}[/white not bold] |" @@ -102,7 +98,7 @@ def prometheus_extrinsic( f"[green not bold] version: [/green not bold][white not bold]{neuron.prometheus_info.version}[/white not bold] |" ) - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [white]Prometheus already served.[/white]".format( external_ip ) @@ -112,7 +108,7 @@ def prometheus_extrinsic( # Add netuid, not in prometheus_info call_params["netuid"] = netuid - with bittensor.__console__.status( + with bt_console.status( ":satellite: Serving prometheus on: [white]{}:{}[/white] ...".format( subtensor.network, netuid ) @@ -126,14 +122,14 @@ def prometheus_extrinsic( if wait_for_inclusion or wait_for_finalization: if success is True: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Served prometheus[/green]\n [bold white]{}[/bold white]".format( json.dumps(call_params, indent=4, sort_keys=True) ) ) return True else: - bittensor.__console__.print(f":cross_mark: [red]Failed[/red]: {err}") + bt_console.print(f":cross_mark: [red]Failed[/red]: {err}") return False else: return True diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index e4c9d5dfd..6b45b7fc2 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -20,21 +20,21 @@ import pytest from bittensor_wallet import Wallet -import bittensor from bittensor.api.extrinsics.prometheus import prometheus_extrinsic from bittensor.core.subtensor import Subtensor +from bittensor.core.settings import version_as_int # Mocking the bittensor and networking modules @pytest.fixture def mock_bittensor(): - with patch("bittensor.subtensor") as mock: + with patch("bittensor.core.subtensor.Subtensor") as mock: yield mock @pytest.fixture def mock_wallet(): - with patch("bittensor.wallet") as mock: + with patch("bittensor_wallet.Wallet") as mock: yield mock @@ -74,7 +74,7 @@ def test_prometheus_extrinsic_happy_path( mock_net.ip_version.return_value = 4 neuron = MagicMock() neuron.is_null = False - neuron.prometheus_info.version = bittensor.__version_as_int__ + neuron.prometheus_info.version = version_as_int neuron.prometheus_info.ip = 3232235521 neuron.prometheus_info.port = port neuron.prometheus_info.ip_type = 4 From 6bd0647ba2605c8ea15e448d477566a4e21e54fe Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 2 Aug 2024 10:36:17 -0700 Subject: [PATCH 052/260] Axon refactoring --- bittensor/core/axon.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 39a8c1dd4..2f914da9e 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -55,7 +55,6 @@ UnknownSynapseError, ) from bittensor.core.settings import defaults, version_as_int -from bittensor.core.subtensor import Subtensor from bittensor.core.synapse import Synapse, TerminalInfo from bittensor.core.threadpool import PriorityThreadPoolExecutor from bittensor.utils import networking From 856be50bc8ac04853b4b220f30872b13ffa35fde Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 2 Aug 2024 10:46:50 -0700 Subject: [PATCH 053/260] Move subnets.py into bittensor/utils since this module is not used within the bittensor package, but is actively used by the community. --- bittensor/{core => utils}/subnets.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) rename bittensor/{core => utils}/subnets.py (81%) diff --git a/bittensor/core/subnets.py b/bittensor/utils/subnets.py similarity index 81% rename from bittensor/core/subnets.py rename to bittensor/utils/subnets.py index 8bb79c131..e4dae0b23 100644 --- a/bittensor/core/subnets.py +++ b/bittensor/utils/subnets.py @@ -15,15 +15,22 @@ # 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 bittensor as bt from abc import ABC, abstractmethod from typing import Any, List, Union, Optional +from bittensor_wallet import Wallet + +from bittensor.core.axon import Axon +from bittensor.core.dendrite import Dendrite +from bittensor.core.synapse import Synapse +from bittensor.utils.btlogging import logging + class SubnetsAPI(ABC): - def __init__(self, wallet: "bt.wallet"): + """This class is not used within the bittensor package, but is actively used by the community.""" + def __init__(self, wallet: "Wallet"): self.wallet = wallet - self.dendrite = bt.dendrite(wallet=wallet) + self.dendrite = Dendrite(wallet=wallet) async def __call__(self, *args, **kwargs): return await self.query_api(*args, **kwargs) @@ -36,7 +43,7 @@ def prepare_synapse(self, *args, **kwargs) -> Any: ... @abstractmethod - def process_responses(self, responses: List[Union["bt.Synapse", Any]]) -> Any: + def process_responses(self, responses: List[Union["Synapse", Any]]) -> Any: """ Process the responses from the network. """ @@ -44,7 +51,7 @@ def process_responses(self, responses: List[Union["bt.Synapse", Any]]) -> Any: async def query_api( self, - axons: Union["bt.Axon", List["bt.Axon"]], + axons: Union["Axon", List["Axon"]], deserialize: Optional[bool] = False, timeout: Optional[int] = 12, **kwargs: Optional[Any], @@ -62,7 +69,7 @@ async def query_api( Any: The result of the process_responses_fn. """ synapse = self.prepare_synapse(**kwargs) - bt.logging.debug(f"Querying validator axons with synapse {synapse.name}...") + logging.debug(f"Querying validator axons with synapse {synapse.name}...") responses = await self.dendrite( axons=axons, synapse=synapse, From a859e947eda3298cad1f12921ff585068a7f0c80 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 2 Aug 2024 11:17:35 -0700 Subject: [PATCH 054/260] Make synapse.py independent of bittensor import. Fix synapse tests. --- bittensor/core/synapse.py | 25 ++++++++++++----------- tests/unit_tests/test_synapse.py | 35 +++++++++++++++++--------------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/bittensor/core/synapse.py b/bittensor/core/synapse.py index cf74be0c8..e252edf3b 100644 --- a/bittensor/core/synapse.py +++ b/bittensor/core/synapse.py @@ -15,11 +15,11 @@ # 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 base64 import json import sys import warnings +from typing import Any, ClassVar, Dict, Optional, Tuple, Union from pydantic import ( BaseModel, @@ -28,8 +28,9 @@ field_validator, model_validator, ) -import bittensor -from typing import Optional, Any, Dict, ClassVar, Tuple + +from bittensor.utils import get_hash +from bittensor.utils.btlogging import logging def get_size(obj, seen=None) -> int: @@ -455,7 +456,7 @@ def set_name_type(cls, values) -> dict: dendrite: Optional[TerminalInfo] = Field( title="dendrite", description="Dendrite Terminal Information", - examples=["bittensor.TerminalInfo"], + examples=["TerminalInfo"], default=TerminalInfo(), frozen=False, repr=False, @@ -465,7 +466,7 @@ def set_name_type(cls, values) -> dict: axon: Optional[TerminalInfo] = Field( title="axon", description="Axon Terminal Information", - examples=["bittensor.TerminalInfo"], + examples=["TerminalInfo"], default=TerminalInfo(), frozen=False, repr=False, @@ -716,9 +717,9 @@ def body_hash(self) -> str: if required_hash_fields: instance_fields = instance_fields or self.model_dump() for field in required_hash_fields: - hashes.append(bittensor.utils.hash(str(instance_fields[field]))) + hashes.append(get_hash(str(instance_fields[field]))) - return bittensor.utils.hash("".join(hashes)) + return get_hash("".join(hashes)) @classmethod def parse_headers_to_inputs(cls, headers: dict) -> dict: @@ -755,7 +756,7 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: """ # Initialize the input dictionary with empty sub-dictionaries for 'axon' and 'dendrite' - inputs_dict: Dict[str, Dict[str, str]] = {"axon": {}, "dendrite": {}} + inputs_dict: Dict[str, Union[Dict, Optional[str]]] = {"axon": {}, "dendrite": {}} # Iterate over each item in the headers for key, value in headers.items(): @@ -765,7 +766,7 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: new_key = key.split("bt_header_axon_")[1] inputs_dict["axon"][new_key] = value except Exception as e: - bittensor.logging.error( + logging.error( f"Error while parsing 'axon' header {key}: {e}" ) continue @@ -775,7 +776,7 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: new_key = key.split("bt_header_dendrite_")[1] inputs_dict["dendrite"][new_key] = value except Exception as e: - bittensor.logging.error( + logging.error( f"Error while parsing 'dendrite' header {key}: {e}" ) continue @@ -791,12 +792,12 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: base64.b64decode(value.encode()).decode("utf-8") ) except json.JSONDecodeError as e: - bittensor.logging.error( + logging.error( f"Error while json decoding 'input_obj' header {key}: {e}" ) continue except Exception as e: - bittensor.logging.error( + logging.error( f"Error while parsing 'input_obj' header {key}: {e}" ) continue diff --git a/tests/unit_tests/test_synapse.py b/tests/unit_tests/test_synapse.py index b0ce4f132..80c127c58 100644 --- a/tests/unit_tests/test_synapse.py +++ b/tests/unit_tests/test_synapse.py @@ -1,28 +1,31 @@ # The MIT License (MIT) -# Copyright © 2022 Opentensor Foundation - +# 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 json + import base64 -import pytest -import bittensor +import json from typing import Optional, ClassVar +import pytest + +from bittensor.core.synapse import Synapse + def test_parse_headers_to_inputs(): - class Test(bittensor.Synapse): + class Test(Synapse): key1: list[int] # Define a mock headers dictionary to use for testing @@ -57,7 +60,7 @@ class Test(bittensor.Synapse): def test_from_headers(): - class Test(bittensor.Synapse): + class Test(Synapse): key1: list[int] # Define a mock headers dictionary to use for testing @@ -93,10 +96,10 @@ class Test(bittensor.Synapse): def test_synapse_create(): # Create an instance of Synapse - synapse = bittensor.Synapse() + synapse = Synapse() # Ensure the instance created is of type Synapse - assert isinstance(synapse, bittensor.Synapse) + assert isinstance(synapse, Synapse) # Check default properties of a newly created Synapse assert synapse.name == "Synapse" @@ -125,7 +128,7 @@ def test_synapse_create(): def test_custom_synapse(): # Define a custom Synapse subclass - class Test(bittensor.Synapse): + class Test(Synapse): a: int # Carried through because required. b: int = None # Not carried through headers c: Optional[int] # Required, carried through headers, cannot be None @@ -177,7 +180,7 @@ class Test(bittensor.Synapse): def test_body_hash_override(): # Create a Synapse instance - synapse_instance = bittensor.Synapse() + synapse_instance = Synapse() # Try to set the body_hash property and expect an AttributeError with pytest.raises( @@ -188,7 +191,7 @@ def test_body_hash_override(): def test_default_instance_fields_dict_consistency(): - synapse_instance = bittensor.Synapse() + synapse_instance = Synapse() assert synapse_instance.model_dump() == { "name": "Synapse", "timeout": 12.0, @@ -222,7 +225,7 @@ def test_default_instance_fields_dict_consistency(): } -class LegacyHashedSynapse(bittensor.Synapse): +class LegacyHashedSynapse(Synapse): """Legacy Synapse subclass that serialized `required_hash_fields`.""" a: int @@ -232,7 +235,7 @@ class LegacyHashedSynapse(bittensor.Synapse): required_hash_fields: Optional[list[str]] = ["b", "a", "d"] -class HashedSynapse(bittensor.Synapse): +class HashedSynapse(Synapse): a: int b: int c: Optional[int] = None From dc394d80fdcb2a3d9a0cc2cb3e62edfd221159a9 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 2 Aug 2024 11:31:22 -0700 Subject: [PATCH 055/260] Make tensor.py independent of bittensor import. Fix tensor tests. --- tests/unit_tests/test_tensor.py | 106 ++++++++++++++++---------------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/tests/unit_tests/test_tensor.py b/tests/unit_tests/test_tensor.py index 9939b397e..8c189c6af 100644 --- a/tests/unit_tests/test_tensor.py +++ b/tests/unit_tests/test_tensor.py @@ -1,25 +1,27 @@ # The MIT License (MIT) -# Copyright © 2022 Opentensor Foundation - +# 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 pytest -import numpy as np -import bittensor + import numpy +import numpy as np +import pytest import torch +from bittensor.core.tensor import tensor as tensor_class, Tensor + # This is a fixture that creates an example tensor for testing @pytest.fixture @@ -28,16 +30,16 @@ def example_tensor(): data = np.array([1, 2, 3, 4]) # Serialize the tensor into a Tensor instance and return it - return bittensor.tensor(data) + return tensor_class(data) @pytest.fixture -def example_tensor_torch(force_legacy_torch_compat_api): +def example_tensor_torch(force_legacy_torch_compatible_api): # Create a tensor from a list using PyTorch data = torch.tensor([1, 2, 3, 4]) # Serialize the tensor into a Tensor instance and return it - return bittensor.tensor(data) + return tensor_class(data) def test_deserialize(example_tensor): @@ -49,7 +51,7 @@ def test_deserialize(example_tensor): assert tensor.tolist() == [1, 2, 3, 4] -def test_deserialize_torch(example_tensor_torch, force_legacy_torch_compat_api): +def test_deserialize_torch(example_tensor_torch, force_legacy_torch_compatible_api): tensor = example_tensor_torch.deserialize() # Check that the result is a PyTorch tensor with the correct values assert isinstance(tensor, torch.Tensor) @@ -58,7 +60,7 @@ def test_deserialize_torch(example_tensor_torch, force_legacy_torch_compat_api): def test_serialize(example_tensor): # Check that the serialized tensor is an instance of Tensor - assert isinstance(example_tensor, bittensor.Tensor) + assert isinstance(example_tensor, Tensor) # Check that the Tensor instance has the correct buffer, dtype, and shape assert example_tensor.buffer == example_tensor.buffer @@ -87,9 +89,9 @@ def test_serialize(example_tensor): assert example_tensor.shape == example_tensor.shape -def test_serialize_torch(example_tensor_torch, force_legacy_torch_compat_api): +def test_serialize_torch(example_tensor_torch, force_legacy_torch_compatible_api): # Check that the serialized tensor is an instance of Tensor - assert isinstance(example_tensor_torch, bittensor.Tensor) + assert isinstance(example_tensor_torch, Tensor) # Check that the Tensor instance has the correct buffer, dtype, and shape assert example_tensor_torch.buffer == example_tensor_torch.buffer @@ -120,7 +122,7 @@ def test_serialize_torch(example_tensor_torch, force_legacy_torch_compat_api): def test_buffer_field(): # Create a Tensor instance with a specified buffer, dtype, and shape - tensor = bittensor.Tensor( + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="float32", shape=[3, 3] ) @@ -128,9 +130,9 @@ def test_buffer_field(): assert tensor.buffer == "0x321e13edqwds231231231232131" -def test_buffer_field_torch(force_legacy_torch_compat_api): +def test_buffer_field_torch(force_legacy_torch_compatible_api): # Create a Tensor instance with a specified buffer, dtype, and shape - tensor = bittensor.Tensor( + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="torch.float32", shape=[3, 3] ) @@ -140,7 +142,7 @@ def test_buffer_field_torch(force_legacy_torch_compat_api): def test_dtype_field(): # Create a Tensor instance with a specified buffer, dtype, and shape - tensor = bittensor.Tensor( + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="float32", shape=[3, 3] ) @@ -148,8 +150,8 @@ def test_dtype_field(): assert tensor.dtype == "float32" -def test_dtype_field_torch(force_legacy_torch_compat_api): - tensor = bittensor.Tensor( +def test_dtype_field_torch(force_legacy_torch_compatible_api): + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="torch.float32", shape=[3, 3] ) assert tensor.dtype == "torch.float32" @@ -157,7 +159,7 @@ def test_dtype_field_torch(force_legacy_torch_compat_api): def test_shape_field(): # Create a Tensor instance with a specified buffer, dtype, and shape - tensor = bittensor.Tensor( + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="float32", shape=[3, 3] ) @@ -165,79 +167,79 @@ def test_shape_field(): assert tensor.shape == [3, 3] -def test_shape_field_torch(force_legacy_torch_compat_api): - tensor = bittensor.Tensor( +def test_shape_field_torch(force_legacy_torch_compatible_api): + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="torch.float32", shape=[3, 3] ) assert tensor.shape == [3, 3] def test_serialize_all_types(): - bittensor.tensor(np.array([1], dtype=np.float16)) - bittensor.tensor(np.array([1], dtype=np.float32)) - bittensor.tensor(np.array([1], dtype=np.float64)) - bittensor.tensor(np.array([1], dtype=np.uint8)) - bittensor.tensor(np.array([1], dtype=np.int32)) - bittensor.tensor(np.array([1], dtype=np.int64)) - bittensor.tensor(np.array([1], dtype=bool)) + tensor_class(np.array([1], dtype=np.float16)) + tensor_class(np.array([1], dtype=np.float32)) + tensor_class(np.array([1], dtype=np.float64)) + tensor_class(np.array([1], dtype=np.uint8)) + tensor_class(np.array([1], dtype=np.int32)) + tensor_class(np.array([1], dtype=np.int64)) + tensor_class(np.array([1], dtype=bool)) -def test_serialize_all_types_torch(force_legacy_torch_compat_api): - bittensor.tensor(torch.tensor([1], dtype=torch.float16)) - bittensor.tensor(torch.tensor([1], dtype=torch.float32)) - bittensor.tensor(torch.tensor([1], dtype=torch.float64)) - bittensor.tensor(torch.tensor([1], dtype=torch.uint8)) - bittensor.tensor(torch.tensor([1], dtype=torch.int32)) - bittensor.tensor(torch.tensor([1], dtype=torch.int64)) - bittensor.tensor(torch.tensor([1], dtype=torch.bool)) +def test_serialize_all_types_torch(force_legacy_torch_compatible_api): + tensor_class(torch.tensor([1], dtype=torch.float16)) + tensor_class(torch.tensor([1], dtype=torch.float32)) + tensor_class(torch.tensor([1], dtype=torch.float64)) + tensor_class(torch.tensor([1], dtype=torch.uint8)) + tensor_class(torch.tensor([1], dtype=torch.int32)) + tensor_class(torch.tensor([1], dtype=torch.int64)) + tensor_class(torch.tensor([1], dtype=torch.bool)) def test_serialize_all_types_equality(): rng = np.random.default_rng() tensor = rng.standard_normal((100,), dtype=np.float32) - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(tensor_class(tensor).tensor() == tensor) tensor = rng.standard_normal((100,), dtype=np.float64) - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(tensor_class(tensor).tensor() == tensor) tensor = np.random.randint(255, 256, (1000,), dtype=np.uint8) - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(tensor_class(tensor).tensor() == tensor) tensor = np.random.randint(2_147_483_646, 2_147_483_647, (1000,), dtype=np.int32) - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(tensor_class(tensor).tensor() == tensor) tensor = np.random.randint( 9_223_372_036_854_775_806, 9_223_372_036_854_775_807, (1000,), dtype=np.int64 ) - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(tensor_class(tensor).tensor() == tensor) tensor = rng.standard_normal((100,), dtype=np.float32) < 0.5 - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(tensor_class(tensor).tensor() == tensor) -def test_serialize_all_types_equality_torch(force_legacy_torch_compat_api): +def test_serialize_all_types_equality_torch(force_legacy_torch_compatible_api): torchtensor = torch.randn([100], dtype=torch.float16) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) torchtensor = torch.randn([100], dtype=torch.float32) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) torchtensor = torch.randn([100], dtype=torch.float64) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) torchtensor = torch.randint(255, 256, (1000,), dtype=torch.uint8) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) torchtensor = torch.randint( 2_147_483_646, 2_147_483_647, (1000,), dtype=torch.int32 ) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) torchtensor = torch.randint( 9_223_372_036_854_775_806, 9_223_372_036_854_775_807, (1000,), dtype=torch.int64 ) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) torchtensor = torch.randn([100], dtype=torch.float32) < 0.5 - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) From 6db9a38c721931f512e43c57f8766c32d848bfb3 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 2 Aug 2024 11:35:21 -0700 Subject: [PATCH 056/260] Change name `bittensor.utils.hash` -> `bittensor.utils.get_hash` --- bittensor/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 4971abc85..d7e16217e 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -250,7 +250,7 @@ def u8_key_to_ss58(u8_key: List[int]) -> str: return scalecodec.ss58_encode(bytes(u8_key).hex(), ss58_format) -def hash(content, encoding="utf-8"): +def get_hash(content, encoding="utf-8"): sha3 = hashlib.sha3_256() # Update the hash object with the concatenated string From a23ff8c77927c7368b9257f1f7392785f8355f80 Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 4 Aug 2024 20:05:12 -0700 Subject: [PATCH 057/260] Moved from using `import bittensor` (withing bittensor codebase) to direct imports of modules and other objects. It helped to avoid namespace conflicts. --- bittensor/__init__.py | 170 +----------- bittensor/api/extrinsics/commit_weights.py | 20 +- bittensor/api/extrinsics/delegation.py | 176 +++++++------ bittensor/api/extrinsics/network.py | 27 +- bittensor/api/extrinsics/registration.py | 90 ++++--- bittensor/api/extrinsics/root.py | 64 ++--- bittensor/api/extrinsics/senate.py | 70 ++--- bittensor/api/extrinsics/serving.py | 125 ++++----- bittensor/api/extrinsics/set_weights.py | 32 +-- bittensor/api/extrinsics/transfer.py | 42 +-- bittensor/api/extrinsics/unstaking.py | 93 +++---- bittensor/api/extrinsics/utils.py | 41 +++ bittensor/btcli/cli.py | 44 ++-- .../btcli/commands/check_coldkey_swap.py | 33 +-- bittensor/btcli/commands/delegates.py | 217 ++++++++-------- bittensor/btcli/commands/identity.py | 74 +++--- bittensor/btcli/commands/inspect.py | 48 ++-- bittensor/btcli/commands/list.py | 22 +- bittensor/btcli/commands/metagraph.py | 35 +-- bittensor/btcli/commands/misc.py | 24 +- bittensor/btcli/commands/network.py | 168 ++++++------ bittensor/btcli/commands/overview.py | 89 +++---- bittensor/btcli/commands/register.py | 91 +++---- bittensor/btcli/commands/root.py | 121 ++++----- bittensor/btcli/commands/senate.py | 97 +++---- bittensor/btcli/commands/stake.py | 79 +++--- bittensor/btcli/commands/transfer.py | 45 ++-- bittensor/btcli/commands/unstake.py | 47 ++-- bittensor/btcli/commands/utils.py | 41 +-- bittensor/btcli/commands/wallets.py | 126 ++++----- bittensor/btcli/commands/weights.py | 60 +++-- bittensor/core/config.py | 134 +++++----- bittensor/core/dendrite.py | 6 +- bittensor/core/errors.py | 9 +- bittensor/core/metagraph.py | 28 +- bittensor/core/settings.py | 2 + bittensor/core/threadpool.py | 31 +-- bittensor/mock/subtensor_mock.py | 8 +- bittensor/utils/__init__.py | 3 +- bittensor/utils/backwards_compatibility.py | 143 +++++++++++ bittensor/utils/registration.py | 241 ++++++++---------- bittensor/utils/test_utils.py | 22 -- bittensor/utils/version.py | 18 ++ bittensor/utils/weight_utils.py | 146 +++++------ tests/helpers/helpers.py | 9 +- tests/integration_tests/test_cli.py | 189 +++++++------- .../integration_tests/test_cli_no_network.py | 28 +- tests/unit_tests/conftest.py | 4 +- tests/unit_tests/extrinsics/test_root.py | 8 +- tests/unit_tests/extrinsics/test_senate.py | 11 +- .../unit_tests/extrinsics/test_set_weights.py | 8 +- tests/unit_tests/extrinsics/test_unstaking.py | 17 +- tests/unit_tests/test_chain_data.py | 4 +- tests/unit_tests/test_metagraph.py | 7 +- tests/unit_tests/test_overview.py | 25 +- tests/unit_tests/utils/test_balance.py | 2 +- tests/unit_tests/utils/test_registration.py | 19 +- tests/unit_tests/utils/test_version.py | 41 +-- tests/unit_tests/utils/test_weight_utils.py | 12 +- 59 files changed, 1812 insertions(+), 1774 deletions(-) create mode 100644 bittensor/api/extrinsics/utils.py create mode 100644 bittensor/utils/backwards_compatibility.py delete mode 100644 bittensor/utils/test_utils.py diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 50f311e3c..f4717dd25 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -14,164 +14,25 @@ # 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. -# Bittensor code and protocol version. - -__version__ = "7.3.0" - -_version_split = __version__.split(".") -__version_info__ = tuple(int(part) for part in _version_split) -_version_int_base = 1000 -assert max(__version_info__) < _version_int_base - -__version_as_int__: int = sum(e * (_version_int_base**i) for i, e in enumerate(reversed(__version_info__))) -assert __version_as_int__ < 2 ** 31 # fits in int32 import os import warnings -from bittensor_wallet.errors import KeyFileError # noqa: F401 -from bittensor_wallet.keyfile import ( # noqa: F401 - 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, -) -from bittensor_wallet.wallet import display_mnemonic_msg, Wallet # noqa: F401 -from rich.console import Console -from rich.traceback import install -from substrateinterface import Keypair # noqa: F401 - -from .btcli.cli import cli as cli, COMMANDS as ALL_COMMANDS -from .core import settings -from .core.axon import Axon -from .core.chain_data import ( - AxonInfo, - NeuronInfo, - NeuronInfoLite, - PrometheusInfo, - DelegateInfo, - StakeInfo, - SubnetInfo, - SubnetHyperparameters, - IPInfo, - ProposalCallData, - ProposalVoteData, -) -from .core.config import ( # noqa: F401 - InvalidConfigFile, - DefaultConfig, - Config, - T, -) -from .core.dendrite import dendrite as dendrite -from .core.errors import ( - BlacklistedException, - ChainConnectionError, - ChainError, - ChainQueryError, - ChainTransactionError, - IdentityError, - InternalServerError, - InvalidRequestNameError, - MetadataError, - NominationError, - NotDelegateError, - NotRegisteredError, - NotVerifiedException, - PostProcessException, - PriorityException, - RegistrationError, - RunException, - StakeError, - SynapseDendriteNoneException, - SynapseParsingError, - TransferError, - UnknownSynapseError, - UnstakeError, -) -from .core.metagraph import metagraph as metagraph -from .core.settings import blocktime -from .core.stream import StreamingSynapse -from .core.subnets import SubnetsAPI as SubnetsAPI -from .core.subtensor import Subtensor -from .core.synapse import TerminalInfo, Synapse -from .core.tensor import tensor, Tensor -from .core.threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor -from .mock.subtensor_mock import MockSubtensor as MockSubtensor -from .utils import ( - ss58_to_vec_u8, - unbiased_topk, - version_checking, - strtobool, - strtobool_with_default, - get_explorer_root_url_by_network_from_map, - get_explorer_root_url_by_network_from_map, - get_explorer_url_for_network, - ss58_address_to_bytes, - U16_NORMALIZED_FLOAT, - U64_NORMALIZED_FLOAT, - u8_key_to_ss58, - hash, - wallet_utils, -) -from .utils.balance import Balance as Balance +from .core.settings import __version__, version_split, defaults +from .utils.backwards_compatibility import * from .utils.btlogging import logging -configs = [ - Axon.config(), - Subtensor.config(), - PriorityThreadPoolExecutor.config(), - Wallet.config(), - logging.get_config(), -] -defaults = Config.merge_all(configs) - - def __getattr__(name): if name == "version_split": warnings.warn( "version_split is deprecated and will be removed in future versions. Use __version__ instead.", DeprecationWarning, ) - return _version_split + return version_split raise AttributeError(f"module {__name__} has no attribute {name}") -# Rich console. -__console__ = Console() -__use_console__ = True - -# Remove overdue locals in debug training. -install(show_locals=False) - - -def turn_console_off(): - global __use_console__ - global __console__ - from io import StringIO - - __use_console__ = False - __console__ = Console(file=StringIO(), stderr=False) - - -def turn_console_on(): - global __use_console__ - global __console__ - __use_console__ = True - __console__ = Console() - - # Logging helpers. def trace(on: bool = True): logging.set_trace(on) @@ -181,9 +42,6 @@ def debug(on: bool = True): logging.set_debug(on) -turn_console_off() - - def __apply_nest_asyncio(): """ Apply nest_asyncio if the environment variable NEST_ASYNCIO is set to "1" or not set. @@ -207,26 +65,4 @@ def __apply_nest_asyncio(): __apply_nest_asyncio() -# Backwards compatibility with previous bittensor versions. -axon = Axon -config = Config -keyfile = Keyfile -wallet = Wallet -subtensor = Subtensor - -__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 diff --git a/bittensor/api/extrinsics/commit_weights.py b/bittensor/api/extrinsics/commit_weights.py index 90cb2474c..0078af973 100644 --- a/bittensor/api/extrinsics/commit_weights.py +++ b/bittensor/api/extrinsics/commit_weights.py @@ -19,16 +19,16 @@ from typing import Tuple, List +from bittensor_wallet import Wallet from rich.prompt import Confirm -import bittensor - from bittensor.utils import format_error_message +from bittensor.utils.btlogging import logging def commit_weights_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", netuid: int, commit_hash: str, wait_for_inclusion: bool = False, @@ -64,16 +64,16 @@ def commit_weights_extrinsic( ) if success: - bittensor.logging.info("Successfully committed weights.") + logging.info("Successfully committed weights.") return True, "Successfully committed weights." else: - bittensor.logging.error(f"Failed to commit weights: {error_message}") + logging.error(f"Failed to commit weights: {error_message}") return False, format_error_message(error_message) def reveal_weights_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", netuid: int, uids: List[int], weights: List[int], @@ -119,8 +119,8 @@ def reveal_weights_extrinsic( ) if success: - bittensor.logging.info("Successfully revealed weights.") + logging.info("Successfully revealed weights.") return True, "Successfully revealed weights." else: - bittensor.logging.error(f"Failed to reveal weights: {error_message}") + logging.error(f"Failed to reveal weights: {error_message}") return False, error_message diff --git a/bittensor/api/extrinsics/delegation.py b/bittensor/api/extrinsics/delegation.py index e981a0dd6..81f3d89c9 100644 --- a/bittensor/api/extrinsics/delegation.py +++ b/bittensor/api/extrinsics/delegation.py @@ -15,8 +15,11 @@ # 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 logging -import bittensor +from typing import Union, Optional + +from bittensor_wallet import Wallet +from rich.prompt import Confirm + from bittensor.core.errors import ( NominationError, NotDelegateError, @@ -24,24 +27,27 @@ StakeError, TakeError, ) -from rich.prompt import Confirm -from typing import Union, Optional +from bittensor.core.settings import bt_console from bittensor.utils.balance import Balance -from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME - -logger = logging.getLogger(BITTENSOR_LOGGER_NAME) +from bittensor.utils.btlogging import logging +import typing +if typing.TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor def nominate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", wait_for_finalization: bool = False, wait_for_inclusion: bool = True, ) -> bool: - r"""Becomes a delegate for the hotkey. + """Becomes a delegate for the hotkey. Args: - wallet (bittensor.wallet): The wallet to become a delegate for. + wait_for_inclusion: + wait_for_finalization: + subtensor: + wallet (Wallet): The wallet to become a delegate for. Returns: success (bool): ``True`` if the transaction was successful. """ @@ -51,12 +57,12 @@ def nominate_extrinsic( # Check if the hotkey is already a delegate. if subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - logger.error( + logging.error( "Hotkey {} is already a delegate.".format(wallet.hotkey.ss58_address) ) return False - with bittensor.__console__.status( + with bt_console.status( ":satellite: Sending nominate call on [white]{}[/white] ...".format( subtensor.network ) @@ -69,10 +75,10 @@ def nominate_extrinsic( ) if success is True: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - bittensor.logging.success( + logging.success( prefix="Become Delegate", suffix="Finalized: " + str(success), ) @@ -81,17 +87,17 @@ def nominate_extrinsic( return success except Exception as e: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: error:{}".format(e) ) - bittensor.logging.warning( + logging.warning( prefix="Set weights", suffix="Failed: " + str(e) ) except NominationError as e: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: error:{}".format(e) ) - bittensor.logging.warning( + logging.warning( prefix="Set weights", suffix="Failed: " + str(e) ) @@ -99,18 +105,19 @@ def nominate_extrinsic( def delegate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", delegate_ss58: Optional[str] = None, amount: Optional[Union[Balance, float]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, ) -> bool: - r"""Delegates the specified amount of stake to the passed delegate. + """Delegates the specified amount of stake to the passed delegate. Args: - wallet (bittensor.wallet): Bittensor wallet object. + subtensor: + wallet (Wallet): Bittensor wallet object. delegate_ss58 (Optional[str]): The ``ss58`` address of the delegate. amount (Union[Balance, float]): Amount to stake as bittensor balance, or ``float`` interpreted as Tao. 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. @@ -136,24 +143,24 @@ def delegate_extrinsic( coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=delegate_ss58 ) - # Convert to bittensor.Balance + # Convert to Balance if amount is None: # Stake it all. - staking_balance = bittensor.Balance.from_tao(my_prev_coldkey_balance.tao) - elif not isinstance(amount, bittensor.Balance): - staking_balance = bittensor.Balance.from_tao(amount) + staking_balance = Balance.from_tao(my_prev_coldkey_balance.tao) + elif not isinstance(amount, Balance): + staking_balance = Balance.from_tao(amount) else: staking_balance = amount # Remove existential balance to keep key alive. - if staking_balance > bittensor.Balance.from_rao(1000): - staking_balance = staking_balance - bittensor.Balance.from_rao(1000) + if staking_balance > Balance.from_rao(1000): + staking_balance = staking_balance - Balance.from_rao(1000) else: staking_balance = staking_balance # Check enough balance to stake. if staking_balance > my_prev_coldkey_balance: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Not enough balance[/red]:[bold white]\n balance:{}\n amount: {}\n coldkey: {}[/bold white]".format( my_prev_coldkey_balance, staking_balance, wallet.name ) @@ -170,7 +177,7 @@ def delegate_extrinsic( return False try: - with bittensor.__console__.status( + with bt_console.status( ":satellite: Staking to: [bold white]{}[/bold white] ...".format( subtensor.network ) @@ -188,10 +195,10 @@ def delegate_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - with bittensor.__console__.status( + with bt_console.status( ":satellite: Checking Balance on: [white]{}[/white] ...".format( subtensor.network ) @@ -204,48 +211,49 @@ def delegate_extrinsic( block=block, ) # Get current stake - bittensor.__console__.print( + bt_console.print( "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( my_prev_coldkey_balance, new_balance ) ) - bittensor.__console__.print( + bt_console.print( "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( my_prev_delegated_stake, new_delegate_stake ) ) return True else: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: Error unknown." ) return False - except NotRegisteredError as e: - bittensor.__console__.print( + except NotRegisteredError: + bt_console.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( wallet.hotkey_str ) ) return False except StakeError as e: - bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) + bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) return False def undelegate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", delegate_ss58: Optional[str] = None, amount: Optional[Union[Balance, float]] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, ) -> bool: - r"""Un-delegates stake from the passed delegate. + """Un-delegates stake from the passed delegate. Args: - wallet (bittensor.wallet): Bittensor wallet object. + subtensor: + wallet (Wallet): Bittensor wallet object. delegate_ss58 (Optional[str]): The ``ss58`` address of the delegate. amount (Union[Balance, float]): Amount to unstake as bittensor balance, or ``float`` interpreted as Tao. 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. @@ -271,20 +279,20 @@ def undelegate_extrinsic( coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=delegate_ss58 ) - # Convert to bittensor.Balance + # Convert to Balance if amount is None: # Stake it all. - unstaking_balance = bittensor.Balance.from_tao(my_prev_delegated_stake.tao) + unstaking_balance = Balance.from_tao(my_prev_delegated_stake.tao) - elif not isinstance(amount, bittensor.Balance): - unstaking_balance = bittensor.Balance.from_tao(amount) + elif not isinstance(amount, Balance): + unstaking_balance = Balance.from_tao(amount) else: unstaking_balance = amount # Check enough stake to unstake. if unstaking_balance > my_prev_delegated_stake: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Not enough delegated stake[/red]:[bold white]\n stake:{}\n amount: {}\n coldkey: {}[/bold white]".format( my_prev_delegated_stake, unstaking_balance, wallet.name ) @@ -301,7 +309,7 @@ def undelegate_extrinsic( return False try: - with bittensor.__console__.status( + with bt_console.status( ":satellite: Unstaking from: [bold white]{}[/bold white] ...".format( subtensor.network ) @@ -319,10 +327,10 @@ def undelegate_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - with bittensor.__console__.status( + with bt_console.status( ":satellite: Checking Balance on: [white]{}[/white] ...".format( subtensor.network ) @@ -335,47 +343,50 @@ def undelegate_extrinsic( block=block, ) # Get current stake - bittensor.__console__.print( + bt_console.print( "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( my_prev_coldkey_balance, new_balance ) ) - bittensor.__console__.print( + bt_console.print( "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( my_prev_delegated_stake, new_delegate_stake ) ) return True else: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: Error unknown." ) return False - except NotRegisteredError as e: - bittensor.__console__.print( + except NotRegisteredError: + bt_console.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( wallet.hotkey_str ) ) return False except StakeError as e: - bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) + bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) return False def decrease_take_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", hotkey_ss58: Optional[str] = None, take: int = 0, wait_for_finalization: bool = False, wait_for_inclusion: bool = True, ) -> bool: - r"""Decrease delegate take for the hotkey. + """Decrease delegate take for the hotkey. Args: - wallet (bittensor.wallet): + wait_for_inclusion: + wait_for_finalization: + subtensor: + wallet (Wallet): Bittensor wallet object. hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. @@ -388,7 +399,7 @@ def decrease_take_extrinsic( wallet.coldkey wallet.hotkey - with bittensor.__console__.status( + with bt_console.status( ":satellite: Sending decrease_take_extrinsic call on [white]{}[/white] ...".format( subtensor.network ) @@ -403,10 +414,10 @@ def decrease_take_extrinsic( ) if success is True: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - bittensor.logging.success( + logging.success( prefix="Decrease Delegate Take", suffix="Finalized: " + str(success), ) @@ -414,10 +425,10 @@ def decrease_take_extrinsic( return success except (TakeError, Exception) as e: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: error:{}".format(e) ) - bittensor.logging.warning( + logging.warning( prefix="Set weights", suffix="Failed: " + str(e) ) @@ -425,22 +436,23 @@ def decrease_take_extrinsic( def increase_take_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", hotkey_ss58: Optional[str] = None, take: int = 0, wait_for_finalization: bool = False, wait_for_inclusion: bool = True, ) -> bool: - r"""Increase delegate take for the hotkey. + """Increase delegate take for the hotkey. Args: - wallet (bittensor.wallet): - Bittensor wallet object. - hotkey_ss58 (Optional[str]): - The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. - take (float): - The ``take`` of the hotkey. + subtensor: + wallet (Wallet): Bittensor wallet object. + hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. + take (float): The ``take`` of the hotkey. + wait_for_inclusion: + wait_for_finalization: + Returns: success (bool): ``True`` if the transaction was successful. """ @@ -448,7 +460,7 @@ def increase_take_extrinsic( wallet.coldkey wallet.hotkey - with bittensor.__console__.status( + with bt_console.status( ":satellite: Sending increase_take_extrinsic call on [white]{}[/white] ...".format( subtensor.network ) @@ -463,10 +475,10 @@ def increase_take_extrinsic( ) if success is True: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - bittensor.logging.success( + logging.success( prefix="Increase Delegate Take", suffix="Finalized: " + str(success), ) @@ -474,17 +486,17 @@ def increase_take_extrinsic( return success except Exception as e: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: error:{}".format(e) ) - bittensor.logging.warning( + logging.warning( prefix="Set weights", suffix="Failed: " + str(e) ) except TakeError as e: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: error:{}".format(e) ) - bittensor.logging.warning( + logging.warning( prefix="Set weights", suffix="Failed: " + str(e) ) diff --git a/bittensor/api/extrinsics/network.py b/bittensor/api/extrinsics/network.py index 095d1199b..03017fe75 100644 --- a/bittensor/api/extrinsics/network.py +++ b/bittensor/api/extrinsics/network.py @@ -20,9 +20,10 @@ import substrateinterface from rich.prompt import Confirm -import bittensor -from bittensor.btcli.commands.network import HYPERPARAMS +from bittensor.api.extrinsics.utils import HYPERPARAMS +from bittensor.core.settings import bt_console from bittensor.utils import format_error_message +from bittensor.utils.balance import Balance def _find_event_attributes_in_extrinsic_receipt( @@ -72,15 +73,15 @@ def register_subnetwork_extrinsic( If we did not wait for finalization / inclusion, the response is ``true``. """ your_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - burn_cost = bittensor.utils.balance.Balance(subtensor.get_subnet_burn_cost()) + burn_cost = Balance(subtensor.get_subnet_burn_cost()) if burn_cost > your_balance: - bittensor.__console__.print( + bt_console.print( f"Your balance of: [green]{your_balance}[/green] is not enough to pay the subnet lock cost of: [green]{burn_cost}[/green]" ) return False if prompt: - bittensor.__console__.print(f"Your balance is: [green]{your_balance}[/green]") + bt_console.print(f"Your balance is: [green]{your_balance}[/green]") if not Confirm.ask( f"Do you want to register a subnet for [green]{ burn_cost }[/green]?" ): @@ -88,7 +89,7 @@ def register_subnetwork_extrinsic( wallet.coldkey # unlock coldkey - with bittensor.__console__.status(":satellite: Registering subnet..."): + with bt_console.status(":satellite: Registering subnet..."): with subtensor.substrate as substrate: # create extrinsic call call = substrate.compose_call( @@ -112,7 +113,7 @@ def register_subnetwork_extrinsic( # process if registration successful response.process_events() if not response.is_success: - bittensor.__console__.print( + bt_console.print( f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" ) time.sleep(0.5) @@ -122,7 +123,7 @@ def register_subnetwork_extrinsic( attributes = _find_event_attributes_in_extrinsic_receipt( response, "NetworkAdded" ) - bittensor.__console__.print( + bt_console.print( f":white_heavy_check_mark: [green]Registered subnetwork with netuid: {attributes[0]}[/green]" ) return True @@ -161,7 +162,7 @@ def set_hyperparameter_extrinsic( If we did not wait for finalization / inclusion, the response is ``true``. """ if subtensor.get_subnet_owner(netuid) != wallet.coldkeypub.ss58_address: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" ) return False @@ -170,12 +171,12 @@ def set_hyperparameter_extrinsic( extrinsic = HYPERPARAMS.get(parameter) if extrinsic is None: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Invalid hyperparameter specified.[/red]" ) return False - with bittensor.__console__.status( + with bt_console.status( f":satellite: Setting hyperparameter {parameter} to {value} on subnet: {netuid} ..." ): with subtensor.substrate as substrate: @@ -230,14 +231,14 @@ def set_hyperparameter_extrinsic( # process if registration successful response.process_events() if not response.is_success: - bittensor.__console__.print( + bt_console.print( f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" ) time.sleep(0.5) # Successful registration, final check for membership else: - bittensor.__console__.print( + bt_console.print( f":white_heavy_check_mark: [green]Hyper parameter {parameter} changed to {value}[/green]" ) return True diff --git a/bittensor/api/extrinsics/registration.py b/bittensor/api/extrinsics/registration.py index 145c05793..f40637a4f 100644 --- a/bittensor/api/extrinsics/registration.py +++ b/bittensor/api/extrinsics/registration.py @@ -20,7 +20,7 @@ from rich.prompt import Confirm -import bittensor + from bittensor.utils import format_error_message from bittensor.utils.registration import ( @@ -29,11 +29,14 @@ torch, log_no_torch_error, ) +from bittensor.core.settings import bt_console +from bittensor.utils.btlogging import logging +from bittensor_wallet import Wallet def register_extrinsic( subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + wallet: "Wallet", netuid: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, @@ -50,7 +53,9 @@ def register_extrinsic( r"""Registers the wallet to the chain. Args: - wallet (bittensor.wallet): + subtensor: + output_in_place: + wallet (Wallet): Bittensor wallet object. netuid (int): The ``netuid`` of the subnet to register on. @@ -79,21 +84,21 @@ def register_extrinsic( Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ if not subtensor.subnet_exists(netuid): - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: error: [bold white]subnet:{}[/bold white] does not exist.".format( netuid ) ) return False - with bittensor.__console__.status( + with bt_console.status( f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]..." ): neuron = subtensor.get_neuron_for_pubkey_and_subnet( wallet.hotkey.ss58_address, netuid=netuid ) if not neuron.is_null: - bittensor.logging.debug( + logging.debug( f"Wallet {wallet} is already registered on {neuron.netuid} with {neuron.uid}" ) return True @@ -115,14 +120,14 @@ def register_extrinsic( # Attempt rolling registration. attempts = 1 while True: - bittensor.__console__.print( + bt_console.print( ":satellite: Registering...({}/{})".format(attempts, max_allowed_attempts) ) # Solve latest POW. if cuda: if not torch.cuda.is_available(): if prompt: - bittensor.__console__.print("CUDA is not available.") + bt_console.print("CUDA is not available.") return False pow_result: Optional[POWSolution] = create_pow( subtensor, @@ -155,14 +160,14 @@ def register_extrinsic( netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address ) if is_registered: - bittensor.__console__.print( + bt_console.print( f":white_heavy_check_mark: [green]Already registered on netuid:{netuid}[/green]" ) return True # pow successful, proceed to submit pow to chain for registration else: - with bittensor.__console__.status(":satellite: Submitting POW..."): + with bt_console.status(":satellite: Submitting POW..."): # check if pow result is still valid while not pow_result.is_stale(subtensor=subtensor): result: Tuple[bool, Optional[str]] = subtensor._do_pow_register( @@ -178,65 +183,66 @@ def register_extrinsic( # Look error here # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs if "HotKeyAlreadyRegisteredInSubNet" in err_msg: - bittensor.__console__.print( + bt_console.print( f":white_heavy_check_mark: [green]Already Registered on [bold]subnet:{netuid}[/bold][/green]" ) return True - bittensor.__console__.print( + bt_console.print( f":cross_mark: [red]Failed[/red]: {err_msg}" ) time.sleep(0.5) # Successful registration, final check for neuron and pubkey else: - bittensor.__console__.print(":satellite: Checking Balance...") + bt_console.print(":satellite: Checking Balance...") is_registered = subtensor.is_hotkey_registered( netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address ) if is_registered: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Registered[/green]" ) return True else: # neuron not found, try again - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) continue else: # Exited loop because pow is no longer valid. - bittensor.__console__.print("[red]POW is stale.[/red]") + bt_console.print("[red]POW is stale.[/red]") # Try again. continue if attempts < max_allowed_attempts: # Failed registration, retry pow attempts += 1 - bittensor.__console__.print( + bt_console.print( ":satellite: Failed registration, retrying pow ...({}/{})".format( attempts, max_allowed_attempts ) ) else: # Failed to register after max attempts. - bittensor.__console__.print("[red]No more attempts.[/red]") + bt_console.print("[red]No more attempts.[/red]") return False def burned_register_extrinsic( subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + wallet: "Wallet", netuid: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - r"""Registers the wallet to chain by recycling TAO. + """Registers the wallet to chain by recycling TAO. Args: - wallet (bittensor.wallet): + subtensor: + wallet (Wallet): Bittensor wallet object. netuid (int): The ``netuid`` of the subnet to register on. @@ -251,7 +257,7 @@ def burned_register_extrinsic( Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ if not subtensor.subnet_exists(netuid): - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: error: [bold white]subnet:{}[/bold white] does not exist.".format( netuid ) @@ -259,7 +265,7 @@ def burned_register_extrinsic( return False wallet.coldkey # unlock coldkey - with bittensor.__console__.status( + with bt_console.status( f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]..." ): neuron = subtensor.get_neuron_for_pubkey_and_subnet( @@ -270,7 +276,7 @@ def burned_register_extrinsic( recycle_amount = subtensor.recycle(netuid=netuid) if not neuron.is_null: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Already Registered[/green]:\n" "uid: [bold white]{}[/bold white]\n" "netuid: [bold white]{}[/bold white]\n" @@ -286,7 +292,7 @@ def burned_register_extrinsic( if not Confirm.ask(f"Recycle {recycle_amount} to register on subnet:{netuid}?"): return False - with bittensor.__console__.status(":satellite: Recycling TAO for Registration..."): + with bt_console.status(":satellite: Recycling TAO for Registration..."): success, err_msg = subtensor._do_burned_register( netuid=netuid, wallet=wallet, @@ -295,18 +301,18 @@ def burned_register_extrinsic( ) if not success: - bittensor.__console__.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") time.sleep(0.5) return False # Successful registration, final check for neuron and pubkey else: - bittensor.__console__.print(":satellite: Checking Balance...") + bt_console.print(":satellite: Checking Balance...") block = subtensor.get_current_block() new_balance = subtensor.get_balance( wallet.coldkeypub.ss58_address, block=block ) - bittensor.__console__.print( + bt_console.print( "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_balance, new_balance ) @@ -315,13 +321,13 @@ def burned_register_extrinsic( netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address ) if is_registered: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Registered[/green]" ) return True else: # neuron not found, try again - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) return False @@ -337,7 +343,7 @@ class MaxAttemptsException(Exception): def run_faucet_extrinsic( subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, @@ -350,10 +356,12 @@ def run_faucet_extrinsic( update_interval: Optional[int] = None, log_verbose: bool = False, ) -> Tuple[bool, str]: - r"""Runs a continual POW to get a faucet of TAO on the test net. + """Runs a continual POW to get a faucet of TAO on the test net. Args: - wallet (bittensor.wallet): + output_in_place: + subtensor: + wallet (Wallet): Bittensor wallet object. prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. @@ -409,7 +417,7 @@ def run_faucet_extrinsic( if cuda: if not torch.cuda.is_available(): if prompt: - bittensor.__console__.print("CUDA is not available.") + bt_console.print("CUDA is not available.") return False, "CUDA is not available." pow_result: Optional[POWSolution] = create_pow( subtensor, @@ -455,7 +463,7 @@ def run_faucet_extrinsic( # process if registration successful, try again if pow is still valid response.process_events() if not response.is_success: - bittensor.__console__.print( + bt_console.print( f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" ) if attempts == max_allowed_attempts: @@ -467,7 +475,7 @@ def run_faucet_extrinsic( # Successful registration else: new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bittensor.__console__.print( + bt_console.print( f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" ) old_balance = new_balance @@ -490,8 +498,8 @@ def run_faucet_extrinsic( def swap_hotkey_extrinsic( subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - new_wallet: "bittensor.wallet", + wallet: "Wallet", + new_wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, @@ -504,7 +512,7 @@ def swap_hotkey_extrinsic( ): return False - with bittensor.__console__.status(":satellite: Swapping hotkeys..."): + with bt_console.status(":satellite: Swapping hotkeys..."): success, err_msg = subtensor._do_swap_hotkey( wallet=wallet, new_wallet=new_wallet, @@ -513,12 +521,12 @@ def swap_hotkey_extrinsic( ) if not success: - bittensor.__console__.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") time.sleep(0.5) return False else: - bittensor.__console__.print( + bt_console.print( f"Hotkey {wallet.hotkey} swapped for new hotkey: {new_wallet.hotkey}" ) return True diff --git a/bittensor/api/extrinsics/root.py b/bittensor/api/extrinsics/root.py index 8da130fa2..28ae10cc3 100644 --- a/bittensor/api/extrinsics/root.py +++ b/bittensor/api/extrinsics/root.py @@ -15,32 +15,33 @@ # 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 bittensor - -import time import logging +import time +from typing import Union, List + import numpy as np +from bittensor_wallet import Wallet from numpy.typing import NDArray from rich.prompt import Confirm -from typing import Union, List -import bittensor.utils.weight_utils as weight_utils -from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME -from bittensor.utils.registration import torch, legacy_torch_api_compat -logger = logging.getLogger(BITTENSOR_LOGGER_NAME) +from bittensor.utils import weight_utils +from bittensor.core.settings import bt_console +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import torch, legacy_torch_api_compat def root_register_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - r"""Registers the wallet to root network. + """Registers the wallet to root network. Args: - wallet (bittensor.wallet): + subtensor: + wallet (Wallet): Bittensor wallet object. 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. @@ -59,7 +60,7 @@ def root_register_extrinsic( netuid=0, hotkey_ss58=wallet.hotkey.ss58_address ) if is_registered: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Already registered on root network.[/green]" ) return True @@ -69,7 +70,7 @@ def root_register_extrinsic( if not Confirm.ask("Register to root network?"): return False - with bittensor.__console__.status(":satellite: Registering to root network..."): + with bt_console.status(":satellite: Registering to root network..."): success, err_msg = subtensor._do_root_register( wallet=wallet, wait_for_inclusion=wait_for_inclusion, @@ -77,7 +78,7 @@ def root_register_extrinsic( ) if not success: - bittensor.__console__.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") time.sleep(0.5) # Successful registration, final check for neuron and pubkey @@ -86,21 +87,21 @@ def root_register_extrinsic( netuid=0, hotkey_ss58=wallet.hotkey.ss58_address ) if is_registered: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Registered[/green]" ) return True else: # neuron not found, try again - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) @legacy_torch_api_compat def set_root_weights_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet, netuids: Union[NDArray[np.int64], "torch.LongTensor", List[int]], weights: Union[NDArray[np.float32], "torch.FloatTensor", List[float]], version_key: int = 0, @@ -111,7 +112,8 @@ def set_root_weights_extrinsic( r"""Sets the given weights and values on chain for wallet hotkey account. Args: - wallet (bittensor.wallet): + subtensor: + wallet (Wallet): Bittensor wallet object. netuids (Union[NDArray[np.int64], torch.LongTensor, List[int]]): The ``netuid`` of the subnet to set weights for. @@ -154,10 +156,10 @@ def set_root_weights_extrinsic( ) # Normalize the weights to max value. - formatted_weights = bittensor.utils.weight_utils.normalize_max_weight( + formatted_weights = weight_utils.normalize_max_weight( x=weights, limit=max_weight_limit ) - bittensor.__console__.print( + bt_console.print( f"\nRaw Weights -> Normalized weights: \n\t{weights} -> \n\t{formatted_weights}\n" ) @@ -170,7 +172,7 @@ def set_root_weights_extrinsic( ): return False - with bittensor.__console__.status( + with bt_console.status( ":satellite: Setting root weights on [white]{}[/white] ...".format( subtensor.network ) @@ -189,36 +191,36 @@ def set_root_weights_extrinsic( wait_for_inclusion=wait_for_inclusion, ) - bittensor.__console__.print(success, error_message) + bt_console.print(success, error_message) if not wait_for_finalization and not wait_for_inclusion: return True if success is True: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - bittensor.logging.success( + logging.success( prefix="Set weights", suffix="Finalized: " + str(success), ) return True else: - bittensor.__console__.print( + bt_console.print( f":cross_mark: [red]Failed[/red]: {error_message}" ) - bittensor.logging.warning( + logging.warning( prefix="Set weights", suffix="Failed: " + str(error_message), ) return False except Exception as e: - # TODO( devs ): lets remove all of the bittensor.__console__ calls and replace with the bittensor logger. - bittensor.__console__.print( + # TODO( devs ): lets remove all of the bt_console calls and replace with the bittensor logger. + bt_console.print( ":cross_mark: [red]Failed[/red]: error:{}".format(e) ) - bittensor.logging.warning( + logging.warning( prefix="Set weights", suffix="Failed: " + str(e) ) return False diff --git a/bittensor/api/extrinsics/senate.py b/bittensor/api/extrinsics/senate.py index 817a589f5..03e284f9d 100644 --- a/bittensor/api/extrinsics/senate.py +++ b/bittensor/api/extrinsics/senate.py @@ -19,32 +19,31 @@ from rich.prompt import Confirm -import bittensor +from bittensor.core.settings import bt_console from bittensor.utils import format_error_message +from bittensor_wallet import Wallet def register_senate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - r"""Registers the wallet to chain for senate voting. + """Registers the wallet to chain for senate voting. Args: - wallet (bittensor.wallet): - Bittensor wallet object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor: + wallet (bittensor.wallet): Bittensor wallet object. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + 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``. + 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``. """ + wallet.coldkey # unlock coldkey wallet.hotkey # unlock hotkey @@ -53,7 +52,7 @@ def register_senate_extrinsic( if not Confirm.ask(f"Register delegate hotkey to senate?"): return False - with bittensor.__console__.status(":satellite: Registering with senate..."): + with bt_console.status(":satellite: Registering with senate..."): with subtensor.substrate as substrate: # create extrinsic call call = substrate.compose_call( @@ -77,30 +76,30 @@ def register_senate_extrinsic( # process if registration successful response.process_events() if not response.is_success: - bittensor.__console__.print( + bt_console.print( f":cross_mark: [red]Failed[/red]:{format_error_message(response.error_message)}" ) time.sleep(0.5) # Successful registration, final check for membership else: - is_registered = wallet.is_senate_member(subtensor) + is_registered = subtensor.is_senate_member(wallet.hotkey.ss58_address) if is_registered: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Registered[/green]" ) return True else: # neuron not found, try again - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Unknown error. Senate membership not found.[/red]" ) def leave_senate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, @@ -108,6 +107,7 @@ def leave_senate_extrinsic( r"""Removes the wallet from chain for senate voting. Args: + subtensor: wallet (bittensor.wallet): Bittensor wallet object. wait_for_inclusion (bool): @@ -128,7 +128,7 @@ def leave_senate_extrinsic( if not Confirm.ask(f"Remove delegate hotkey from senate?"): return False - with bittensor.__console__.status(":satellite: Leaving senate..."): + with bt_console.status(":satellite: Leaving senate..."): with subtensor.substrate as substrate: # create extrinsic call call = substrate.compose_call( @@ -152,30 +152,30 @@ def leave_senate_extrinsic( # process if registration successful response.process_events() if not response.is_success: - bittensor.__console__.print( + bt_console.print( f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" ) time.sleep(0.5) # Successful registration, final check for membership else: - is_registered = wallet.is_senate_member(subtensor) + is_registered = subtensor.is_senate_member(wallet.hotkey.ss58_address) if not is_registered: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Left senate[/green]" ) return True else: # neuron not found, try again - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Unknown error. Senate membership still found.[/red]" ) def vote_senate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", proposal_hash: str, proposal_idx: int, vote: bool, @@ -183,9 +183,13 @@ def vote_senate_extrinsic( wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - r"""Votes ayes or nays on proposals. + """Votes ayes or nays on proposals. Args: + vote: + proposal_idx: + proposal_hash: + subtensor: wallet (bittensor.wallet): Bittensor wallet object. wait_for_inclusion (bool): @@ -206,7 +210,7 @@ def vote_senate_extrinsic( if not Confirm.ask("Cast a vote of {}?".format(vote)): return False - with bittensor.__console__.status(":satellite: Casting vote.."): + with bt_console.status(":satellite: Casting vote.."): with subtensor.substrate as substrate: # create extrinsic call call = substrate.compose_call( @@ -235,7 +239,7 @@ def vote_senate_extrinsic( # process if vote successful response.process_events() if not response.is_success: - bittensor.__console__.print( + bt_console.print( f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" ) time.sleep(0.5) @@ -249,12 +253,12 @@ def vote_senate_extrinsic( ) if has_voted: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Vote cast.[/green]" ) return True else: # hotkey not found in ayes/nays - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Unknown error. Couldn't find vote.[/red]" ) diff --git a/bittensor/api/extrinsics/serving.py b/bittensor/api/extrinsics/serving.py index ec6417d34..8aa4939cc 100644 --- a/bittensor/api/extrinsics/serving.py +++ b/bittensor/api/extrinsics/serving.py @@ -18,18 +18,21 @@ import json from typing import Optional +from bittensor_wallet import Wallet from retry import retry from rich.prompt import Confirm -import bittensor -import bittensor.utils.networking as net -from bittensor.utils import format_error_message +from bittensor.core.axon import Axon from bittensor.core.errors import MetadataError +from bittensor.core.settings import version_as_int, bt_console +from bittensor.core.types import AxonServeCallParams +from bittensor.utils import format_error_message, networking as net +from bittensor.utils.btlogging import logging def serve_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", ip: str, port: int, protocol: int, @@ -40,37 +43,28 @@ def serve_extrinsic( wait_for_finalization=True, prompt: bool = False, ) -> bool: - r"""Subscribes a Bittensor endpoint to the subtensor chain. + """Subscribes a Bittensor endpoint to the subtensor chain. Args: - wallet (bittensor.wallet): - Bittensor wallet object. - ip (str): - Endpoint host port i.e., ``192.122.31.4``. - port (int): - Endpoint port number i.e., ``9221``. - protocol (int): - An ``int`` representation of the protocol. - netuid (int): - The network uid to serve on. - placeholder1 (int): - A placeholder for future use. - placeholder2 (int): - A placeholder for future use. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance object. + wallet (bittensor.wallet): Bittensor wallet object. + ip (str): Endpoint host port i.e., ``192.122.31.4``. + port (int): Endpoint port number i.e., ``9221``. + protocol (int): An ``int`` representation of the protocol. + netuid (int): The network uid to serve on. + placeholder1 (int): A placeholder for future use. + placeholder2 (int): A placeholder for future use. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ # Decrypt hotkey wallet.hotkey - params: "bittensor.AxonServeCallParams" = { - "version": bittensor.__version_as_int__, + params: "AxonServeCallParams" = { + "version": version_as_int, "ip": net.ip_to_int(ip), "port": port, "ip_type": net.ip_version(ip), @@ -81,7 +75,7 @@ def serve_extrinsic( "placeholder1": placeholder1, "placeholder2": placeholder2, } - bittensor.logging.debug("Checking axon ...") + logging.debug("Checking axon ...") neuron = subtensor.get_neuron_for_pubkey_and_subnet( wallet.hotkey.ss58_address, netuid=netuid ) @@ -101,7 +95,7 @@ def serve_extrinsic( output["coldkey"] = wallet.coldkeypub.ss58_address output["hotkey"] = wallet.hotkey.ss58_address if neuron_up_to_date: - bittensor.logging.debug( + logging.debug( f"Axon already served on: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) " ) return True @@ -117,7 +111,7 @@ def serve_extrinsic( ): return False - bittensor.logging.debug( + logging.debug( f"Serving axon with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) -> {subtensor.network}:{netuid}" ) success, error_message = subtensor._do_serve_axon( @@ -129,41 +123,37 @@ def serve_extrinsic( if wait_for_inclusion or wait_for_finalization: if success is True: - bittensor.logging.debug( + logging.debug( f"Axon served with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) on {subtensor.network}:{netuid} " ) return True else: - bittensor.logging.error(f"Failed: {error_message}") + logging.error(f"Failed: {error_message}") return False else: return True def serve_axon_extrinsic( - subtensor: "bittensor.subtensor", + subtensor, netuid: int, - axon: "bittensor.Axon", + axon: "Axon", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - r"""Serves the axon to the network. + """Serves the axon to the network. Args: - netuid ( int ): - The ``netuid`` being served on. - axon (bittensor.Axon): - Axon to serve. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance object. + netuid (int): The ``netuid`` being served on. + axon (bittensor.core.axon.Axon): Axon to serve. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ axon.wallet.hotkey axon.wallet.coldkeypub @@ -173,12 +163,12 @@ def serve_axon_extrinsic( if axon.external_ip is None: try: external_ip = net.get_external_ip() - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Found external ip: {}[/green]".format( external_ip ) ) - bittensor.logging.success( + logging.success( prefix="External IP", suffix="{}".format(external_ip) ) except Exception as E: @@ -204,8 +194,8 @@ def serve_axon_extrinsic( def publish_metadata( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", netuid: int, data_type: str, data: bytes, @@ -216,28 +206,19 @@ def publish_metadata( Publishes metadata on the Bittensor network using the specified wallet and network identifier. Args: - subtensor (bittensor.subtensor): - The subtensor instance representing the Bittensor blockchain connection. - wallet (bittensor.wallet): - The wallet object used for authentication in the transaction. - netuid (int): - Network UID on which the metadata is to be published. - data_type (str): - The data type of the information being submitted. It should be one of the following: ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing algorithm used for the data. - data (str): - The actual metadata content to be published. This should be formatted or hashed according to the ``type`` specified. (Note: max ``str`` length is 128 bytes) - wait_for_inclusion (bool, optional): - If ``True``, the function will wait for the extrinsic to be included in a 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``. + subtensor (bittensor.subtensor): The subtensor instance representing the Bittensor blockchain connection. + wallet (bittensor.wallet): The wallet object used for authentication in the transaction. + netuid (int): Network UID on which the metadata is to be published. + data_type (str): The data type of the information being submitted. It should be one of the following: ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing algorithm used for the data. + data (str): The actual metadata content to be published. This should be formatted or hashed according to the ``type`` specified. (Note: max ``str`` length is 128 bytes) + wait_for_inclusion (bool, optional): If ``True``, the function will wait for the extrinsic to be included in a 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``. Returns: - bool: - ``True`` if the metadata was successfully published (and finalized if specified). ``False`` otherwise. + 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 failure. + MetadataError: If there is an error in submitting the extrinsic or if the response from the blockchain indicates failure. """ wallet.hotkey diff --git a/bittensor/api/extrinsics/set_weights.py b/bittensor/api/extrinsics/set_weights.py index 13845e9d5..4312d8a85 100644 --- a/bittensor/api/extrinsics/set_weights.py +++ b/bittensor/api/extrinsics/set_weights.py @@ -15,23 +15,23 @@ # 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 bittensor - import logging +from typing import Union, Tuple + import numpy as np +from bittensor_wallet import Wallet from numpy.typing import NDArray from rich.prompt import Confirm -from typing import Union, Tuple -import bittensor.utils.weight_utils as weight_utils -from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME -from bittensor.utils.registration import torch, use_torch -logger = logging.getLogger(BITTENSOR_LOGGER_NAME) +from bittensor.utils import weight_utils +from bittensor.core.settings import bt_console +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import torch, use_torch def set_weights_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", netuid: int, uids: Union[NDArray[np.int64], "torch.LongTensor", list], weights: Union[NDArray[np.float32], "torch.FloatTensor", list], @@ -40,7 +40,7 @@ def set_weights_extrinsic( wait_for_finalization: bool = False, prompt: bool = False, ) -> Tuple[bool, str]: - r"""Sets the given weights and values on chain for wallet hotkey account. + """Sets the given weights and values on chain for wallet hotkey account. Args: subtensor (bittensor.subtensor): @@ -91,7 +91,7 @@ def set_weights_extrinsic( ): return False, "Prompt refused." - with bittensor.__console__.status( + with bt_console.status( ":satellite: Setting weights on [white]{}[/white] ...".format(subtensor.network) ): try: @@ -109,16 +109,16 @@ def set_weights_extrinsic( return True, "Not waiting for finalization or inclusion." if success is True: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - bittensor.logging.success( + logging.success( prefix="Set weights", suffix="Finalized: " + str(success), ) return True, "Successfully set weights and Finalized." else: - bittensor.logging.error( + logging.error( msg=error_message, prefix="Set weights", suffix="Failed: ", @@ -126,10 +126,10 @@ def set_weights_extrinsic( return False, error_message except Exception as e: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: error:{}".format(e) ) - bittensor.logging.warning( + logging.warning( prefix="Set weights", suffix="Failed: " + str(e) ) return False, str(e) diff --git a/bittensor/api/extrinsics/transfer.py b/bittensor/api/extrinsics/transfer.py index 2a9f76e01..a58cf6c40 100644 --- a/bittensor/api/extrinsics/transfer.py +++ b/bittensor/api/extrinsics/transfer.py @@ -15,12 +15,14 @@ # 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 bittensor +from typing import Union from rich.prompt import Confirm -from typing import Union -from bittensor.utils.balance import Balance + +from bittensor.core.settings import bt_console, network_explorer_map +from bittensor.utils import get_explorer_url_for_network from bittensor.utils import is_valid_bittensor_address_or_public_key +from bittensor.utils.balance import Balance def transfer_extrinsic( @@ -56,7 +58,7 @@ def transfer_extrinsic( """ # Validate destination address. if not is_valid_bittensor_address_or_public_key(dest): - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Invalid destination address[/red]:[bold white]\n {}[/bold white]".format( dest ) @@ -71,29 +73,29 @@ def transfer_extrinsic( wallet.coldkey # Convert to bittensor.Balance - if not isinstance(amount, bittensor.Balance): - transfer_balance = bittensor.Balance.from_tao(amount) + if not isinstance(amount, Balance): + transfer_balance = Balance.from_tao(amount) else: transfer_balance = amount # Check balance. - with bittensor.__console__.status(":satellite: Checking Balance..."): + with bt_console.status(":satellite: Checking Balance..."): account_balance = subtensor.get_balance(wallet.coldkey.ss58_address) # check existential deposit. existential_deposit = subtensor.get_existential_deposit() - with bittensor.__console__.status(":satellite: Transferring..."): + with bt_console.status(":satellite: Transferring..."): fee = subtensor.get_transfer_fee( wallet=wallet, dest=dest, value=transfer_balance.rao ) if not keep_alive: # Check if the transfer should keep_alive the account - existential_deposit = bittensor.Balance(0) + existential_deposit = Balance(0) # Check if we have enough balance. if account_balance < (transfer_balance + fee + existential_deposit): - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Not enough balance[/red]:[bold white]\n balance: {}\n amount: {}\n for fee: {}[/bold white]".format( account_balance, transfer_balance, fee ) @@ -109,7 +111,7 @@ def transfer_extrinsic( ): return False - with bittensor.__console__.status(":satellite: Transferring..."): + with bt_console.status(":satellite: Transferring..."): success, block_hash, err_msg = subtensor._do_transfer( wallet, dest, @@ -119,34 +121,34 @@ def transfer_extrinsic( ) if success: - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - bittensor.__console__.print( + bt_console.print( "[green]Block Hash: {}[/green]".format(block_hash) ) - explorer_urls = bittensor.utils.get_explorer_url_for_network( - subtensor.network, block_hash, bittensor.__network_explorer_map__ + explorer_urls = get_explorer_url_for_network( + subtensor.network, block_hash, network_explorer_map ) if explorer_urls != {} and explorer_urls: - bittensor.__console__.print( + bt_console.print( "[green]Opentensor Explorer Link: {}[/green]".format( explorer_urls.get("opentensor") ) ) - bittensor.__console__.print( + bt_console.print( "[green]Taostats Explorer Link: {}[/green]".format( explorer_urls.get("taostats") ) ) else: - bittensor.__console__.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") if success: - with bittensor.__console__.status(":satellite: Checking Balance..."): + with bt_console.status(":satellite: Checking Balance..."): new_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - bittensor.__console__.print( + bt_console.print( "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( account_balance, new_balance ) diff --git a/bittensor/api/extrinsics/unstaking.py b/bittensor/api/extrinsics/unstaking.py index 89af9cdad..4d1517d47 100644 --- a/bittensor/api/extrinsics/unstaking.py +++ b/bittensor/api/extrinsics/unstaking.py @@ -15,19 +15,22 @@ # 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 bittensor -from rich.prompt import Confirm from time import sleep from typing import List, Union, Optional -from bittensor.utils.balance import Balance + +from bittensor_wallet import Wallet +from rich.prompt import Confirm + from bittensor.core.errors import NotRegisteredError, StakeError +from bittensor.core.settings import bt_console +from bittensor.utils.balance import Balance def __do_remove_stake_single( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", hotkey_ss58: str, - amount: "bittensor.Balance", + amount: "Balance", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: @@ -35,7 +38,7 @@ def __do_remove_stake_single( Executes an unstake call to the chain using the wallet and the amount specified. Args: - wallet (bittensor.wallet): + wallet (Wallet): Bittensor wallet object. hotkey_ss58 (str): Hotkey address to unstake from. @@ -72,7 +75,7 @@ def __do_remove_stake_single( def check_threshold_amount( - subtensor: "bittensor.subtensor", stake_balance: Balance + subtensor, stake_balance: Balance ) -> bool: """ Checks if the remaining stake balance is above the minimum required stake threshold. @@ -89,7 +92,7 @@ def check_threshold_amount( min_req_stake: Balance = subtensor.get_minimum_required_stake() if min_req_stake > stake_balance > 0: - bittensor.__console__.print( + bt_console.print( f":cross_mark: [yellow]Remaining stake balance of {stake_balance} less than minimum of {min_req_stake} TAO[/yellow]" ) return False @@ -98,8 +101,8 @@ def check_threshold_amount( def unstake_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", hotkey_ss58: Optional[str] = None, amount: Optional[Union[Balance, float]] = None, wait_for_inclusion: bool = True, @@ -109,7 +112,7 @@ def unstake_extrinsic( r"""Removes stake into the wallet coldkey from the specified hotkey ``uid``. Args: - wallet (bittensor.wallet): + wallet (Wallet): Bittensor wallet object. hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey is used. @@ -131,7 +134,7 @@ def unstake_extrinsic( if hotkey_ss58 is None: hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. - with bittensor.__console__.status( + with bt_console.status( ":satellite: Syncing with chain: [white]{}[/white] ...".format( subtensor.network ) @@ -148,15 +151,15 @@ def unstake_extrinsic( if amount is None: # Unstake it all. unstaking_balance = old_stake - elif not isinstance(amount, bittensor.Balance): - unstaking_balance = bittensor.Balance.from_tao(amount) + elif not isinstance(amount, Balance): + unstaking_balance = Balance.from_tao(amount) else: unstaking_balance = amount # Check enough to unstake. stake_on_uid = old_stake if unstaking_balance > stake_on_uid: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Not enough stake[/red]: [green]{}[/green] to unstake: [blue]{}[/blue] from hotkey: [white]{}[/white]".format( stake_on_uid, unstaking_balance, wallet.hotkey_str ) @@ -167,7 +170,7 @@ def unstake_extrinsic( if not own_hotkey and not check_threshold_amount( subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) ): - bittensor.__console__.print( + bt_console.print( f":warning: [yellow]This action will unstake the entire staked balance![/yellow]" ) unstaking_balance = stake_on_uid @@ -182,7 +185,7 @@ def unstake_extrinsic( return False try: - with bittensor.__console__.status( + with bt_console.status( ":satellite: Unstaking from chain: [white]{}[/white] ...".format( subtensor.network ) @@ -201,10 +204,10 @@ def unstake_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - with bittensor.__console__.status( + with bt_console.status( ":satellite: Checking Balance on: [white]{}[/white] ...".format( subtensor.network ) @@ -215,38 +218,38 @@ def unstake_extrinsic( new_stake = subtensor.get_stake_for_coldkey_and_hotkey( coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 ) # Get stake on hotkey. - bittensor.__console__.print( + bt_console.print( "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_balance, new_balance ) ) - bittensor.__console__.print( + bt_console.print( "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_stake, new_stake ) ) return True else: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: Unknown Error." ) return False except NotRegisteredError as e: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( wallet.hotkey_str ) ) return False except StakeError as e: - bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) + bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) return False def unstake_multiple_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor, + wallet: "Wallet", hotkey_ss58s: List[str], amounts: Optional[List[Union[Balance, float]]] = None, wait_for_inclusion: bool = True, @@ -256,7 +259,7 @@ def unstake_multiple_extrinsic( r"""Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. Args: - wallet (bittensor.wallet): + wallet (Wallet): The wallet with the coldkey to unstake to. hotkey_ss58s (List[str]): List of hotkeys to unstake from. @@ -287,7 +290,7 @@ def unstake_multiple_extrinsic( isinstance(amount, (Balance, float)) for amount in amounts ): raise TypeError( - "amounts must be a [list of bittensor.Balance or float] or None" + "amounts must be a [list of Balance or float] or None" ) if amounts is None: @@ -295,7 +298,7 @@ def unstake_multiple_extrinsic( else: # Convert to Balance amounts = [ - bittensor.Balance.from_tao(amount) if isinstance(amount, float) else amount + Balance.from_tao(amount) if isinstance(amount, float) else amount for amount in amounts ] @@ -308,7 +311,7 @@ def unstake_multiple_extrinsic( old_stakes = [] own_hotkeys = [] - with bittensor.__console__.status( + with bt_console.status( ":satellite: Syncing with chain: [white]{}[/white] ...".format( subtensor.network ) @@ -332,15 +335,15 @@ def unstake_multiple_extrinsic( if amount is None: # Unstake it all. unstaking_balance = old_stake - elif not isinstance(amount, bittensor.Balance): - unstaking_balance = bittensor.Balance.from_tao(amount) + elif not isinstance(amount, Balance): + unstaking_balance = Balance.from_tao(amount) else: unstaking_balance = amount # Check enough to unstake. stake_on_uid = old_stake if unstaking_balance > stake_on_uid: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Not enough stake[/red]: [green]{}[/green] to unstake: [blue]{}[/blue] from hotkey: [white]{}[/white]".format( stake_on_uid, unstaking_balance, wallet.hotkey_str ) @@ -351,7 +354,7 @@ def unstake_multiple_extrinsic( if not own_hotkey and not check_threshold_amount( subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) ): - bittensor.__console__.print( + bt_console.print( f":warning: [yellow]This action will unstake the entire staked balance![/yellow]" ) unstaking_balance = stake_on_uid @@ -366,7 +369,7 @@ def unstake_multiple_extrinsic( continue try: - with bittensor.__console__.status( + with bt_console.status( ":satellite: Unstaking from chain: [white]{}[/white] ...".format( subtensor.network ) @@ -387,7 +390,7 @@ def unstake_multiple_extrinsic( # Wait for tx rate limit. tx_rate_limit_blocks = subtensor.tx_rate_limit() if tx_rate_limit_blocks > 0: - bittensor.__console__.print( + bt_console.print( ":hourglass: [yellow]Waiting for tx rate limit: [white]{}[/white] blocks[/yellow]".format( tx_rate_limit_blocks ) @@ -398,10 +401,10 @@ def unstake_multiple_extrinsic( successful_unstakes += 1 continue - bittensor.__console__.print( + bt_console.print( ":white_heavy_check_mark: [green]Finalized[/green]" ) - with bittensor.__console__.status( + with bt_console.status( ":satellite: Checking Balance on: [white]{}[/white] ...".format( subtensor.network ) @@ -412,37 +415,37 @@ def unstake_multiple_extrinsic( hotkey_ss58=hotkey_ss58, block=block, ) - bittensor.__console__.print( + bt_console.print( "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( hotkey_ss58, stake_on_uid, new_stake ) ) successful_unstakes += 1 else: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Failed[/red]: Unknown Error." ) continue except NotRegisteredError as e: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]{} is not registered.[/red]".format(hotkey_ss58) ) continue except StakeError as e: - bittensor.__console__.print( + bt_console.print( ":cross_mark: [red]Stake Error: {}[/red]".format(e) ) continue if successful_unstakes != 0: - with bittensor.__console__.status( + with bt_console.status( ":satellite: Checking Balance on: ([white]{}[/white] ...".format( subtensor.network ) ): new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bittensor.__console__.print( + bt_console.print( "Balance: [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_balance, new_balance ) diff --git a/bittensor/api/extrinsics/utils.py b/bittensor/api/extrinsics/utils.py new file mode 100644 index 000000000..16466e9c6 --- /dev/null +++ b/bittensor/api/extrinsics/utils.py @@ -0,0 +1,41 @@ +# 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. + +HYPERPARAMS = { + "serving_rate_limit": "sudo_set_serving_rate_limit", + "min_difficulty": "sudo_set_min_difficulty", + "max_difficulty": "sudo_set_max_difficulty", + "weights_version": "sudo_set_weights_version_key", + "weights_rate_limit": "sudo_set_weights_set_rate_limit", + "max_weight_limit": "sudo_set_max_weight_limit", + "immunity_period": "sudo_set_immunity_period", + "min_allowed_weights": "sudo_set_min_allowed_weights", + "activity_cutoff": "sudo_set_activity_cutoff", + "network_registration_allowed": "sudo_set_network_registration_allowed", + "network_pow_registration_allowed": "sudo_set_network_pow_registration_allowed", + "min_burn": "sudo_set_min_burn", + "max_burn": "sudo_set_max_burn", + "adjustment_alpha": "sudo_set_adjustment_alpha", + "rho": "sudo_set_rho", + "kappa": "sudo_set_kappa", + "difficulty": "sudo_set_difficulty", + "bonds_moving_avg": "sudo_set_bonds_moving_average", + "commit_reveal_weights_interval": "sudo_set_commit_reveal_weights_interval", + "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", + "alpha_values": "sudo_set_alpha_values", + "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", +} diff --git a/bittensor/btcli/cli.py b/bittensor/btcli/cli.py index 423394ac3..917e2ac45 100644 --- a/bittensor/btcli/cli.py +++ b/bittensor/btcli/cli.py @@ -15,12 +15,15 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from rich.console import Console -import sys -import shtab import argparse -import bittensor +import sys from typing import List, Optional + +import shtab + +from bittensor.core.config import Config +from bittensor.core.settings import bt_console, turn_console_on, __version__ +from bittensor.utils import check_version, VersionCheckError from .commands import ( AutocompleteCommand, DelegateStakeCommand, @@ -73,9 +76,6 @@ CheckColdKeySwapCommand, ) -# Create a console instance for CLI display. -console = Console() - ALIAS_TO_COMMAND = { "subnets": "subnets", "root": "root", @@ -233,18 +233,18 @@ class cli: def __init__( self, - config: Optional["bittensor.config"] = None, + config: Optional["Config"] = None, args: Optional[List[str]] = None, ): """ Initializes a bittensor.CLI object. Args: - config (bittensor.config, optional): The configuration settings for the CLI. + config (Config, optional): The configuration settings for the CLI. args (List[str], optional): List of command line arguments. """ # Turns on console for cli. - bittensor.turn_console_on() + turn_console_on() # If no config is provided, create a new one from args. if config is None: @@ -254,7 +254,7 @@ def __init__( if self.config.command in ALIAS_TO_COMMAND: self.config.command = ALIAS_TO_COMMAND[self.config.command] else: - console.print( + bt_console.print( f":cross_mark:[red]Unknown command: {self.config.command}[/red]" ) sys.exit() @@ -265,8 +265,8 @@ def __init__( # If no_version_checking is not set or set as False in the config, version checking is done. if not self.config.get("no_version_checking", d=True): try: - bittensor.utils.check_version() - except bittensor.utils.VersionCheckError: + check_version() + except VersionCheckError: # If version checking fails, inform user with an exception. raise RuntimeError( "To avoid internet-based version checking, pass --no_version_checking while running the CLI." @@ -282,7 +282,7 @@ def __create_parser__() -> "argparse.ArgumentParser": """ # Define the basic argument parser. parser = CLIErrorParser( - description=f"bittensor cli v{bittensor.__version__}", + description=f"bittensor cli v{__version__}", usage="btcli ", add_help=True, ) @@ -314,7 +314,7 @@ def __create_parser__() -> "argparse.ArgumentParser": return parser @staticmethod - def create_config(args: List[str]) -> "bittensor.config": + def create_config(args: List[str]) -> "Config": """ From the argument parser, add config to bittensor.executor and local config @@ -322,7 +322,7 @@ def create_config(args: List[str]) -> "bittensor.config": args (List[str]): List of command line arguments. Returns: - bittensor.config: The configuration object for Bittensor CLI. + Config: The configuration object for Bittensor CLI. """ parser = cli.__create_parser__() @@ -331,15 +331,15 @@ def create_config(args: List[str]) -> "bittensor.config": parser.print_help() sys.exit() - return bittensor.config(parser, args=args) + return Config(parser, args=args) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): """ Checks if the essential configuration exists under different command Args: - config (bittensor.config): The configuration settings for the CLI. + config (Config): The configuration settings for the CLI. """ # Check if command exists, if so, run the corresponding check_config. # If command doesn't exist, inform user and exit the program. @@ -351,14 +351,14 @@ def check_config(config: "bittensor.config"): if config["subcommand"] is not None: command_data["commands"][config["subcommand"]].check_config(config) else: - console.print( + bt_console.print( f":cross_mark:[red]Missing subcommand for: {config.command}[/red]" ) sys.exit(1) else: command_data.check_config(config) else: - console.print(f":cross_mark:[red]Unknown command: {config.command}[/red]") + bt_console.print(f":cross_mark:[red]Unknown command: {config.command}[/red]") sys.exit(1) def run(self): @@ -383,7 +383,7 @@ def run(self): else: command_data.run(self) else: - console.print( + bt_console.print( f":cross_mark:[red]Unknown command: {self.config.command}[/red]" ) sys.exit() diff --git a/bittensor/btcli/commands/check_coldkey_swap.py b/bittensor/btcli/commands/check_coldkey_swap.py index 008ab610b..9864d0959 100644 --- a/bittensor/btcli/commands/check_coldkey_swap.py +++ b/bittensor/btcli/commands/check_coldkey_swap.py @@ -17,15 +17,16 @@ import argparse -from rich.console import Console +from bittensor_wallet import Wallet from rich.prompt import Prompt -import bittensor +from bittensor.core.config import Config +from bittensor.core.settings import bt_console +from bittensor.core.subtensor import Subtensor +from bittensor.utils.btlogging import logging from bittensor.utils.formatting import convert_blocks_to_time from . import defaults -console = Console() - def fetch_arbitration_stats(subtensor, wallet): """ @@ -35,7 +36,7 @@ def fetch_arbitration_stats(subtensor, wallet): subtensor.check_in_arbitration(wallet.coldkeypub.ss58_address) ) if arbitration_check == 0: - bittensor.__console__.print( + bt_console.print( "[green]There has been no previous key swap initiated for your coldkey.[/green]" ) if arbitration_check == 1: @@ -43,14 +44,14 @@ def fetch_arbitration_stats(subtensor, wallet): wallet.coldkeypub.ss58_address ) hours, minutes, seconds = convert_blocks_to_time(arbitration_remaining) - bittensor.__console__.print( + bt_console.print( "[yellow]There has been 1 swap request made for this coldkey already." " By adding another swap request, the key will enter arbitration." f" Your key swap is scheduled for {hours} hours, {minutes} minutes, {seconds} seconds" " from now.[/yellow]" ) if arbitration_check > 1: - bittensor.__console__.print( + bt_console.print( f"[red]This coldkey is currently in arbitration with a total swaps of {arbitration_check}.[/red]" ) @@ -67,7 +68,7 @@ class CheckColdKeySwapCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): """ Runs the check coldkey swap command. Args: @@ -75,19 +76,19 @@ def run(cli: "bittensor.cli"): """ try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) CheckColdKeySwapCommand._run(cli, subtensor) except Exception as e: - bittensor.logging.warning(f"Failed to get swap status: {e}") + logging.warning(f"Failed to get swap status: {e}") finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): """ Internal method to check coldkey swap status. Args: @@ -95,12 +96,12 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): subtensor (bittensor.subtensor): The subtensor object for blockchain interactions. """ config = cli.config.copy() - wallet = bittensor.wallet(config=config) + wallet = Wallet(config=config) fetch_arbitration_stats(subtensor, wallet) @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): """ Checks and prompts for necessary configuration settings. Args: @@ -125,5 +126,5 @@ def add_args(command_parser: argparse.ArgumentParser): help="""Check the status of swap requests for a coldkey on the Bittensor network. Adding more than one swap request will make the key go into arbitration mode.""", ) - bittensor.wallet.add_args(swap_parser) - bittensor.subtensor.add_args(swap_parser) + Wallet.add_args(swap_parser) + Subtensor.add_args(swap_parser) diff --git a/bittensor/btcli/commands/delegates.py b/bittensor/btcli/commands/delegates.py index 257959a36..b640e8f8c 100644 --- a/bittensor/btcli/commands/delegates.py +++ b/bittensor/btcli/commands/delegates.py @@ -20,23 +20,28 @@ import sys from typing import List, Dict, Optional +from bittensor_wallet import Wallet from rich.console import Text, Console from rich.prompt import Prompt, FloatPrompt, Confirm from rich.table import Table from substrateinterface.exceptions import SubstrateRequestException from tqdm import tqdm -import bittensor +from bittensor.core.chain_data import DelegateInfoLite, DelegateInfo +from bittensor.core.config import Config +from bittensor.core.settings import bt_console, delegates_details_url +from bittensor.core.subtensor import Subtensor +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging from . import defaults -from ...core import settings from .identity import SetIdentityCommand from .utils import get_delegates_details, DelegatesDetails -def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: +def _get_coldkey_wallets_for_path(path: str) -> List["Wallet"]: try: wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [bittensor.wallet(path=path, name=name) for name in wallet_names] + return [Wallet(path=path, name=name) for name in wallet_names] except StopIteration: # No wallet files found. wallets = [] @@ -47,7 +52,7 @@ def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: def show_delegates_lite( - delegates_lite: List["bittensor.DelegateInfoLite"], width: Optional[int] = None + delegates_lite: List["DelegateInfoLite"], width: Optional[int] = None ): """ This method is a lite version of the :func:`show_delegates`. This method displays a formatted table of Bittensor network delegates with detailed statistics to the console. @@ -58,7 +63,7 @@ def show_delegates_lite( This helper function is not intended to be used directly in user code unless specifically required. Args: - delegates_lite (List[bittensor.DelegateInfoLite]): A list of delegate information objects to be displayed. + delegates_lite (List[bittensor.core.chain_data.DelegateInfoLite]): A list of delegate information objects to be displayed. width (Optional[int]): The width of the console output table. Defaults to ``None``, which will make the table expand to the maximum width of the console. The output table contains the following columns. To display more columns, use the :func:`show_delegates` function. @@ -85,10 +90,10 @@ def show_delegates_lite( """ registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=settings.delegates_details_url) + get_delegates_details(url=delegates_details_url) ) if registered_delegate_info is None: - bittensor.__console__.print( + bt_console.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" ) registered_delegate_info = {} @@ -144,19 +149,19 @@ def show_delegates_lite( # `TAKE` column f"{d.take * 100:.1f}%", # `DELEGATE/(24h)` column - f"τ{bittensor.Balance.from_tao(d.total_daily_return * 0.18) !s:6.6}", + f"τ{Balance.from_tao(d.total_daily_return * 0.18) !s:6.6}", # `Desc` column str(delegate_description), end_section=True, ) - bittensor.__console__.print(table) + bt_console.print(table) # Uses rich console to pretty print a table of delegates. def show_delegates( - delegates: List["bittensor.DelegateInfo"], - prev_delegates: Optional[List["bittensor.DelegateInfo"]], - width: Optional[int] = None, + delegates: List["DelegateInfo"], + prev_delegates: Optional[List["DelegateInfo"]], + width: Optional[int] = None, ): """ Displays a formatted table of Bittensor network delegates with detailed statistics to the console. @@ -208,10 +213,10 @@ def show_delegates( prev_delegates_dict[prev_delegate.hotkey_ss58] = prev_delegate registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=settings.delegates_details_url) + get_delegates_details(url=delegates_details_url) ) if registered_delegate_info is None: - bittensor.__console__.print( + bt_console.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" ) registered_delegate_info = {} @@ -264,7 +269,7 @@ def show_delegates( lambda x: x[0] == delegate.owner_ss58, delegate.nominators ), # filter for owner ), - bittensor.Balance.from_rao(0), # default to 0 if no owner stake. + Balance.from_rao(0), # default to 0 if no owner stake. ) if delegate.hotkey_ss58 in registered_delegate_info: delegate_name = registered_delegate_info[delegate.hotkey_ss58].name @@ -283,9 +288,9 @@ def show_delegates( rate_change_in_stake_str = "[green]100%[/green]" else: rate_change_in_stake = ( - 100 - * (float(delegate.total_stake) - float(prev_stake)) - / float(prev_stake) + 100 + * (float(delegate.total_stake) - float(prev_stake)) + / float(prev_stake) ) if rate_change_in_stake > 0: rate_change_in_stake_str = "[green]{:.2f}%[/green]".format( @@ -320,14 +325,14 @@ def show_delegates( # TAKE f"{delegate.take * 100:.1f}%", # NOMINATOR/(24h)/k - f"{bittensor.Balance.from_tao( delegate.total_daily_return.tao * (1000/ (0.001 + delegate.total_stake.tao)))!s:6.6}", + f"{Balance.from_tao(delegate.total_daily_return.tao * (1000 / (0.001 + delegate.total_stake.tao)))!s:6.6}", # DELEGATE/(24h) - f"{bittensor.Balance.from_tao(delegate.total_daily_return.tao * 0.18) !s:6.6}", + f"{Balance.from_tao(delegate.total_daily_return.tao * 0.18) !s:6.6}", # Desc str(delegate_description), end_section=True, ) - bittensor.__console__.print(table) + bt_console.print(table) class DelegateStakeCommand: @@ -362,12 +367,12 @@ class DelegateStakeCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): """Delegates stake to a chain delegate.""" try: config = cli.config.copy() - wallet = bittensor.wallet(config=config) - subtensor: "bittensor.subtensor" = bittensor.subtensor( + wallet = Wallet(config=config) + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) subtensor.delegate( @@ -380,7 +385,7 @@ def run(cli: "bittensor.cli"): finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod def add_args(parser: argparse.ArgumentParser): @@ -401,16 +406,16 @@ def add_args(parser: argparse.ArgumentParser): delegate_stake_parser.add_argument( "--amount", dest="amount", type=float, required=False ) - bittensor.wallet.add_args(delegate_stake_parser) - bittensor.subtensor.add_args(delegate_stake_parser) + Wallet.add_args(delegate_stake_parser) + Subtensor.add_args(delegate_stake_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.get("delegate_ss58key"): # Check for delegates. - with bittensor.__console__.status(":satellite: Loading delegates..."): - subtensor = bittensor.subtensor(config=config, log_verbose=False) - delegates: List[bittensor.DelegateInfo] = subtensor.get_delegates() + with bt_console.status(":satellite: Loading delegates..."): + subtensor = Subtensor(config=config, log_verbose=False) + delegates: List["DelegateInfo"] = subtensor.get_delegates() try: prev_delegates = subtensor.get_delegates( max(0, subtensor.block - 1200) @@ -419,7 +424,7 @@ def check_config(config: "bittensor.config"): prev_delegates = None if prev_delegates is None: - bittensor.__console__.print( + bt_console.print( ":warning: [yellow]Could not fetch delegates history[/yellow]" ) @@ -446,9 +451,9 @@ def check_config(config: "bittensor.config"): # Get amount. if not config.get("amount") and not config.get("stake_all"): if not Confirm.ask( - "Stake all Tao from account: [bold]'{}'[/bold]?".format( - config.wallet.get("name", defaults.wallet.name) - ) + "Stake all Tao from account: [bold]'{}'[/bold]?".format( + config.wallet.get("name", defaults.wallet.name) + ) ): amount = Prompt.ask("Enter Tao amount to stake") try: @@ -498,23 +503,23 @@ class DelegateUnstakeCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): """Undelegates stake from a chain delegate.""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) DelegateUnstakeCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") - def _run(self: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(self, subtensor: "Subtensor"): """Undelegates stake from a chain delegate.""" config = self.config.copy() - wallet = bittensor.wallet(config=config) + wallet = Wallet(config=config) subtensor.undelegate( wallet=wallet, delegate_ss58=config.get("delegate_ss58key"), @@ -542,20 +547,20 @@ def add_args(parser: argparse.ArgumentParser): undelegate_stake_parser.add_argument( "--amount", dest="amount", type=float, required=False ) - bittensor.wallet.add_args(undelegate_stake_parser) - bittensor.subtensor.add_args(undelegate_stake_parser) + Wallet.add_args(undelegate_stake_parser) + Subtensor.add_args(undelegate_stake_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) if not config.get("delegate_ss58key"): # Check for delegates. - with bittensor.__console__.status(":satellite: Loading delegates..."): - subtensor = bittensor.subtensor(config=config, log_verbose=False) - delegates: List[bittensor.DelegateInfo] = subtensor.get_delegates() + with bt_console.status(":satellite: Loading delegates..."): + subtensor = Subtensor(config=config, log_verbose=False) + delegates: List["DelegateInfo"] = subtensor.get_delegates() try: prev_delegates = subtensor.get_delegates( max(0, subtensor.block - 1200) @@ -564,7 +569,7 @@ def check_config(config: "bittensor.config"): prev_delegates = None if prev_delegates is None: - bittensor.__console__.print( + bt_console.print( ":warning: [yellow]Could not fetch delegates history[/yellow]" ) @@ -587,9 +592,9 @@ def check_config(config: "bittensor.config"): # Get amount. if not config.get("amount") and not config.get("unstake_all"): if not Confirm.ask( - "Unstake all Tao to account: [bold]'{}'[/bold]?".format( - config.wallet.get("name", defaults.wallet.name) - ) + "Unstake all Tao to account: [bold]'{}'[/bold]?".format( + config.wallet.get("name", defaults.wallet.name) + ) ): amount = Prompt.ask("Enter Tao amount to unstake") try: @@ -646,7 +651,7 @@ class ListDelegatesCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r""" List all delegates on the network. """ @@ -655,22 +660,22 @@ def run(cli: "bittensor.cli"): cli.config.subtensor.chain_endpoint = ( "wss://archive.chain.opentensor.ai:443" ) - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) ListDelegatesCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r""" List all delegates on the network. """ - with bittensor.__console__.status(":satellite: Loading delegates..."): - delegates: list[bittensor.DelegateInfo] = subtensor.get_delegates() + with bt_console.status(":satellite: Loading delegates..."): + delegates: list["DelegateInfo"] = subtensor.get_delegates() try: prev_delegates = subtensor.get_delegates(max(0, subtensor.block - 1200)) @@ -678,7 +683,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): prev_delegates = None if prev_delegates is None: - bittensor.__console__.print( + bt_console.print( ":warning: [yellow]Could not fetch delegates history[/yellow]" ) @@ -693,10 +698,10 @@ def add_args(parser: argparse.ArgumentParser): list_delegates_parser = parser.add_parser( "list_delegates", help="""List all delegates on the network""" ) - bittensor.subtensor.add_args(list_delegates_parser) + Subtensor.add_args(list_delegates_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): pass @@ -734,22 +739,22 @@ class NominateCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Nominate wallet.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) NominateCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Nominate wallet.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) # Unlock the wallet. wallet.hotkey @@ -757,7 +762,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Check if the hotkey is already a delegate. if subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - bittensor.__console__.print( + bt_console.print( "Aborting: Hotkey {} is already a delegate.".format( wallet.hotkey.ss58_address ) @@ -766,7 +771,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): result: bool = subtensor.nominate(wallet) if not result: - bittensor.__console__.print( + bt_console.print( "Could not became a delegate on [white]{}[/white]".format( subtensor.network ) @@ -775,13 +780,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Check if we are a delegate. is_delegate: bool = subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address) if not is_delegate: - bittensor.__console__.print( + bt_console.print( "Could not became a delegate on [white]{}[/white]".format( subtensor.network ) ) return - bittensor.__console__.print( + bt_console.print( "Successfully became a delegate on [white]{}[/white]".format( subtensor.network ) @@ -806,11 +811,11 @@ def add_args(parser: argparse.ArgumentParser): nominate_parser = parser.add_parser( "nominate", help="""Become a delegate on the network""" ) - bittensor.wallet.add_args(nominate_parser) - bittensor.subtensor.add_args(nominate_parser) + Wallet.add_args(nominate_parser) + Subtensor.add_args(nominate_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -865,27 +870,27 @@ class MyDelegatesCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): """Delegates stake to a chain delegate.""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) MyDelegatesCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): """Delegates stake to a chain delegate.""" config = cli.config.copy() if config.get("all", d=None): wallets = _get_coldkey_wallets_for_path(config.wallet.path) else: - wallets = [bittensor.wallet(config=config)] + wallets = [Wallet(config=config)] table = Table(show_footer=True, pad_edge=False, box=None, expand=True) table.add_column( @@ -941,8 +946,8 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): for delegate in delegates: for coldkey_addr, staked in delegate[0].nominators: if ( - coldkey_addr == wallet.coldkeypub.ss58_address - and staked.tao > 0 + coldkey_addr == wallet.coldkeypub.ss58_address + and staked.tao > 0 ): my_delegates[delegate[0].hotkey_ss58] = staked @@ -950,10 +955,10 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): total_delegated += sum(my_delegates.values()) registered_delegate_info: Optional[DelegatesDetails] = ( - get_delegates_details(url=settings.delegates_details_url) + get_delegates_details(url=delegates_details_url) ) if registered_delegate_info is None: - bittensor.__console__.print( + bt_console.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" ) registered_delegate_info = {} @@ -967,7 +972,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): delegate[0].nominators, ), # filter for owner ), - bittensor.Balance.from_rao(0), # default to 0 if no owner stake. + Balance.from_rao(0), # default to 0 if no owner stake. ) if delegate[0].hotkey_ss58 in registered_delegate_info: delegate_name = registered_delegate_info[ @@ -988,7 +993,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): Text(delegate_name, style=f"link {delegate_url}"), f"{delegate[0].hotkey_ss58:8.8}...", f"{my_delegates[delegate[0].hotkey_ss58]!s:13.13}", - f"{delegate[0].total_daily_return.tao * (my_delegates[delegate[0].hotkey_ss58]/delegate[0].total_stake.tao)!s:6.6}", + f"{delegate[0].total_daily_return.tao * (my_delegates[delegate[0].hotkey_ss58] / delegate[0].total_stake.tao)!s:6.6}", str(len(delegate[0].nominators)), f"{owner_stake!s:13.13}", f"{delegate[0].total_stake!s:13.13}", @@ -1000,13 +1005,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ] ), # f'{delegate.take * 100:.1f}%',s - f"{ delegate[0].total_daily_return.tao * ( 1000 / ( 0.001 + delegate[0].total_stake.tao ) )!s:6.6}", + f"{delegate[0].total_daily_return.tao * (1000 / (0.001 + delegate[0].total_stake.tao))!s:6.6}", str(delegate_description), # f'{delegate_profile.description:140.140}', ) - bittensor.__console__.print(table) - bittensor.__console__.print("Total delegated Tao: {}".format(total_delegated)) + bt_console.print(table) + bt_console.print("Total delegated Tao: {}".format(total_delegated)) @staticmethod def add_args(parser: argparse.ArgumentParser): @@ -1020,15 +1025,15 @@ def add_args(parser: argparse.ArgumentParser): help="""Check all coldkey wallets.""", default=False, ) - bittensor.wallet.add_args(delegate_stake_parser) - bittensor.subtensor.add_args(delegate_stake_parser) + Wallet.add_args(delegate_stake_parser) + Subtensor.add_args(delegate_stake_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if ( - not config.get("all", d=None) - and not config.is_set("wallet.name") - and not config.no_prompt + not config.get("all", d=None) + and not config.is_set("wallet.name") + and not config.no_prompt ): wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -1059,23 +1064,23 @@ class SetTakeCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Set delegate take.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) SetTakeCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Set delegate take.""" config = cli.config.copy() - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) # Unlock the wallet. wallet.hotkey @@ -1083,7 +1088,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Check if the hotkey is not a delegate. if not subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - bittensor.__console__.print( + bt_console.print( "Aborting: Hotkey {} is NOT a delegate.".format( wallet.hotkey.ss58_address ) @@ -1092,13 +1097,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Prompt user for take value. new_take_str = config.get("take") - if new_take_str == None: + if new_take_str is None: new_take = FloatPrompt.ask(f"Enter take value (0.18 for 18%)") else: new_take = float(new_take_str) if new_take > 0.18: - bittensor.__console__.print("ERROR: Take value should not exceed 18%") + bt_console.print("ERROR: Take value should not exceed 18%") return result: bool = subtensor.set_take( @@ -1107,16 +1112,16 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): take=new_take, ) if not result: - bittensor.__console__.print("Could not set the take") + bt_console.print("Could not set the take") else: # Check if we are a delegate. is_delegate: bool = subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address) if not is_delegate: - bittensor.__console__.print( + bt_console.print( "Could not set the take [white]{}[/white]".format(subtensor.network) ) return - bittensor.__console__.print( + bt_console.print( "Successfully set the take on [white]{}[/white]".format( subtensor.network ) @@ -1134,11 +1139,11 @@ def add_args(parser: argparse.ArgumentParser): required=False, help="""Take as a float number""", ) - bittensor.wallet.add_args(set_take_parser) - bittensor.subtensor.add_args(set_take_parser) + Wallet.add_args(set_take_parser) + Subtensor.add_args(set_take_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) diff --git a/bittensor/btcli/commands/identity.py b/bittensor/btcli/commands/identity.py index 373cb8d87..962082c84 100644 --- a/bittensor/btcli/commands/identity.py +++ b/bittensor/btcli/commands/identity.py @@ -3,7 +3,12 @@ from rich.prompt import Prompt from sys import getsizeof from ...core.settings import networks -import bittensor +from bittensor.core.settings import bt_console +from bittensor.core.subtensor import Subtensor +from bittensor.core.config import Config +from bittensor_wallet import Wallet +from bittensor.utils.btlogging import logging +from bittensor.btcli.commands import defaults class SetIdentityCommand: @@ -54,24 +59,24 @@ class SetIdentityCommand: that makes changes to the blockchain state and should not be used programmatically as part of other scripts or applications. """ - - def run(cli: "bittensor.cli"): + @staticmethod + def run(cli): r"""Create a new or update existing identity on-chain.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) SetIdentityCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + logging.debug("closing subtensor connection") + + @staticmethod + def _run(cli, subtensor: "Subtensor"): r"""Create a new or update existing identity on-chain.""" - console = bittensor.__console__ - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) id_dict = { "display": cli.config.display, @@ -112,11 +117,11 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ).lower() == "n" ): - console.print(":cross_mark: Aborted!") + bt_console.print(":cross_mark: Aborted!") exit(0) wallet.coldkey # unlock coldkey - with console.status(":satellite: [bold green]Updating identity on-chain..."): + with bt_console.status(":satellite: [bold green]Updating identity on-chain..."): try: subtensor.update_identity( identified=identified, @@ -124,10 +129,10 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): params=id_dict, ) except Exception as e: - console.print(f"[red]:cross_mark: Failed![/red] {e}") + bt_console.print(f"[red]:cross_mark: Failed![/red] {e}") exit(1) - console.print(":white_heavy_check_mark: Success!") + bt_console.print(":white_heavy_check_mark: Success!") identity = subtensor.query_identity(identified or wallet.coldkey.ss58_address) @@ -139,28 +144,28 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): for key, value in identity.items(): table.add_row(key, str(value) if value is not None else "None") - console.print(table) + bt_console.print(table) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: config.wallet.name = Prompt.ask( - "Enter wallet name", default=bittensor.defaults.wallet.name + "Enter wallet name", default=defaults.wallet.name ) if not config.is_set("wallet.hotkey") and not config.no_prompt: config.wallet.hotkey = Prompt.ask( - "Enter wallet hotkey", default=bittensor.defaults.wallet.hotkey + "Enter wallet hotkey", default=defaults.wallet.hotkey ) if not config.is_set("subtensor.network") and not config.no_prompt: config.subtensor.network = Prompt.ask( "Enter subtensor network", - default=bittensor.defaults.subtensor.network, + default=defaults.subtensor.network, choices=networks, ) ( _, config.subtensor.chain_endpoint, - ) = bittensor.subtensor.determine_chain_endpoint_and_network( + ) = Subtensor.determine_chain_endpoint_and_network( config.subtensor.network ) if not config.is_set("display") and not config.no_prompt: @@ -235,8 +240,8 @@ def add_args(parser: argparse.ArgumentParser): type=str, help="""The twitter url for the identity.""", ) - bittensor.wallet.add_args(new_coldkey_parser) - bittensor.subtensor.add_args(new_coldkey_parser) + Wallet.add_args(new_coldkey_parser) + Subtensor.add_args(new_coldkey_parser) class GetIdentityCommand: @@ -272,22 +277,23 @@ class GetIdentityCommand: primarily used for informational purposes and has no side effects on the network state. """ - def run(cli: "bittensor.cli"): - r"""Queries the subtensor chain for user identity.""" + @staticmethod + def run(cli): + """Queries the subtensor chain for user identity.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) GetIdentityCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - console = bittensor.__console__ + @staticmethod + def _run(cli, subtensor: "Subtensor"): - with console.status(":satellite: [bold green]Querying chain identity..."): + with bt_console.status(":satellite: [bold green]Querying chain identity..."): identity = subtensor.query_identity(cli.config.key) table = Table(title="[bold white italic]On-Chain Identity") @@ -298,10 +304,10 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): for key, value in identity.items(): table.add_row(key, str(value) if value is not None else "None") - console.print(table) + bt_console.print(table) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("key") and not config.no_prompt: config.key = Prompt.ask( "Enter coldkey or hotkey ss58 address", default=None @@ -311,13 +317,13 @@ def check_config(config: "bittensor.config"): if not config.is_set("subtensor.network") and not config.no_prompt: config.subtensor.network = Prompt.ask( "Enter subtensor network", - default=bittensor.defaults.subtensor.network, + default=defaults.subtensor.network, choices=networks, ) ( _, config.subtensor.chain_endpoint, - ) = bittensor.subtensor.determine_chain_endpoint_and_network( + ) = Subtensor.determine_chain_endpoint_and_network( config.subtensor.network ) @@ -333,5 +339,5 @@ def add_args(parser: argparse.ArgumentParser): default=None, help="""The coldkey or hotkey ss58 address to query.""", ) - bittensor.wallet.add_args(new_coldkey_parser) - bittensor.subtensor.add_args(new_coldkey_parser) + Wallet.add_args(new_coldkey_parser) + Subtensor.add_args(new_coldkey_parser) diff --git a/bittensor/btcli/commands/inspect.py b/bittensor/btcli/commands/inspect.py index 5ab093bc3..ee04c728c 100644 --- a/bittensor/btcli/commands/inspect.py +++ b/bittensor/btcli/commands/inspect.py @@ -19,14 +19,11 @@ import os from typing import List, Tuple, Optional, Dict -from rich.console import Console from rich.prompt import Prompt from rich.table import Table from tqdm import tqdm -import bittensor from . import defaults -from ...core import settings from .utils import ( get_delegates_details, DelegatesDetails, @@ -34,21 +31,26 @@ get_all_wallets_for_path, filter_netuids_by_registered_hotkeys, ) +from bittensor.core.subtensor import Subtensor +from bittensor_wallet import Wallet +from bittensor.core.config import Config +from bittensor.core.settings import bt_console, delegates_details_url +from bittensor.utils.btlogging import logging +from bittensor.utils.balance import Balance +from bittensor.core.chain_data import DelegateInfo -console = Console() - -def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: +def _get_coldkey_wallets_for_path(path: str) -> List["Wallet"]: try: wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [bittensor.wallet(path=path, name=name) for name in wallet_names] + return [Wallet(path=path, name=name) for name in wallet_names] except StopIteration: # No wallet files found. wallets = [] return wallets -def _get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: +def _get_hotkey_wallets_for_wallet(wallet) -> List["Wallet"]: hotkey_wallets = [] hotkeys_path = wallet.path + "/" + wallet.name + "/hotkeys" try: @@ -57,7 +59,7 @@ def _get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: hotkey_files = [] for hotkey_file_name in hotkey_files: try: - hotkey_for_name = bittensor.wallet( + hotkey_for_name = Wallet( path=wallet.path, name=wallet.name, hotkey=hotkey_file_name ) if ( @@ -113,38 +115,38 @@ class InspectCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Inspect a cold, hot pair.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) InspectCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): if cli.config.get("all", d=False) == True: wallets = _get_coldkey_wallets_for_path(cli.config.wallet.path) all_hotkeys = get_all_wallets_for_path(cli.config.wallet.path) else: - wallets = [bittensor.wallet(config=cli.config)] + wallets = [Wallet(config=cli.config)] all_hotkeys = get_hotkey_wallets_for_wallet(wallets[0]) netuids = subtensor.get_all_subnet_netuids() netuids = filter_netuids_by_registered_hotkeys( cli, subtensor, netuids, all_hotkeys ) - bittensor.logging.debug(f"Netuids to check: {netuids}") + logging.debug(f"Netuids to check: {netuids}") registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=settings.delegates_details_url) + get_delegates_details(url=delegates_details_url) ) if registered_delegate_info is None: - bittensor.__console__.print( + bt_console.print( ":warning:[yellow]Could not get delegate info from chain.[/yellow]" ) registered_delegate_info = {} @@ -183,7 +185,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): "[overline white]Emission", footer_style="overline white", style="green" ) for wallet in tqdm(wallets): - delegates: List[Tuple[bittensor.DelegateInfo, bittensor.Balance]] = ( + delegates: List[Tuple[DelegateInfo, Balance]] = ( subtensor.get_delegated(coldkey_ss58=wallet.coldkeypub.ss58_address) ) if not wallet.coldkeypub_file.exists_on_device(): @@ -236,13 +238,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): str(netuid), f"{hotkey_name}{neuron.hotkey}", str(neuron.stake), - str(bittensor.Balance.from_tao(neuron.emission)), + str(Balance.from_tao(neuron.emission)), ) - bittensor.__console__.print(table) + bt_console.print(table) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if ( not config.is_set("wallet.name") and not config.no_prompt @@ -277,5 +279,5 @@ def add_args(parser: argparse.ArgumentParser): default=None, ) - bittensor.wallet.add_args(inspect_parser) - bittensor.subtensor.add_args(inspect_parser) + Wallet.add_args(inspect_parser) + Subtensor.add_args(inspect_parser) diff --git a/bittensor/btcli/commands/list.py b/bittensor/btcli/commands/list.py index 6d5ccec8a..7f1f3c0ce 100644 --- a/bittensor/btcli/commands/list.py +++ b/bittensor/btcli/commands/list.py @@ -15,14 +15,16 @@ # 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 os import argparse -import bittensor +import os + +from bittensor_wallet import Wallet + from rich import print -from rich.console import Console from rich.tree import Tree -console = Console() +from bittensor.core.config import Config +from bittensor.core.subtensor import Subtensor class ListCommand: @@ -60,10 +62,10 @@ def run(cli): ListCommand._run(cli, wallets) @staticmethod - def _run(cli: "bittensor.cli", wallets, return_value=False): + def _run(cli, wallets, return_value=False): root = Tree("Wallets") for w_name in wallets: - wallet_for_name = bittensor.wallet(path=cli.config.wallet.path, name=w_name) + wallet_for_name = Wallet(path=cli.config.wallet.path, name=w_name) try: if ( wallet_for_name.coldkeypub_file.exists_on_device() @@ -83,7 +85,7 @@ def _run(cli: "bittensor.cli", wallets, return_value=False): hotkeys = next(os.walk(os.path.expanduser(hotkeys_path))) if len(hotkeys) > 1: for h_name in hotkeys[2]: - hotkey_for_name = bittensor.wallet( + hotkey_for_name = Wallet( path=cli.config.wallet.path, name=w_name, hotkey=h_name ) try: @@ -110,14 +112,14 @@ def _run(cli: "bittensor.cli", wallets, return_value=False): return root @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): pass @staticmethod def add_args(parser: argparse.ArgumentParser): list_parser = parser.add_parser("list", help="""List wallets""") - bittensor.wallet.add_args(list_parser) - bittensor.subtensor.add_args(list_parser) + Wallet.add_args(list_parser) + Subtensor.add_args(list_parser) @staticmethod def get_tree(cli): diff --git a/bittensor/btcli/commands/metagraph.py b/bittensor/btcli/commands/metagraph.py index 1814053ce..11a0c83f2 100644 --- a/bittensor/btcli/commands/metagraph.py +++ b/bittensor/btcli/commands/metagraph.py @@ -17,14 +17,16 @@ import argparse -from rich.console import Console from rich.table import Table -import bittensor +from bittensor.core.config import Config +from bittensor.core.metagraph import Metagraph +from bittensor.core.settings import bt_console +from bittensor.core.subtensor import Subtensor +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging from .utils import check_netuid_set -console = Console() - class MetagraphCommand: """ @@ -73,33 +75,34 @@ class MetagraphCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Prints an entire metagraph.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) MetagraphCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + @staticmethod + def _run(cli, subtensor: "Subtensor"): r"""Prints an entire metagraph.""" - console = bittensor.__console__ + console = bt_console console.print( ":satellite: Syncing with chain: [white]{}[/white] ...".format( cli.config.subtensor.network ) ) - metagraph: bittensor.metagraph = subtensor.metagraph(netuid=cli.config.netuid) + metagraph: Metagraph = subtensor.metagraph(netuid=cli.config.netuid) metagraph.save() difficulty = subtensor.difficulty(cli.config.netuid) - subnet_emission = bittensor.Balance.from_tao( + subnet_emission = Balance.from_tao( subtensor.get_emission_value_by_subnet(cli.config.netuid) ) - total_issuance = bittensor.Balance.from_rao(subtensor.total_issuance().rao) + total_issuance = Balance.from_rao(subtensor.total_issuance().rao) TABLE_DATA = [] total_stake = 0.0 @@ -151,7 +154,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): metagraph.block.item(), sum(metagraph.active.tolist()), metagraph.n.item(), - bittensor.Balance.from_tao(total_stake), + Balance.from_tao(total_stake), total_issuance, difficulty, ) @@ -247,9 +250,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): console.print(table) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): check_netuid_set( - config, subtensor=bittensor.subtensor(config=config, log_verbose=False) + config, subtensor=Subtensor(config=config, log_verbose=False) ) @staticmethod @@ -265,4 +268,4 @@ def add_args(parser: argparse.ArgumentParser): default=False, ) - bittensor.subtensor.add_args(metagraph_parser) + Subtensor.add_args(metagraph_parser) diff --git a/bittensor/btcli/commands/misc.py b/bittensor/btcli/commands/misc.py index 6254ff112..8d6a34e8d 100644 --- a/bittensor/btcli/commands/misc.py +++ b/bittensor/btcli/commands/misc.py @@ -15,14 +15,15 @@ # 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 os import argparse -import bittensor -from rich.console import Console +import os + from rich.prompt import Prompt from rich.table import Table -console = Console() +from bittensor.core.config import Config +from bittensor.core.settings import bt_console +from bittensor.core.subtensor import Subtensor class UpdateCommand: @@ -58,7 +59,7 @@ def run(cli): os.system("pip install -e ~/.bittensor/bittensor/") @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.no_prompt: answer = Prompt.ask( "This will update the local bittensor package", @@ -73,7 +74,7 @@ def add_args(parser: argparse.ArgumentParser): "update", add_help=False, help="""Update bittensor """ ) - bittensor.subtensor.add_args(update_parser) + Subtensor.add_args(update_parser) class AutocompleteCommand: @@ -81,7 +82,6 @@ class AutocompleteCommand: @staticmethod def run(cli): - console = bittensor.__console__ shell_commands = { "Bash": "btcli --print-completion bash >> ~/.bashrc", "Zsh": "btcli --print-completion zsh >> ~/.zshrc", @@ -95,16 +95,16 @@ def run(cli): for shell, command in shell_commands.items(): table.add_row(shell, command) - console.print( + bt_console.print( "To enable autocompletion for Bittensor CLI, run the appropriate command for your shell:" ) - console.print(table) + bt_console.print(table) - console.print( + bt_console.print( "\n[bold]After running the command, execute the following to apply the changes:[/bold]" ) - console.print(" [yellow]source ~/.bashrc[/yellow] # For Bash and Zsh") - console.print(" [yellow]source ~/.tcshrc[/yellow] # For Tcsh") + bt_console.print(" [yellow]source ~/.bashrc[/yellow] # For Bash and Zsh") + bt_console.print(" [yellow]source ~/.tcshrc[/yellow] # For Tcsh") @staticmethod def add_args(parser): diff --git a/bittensor/btcli/commands/network.py b/bittensor/btcli/commands/network.py index 35199b957..03c943c0a 100644 --- a/bittensor/btcli/commands/network.py +++ b/bittensor/btcli/commands/network.py @@ -18,12 +18,9 @@ import argparse from typing import List, Optional, Dict, Union, Tuple -from rich.console import Console from rich.prompt import Prompt from rich.table import Table -import bittensor -from . import defaults # type: ignore from ...core import settings from .identity import SetIdentityCommand from .utils import ( @@ -32,8 +29,16 @@ check_netuid_set, normalize_hyperparameters, ) - -console = Console() +from bittensor.core.subtensor import Subtensor +from bittensor.api.extrinsics.utils import HYPERPARAMS +from bittensor_wallet import Wallet +from bittensor.core.config import Config +from bittensor.core.settings import bt_console +from bittensor.utils.btlogging import logging +from bittensor.utils.balance import Balance +from bittensor.utils import formatting +from bittensor.utils import RAOPERTAO +from bittensor.core.chain_data import SubnetHyperparameters, SubnetInfo class RegisterSubnetworkCommand: @@ -68,23 +73,23 @@ class RegisterSubnetworkCommand: """ @staticmethod - def run(cli: "bittensor.cli"): - r"""Register a subnetwork""" + def run(cli): + """Register a subnetwork""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) RegisterSubnetworkCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Register a subnetwork""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) # Call register command. success = subtensor.register_subnetwork( @@ -106,9 +111,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): SetIdentityCommand.run(cli) @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) + wallet_name = Prompt.ask("Enter wallet name", default=settings.defaults.wallet.name) config.wallet.name = str(wallet_name) @classmethod @@ -118,8 +123,8 @@ def add_args(cls, parser: argparse.ArgumentParser): help="""Create a new bittensor subnetwork on this chain.""", ) - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) + Wallet.add_args(parser) + Subtensor.add_args(parser) class SubnetLockCostCommand: @@ -155,35 +160,34 @@ class SubnetLockCostCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""View locking cost of creating a new subnetwork""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) SubnetLockCostCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""View locking cost of creating a new subnetwork""" - config = cli.config.copy() + def _run(cli, subtensor: "Subtensor"): + """View locking cost of creating a new subnetwork""" try: - bittensor.__console__.print( - f"Subnet lock cost: [green]{bittensor.utils.balance.Balance( subtensor.get_subnet_burn_cost() )}[/green]" + bt_console.print( + f"Subnet lock cost: [green]{Balance( subtensor.get_subnet_burn_cost() )}[/green]" ) except Exception as e: - bittensor.__console__.print( + bt_console.print( f"Subnet lock cost: [red]Failed to get subnet lock cost[/red]" f"Error: {e}" ) @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): pass @classmethod @@ -193,7 +197,7 @@ def add_args(cls, parser: argparse.ArgumentParser): help=""" Return the lock cost to register a subnet""", ) - bittensor.subtensor.add_args(parser) + Subtensor.add_args(parser) class SubnetListCommand: @@ -232,26 +236,26 @@ class SubnetListCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""List all subnet netuids in the network.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) SubnetListCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""List all subnet netuids in the network.""" - subnets: List[bittensor.SubnetInfo] = subtensor.get_all_subnets_info() + subnets: List["SubnetInfo"] = subtensor.get_all_subnets_info() rows = [] total_neurons = 0 - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( + delegate_info: Optional[Dict[str, "DelegatesDetails"]] = get_delegates_details( url=settings.delegates_details_url ) @@ -261,11 +265,11 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ( str(subnet.netuid), str(subnet.subnetwork_n), - str(bittensor.utils.formatting.millify(subnet.max_n)), - f"{subnet.emission_value / bittensor.utils.RAOPERTAO * 100:0.2f}%", + str(formatting.millify(subnet.max_n)), + f"{subnet.emission_value / RAOPERTAO * 100:0.2f}%", str(subnet.tempo), f"{subnet.burn!s:8.8}", - str(bittensor.utils.formatting.millify(subnet.difficulty)), + str(formatting.millify(subnet.difficulty)), f"{delegate_info[subnet.owner_ss58].name if subnet.owner_ss58 in delegate_info else subnet.owner_ss58}", ) ) @@ -299,10 +303,10 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): table.add_column("[overline white]SUDO", style="white") for row in rows: table.add_row(*row) - bittensor.__console__.print(table) + bt_console.print(table) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): pass @staticmethod @@ -310,33 +314,7 @@ def add_args(parser: argparse.ArgumentParser): list_subnets_parser = parser.add_parser( "list", help="""List all subnets on the network""" ) - bittensor.subtensor.add_args(list_subnets_parser) - - -HYPERPARAMS = { - "serving_rate_limit": "sudo_set_serving_rate_limit", - "min_difficulty": "sudo_set_min_difficulty", - "max_difficulty": "sudo_set_max_difficulty", - "weights_version": "sudo_set_weights_version_key", - "weights_rate_limit": "sudo_set_weights_set_rate_limit", - "max_weight_limit": "sudo_set_max_weight_limit", - "immunity_period": "sudo_set_immunity_period", - "min_allowed_weights": "sudo_set_min_allowed_weights", - "activity_cutoff": "sudo_set_activity_cutoff", - "network_registration_allowed": "sudo_set_network_registration_allowed", - "network_pow_registration_allowed": "sudo_set_network_pow_registration_allowed", - "min_burn": "sudo_set_min_burn", - "max_burn": "sudo_set_max_burn", - "adjustment_alpha": "sudo_set_adjustment_alpha", - "rho": "sudo_set_rho", - "kappa": "sudo_set_kappa", - "difficulty": "sudo_set_difficulty", - "bonds_moving_avg": "sudo_set_bonds_moving_average", - "commit_reveal_weights_interval": "sudo_set_commit_reveal_weights_interval", - "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", - "alpha_values": "sudo_set_alpha_values", - "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", -} + Subtensor.add_args(list_subnets_parser) class SubnetSudoCommand: @@ -362,25 +340,25 @@ class SubnetSudoCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Set subnet hyperparameters.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) SubnetSudoCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod def _run( - cli: "bittensor.cli", - subtensor: "bittensor.subtensor", + cli, + subtensor: "Subtensor", ): r"""Set subnet hyperparameters.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) print("\n") SubnetHyperparamsCommand.run(cli) if not cli.config.is_set("param") and not cli.config.no_prompt: @@ -417,14 +395,14 @@ def _run( ) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) + wallet_name = Prompt.ask("Enter wallet name", default=settings.defaults.wallet.name) config.wallet.name = str(wallet_name) if not config.is_set("netuid") and not config.no_prompt: check_netuid_set( - config, bittensor.subtensor(config=config, log_verbose=False) + config, Subtensor(config=config, log_verbose=False) ) @staticmethod @@ -436,8 +414,8 @@ def add_args(parser: argparse.ArgumentParser): parser.add_argument("--param", dest="param", type=str, required=False) parser.add_argument("--value", dest="value", type=str, required=False) - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) + Wallet.add_args(parser) + Subtensor.add_args(parser) class SubnetHyperparamsCommand: @@ -483,22 +461,22 @@ class SubnetHyperparamsCommand: """ @staticmethod - def run(cli: "bittensor.cli"): - r"""View hyperparameters of a subnetwork.""" + def run(cli): + """View hyperparameters of a subnetwork.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) SubnetHyperparamsCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""View hyperparameters of a subnetwork.""" - subnet: bittensor.SubnetHyperparameters = subtensor.get_subnet_hyperparameters( + subnet: SubnetHyperparameters = subtensor.get_subnet_hyperparameters( cli.config.netuid ) @@ -521,13 +499,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): for param, value, norm_value in normalized_values: table.add_row(" " + param, value, norm_value) - bittensor.__console__.print(table) + bt_console.print(table) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("netuid") and not config.no_prompt: check_netuid_set( - config, bittensor.subtensor(config=config, log_verbose=False) + config, Subtensor(config=config, log_verbose=False) ) @staticmethod @@ -538,7 +516,7 @@ def add_args(parser: argparse.ArgumentParser): parser.add_argument( "--netuid", dest="netuid", type=int, required=False, default=False ) - bittensor.subtensor.add_args(parser) + Subtensor.add_args(parser) class SubnetGetHyperparamsCommand: @@ -583,22 +561,22 @@ class SubnetGetHyperparamsCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""View hyperparameters of a subnetwork.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) SubnetGetHyperparamsCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""View hyperparameters of a subnetwork.""" - subnet: bittensor.SubnetHyperparameters = subtensor.get_subnet_hyperparameters( + def _run(cli, subtensor: "Subtensor"): + """View hyperparameters of a subnetwork.""" + subnet: SubnetHyperparameters = subtensor.get_subnet_hyperparameters( cli.config.netuid ) @@ -621,13 +599,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): for param, value, norm_value in normalized_values: table.add_row(" " + param, value, norm_value) - bittensor.__console__.print(table) + bt_console.print(table) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("netuid") and not config.no_prompt: check_netuid_set( - config, bittensor.subtensor(config=config, log_verbose=False) + config, Subtensor(config=config, log_verbose=False) ) @staticmethod @@ -636,7 +614,7 @@ def add_args(parser: argparse.ArgumentParser): parser.add_argument( "--netuid", dest="netuid", type=int, required=False, default=False ) - bittensor.subtensor.add_args(parser) + Subtensor.add_args(parser) def allowed_value( diff --git a/bittensor/btcli/commands/overview.py b/bittensor/btcli/commands/overview.py index 7a439e2fc..8daa56c42 100644 --- a/bittensor/btcli/commands/overview.py +++ b/bittensor/btcli/commands/overview.py @@ -20,14 +20,19 @@ from concurrent.futures import ProcessPoolExecutor from typing import List, Optional, Dict, Tuple +from bittensor_wallet import Wallet from fuzzywuzzy import fuzz from rich.align import Align -from rich.console import Console from rich.prompt import Prompt from rich.table import Table from tqdm import tqdm -import bittensor +from bittensor.core.chain_data import NeuronInfoLite +from bittensor.core.settings import bt_console +from bittensor.core.subtensor import Subtensor +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from bittensor.utils.networking import int_to_ip from . import defaults from .utils import ( get_hotkey_wallets_for_wallet, @@ -36,8 +41,6 @@ filter_netuids_by_registered_hotkeys, ) -console = Console() - class OverviewCommand: """ @@ -84,21 +87,21 @@ class OverviewCommand: def run(cli: "bittensor.cli"): r"""Prints an overview for the wallet's colkey.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) OverviewCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod def _get_total_balance( - total_balance: "bittensor.Balance", - subtensor: "bittensor.subtensor", + total_balance: "Balance", + subtensor: "Subtensor", cli: "bittensor.cli", - ) -> Tuple[List["bittensor.wallet"], "bittensor.Balance"]: + ) -> Tuple[List["Wallet"], "Balance"]: if cli.config.get("all", d=None): cold_wallets = get_coldkey_wallets_for_path(cli.config.wallet.path) for cold_wallet in tqdm(cold_wallets, desc="Pulling balances"): @@ -112,7 +115,7 @@ def _get_total_balance( all_hotkeys = get_all_wallets_for_path(cli.config.wallet.path) else: # We are only printing keys for a single coldkey - coldkey_wallet = bittensor.wallet(config=cli.config) + coldkey_wallet = Wallet(config=cli.config) if ( coldkey_wallet.coldkeypub_file.exists_on_device() and not coldkey_wallet.coldkeypub_file.is_encrypted() @@ -121,7 +124,7 @@ def _get_total_balance( coldkey_wallet.coldkeypub.ss58_address ) if not coldkey_wallet.coldkeypub_file.exists_on_device(): - console.print("[bold red]No wallets found.") + bt_console.print("[bold red]No wallets found.") return [], None all_hotkeys = get_hotkey_wallets_for_wallet(coldkey_wallet) @@ -129,8 +132,8 @@ def _get_total_balance( @staticmethod def _get_hotkeys( - cli: "bittensor.cli", all_hotkeys: List["bittensor.wallet"] - ) -> List["bittensor.wallet"]: + cli: "bittensor.cli", all_hotkeys: List["Wallet"] + ) -> List["Wallet"]: if not cli.config.get("all_hotkeys", False): # We are only showing hotkeys that are specified. all_hotkeys = [ @@ -148,7 +151,7 @@ def _get_hotkeys( return all_hotkeys @staticmethod - def _get_key_address(all_hotkeys: List["bittensor.wallet"]): + def _get_key_address(all_hotkeys: List["Wallet"]): hotkey_coldkey_to_hotkey_wallet = {} for hotkey_wallet in all_hotkeys: if hotkey_wallet.hotkey.ss58_address not in hotkey_coldkey_to_hotkey_wallet: @@ -171,7 +174,7 @@ def _process_neuron_results( for result in results: netuid, neurons_result, err_msg = result if err_msg is not None: - console.print(f"netuid '{netuid}': {err_msg}") + bt_console.print(f"netuid '{netuid}': {err_msg}") if len(neurons_result) == 0: # Remove netuid from overview if no neurons are found. @@ -182,13 +185,13 @@ def _process_neuron_results( neurons[str(netuid)] = neurons_result return neurons - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Prints an overview for the wallet's colkey.""" - console = bittensor.__console__ - wallet = bittensor.wallet(config=cli.config) + @staticmethod + def _run(cli, subtensor: "Subtensor"): + """Prints an overview for the wallet's coldkey.""" + wallet = Wallet(config=cli.config) all_hotkeys = [] - total_balance = bittensor.Balance(0) + total_balance = Balance(0) # We are printing for every coldkey. all_hotkeys, total_balance = OverviewCommand._get_total_balance( @@ -201,25 +204,25 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Check we have keys to display. if len(all_hotkeys) == 0: - console.print("[red]No wallets found.[/red]") + bt_console.print("[red]No wallets found.[/red]") return # Pull neuron info for all keys. - neurons: Dict[str, List[bittensor.NeuronInfoLite]] = {} + neurons: Dict[str, List[NeuronInfoLite]] = {} block = subtensor.block netuids = subtensor.get_all_subnet_netuids() netuids = filter_netuids_by_registered_hotkeys( cli, subtensor, netuids, all_hotkeys ) - bittensor.logging.debug(f"Netuids to check: {netuids}") + logging.debug(f"Netuids to check: {netuids}") for netuid in netuids: neurons[str(netuid)] = [] all_wallet_names = {wallet.name for wallet in all_hotkeys} all_coldkey_wallets = [ - bittensor.wallet(name=wallet_name) for wallet_name in all_wallet_names + Wallet(name=wallet_name) for wallet_name in all_wallet_names ] ( @@ -227,10 +230,10 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): hotkey_coldkey_to_hotkey_wallet, ) = OverviewCommand._get_key_address(all_hotkeys) - with console.status( + with bt_console.status( ":satellite: Syncing with chain: [white]{}[/white] ...".format( cli.config.subtensor.get( - "network", bittensor.defaults.subtensor.network + "network", defaults.subtensor.network ) ) ): @@ -254,7 +257,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) total_coldkey_stake_from_metagraph = defaultdict( - lambda: bittensor.Balance(0.0) + lambda: Balance(0.0) ) checked_hotkeys = set() for neuron_list in neurons.values(): @@ -312,7 +315,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): for result in results: coldkey_wallet, de_registered_stake, err_msg = result if err_msg is not None: - console.print(err_msg) + bt_console.print(err_msg) if len(de_registered_stake) == 0: continue # We have no de-registered stake with this coldkey. @@ -320,17 +323,17 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): de_registered_neurons = [] for hotkey_addr, our_stake in de_registered_stake: # Make a neuron info lite for this hotkey and coldkey. - de_registered_neuron = bittensor.NeuronInfoLite.get_null_neuron() + de_registered_neuron = NeuronInfoLite.get_null_neuron() de_registered_neuron.hotkey = hotkey_addr de_registered_neuron.coldkey = ( coldkey_wallet.coldkeypub.ss58_address ) - de_registered_neuron.total_stake = bittensor.Balance(our_stake) + de_registered_neuron.total_stake = Balance(our_stake) de_registered_neurons.append(de_registered_neuron) # Add this hotkey to the wallets dict - wallet_ = bittensor.wallet( + wallet_ = Wallet( name=wallet, ) wallet_.hotkey_ss58 = hotkey_addr @@ -389,7 +392,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): hotwallet = argparse.Namespace() hotwallet.name = nn.coldkey[:7] hotwallet.hotkey_str = nn.hotkey[:7] - nn: bittensor.NeuronInfoLite + nn: NeuronInfoLite uid = nn.uid active = nn.active stake = nn.total_stake.tao @@ -418,7 +421,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): "*" if validator_permit else "", str(last_update), ( - bittensor.utils.networking.int_to_ip(nn.axon_info.ip) + int_to_ip(nn.axon_info.ip) + ":" + str(nn.axon_info.port) if nn.axon_info.port != 0 @@ -604,7 +607,7 @@ def overview_sort_function(row): grid.add_row(table) - console.clear() + bt_console.clear() caption = "[italic][dim][white]Wallet balance: [green]\u03c4" + str( total_balance.tao @@ -612,7 +615,7 @@ def overview_sort_function(row): grid.add_row(Align(caption, vertical="middle", align="center")) # Print the entire table/grid - console.print(grid, width=cli.config.get("width", None)) + bt_console.print(grid, width=cli.config.get("width", None)) @staticmethod def _get_neurons_for_netuid( @@ -623,7 +626,7 @@ def _get_neurons_for_netuid( result: List["bittensor.NeuronInfoLite"] = [] try: - subtensor = bittensor.subtensor(config=subtensor_config, log_verbose=False) + subtensor = Subtensor(config=subtensor_config, log_verbose=False) all_neurons: List["bittensor.NeuronInfoLite"] = subtensor.neurons_lite( netuid=netuid @@ -640,7 +643,7 @@ def _get_neurons_for_netuid( finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") return netuid, result, None @@ -648,15 +651,15 @@ def _get_neurons_for_netuid( def _get_de_registered_stake_for_coldkey_wallet( args_tuple, ) -> Tuple[ - "bittensor.Wallet", List[Tuple[str, "bittensor.Balance"]], Optional[str] + "bittensor.Wallet", List[Tuple[str, "Balance"]], Optional[str] ]: subtensor_config, all_hotkey_addresses, coldkey_wallet = args_tuple # List of (hotkey_addr, our_stake) tuples. - result: List[Tuple[str, "bittensor.Balance"]] = [] + result: List[Tuple[str, "Balance"]] = [] try: - subtensor = bittensor.subtensor(config=subtensor_config, log_verbose=False) + subtensor = Subtensor(config=subtensor_config, log_verbose=False) # Pull all stake for our coldkey all_stake_info_for_coldkey = subtensor.get_stake_info_for_coldkey( @@ -689,7 +692,7 @@ def _filter_stake_info(stake_info: "bittensor.StakeInfo") -> bool: finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") return coldkey_wallet, result, None @@ -761,8 +764,8 @@ def add_args(parser: argparse.ArgumentParser): help="""Set the netuid(s) to filter by.""", default=None, ) - bittensor.wallet.add_args(overview_parser) - bittensor.subtensor.add_args(overview_parser) + Wallet.add_args(overview_parser) + Subtensor.add_args(overview_parser) @staticmethod def check_config(config: "bittensor.config"): diff --git a/bittensor/btcli/commands/register.py b/bittensor/btcli/commands/register.py index 53c12c621..f11b3bb0b 100644 --- a/bittensor/btcli/commands/register.py +++ b/bittensor/btcli/commands/register.py @@ -22,10 +22,13 @@ from rich.console import Console from rich.prompt import Prompt, Confirm -import bittensor from . import defaults from .utils import check_netuid_set, check_for_cuda_reg_config -from ...core.settings import networks +from bittensor.core.settings import networks, bt_console +from bittensor_wallet import Wallet +from bittensor.utils.btlogging import logging +from bittensor.core.subtensor import Subtensor +from bittensor.core.config import Config console = Console() @@ -64,27 +67,27 @@ class RegisterCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Register neuron by recycling some TAO.""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) RegisterCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Register neuron by recycling some TAO.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) # Verify subnet exists if not subtensor.subnet_exists(netuid=cli.config.netuid): - bittensor.__console__.print( + bt_console.print( f"[red]Subnet {cli.config.netuid} does not exist[/red]" ) sys.exit(1) @@ -95,7 +98,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Check balance is sufficient if balance < current_recycle: - bittensor.__console__.print( + bt_console.print( f"[red]Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO[/red]" ) sys.exit(1) @@ -126,11 +129,11 @@ def add_args(parser: argparse.ArgumentParser): default=argparse.SUPPRESS, ) - bittensor.wallet.add_args(register_parser) - bittensor.subtensor.add_args(register_parser) + Wallet.add_args(register_parser) + Subtensor.add_args(register_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if ( not config.is_set("subtensor.network") and not config.is_set("subtensor.chain_endpoint") @@ -141,13 +144,13 @@ def check_config(config: "bittensor.config"): choices=networks, default=defaults.subtensor.network, ) - _, endpoint = bittensor.subtensor.determine_chain_endpoint_and_network( + _, endpoint = Subtensor.determine_chain_endpoint_and_network( config.subtensor.network ) config.subtensor.chain_endpoint = endpoint check_netuid_set( - config, subtensor=bittensor.subtensor(config=config, log_verbose=False) + config, subtensor=Subtensor(config=config, log_verbose=False) ) if not config.is_set("wallet.name") and not config.no_prompt: @@ -195,26 +198,26 @@ class PowRegisterCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Register neuron.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) PowRegisterCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Register neuron.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) # Verify subnet exists if not subtensor.subnet_exists(netuid=cli.config.netuid): - bittensor.__console__.print( + bt_console.print( f"[red]Subnet {cli.config.netuid} does not exist[/red]" ) sys.exit(1) @@ -325,11 +328,11 @@ def add_args(parser: argparse.ArgumentParser): required=False, ) - bittensor.wallet.add_args(register_parser) - bittensor.subtensor.add_args(register_parser) + Wallet.add_args(register_parser) + Subtensor.add_args(register_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if ( not config.is_set("subtensor.network") and not config.is_set("subtensor.chain_endpoint") @@ -340,13 +343,13 @@ def check_config(config: "bittensor.config"): choices=networks, default=defaults.subtensor.network, ) - _, endpoint = bittensor.subtensor.determine_chain_endpoint_and_network( + _, endpoint = Subtensor.determine_chain_endpoint_and_network( config.subtensor.network ) config.subtensor.chain_endpoint = endpoint check_netuid_set( - config, subtensor=bittensor.subtensor(config=config, log_verbose=False) + config, subtensor=Subtensor(config=config, log_verbose=False) ) if not config.is_set("wallet.name") and not config.no_prompt: @@ -397,22 +400,22 @@ class RunFaucetCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Register neuron.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) RunFaucetCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Register neuron.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) success = subtensor.run_faucet( wallet=wallet, prompt=not cli.config.no_prompt, @@ -431,7 +434,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ), ) if not success: - bittensor.logging.error("Faucet run failed.") + logging.error("Faucet run failed.") sys.exit(1) @staticmethod @@ -511,11 +514,11 @@ def add_args(parser: argparse.ArgumentParser): help="""Set the number of Threads Per Block for CUDA.""", required=False, ) - bittensor.wallet.add_args(run_faucet_parser) - bittensor.subtensor.add_args(run_faucet_parser) + Wallet.add_args(run_faucet_parser) + Subtensor.add_args(run_faucet_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -525,7 +528,7 @@ def check_config(config: "bittensor.config"): class SwapHotkeyCommand: @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): """ Executes the ``swap_hotkey`` command to swap the hotkeys for a neuron on the network. @@ -542,24 +545,24 @@ def run(cli: "bittensor.cli"): btcli wallet swap_hotkey --wallet.name your_wallet_name --wallet.hotkey original_hotkey --wallet.hotkey_b new_hotkey """ try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) SwapHotkeyCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Swap your hotkey for all registered axons on the network.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) # This creates an unnecessary amount of extra data, but simplifies implementation. new_config = deepcopy(cli.config) new_config.wallet.hotkey = new_config.wallet.hotkey_b - new_wallet = bittensor.wallet(config=new_config) + new_wallet = Wallet(config=new_config) subtensor.swap_hotkey( wallet=wallet, @@ -583,11 +586,11 @@ def add_args(parser: argparse.ArgumentParser): required=False, ) - bittensor.wallet.add_args(swap_hotkey_parser) - bittensor.subtensor.add_args(swap_hotkey_parser) + Wallet.add_args(swap_hotkey_parser) + Subtensor.add_args(swap_hotkey_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if ( not config.is_set("subtensor.network") and not config.is_set("subtensor.chain_endpoint") @@ -598,7 +601,7 @@ def check_config(config: "bittensor.config"): choices=networks, default=defaults.subtensor.network, ) - _, endpoint = bittensor.subtensor.determine_chain_endpoint_and_network( + _, endpoint = Subtensor.determine_chain_endpoint_and_network( config.subtensor.network ) config.subtensor.chain_endpoint = endpoint diff --git a/bittensor/btcli/commands/root.py b/bittensor/btcli/commands/root.py index 2aae7e5e3..12d8725a9 100644 --- a/bittensor/btcli/commands/root.py +++ b/bittensor/btcli/commands/root.py @@ -19,7 +19,7 @@ import typing import argparse import numpy as np -import bittensor + from typing import List, Optional, Dict from rich.console import Console from rich.prompt import Prompt @@ -27,7 +27,12 @@ from .utils import get_delegates_details, DelegatesDetails from . import defaults -from ...core import settings +from bittensor.core.settings import bt_console, delegates_details_url +from bittensor.core.subtensor import Subtensor +from bittensor.core.config import Config +from bittensor_wallet import Wallet +from bittensor.utils.btlogging import logging +from bittensor.core.chain_data import NeuronInfoLite, SubnetInfo console = Console() @@ -55,22 +60,22 @@ class RootRegisterCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Register to root network.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) RootRegisterCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Register to root network.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) subtensor.root_register(wallet=wallet, prompt=not cli.config.no_prompt) @@ -80,11 +85,11 @@ def add_args(parser: argparse.ArgumentParser): "register", help="""Register a wallet to the root network.""" ) - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) + Wallet.add_args(parser) + Subtensor.add_args(parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -125,20 +130,20 @@ class RootList: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""List the root network""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) RootList._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""List the root network""" console.print( ":satellite: Syncing with chain: [white]{}[/white] ...".format( @@ -147,11 +152,11 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) senate_members = subtensor.get_senate_members() - root_neurons: typing.List[bittensor.NeuronInfoLite] = subtensor.neurons_lite( + root_neurons: typing.List["NeuronInfoLite"] = subtensor.neurons_lite( netuid=0 ) delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - url=settings.delegates_details_url + url=delegates_details_url ) table = Table(show_footer=False) @@ -207,15 +212,15 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): table.box = None table.pad_edge = False table.width = None - bittensor.__console__.print(table) + bt_console.print(table) @staticmethod def add_args(parser: argparse.ArgumentParser): parser = parser.add_parser("list", help="""List the root network""") - bittensor.subtensor.add_args(parser) + Subtensor.add_args(parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): pass @@ -268,28 +273,28 @@ class RootSetBoostCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Set weights for root network.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) RootSetBoostCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Set weights for root network.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) root = subtensor.metagraph(0, lite=False) try: my_uid = root.hotkeys.index(wallet.hotkey.ss58_address) except ValueError: - bittensor.__console__.print( + bt_console.print( "Wallet hotkey: {} not found in root metagraph".format(wallet.hotkey) ) exit() @@ -297,13 +302,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): prev_weight = my_weights[cli.config.netuid] new_weight = prev_weight + cli.config.amount - bittensor.__console__.print( + bt_console.print( f"Boosting weight for netuid {cli.config.netuid} from {prev_weight} -> {new_weight}" ) my_weights[cli.config.netuid] = new_weight all_netuids = np.arange(len(my_weights)) - bittensor.__console__.print("Setting root weights...") + bt_console.print("Setting root weights...") subtensor.root_set_weights( wallet=wallet, netuids=all_netuids, @@ -322,11 +327,11 @@ def add_args(parser: argparse.ArgumentParser): parser.add_argument("--netuid", dest="netuid", type=int, required=False) parser.add_argument("--increase", dest="amount", type=float, required=False) - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) + Wallet.add_args(parser) + Subtensor.add_args(parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -386,23 +391,23 @@ class RootSetSlashCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): """Set weights for root network with decreased values.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) RootSetSlashCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - wallet = bittensor.wallet(config=cli.config) + def _run(cli, subtensor: "Subtensor"): + wallet = Wallet(config=cli.config) - bittensor.__console__.print( + bt_console.print( "Slashing weight for subnet: {} by amount: {}".format( cli.config.netuid, cli.config.amount ) @@ -411,7 +416,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): try: my_uid = root.hotkeys.index(wallet.hotkey.ss58_address) except ValueError: - bittensor.__console__.print( + bt_console.print( "Wallet hotkey: {} not found in root metagraph".format(wallet.hotkey) ) exit() @@ -438,11 +443,11 @@ def add_args(parser: argparse.ArgumentParser): parser.add_argument("--netuid", dest="netuid", type=int, required=False) parser.add_argument("--decrease", dest="amount", type=float, required=False) - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) + Wallet.add_args(parser) + Subtensor.add_args(parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -478,23 +483,23 @@ class RootSetWeightsCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Set weights for root network.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) RootSetWeightsCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Set weights for root network.""" - wallet = bittensor.wallet(config=cli.config) - subnets: List[bittensor.SubnetInfo] = subtensor.get_all_subnets_info() + wallet = Wallet(config=cli.config) + subnets: List["SubnetInfo"] = subtensor.get_all_subnets_info() # Get values if not set. if not cli.config.is_set("netuids"): @@ -544,11 +549,11 @@ def add_args(parser: argparse.ArgumentParser): parser.add_argument("--netuids", dest="netuids", type=str, required=False) parser.add_argument("--weights", dest="weights", type=str, required=False) - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) + Wallet.add_args(parser) + Subtensor.add_args(parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -592,21 +597,21 @@ class RootGetWeightsCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Get weights for root network.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) RootGetWeightsCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Get weights for root network.""" + def _run(cli, subtensor: "Subtensor"): + """Get weights for root network.""" weights = subtensor.weights(0) table = Table(show_footer=False) @@ -667,7 +672,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): table.box = None table.pad_edge = False table.width = None - bittensor.__console__.print(table) + bt_console.print(table) @staticmethod def add_args(parser: argparse.ArgumentParser): @@ -675,9 +680,9 @@ def add_args(parser: argparse.ArgumentParser): "get_weights", help="""Get weights for root network.""" ) - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) + Wallet.add_args(parser) + Subtensor.add_args(parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): pass diff --git a/bittensor/btcli/commands/senate.py b/bittensor/btcli/commands/senate.py index 6a8e80db7..0e198f28a 100644 --- a/bittensor/btcli/commands/senate.py +++ b/bittensor/btcli/commands/senate.py @@ -19,14 +19,17 @@ import argparse from typing import Optional, Dict +from bittensor_wallet import Wallet from rich.console import Console from rich.prompt import Prompt, Confirm from rich.table import Table -import bittensor +from bittensor.btcli.commands.utils import get_delegates_details, DelegatesDetails +from bittensor.core.config import Config +from bittensor.core.settings import bt_console, delegates_details_url +from bittensor.core.subtensor import Subtensor +from bittensor.utils.btlogging import logging from . import defaults -from ...core import settings -from .utils import get_delegates_details, DelegatesDetails console = Console() @@ -54,19 +57,19 @@ def run(cli: "bittensor.cli"): r"""View Bittensor's governance protocol proposals""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) SenateCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli: "bittensor.cli", subtensor: "Subtensor"): r"""View Bittensor's governance protocol proposals""" - console = bittensor.__console__ + console = bt_console console.print( ":satellite: Syncing with chain: [white]{}[/white] ...".format( cli.config.subtensor.network @@ -75,7 +78,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): senate_members = subtensor.get_senate_members() delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - url=settings.delegates_details_url + url=delegates_details_url ) table = Table(show_footer=False) @@ -110,7 +113,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): console.print(table) @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): None @classmethod @@ -119,8 +122,8 @@ def add_args(cls, parser: argparse.ArgumentParser): "senate", help="""View senate and it's members""" ) - bittensor.wallet.add_args(senate_parser) - bittensor.subtensor.add_args(senate_parser) + Wallet.add_args(senate_parser) + Subtensor.add_args(senate_parser) def format_call_data(call_data: "bittensor.ProposalCallData") -> str: @@ -193,19 +196,19 @@ def run(cli: "bittensor.cli"): r"""View Bittensor's governance protocol proposals""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) ProposalsCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli: "bittensor.cli", subtensor: "Subtensor"): r"""View Bittensor's governance protocol proposals""" - console = bittensor.__console__ + console = bt_console console.print( ":satellite: Syncing with chain: [white]{}[/white] ...".format( subtensor.network @@ -216,7 +219,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): proposals = subtensor.get_proposals() registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=settings.delegates_details_url) + get_delegates_details(url=delegates_details_url) ) table = Table(show_footer=False) @@ -272,7 +275,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): console.print(table) @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): None @classmethod @@ -281,8 +284,8 @@ def add_args(cls, parser: argparse.ArgumentParser): "proposals", help="""View active triumvirate proposals and their status""" ) - bittensor.wallet.add_args(proposals_parser) - bittensor.subtensor.add_args(proposals_parser) + Wallet.add_args(proposals_parser) + Subtensor.add_args(proposals_parser) class ShowVotesCommand: @@ -316,17 +319,17 @@ def run(cli: "bittensor.cli"): r"""View Bittensor's governance protocol proposals active votes""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) ShowVotesCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli: "bittensor.cli", subtensor: "Subtensor"): r"""View Bittensor's governance protocol proposals active votes""" console.print( ":satellite: Syncing with chain: [white]{}[/white] ...".format( @@ -342,12 +345,12 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): return proposal_vote_data = subtensor.get_vote_data(proposal_hash) - if proposal_vote_data == None: + if proposal_vote_data is None: console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") return registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=settings.delegates_details_url) + get_delegates_details(url=delegates_details_url) ) table = Table(show_footer=False) @@ -374,7 +377,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): console.print(table) @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): if config.proposal_hash == "" and not config.no_prompt: proposal_hash = Prompt.ask("Enter proposal hash") config.proposal_hash = str(proposal_hash) @@ -392,8 +395,8 @@ def add_args(cls, parser: argparse.ArgumentParser): help="""Set the proposal to show votes for.""", default="", ) - bittensor.wallet.add_args(show_votes_parser) - bittensor.subtensor.add_args(show_votes_parser) + Wallet.add_args(show_votes_parser) + Subtensor.add_args(show_votes_parser) class SenateRegisterCommand: @@ -420,19 +423,19 @@ def run(cli: "bittensor.cli"): r"""Register to participate in Bittensor's governance protocol proposals""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) SenateRegisterCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli: "bittensor.cli", subtensor: "Subtensor"): r"""Register to participate in Bittensor's governance protocol proposals""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) # Unlock the wallet. wallet.hotkey @@ -458,7 +461,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): subtensor.register_senate(wallet=wallet, prompt=not cli.config.no_prompt) @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -474,8 +477,8 @@ def add_args(cls, parser: argparse.ArgumentParser): help="""Register as a senate member to participate in proposals""", ) - bittensor.wallet.add_args(senate_register_parser) - bittensor.subtensor.add_args(senate_register_parser) + Wallet.add_args(senate_register_parser) + Subtensor.add_args(senate_register_parser) class SenateLeaveCommand: @@ -502,19 +505,19 @@ def run(cli: "bittensor.cli"): r"""Discard membership in Bittensor's governance protocol proposals""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) SenateLeaveCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.cli"): r"""Discard membership in Bittensor's governance protocol proposals""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) # Unlock the wallet. wallet.hotkey @@ -531,7 +534,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.cli"): subtensor.leave_senate(wallet=wallet, prompt=not cli.config.no_prompt) @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -547,8 +550,8 @@ def add_args(cls, parser: argparse.ArgumentParser): help="""Discard senate membership in the governance protocol""", ) - bittensor.wallet.add_args(senate_leave_parser) - bittensor.subtensor.add_args(senate_leave_parser) + Wallet.add_args(senate_leave_parser) + Subtensor.add_args(senate_leave_parser) class VoteCommand: @@ -576,19 +579,19 @@ def run(cli: "bittensor.cli"): r"""Vote in Bittensor's governance protocol proposals""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) VoteCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli: "bittensor.cli", subtensor: "Subtensor"): r"""Vote in Bittensor's governance protocol proposals""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) proposal_hash = cli.config.proposal_hash if len(proposal_hash) == 0: @@ -624,7 +627,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -650,5 +653,5 @@ def add_args(cls, parser: argparse.ArgumentParser): help="""Set the proposal to show votes for.""", default="", ) - bittensor.wallet.add_args(vote_parser) - bittensor.subtensor.add_args(vote_parser) + Wallet.add_args(vote_parser) + Subtensor.add_args(vote_parser) diff --git a/bittensor/btcli/commands/stake.py b/bittensor/btcli/commands/stake.py index cdf7ede04..2e9d96c0c 100644 --- a/bittensor/btcli/commands/stake.py +++ b/bittensor/btcli/commands/stake.py @@ -20,23 +20,24 @@ import sys from typing import List, Union, Optional, Dict, Tuple -from rich.console import Console +from bittensor_wallet import Wallet from rich.prompt import Confirm, Prompt from rich.table import Table from tqdm import tqdm -import bittensor +from bittensor.core.config import Config +from bittensor.core.settings import bt_console, tao_symbol, delegates_details_url +from bittensor.core.subtensor import Subtensor +from bittensor.utils import is_valid_ss58_address from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging from . import defaults -from ...core import settings from .utils import ( get_hotkey_wallets_for_wallet, get_delegates_details, DelegatesDetails, ) -console = Console() - class StakeCommand: """ @@ -68,30 +69,30 @@ class StakeCommand: """ @staticmethod - def run(cli: "bittensor.cli"): - r"""Stake token of amount to hotkey(s).""" + def run(cli): + """Stake token of amount to hotkey(s).""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) StakeCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Stake token of amount to hotkey(s).""" config = cli.config.copy() - wallet = bittensor.wallet(config=config) + wallet = Wallet(config=config) # Get the hotkey_names (if any) and the hotkey_ss58s. hotkeys_to_stake_to: List[Tuple[Optional[str], str]] = [] if config.get("all_hotkeys"): # Stake to all hotkeys. - all_hotkeys: List[bittensor.wallet] = get_hotkey_wallets_for_wallet( + all_hotkeys: List[Wallet] = get_hotkey_wallets_for_wallet( wallet=wallet ) # Get the hotkeys to exclude. (d)efault to no exclusions. @@ -106,13 +107,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): elif config.get("hotkeys"): # Stake to specific hotkeys. for hotkey_ss58_or_hotkey_name in config.get("hotkeys"): - if bittensor.utils.is_valid_ss58_address(hotkey_ss58_or_hotkey_name): + if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): # If the hotkey is a valid ss58 address, we add it to the list. hotkeys_to_stake_to.append((None, hotkey_ss58_or_hotkey_name)) else: # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. # We then get the hotkey from the wallet and add it to the list. - wallet_ = bittensor.wallet( + wallet_ = Wallet( config=config, hotkey=hotkey_ss58_or_hotkey_name ) hotkeys_to_stake_to.append( @@ -122,11 +123,11 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Only config.wallet.hotkey is specified. # so we stake to that single hotkey. hotkey_ss58_or_name = config.wallet.get("hotkey") - if bittensor.utils.is_valid_ss58_address(hotkey_ss58_or_name): + if is_valid_ss58_address(hotkey_ss58_or_name): hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] else: # Hotkey is not a valid ss58 address, so we assume it is a hotkey name. - wallet_ = bittensor.wallet(config=config, hotkey=hotkey_ss58_or_name) + wallet_ = Wallet(config=config, hotkey=hotkey_ss58_or_name) hotkeys_to_stake_to = [ (wallet_.hotkey_str, wallet_.hotkey.ss58_address) ] @@ -135,7 +136,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # so we stake to that single hotkey. assert config.wallet.hotkey is not None hotkeys_to_stake_to = [ - (None, bittensor.wallet(config=config).hotkey.ss58_address) + (None, Wallet(config=config).hotkey.ss58_address) ] # Get coldkey balance @@ -148,13 +149,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Hotkey is not registered. if len(hotkeys_to_stake_to) == 1: # Only one hotkey, error - bittensor.__console__.print( + bt_console.print( f"[red]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Aborting.[/red]" ) return None else: # Otherwise, print warning and skip - bittensor.__console__.print( + bt_console.print( f"[yellow]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Skipping.[/yellow]" ) continue @@ -185,7 +186,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): if len(final_hotkeys) == 0: # No hotkeys to stake to. - bittensor.__console__.print( + bt_console.print( "Not enough balance to stake to any hotkeys or max_stake is less than current stake." ) return None @@ -196,7 +197,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): f"Do you want to stake to the following keys from {wallet.name}:\n" + "".join( [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {settings.tao_symbol}' if amount else 'All'}[/bold white]\n" + f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {tao_symbol}' if amount else 'All'}[/bold white]\n" for hotkey, amount in zip(final_hotkeys, final_amounts) ] ) @@ -222,7 +223,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -251,7 +252,7 @@ def check_config(cls, config: "bittensor.config"): try: config.amount = float(amount) except ValueError: - console.print( + bt_console.print( ":cross_mark:[red]Invalid Tao amount[/red] [bold white]{}[/bold white]".format( amount ) @@ -297,21 +298,21 @@ def add_args(cls, parser: argparse.ArgumentParser): default=False, help="""To specify all hotkeys. Specifying hotkeys will exclude them from this all.""", ) - bittensor.wallet.add_args(stake_parser) - bittensor.subtensor.add_args(stake_parser) + Wallet.add_args(stake_parser) + Subtensor.add_args(stake_parser) -def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: +def _get_coldkey_wallets_for_path(path: str) -> List["Wallet"]: try: wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [bittensor.wallet(path=path, name=name) for name in wallet_names] + return [Wallet(path=path, name=name) for name in wallet_names] except StopIteration: # No wallet files found. wallets = [] return wallets -def _get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: +def _get_hotkey_wallets_for_wallet(wallet) -> List["Wallet"]: hotkey_wallets = [] hotkeys_path = wallet.path + "/" + wallet.name + "/hotkeys" try: @@ -320,7 +321,7 @@ def _get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: hotkey_files = [] for hotkey_file_name in hotkey_files: try: - hotkey_for_name = bittensor.wallet( + hotkey_for_name = Wallet( path=wallet.path, name=wallet.name, hotkey=hotkey_file_name ) if ( @@ -364,27 +365,27 @@ class StakeShow: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Show all stake accounts.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) StakeShow._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): """Show all stake accounts.""" if cli.config.get("all", d=False): wallets = _get_coldkey_wallets_for_path(cli.config.wallet.path) else: - wallets = [bittensor.wallet(config=cli.config)] + wallets = [Wallet(config=cli.config)] registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=settings.delegates_details_url) + get_delegates_details(url=delegates_details_url) ) def get_stake_accounts( @@ -542,10 +543,10 @@ def get_all_wallet_accounts( table.add_row( "", "", value["name"], value["stake"], str(value["rate"]) + "/d" ) - bittensor.__console__.print(table) + bt_console.print(table) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if ( not config.get("all", d=None) and not config.is_set("wallet.name") @@ -566,5 +567,5 @@ def add_args(parser: argparse.ArgumentParser): default=False, ) - bittensor.wallet.add_args(list_parser) - bittensor.subtensor.add_args(list_parser) + Wallet.add_args(list_parser) + Subtensor.add_args(list_parser) diff --git a/bittensor/btcli/commands/transfer.py b/bittensor/btcli/commands/transfer.py index c7644683f..ccc224094 100644 --- a/bittensor/btcli/commands/transfer.py +++ b/bittensor/btcli/commands/transfer.py @@ -17,12 +17,15 @@ import sys import argparse -import bittensor -from rich.console import Console from rich.prompt import Prompt -from . import defaults -console = Console() +from bittensor_wallet import Wallet +from bittensor.core.settings import bt_console +from bittensor.core.subtensor import Subtensor +from bittensor.utils.btlogging import logging +from bittensor.core.config import Config +from . import defaults +from bittensor.utils import is_valid_bittensor_address_or_public_key class TransferCommand: @@ -50,22 +53,22 @@ class TransferCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Transfer token of amount to destination.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) TransferCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Transfer token of amount to destination.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) subtensor.transfer( wallet=wallet, dest=cli.config.dest, @@ -75,7 +78,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -83,18 +86,18 @@ def check_config(config: "bittensor.config"): # Get destination. if not config.dest and not config.no_prompt: dest = Prompt.ask("Enter destination public key: (ss58 or ed2519)") - if not bittensor.utils.is_valid_bittensor_address_or_public_key(dest): + if not is_valid_bittensor_address_or_public_key(dest): sys.exit() else: config.dest = str(dest) # Get current balance and print to user. if not config.no_prompt: - wallet = bittensor.wallet(config=config) - subtensor = bittensor.subtensor(config=config, log_verbose=False) - with bittensor.__console__.status(":satellite: Checking Balance..."): + wallet = Wallet(config=config) + subtensor = Subtensor(config=config, log_verbose=False) + with bt_console.status(":satellite: Checking Balance..."): account_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bittensor.__console__.print( + bt_console.print( "Balance: [green]{}[/green]".format(account_balance) ) @@ -105,18 +108,14 @@ def check_config(config: "bittensor.config"): try: config.amount = float(amount) except ValueError: - console.print( + bt_console.print( ":cross_mark:[red] Invalid TAO amount[/red] [bold white]{}[/bold white]".format( amount ) ) sys.exit() else: - console.print( - ":cross_mark:[red] Invalid TAO amount[/red] [bold white]{}[/bold white]".format( - amount - ) - ) + bt_console.print(":cross_mark:[red] Invalid TAO amount[/red]") sys.exit(1) @staticmethod @@ -129,5 +128,5 @@ def add_args(parser: argparse.ArgumentParser): "--amount", dest="amount", type=float, required=False ) - bittensor.wallet.add_args(transfer_parser) - bittensor.subtensor.add_args(transfer_parser) + Wallet.add_args(transfer_parser) + Subtensor.add_args(transfer_parser) diff --git a/bittensor/btcli/commands/unstake.py b/bittensor/btcli/commands/unstake.py index b56e255b8..6f7a280a8 100644 --- a/bittensor/btcli/commands/unstake.py +++ b/bittensor/btcli/commands/unstake.py @@ -18,18 +18,19 @@ import sys from typing import List, Union, Optional, Tuple -from rich.console import Console +from bittensor_wallet import Wallet from rich.prompt import Confirm, Prompt from tqdm import tqdm -import bittensor +from bittensor.core.config import Config +from bittensor.core.settings import tao_symbol, bt_console +from bittensor.core.subtensor import Subtensor +from bittensor.utils import is_valid_ss58_address from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging from . import defaults -from ...core import settings from .utils import get_hotkey_wallets_for_wallet -console = Console() - class UnStakeCommand: """ @@ -60,7 +61,7 @@ class UnStakeCommand: """ @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -102,7 +103,7 @@ def check_config(cls, config: "bittensor.config"): try: config.amount = float(amount) except ValueError: - console.print( + bt_console.print( f":cross_mark:[red] Invalid Tao amount[/red] [bold white]{amount}[/bold white]" ) sys.exit() @@ -151,28 +152,28 @@ def add_args(command_parser): default=False, help="""To specify all hotkeys. Specifying hotkeys will exclude them from this all.""", ) - bittensor.wallet.add_args(unstake_parser) - bittensor.subtensor.add_args(unstake_parser) + Wallet.add_args(unstake_parser) + Subtensor.add_args(unstake_parser) @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Unstake token of amount from hotkey(s).""" try: config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=config, log_verbose=False ) UnStakeCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Unstake token of amount from hotkey(s).""" config = cli.config.copy() - wallet = bittensor.wallet(config=config) + wallet = Wallet(config=config) # Get the hotkey_names (if any) and the hotkey_ss58s. hotkeys_to_unstake_from: List[Tuple[Optional[str], str]] = [] @@ -181,7 +182,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): hotkeys_to_unstake_from = [(None, cli.config.get("hotkey_ss58address"))] elif cli.config.get("all_hotkeys"): # Stake to all hotkeys. - all_hotkeys: List[bittensor.wallet] = get_hotkey_wallets_for_wallet( + all_hotkeys: List["Wallet"] = get_hotkey_wallets_for_wallet( wallet=wallet ) # Get the hotkeys to exclude. (d)efault to no exclusions. @@ -196,13 +197,13 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): elif cli.config.get("hotkeys"): # Stake to specific hotkeys. for hotkey_ss58_or_hotkey_name in cli.config.get("hotkeys"): - if bittensor.utils.is_valid_ss58_address(hotkey_ss58_or_hotkey_name): + if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): # If the hotkey is a valid ss58 address, we add it to the list. hotkeys_to_unstake_from.append((None, hotkey_ss58_or_hotkey_name)) else: # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. # We then get the hotkey from the wallet and add it to the list. - wallet_ = bittensor.wallet( + wallet_ = Wallet( config=cli.config, hotkey=hotkey_ss58_or_hotkey_name ) hotkeys_to_unstake_from.append( @@ -212,11 +213,11 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Only cli.config.wallet.hotkey is specified. # so we stake to that single hotkey. hotkey_ss58_or_name = cli.config.wallet.get("hotkey") - if bittensor.utils.is_valid_ss58_address(hotkey_ss58_or_name): + if is_valid_ss58_address(hotkey_ss58_or_name): hotkeys_to_unstake_from = [(None, hotkey_ss58_or_name)] else: # Hotkey is not a valid ss58 address, so we assume it is a hotkey name. - wallet_ = bittensor.wallet( + wallet_ = Wallet( config=cli.config, hotkey=hotkey_ss58_or_name ) hotkeys_to_unstake_from = [ @@ -227,7 +228,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # so we stake to that single hotkey. assert cli.config.wallet.hotkey is not None hotkeys_to_unstake_from = [ - (None, bittensor.wallet(config=cli.config).hotkey.ss58_address) + (None, Wallet(config=cli.config).hotkey.ss58_address) ] final_hotkeys: List[Tuple[str, str]] = [] @@ -240,7 +241,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): hotkey_stake: Balance = subtensor.get_stake_for_coldkey_and_hotkey( hotkey_ss58=hotkey[1], coldkey_ss58=wallet.coldkeypub.ss58_address ) - if unstake_amount_tao == None: + if unstake_amount_tao is None: unstake_amount_tao = hotkey_stake.tao if cli.config.get("max_stake"): # Get the current stake of the hotkey from this coldkey. @@ -263,7 +264,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): if len(final_hotkeys) == 0: # No hotkeys to unstake from. - bittensor.__console__.print( + bt_console.print( "Not enough stake to unstake from any hotkeys or max_stake is more than current stake." ) return None @@ -274,7 +275,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): f"Do you want to unstake from the following keys to {wallet.name}:\n" + "".join( [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {settings.tao_symbol}' if amount else 'All'}[/bold white]\n" + f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {tao_symbol}' if amount else 'All'}[/bold white]\n" for hotkey, amount in zip(final_hotkeys, final_amounts) ] ) diff --git a/bittensor/btcli/commands/utils.py b/bittensor/btcli/commands/utils.py index 3b0270fcf..c3d871ca6 100644 --- a/bittensor/btcli/commands/utils.py +++ b/bittensor/btcli/commands/utils.py @@ -21,17 +21,18 @@ from typing import List, Dict, Any, Optional, Tuple import requests -from rich.console import Console +from bittensor_wallet import Wallet from rich.prompt import Confirm, PromptBase -import bittensor +from bittensor.btcli.commands import defaults +from bittensor.core.chain_data import SubnetHyperparameters +from bittensor.core.config import Config +from bittensor.core.settings import bt_console +from bittensor.core.subtensor import Subtensor from bittensor.utils import U64_NORMALIZED_FLOAT, U16_NORMALIZED_FLOAT from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch -from . import defaults -from ...core.chain_data import SubnetHyperparameters - -console = Console() class IntListPrompt(PromptBase): @@ -50,14 +51,14 @@ def check_choice(self, value: str) -> bool: def check_netuid_set( - config: "bittensor.config", - subtensor: "bittensor.subtensor", + config: "Config", + subtensor: "Subtensor", allow_none: bool = False, ): if subtensor.network != "nakamoto": all_netuids = [str(netuid) for netuid in subtensor.get_subnets()] if len(all_netuids) == 0: - console.print(":cross_mark:[red]There are no open networks.[/red]") + bt_console.print(":cross_mark:[red]There are no open networks.[/red]") sys.exit() # Make sure netuid is set. @@ -82,7 +83,7 @@ def check_netuid_set( raise ValueError('netuid must be an integer or "None" (if applicable)') -def check_for_cuda_reg_config(config: "bittensor.config") -> None: +def check_for_cuda_reg_config(config: "Config") -> None: """Checks, when CUDA is available, if the user would like to register with their CUDA device.""" if torch and torch.cuda.is_available(): if not config.no_prompt: @@ -101,11 +102,11 @@ def check_for_cuda_reg_config(config: "bittensor.config") -> None: torch.cuda.get_device_name(x) for x in range(torch.cuda.device_count()) ] - console.print("Available CUDA devices:") + bt_console.print("Available CUDA devices:") choices_str: str = "" for i, device in enumerate(devices): choices_str += " {}: {}\n".format(device, device_names[i]) - console.print(choices_str) + bt_console.print(choices_str) dev_id = IntListPrompt.ask( "Which GPU(s) would you like to use? Please list one, or comma-separated", choices=devices, @@ -122,7 +123,7 @@ def check_for_cuda_reg_config(config: "bittensor.config") -> None: for dev_id in dev_id.replace(",", " ").split() ] except ValueError: - console.log( + bt_console.log( ":cross_mark:[red]Invalid GPU device[/red] [bold white]{}[/bold white]\nAvailable CUDA devices:{}".format( dev_id, choices_str ) @@ -135,7 +136,7 @@ def check_for_cuda_reg_config(config: "bittensor.config") -> None: config.pow_register.cuda.use_cuda = defaults.pow_register.cuda.use_cuda -def get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: +def get_hotkey_wallets_for_wallet(wallet) -> List["Wallet"]: hotkey_wallets = [] hotkeys_path = wallet.path + "/" + wallet.name + "/hotkeys" try: @@ -144,7 +145,7 @@ def get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: hotkey_files = [] for hotkey_file_name in hotkey_files: try: - hotkey_for_name = bittensor.wallet( + hotkey_for_name = Wallet( path=wallet.path, name=wallet.name, hotkey=hotkey_file_name ) if ( @@ -157,17 +158,17 @@ def get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: return hotkey_wallets -def get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: +def get_coldkey_wallets_for_path(path: str) -> List["Wallet"]: try: wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [bittensor.wallet(path=path, name=name) for name in wallet_names] + return [Wallet(path=path, name=name) for name in wallet_names] except StopIteration: # No wallet files found. wallets = [] return wallets -def get_all_wallets_for_path(path: str) -> List["bittensor.wallet"]: +def get_all_wallets_for_path(path: str) -> List["Wallet"]: all_wallets = [] cold_wallets = get_coldkey_wallets_for_path(path) for cold_wallet in cold_wallets: @@ -185,7 +186,7 @@ def filter_netuids_by_registered_hotkeys( netuids_with_registered_hotkeys = [] for wallet in all_hotkeys: netuids_list = subtensor.get_netuids_for_hotkey(wallet.hotkey.ss58_address) - bittensor.logging.debug( + logging.debug( f"Hotkey {wallet.hotkey.ss58_address} registered in netuids: {netuids_list}" ) netuids_with_registered_hotkeys.extend(netuids_list) @@ -238,7 +239,7 @@ def normalize_hyperparameters( else: norm_value = value except Exception as e: - bittensor.logging.warning(f"Error normalizing parameter '{param}': {e}") + logging.warning(f"Error normalizing parameter '{param}': {e}") norm_value = "-" normalized_values.append((param, str(value), str(norm_value))) diff --git a/bittensor/btcli/commands/wallets.py b/bittensor/btcli/commands/wallets.py index 83ed62768..59423f53f 100644 --- a/bittensor/btcli/commands/wallets.py +++ b/bittensor/btcli/commands/wallets.py @@ -24,11 +24,16 @@ from rich.prompt import Confirm, Prompt from rich.table import Table -import bittensor +from bittensor.utils import RAOPERTAO, is_valid_bittensor_address_or_public_key +from ...core.settings import networks -from bittensor.utils import RAOPERTAO +from bittensor.core.config import Config +from bittensor.core.settings import bt_console +from bittensor.core.subtensor import Subtensor +from bittensor.utils.btlogging import logging from . import defaults -from ...core.settings import networks +from bittensor_wallet import Wallet +from bittensor_wallet.keyfile import Keyfile class RegenColdkeyCommand: @@ -58,9 +63,10 @@ class RegenColdkeyCommand: It should be used with caution to avoid overwriting existing keys unintentionally. """ + @staticmethod def run(cli): r"""Creates a new coldkey under this wallet.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) json_str: Optional[str] = None json_password: Optional[str] = None @@ -81,7 +87,7 @@ def run(cli): ) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -151,8 +157,8 @@ def add_args(parser: argparse.ArgumentParser): action="store_true", help="""Overwrite the old coldkey with the newly generated coldkey""", ) - bittensor.wallet.add_args(regen_coldkey_parser) - bittensor.subtensor.add_args(regen_coldkey_parser) + Wallet.add_args(regen_coldkey_parser) + Subtensor.add_args(regen_coldkey_parser) class RegenColdkeypubCommand: @@ -178,9 +184,10 @@ class RegenColdkeypubCommand: It is a recovery-focused utility that ensures continued access to wallet functionalities. """ + @staticmethod def run(cli): r"""Creates a new coldkeypub under this wallet.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) wallet.regenerate_coldkeypub( ss58_address=cli.config.get("ss58_address"), public_key=cli.config.get("public_key_hex"), @@ -188,7 +195,7 @@ def run(cli): ) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -200,7 +207,7 @@ def check_config(config: "bittensor.config"): config.public_key_hex = prompt_answer else: config.ss58_address = prompt_answer - if not bittensor.utils.is_valid_bittensor_address_or_public_key( + if not is_valid_bittensor_address_or_public_key( address=( config.ss58_address if config.ss58_address else config.public_key_hex ) @@ -238,8 +245,8 @@ def add_args(parser: argparse.ArgumentParser): action="store_true", help="""Overwrite the old coldkeypub file with the newly generated coldkeypub""", ) - bittensor.wallet.add_args(regen_coldkeypub_parser) - bittensor.subtensor.add_args(regen_coldkeypub_parser) + Wallet.add_args(regen_coldkeypub_parser) + Subtensor.add_args(regen_coldkeypub_parser) class RegenHotkeyCommand: @@ -271,8 +278,8 @@ class RegenHotkeyCommand: """ def run(cli): - r"""Creates a new coldkey under this wallet.""" - wallet = bittensor.wallet(config=cli.config) + """Creates a new coldkey under this wallet.""" + wallet = Wallet(config=cli.config) json_str: Optional[str] = None json_password: Optional[str] = None @@ -295,7 +302,7 @@ def run(cli): ) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -304,9 +311,9 @@ def check_config(config: "bittensor.config"): hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) config.wallet.hotkey = str(hotkey) if ( - config.mnemonic == None - and config.get("seed", d=None) == None - and config.get("json", d=None) == None + config.mnemonic is None + and config.get("seed", d=None) is None + and config.get("json", d=None) is None ): prompt_answer = Prompt.ask("Enter mnemonic, seed, or json file location") if prompt_answer.startswith("0x"): @@ -316,7 +323,7 @@ def check_config(config: "bittensor.config"): else: config.json = prompt_answer - if config.get("json", d=None) and config.get("json_password", d=None) == None: + if config.get("json", d=None) and config.get("json_password", d=None) is None: config.json_password = Prompt.ask( "Enter json backup password", password=True ) @@ -370,8 +377,8 @@ def add_args(parser: argparse.ArgumentParser): default=False, help="""Overwrite the old hotkey with the newly generated hotkey""", ) - bittensor.wallet.add_args(regen_hotkey_parser) - bittensor.subtensor.add_args(regen_hotkey_parser) + Wallet.add_args(regen_hotkey_parser) + Subtensor.add_args(regen_hotkey_parser) class NewHotkeyCommand: @@ -398,9 +405,10 @@ class NewHotkeyCommand: such as running multiple miners or separating operational roles within the network. """ + @staticmethod def run(cli): """Creates a new hotke under this wallet.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) wallet.create_new_hotkey( n_words=cli.config.n_words, use_password=cli.config.use_password, @@ -408,7 +416,7 @@ def run(cli): ) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -449,8 +457,8 @@ def add_args(parser: argparse.ArgumentParser): default=False, help="""Overwrite the old hotkey with the newly generated hotkey""", ) - bittensor.wallet.add_args(new_hotkey_parser) - bittensor.subtensor.add_args(new_hotkey_parser) + Wallet.add_args(new_hotkey_parser) + Subtensor.add_args(new_hotkey_parser) class NewColdkeyCommand: @@ -477,9 +485,10 @@ class NewColdkeyCommand: It's a foundational step in establishing a secure presence on the Bittensor network. """ + @staticmethod def run(cli): r"""Creates a new coldkey under this wallet.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) wallet.create_new_coldkey( n_words=cli.config.n_words, use_password=cli.config.use_password, @@ -487,7 +496,7 @@ def run(cli): ) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -524,8 +533,8 @@ def add_args(parser: argparse.ArgumentParser): default=False, help="""Overwrite the old coldkey with the newly generated coldkey""", ) - bittensor.wallet.add_args(new_coldkey_parser) - bittensor.subtensor.add_args(new_coldkey_parser) + Wallet.add_args(new_coldkey_parser) + Subtensor.add_args(new_coldkey_parser) class WalletCreateCommand: @@ -553,9 +562,10 @@ class WalletCreateCommand: It ensures a fresh start with new keys for secure and effective participation in the network. """ + @staticmethod def run(cli): r"""Creates a new coldkey and hotkey under this wallet.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) wallet.create_new_coldkey( n_words=cli.config.n_words, use_password=cli.config.use_password, @@ -568,7 +578,7 @@ def run(cli): ) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -614,15 +624,15 @@ def add_args(parser: argparse.ArgumentParser): default=False, help="""Overwrite the old hotkey with the newly generated hotkey""", ) - bittensor.wallet.add_args(new_coldkey_parser) - bittensor.subtensor.add_args(new_coldkey_parser) + Wallet.add_args(new_coldkey_parser) + Subtensor.add_args(new_coldkey_parser) -def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: +def _get_coldkey_wallets_for_path(path: str) -> List["Wallet"]: """Get all coldkey wallet names from path.""" try: wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [bittensor.wallet(path=path, name=name) for name in wallet_names] + return [Wallet(path=path, name=name) for name in wallet_names] except StopIteration: # No wallet files found. wallets = [] @@ -658,7 +668,7 @@ def run(cli): if config.get("all", d=False) == True: wallets = _get_coldkey_wallets_for_path(config.wallet.path) else: - wallets = [bittensor.wallet(config=config)] + wallets = [Wallet(config=config)] for wallet in wallets: print("\n===== ", wallet, " =====") @@ -671,8 +681,8 @@ def add_args(parser: argparse.ArgumentParser): help="""Updates the wallet security using NaCL instead of ansible vault.""", ) update_wallet_parser.add_argument("--all", action="store_true") - bittensor.wallet.add_args(update_wallet_parser) - bittensor.subtensor.add_args(update_wallet_parser) + Wallet.add_args(update_wallet_parser) + Subtensor.add_args(update_wallet_parser) @staticmethod def check_config(config: "bittensor.Config"): @@ -684,11 +694,11 @@ def check_config(config: "bittensor.Config"): # Ask the user to specify the wallet if the wallet name is not clear. if ( config.get("all", d=False) == False - and config.wallet.get("name") == bittensor.defaults.wallet.name + and config.wallet.get("name") == defaults.wallet.name and not config.no_prompt ): wallet_name = Prompt.ask( - "Enter wallet name", default=bittensor.defaults.wallet.name + "Enter wallet name", default=defaults.wallet.name ) config.wallet.name = str(wallet_name) @@ -711,14 +721,14 @@ def list_coldkeypub_files(dir_path): coldkey_files.append(coldkey_path) wallet_names.append(potential_wallet_name) else: - bittensor.logging.warning( + logging.warning( f"{coldkey_path} does not exist. Excluding..." ) return coldkey_files, wallet_names coldkey_files, wallet_names = list_coldkeypub_files(path) addresses = [ - bittensor.keyfile(coldkey_path).keypair.ss58_address + Keyfile(coldkey_path).keypair.ss58_address for coldkey_path in coldkey_files ] return addresses, wallet_names @@ -766,18 +776,18 @@ class WalletBalanceCommand: def run(cli: "bittensor.cli"): """Check the balance of the wallet.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) WalletBalanceCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - wallet = bittensor.wallet(config=cli.config) + def _run(cli: "bittensor.cli", subtensor: "Subtensor"): + wallet = Wallet(config=cli.config) wallet_names = [] coldkeys = [] @@ -811,7 +821,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) } else: - coldkey_wallet = bittensor.wallet(config=cli.config) + coldkey_wallet = Wallet(config=cli.config) if ( coldkey_wallet.coldkeypub_file.exists_on_device() and not coldkey_wallet.coldkeypub_file.is_encrypted() @@ -839,7 +849,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): } if not coldkey_wallet.coldkeypub_file.exists_on_device(): - bittensor.__console__.print("[bold red]No wallets found.") + bt_console.print("[bold red]No wallets found.") return table = Table(show_footer=False) @@ -891,7 +901,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): table.box = None table.pad_edge = False table.width = None - bittensor.__console__.print(table) + bt_console.print(table) @staticmethod def add_args(parser: argparse.ArgumentParser): @@ -906,11 +916,11 @@ def add_args(parser: argparse.ArgumentParser): default=False, ) - bittensor.wallet.add_args(balance_parser) - bittensor.subtensor.add_args(balance_parser) + Wallet.add_args(balance_parser) + Subtensor.add_args(balance_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if ( not config.is_set("wallet.path") and not config.no_prompt @@ -939,7 +949,7 @@ def check_config(config: "bittensor.config"): ( _, config.subtensor.chain_endpoint, - ) = bittensor.subtensor.determine_chain_endpoint_and_network(str(network)) + ) = Subtensor.determine_chain_endpoint_and_network(str(network)) API_URL = "https://api.subquery.network/sq/TaoStats/bittensor-indexer" @@ -990,7 +1000,7 @@ class GetWalletHistoryCommand: @staticmethod def run(cli): r"""Check the transfer history of the provided wallet.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) wallet_address = wallet.get_coldkeypub().ss58_address # Fetch all transfers transfers = get_wallet_transfers(wallet_address) @@ -998,7 +1008,7 @@ def run(cli): # Create output table table = create_transfer_history_table(transfers) - bittensor.__console__.print(table) + bt_console.print(table) @staticmethod def add_args(parser: argparse.ArgumentParser): @@ -1006,11 +1016,11 @@ def add_args(parser: argparse.ArgumentParser): "history", help="""Fetch transfer history associated with the provided wallet""", ) - bittensor.wallet.add_args(history_parser) - bittensor.subtensor.add_args(history_parser) + Wallet.add_args(history_parser) + Subtensor.add_args(history_parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) diff --git a/bittensor/btcli/commands/weights.py b/bittensor/btcli/commands/weights.py index ac4d9dfc3..ae311cf56 100644 --- a/bittensor/btcli/commands/weights.py +++ b/bittensor/btcli/commands/weights.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -24,10 +23,15 @@ import re import numpy as np +from bittensor_wallet import Wallet from rich.prompt import Prompt, Confirm -import bittensor -import bittensor.utils.weight_utils as weight_utils +from bittensor.utils import weight_utils +from bittensor.core.config import Config +from bittensor.core.settings import bt_console +from bittensor.core.settings import version_as_int +from bittensor.core.subtensor import Subtensor +from bittensor.utils.btlogging import logging from . import defaults # type: ignore @@ -51,22 +55,22 @@ class CommitWeightCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Commit weights for a specific subnet.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) CommitWeightCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Commit weights for a specific subnet""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) # Get values if not set if not cli.config.is_set("netuid"): @@ -120,9 +124,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Result if success: - bittensor.__console__.print(f"Weights committed successfully") + bt_console.print(f"Weights committed successfully") else: - bittensor.__console__.print(f"Failed to commit weights: {message}") + bt_console.print(f"Failed to commit weights: {message}") @staticmethod def add_args(parser: argparse.ArgumentParser): @@ -152,11 +156,11 @@ def add_args(parser: argparse.ArgumentParser): default=False, ) - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) + Wallet.add_args(parser) + Subtensor.add_args(parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.no_prompt and not config.is_set("wallet.name"): wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) @@ -182,22 +186,22 @@ class RevealWeightCommand: """ @staticmethod - def run(cli: "bittensor.cli"): + def run(cli): r"""Reveal weights for a specific subnet.""" try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( + subtensor: "Subtensor" = Subtensor( config=cli.config, log_verbose=False ) RevealWeightCommand._run(cli, subtensor) finally: if "subtensor" in locals(): subtensor.close() - bittensor.logging.debug("closing subtensor connection") + logging.debug("closing subtensor connection") @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + def _run(cli, subtensor: "Subtensor"): r"""Reveal weights for a specific subnet.""" - wallet = bittensor.wallet(config=cli.config) + wallet = Wallet(config=cli.config) # Get values if not set. if not cli.config.is_set("netuid"): @@ -214,7 +218,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Parse from string netuid = cli.config.netuid - version = bittensor.__version_as_int__ + version = version_as_int uids = np.array( [int(x) for x in re.split(r"[ ,]+", cli.config.uids)], dtype=np.int64, @@ -245,9 +249,9 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ) if success: - bittensor.__console__.print(f"Weights revealed successfully") + bt_console.print(f"Weights revealed successfully") else: - bittensor.__console__.print(f"Failed to reveal weights: {message}") + bt_console.print(f"Failed to reveal weights: {message}") @staticmethod def add_args(parser: argparse.ArgumentParser): @@ -277,11 +281,11 @@ def add_args(parser: argparse.ArgumentParser): default=False, ) - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) + Wallet.add_args(parser) + Subtensor.add_args(parser) @staticmethod - def check_config(config: "bittensor.config"): + def check_config(config: "Config"): if not config.is_set("wallet.name") and not config.no_prompt: wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) diff --git a/bittensor/core/config.py b/bittensor/core/config.py index a87863b9e..b18db2f40 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -1,45 +1,50 @@ +""" +Implementation of the config class, which manages the configuration of different Bittensor modules. +""" + # The MIT License (MIT) -# Copyright © 2024 Opentensor Foundation -# +# Copyright © 2021 Yuma Rao +# Copyright © 2022 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. -"""Implementation of the config class, which manages the configuration of different Bittensor modules.""" - -import argparse -import copy import os import sys -from copy import deepcopy -from typing import List, Optional, Dict, Any, TypeVar, Type - import yaml +import copy +from copy import deepcopy from munch import DefaultMunch +from typing import List, Optional, Dict, Any, TypeVar, Type +import argparse class InvalidConfigFile(Exception): """In place of YAMLError""" + pass -class Config(DefaultMunch): - """Implementation of the config class, which manages the configuration of different Bittensor modules.""" + +class config(DefaultMunch): + """ + Implementation of the config class, which manages the configuration of different Bittensor modules. + """ __is_set: Dict[str, bool] - """ - Translates the passed parser into a nested Bittensor config. - + r""" Translates the passed parser into a nested Bittensor config. + Args: parser (argparse.ArgumentParser): Command line parser object. @@ -51,23 +56,23 @@ class Config(DefaultMunch): Default value for the Config. Defaults to ``None``. This default will be returned for attributes that are undefined. Returns: - config (Config): + config (bittensor.config): Nested config object created from parser arguments. """ def __init__( - self, - parser: argparse.ArgumentParser = None, - args: Optional[List[str]] = None, - strict: bool = False, - default: Optional[Any] = None, + self, + parser: argparse.ArgumentParser = None, + args: Optional[List[str]] = None, + strict: bool = False, + default: Optional[Any] = None, ) -> None: super().__init__(default) self["__is_set"] = {} - if parser is None: - return + if parser == None: + return None # Optionally add config specific arguments try: @@ -76,7 +81,7 @@ def __init__( type=str, help="If set, defaults are overridden by passed file.", ) - except Exception: + except: # this can fail if --config has already been added. pass @@ -87,7 +92,7 @@ def __init__( help="""If flagged, config will check that only exact arguments have been set.""", default=False, ) - except Exception: + except: # this can fail if --strict has already been added. pass @@ -98,7 +103,7 @@ def __init__( help="Set ``true`` to stop cli version checking.", default=False, ) - except Exception: + except: # this can fail if --no_version_checking has already been added. pass @@ -110,12 +115,12 @@ def __init__( help="Set ``true`` to stop cli from prompting the user.", default=False, ) - except Exception: + except: # this can fail if --no_version_checking has already been added. pass # Get args from argv if not passed in. - if args is None: + if args == None: args = sys.argv[1:] # Check for missing required arguments before proceeding @@ -129,21 +134,21 @@ def __init__( # 1.1 Optionally load defaults if the --config is set. try: config_file_path = ( - str(os.getcwd()) - + "/" - + vars(parser.parse_known_args(args)[0])["config"] + str(os.getcwd()) + + "/" + + vars(parser.parse_known_args(args)[0])["config"] ) - except Exception: + except Exception as e: config_file_path = None # Parse args not strict - config_params = Config.__parse_args__(args=args, parser=parser, strict=False) + config_params = config.__parse_args__(args=args, parser=parser, strict=False) # 2. Optionally check for --strict - # strict=True when passed in OR when --strict is set + ## strict=True when passed in OR when --strict is set strict = config_params.strict or strict - if config_file_path is not None: + if config_file_path != None: config_file_path = os.path.expanduser(config_file_path) try: with open(config_file_path) as f: @@ -154,46 +159,46 @@ def __init__( print("Error in loading: {} using default parser settings".format(e)) # 2. Continue with loading in params. - params = Config.__parse_args__(args=args, parser=parser, strict=strict) + params = config.__parse_args__(args=args, parser=parser, strict=strict) _config = self # Splits params and add to config - Config.__split_params__(params=params, _config=_config) + config.__split_params__(params=params, _config=_config) # Make the is_set map _config["__is_set"] = {} - # Reparse args using default of unset + ## Reparse args using default of unset parser_no_defaults = copy.deepcopy(parser) # Only command as the arg, else no args default_param_args = ( [_config.get("command")] - if _config.get("command") is not None and _config.get("subcommand") is None + if _config.get("command") != None and _config.get("subcommand") == None else [] ) - if _config.get("command") is not None and _config.get("subcommand") is not None: + if _config.get("command") != None and _config.get("subcommand") != None: default_param_args = [_config.get("command"), _config.get("subcommand")] - # Get all args by name + ## Get all args by name default_params = parser.parse_args(args=default_param_args) all_default_args = default_params.__dict__.keys() | [] - # Make a dict with keys as args and values as argparse.SUPPRESS + ## Make a dict with keys as args and values as argparse.SUPPRESS defaults_as_suppress = {key: argparse.SUPPRESS for key in all_default_args} - # Set the defaults to argparse.SUPPRESS, should remove them from the namespace + ## Set the defaults to argparse.SUPPRESS, should remove them from the namespace parser_no_defaults.set_defaults(**defaults_as_suppress) parser_no_defaults._defaults.clear() # Needed for quirk of argparse - # Check for subparsers and do the same - if parser_no_defaults._subparsers is not None: + ### Check for subparsers and do the same + if parser_no_defaults._subparsers != None: for action in parser_no_defaults._subparsers._actions: # Should only be the "command" subparser action if isinstance(action, argparse._SubParsersAction): # Set the defaults to argparse.SUPPRESS, should remove them from the namespace # Each choice is the keyword for a command, we need to set the defaults for each of these - # Note: we also need to clear the _defaults dict for each, this is a quirk of argparse + ## Note: we also need to clear the _defaults dict for each, this is a quirk of argparse cmd_parser: argparse.ArgumentParser for cmd_parser in action.choices.values(): # If this choice is also a subparser, set defaults recursively @@ -210,7 +215,7 @@ def __init__( cmd_parser._defaults.clear() # Needed for quirk of argparse # Reparse the args, but this time with the defaults as argparse.SUPPRESS - params_no_defaults = Config.__parse_args__( + params_no_defaults = config.__parse_args__( args=args, parser=parser_no_defaults, strict=strict ) @@ -227,7 +232,7 @@ def __init__( } @staticmethod - def __split_params__(params: argparse.Namespace, _config: "Config"): + def __split_params__(params: argparse.Namespace, _config: "config"): # Splits params on dot syntax i.e neuron.axon_port and adds to _config for arg_key, arg_val in params.__dict__.items(): split_keys = arg_key.split(".") @@ -235,12 +240,12 @@ def __split_params__(params: argparse.Namespace, _config: "Config"): keys = split_keys while len(keys) > 1: if ( - hasattr(head, keys[0]) and head[keys[0]] is not None + hasattr(head, keys[0]) and head[keys[0]] is not None ): # Needs to be Config head = getattr(head, keys[0]) keys = keys[1:] else: - head[keys[0]] = Config() + head[keys[0]] = config() head = head[keys[0]] keys = keys[1:] if len(keys) == 1: @@ -248,7 +253,7 @@ def __split_params__(params: argparse.Namespace, _config: "Config"): @staticmethod def __parse_args__( - args: List[str], parser: argparse.ArgumentParser = None, strict: bool = False + args: List[str], parser: argparse.ArgumentParser = None, strict: bool = False ) -> argparse.Namespace: """Parses the passed args use the passed parser. @@ -276,11 +281,11 @@ def __parse_args__( return params - def __deepcopy__(self, memo) -> "Config": + def __deepcopy__(self, memo) -> "config": _default = self.__default__ config_state = self.__getstate__() - config_copy = Config() + config_copy = config() memo[id(self)] = config_copy config_copy.__setstate__(config_state) @@ -301,7 +306,7 @@ def _remove_private_keys(d): d.pop("__is_set", None) for k, v in list(d.items()): if isinstance(v, dict): - Config._remove_private_keys(v) + config._remove_private_keys(v) return d def __str__(self) -> str: @@ -309,10 +314,10 @@ def __str__(self) -> str: visible = copy.deepcopy(self.toDict()) visible.pop("__parser", None) visible.pop("__is_set", None) - cleaned = Config._remove_private_keys(visible) + cleaned = config._remove_private_keys(visible) return "\n" + yaml.dump(cleaned, sort_keys=False) - def copy(self) -> "Config": + def copy(self) -> "config": return copy.deepcopy(self) def to_string(self, items) -> str: @@ -346,10 +351,10 @@ def merge(self, b): Args: b: Another config to merge. """ - self._merge(self, b) + self = self._merge(self, b) @classmethod - def merge_all(cls, configs: List["Config"]) -> "Config": + def merge_all(cls, configs: List["config"]) -> "config": """ Merge all configs in the list into one config. If there is a conflict, the value from the last configuration in the list will take precedence. @@ -377,7 +382,7 @@ def is_set(self, param_name: str) -> bool: return self.get("__is_set")[param_name] def __check_for_missing_required_args( - self, parser: argparse.ArgumentParser, args: List[str] + self, parser: argparse.ArgumentParser, args: List[str] ) -> List[str]: required_args = self.__get_required_args_from_parser(parser) missing_args = [arg for arg in required_args if not any(arg in s for s in args)] @@ -397,8 +402,10 @@ def __get_required_args_from_parser(parser: argparse.ArgumentParser) -> List[str T = TypeVar("T", bound="DefaultConfig") -class DefaultConfig(Config): - """A Config with a set of default values.""" +class DefaultConfig(config): + """ + A Config with a set of default values. + """ @classmethod def default(cls: Type[T]) -> T: @@ -406,3 +413,6 @@ def default(cls: Type[T]) -> T: Get default config. """ raise NotImplementedError("Function default is not implemented.") + + +Config = config diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index eb552e8fa..7bd618acd 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -558,10 +558,10 @@ async def call( # Make the HTTP POST request async with (await self.session).post( - url, + url=url, headers=synapse.to_headers(), json=synapse.model_dump(), - timeout=timeout, + timeout=aiohttp.ClientTimeout(total=timeout), ) as response: # Extract the JSON response from the server json_response = await response.json() @@ -640,7 +640,7 @@ async def call_stream( url, headers=synapse.to_headers(), json=synapse.model_dump(), - timeout=timeout, + timeout=aiohttp.ClientTimeout(total=timeout), ) as response: # Use synapse subclass' process_streaming_response method to yield the response chunks async for chunk in synapse.process_streaming_response(response): # type: ignore diff --git a/bittensor/core/errors.py b/bittensor/core/errors.py index dea8db9d0..5b5c3d5cf 100644 --- a/bittensor/core/errors.py +++ b/bittensor/core/errors.py @@ -17,10 +17,7 @@ from __future__ import annotations -import typing - -if typing.TYPE_CHECKING: - import bittensor +from bittensor.core.synapse import Synapse class ChainError(BaseException): @@ -85,7 +82,7 @@ class InvalidRequestNameError(Exception): class SynapseException(Exception): def __init__( - self, message="Synapse Exception", synapse: bittensor.Synapse | None = None + self, message="Synapse Exception", synapse: "Synapse" | None = None ): self.message = message self.synapse = synapse @@ -128,7 +125,7 @@ class SynapseDendriteNoneException(SynapseException): def __init__( self, message="Synapse Dendrite is None", - synapse: bittensor.Synapse | None = None, + synapse: "Synapse" | None = None, ): self.message = message super().__init__(self.message, synapse) diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 6f526f514..523b68c85 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -26,10 +26,8 @@ from numpy.typing import NDArray from rich.console import Console -from bittensor import __version__, __version_as_int__ from . import settings from .chain_data import AxonInfo -from .subtensor import Subtensor from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch from bittensor.utils.weight_utils import convert_weight_uids_and_vals_to_tensor, convert_bond_uids_and_vals_to_tensor @@ -444,7 +442,7 @@ def metadata(self) -> dict: "n": self.n.item(), "block": self.block.item(), "network": self.network, - "version": __version__, + "version": settings.__version__, } def state_dict(self): @@ -509,20 +507,18 @@ def sync( For example:: - subtensor = bittensor.subtensor(network='archive') + subtensor = bittensor.core.subtensor.Subtensor(network='archive') """ # Initialize subtensor subtensor = self._initialize_subtensor(subtensor) - if ( - subtensor.chain_endpoint != settings.archive_entrypoint # type: ignore - or subtensor.network != "archive" # type: ignore - ): - cur_block = subtensor.get_current_block() # type: ignore + if subtensor.chain_endpoint != settings.archive_entrypoint or subtensor.network != settings.networks[3]: + cur_block = subtensor.get_current_block() if block and block < (cur_block - 300): logging.warning( - "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' network for subtensor and retry." + "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' " + "network for subtensor and retry." ) # Assign neurons based on 'lite' flag @@ -556,6 +552,8 @@ def _initialize_subtensor(self, subtensor): """ if not subtensor: # TODO: Check and test the initialization of the new subtensor + # Lazy import due to circular import (subtensor -> metagraph, metagraph -> subtensor) + from bittensor.core.subtensor import Subtensor subtensor = Subtensor(network=self.network) return subtensor @@ -874,7 +872,7 @@ def __init__( self.netuid = netuid self.network = network self.version = torch.nn.Parameter( - torch.tensor([__version_as_int__], dtype=torch.int64), + torch.tensor([settings.version_as_int], dtype=torch.int64), requires_grad=False, ) self.n: torch.nn.Parameter = torch.nn.Parameter( @@ -948,7 +946,7 @@ def _set_metagraph_attributes(self, block, subtensor): self._set_metagraph_attributes(block, subtensor) """ self.n = self._create_tensor(len(self.neurons), dtype=torch.int64) - self.version = self._create_tensor([__version_as_int__], dtype=torch.int64) + self.version = self._create_tensor([settings.version_as_int], dtype=torch.int64) self.block = self._create_tensor( block if block else subtensor.block, dtype=torch.int64 ) @@ -1045,7 +1043,7 @@ def __init__( self.netuid = netuid self.network = network - self.version = (np.array([__version_as_int__], dtype=np.int64),) + self.version = (np.array([settings.version_as_int], dtype=np.int64),) self.n = np.array([0], dtype=np.int64) self.block = np.array([0], dtype=np.int64) self.stake = np.array([], dtype=np.float32) @@ -1085,7 +1083,7 @@ def _set_metagraph_attributes(self, block, subtensor): # TODO: Check and test the setting of each attribute self.n = self._create_tensor(len(self.neurons), dtype=np.int64) self.version = self._create_tensor( - [__version_as_int__], dtype=np.int64 + [settings.version_as_int], dtype=np.int64 ) self.block = self._create_tensor( block if block else subtensor.block, dtype=np.int64 @@ -1178,4 +1176,4 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore return self -metagraph = TorchMetaGraph if use_torch() else NonTorchMetagraph +Metagraph = TorchMetaGraph if use_torch() else NonTorchMetagraph diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 60ce69df2..9c26d3330 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -52,6 +52,8 @@ def turn_console_on(): turn_console_off() +bt_console = __console__ + HOME_DIR = Path.home() USER_BITTENSOR_DIR = HOME_DIR / ".bittensor" diff --git a/bittensor/core/threadpool.py b/bittensor/core/threadpool.py index e3b0d92f1..7182c660f 100644 --- a/bittensor/core/threadpool.py +++ b/bittensor/core/threadpool.py @@ -13,14 +13,15 @@ import weakref import logging import argparse -import bittensor import itertools import threading from typing import Callable from concurrent.futures import _base +from bittensor.core.config import Config from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME +from bittensor.core.settings import blocktime # Workers are created as daemon threads. This is done to allow the interpreter # to exit when there are still idle threads in a ThreadPoolExecutor's thread @@ -54,7 +55,7 @@ def run(self): """Run the given work item""" # Checks if future is canceled or if work item is stale if (not self.future.set_running_or_notify_cancel()) or ( - time.time() - self.start_time > bittensor.__blocktime__ + time.time() - self.start_time > blocktime ): return @@ -120,18 +121,18 @@ class BrokenThreadPool(_base.BrokenExecutor): class PriorityThreadPoolExecutor(_base.Executor): - """Base threadpool executor with a priority queue""" + """Base threadpool executor with a priority queue.""" # Used to assign unique thread names when thread_name_prefix is not supplied. _counter = itertools.count().__next__ def __init__( - self, - maxsize=-1, - max_workers=None, - thread_name_prefix="", - initializer=None, - initargs=(), + self, + maxsize=-1, + max_workers=None, + thread_name_prefix="", + initializer=None, + initargs=(), ): """Initializes a new `ThreadPoolExecutor `_ instance. @@ -160,7 +161,7 @@ def __init__( self._shutdown = False self._shutdown_lock = threading.Lock() self._thread_name_prefix = thread_name_prefix or ( - "ThreadPoolExecutor-%d" % self._counter() + "ThreadPoolExecutor-%d" % self._counter() ) self._initializer = initializer self._initargs = initargs @@ -168,16 +169,16 @@ def __init__( @classmethod def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): """Accept specific arguments from parser""" - prefix_str = "" if prefix == None else prefix + "." + prefix_str = "" if prefix is None else prefix + "." try: default_max_workers = ( os.getenv("BT_PRIORITY_MAX_WORKERS") - if os.getenv("BT_PRIORITY_MAX_WORKERS") != None + if os.getenv("BT_PRIORITY_MAX_WORKERS") is not None else 5 ) default_maxsize = ( os.getenv("BT_PRIORITY_MAXSIZE") - if os.getenv("BT_PRIORITY_MAXSIZE") != None + if os.getenv("BT_PRIORITY_MAXSIZE") is not None else 10 ) parser.add_argument( @@ -197,14 +198,14 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): pass @classmethod - def config(cls) -> "bittensor.Config": + def config(cls) -> "Config": """Get config from the argument parser. Return: :func:`bittensor.Config` object. """ parser = argparse.ArgumentParser() PriorityThreadPoolExecutor.add_args(parser) - return bittensor.Config(parser, args=[]) + return Config(parser, args=[]) @property def is_empty(self): diff --git a/bittensor/mock/subtensor_mock.py b/bittensor/mock/subtensor_mock.py index ade11ba7a..dcc8112f8 100644 --- a/bittensor/mock/subtensor_mock.py +++ b/bittensor/mock/subtensor_mock.py @@ -46,9 +46,7 @@ class AxonServeCallParams(TypedDict): - """ - Axon serve chain call parameters. - """ + """Axon serve chain call parameters.""" version: int ip: int @@ -58,9 +56,7 @@ class AxonServeCallParams(TypedDict): class PrometheusServeCallParams(TypedDict): - """ - Prometheus serve chain call parameters. - """ + """Prometheus serve chain call parameters.""" version: int ip: int diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index d7e16217e..f5bff9869 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -21,7 +21,6 @@ import numpy as np import scalecodec -import bittensor from .registration import torch, use_torch from .version import version_checking, check_version, VersionCheckError from .wallet_utils import * # noqa F401 @@ -33,7 +32,7 @@ def ss58_to_vec_u8(ss58_address: str) -> List[int]: - ss58_bytes: bytes = bittensor.utils.ss58_address_to_bytes(ss58_address) + ss58_bytes: bytes = ss58_address_to_bytes(ss58_address) encoded_address: List[int] = [int(byte) for byte in ss58_bytes] return encoded_address diff --git a/bittensor/utils/backwards_compatibility.py b/bittensor/utils/backwards_compatibility.py new file mode 100644 index 000000000..f8fa4b0f9 --- /dev/null +++ b/bittensor/utils/backwards_compatibility.py @@ -0,0 +1,143 @@ +# 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. + +""" +The Bittensor Compatibility Module is designed to ensure seamless integration and functionality with legacy versions of +the Bittensor framework, specifically up to and including version 7.3.0. This module addresses changes and deprecated +features in recent versions, allowing users to maintain compatibility with older systems and projects. +""" + +from bittensor_wallet.errors import KeyFileError # noqa: F401 +from bittensor_wallet.keyfile import ( # noqa: F401 + 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, +) +from bittensor_wallet.wallet import display_mnemonic_msg, Wallet # noqa: F401 +from substrateinterface import Keypair # noqa: F401 + +from bittensor.core import settings +from bittensor.core.axon import Axon +from bittensor.core.chain_data import ( # noqa: F401 + AxonInfo, + NeuronInfo, + NeuronInfoLite, + PrometheusInfo, + DelegateInfo, + StakeInfo, + SubnetInfo, + SubnetHyperparameters, + IPInfo, + ProposalCallData, + ProposalVoteData, +) +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 + BlacklistedException, + ChainConnectionError, + ChainError, + ChainQueryError, + ChainTransactionError, + IdentityError, + InternalServerError, + InvalidRequestNameError, + MetadataError, + NominationError, + NotDelegateError, + NotRegisteredError, + NotVerifiedException, + PostProcessException, + PriorityException, + RegistrationError, + RunException, + StakeError, + SynapseDendriteNoneException, + SynapseParsingError, + TransferError, + UnknownSynapseError, + UnstakeError, +) +from bittensor.core.metagraph import Metagraph +from bittensor.core.settings import blocktime +from bittensor.core.stream import StreamingSynapse # noqa: F401 +from bittensor.core.subtensor import Subtensor +from bittensor.core.synapse import TerminalInfo, Synapse # noqa: F401 +from bittensor.core.tensor import tensor, Tensor # noqa: F401 +from bittensor.core.threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor # noqa: F401 +from bittensor.mock.subtensor_mock import MockSubtensor as MockSubtensor # noqa: F401 +from bittensor.utils import ( # noqa: F401 + ss58_to_vec_u8, + unbiased_topk, + version_checking, + strtobool, + strtobool_with_default, + get_explorer_root_url_by_network_from_map, + get_explorer_root_url_by_network_from_map, + get_explorer_url_for_network, + ss58_address_to_bytes, + U16_NORMALIZED_FLOAT, + U64_NORMALIZED_FLOAT, + u8_key_to_ss58, + get_hash, + wallet_utils, +) +from bittensor.utils.balance import Balance as Balance # noqa: F401 +from bittensor.utils.subnets import SubnetsAPI # noqa: F401 + + +# Backwards compatibility with previous bittensor versions. +axon = Axon +config = Config +keyfile = Keyfile +metagraph = Metagraph +wallet = Wallet +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 diff --git a/bittensor/utils/registration.py b/bittensor/utils/registration.py index d606929dc..6ba28b5a9 100644 --- a/bittensor/utils/registration.py +++ b/bittensor/utils/registration.py @@ -1,7 +1,22 @@ -import binascii +# 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 functools import hashlib -import math import multiprocessing import multiprocessing.queues # this must be imported separately, or could break type annotations import os @@ -14,15 +29,21 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union import backoff +import binascii +import math import numpy - -import bittensor from Crypto.Hash import keccak from rich import console as rich_console from rich import status as rich_status -from .formatting import get_human_readable, millify -from ._register_cuda import solve_cuda +from bittensor.core.settings import bt_console +from bittensor.utils._register_cuda import solve_cuda +from bittensor.utils.btlogging import logging +from bittensor.utils.formatting import get_human_readable, millify + +if typing.TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + from bittensor_wallet import Wallet def use_torch() -> bool: @@ -35,11 +56,10 @@ def legacy_torch_api_compat(func): Convert function operating on numpy Input&Output to legacy torch Input&Output API if `use_torch()` is True. Args: - func (function): - Function with numpy Input/Output to be decorated. + func (function): Function with numpy Input/Output to be decorated. + Returns: - decorated (function): - Decorated function. + decorated (function): Decorated function. """ @functools.wraps(func) @@ -74,7 +94,7 @@ def _get_real_torch(): def log_no_torch_error(): - bittensor.logging.error( + logging.error( "This command requires torch. You can install torch for bittensor" ' with `pip install bittensor[torch]` or `pip install ".[torch]"`' " if installing from source, and then run the command with USE_TORCH=1 {command}" @@ -102,11 +122,9 @@ def __getattr__(self, name): class CUDAException(Exception): """An exception raised when an error occurs in the CUDA environment.""" - pass - def _hex_bytes_to_u8_list(hex_bytes: bytes): - hex_chunks = [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)] + hex_chunks = [int(hex_bytes[i: i + 2], 16) for i in range(0, len(hex_bytes), 2)] return hex_chunks @@ -134,9 +152,10 @@ class POWSolution: difficulty: int seal: bytes - def is_stale(self, subtensor: "bittensor.subtensor") -> bool: - """Returns True if the POW is stale. - This means the block the POW is solved for is within 3 blocks of the current block. + def is_stale(self, subtensor: "Subtensor") -> bool: + """ + Returns True if the POW is stale. This means the block the POW is solved for is within 3 blocks of the current + block. """ return self.block_number < subtensor.get_current_block() - 3 @@ -183,7 +202,6 @@ class _SolverBase(multiprocessing.Process): limit: int The limit of the pow solve for a valid solution. """ - proc_num: int num_proc: int update_interval: int @@ -461,7 +479,6 @@ def get_cpu_count() -> int: @dataclass class RegistrationStatistics: """Statistics for a registration.""" - time_spent_total: float rounds_total: int time_average: float @@ -525,8 +542,8 @@ def update(self, stats: RegistrationStatistics, verbose: bool = False) -> None: def _solve_for_difficulty_fast( - subtensor, - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", netuid: int, output_in_place: bool = True, num_processes: Optional[int] = None, @@ -538,29 +555,23 @@ def _solve_for_difficulty_fast( """ Solves the POW for registration using multiprocessing. Args: - subtensor - Subtensor to connect to for block information and to submit. - wallet: - wallet to use for registration. - netuid: int - The netuid of the subnet to register to. - output_in_place: bool - If true, prints the status in place. Otherwise, prints the status on a new line. - num_processes: int - Number of processes to use. - update_interval: int - Number of nonces to solve before updating block information. - n_samples: int - The number of samples of the hash_rate to keep for the EWMA - alpha_: float - The alpha for the EWMA for the hash_rate calculation - log_verbose: bool - If true, prints more verbose logging of the registration metrics. - Note: The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. + subtensor (bittensor.core.subtensor.Subtensor): Subtensor to connect to for block information and to submit. + wallet (bittensor_wallet.Wallet): wallet to use for registration. + netuid (int): The netuid of the subnet to register to. + output_in_place (bool): If true, prints the status in place. Otherwise, prints the status on a new line. + num_processes (int): Number of processes to use. + update_interval (int): Number of nonces to solve before updating block information. + n_samples (int): The number of samples of the hash_rate to keep for the EWMA. + alpha_ (float): The alpha for the EWMA for the hash_rate calculation. + log_verbose (bool): If true, prints more verbose logging of the registration metrics. + + Note: + The hash rate is calculated as an exponentially weighted moving average in order to make the measure more + robust. Note: - - We can also modify the update interval to do smaller blocks of work, - while still updating the block information after a different number of nonces, - to increase the transparency of the process while still keeping the speed. + - We can also modify the update interval to do smaller blocks of work, while still updating the block + information after a different number of nonces, to increase the transparency of the process while still keeping + the speed. """ if num_processes is None: # get the number of allowed processes for this process @@ -574,7 +585,7 @@ def _solve_for_difficulty_fast( curr_block, curr_block_num, curr_diff = _Solver.create_shared_memory() # Establish communication queues - ## See the _Solver class for more information on the queues. + # See the _Solver class for more information on the queues. stopEvent = multiprocessing.Event() stopEvent.clear() @@ -646,8 +657,7 @@ def _solve_for_difficulty_fast( start_time_perpetual = time.time() - console = bittensor.__console__ - logger = RegistrationStatisticsLogger(console, output_in_place) + logger = RegistrationStatisticsLogger(bt_console, output_in_place) logger.start() solution = None @@ -735,27 +745,19 @@ def _solve_for_difficulty_fast( @backoff.on_exception(backoff.constant, Exception, interval=1, max_tries=3) def _get_block_with_retry( - subtensor: "bittensor.subtensor", netuid: int + subtensor: "Subtensor", netuid: int ) -> Tuple[int, int, bytes]: """ Gets the current block number, difficulty, and block hash from the substrate node. Args: - subtensor (:obj:`bittensor.subtensor`, `required`): - The subtensor object to use to get the block number, difficulty, and block hash. - - netuid (:obj:`int`, `required`): - The netuid of the network to get the block number, difficulty, and block hash from. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor object to use to get the block number, difficulty, and block hash. + netuid (int): The netuid of the network to get the block number, difficulty, and block hash from. Returns: - block_number (:obj:`int`): - The current block number. - - difficulty (:obj:`int`): - The current difficulty of the subnet. - - block_hash (:obj:`bytes`): - The current block hash. + block_number (int): The current block number. + difficulty (int): The current difficulty of the subnet. + block_hash (bytes): The current block hash. Raises: Exception: If the block hash is None. @@ -791,7 +793,7 @@ def __exit__(self, *args): def _check_for_newest_block_and_update( - subtensor: "bittensor.subtensor", + subtensor: "Subtensor", netuid: int, old_block_number: int, hotkey_bytes: bytes, @@ -807,28 +809,17 @@ def _check_for_newest_block_and_update( Checks for a new block and updates the current block information if a new block is found. Args: - subtensor (:obj:`bittensor.subtensor`, `required`): - The subtensor object to use for getting the current block. - netuid (:obj:`int`, `required`): - The netuid to use for retrieving the difficulty. - old_block_number (:obj:`int`, `required`): - The old block number to check against. - hotkey_bytes (:obj:`bytes`, `required`): - The bytes of the hotkey's pubkey. - curr_diff (:obj:`multiprocessing.Array`, `required`): - The current difficulty as a multiprocessing array. - curr_block (:obj:`multiprocessing.Array`, `required`): - Where the current block is stored as a multiprocessing array. - curr_block_num (:obj:`multiprocessing.Value`, `required`): - Where the current block number is stored as a multiprocessing value. - update_curr_block (:obj:`Callable`, `required`): - A function that updates the current block. - check_block (:obj:`multiprocessing.Lock`, `required`): - A mp lock that is used to check for a new block. - solvers (:obj:`List[_Solver]`, `required`): - A list of solvers to update the current block for. - curr_stats (:obj:`RegistrationStatistics`, `required`): - The current registration statistics to update. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor object to use for getting the current block. + netuid (int): The netuid to use for retrieving the difficulty. + old_block_number (int): The old block number to check against. + hotkey_bytes (bytes): The bytes of the hotkey's pubkey. + curr_diff (multiprocessing.Array): The current difficulty as a multiprocessing array. + curr_block (multiprocessing.Array): Where the current block is stored as a multiprocessing array. + curr_block_num (multiprocessing.Value): Where the current block number is stored as a multiprocessing value. + update_curr_block (Callable): A function that updates the current block. + check_block (multiprocessing.Lock): A mp lock that is used to check for a new block. + solvers (List[_Solver]): A list of solvers to update the current block for. + curr_stats (RegistrationStatistics): The current registration statistics to update. Returns: (int) The current block number. @@ -866,8 +857,8 @@ def _check_for_newest_block_and_update( def _solve_for_difficulty_fast_cuda( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", netuid: int, output_in_place: bool = True, update_interval: int = 50_000, @@ -880,27 +871,19 @@ def _solve_for_difficulty_fast_cuda( """ Solves the registration fast using CUDA Args: - subtensor: bittensor.subtensor - The subtensor node to grab blocks - wallet: bittensor.wallet - The wallet to register - netuid: int - The netuid of the subnet to register to. - output_in_place: bool - If true, prints the output in place, otherwise prints to new lines - update_interval: int - The number of nonces to try before checking for more blocks - tpb: int - The number of threads per block. CUDA param that should match the GPU capability - dev_id: Union[List[int], int] - The CUDA device IDs to execute the registration on, either a single device or a list of devices - n_samples: int - The number of samples of the hash_rate to keep for the EWMA - alpha_: float - The alpha for the EWMA for the hash_rate calculation - log_verbose: bool - If true, prints more verbose logging of the registration metrics. - Note: The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor node to grab blocks. + wallet (bittensor_wallet.Wallet): The wallet to register. + netuid (int): The netuid of the subnet to register to. + output_in_place (bool): If true, prints the output in place, otherwise prints to new lines. + update_interval (int): The number of nonces to try before checking for more blocks. + tpb (int): The number of threads per block. CUDA param that should match the GPU capability. + dev_id (Union[List[int], int]): The CUDA device IDs to execute the registration on, either a single device or a list of devices. + n_samples (int): The number of samples of the hash_rate to keep for the EWMA. + alpha_ (float): The alpha for the EWMA for the hash_rate calculation. + log_verbose (bool): If true, prints more verbose logging of the registration metrics. + + Note: + The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. """ if isinstance(dev_id, int): dev_id = [dev_id] @@ -919,7 +902,7 @@ def _solve_for_difficulty_fast_cuda( with _UsingSpawnStartMethod(force=True): curr_block, curr_block_num, curr_diff = _CUDASolver.create_shared_memory() - ## Create a worker per CUDA device + # Create a worker per CUDA device num_processes = len(dev_id) # Establish communication queues @@ -994,8 +977,7 @@ def _solve_for_difficulty_fast_cuda( start_time_perpetual = time.time() - console = bittensor.__console__ - logger = RegistrationStatisticsLogger(console, output_in_place) + logger = RegistrationStatisticsLogger(bt_console, output_in_place) logger.start() hash_rates = [0] * n_samples # The last n true hash_rates @@ -1109,37 +1091,22 @@ def create_pow( """ Creates a proof of work for the given subtensor and wallet. Args: - subtensor (:obj:`bittensor.subtensor.subtensor`, `required`): - The subtensor to create a proof of work for. - wallet (:obj:`bittensor.wallet.wallet`, `required`): - The wallet to create a proof of work for. - netuid (:obj:`int`, `required`): - The netuid for the subnet to create a proof of work for. - output_in_place (:obj:`bool`, `optional`, defaults to :obj:`True`): - If true, prints the progress of the proof of work to the console - in-place. Meaning the progress is printed on the same lines. - cuda (:obj:`bool`, `optional`, defaults to :obj:`False`): - If true, uses CUDA to solve the proof of work. - dev_id (:obj:`Union[List[int], int]`, `optional`, defaults to :obj:`0`): - The CUDA device id(s) to use. If cuda is true and dev_id is a list, - then multiple CUDA devices will be used to solve the proof of work. - tpb (:obj:`int`, `optional`, defaults to :obj:`256`): - The number of threads per block to use when solving the proof of work. - Should be a multiple of 32. - num_processes (:obj:`int`, `optional`, defaults to :obj:`None`): - The number of processes to use when solving the proof of work. - If None, then the number of processes is equal to the number of - CPU cores. - update_interval (:obj:`int`, `optional`, defaults to :obj:`None`): - The number of nonces to run before checking for a new block. - log_verbose (:obj:`bool`, `optional`, defaults to :obj:`False`): - If true, prints the progress of the proof of work more verbosely. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor to create a proof of work for. + wallet (bittensor_wallet.Wallet): The wallet to create a proof of work for. + netuid (int): The netuid for the subnet to create a proof of work for. + output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning the progress is printed on the same lines. + cuda (bool): If true, uses CUDA to solve the proof of work. + dev_id (Union[List[int], int]): The CUDA device id(s) to use. If cuda is true and dev_id is a list, then multiple CUDA devices will be used to solve the proof of work. + tpb (int): The number of threads per block to use when solving the proof of work. Should be a multiple of 32. + num_processes (int): The number of processes to use when solving the proof of work. If None, then the number of processes is equal to the number of CPU cores. + update_interval (int): The number of nonces to run before checking for a new block. + log_verbose (bool): If true, prints the progress of the proof of work more verbosely. + Returns: - :obj:`Optional[Dict[str, Any]]`: The proof of work solution or None if - the wallet is already registered or there is a different error. + Optional[Dict[str, Any]] : The proof of work solution or None if the wallet is already registered or there is a different error. Raises: - :obj:`ValueError`: If the subnet does not exist. + ValueError: If the subnet does not exist. """ if netuid != -1: if not subtensor.subnet_exists(netuid=netuid): diff --git a/bittensor/utils/test_utils.py b/bittensor/utils/test_utils.py deleted file mode 100644 index fdaa1bda9..000000000 --- a/bittensor/utils/test_utils.py +++ /dev/null @@ -1,22 +0,0 @@ -import socket -from random import randint -from typing import Set - -max_tries = 10 - - -def get_random_unused_port(allocated_ports: Set = set()): - def port_in_use(port: int) -> bool: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - return s.connect_ex(("localhost", port)) == 0 - - tries = 0 - while tries < max_tries: - tries += 1 - port = randint(2**14, 2**16 - 1) - - if port not in allocated_ports and not port_in_use(port): - allocated_ports.add(port) - return port - - raise RuntimeError(f"Tried {max_tries} random ports and could not find an open one") diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index fbc1e297a..44d8e1198 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -1,3 +1,20 @@ +# 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. + from typing import Optional from pathlib import Path import time @@ -83,6 +100,7 @@ def check_version(timeout: int = 15): __version__, latest_version ) ) + pass except Exception as e: raise VersionCheckError("Version check failed") from e diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index de26d98c0..1e0b9ade8 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -1,26 +1,25 @@ -""" -Conversion for weight between chain representation and np.array or torch.Tensor -""" - # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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. +"""Conversion for weight between chain representation and np.array or torch.Tensor""" + import hashlib import logging +import typing from typing import Tuple, List, Union import numpy as np @@ -28,9 +27,14 @@ from scalecodec import ScaleBytes, U16, Vec from substrateinterface import Keypair -import bittensor +from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch, legacy_torch_api_compat +if typing.TYPE_CHECKING: + from bittensor.core.metagraph import Metagraph + from bittensor.core.subtensor import Subtensor + + U32_MAX = 4294967295 U16_MAX = 65535 @@ -39,15 +43,13 @@ def normalize_max_weight( x: Union[NDArray[np.float32], "torch.FloatTensor"], limit: float = 0.1 ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: - r"""Normalizes the tensor x so that sum(x) = 1 and the max value is not greater than the limit. + """Normalizes the tensor x so that sum(x) = 1 and the max value is not greater than the limit. Args: - x (:obj:`np.float32`): - Tensor to be max_value normalized. - limit: float: - Max value after normalization. + x (:obj:`np.float32`): Tensor to be max_value normalized. + limit: float: Max value after normalization. + Returns: - y (:obj:`np.float32`): - Normalized x tensor. + y (:obj:`np.float32`): Normalized x tensor. """ epsilon = 1e-7 # For numerical stability after normalization @@ -88,17 +90,14 @@ def normalize_max_weight( def convert_weight_uids_and_vals_to_tensor( n: int, uids: List[int], weights: List[int] ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: - r"""Converts weights and uids from chain representation into a np.array (inverse operation from convert_weights_and_uids_for_emit) + """Converts weights and uids from chain representation into a np.array (inverse operation from convert_weights_and_uids_for_emit) Args: - n: int: - number of neurons on network. - uids (:obj:`List[int],`): - Tensor of uids as destinations for passed weights. - weights (:obj:`List[int],`): - Tensor of weights. + n (int): number of neurons on network. + uids (List[int]): Tensor of uids as destinations for passed weights. + weights (List[int]): Tensor of weights. + Returns: - row_weights ( np.float32 or torch.FloatTensor ): - Converted row weights. + row_weights (np.float32 or torch.FloatTensor): Converted row weights. """ row_weights = ( torch.zeros([n], dtype=torch.float32) @@ -118,21 +117,16 @@ def convert_weight_uids_and_vals_to_tensor( def convert_root_weight_uids_and_vals_to_tensor( n: int, uids: List[int], weights: List[int], subnets: List[int] ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: - r"""Converts root weights and uids from chain representation into a np.array or torch FloatTensor (inverse operation from convert_weights_and_uids_for_emit) + """Converts root weights and uids from chain representation into a np.array or torch FloatTensor (inverse operation from convert_weights_and_uids_for_emit) Args: - n: int: - number of neurons on network. - uids (:obj:`List[int],`): - Tensor of uids as destinations for passed weights. - weights (:obj:`List[int],`): - Tensor of weights. - subnets (:obj:`List[int],`): - list of subnets on the network + n (int): number of neurons on network. + uids (List[int]): Tensor of uids as destinations for passed weights. + weights (List[int]): Tensor of weights. + subnets (List[int]): List of subnets on the network. + Returns: - row_weights ( np.float32 ): - Converted row weights. + row_weights (np.float32): Converted row weights. """ - row_weights = ( torch.zeros([n], dtype=torch.float32) if use_torch() @@ -158,17 +152,14 @@ def convert_root_weight_uids_and_vals_to_tensor( def convert_bond_uids_and_vals_to_tensor( n: int, uids: List[int], bonds: List[int] ) -> Union[NDArray[np.int64], "torch.LongTensor"]: - r"""Converts bond and uids from chain representation into a np.array. + """Converts bond and uids from chain representation into a np.array. Args: - n: int: - number of neurons on network. - uids (:obj:`List[int],`): - Tensor of uids as destinations for passed bonds. - bonds (:obj:`List[int],`): - Tensor of bonds. + n (int): number of neurons on network. + uids (List[int]): Tensor of uids as destinations for passed bonds. + bonds (List[int]): Tensor of bonds. + Returns: - row_bonds ( np.float32 ): - Converted row bonds. + row_bonds (np.float32): Converted row bonds. """ row_bonds = ( torch.zeros([n], dtype=torch.int64) @@ -184,17 +175,14 @@ def convert_weights_and_uids_for_emit( uids: Union[NDArray[np.int64], "torch.LongTensor"], weights: Union[NDArray[np.float32], "torch.FloatTensor"], ) -> Tuple[List[int], List[int]]: - r"""Converts weights into integer u32 representation that sum to MAX_INT_WEIGHT. + """Converts weights into integer u32 representation that sum to MAX_INT_WEIGHT. Args: - uids (:obj:`np.int64,`): - Tensor of uids as destinations for passed weights. - weights (:obj:`np.float32,`): - Tensor of weights. + uids (np.int64):Tensor of uids as destinations for passed weights. + weights (np.float32):Tensor of weights. + Returns: - weight_uids (List[int]): - Uids as a list. - weight_vals (List[int]): - Weights as a list. + weight_uids (List[int]): Uids as a list. + weight_vals (List[int]): Weights as a list. """ # Checks. weights = weights.tolist() @@ -238,18 +226,18 @@ def process_weights_for_netuid( uids: Union[NDArray[np.int64], "torch.Tensor"], weights: Union[NDArray[np.float32], "torch.Tensor"], netuid: int, - subtensor: "bittensor.subtensor", - metagraph: "bittensor.metagraph" = None, + subtensor: "Subtensor", + metagraph: "Metagraph" = None, exclude_quantile: int = 0, ) -> Union[ Tuple["torch.Tensor", "torch.FloatTensor"], Tuple[NDArray[np.int64], NDArray[np.float32]], ]: - bittensor.logging.debug("process_weights_for_netuid()") - bittensor.logging.debug("weights", weights) - bittensor.logging.debug("netuid", netuid) - bittensor.logging.debug("subtensor", subtensor) - bittensor.logging.debug("metagraph", metagraph) + logging.debug("process_weights_for_netuid()") + logging.debug("weights", weights) + logging.debug("netuid", netuid) + logging.debug("subtensor", subtensor) + logging.debug("metagraph", metagraph) # Get latest metagraph from chain if metagraph is None. if metagraph is None: @@ -268,9 +256,9 @@ def process_weights_for_netuid( quantile = exclude_quantile / U16_MAX min_allowed_weights = subtensor.min_allowed_weights(netuid=netuid) max_weight_limit = subtensor.max_weight_limit(netuid=netuid) - bittensor.logging.debug("quantile", quantile) - bittensor.logging.debug("min_allowed_weights", min_allowed_weights) - bittensor.logging.debug("max_weight_limit", max_weight_limit) + logging.debug("quantile", quantile) + logging.debug("min_allowed_weights", min_allowed_weights) + logging.debug("max_weight_limit", max_weight_limit) # Find all non zero weights. non_zero_weight_idx = ( @@ -282,13 +270,13 @@ def process_weights_for_netuid( non_zero_weights = weights[non_zero_weight_idx] nzw_size = non_zero_weights.numel() if use_torch() else non_zero_weights.size if nzw_size == 0 or metagraph.n < min_allowed_weights: - bittensor.logging.warning("No non-zero weights returning all ones.") + logging.warning("No non-zero weights returning all ones.") final_weights = ( torch.ones((metagraph.n)).to(metagraph.n) / metagraph.n if use_torch() else np.ones((metagraph.n), dtype=np.int64) / metagraph.n ) - bittensor.logging.debug("final_weights", final_weights) + logging.debug("final_weights", final_weights) final_weights_count = ( torch.tensor(list(range(len(final_weights)))) if use_torch() @@ -301,7 +289,7 @@ def process_weights_for_netuid( ) elif nzw_size < min_allowed_weights: - bittensor.logging.warning( + logging.warning( "No non-zero weights less then min allowed weight, returning all ones." ) # ( const ): Should this be np.zeros( ( metagraph.n ) ) to reset everyone to build up weight? @@ -311,10 +299,8 @@ def process_weights_for_netuid( else np.ones((metagraph.n), dtype=np.int64) * 1e-5 ) # creating minimum even non-zero weights weights[non_zero_weight_idx] += non_zero_weights - bittensor.logging.debug("final_weights", weights) - normalized_weights = bittensor.utils.weight_utils.normalize_max_weight( - x=weights, limit=max_weight_limit - ) + logging.debug("final_weights", weights) + normalized_weights = normalize_max_weight(x=weights, limit=max_weight_limit) nw_arange = ( torch.tensor(list(range(len(normalized_weights)))) if use_torch() @@ -322,7 +308,7 @@ def process_weights_for_netuid( ) return nw_arange, normalized_weights - bittensor.logging.debug("non_zero_weights", non_zero_weights) + logging.debug("non_zero_weights", non_zero_weights) # Compute the exclude quantile and find the weights in the lowest quantile max_exclude = max(0, len(non_zero_weights) - min_allowed_weights) / len( @@ -334,21 +320,21 @@ def process_weights_for_netuid( if use_torch() else np.quantile(non_zero_weights, exclude_quantile) ) - bittensor.logging.debug("max_exclude", max_exclude) - bittensor.logging.debug("exclude_quantile", exclude_quantile) - bittensor.logging.debug("lowest_quantile", lowest_quantile) + logging.debug("max_exclude", max_exclude) + logging.debug("exclude_quantile", exclude_quantile) + logging.debug("lowest_quantile", lowest_quantile) # Exclude all weights below the allowed quantile. non_zero_weight_uids = non_zero_weight_uids[lowest_quantile <= non_zero_weights] non_zero_weights = non_zero_weights[lowest_quantile <= non_zero_weights] - bittensor.logging.debug("non_zero_weight_uids", non_zero_weight_uids) - bittensor.logging.debug("non_zero_weights", non_zero_weights) + logging.debug("non_zero_weight_uids", non_zero_weight_uids) + logging.debug("non_zero_weights", non_zero_weights) # Normalize weights and return. - normalized_weights = bittensor.utils.weight_utils.normalize_max_weight( + normalized_weights = normalize_max_weight( x=non_zero_weights, limit=max_weight_limit ) - bittensor.logging.debug("final_weights", normalized_weights) + logging.debug("final_weights", normalized_weights) return non_zero_weight_uids, normalized_weights diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index e88b92e9d..8d05baa40 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -26,7 +26,8 @@ from rich.console import Console from rich.text import Text -from bittensor import Balance, NeuronInfo, AxonInfo, PrometheusInfo +from bittensor.utils.balance import Balance +from bittensor.core.chain_data import AxonInfo, NeuronInfo, PrometheusInfo def __mock_wallet_factory__(*args, **kwargs) -> _MockWallet: @@ -53,11 +54,9 @@ def __eq__(self, __o: Union[float, int, Balance]) -> bool: # True if __o \in [value - tolerance, value + tolerance] # or if value \in [__o - tolerance, __o + tolerance] return ( - (self.value - self.tolerance) <= __o - and __o <= (self.value + self.tolerance) + (self.value - self.tolerance) <= __o <= (self.value + self.tolerance) ) or ( - (__o - self.tolerance) <= self.value - and self.value <= (__o + self.tolerance) + (__o - self.tolerance) <= self.value <= (__o + self.tolerance) ) diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py index f935d89ac..174917042 100644 --- a/tests/integration_tests/test_cli.py +++ b/tests/integration_tests/test_cli.py @@ -27,12 +27,15 @@ from bittensor_wallet import Wallet from bittensor_wallet.mock import get_mock_keypair, get_mock_wallet as generate_wallet -import bittensor -from bittensor import Balance +from bittensor.btcli.cli import COMMANDS as ALL_COMMANDS +from bittensor.btcli.cli import cli as btcli from bittensor.btcli.commands.delegates import _get_coldkey_wallets_for_path from bittensor.btcli.commands.identity import SetIdentityCommand from bittensor.btcli.commands.wallets import _get_coldkey_ss58_addresses_for_path +from bittensor.core.config import Config +from bittensor.core.subtensor import Subtensor from bittensor.mock import MockSubtensor +from bittensor.utils.balance import Balance from tests.helpers import is_running_in_circleci, MockConsole _subtensor_mock: MockSubtensor = MockSubtensor() @@ -55,7 +58,7 @@ def return_mock_sub(*args, **kwargs): return MockSubtensor -@patch("bittensor.subtensor", new_callable=return_mock_sub) +@patch("bittensor.core.subtensor.Subtensor", new_callable=return_mock_sub) class TestCLIWithNetworkAndConfig(unittest.TestCase): def setUp(self): self._config = TestCLIWithNetworkAndConfig.construct_config() @@ -67,20 +70,20 @@ def config(self): @staticmethod def construct_config(): - parser = bittensor.cli.__create_parser__() - defaults = bittensor.config(parser=parser, args=[]) + parser = btcli.__create_parser__() + defaults = Config(parser=parser, args=[]) # Parse commands and subcommands - for command in bittensor.ALL_COMMANDS: + for command in ALL_COMMANDS: if ( - command in bittensor.ALL_COMMANDS - and "commands" in bittensor.ALL_COMMANDS[command] + command in ALL_COMMANDS + and "commands" in ALL_COMMANDS[command] ): - for subcommand in bittensor.ALL_COMMANDS[command]["commands"]: + for subcommand in ALL_COMMANDS[command]["commands"]: defaults.merge( - bittensor.config(parser=parser, args=[command, subcommand]) + Config(parser=parser, args=[command, subcommand]) ) else: - defaults.merge(bittensor.config(parser=parser, args=[command])) + defaults.merge(Config(parser=parser, args=[command])) defaults.netuid = 1 # Always use mock subtensor. @@ -101,7 +104,7 @@ def test_overview(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) mock_hotkeys = ["hk0", "hk1", "hk2", "hk3", "hk4"] @@ -168,9 +171,9 @@ def mock_get_wallet(*args, **kwargs): "bittensor.btcli.commands.overview.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet - with patch("bittensor.__console__", mock_console): + with patch("bittensor.core.settings.bt_console", mock_console): cli.run() # Check that the overview was printed. @@ -209,7 +212,7 @@ def test_overview_not_in_first_subnet(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) mock_hotkeys = ["hk0", "hk1", "hk2", "hk3", "hk4"] @@ -273,9 +276,9 @@ def mock_get_wallet(*args, **kwargs): "bittensor.btcli.commands.overview.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet - with patch("bittensor.__console__", mock_console): + with patch("bittensor.core.settings.bt_console", mock_console): cli.run() # Check that the overview was printed. @@ -313,7 +316,7 @@ def test_overview_with_hotkeys_config(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_overview_without_hotkeys_config(self, _): @@ -324,7 +327,7 @@ def test_overview_without_hotkeys_config(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_overview_with_sort_by_config(self, _): @@ -336,7 +339,7 @@ def test_overview_with_sort_by_config(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_overview_with_sort_by_bad_column_name(self, _): @@ -348,7 +351,7 @@ def test_overview_with_sort_by_bad_column_name(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_overview_without_sort_by_config(self, _): @@ -359,7 +362,7 @@ def test_overview_without_sort_by_config(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_overview_with_sort_order_config(self, _): @@ -371,7 +374,7 @@ def test_overview_with_sort_order_config(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_overview_with_sort_order_config_bad_sort_type(self, _): @@ -383,7 +386,7 @@ def test_overview_with_sort_order_config_bad_sort_type(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_overview_without_sort_order_config(self, _): @@ -395,7 +398,7 @@ def test_overview_without_sort_order_config(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_overview_with_width_config(self, _): @@ -407,7 +410,7 @@ def test_overview_with_width_config(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_overview_without_width_config(self, _): @@ -419,7 +422,7 @@ def test_overview_without_width_config(self, _): config.all = False config.netuid = [] # Don't set, so it tries all networks. - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_overview_all(self, _): @@ -430,7 +433,7 @@ def test_overview_all(self, _): config.netuid = [] # Don't set, so it tries all networks. config.all = True - cli = bittensor.cli(config) + cli = btcli(config) cli.run() def test_unstake_with_specific_hotkeys(self, _): @@ -474,7 +477,7 @@ def test_unstake_with_specific_hotkeys(self, _): stake=mock_stakes[wallet.hotkey_str].rao, ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -484,7 +487,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before unstaking @@ -550,7 +553,7 @@ def test_unstake_with_all_hotkeys(self, _): stake=mock_stakes[wallet.hotkey_str].rao, ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -564,7 +567,7 @@ def mock_get_wallet(*args, **kwargs): "bittensor.btcli.commands.unstake.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before unstaking @@ -629,7 +632,7 @@ def test_unstake_with_exclude_hotkeys_from_all(self, _): stake=mock_stakes[wallet.hotkey_str].rao, ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -643,7 +646,7 @@ def mock_get_wallet(*args, **kwargs): "bittensor.btcli.commands.unstake.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before unstaking @@ -717,7 +720,7 @@ def test_unstake_with_multiple_hotkeys_max_stake(self, _): stake=mock_stakes[wallet.hotkey_str].rao, ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -731,7 +734,7 @@ def mock_get_wallet(*args, **kwargs): "bittensor.btcli.commands.unstake.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before unstaking @@ -829,7 +832,7 @@ def mock_get_wallet(*args, **kwargs): if wallet.name == kwargs["config"].wallet.name: return wallet - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet for wallet in mock_wallets: @@ -843,10 +846,10 @@ def mock_get_wallet(*args, **kwargs): config.wallet.name = wallet.name config.hotkey_ss58address = delegate_hotkey # Single unstake - cli = bittensor.cli(config) + cli = btcli(config) with patch.object(_subtensor_mock, "_do_unstake") as mock_unstake: with patch( - "bittensor.__console__.print" + "bittensor.core.settings.bt_console.print" ) as mock_print: # Catch console print cli.run() @@ -870,7 +873,7 @@ def mock_get_wallet(*args, **kwargs): # This wallet owns the delegate # Should unstake specified amount self.assertEqual( - kwargs["amount"], bittensor.Balance(config.amount) + kwargs["amount"], Balance(config.amount) ) # No warning for w0 self.assertRaises( @@ -921,7 +924,7 @@ def test_unstake_all(self, _): stake=mock_stakes[wallet.hotkey_str].rao, ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -931,7 +934,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before unstaking @@ -994,7 +997,7 @@ def test_stake_with_specific_hotkeys(self, _): ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -1004,7 +1007,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking @@ -1069,7 +1072,7 @@ def test_stake_with_all_hotkeys(self, _): ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -1079,7 +1082,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet with patch( "bittensor.btcli.commands.stake.get_hotkey_wallets_for_wallet" @@ -1167,7 +1170,7 @@ def test_stake_with_exclude_hotkeys_from_all(self, _): ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -1181,7 +1184,7 @@ def mock_get_wallet(*args, **kwargs): "bittensor.btcli.commands.stake.get_hotkey_wallets_for_wallet" ) as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking @@ -1281,7 +1284,7 @@ def test_stake_with_multiple_hotkeys_max_stake(self, _): ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -1291,7 +1294,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking @@ -1379,7 +1382,7 @@ def test_stake_with_multiple_hotkeys_max_stake_not_enough_balance(self, _): ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -1389,7 +1392,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking @@ -1472,7 +1475,7 @@ def test_stake_with_single_hotkey_max_stake(self, _): ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -1482,7 +1485,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking @@ -1559,7 +1562,7 @@ def test_stake_with_single_hotkey_max_stake_not_enough_balance(self, _): ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -1569,7 +1572,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking @@ -1653,7 +1656,7 @@ def test_stake_with_single_hotkey_max_stake_enough_stake(self, _): ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): if kwargs.get("hotkey"): @@ -1663,7 +1666,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet # Check stakes before staking @@ -1770,7 +1773,7 @@ def mock_get_wallet(*args, **kwargs): if wallet.name == kwargs["config"].wallet.name: return wallet - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet for wallet in mock_wallets: @@ -1794,10 +1797,10 @@ def mock_get_wallet(*args, **kwargs): wallet.name ].tao # Stake an amount below the threshold - cli = bittensor.cli(config) + cli = btcli(config) with patch.object(_subtensor_mock, "_do_stake") as mock_stake: with patch( - "bittensor.__console__.print" + "bittensor.core.settings.bt_console.print" ) as mock_print: # Catch console print cli.run() @@ -1819,7 +1822,7 @@ def mock_get_wallet(*args, **kwargs): # Should stake specified amount self.assertEqual( - kwargs["amount"], bittensor.Balance(config.amount) + kwargs["amount"], Balance(config.amount) ) # No error for w0 self.assertRaises( @@ -1857,7 +1860,7 @@ def test_nominate(self, _): balance=mock_balance.rao, ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): hk = kwargs.get("hotkey") @@ -1873,7 +1876,7 @@ def mock_get_wallet(*args, **kwargs): else: raise ValueError("Mock wallet not found") - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet cli.run() @@ -1936,7 +1939,7 @@ def test_delegate_stake(self, _): success = _subtensor_mock.nominate(wallet=mock_wallets[0]) self.assertTrue(success) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): hk = kwargs.get("hotkey") @@ -1957,7 +1960,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet cli.run() @@ -2038,7 +2041,7 @@ def test_undelegate_stake(self, _): ) self.assertAlmostEqual(stake.tao, mock_delegated.tao, places=4) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): hk = kwargs.get("hotkey") @@ -2059,7 +2062,7 @@ def mock_get_wallet(*args, **kwargs): else: return mock_wallets[0] - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet cli.run() @@ -2106,7 +2109,7 @@ def test_transfer(self, _): balance=mock_balances[wallet.name].rao, ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): name_ = kwargs.get("name") @@ -2120,7 +2123,7 @@ def mock_get_wallet(*args, **kwargs): else: raise ValueError(f"No mock wallet found with name: {name_}") - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet cli.run() @@ -2174,7 +2177,7 @@ def test_transfer_not_enough_balance(self, _): balance=mock_balances[wallet.name].rao, ) - cli = bittensor.cli(config) + cli = btcli(config) def mock_get_wallet(*args, **kwargs): name_ = kwargs.get("name") @@ -2189,10 +2192,10 @@ def mock_get_wallet(*args, **kwargs): raise ValueError(f"No mock wallet found with name: {name_}") mock_console = MockConsole() - with patch("bittensor.wallet") as mock_create_wallet: + with patch("bittensor_wallet.Wallet") as mock_create_wallet: mock_create_wallet.side_effect = mock_get_wallet - with patch("bittensor.__console__", mock_console): + with patch("bittensor.core.settings.bt_console", mock_console): cli.run() # Check that the overview was printed. @@ -2234,13 +2237,13 @@ def test_register(self, _): balance=Balance.from_float(200.0), ) - with patch("bittensor.wallet", return_value=mock_wallet) as mock_create_wallet: - cli = bittensor.cli(config) + with patch("bittensor_wallet.Wallet", return_value=mock_wallet) as mock_create_wallet: + cli = btcli(config) cli.run() mock_create_wallet.assert_called_once() # Verify that the wallet was registered - subtensor = bittensor.subtensor(config) + subtensor = Subtensor(config) registered = subtensor.is_hotkey_registered_on_subnet( hotkey_ss58=mock_wallet.hotkey.ss58_address, netuid=1 ) @@ -2262,13 +2265,13 @@ def test_pow_register(self, _): class MockException(Exception): pass - with patch("bittensor.wallet", return_value=mock_wallet) as mock_create_wallet: + with patch("bittensor_wallet.Wallet", return_value=mock_wallet) as mock_create_wallet: with patch( - "bittensor.api.extrinsics.registration.POWSolution.is_stale", + "bittensor.utils.registration.POWSolution.is_stale", side_effect=MockException, ) as mock_is_stale: with pytest.raises(MockException): - cli = bittensor.cli(config) + cli = btcli(config) cli.run() mock_create_wallet.assert_called_once() @@ -2286,7 +2289,7 @@ def test_stake(self, _): config.model = "core_server" config.hotkey = "hk0" - subtensor = bittensor.subtensor(config) + subtensor = Subtensor(config) mock_wallet = generate_wallet(hotkey=get_mock_keypair(100, self.id())) @@ -2300,13 +2303,13 @@ def test_stake(self, _): ).rao, # 1.0 tao extra for fees, etc ) - with patch("bittensor.wallet", return_value=mock_wallet) as mock_create_wallet: + with patch("bittensor_wallet.Wallet", return_value=mock_wallet) as mock_create_wallet: old_stake = subtensor.get_stake_for_coldkey_and_hotkey( hotkey_ss58=mock_wallet.hotkey.ss58_address, coldkey_ss58=mock_wallet.coldkey.ss58_address, ) - cli = bittensor.cli(config) + cli = btcli(config) cli.run() mock_create_wallet.assert_called() self.assertEqual(mock_create_wallet.call_count, 2) @@ -2351,10 +2354,10 @@ def register_mock_neuron(i: int) -> int: _subtensor_mock.neurons_lite(netuid=config.netuid) - cli = bittensor.cli(config) + cli = btcli(config) mock_console = MockConsole() - with patch("bittensor.__console__", mock_console): + with patch("bittensor.core.settings.bt_console", mock_console): cli.run() # Check that the overview was printed. @@ -2383,7 +2386,7 @@ def test_inspect(self, _): # First create a new coldkey config.command = "wallet" config.subcommand = "new_coldkey" - cli = bittensor.cli(config) + cli = btcli(config) cli.run() # Now let's give it a hotkey @@ -2410,7 +2413,7 @@ def test_inspect(self, _): cli.run() -@patch("bittensor.subtensor", new_callable=return_mock_sub) +@patch("bittensor.core.subtensor.Subtensor", new_callable=return_mock_sub) class TestCLIWithNetworkUsingArgs(unittest.TestCase): """ Test the CLI by passing args directly to the bittensor.cli factory @@ -2419,7 +2422,7 @@ class TestCLIWithNetworkUsingArgs(unittest.TestCase): @unittest.mock.patch.object(MockSubtensor, "get_delegates") def test_list_delegates(self, mocked_get_delegates, _): # Call - cli = bittensor.cli(args=["root", "list_delegates"]) + cli = btcli(args=["root", "list_delegates"]) cli.run() # Assertions @@ -2427,7 +2430,7 @@ def test_list_delegates(self, mocked_get_delegates, _): self.assertEqual(mocked_get_delegates.call_count, 2) def test_list_subnets(self, _): - cli = bittensor.cli( + cli = btcli( args=[ "subnets", "list", @@ -2484,9 +2487,9 @@ def test_delegate(self, _): ) with patch( - "bittensor.wallet", return_value=mock_wallet + "bittensor_wallet.Wallet", return_value=mock_wallet ): # Mock wallet creation. SHOULD NOT BE REGISTERED - cli = bittensor.cli( + cli = btcli( args=[ "root", "delegate", @@ -2675,9 +2678,9 @@ def test_set_identity_command( mock_wallet.coldkey.ss58_address = "fake_coldkey_ss58_address" mock_wallet.coldkey = MagicMock() - with patch("bittensor.subtensor", return_value=mock_subtensor), patch( - "bittensor.wallet", return_value=mock_wallet - ), patch("bittensor.__console__", MagicMock()), patch( + with patch("bittensor.core.subtensor.Subtensor", return_value=mock_subtensor), patch( + "bittensor_wallet.Wallet", return_value=mock_wallet + ), patch("bittensor.core.settings.bt_console", MagicMock()), patch( "rich.prompt.Prompt.ask", side_effect=["y", "y"] ), patch("sys.exit") as mock_exit: # Act diff --git a/tests/integration_tests/test_cli_no_network.py b/tests/integration_tests/test_cli_no_network.py index 2470d1560..f3f4f0bdb 100644 --- a/tests/integration_tests/test_cli_no_network.py +++ b/tests/integration_tests/test_cli_no_network.py @@ -26,8 +26,8 @@ from tests.helpers import _get_mock_coldkey, __mock_wallet_factory__, MockConsole -import bittensor -from bittensor import Balance +# import bittensor +from bittensor.utils.balance import Balance from rich.table import Table @@ -37,14 +37,14 @@ class MockException(Exception): mock_delegate_info = { "hotkey_ss58": "", - "total_stake": bittensor.Balance.from_rao(0), + "total_stake": Balance.from_rao(0), "nominators": [], "owner_ss58": "", "take": 0.18, "validator_permits": [], "registrations": [], - "return_per_1000": bittensor.Balance.from_rao(0), - "total_daily_return": bittensor.Balance.from_rao(0), + "return_per_1000": Balance.from_rao(0), + "total_daily_return": Balance.from_rao(0), } @@ -457,14 +457,14 @@ def test_command_no_args(self, _, __, patched_prompt_ask): mock_delegate_info = { "hotkey_ss58": "", - "total_stake": bittensor.Balance.from_rao(0), + "total_stake": Balance.from_rao(0), "nominators": [], "owner_ss58": "", "take": 0.18, "validator_permits": [], "registrations": [], - "return_per_1000": bittensor.Balance.from_rao(0), - "total_daily_return": bittensor.Balance.from_rao(0), + "return_per_1000": Balance.from_rao(0), + "total_daily_return": Balance.from_rao(0), } @@ -1105,14 +1105,14 @@ def test_delegate_prompt_hotkey(self, _): return_value=[ bittensor.DelegateInfo( hotkey_ss58=delegate_ss58, # return delegate with mock coldkey - total_stake=bittensor.Balance.from_float(0.1), + total_stake=Balance.from_float(0.1), nominators=[], owner_ss58="", take=0.18, validator_permits=[], registrations=[], - return_per_1000=bittensor.Balance.from_float(0.1), - total_daily_return=bittensor.Balance.from_float(0.1), + return_per_1000=Balance.from_float(0.1), + total_daily_return=Balance.from_float(0.1), ) ], ): @@ -1192,14 +1192,14 @@ def test_undelegate_prompt_hotkey(self, _): return_value=[ bittensor.DelegateInfo( hotkey_ss58=delegate_ss58, # return delegate with mock coldkey - total_stake=bittensor.Balance.from_float(0.1), + total_stake=Balance.from_float(0.1), nominators=[], owner_ss58="", take=0.18, validator_permits=[], registrations=[], - return_per_1000=bittensor.Balance.from_float(0.1), - total_daily_return=bittensor.Balance.from_float(0.1), + return_per_1000=Balance.from_float(0.1), + total_daily_return=Balance.from_float(0.1), ) ], ): diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 90b6f2574..a5503f896 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -3,11 +3,11 @@ @pytest.fixture -def force_legacy_torch_compat_api(monkeypatch): +def force_legacy_torch_compatible_api(monkeypatch): monkeypatch.setenv("USE_TORCH", "1") @pytest.fixture -def mock_aioresponse(): +def mock_aio_response(): with aioresponses() as m: yield m diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index 7ef91c92b..b3927ea70 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -1,10 +1,12 @@ -import pytest from unittest.mock import MagicMock, patch -from bittensor.core.subtensor import Subtensor + +import pytest + from bittensor.api.extrinsics.root import ( root_register_extrinsic, set_root_weights_extrinsic, ) +from bittensor.core.subtensor import Subtensor @pytest.fixture @@ -293,7 +295,7 @@ def test_set_root_weights_extrinsic_torch( prompt, user_response, expected_success, - force_legacy_torch_compat_api, + force_legacy_torch_compatible_api, ): test_set_root_weights_extrinsic( mock_subtensor, diff --git a/tests/unit_tests/extrinsics/test_senate.py b/tests/unit_tests/extrinsics/test_senate.py index ecf9c5b17..32bb7bdff 100644 --- a/tests/unit_tests/extrinsics/test_senate.py +++ b/tests/unit_tests/extrinsics/test_senate.py @@ -1,6 +1,7 @@ import pytest from unittest.mock import MagicMock, patch -from bittensor import subtensor, wallet +from bittensor.core.subtensor import Subtensor +from bittensor_wallet import Wallet from bittensor.api.extrinsics.senate import ( leave_senate_extrinsic, register_senate_extrinsic, @@ -11,14 +12,14 @@ # Mocking external dependencies @pytest.fixture def mock_subtensor(): - mock = MagicMock(spec=subtensor) + mock = MagicMock(spec=Subtensor) mock.substrate = MagicMock() return mock @pytest.fixture def mock_wallet(): - mock = MagicMock(spec=wallet) + mock = MagicMock(spec=Wallet) mock.coldkey = MagicMock() mock.hotkey = MagicMock() mock.hotkey.ss58_address = "fake_hotkey_address" @@ -67,7 +68,7 @@ def test_register_senate_extrinsic( error_message="error", ), ) as mock_submit_extrinsic, patch.object( - mock_wallet, "is_senate_member", return_value=is_registered + mock_subtensor, "is_senate_member", return_value=is_registered ): # Act result = register_senate_extrinsic( @@ -223,7 +224,7 @@ def test_leave_senate_extrinsic( process_events=MagicMock(), error_message="error", ), - ), patch.object(mock_wallet, "is_senate_member", return_value=is_registered): + ), patch.object(mock_subtensor, "is_senate_member", return_value=is_registered): # Act result = leave_senate_extrinsic( subtensor=mock_subtensor, diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index e25fe46cd..38d4a6944 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -1,20 +1,22 @@ import torch import pytest from unittest.mock import MagicMock, patch -from bittensor import subtensor, wallet + +from bittensor.core.subtensor import Subtensor +from bittensor_wallet import Wallet from bittensor.api.extrinsics.set_weights import set_weights_extrinsic @pytest.fixture def mock_subtensor(): - mock = MagicMock(spec=subtensor) + mock = MagicMock(spec=Subtensor) mock.network = "mock_network" return mock @pytest.fixture def mock_wallet(): - mock = MagicMock(spec=wallet) + mock = MagicMock(spec=Wallet) return mock diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py index 87afc66e6..bc96b0256 100644 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ b/tests/unit_tests/extrinsics/test_unstaking.py @@ -1,22 +1,23 @@ -import bittensor -import pytest - from unittest.mock import patch, MagicMock -from bittensor.utils.balance import Balance +import pytest +from bittensor_wallet import Wallet + from bittensor.api.extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic +from bittensor.core.subtensor import Subtensor +from bittensor.utils.balance import Balance @pytest.fixture def mock_subtensor(): - mock = MagicMock(spec=bittensor.subtensor) + mock = MagicMock(spec=Subtensor) mock.network = "mock_network" return mock @pytest.fixture def mock_wallet(): - mock = MagicMock(spec=bittensor.wallet) + mock = MagicMock(spec=Wallet) mock.hotkey.ss58_address = "5FHneW46..." mock.coldkeypub.ss58_address = "5Gv8YYFu8..." mock.hotkey_str = "mock_hotkey_str" @@ -105,7 +106,7 @@ def test_unstake_extrinsic( mock_subtensor._do_unstake.assert_called_once_with( wallet=mock_wallet, hotkey_ss58=hotkey_ss58 or mock_wallet.hotkey.ss58_address, - amount=bittensor.Balance.from_tao(amount) + amount=Balance.from_tao(amount) if amount else mock_current_stake, wait_for_inclusion=wait_for_inclusion, @@ -241,7 +242,7 @@ def test_unstake_extrinsic( None, 0, TypeError, - "amounts must be a [list of bittensor.Balance or float] or None", + "amounts must be a [list of Balance or float] or None", ), ], ids=[ diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index be567e542..9256a5213 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -240,7 +240,7 @@ def test_to_parameter_dict(axon_info, test_case): def test_to_parameter_dict_torch( axon_info, test_case, - force_legacy_torch_compat_api, + force_legacy_torch_compatible_api, ): result = axon_info.to_parameter_dict() @@ -310,7 +310,7 @@ def test_from_parameter_dict(parameter_dict, expected, test_case): ], ) def test_from_parameter_dict_torch( - parameter_dict, expected, test_case, force_legacy_torch_compat_api + parameter_dict, expected, test_case, force_legacy_torch_compatible_api ): # Act result = AxonInfo.from_parameter_dict(parameter_dict) diff --git a/tests/unit_tests/test_metagraph.py b/tests/unit_tests/test_metagraph.py index 7b39ba0e1..826fa527f 100644 --- a/tests/unit_tests/test_metagraph.py +++ b/tests/unit_tests/test_metagraph.py @@ -21,9 +21,8 @@ import numpy as np import pytest -import bittensor from bittensor.core import settings -from bittensor.core.metagraph import metagraph as Metagraph +from bittensor.core.metagraph import Metagraph @pytest.fixture @@ -59,7 +58,7 @@ def mock_environment(): def test_set_metagraph_attributes(mock_environment): subtensor, neurons = mock_environment - metagraph = bittensor.metagraph(1, sync=False) + metagraph = Metagraph(1, sync=False) metagraph.neurons = neurons metagraph._set_metagraph_attributes(block=5, subtensor=subtensor) @@ -97,7 +96,7 @@ def test_set_metagraph_attributes(mock_environment): def test_process_weights_or_bonds(mock_environment): _, neurons = mock_environment - metagraph = bittensor.metagraph(1, sync=False) + metagraph = Metagraph(1, sync=False) metagraph.neurons = neurons # Test weights processing diff --git a/tests/unit_tests/test_overview.py b/tests/unit_tests/test_overview.py index 0d8d61017..597d76669 100644 --- a/tests/unit_tests/test_overview.py +++ b/tests/unit_tests/test_overview.py @@ -1,13 +1,12 @@ -# Standard Lib + from copy import deepcopy from unittest.mock import MagicMock, patch -# Pytest import pytest -# Bittensor -import bittensor +from bittensor.btcli.cli import cli as btcli, COMMANDS as ALL_COMMANDS from bittensor.btcli.commands import OverviewCommand +from bittensor.core.config import Config from tests.unit_tests.factories.neuron_factory import NeuronInfoLiteFactory @@ -26,20 +25,20 @@ def fake_config(**kwargs): def construct_config(): - parser = bittensor.cli.__create_parser__() - defaults = bittensor.config(parser=parser, args=[]) + parser = btcli.__create_parser__() + defaults = Config(parser=parser, args=[]) # Parse commands and subcommands - for command in bittensor.ALL_COMMANDS: + for command in ALL_COMMANDS: if ( - command in bittensor.ALL_COMMANDS - and "commands" in bittensor.ALL_COMMANDS[command] + command in ALL_COMMANDS + and "commands" in ALL_COMMANDS[command] ): - for subcommand in bittensor.ALL_COMMANDS[command]["commands"]: + for subcommand in ALL_COMMANDS[command]["commands"]: defaults.merge( - bittensor.config(parser=parser, args=[command, subcommand]) + Config(parser=parser, args=[command, subcommand]) ) else: - defaults.merge(bittensor.config(parser=parser, args=[command])) + defaults.merge(Config(parser=parser, args=[command])) defaults.netuid = 1 # Always use mock subtensor. @@ -93,7 +92,7 @@ def test_get_total_balance( mock_wallet.coldkeypub_file.is_encrypted.return_value = is_encrypted with patch( - "bittensor.wallet", return_value=mock_wallet + "bittensor_wallet.Wallet", return_value=mock_wallet ) as mock_wallet_constructor, patch( "bittensor.btcli.commands.overview.get_coldkey_wallets_for_path", return_value=[mock_wallet] if config_all else [], diff --git a/tests/unit_tests/utils/test_balance.py b/tests/unit_tests/utils/test_balance.py index b99bc111f..bf9ac10bb 100644 --- a/tests/unit_tests/utils/test_balance.py +++ b/tests/unit_tests/utils/test_balance.py @@ -3,7 +3,7 @@ from hypothesis import strategies as st from typing import Union -from bittensor import Balance +from bittensor.utils.balance import Balance from tests.helpers import CLOSE_IN_VALUE """ diff --git a/tests/unit_tests/utils/test_registration.py b/tests/unit_tests/utils/test_registration.py index d0c4fc743..c85608b5f 100644 --- a/tests/unit_tests/utils/test_registration.py +++ b/tests/unit_tests/utils/test_registration.py @@ -1,3 +1,20 @@ +# 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 pytest from bittensor.utils.registration import LazyLoadedTorch @@ -14,7 +31,7 @@ def error(self, message): @pytest.fixture def mock_bittensor_logging(monkeypatch): mock_logger = MockBittensorLogging() - monkeypatch.setattr("bittensor.logging", mock_logger) + monkeypatch.setattr("bittensor.utils.registration.logging", mock_logger) return mock_logger diff --git a/tests/unit_tests/utils/test_version.py b/tests/unit_tests/utils/test_version.py index f9760933f..fa96bddad 100644 --- a/tests/unit_tests/utils/test_version.py +++ b/tests/unit_tests/utils/test_version.py @@ -22,13 +22,16 @@ from freezegun import freeze_time from datetime import datetime, timedelta, timezone -from bittensor.utils.version import ( - VERSION_CHECK_THRESHOLD, - VersionCheckError, - get_and_save_latest_version, - check_version, - version_checking, -) +# from bittensor.utils.version import ( +# VERSION_CHECK_THRESHOLD, +# VersionCheckError, +# get_and_save_latest_version, +# check_version, +# version_checking, +# __version__ +# ) +from bittensor.utils import version + from unittest.mock import MagicMock from pytest_mock import MockerFixture @@ -62,14 +65,14 @@ def test_get_and_save_latest_version_no_file( ): assert not version_file_path.exists() - assert get_and_save_latest_version() == pypi_version + assert version.get_and_save_latest_version() == pypi_version mock_get_version_from_pypi.assert_called_once() assert version_file_path.exists() assert version_file_path.read_text() == pypi_version -@pytest.mark.parametrize("elapsed", [0, VERSION_CHECK_THRESHOLD - 5]) +@pytest.mark.parametrize("elapsed", [0, version.VERSION_CHECK_THRESHOLD - 5]) def test_get_and_save_latest_version_file_fresh_check( mock_get_version_from_pypi: MagicMock, version_file_path: Path, elapsed: int ): @@ -78,7 +81,7 @@ def test_get_and_save_latest_version_file_fresh_check( version_file_path.write_text("6.9.5") with freeze_time(now + timedelta(seconds=elapsed)): - assert get_and_save_latest_version() == "6.9.5" + assert version.get_and_save_latest_version() == "6.9.5" mock_get_version_from_pypi.assert_not_called() @@ -90,8 +93,8 @@ def test_get_and_save_latest_version_file_expired_check( version_file_path.write_text("6.9.5") - with freeze_time(now + timedelta(seconds=VERSION_CHECK_THRESHOLD + 1)): - assert get_and_save_latest_version() == pypi_version + with freeze_time(now + timedelta(seconds=version.VERSION_CHECK_THRESHOLD + 1)): + assert version.get_and_save_latest_version() == pypi_version mock_get_version_from_pypi.assert_called_once() assert version_file_path.read_text() == pypi_version @@ -111,13 +114,13 @@ def test_get_and_save_latest_version_file_expired_check( def test_check_version_newer_available( mocker: MockerFixture, current_version: str, latest_version: str, capsys ): - mocker.patch("bittensor.utils.version.bittensor.__version__", current_version) + version.__version__ = current_version mocker.patch( "bittensor.utils.version.get_and_save_latest_version", return_value=latest_version, ) - check_version() + version.check_version() captured = capsys.readouterr() @@ -137,13 +140,13 @@ def test_check_version_newer_available( def test_check_version_up_to_date( mocker: MockerFixture, current_version: str, latest_version: str, capsys ): - mocker.patch("bittensor.utils.version.bittensor.__version__", current_version) + version.__version__ = current_version mocker.patch( "bittensor.utils.version.get_and_save_latest_version", return_value=latest_version, ) - check_version() + version.check_version() captured = capsys.readouterr() @@ -153,16 +156,16 @@ def test_check_version_up_to_date( def test_version_checking(mocker: MockerFixture): mock = mocker.patch("bittensor.utils.version.check_version") - version_checking() + version.version_checking() mock.assert_called_once() def test_version_checking_exception(mocker: MockerFixture): mock = mocker.patch( - "bittensor.utils.version.check_version", side_effect=VersionCheckError + "bittensor.utils.version.check_version", side_effect=version.VersionCheckError ) - version_checking() + version.version_checking() mock.assert_called_once() diff --git a/tests/unit_tests/utils/test_weight_utils.py b/tests/unit_tests/utils/test_weight_utils.py index 66f3c8127..77f7fad0f 100644 --- a/tests/unit_tests/utils/test_weight_utils.py +++ b/tests/unit_tests/utils/test_weight_utils.py @@ -57,7 +57,7 @@ def test_convert_weight_and_uids(): weight_utils.convert_weights_and_uids_for_emit(uids, weights) -def test_convert_weight_and_uids_torch(force_legacy_torch_compat_api): +def test_convert_weight_and_uids_torch(force_legacy_torch_compatible_api): uids = torch.tensor(list(range(10))) weights = torch.rand(10) weight_utils.convert_weights_and_uids_for_emit(uids, weights) @@ -144,7 +144,7 @@ def test_normalize_with_max_weight(): def test_normalize_with_max_weight__legacy_torch_api_compat( - force_legacy_torch_compat_api, + force_legacy_torch_compatible_api, ): weights = torch.rand(1000) wn = weight_utils.normalize_max_weight(weights, limit=0.01) @@ -240,7 +240,7 @@ def test_convert_weight_uids_and_vals_to_tensor_happy_path( ], ) def test_convert_weight_uids_and_vals_to_tensor_happy_path_torch( - test_id, n, uids, weights, subnets, expected, force_legacy_torch_compat_api + test_id, n, uids, weights, subnets, expected, force_legacy_torch_compatible_api ): # Act result = weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) @@ -338,7 +338,7 @@ def test_convert_root_weight_uids_and_vals_to_tensor_happy_paths( ], ) def test_convert_root_weight_uids_and_vals_to_tensor_edge_cases( - test_id, n, uids, weights, subnets, expected, force_legacy_torch_compat_api + test_id, n, uids, weights, subnets, expected, force_legacy_torch_compatible_api ): # Act result = weight_utils.convert_root_weight_uids_and_vals_to_tensor( @@ -468,7 +468,7 @@ def test_happy_path(test_id, n, uids, bonds, expected_output): ], ) def test_happy_path_torch( - test_id, n, uids, bonds, expected_output, force_legacy_torch_compat_api + test_id, n, uids, bonds, expected_output, force_legacy_torch_compatible_api ): # Act result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) @@ -512,7 +512,7 @@ def test_edge_cases(test_id, n, uids, bonds, expected_output): ], ) def test_edge_cases_torch( - test_id, n, uids, bonds, expected_output, force_legacy_torch_compat_api + test_id, n, uids, bonds, expected_output, force_legacy_torch_compatible_api ): # Act result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) From a95ee4c8e1dd62f6073041ad3263aa37a84efe3b Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 5 Aug 2024 09:11:22 -0700 Subject: [PATCH 058/260] Add backwards compatibility for 'bittensor.api.extrinsics' as 'bittensor.extrinsics' --- bittensor/utils/backwards_compatibility.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bittensor/utils/backwards_compatibility.py b/bittensor/utils/backwards_compatibility.py index f8fa4b0f9..1171ae012 100644 --- a/bittensor/utils/backwards_compatibility.py +++ b/bittensor/utils/backwards_compatibility.py @@ -20,6 +20,8 @@ the Bittensor framework, specifically up to and including version 7.3.0. This module addresses changes and deprecated features in recent versions, allowing users to maintain compatibility with older systems and projects. """ +import sys +import importlib from bittensor_wallet.errors import KeyFileError # noqa: F401 from bittensor_wallet.keyfile import ( # noqa: F401 @@ -141,3 +143,7 @@ __tao_symbol__ = settings.tao_symbol __rao_symbol__ = settings.rao_symbol + +# Makes the `bittensor.api.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. +extrinsics = importlib.import_module('bittensor.api.extrinsics') +sys.modules['bittensor.extrinsics'] = extrinsics From f68ed6ee02d18372da7f754671010ec1ed75f4cb Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 5 Aug 2024 09:30:12 -0700 Subject: [PATCH 059/260] Move the contents of `wallet_utils.py` to `bittensor/utils/__init__.py`, since it is used directly from utils. --- bittensor/core/metagraph.py | 15 +- bittensor/core/subtensor.py | 11 +- bittensor/utils/__init__.py | 174 ++++++++++++++++++--- bittensor/utils/backwards_compatibility.py | 3 +- bittensor/utils/wallet_utils.py | 168 -------------------- bittensor/utils/weight_utils.py | 6 +- 6 files changed, 169 insertions(+), 208 deletions(-) delete mode 100644 bittensor/utils/wallet_utils.py diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 523b68c85..659b994df 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -24,15 +24,16 @@ import numpy as np from numpy.typing import NDArray -from rich.console import Console from . import settings from .chain_data import AxonInfo from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch -from bittensor.utils.weight_utils import convert_weight_uids_and_vals_to_tensor, convert_bond_uids_and_vals_to_tensor - -console = Console() +from bittensor.utils.weight_utils import ( + convert_weight_uids_and_vals_to_tensor, + convert_bond_uids_and_vals_to_tensor, + convert_root_weight_uids_and_vals_to_tensor +) METAGRAPH_STATE_DICT_NDARRAY_KEYS = [ "version", @@ -1135,10 +1136,10 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore with open(graph_filename, "rb") as graph_file: state_dict = pickle.load(graph_file) except pickle.UnpicklingError: - console.print( + settings.bt_console.print( "Unable to load file. Attempting to restore metagraph using torch." ) - console.print( + settings.bt_console.print( ":warning:[yellow]Warning:[/yellow] This functionality exists to load " "metagraph state from legacy saves, but will not be supported in the future." ) @@ -1150,7 +1151,7 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore state_dict[key] = state_dict[key].detach().numpy() del real_torch except (RuntimeError, ImportError): - console.print("Unable to load file. It may be corrupted.") + settings.bt_console.print("Unable to load file. It may be corrupted.") raise self.n = state_dict["n"] diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 5af6a5977..7d4ff1582 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -94,21 +94,20 @@ custom_rpc_type_registry, ) from bittensor.core.config import Config +from bittensor.core.errors import IdentityError, NominationError, StakeError, TakeError from bittensor.core.metagraph import Metagraph +from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams from bittensor.utils import ( U16_NORMALIZED_FLOAT, ss58_to_vec_u8, U64_NORMALIZED_FLOAT, networking, ) -from bittensor.utils import torch, weight_utils, format_error_message -from bittensor.utils import wallet_utils +from bittensor.utils import torch, weight_utils, format_error_message, create_identity_dict, decode_hex_identity_dict from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.registration import POWSolution from bittensor.utils.registration import legacy_torch_api_compat -from .errors import IdentityError, NominationError, StakeError, TakeError -from .types import AxonServeCallParams, PrometheusServeCallParams KEY_NONCE: Dict[str, int] = {} @@ -2779,7 +2778,7 @@ def make_substrate_call_with_retry() -> "ScaleType": identity_info = make_substrate_call_with_retry() - return wallet_utils.decode_hex_identity_dict( + return decode_hex_identity_dict( identity_info.value["info"] ) @@ -2818,7 +2817,7 @@ def update_identity( params = {} if params is None else params - call_params = wallet_utils.create_identity_dict(**params) + call_params = create_identity_dict(**params) call_params["identified"] = identified @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index f5bff9869..3f67321fa 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -16,15 +16,17 @@ # DEALINGS IN THE SOFTWARE. import hashlib -from typing import Callable, List, Dict, Literal, Tuple +from typing import Callable, List, Dict, Literal, Tuple, Union, Optional import numpy as np import scalecodec +from substrateinterface import Keypair as Keypair +from substrateinterface.utils import ss58 +from bittensor.core.settings import ss58_format from .registration import torch, use_torch from .version import version_checking, check_version, VersionCheckError -from .wallet_utils import * # noqa F401 -from ..core.settings import ss58_format + RAOPERTAO = 1e9 U16_MAX = 65535 @@ -48,26 +50,17 @@ def _unbiased_topk( ) -> Union[Tuple[np.ndarray, np.ndarray], Tuple["torch.Tensor", "torch.LongTensor"]]: """Selects topk as in torch.topk but does not bias lower indices when values are equal. Args: - values: (np.ndarray) if using numpy, (torch.Tensor) if using torch: - Values to index into. - k: (int): - Number to take. - dim: (int): - Dimension to index into (used by Torch) - sorted: (bool): - Whether to sort indices. - largest: (bool): - Whether to take the largest value. - axis: (int): - Axis along which to index into (used by Numpy) - return_type: (str): - Whether or use torch or numpy approach + values: (np.ndarray) if using numpy, (torch.Tensor) if using torch: Values to index into. + k (int): Number to take. + dim (int): Dimension to index into (used by Torch) + sorted (bool): Whether to sort indices. + largest (bool): Whether to take the largest value. + axis (int): Axis along which to index into (used by Numpy) + return_type (str): Whether or use torch or numpy approach Return: - topk: (np.ndarray) if using numpy, (torch.Tensor) if using torch: - topk k values. - indices: (np.ndarray) if using numpy, (torch.LongTensor) if using torch: - indices of the topk values. + topk (np.ndarray): if using numpy, (torch.Tensor) if using torch: topk k values. + indices (np.ndarray): if using numpy, (torch.LongTensor) if using torch: indices of the topk values. """ if return_type == "torch": permutation = torch.randperm(values.shape[dim]) @@ -279,3 +272,142 @@ def format_error_message(error_message: dict) -> str: err_docs = error_message.get("docs", []) err_description = err_docs[0] if len(err_docs) > 0 else err_description return f"Subtensor returned `{err_name} ({err_type})` error. This means: `{err_description}`" + + +def create_identity_dict( + display: str = "", + legal: str = "", + web: str = "", + riot: str = "", + email: str = "", + pgp_fingerprint: Optional[str] = None, + image: str = "", + info: str = "", + twitter: str = "", +) -> dict: + """ + Creates a dictionary with structure for identity extrinsic. Must fit within 64 bits. + + Args: + display (str): String to be converted and stored under 'display'. + legal (str): String to be converted and stored under 'legal'. + web (str): String to be converted and stored under 'web'. + riot (str): String to be converted and stored under 'riot'. + email (str): String to be converted and stored under 'email'. + pgp_fingerprint (str): String to be converted and stored under 'pgp_fingerprint'. + image (str): String to be converted and stored under 'image'. + info (str): String to be converted and stored under 'info'. + twitter (str): String to be converted and stored under 'twitter'. + + Returns: + dict: A dictionary with the specified structure and byte string conversions. + + Raises: + ValueError: If pgp_fingerprint is not exactly 20 bytes long when encoded. + """ + if pgp_fingerprint and len(pgp_fingerprint.encode()) != 20: + raise ValueError("pgp_fingerprint must be exactly 20 bytes long when encoded") + + return { + "info": { + "additional": [[]], + "display": {f"Raw{len(display.encode())}": display.encode()}, + "legal": {f"Raw{len(legal.encode())}": legal.encode()}, + "web": {f"Raw{len(web.encode())}": web.encode()}, + "riot": {f"Raw{len(riot.encode())}": riot.encode()}, + "email": {f"Raw{len(email.encode())}": email.encode()}, + "pgp_fingerprint": pgp_fingerprint.encode() if pgp_fingerprint else None, + "image": {f"Raw{len(image.encode())}": image.encode()}, + "info": {f"Raw{len(info.encode())}": info.encode()}, + "twitter": {f"Raw{len(twitter.encode())}": twitter.encode()}, + } + } + + +def decode_hex_identity_dict(info_dictionary): + for key, value in info_dictionary.items(): + if isinstance(value, dict): + item = list(value.values())[0] + if isinstance(item, str) and item.startswith("0x"): + try: + info_dictionary[key] = bytes.fromhex(item[2:]).decode() + except UnicodeDecodeError: + print(f"Could not decode: {key}: {item}") + else: + info_dictionary[key] = item + return info_dictionary + + +def is_valid_ss58_address(address: str) -> bool: + """ + Checks if the given address is a valid ss58 address. + + Args: + address(str): The address to check. + + Returns: + True if the address is a valid ss58 address for Bittensor, False otherwise. + """ + try: + return ss58.is_valid_ss58_address( + address, valid_ss58_format=ss58_format + ) or ss58.is_valid_ss58_address( + address, valid_ss58_format=42 + ) # Default substrate ss58 format (legacy) + except IndexError: + return False + + +def _is_valid_ed25519_pubkey(public_key: Union[str, bytes]) -> bool: + """ + Checks if the given public_key is a valid ed25519 key. + + Args: + public_key(Union[str, bytes]): The public_key to check. + + Returns: + True if the public_key is a valid ed25519 key, False otherwise. + + """ + try: + if isinstance(public_key, str): + if len(public_key) != 64 and len(public_key) != 66: + raise ValueError("a public_key should be 64 or 66 characters") + elif isinstance(public_key, bytes): + if len(public_key) != 32: + raise ValueError("a public_key should be 32 bytes") + else: + raise ValueError("public_key must be a string or bytes") + + keypair = Keypair(public_key=public_key, ss58_format=ss58_format) + + ss58_addr = keypair.ss58_address + return ss58_addr is not None + + except (ValueError, IndexError): + return False + + +def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool: + """ + Checks if the given address is a valid destination address. + + Args: + address(Union[str, bytes]): The address to check. + + Returns: + True if the address is a valid destination address, False otherwise. + """ + if isinstance(address, str): + # Check if ed25519 + if address.startswith("0x"): + return _is_valid_ed25519_pubkey(address) + else: + # Assume ss58 address + return is_valid_ss58_address(address) + elif isinstance(address, bytes): + # Check if ed25519 + return _is_valid_ed25519_pubkey(address) + else: + # Invalid address type + return False diff --git a/bittensor/utils/backwards_compatibility.py b/bittensor/utils/backwards_compatibility.py index 1171ae012..a1780d888 100644 --- a/bittensor/utils/backwards_compatibility.py +++ b/bittensor/utils/backwards_compatibility.py @@ -111,8 +111,7 @@ U16_NORMALIZED_FLOAT, U64_NORMALIZED_FLOAT, u8_key_to_ss58, - get_hash, - wallet_utils, + get_hash ) from bittensor.utils.balance import Balance as Balance # noqa: F401 from bittensor.utils.subnets import SubnetsAPI # noqa: F401 diff --git a/bittensor/utils/wallet_utils.py b/bittensor/utils/wallet_utils.py deleted file mode 100644 index 24c1cb703..000000000 --- a/bittensor/utils/wallet_utils.py +++ /dev/null @@ -1,168 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - -# 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. - -from substrateinterface.utils import ss58 -from typing import Union, Optional - -from ..core.settings import ss58_format -from substrateinterface import Keypair as Keypair - - -def get_ss58_format(ss58_address: str) -> int: - """Returns the ss58 format of the given ss58 address.""" - return ss58.get_ss58_format(ss58_address) - - -def is_valid_ss58_address(address: str) -> bool: - """ - Checks if the given address is a valid ss58 address. - - Args: - address(str): The address to check. - - Returns: - True if the address is a valid ss58 address for Bittensor, False otherwise. - """ - try: - return ss58.is_valid_ss58_address( - address, valid_ss58_format=ss58_format - ) or ss58.is_valid_ss58_address( - address, valid_ss58_format=42 - ) # Default substrate ss58 format (legacy) - except IndexError: - return False - - -def is_valid_ed25519_pubkey(public_key: Union[str, bytes]) -> bool: - """ - Checks if the given public_key is a valid ed25519 key. - - Args: - public_key(Union[str, bytes]): The public_key to check. - - Returns: - True if the public_key is a valid ed25519 key, False otherwise. - - """ - try: - if isinstance(public_key, str): - if len(public_key) != 64 and len(public_key) != 66: - raise ValueError("a public_key should be 64 or 66 characters") - elif isinstance(public_key, bytes): - if len(public_key) != 32: - raise ValueError("a public_key should be 32 bytes") - else: - raise ValueError("public_key must be a string or bytes") - - keypair = Keypair(public_key=public_key, ss58_format=ss58_format) - - ss58_addr = keypair.ss58_address - return ss58_addr is not None - - except (ValueError, IndexError): - return False - - -def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool: - """ - Checks if the given address is a valid destination address. - - Args: - address(Union[str, bytes]): The address to check. - - Returns: - True if the address is a valid destination address, False otherwise. - """ - if isinstance(address, str): - # Check if ed25519 - if address.startswith("0x"): - return is_valid_ed25519_pubkey(address) - else: - # Assume ss58 address - return is_valid_ss58_address(address) - elif isinstance(address, bytes): - # Check if ed25519 - return is_valid_ed25519_pubkey(address) - else: - # Invalid address type - return False - - -def create_identity_dict( - display: str = "", - legal: str = "", - web: str = "", - riot: str = "", - email: str = "", - pgp_fingerprint: Optional[str] = None, - image: str = "", - info: str = "", - twitter: str = "", -) -> dict: - """ - Creates a dictionary with structure for identity extrinsic. Must fit within 64 bits. - - Args: - display (str): String to be converted and stored under 'display'. - legal (str): String to be converted and stored under 'legal'. - web (str): String to be converted and stored under 'web'. - riot (str): String to be converted and stored under 'riot'. - email (str): String to be converted and stored under 'email'. - pgp_fingerprint (str): String to be converted and stored under 'pgp_fingerprint'. - image (str): String to be converted and stored under 'image'. - info (str): String to be converted and stored under 'info'. - twitter (str): String to be converted and stored under 'twitter'. - - Returns: - dict: A dictionary with the specified structure and byte string conversions. - - Raises: - ValueError: If pgp_fingerprint is not exactly 20 bytes long when encoded. - """ - if pgp_fingerprint and len(pgp_fingerprint.encode()) != 20: - raise ValueError("pgp_fingerprint must be exactly 20 bytes long when encoded") - - return { - "info": { - "additional": [[]], - "display": {f"Raw{len(display.encode())}": display.encode()}, - "legal": {f"Raw{len(legal.encode())}": legal.encode()}, - "web": {f"Raw{len(web.encode())}": web.encode()}, - "riot": {f"Raw{len(riot.encode())}": riot.encode()}, - "email": {f"Raw{len(email.encode())}": email.encode()}, - "pgp_fingerprint": pgp_fingerprint.encode() if pgp_fingerprint else None, - "image": {f"Raw{len(image.encode())}": image.encode()}, - "info": {f"Raw{len(info.encode())}": info.encode()}, - "twitter": {f"Raw{len(twitter.encode())}": twitter.encode()}, - } - } - - -def decode_hex_identity_dict(info_dictionary): - for key, value in info_dictionary.items(): - if isinstance(value, dict): - item = list(value.values())[0] - if isinstance(item, str) and item.startswith("0x"): - try: - info_dictionary[key] = bytes.fromhex(item[2:]).decode() - except UnicodeDecodeError: - print(f"Could not decode: {key}: {item}") - else: - info_dictionary[key] = item - return info_dictionary diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 1e0b9ade8..0b9aefc49 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -222,6 +222,7 @@ def convert_weights_and_uids_for_emit( return weight_uids, weight_vals +# The community uses / bittensor does not def process_weights_for_netuid( uids: Union[NDArray[np.int64], "torch.Tensor"], weights: Union[NDArray[np.float32], "torch.Tensor"], @@ -229,10 +230,7 @@ def process_weights_for_netuid( subtensor: "Subtensor", metagraph: "Metagraph" = None, exclude_quantile: int = 0, -) -> Union[ - Tuple["torch.Tensor", "torch.FloatTensor"], - Tuple[NDArray[np.int64], NDArray[np.float32]], -]: +) -> Union[Tuple["torch.Tensor", "torch.FloatTensor"], Tuple[NDArray[np.int64], NDArray[np.float32]]]: logging.debug("process_weights_for_netuid()") logging.debug("weights", weights) logging.debug("netuid", netuid) From 3c6aa6734632c43ab06beb287b9ec140099b3e0c Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 5 Aug 2024 09:35:43 -0700 Subject: [PATCH 060/260] Delete `bccli` staff and related tests. Fix setup.py. --- bin/btcli | 50 - bittensor/btcli/__init__.py | 0 bittensor/btcli/cli.py | 389 --- bittensor/btcli/commands/__init__.py | 124 - .../btcli/commands/check_coldkey_swap.py | 130 - bittensor/btcli/commands/delegates.py | 1153 ------- bittensor/btcli/commands/identity.py | 343 --- bittensor/btcli/commands/inspect.py | 283 -- bittensor/btcli/commands/list.py | 131 - bittensor/btcli/commands/metagraph.py | 271 -- bittensor/btcli/commands/misc.py | 118 - bittensor/btcli/commands/network.py | 654 ---- bittensor/btcli/commands/overview.py | 784 ----- bittensor/btcli/commands/register.py | 619 ---- bittensor/btcli/commands/root.py | 688 ----- bittensor/btcli/commands/senate.py | 657 ---- bittensor/btcli/commands/stake.py | 571 ---- bittensor/btcli/commands/transfer.py | 132 - bittensor/btcli/commands/unstake.py | 301 -- bittensor/btcli/commands/utils.py | 288 -- bittensor/btcli/commands/wallets.py | 1112 ------- bittensor/btcli/commands/weights.py | 294 -- setup.py | 11 +- tests/integration_tests/__init__.py | 16 + tests/integration_tests/test_cli.py | 2741 ----------------- .../integration_tests/test_cli_no_network.py | 1533 --------- .../test_metagraph_integration.py | 9 +- .../test_subtensor_integration.py | 13 +- 28 files changed, 31 insertions(+), 13384 deletions(-) delete mode 100755 bin/btcli delete mode 100644 bittensor/btcli/__init__.py delete mode 100644 bittensor/btcli/cli.py delete mode 100644 bittensor/btcli/commands/__init__.py delete mode 100644 bittensor/btcli/commands/check_coldkey_swap.py delete mode 100644 bittensor/btcli/commands/delegates.py delete mode 100644 bittensor/btcli/commands/identity.py delete mode 100644 bittensor/btcli/commands/inspect.py delete mode 100644 bittensor/btcli/commands/list.py delete mode 100644 bittensor/btcli/commands/metagraph.py delete mode 100644 bittensor/btcli/commands/misc.py delete mode 100644 bittensor/btcli/commands/network.py delete mode 100644 bittensor/btcli/commands/overview.py delete mode 100644 bittensor/btcli/commands/register.py delete mode 100644 bittensor/btcli/commands/root.py delete mode 100644 bittensor/btcli/commands/senate.py delete mode 100644 bittensor/btcli/commands/stake.py delete mode 100644 bittensor/btcli/commands/transfer.py delete mode 100644 bittensor/btcli/commands/unstake.py delete mode 100644 bittensor/btcli/commands/utils.py delete mode 100644 bittensor/btcli/commands/wallets.py delete mode 100644 bittensor/btcli/commands/weights.py delete mode 100644 tests/integration_tests/test_cli.py delete mode 100644 tests/integration_tests/test_cli_no_network.py diff --git a/bin/btcli b/bin/btcli deleted file mode 100755 index fa98536a0..000000000 --- a/bin/btcli +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python - -import websocket - -import sys -import shtab -from bittensor import cli as btcli -from bittensor import logging as bt_logging - - -def main(): - # Create the parser with shtab support - parser = btcli.__create_parser__() - args, unknown = parser.parse_known_args() - - if args.print_completion: # Check for print-completion argument - print(shtab.complete(parser, args.print_completion)) - return - - try: - cli_instance = btcli(args=sys.argv[1:]) - cli_instance.run() - except KeyboardInterrupt: - print('KeyboardInterrupt') - except RuntimeError as e: - bt_logging.error(f'RuntimeError: {e}') - except websocket.WebSocketConnectionClosedException as e: - bt_logging.error(f'Subtensor related error. WebSocketConnectionClosedException: {e}') - - -if __name__ == '__main__': - main() - -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 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. diff --git a/bittensor/btcli/__init__.py b/bittensor/btcli/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/bittensor/btcli/cli.py b/bittensor/btcli/cli.py deleted file mode 100644 index 917e2ac45..000000000 --- a/bittensor/btcli/cli.py +++ /dev/null @@ -1,389 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 sys -from typing import List, Optional - -import shtab - -from bittensor.core.config import Config -from bittensor.core.settings import bt_console, turn_console_on, __version__ -from bittensor.utils import check_version, VersionCheckError -from .commands import ( - AutocompleteCommand, - DelegateStakeCommand, - DelegateUnstakeCommand, - GetIdentityCommand, - GetWalletHistoryCommand, - InspectCommand, - ListCommand, - ListDelegatesCommand, - MetagraphCommand, - MyDelegatesCommand, - NewColdkeyCommand, - NewHotkeyCommand, - NominateCommand, - OverviewCommand, - PowRegisterCommand, - ProposalsCommand, - RegenColdkeyCommand, - RegenColdkeypubCommand, - RegenHotkeyCommand, - RegisterCommand, - RegisterSubnetworkCommand, - RootGetWeightsCommand, - RootList, - RootRegisterCommand, - RootSetBoostCommand, - RootSetSlashCommand, - RootSetWeightsCommand, - RunFaucetCommand, - SenateCommand, - SetIdentityCommand, - SetTakeCommand, - StakeCommand, - StakeShow, - SubnetGetHyperparamsCommand, - SubnetHyperparamsCommand, - SubnetListCommand, - SubnetLockCostCommand, - SubnetSudoCommand, - SwapHotkeyCommand, - TransferCommand, - UnStakeCommand, - UpdateCommand, - UpdateWalletCommand, - VoteCommand, - WalletBalanceCommand, - WalletCreateCommand, - CommitWeightCommand, - RevealWeightCommand, - CheckColdKeySwapCommand, -) - -ALIAS_TO_COMMAND = { - "subnets": "subnets", - "root": "root", - "wallet": "wallet", - "stake": "stake", - "sudo": "sudo", - "legacy": "legacy", - "s": "subnets", - "r": "root", - "w": "wallet", - "st": "stake", - "su": "sudo", - "l": "legacy", - "subnet": "subnets", - "roots": "root", - "wallets": "wallet", - "stakes": "stake", - "sudos": "sudo", - "i": "info", - "info": "info", - "weights": "weights", - "wt": "weights", - "weight": "weights", -} -COMMANDS = { - "subnets": { - "name": "subnets", - "aliases": ["s", "subnet"], - "help": "Commands for managing and viewing subnetworks.", - "commands": { - "list": SubnetListCommand, - "metagraph": MetagraphCommand, - "lock_cost": SubnetLockCostCommand, - "create": RegisterSubnetworkCommand, - "pow_register": PowRegisterCommand, - "register": RegisterCommand, - "hyperparameters": SubnetHyperparamsCommand, - }, - }, - "root": { - "name": "root", - "aliases": ["r", "roots"], - "help": "Commands for managing and viewing the root network.", - "commands": { - "list": RootList, - "weights": RootSetWeightsCommand, - "get_weights": RootGetWeightsCommand, - "boost": RootSetBoostCommand, - "slash": RootSetSlashCommand, - "senate_vote": VoteCommand, - "senate": SenateCommand, - "register": RootRegisterCommand, - "proposals": ProposalsCommand, - "set_take": SetTakeCommand, - "delegate": DelegateStakeCommand, - "undelegate": DelegateUnstakeCommand, - "my_delegates": MyDelegatesCommand, - "list_delegates": ListDelegatesCommand, - "nominate": NominateCommand, - }, - }, - "wallet": { - "name": "wallet", - "aliases": ["w", "wallets"], - "help": "Commands for managing and viewing wallets.", - "commands": { - "list": ListCommand, - "overview": OverviewCommand, - "transfer": TransferCommand, - "inspect": InspectCommand, - "balance": WalletBalanceCommand, - "create": WalletCreateCommand, - "new_hotkey": NewHotkeyCommand, - "new_coldkey": NewColdkeyCommand, - "regen_coldkey": RegenColdkeyCommand, - "regen_coldkeypub": RegenColdkeypubCommand, - "regen_hotkey": RegenHotkeyCommand, - "faucet": RunFaucetCommand, - "update": UpdateWalletCommand, - "swap_hotkey": SwapHotkeyCommand, - "set_identity": SetIdentityCommand, - "get_identity": GetIdentityCommand, - "history": GetWalletHistoryCommand, - "check_coldkey_swap": CheckColdKeySwapCommand, - }, - }, - "stake": { - "name": "stake", - "aliases": ["st", "stakes"], - "help": "Commands for staking and removing stake from hotkey accounts.", - "commands": { - "show": StakeShow, - "add": StakeCommand, - "remove": UnStakeCommand, - }, - }, - "weights": { - "name": "weights", - "aliases": ["wt", "weight"], - "help": "Commands for managing weight for subnets.", - "commands": { - "commit": CommitWeightCommand, - "reveal": RevealWeightCommand, - }, - }, - "sudo": { - "name": "sudo", - "aliases": ["su", "sudos"], - "help": "Commands for subnet management", - "commands": { - # "dissolve": None, - "set": SubnetSudoCommand, - "get": SubnetGetHyperparamsCommand, - }, - }, - "legacy": { - "name": "legacy", - "aliases": ["l"], - "help": "Miscellaneous commands.", - "commands": { - "update": UpdateCommand, - "faucet": RunFaucetCommand, - }, - }, - "info": { - "name": "info", - "aliases": ["i"], - "help": "Instructions for enabling autocompletion for the CLI.", - "commands": { - "autocomplete": AutocompleteCommand, - }, - }, -} - - -class CLIErrorParser(argparse.ArgumentParser): - """ - Custom ArgumentParser for better error messages. - """ - - def error(self, message): - """ - This method is called when an error occurs. It prints a custom error message. - """ - sys.stderr.write(f"Error: {message}\n") - self.print_help() - sys.exit(2) - - -class cli: - """ - Implementation of the Command Line Interface (CLI) class for the Bittensor protocol. - This class handles operations like key management (hotkey and coldkey) and token transfer. - """ - - def __init__( - self, - config: Optional["Config"] = None, - args: Optional[List[str]] = None, - ): - """ - Initializes a bittensor.CLI object. - - Args: - config (Config, optional): The configuration settings for the CLI. - args (List[str], optional): List of command line arguments. - """ - # Turns on console for cli. - turn_console_on() - - # If no config is provided, create a new one from args. - if config is None: - config = cli.create_config(args) - - self.config = config - if self.config.command in ALIAS_TO_COMMAND: - self.config.command = ALIAS_TO_COMMAND[self.config.command] - else: - bt_console.print( - f":cross_mark:[red]Unknown command: {self.config.command}[/red]" - ) - sys.exit() - - # Check if the config is valid. - cli.check_config(self.config) - - # If no_version_checking is not set or set as False in the config, version checking is done. - if not self.config.get("no_version_checking", d=True): - try: - check_version() - except VersionCheckError: - # If version checking fails, inform user with an exception. - raise RuntimeError( - "To avoid internet-based version checking, pass --no_version_checking while running the CLI." - ) - - @staticmethod - def __create_parser__() -> "argparse.ArgumentParser": - """ - Creates the argument parser for the Bittensor CLI. - - Returns: - argparse.ArgumentParser: An argument parser object for Bittensor CLI. - """ - # Define the basic argument parser. - parser = CLIErrorParser( - description=f"bittensor cli v{__version__}", - usage="btcli ", - add_help=True, - ) - # Add shtab completion - parser.add_argument( - "--print-completion", - choices=shtab.SUPPORTED_SHELLS, - help="Print shell tab completion script", - ) - # Add arguments for each sub-command. - cmd_parsers = parser.add_subparsers(dest="command") - # Add argument parsers for all available commands. - for command in COMMANDS.values(): - if isinstance(command, dict): - subcmd_parser = cmd_parsers.add_parser( - name=command["name"], - aliases=command["aliases"], - help=command["help"], - ) - subparser = subcmd_parser.add_subparsers( - help=command["help"], dest="subcommand", required=True - ) - - for subcommand in command["commands"].values(): - subcommand.add_args(subparser) - else: - command.add_args(cmd_parsers) - - return parser - - @staticmethod - def create_config(args: List[str]) -> "Config": - """ - From the argument parser, add config to bittensor.executor and local config - - Args: - args (List[str]): List of command line arguments. - - Returns: - Config: The configuration object for Bittensor CLI. - """ - parser = cli.__create_parser__() - - # If no arguments are passed, print help text and exit the program. - if len(args) == 0: - parser.print_help() - sys.exit() - - return Config(parser, args=args) - - @staticmethod - def check_config(config: "Config"): - """ - Checks if the essential configuration exists under different command - - Args: - config (Config): The configuration settings for the CLI. - """ - # Check if command exists, if so, run the corresponding check_config. - # If command doesn't exist, inform user and exit the program. - if config.command in COMMANDS: - command = config.command - command_data = COMMANDS[command] - - if isinstance(command_data, dict): - if config["subcommand"] is not None: - command_data["commands"][config["subcommand"]].check_config(config) - else: - bt_console.print( - f":cross_mark:[red]Missing subcommand for: {config.command}[/red]" - ) - sys.exit(1) - else: - command_data.check_config(config) - else: - bt_console.print(f":cross_mark:[red]Unknown command: {config.command}[/red]") - sys.exit(1) - - def run(self): - """ - Executes the command from the configuration. - """ - # Check for print-completion argument - if self.config.print_completion: - parser = cli.__create_parser__() - shell = self.config.print_completion - print(shtab.complete(parser, shell)) - return - - # Check if command exists, if so, run the corresponding method. - # If command doesn't exist, inform user and exit the program. - command = self.config.command - if command in COMMANDS: - command_data = COMMANDS[command] - - if isinstance(command_data, dict): - command_data["commands"][self.config["subcommand"]].run(self) - else: - command_data.run(self) - else: - bt_console.print( - f":cross_mark:[red]Unknown command: {self.config.command}[/red]" - ) - sys.exit() diff --git a/bittensor/btcli/commands/__init__.py b/bittensor/btcli/commands/__init__.py deleted file mode 100644 index 514a081c4..000000000 --- a/bittensor/btcli/commands/__init__.py +++ /dev/null @@ -1,124 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2023 Opentensor Technologies Inc - -# 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. - -from munch import Munch, munchify - -defaults: Munch = munchify( - { - "netuid": 1, - "subtensor": {"network": "finney", "chain_endpoint": None, "_mock": False}, - "pow_register": { - "num_processes": None, - "update_interval": 50000, - "output_in_place": True, - "verbose": False, - "cuda": {"dev_id": [0], "use_cuda": False, "tpb": 256}, - }, - "axon": { - "port": 8091, - "ip": "[::]", - "external_port": None, - "external_ip": None, - "max_workers": 10, - "maximum_concurrent_rpcs": 400, - }, - "priority": {"max_workers": 5, "maxsize": 10}, - "prometheus": {"port": 7091, "level": "INFO"}, - "wallet": { - "name": "default", - "hotkey": "default", - "path": "~/.bittensor/wallets/", - }, - "dataset": { - "batch_size": 10, - "block_size": 20, - "num_workers": 0, - "dataset_names": "default", - "data_dir": "~/.bittensor/data/", - "save_dataset": False, - "max_datasets": 3, - "num_batches": 100, - }, - "logging": { - "debug": False, - "trace": False, - "record_log": False, - "logging_dir": "~/.bittensor/miners", - }, - } -) - -from .stake import StakeCommand, StakeShow -from .unstake import UnStakeCommand -from .overview import OverviewCommand -from .register import ( - PowRegisterCommand, - RegisterCommand, - RunFaucetCommand, - SwapHotkeyCommand, -) -from .delegates import ( - NominateCommand, - ListDelegatesCommand, - DelegateStakeCommand, - DelegateUnstakeCommand, - MyDelegatesCommand, - SetTakeCommand, -) -from .wallets import ( - NewColdkeyCommand, - NewHotkeyCommand, - RegenColdkeyCommand, - RegenColdkeypubCommand, - RegenHotkeyCommand, - UpdateWalletCommand, - WalletCreateCommand, - WalletBalanceCommand, - GetWalletHistoryCommand, -) -from .weights import CommitWeightCommand, RevealWeightCommand -from .transfer import TransferCommand -from .inspect import InspectCommand -from .metagraph import MetagraphCommand -from .list import ListCommand -from .misc import UpdateCommand, AutocompleteCommand -from .senate import ( - SenateCommand, - ProposalsCommand, - ShowVotesCommand, - SenateRegisterCommand, - SenateLeaveCommand, - VoteCommand, -) -from .network import ( - RegisterSubnetworkCommand, - SubnetLockCostCommand, - SubnetListCommand, - SubnetSudoCommand, - SubnetHyperparamsCommand, - SubnetGetHyperparamsCommand, -) -from .root import ( - RootRegisterCommand, - RootList, - RootSetWeightsCommand, - RootGetWeightsCommand, - RootSetBoostCommand, - RootSetSlashCommand, -) -from .identity import GetIdentityCommand, SetIdentityCommand -from .check_coldkey_swap import CheckColdKeySwapCommand diff --git a/bittensor/btcli/commands/check_coldkey_swap.py b/bittensor/btcli/commands/check_coldkey_swap.py deleted file mode 100644 index 9864d0959..000000000 --- a/bittensor/btcli/commands/check_coldkey_swap.py +++ /dev/null @@ -1,130 +0,0 @@ -# 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 - -from bittensor_wallet import Wallet -from rich.prompt import Prompt - -from bittensor.core.config import Config -from bittensor.core.settings import bt_console -from bittensor.core.subtensor import Subtensor -from bittensor.utils.btlogging import logging -from bittensor.utils.formatting import convert_blocks_to_time -from . import defaults - - -def fetch_arbitration_stats(subtensor, wallet): - """ - Performs a check of the current arbitration data (if any), and displays it through the bittensor console. - """ - arbitration_check = len( - subtensor.check_in_arbitration(wallet.coldkeypub.ss58_address) - ) - if arbitration_check == 0: - bt_console.print( - "[green]There has been no previous key swap initiated for your coldkey.[/green]" - ) - if arbitration_check == 1: - arbitration_remaining = subtensor.get_remaining_arbitration_period( - wallet.coldkeypub.ss58_address - ) - hours, minutes, seconds = convert_blocks_to_time(arbitration_remaining) - bt_console.print( - "[yellow]There has been 1 swap request made for this coldkey already." - " By adding another swap request, the key will enter arbitration." - f" Your key swap is scheduled for {hours} hours, {minutes} minutes, {seconds} seconds" - " from now.[/yellow]" - ) - if arbitration_check > 1: - bt_console.print( - f"[red]This coldkey is currently in arbitration with a total swaps of {arbitration_check}.[/red]" - ) - - -class CheckColdKeySwapCommand: - """ - Executes the ``check_coldkey_swap`` command to check swap status of a coldkey in the Bittensor network. - Usage: - Users need to specify the wallet they want to check the swap status of. - Example usage:: - btcli wallet check_coldkey_swap - Note: - This command is important for users who wish check if swap requests were made against their coldkey. - """ - - @staticmethod - def run(cli): - """ - Runs the check coldkey swap command. - Args: - cli (bittensor.cli): The CLI object containing configuration and command-line interface utilities. - """ - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - CheckColdKeySwapCommand._run(cli, subtensor) - except Exception as e: - logging.warning(f"Failed to get swap status: {e}") - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - """ - Internal method to check coldkey swap status. - Args: - cli (bittensor.cli): The CLI object containing configuration and command-line interface utilities. - subtensor (bittensor.subtensor): The subtensor object for blockchain interactions. - """ - config = cli.config.copy() - wallet = Wallet(config=config) - - fetch_arbitration_stats(subtensor, wallet) - - @classmethod - def check_config(cls, config: "Config"): - """ - Checks and prompts for necessary configuration settings. - Args: - config (bittensor.config): The configuration object. - Prompts the user for wallet name if not set in the config. - """ - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name: str = Prompt.ask( - "Enter wallet name", default=defaults.wallet.name - ) - config.wallet.name = str(wallet_name) - - @staticmethod - def add_args(command_parser: argparse.ArgumentParser): - """ - Adds arguments to the command parser. - Args: - command_parser (argparse.ArgumentParser): The command parser to add arguments to. - """ - swap_parser = command_parser.add_parser( - "check_coldkey_swap", - help="""Check the status of swap requests for a coldkey on the Bittensor network. - Adding more than one swap request will make the key go into arbitration mode.""", - ) - Wallet.add_args(swap_parser) - Subtensor.add_args(swap_parser) diff --git a/bittensor/btcli/commands/delegates.py b/bittensor/btcli/commands/delegates.py deleted file mode 100644 index b640e8f8c..000000000 --- a/bittensor/btcli/commands/delegates.py +++ /dev/null @@ -1,1153 +0,0 @@ -# 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 os -import sys -from typing import List, Dict, Optional - -from bittensor_wallet import Wallet -from rich.console import Text, Console -from rich.prompt import Prompt, FloatPrompt, Confirm -from rich.table import Table -from substrateinterface.exceptions import SubstrateRequestException -from tqdm import tqdm - -from bittensor.core.chain_data import DelegateInfoLite, DelegateInfo -from bittensor.core.config import Config -from bittensor.core.settings import bt_console, delegates_details_url -from bittensor.core.subtensor import Subtensor -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging -from . import defaults -from .identity import SetIdentityCommand -from .utils import get_delegates_details, DelegatesDetails - - -def _get_coldkey_wallets_for_path(path: str) -> List["Wallet"]: - try: - wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [Wallet(path=path, name=name) for name in wallet_names] - except StopIteration: - # No wallet files found. - wallets = [] - return wallets - - -console = Console() - - -def show_delegates_lite( - delegates_lite: List["DelegateInfoLite"], width: Optional[int] = None -): - """ - This method is a lite version of the :func:`show_delegates`. This method displays a formatted table of Bittensor network delegates with detailed statistics to the console. - - The table is sorted by total stake in descending order and provides - a snapshot of delegate performance and status, helping users make informed decisions for staking or nominating. - - This helper function is not intended to be used directly in user code unless specifically required. - - Args: - delegates_lite (List[bittensor.core.chain_data.DelegateInfoLite]): A list of delegate information objects to be displayed. - width (Optional[int]): The width of the console output table. Defaults to ``None``, which will make the table expand to the maximum width of the console. - - The output table contains the following columns. To display more columns, use the :func:`show_delegates` function. - - - INDEX: The numerical index of the delegate. - - DELEGATE: The name of the delegate. - - SS58: The truncated SS58 address of the delegate. - - NOMINATORS: The number of nominators supporting the delegate. - - VPERMIT: Validator permits held by the delegate for the subnets. - - TAKE: The percentage of the delegate's earnings taken by the network. - - DELEGATE/(24h): The earnings of the delegate in the last 24 hours. - - Desc: A brief description provided by the delegate. - - Usage: - This function is typically used within the Bittensor CLI to show current delegate options to users who are considering where to stake their tokens. - - Example usage:: - - show_delegates_lite(delegates_lite, width=80) - - Note: - This function is primarily for display purposes within a command-line interface and does not return any values. It relies on the `rich `_ Python library to render - the table in the console. - """ - - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=delegates_details_url) - ) - if registered_delegate_info is None: - bt_console.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - registered_delegate_info = {} - - table = Table(show_footer=True, width=width, pad_edge=False, box=None, expand=True) - table.add_column( - "[overline white]INDEX", - str(len(delegates_lite)), - footer_style="overline white", - style="bold white", - ) - table.add_column( - "[overline white]DELEGATE", - style="rgb(50,163,219)", - no_wrap=True, - justify="left", - ) - table.add_column( - "[overline white]SS58", - str(len(delegates_lite)), - footer_style="overline white", - style="bold yellow", - ) - table.add_column( - "[overline white]NOMINATORS", justify="center", style="green", no_wrap=True - ) - table.add_column("[overline white]VPERMIT", justify="right", no_wrap=False) - table.add_column("[overline white]TAKE", style="white", no_wrap=True) - table.add_column("[overline white]DELEGATE/(24h)", style="green", justify="center") - table.add_column("[overline white]Desc", style="rgb(50,163,219)") - - for i, d in enumerate(delegates_lite): - if d.delegate_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[d.delegate_ss58].name - delegate_url = registered_delegate_info[d.delegate_ss58].url - delegate_description = registered_delegate_info[d.delegate_ss58].description - else: - delegate_name = "" - delegate_url = "" - delegate_description = "" - - table.add_row( - # `INDEX` column - str(i), - # `DELEGATE` column - Text(delegate_name, style=f"link {delegate_url}"), - # `SS58` column - f"{d.delegate_ss58:8.8}...", - # `NOMINATORS` column - str(d.nominators), - # `VPERMIT` column - str(d.registrations), - # `TAKE` column - f"{d.take * 100:.1f}%", - # `DELEGATE/(24h)` column - f"τ{Balance.from_tao(d.total_daily_return * 0.18) !s:6.6}", - # `Desc` column - str(delegate_description), - end_section=True, - ) - bt_console.print(table) - - -# Uses rich console to pretty print a table of delegates. -def show_delegates( - delegates: List["DelegateInfo"], - prev_delegates: Optional[List["DelegateInfo"]], - width: Optional[int] = None, -): - """ - Displays a formatted table of Bittensor network delegates with detailed statistics to the console. - - The table is sorted by total stake in descending order and provides - a snapshot of delegate performance and status, helping users make informed decisions for staking or nominating. - - This is a helper function that is called by the :func:`list_delegates` and :func:`my_delegates`, and is not intended - to be used directly in user code unless specifically required. - - Args: - delegates (List[bittensor.DelegateInfo]): A list of delegate information objects to be displayed. - prev_delegates (Optional[List[bittensor.DelegateInfo]]): A list of delegate information objects from a previous state, used to calculate changes in stake. Defaults to ``None``. - width (Optional[int]): The width of the console output table. Defaults to ``None``, which will make the table expand to the maximum width of the console. - - The output table contains the following columns: - - - INDEX: The numerical index of the delegate. - - DELEGATE: The name of the delegate. - - SS58: The truncated SS58 address of the delegate. - - NOMINATORS: The number of nominators supporting the delegate. - - DELEGATE STAKE(τ): The stake that is directly delegated to the delegate. - - TOTAL STAKE(τ): The total stake held by the delegate, including nominators' stake. - - CHANGE/(4h): The percentage change in the delegate's stake over the past 4 hours. - - VPERMIT: Validator permits held by the delegate for the subnets. - - TAKE: The percentage of the delegate's earnings taken by the network. - - NOMINATOR/(24h)/kτ: The earnings per 1000 τ staked by nominators in the last 24 hours. - - DELEGATE/(24h): The earnings of the delegate in the last 24 hours. - - Desc: A brief description provided by the delegate. - - Usage: - This function is typically used within the Bittensor CLI to show current delegate options to users who are considering where to stake their tokens. - - Example usage:: - - show_delegates(current_delegates, previous_delegates, width=80) - - Note: - This function is primarily for display purposes within a command-line interface and does - not return any values. It relies on the `rich `_ Python library to render - the table in the - console. - """ - - delegates.sort(key=lambda delegate: delegate.total_stake, reverse=True) - prev_delegates_dict = {} - if prev_delegates is not None: - for prev_delegate in prev_delegates: - prev_delegates_dict[prev_delegate.hotkey_ss58] = prev_delegate - - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=delegates_details_url) - ) - if registered_delegate_info is None: - bt_console.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - registered_delegate_info = {} - - table = Table(show_footer=True, width=width, pad_edge=False, box=None, expand=True) - table.add_column( - "[overline white]INDEX", - str(len(delegates)), - footer_style="overline white", - style="bold white", - ) - table.add_column( - "[overline white]DELEGATE", - style="rgb(50,163,219)", - no_wrap=True, - justify="left", - ) - table.add_column( - "[overline white]SS58", - str(len(delegates)), - footer_style="overline white", - style="bold yellow", - ) - table.add_column( - "[overline white]NOMINATORS", justify="center", style="green", no_wrap=True - ) - table.add_column( - "[overline white]DELEGATE STAKE(\u03c4)", justify="right", no_wrap=True - ) - table.add_column( - "[overline white]TOTAL STAKE(\u03c4)", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column("[overline white]CHANGE/(4h)", style="grey0", justify="center") - table.add_column("[overline white]VPERMIT", justify="right", no_wrap=False) - table.add_column("[overline white]TAKE", style="white", no_wrap=True) - table.add_column( - "[overline white]NOMINATOR/(24h)/k\u03c4", style="green", justify="center" - ) - table.add_column("[overline white]DELEGATE/(24h)", style="green", justify="center") - table.add_column("[overline white]Desc", style="rgb(50,163,219)") - - for i, delegate in enumerate(delegates): - owner_stake = next( - map( - lambda x: x[1], # get stake - filter( - lambda x: x[0] == delegate.owner_ss58, delegate.nominators - ), # filter for owner - ), - Balance.from_rao(0), # default to 0 if no owner stake. - ) - if delegate.hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[delegate.hotkey_ss58].name - delegate_url = registered_delegate_info[delegate.hotkey_ss58].url - delegate_description = registered_delegate_info[ - delegate.hotkey_ss58 - ].description - else: - delegate_name = "" - delegate_url = "" - delegate_description = "" - - if delegate.hotkey_ss58 in prev_delegates_dict: - prev_stake = prev_delegates_dict[delegate.hotkey_ss58].total_stake - if prev_stake == 0: - rate_change_in_stake_str = "[green]100%[/green]" - else: - rate_change_in_stake = ( - 100 - * (float(delegate.total_stake) - float(prev_stake)) - / float(prev_stake) - ) - if rate_change_in_stake > 0: - rate_change_in_stake_str = "[green]{:.2f}%[/green]".format( - rate_change_in_stake - ) - elif rate_change_in_stake < 0: - rate_change_in_stake_str = "[red]{:.2f}%[/red]".format( - rate_change_in_stake - ) - else: - rate_change_in_stake_str = "[grey0]0%[/grey0]" - else: - rate_change_in_stake_str = "[grey0]NA[/grey0]" - - table.add_row( - # INDEX - str(i), - # DELEGATE - Text(delegate_name, style=f"link {delegate_url}"), - # SS58 - f"{delegate.hotkey_ss58:8.8}...", - # NOMINATORS - str(len([nom for nom in delegate.nominators if nom[1].rao > 0])), - # DELEGATE STAKE - f"{owner_stake!s:13.13}", - # TOTAL STAKE - f"{delegate.total_stake!s:13.13}", - # CHANGE/(4h) - rate_change_in_stake_str, - # VPERMIT - str(delegate.registrations), - # TAKE - f"{delegate.take * 100:.1f}%", - # NOMINATOR/(24h)/k - f"{Balance.from_tao(delegate.total_daily_return.tao * (1000 / (0.001 + delegate.total_stake.tao)))!s:6.6}", - # DELEGATE/(24h) - f"{Balance.from_tao(delegate.total_daily_return.tao * 0.18) !s:6.6}", - # Desc - str(delegate_description), - end_section=True, - ) - bt_console.print(table) - - -class DelegateStakeCommand: - """ - Executes the ``delegate`` command, which stakes Tao to a specified delegate on the Bittensor network. - - This action allocates the user's Tao to support a delegate, potentially earning staking rewards in return. - - Optional Arguments: - - ``wallet.name``: The name of the wallet to use for the command. - - ``delegate_ss58key``: The ``SS58`` address of the delegate to stake to. - - ``amount``: The amount of Tao to stake. - - ``all``: If specified, the command stakes all available Tao. - - The command interacts with the user to determine the delegate and the amount of Tao to be staked. If the ``--all`` - flag is used, it delegates the entire available balance. - - Usage: - The user must specify the delegate's SS58 address and the amount of Tao to stake. The function sends a - transaction to the subtensor network to delegate the specified amount to the chosen delegate. These values are - prompted if not provided. - - Example usage:: - - btcli delegate --delegate_ss58key --amount - btcli delegate --delegate_ss58key --all - - Note: - This command modifies the blockchain state and may incur transaction fees. It requires user confirmation and - interaction, and is designed to be used within the Bittensor CLI environment. The user should ensure the - delegate's address and the amount to be staked are correct before executing the command. - """ - - @staticmethod - def run(cli): - """Delegates stake to a chain delegate.""" - try: - config = cli.config.copy() - wallet = Wallet(config=config) - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - subtensor.delegate( - wallet=wallet, - delegate_ss58=config.get("delegate_ss58key"), - amount=config.get("amount"), - wait_for_inclusion=True, - prompt=not config.no_prompt, - ) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - delegate_stake_parser = parser.add_parser( - "delegate", help="""Delegate Stake to an account.""" - ) - delegate_stake_parser.add_argument( - "--delegate_ss58key", - "--delegate_ss58", - dest="delegate_ss58key", - type=str, - required=False, - help="""The ss58 address of the choosen delegate""", - ) - delegate_stake_parser.add_argument( - "--all", dest="stake_all", action="store_true" - ) - delegate_stake_parser.add_argument( - "--amount", dest="amount", type=float, required=False - ) - Wallet.add_args(delegate_stake_parser) - Subtensor.add_args(delegate_stake_parser) - - @staticmethod - def check_config(config: "Config"): - if not config.get("delegate_ss58key"): - # Check for delegates. - with bt_console.status(":satellite: Loading delegates..."): - subtensor = Subtensor(config=config, log_verbose=False) - delegates: List["DelegateInfo"] = subtensor.get_delegates() - try: - prev_delegates = subtensor.get_delegates( - max(0, subtensor.block - 1200) - ) - except SubstrateRequestException: - prev_delegates = None - - if prev_delegates is None: - bt_console.print( - ":warning: [yellow]Could not fetch delegates history[/yellow]" - ) - - if len(delegates) == 0: - console.print( - ":cross_mark: [red]There are no delegates on {}[/red]".format( - subtensor.network - ) - ) - sys.exit(1) - - delegates.sort(key=lambda delegate: delegate.total_stake, reverse=True) - show_delegates(delegates, prev_delegates=prev_delegates) - delegate_index = Prompt.ask("Enter delegate index") - config.delegate_ss58key = str(delegates[int(delegate_index)].hotkey_ss58) - console.print( - "Selected: [yellow]{}[/yellow]".format(config.delegate_ss58key) - ) - - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - # Get amount. - if not config.get("amount") and not config.get("stake_all"): - if not Confirm.ask( - "Stake all Tao from account: [bold]'{}'[/bold]?".format( - config.wallet.get("name", defaults.wallet.name) - ) - ): - amount = Prompt.ask("Enter Tao amount to stake") - try: - config.amount = float(amount) - except ValueError: - console.print( - ":cross_mark: [red]Invalid Tao amount[/red] [bold white]{}[/bold white]".format( - amount - ) - ) - sys.exit() - else: - config.stake_all = True - - -class DelegateUnstakeCommand: - """ - Executes the ``undelegate`` command, allowing users to withdraw their staked Tao from a delegate on the Bittensor - network. - - This process is known as "undelegating" and it reverses the delegation process, freeing up the staked tokens. - - Optional Arguments: - - ``wallet.name``: The name of the wallet to use for the command. - - ``delegate_ss58key``: The ``SS58`` address of the delegate to undelegate from. - - ``amount``: The amount of Tao to undelegate. - - ``all``: If specified, the command undelegates all staked Tao from the delegate. - - The command prompts the user for the amount of Tao to undelegate and the ``SS58`` address of the delegate from which - to undelegate. If the ``--all`` flag is used, it will attempt to undelegate the entire staked amount from the - specified delegate. - - Usage: - The user must provide the delegate's SS58 address and the amount of Tao to undelegate. The function will then - send a transaction to the Bittensor network to process the undelegation. - - Example usage:: - - btcli undelegate --delegate_ss58key --amount - btcli undelegate --delegate_ss58key --all - - Note: - This command can result in a change to the blockchain state and may incur transaction fees. It is interactive - and requires confirmation from the user before proceeding. It should be used with care as undelegating can - affect the delegate's total stake and - potentially the user's staking rewards. - """ - - @staticmethod - def run(cli): - """Undelegates stake from a chain delegate.""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - DelegateUnstakeCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - def _run(self, subtensor: "Subtensor"): - """Undelegates stake from a chain delegate.""" - config = self.config.copy() - wallet = Wallet(config=config) - subtensor.undelegate( - wallet=wallet, - delegate_ss58=config.get("delegate_ss58key"), - amount=config.get("amount"), - wait_for_inclusion=True, - prompt=not config.no_prompt, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - undelegate_stake_parser = parser.add_parser( - "undelegate", help="""Undelegate Stake from an account.""" - ) - undelegate_stake_parser.add_argument( - "--delegate_ss58key", - "--delegate_ss58", - dest="delegate_ss58key", - type=str, - required=False, - help="""The ss58 address of the choosen delegate""", - ) - undelegate_stake_parser.add_argument( - "--all", dest="unstake_all", action="store_true" - ) - undelegate_stake_parser.add_argument( - "--amount", dest="amount", type=float, required=False - ) - Wallet.add_args(undelegate_stake_parser) - Subtensor.add_args(undelegate_stake_parser) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.get("delegate_ss58key"): - # Check for delegates. - with bt_console.status(":satellite: Loading delegates..."): - subtensor = Subtensor(config=config, log_verbose=False) - delegates: List["DelegateInfo"] = subtensor.get_delegates() - try: - prev_delegates = subtensor.get_delegates( - max(0, subtensor.block - 1200) - ) - except SubstrateRequestException: - prev_delegates = None - - if prev_delegates is None: - bt_console.print( - ":warning: [yellow]Could not fetch delegates history[/yellow]" - ) - - if len(delegates) == 0: - console.print( - ":cross_mark: [red]There are no delegates on {}[/red]".format( - subtensor.network - ) - ) - sys.exit(1) - - delegates.sort(key=lambda delegate: delegate.total_stake, reverse=True) - show_delegates(delegates, prev_delegates=prev_delegates) - delegate_index = Prompt.ask("Enter delegate index") - config.delegate_ss58key = str(delegates[int(delegate_index)].hotkey_ss58) - console.print( - "Selected: [yellow]{}[/yellow]".format(config.delegate_ss58key) - ) - - # Get amount. - if not config.get("amount") and not config.get("unstake_all"): - if not Confirm.ask( - "Unstake all Tao to account: [bold]'{}'[/bold]?".format( - config.wallet.get("name", defaults.wallet.name) - ) - ): - amount = Prompt.ask("Enter Tao amount to unstake") - try: - config.amount = float(amount) - except ValueError: - console.print( - ":cross_mark: [red]Invalid Tao amount[/red] [bold white]{}[/bold white]".format( - amount - ) - ) - sys.exit() - else: - config.unstake_all = True - - -class ListDelegatesCommand: - """ - Displays a formatted table of Bittensor network delegates, providing a comprehensive overview of delegate statistics and information. - - This table helps users make informed decisions on which delegates to allocate their TAO stake. - - Optional Arguments: - - ``wallet.name``: The name of the wallet to use for the command. - - ``subtensor.network``: The name of the network to use for the command. - - The table columns include: - - - INDEX: The delegate's index in the sorted list. - - DELEGATE: The name of the delegate. - - SS58: The delegate's unique SS58 address (truncated for display). - - NOMINATORS: The count of nominators backing the delegate. - - DELEGATE STAKE(τ): The amount of delegate's own stake (not the TAO delegated from any nominators). - - TOTAL STAKE(τ): The delegate's cumulative stake, including self-staked and nominators' stakes. - - CHANGE/(4h): The percentage change in the delegate's stake over the last four hours. - - SUBNETS: The subnets to which the delegate is registered. - - VPERMIT: Indicates the subnets for which the delegate has validator permits. - - NOMINATOR/(24h)/kτ: The earnings per 1000 τ staked by nominators in the last 24 hours. - - DELEGATE/(24h): The total earnings of the delegate in the last 24 hours. - - DESCRIPTION: A brief description of the delegate's purpose and operations. - - Sorting is done based on the ``TOTAL STAKE`` column in descending order. Changes in stake are highlighted: - increases in green and decreases in red. Entries with no previous data are marked with ``NA``. Each delegate's name - is a hyperlink to their respective URL, if available. - - Example usage:: - - btcli root list_delegates - btcli root list_delegates --wallet.name my_wallet - btcli root list_delegates --subtensor.network finney # can also be `test` or `local` - - Note: - This function is part of the Bittensor CLI tools and is intended for use within a console application. It prints - directly to the console and does not return any value. - """ - - @staticmethod - def run(cli): - r""" - List all delegates on the network. - """ - try: - cli.config.subtensor.network = "archive" - cli.config.subtensor.chain_endpoint = ( - "wss://archive.chain.opentensor.ai:443" - ) - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - ListDelegatesCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r""" - List all delegates on the network. - """ - with bt_console.status(":satellite: Loading delegates..."): - delegates: list["DelegateInfo"] = subtensor.get_delegates() - - try: - prev_delegates = subtensor.get_delegates(max(0, subtensor.block - 1200)) - except SubstrateRequestException: - prev_delegates = None - - if prev_delegates is None: - bt_console.print( - ":warning: [yellow]Could not fetch delegates history[/yellow]" - ) - - show_delegates( - delegates, - prev_delegates=prev_delegates, - width=cli.config.get("width", None), - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - list_delegates_parser = parser.add_parser( - "list_delegates", help="""List all delegates on the network""" - ) - Subtensor.add_args(list_delegates_parser) - - @staticmethod - def check_config(config: "Config"): - pass - - -class NominateCommand: - """ - Executes the ``nominate`` command, which facilitates a wallet to become a delegate on the Bittensor network. - - This command handles the nomination process, including wallet unlocking and verification of the hotkey's current - delegate status. - - The command performs several checks: - - - Verifies that the hotkey is not already a delegate to prevent redundant nominations. - - Tries to nominate the wallet and reports success or failure. - - Upon success, the wallet's hotkey is registered as a delegate on the network. - - Optional Arguments: - - ``wallet.name``: The name of the wallet to use for the command. - - ``wallet.hotkey``: The name of the hotkey to use for the command. - - Usage: - To run the command, the user must have a configured wallet with both hotkey and coldkey. If the wallet is not - already nominated, this command will initiate the process. - - Example usage:: - - btcli root nominate - btcli root nominate --wallet.name my_wallet --wallet.hotkey my_hotkey - - Note: - This function is intended to be used as a CLI command. It prints the outcome directly to the console and does - not return any value. It should not be called programmatically in user code due to its interactive nature and - side effects on the network state. - """ - - @staticmethod - def run(cli): - r"""Nominate wallet.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - NominateCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Nominate wallet.""" - wallet = Wallet(config=cli.config) - - # Unlock the wallet. - wallet.hotkey - wallet.coldkey - - # Check if the hotkey is already a delegate. - if subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - bt_console.print( - "Aborting: Hotkey {} is already a delegate.".format( - wallet.hotkey.ss58_address - ) - ) - return - - result: bool = subtensor.nominate(wallet) - if not result: - bt_console.print( - "Could not became a delegate on [white]{}[/white]".format( - subtensor.network - ) - ) - else: - # Check if we are a delegate. - is_delegate: bool = subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address) - if not is_delegate: - bt_console.print( - "Could not became a delegate on [white]{}[/white]".format( - subtensor.network - ) - ) - return - bt_console.print( - "Successfully became a delegate on [white]{}[/white]".format( - subtensor.network - ) - ) - - # Prompt use to set identity on chain. - if not cli.config.no_prompt: - do_set_identity = Prompt.ask( - f"Subnetwork registered successfully. Would you like to set your identity? [y/n]", - choices=["y", "n"], - ) - - if do_set_identity.lower() == "y": - subtensor.close() - config = cli.config.copy() - SetIdentityCommand.check_config(config) - cli.config = config - SetIdentityCommand.run(cli) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - nominate_parser = parser.add_parser( - "nominate", help="""Become a delegate on the network""" - ) - Wallet.add_args(nominate_parser) - Subtensor.add_args(nominate_parser) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - -class MyDelegatesCommand: - """ - Executes the ``my_delegates`` command within the Bittensor CLI, which retrieves and displays a table of delegated - stakes from a user's wallet(s) to various delegates on the Bittensor network. - - The command provides detailed insights into the user's - staking activities and the performance of their chosen delegates. - - Optional Arguments: - - ``wallet.name``: The name of the wallet to use for the command. - - ``all``: If specified, the command aggregates information across all wallets. - - The table output includes the following columns: - - - Wallet: The name of the user's wallet. - - OWNER: The name of the delegate's owner. - - SS58: The truncated SS58 address of the delegate. - - Delegation: The amount of Tao staked by the user to the delegate. - - τ/24h: The earnings from the delegate to the user over the past 24 hours. - - NOMS: The number of nominators for the delegate. - - OWNER STAKE(τ): The stake amount owned by the delegate. - - TOTAL STAKE(τ): The total stake amount held by the delegate. - - SUBNETS: The list of subnets the delegate is a part of. - - VPERMIT: Validator permits held by the delegate for various subnets. - - 24h/kτ: Earnings per 1000 Tao staked over the last 24 hours. - - Desc: A description of the delegate. - - The command also sums and prints the total amount of Tao delegated across all wallets. - - Usage: - The command can be run as part of the Bittensor CLI suite of tools and requires no parameters if a single wallet - is used. If multiple wallets are present, the ``--all`` flag can be specified to aggregate information across - all wallets. - - Example usage:: - - btcli my_delegates - btcli my_delegates --all - btcli my_delegates --wallet.name my_wallet - - Note: - This function is typically called by the CLI parser and is not intended to be used directly in user code. - """ - - @staticmethod - def run(cli): - """Delegates stake to a chain delegate.""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - MyDelegatesCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - """Delegates stake to a chain delegate.""" - config = cli.config.copy() - if config.get("all", d=None): - wallets = _get_coldkey_wallets_for_path(config.wallet.path) - else: - wallets = [Wallet(config=config)] - - table = Table(show_footer=True, pad_edge=False, box=None, expand=True) - table.add_column( - "[overline white]Wallet", footer_style="overline white", style="bold white" - ) - table.add_column( - "[overline white]OWNER", - style="rgb(50,163,219)", - no_wrap=True, - justify="left", - ) - table.add_column( - "[overline white]SS58", footer_style="overline white", style="bold yellow" - ) - table.add_column( - "[overline green]Delegation", - footer_style="overline green", - style="bold green", - ) - table.add_column( - "[overline green]\u03c4/24h", - footer_style="overline green", - style="bold green", - ) - table.add_column( - "[overline white]NOMS", justify="center", style="green", no_wrap=True - ) - table.add_column( - "[overline white]OWNER STAKE(\u03c4)", justify="right", no_wrap=True - ) - table.add_column( - "[overline white]TOTAL STAKE(\u03c4)", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]SUBNETS", justify="right", style="white", no_wrap=True - ) - table.add_column("[overline white]VPERMIT", justify="right", no_wrap=True) - table.add_column("[overline white]24h/k\u03c4", style="green", justify="center") - table.add_column("[overline white]Desc", style="rgb(50,163,219)") - total_delegated = 0 - - for wallet in tqdm(wallets): - if not wallet.coldkeypub_file.exists_on_device(): - continue - delegates = subtensor.get_delegated( - coldkey_ss58=wallet.coldkeypub.ss58_address - ) - - my_delegates = {} # hotkey, amount - for delegate in delegates: - for coldkey_addr, staked in delegate[0].nominators: - if ( - coldkey_addr == wallet.coldkeypub.ss58_address - and staked.tao > 0 - ): - my_delegates[delegate[0].hotkey_ss58] = staked - - delegates.sort(key=lambda delegate: delegate[0].total_stake, reverse=True) - total_delegated += sum(my_delegates.values()) - - registered_delegate_info: Optional[DelegatesDetails] = ( - get_delegates_details(url=delegates_details_url) - ) - if registered_delegate_info is None: - bt_console.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - registered_delegate_info = {} - - for i, delegate in enumerate(delegates): - owner_stake = next( - map( - lambda x: x[1], # get stake - filter( - lambda x: x[0] == delegate[0].owner_ss58, - delegate[0].nominators, - ), # filter for owner - ), - Balance.from_rao(0), # default to 0 if no owner stake. - ) - if delegate[0].hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[ - delegate[0].hotkey_ss58 - ].name - delegate_url = registered_delegate_info[delegate[0].hotkey_ss58].url - delegate_description = registered_delegate_info[ - delegate[0].hotkey_ss58 - ].description - else: - delegate_name = "" - delegate_url = "" - delegate_description = "" - - if delegate[0].hotkey_ss58 in my_delegates: - table.add_row( - wallet.name, - Text(delegate_name, style=f"link {delegate_url}"), - f"{delegate[0].hotkey_ss58:8.8}...", - f"{my_delegates[delegate[0].hotkey_ss58]!s:13.13}", - f"{delegate[0].total_daily_return.tao * (my_delegates[delegate[0].hotkey_ss58] / delegate[0].total_stake.tao)!s:6.6}", - str(len(delegate[0].nominators)), - f"{owner_stake!s:13.13}", - f"{delegate[0].total_stake!s:13.13}", - str(delegate[0].registrations), - str( - [ - "*" if subnet in delegate[0].validator_permits else "" - for subnet in delegate[0].registrations - ] - ), - # f'{delegate.take * 100:.1f}%',s - f"{delegate[0].total_daily_return.tao * (1000 / (0.001 + delegate[0].total_stake.tao))!s:6.6}", - str(delegate_description), - # f'{delegate_profile.description:140.140}', - ) - - bt_console.print(table) - bt_console.print("Total delegated Tao: {}".format(total_delegated)) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - delegate_stake_parser = parser.add_parser( - "my_delegates", - help="""Show all delegates where I am delegating a positive amount of stake""", - ) - delegate_stake_parser.add_argument( - "--all", - action="store_true", - help="""Check all coldkey wallets.""", - default=False, - ) - Wallet.add_args(delegate_stake_parser) - Subtensor.add_args(delegate_stake_parser) - - @staticmethod - def check_config(config: "Config"): - if ( - not config.get("all", d=None) - and not config.is_set("wallet.name") - and not config.no_prompt - ): - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - -class SetTakeCommand: - """ - Executes the ``set_take`` command, which sets the delegate take. - - The command performs several checks: - - 1. Hotkey is already a delegate - 2. New take value is within 0-18% range - - Optional Arguments: - - ``take``: The new take value - - ``wallet.name``: The name of the wallet to use for the command. - - ``wallet.hotkey``: The name of the hotkey to use for the command. - - Usage: - To run the command, the user must have a configured wallet with both hotkey and coldkey. Also, the hotkey should already be a delegate. - - Example usage:: - btcli root set_take --wallet.name my_wallet --wallet.hotkey my_hotkey - - Note: - This function can be used to update the takes individually for every subnet - """ - - @staticmethod - def run(cli): - r"""Set delegate take.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - SetTakeCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Set delegate take.""" - config = cli.config.copy() - wallet = Wallet(config=cli.config) - - # Unlock the wallet. - wallet.hotkey - wallet.coldkey - - # Check if the hotkey is not a delegate. - if not subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - bt_console.print( - "Aborting: Hotkey {} is NOT a delegate.".format( - wallet.hotkey.ss58_address - ) - ) - return - - # Prompt user for take value. - new_take_str = config.get("take") - if new_take_str is None: - new_take = FloatPrompt.ask(f"Enter take value (0.18 for 18%)") - else: - new_take = float(new_take_str) - - if new_take > 0.18: - bt_console.print("ERROR: Take value should not exceed 18%") - return - - result: bool = subtensor.set_take( - wallet=wallet, - delegate_ss58=wallet.hotkey.ss58_address, - take=new_take, - ) - if not result: - bt_console.print("Could not set the take") - else: - # Check if we are a delegate. - is_delegate: bool = subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address) - if not is_delegate: - bt_console.print( - "Could not set the take [white]{}[/white]".format(subtensor.network) - ) - return - bt_console.print( - "Successfully set the take on [white]{}[/white]".format( - subtensor.network - ) - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - set_take_parser = parser.add_parser( - "set_take", help="""Set take for delegate""" - ) - set_take_parser.add_argument( - "--take", - dest="take", - type=float, - required=False, - help="""Take as a float number""", - ) - Wallet.add_args(set_take_parser) - Subtensor.add_args(set_take_parser) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) diff --git a/bittensor/btcli/commands/identity.py b/bittensor/btcli/commands/identity.py deleted file mode 100644 index 962082c84..000000000 --- a/bittensor/btcli/commands/identity.py +++ /dev/null @@ -1,343 +0,0 @@ -import argparse -from rich.table import Table -from rich.prompt import Prompt -from sys import getsizeof -from ...core.settings import networks -from bittensor.core.settings import bt_console -from bittensor.core.subtensor import Subtensor -from bittensor.core.config import Config -from bittensor_wallet import Wallet -from bittensor.utils.btlogging import logging -from bittensor.btcli.commands import defaults - - -class SetIdentityCommand: - """ - Executes the :func:`set_identity` command within the Bittensor network, which allows for the creation or update of a delegate's on-chain identity. - - This identity includes various - attributes such as display name, legal name, web URL, PGP fingerprint, and contact - information, among others. - - Optional Arguments: - - ``display``: The display name for the identity. - - ``legal``: The legal name for the identity. - - ``web``: The web URL for the identity. - - ``riot``: The riot handle for the identity. - - ``email``: The email address for the identity. - - ``pgp_fingerprint``: The PGP fingerprint for the identity. - - ``image``: The image URL for the identity. - - ``info``: The info for the identity. - - ``twitter``: The X (twitter) URL for the identity. - - The command prompts the user for the different identity attributes and validates the - input size for each attribute. It provides an option to update an existing validator - hotkey identity. If the user consents to the transaction cost, the identity is updated - on the blockchain. - - Each field has a maximum size of 64 bytes. The PGP fingerprint field is an exception - and has a maximum size of 20 bytes. The user is prompted to enter the PGP fingerprint - as a hex string, which is then converted to bytes. The user is also prompted to enter - the coldkey or hotkey ``ss58`` address for the identity to be updated. If the user does - not have a hotkey, the coldkey address is used by default. - - If setting a validator identity, the hotkey will be used by default. If the user is - setting an identity for a subnet, the coldkey will be used by default. - - Usage: - The user should call this command from the command line and follow the interactive - prompts to enter or update the identity information. The command will display the - updated identity details in a table format upon successful execution. - - Example usage:: - - btcli wallet set_identity - - Note: - This command should only be used if the user is willing to incur the 1 TAO transaction - fee associated with setting an identity on the blockchain. It is a high-level command - that makes changes to the blockchain state and should not be used programmatically as - part of other scripts or applications. - """ - @staticmethod - def run(cli): - r"""Create a new or update existing identity on-chain.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - SetIdentityCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Create a new or update existing identity on-chain.""" - - wallet = Wallet(config=cli.config) - - id_dict = { - "display": cli.config.display, - "legal": cli.config.legal, - "web": cli.config.web, - "pgp_fingerprint": cli.config.pgp_fingerprint, - "riot": cli.config.riot, - "email": cli.config.email, - "image": cli.config.image, - "twitter": cli.config.twitter, - "info": cli.config.info, - } - - for field, string in id_dict.items(): - if getsizeof(string) > 113: # 64 + 49 overhead bytes for string - raise ValueError(f"Identity value `{field}` must be <= 64 raw bytes") - - identified = ( - wallet.hotkey.ss58_address - if str( - Prompt.ask( - "Are you updating a validator hotkey identity?", - default="y", - choices=["y", "n"], - ) - ).lower() - == "y" - else None - ) - - if ( - str( - Prompt.ask( - "Cost to register an Identity is [bold white italic]0.1 Tao[/bold white italic], are you sure you wish to continue?", - default="n", - choices=["y", "n"], - ) - ).lower() - == "n" - ): - bt_console.print(":cross_mark: Aborted!") - exit(0) - - wallet.coldkey # unlock coldkey - with bt_console.status(":satellite: [bold green]Updating identity on-chain..."): - try: - subtensor.update_identity( - identified=identified, - wallet=wallet, - params=id_dict, - ) - except Exception as e: - bt_console.print(f"[red]:cross_mark: Failed![/red] {e}") - exit(1) - - bt_console.print(":white_heavy_check_mark: Success!") - - identity = subtensor.query_identity(identified or wallet.coldkey.ss58_address) - - table = Table(title="[bold white italic]Updated On-Chain Identity") - table.add_column("Key", justify="right", style="cyan", no_wrap=True) - table.add_column("Value", style="magenta") - - table.add_row("Address", identified or wallet.coldkey.ss58_address) - for key, value in identity.items(): - table.add_row(key, str(value) if value is not None else "None") - - bt_console.print(table) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - config.wallet.name = Prompt.ask( - "Enter wallet name", default=defaults.wallet.name - ) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - config.wallet.hotkey = Prompt.ask( - "Enter wallet hotkey", default=defaults.wallet.hotkey - ) - if not config.is_set("subtensor.network") and not config.no_prompt: - config.subtensor.network = Prompt.ask( - "Enter subtensor network", - default=defaults.subtensor.network, - choices=networks, - ) - ( - _, - config.subtensor.chain_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - if not config.is_set("display") and not config.no_prompt: - config.display = Prompt.ask("Enter display name", default="") - if not config.is_set("legal") and not config.no_prompt: - config.legal = Prompt.ask("Enter legal string", default="") - if not config.is_set("web") and not config.no_prompt: - config.web = Prompt.ask("Enter web url", default="") - if not config.is_set("pgp_fingerprint") and not config.no_prompt: - config.pgp_fingerprint = Prompt.ask( - "Enter pgp fingerprint (must be 20 bytes)", default=None - ) - if not config.is_set("riot") and not config.no_prompt: - config.riot = Prompt.ask("Enter riot", default="") - if not config.is_set("email") and not config.no_prompt: - config.email = Prompt.ask("Enter email address", default="") - if not config.is_set("image") and not config.no_prompt: - config.image = Prompt.ask("Enter image url", default="") - if not config.is_set("twitter") and not config.no_prompt: - config.twitter = Prompt.ask("Enter twitter url", default="") - if not config.is_set("info") and not config.no_prompt: - config.info = Prompt.ask("Enter info", default="") - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - new_coldkey_parser = parser.add_parser( - "set_identity", - help="""Create or update identity on-chain for a given cold wallet. Must be a subnet owner.""", - ) - new_coldkey_parser.add_argument( - "--display", - type=str, - help="""The display name for the identity.""", - ) - new_coldkey_parser.add_argument( - "--legal", - type=str, - help="""The legal name for the identity.""", - ) - new_coldkey_parser.add_argument( - "--web", - type=str, - help="""The web url for the identity.""", - ) - new_coldkey_parser.add_argument( - "--riot", - type=str, - help="""The riot handle for the identity.""", - ) - new_coldkey_parser.add_argument( - "--email", - type=str, - help="""The email address for the identity.""", - ) - new_coldkey_parser.add_argument( - "--pgp_fingerprint", - type=str, - help="""The pgp fingerprint for the identity.""", - ) - new_coldkey_parser.add_argument( - "--image", - type=str, - help="""The image url for the identity.""", - ) - new_coldkey_parser.add_argument( - "--info", - type=str, - help="""The info for the identity.""", - ) - new_coldkey_parser.add_argument( - "--twitter", - type=str, - help="""The twitter url for the identity.""", - ) - Wallet.add_args(new_coldkey_parser) - Subtensor.add_args(new_coldkey_parser) - - -class GetIdentityCommand: - """ - Executes the :func:`get_identity` command, which retrieves and displays the identity details of a user's coldkey or hotkey associated with the Bittensor network. This function - queries the subtensor chain for information such as the stake, rank, and trust associated - with the provided key. - - Optional Arguments: - - ``key``: The ``ss58`` address of the coldkey or hotkey to query. - - The command performs the following actions: - - - Connects to the subtensor network and retrieves the identity information. - - Displays the information in a structured table format. - - The displayed table includes: - - - **Address**: The ``ss58`` address of the queried key. - - **Item**: Various attributes of the identity such as stake, rank, and trust. - - **Value**: The corresponding values of the attributes. - - Usage: - The user must provide an ``ss58`` address as input to the command. If the address is not - provided in the configuration, the user is prompted to enter one. - - Example usage:: - - btcli wallet get_identity --key - - Note: - This function is designed for CLI use and should be executed in a terminal. It is - primarily used for informational purposes and has no side effects on the network state. - """ - - @staticmethod - def run(cli): - """Queries the subtensor chain for user identity.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - GetIdentityCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - - with bt_console.status(":satellite: [bold green]Querying chain identity..."): - identity = subtensor.query_identity(cli.config.key) - - table = Table(title="[bold white italic]On-Chain Identity") - table.add_column("Item", justify="right", style="cyan", no_wrap=True) - table.add_column("Value", style="magenta") - - table.add_row("Address", cli.config.key) - for key, value in identity.items(): - table.add_row(key, str(value) if value is not None else "None") - - bt_console.print(table) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("key") and not config.no_prompt: - config.key = Prompt.ask( - "Enter coldkey or hotkey ss58 address", default=None - ) - if config.key is None: - raise ValueError("key must be set") - if not config.is_set("subtensor.network") and not config.no_prompt: - config.subtensor.network = Prompt.ask( - "Enter subtensor network", - default=defaults.subtensor.network, - choices=networks, - ) - ( - _, - config.subtensor.chain_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - new_coldkey_parser = parser.add_parser( - "get_identity", - help="""Creates a new coldkey (for containing balance) under the specified path. """, - ) - new_coldkey_parser.add_argument( - "--key", - type=str, - default=None, - help="""The coldkey or hotkey ss58 address to query.""", - ) - Wallet.add_args(new_coldkey_parser) - Subtensor.add_args(new_coldkey_parser) diff --git a/bittensor/btcli/commands/inspect.py b/bittensor/btcli/commands/inspect.py deleted file mode 100644 index ee04c728c..000000000 --- a/bittensor/btcli/commands/inspect.py +++ /dev/null @@ -1,283 +0,0 @@ -# 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 os -from typing import List, Tuple, Optional, Dict - -from rich.prompt import Prompt -from rich.table import Table -from tqdm import tqdm - -from . import defaults -from .utils import ( - get_delegates_details, - DelegatesDetails, - get_hotkey_wallets_for_wallet, - get_all_wallets_for_path, - filter_netuids_by_registered_hotkeys, -) -from bittensor.core.subtensor import Subtensor -from bittensor_wallet import Wallet -from bittensor.core.config import Config -from bittensor.core.settings import bt_console, delegates_details_url -from bittensor.utils.btlogging import logging -from bittensor.utils.balance import Balance -from bittensor.core.chain_data import DelegateInfo - - -def _get_coldkey_wallets_for_path(path: str) -> List["Wallet"]: - try: - wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [Wallet(path=path, name=name) for name in wallet_names] - except StopIteration: - # No wallet files found. - wallets = [] - return wallets - - -def _get_hotkey_wallets_for_wallet(wallet) -> List["Wallet"]: - hotkey_wallets = [] - hotkeys_path = wallet.path + "/" + wallet.name + "/hotkeys" - try: - hotkey_files = next(os.walk(os.path.expanduser(hotkeys_path)))[2] - except StopIteration: - hotkey_files = [] - for hotkey_file_name in hotkey_files: - try: - hotkey_for_name = Wallet( - path=wallet.path, name=wallet.name, hotkey=hotkey_file_name - ) - if ( - hotkey_for_name.hotkey_file.exists_on_device() - and not hotkey_for_name.hotkey_file.is_encrypted() - ): - hotkey_wallets.append(hotkey_for_name) - except Exception: - pass - return hotkey_wallets - - -class InspectCommand: - """ - Executes the ``inspect`` command, which compiles and displays a detailed report of a user's wallet pairs (coldkey, hotkey) on the Bittensor network. - - This report includes balance and - staking information for both the coldkey and hotkey associated with the wallet. - - Optional arguments: - - ``all``: If set to ``True``, the command will inspect all wallets located within the specified path. If set to ``False``, the command will inspect only the wallet specified by the user. - - The command gathers data on: - - - Coldkey balance and delegated stakes. - - Hotkey stake and emissions per neuron on the network. - - Delegate names and details fetched from the network. - - The resulting table includes columns for: - - - **Coldkey**: The coldkey associated with the user's wallet. - - **Balance**: The balance of the coldkey. - - **Delegate**: The name of the delegate to which the coldkey has staked funds. - - **Stake**: The amount of stake held by both the coldkey and hotkey. - - **Emission**: The emission or rewards earned from staking. - - **Netuid**: The network unique identifier of the subnet where the hotkey is active. - - **Hotkey**: The hotkey associated with the neuron on the network. - - Usage: - This command can be used to inspect a single wallet or all wallets located within a - specified path. It is useful for a comprehensive overview of a user's participation - and performance in the Bittensor network. - - Example usage:: - - btcli wallet inspect - btcli wallet inspect --all - - Note: - The ``inspect`` command is for displaying information only and does not perform any - transactions or state changes on the Bittensor network. It is intended to be used as - part of the Bittensor CLI and not as a standalone function within user code. - """ - - @staticmethod - def run(cli): - r"""Inspect a cold, hot pair.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - InspectCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - if cli.config.get("all", d=False) == True: - wallets = _get_coldkey_wallets_for_path(cli.config.wallet.path) - all_hotkeys = get_all_wallets_for_path(cli.config.wallet.path) - else: - wallets = [Wallet(config=cli.config)] - all_hotkeys = get_hotkey_wallets_for_wallet(wallets[0]) - - netuids = subtensor.get_all_subnet_netuids() - netuids = filter_netuids_by_registered_hotkeys( - cli, subtensor, netuids, all_hotkeys - ) - logging.debug(f"Netuids to check: {netuids}") - - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=delegates_details_url) - ) - if registered_delegate_info is None: - bt_console.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - registered_delegate_info = {} - - neuron_state_dict = {} - for netuid in tqdm(netuids): - neurons = subtensor.neurons_lite(netuid) - neuron_state_dict[netuid] = neurons if neurons != None else [] - - table = Table(show_footer=True, pad_edge=False, box=None, expand=True) - table.add_column( - "[overline white]Coldkey", footer_style="overline white", style="bold white" - ) - table.add_column( - "[overline white]Balance", footer_style="overline white", style="green" - ) - table.add_column( - "[overline white]Delegate", footer_style="overline white", style="blue" - ) - table.add_column( - "[overline white]Stake", footer_style="overline white", style="green" - ) - table.add_column( - "[overline white]Emission", footer_style="overline white", style="green" - ) - table.add_column( - "[overline white]Netuid", footer_style="overline white", style="bold white" - ) - table.add_column( - "[overline white]Hotkey", footer_style="overline white", style="yellow" - ) - table.add_column( - "[overline white]Stake", footer_style="overline white", style="green" - ) - table.add_column( - "[overline white]Emission", footer_style="overline white", style="green" - ) - for wallet in tqdm(wallets): - delegates: List[Tuple[DelegateInfo, Balance]] = ( - subtensor.get_delegated(coldkey_ss58=wallet.coldkeypub.ss58_address) - ) - if not wallet.coldkeypub_file.exists_on_device(): - continue - cold_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - table.add_row(wallet.name, str(cold_balance), "", "", "", "", "", "", "") - for dele, staked in delegates: - if dele.hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[dele.hotkey_ss58].name - else: - delegate_name = dele.hotkey_ss58 - table.add_row( - "", - "", - str(delegate_name), - str(staked), - str( - dele.total_daily_return.tao - * (staked.tao / dele.total_stake.tao) - ), - "", - "", - "", - "", - ) - - hotkeys = _get_hotkey_wallets_for_wallet(wallet) - for netuid in netuids: - for neuron in neuron_state_dict[netuid]: - if neuron.coldkey == wallet.coldkeypub.ss58_address: - hotkey_name: str = "" - - hotkey_names: List[str] = [ - wallet.hotkey_str - for wallet in filter( - lambda hotkey: hotkey.hotkey.ss58_address - == neuron.hotkey, - hotkeys, - ) - ] - if len(hotkey_names) > 0: - hotkey_name = f"{hotkey_names[0]}-" - - table.add_row( - "", - "", - "", - "", - "", - str(netuid), - f"{hotkey_name}{neuron.hotkey}", - str(neuron.stake), - str(Balance.from_tao(neuron.emission)), - ) - - bt_console.print(table) - - @staticmethod - def check_config(config: "Config"): - if ( - not config.is_set("wallet.name") - and not config.no_prompt - and not config.get("all", d=None) - ): - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if config.netuids != [] and config.netuids != None: - if not isinstance(config.netuids, list): - config.netuids = [int(config.netuids)] - else: - config.netuids = [int(netuid) for netuid in config.netuids] - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - inspect_parser = parser.add_parser( - "inspect", help="""Inspect a wallet (cold, hot) pair""" - ) - inspect_parser.add_argument( - "--all", - action="store_true", - help="""Check all coldkey wallets.""", - default=False, - ) - inspect_parser.add_argument( - "--netuids", - dest="netuids", - type=int, - nargs="*", - help="""Set the netuid(s) to filter by.""", - default=None, - ) - - Wallet.add_args(inspect_parser) - Subtensor.add_args(inspect_parser) diff --git a/bittensor/btcli/commands/list.py b/bittensor/btcli/commands/list.py deleted file mode 100644 index 7f1f3c0ce..000000000 --- a/bittensor/btcli/commands/list.py +++ /dev/null @@ -1,131 +0,0 @@ -# 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 os - -from bittensor_wallet import Wallet - -from rich import print -from rich.tree import Tree - -from bittensor.core.config import Config -from bittensor.core.subtensor import Subtensor - - -class ListCommand: - """ - Executes the ``list`` command which enumerates all wallets and their respective hotkeys present in the user's Bittensor configuration directory. - - The command organizes the information in a tree structure, displaying each wallet along with the ``ss58`` addresses for the coldkey public key and any hotkeys associated with it. - - Optional arguments: - - ``-p``, ``--path``: The path to the Bittensor configuration directory. Defaults to '~/.bittensor'. - - The output is presented in a hierarchical tree format, with each wallet as a root node, - and any associated hotkeys as child nodes. The ``ss58`` address is displayed for each - coldkey and hotkey that is not encrypted and exists on the device. - - Usage: - Upon invocation, the command scans the wallet directory and prints a list of all wallets, indicating whether the public keys are available (``?`` denotes unavailable or encrypted keys). - - Example usage:: - - btcli wallet list --path ~/.bittensor - - Note: - This command is read-only and does not modify the filesystem or the network state. It is intended for use within the Bittensor CLI to provide a quick overview of the user's wallets. - """ - - @staticmethod - def run(cli): - r"""Lists wallets.""" - try: - wallets = next(os.walk(os.path.expanduser(cli.config.wallet.path)))[1] - except StopIteration: - # No wallet files found. - wallets = [] - ListCommand._run(cli, wallets) - - @staticmethod - def _run(cli, wallets, return_value=False): - root = Tree("Wallets") - for w_name in wallets: - wallet_for_name = Wallet(path=cli.config.wallet.path, name=w_name) - try: - if ( - wallet_for_name.coldkeypub_file.exists_on_device() - and not wallet_for_name.coldkeypub_file.is_encrypted() - ): - coldkeypub_str = wallet_for_name.coldkeypub.ss58_address - else: - coldkeypub_str = "?" - except: - coldkeypub_str = "?" - - wallet_tree = root.add( - "\n[bold white]{} ({})".format(w_name, coldkeypub_str) - ) - hotkeys_path = os.path.join(cli.config.wallet.path, w_name, "hotkeys") - try: - hotkeys = next(os.walk(os.path.expanduser(hotkeys_path))) - if len(hotkeys) > 1: - for h_name in hotkeys[2]: - hotkey_for_name = Wallet( - path=cli.config.wallet.path, name=w_name, hotkey=h_name - ) - try: - if ( - hotkey_for_name.hotkey_file.exists_on_device() - and not hotkey_for_name.hotkey_file.is_encrypted() - ): - hotkey_str = hotkey_for_name.hotkey.ss58_address - else: - hotkey_str = "?" - except: - hotkey_str = "?" - wallet_tree.add("[bold grey]{} ({})".format(h_name, hotkey_str)) - except: - continue - - if len(wallets) == 0: - root.add("[bold red]No wallets found.") - - # Uses rich print to display the tree. - if not return_value: - print(root) - else: - return root - - @staticmethod - def check_config(config: "Config"): - pass - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - list_parser = parser.add_parser("list", help="""List wallets""") - Wallet.add_args(list_parser) - Subtensor.add_args(list_parser) - - @staticmethod - def get_tree(cli): - try: - wallets = next(os.walk(os.path.expanduser(cli.config.wallet.path)))[1] - except StopIteration: - # No wallet files found. - wallets = [] - return ListCommand._run(cli=cli, wallets=wallets, return_value=True) diff --git a/bittensor/btcli/commands/metagraph.py b/bittensor/btcli/commands/metagraph.py deleted file mode 100644 index 11a0c83f2..000000000 --- a/bittensor/btcli/commands/metagraph.py +++ /dev/null @@ -1,271 +0,0 @@ -# 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 - -from rich.table import Table - -from bittensor.core.config import Config -from bittensor.core.metagraph import Metagraph -from bittensor.core.settings import bt_console -from bittensor.core.subtensor import Subtensor -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging -from .utils import check_netuid_set - - -class MetagraphCommand: - """ - Executes the ``metagraph`` command to retrieve and display the entire metagraph for a specified network. - - This metagraph contains detailed information about - all the neurons (nodes) participating in the network, including their stakes, - trust scores, and more. - - Optional arguments: - - ``--netuid``: The netuid of the network to query. Defaults to the default network UID. - - ``--subtensor.network``: The name of the network to query. Defaults to the default network name. - - The table displayed includes the following columns for each neuron: - - - UID: Unique identifier of the neuron. - - STAKE(τ): Total stake of the neuron in Tau (τ). - - RANK: Rank score of the neuron. - - TRUST: Trust score assigned to the neuron by other neurons. - - CONSENSUS: Consensus score of the neuron. - - INCENTIVE: Incentive score representing the neuron's incentive alignment. - - DIVIDENDS: Dividends earned by the neuron. - - EMISSION(p): Emission in Rho (p) received by the neuron. - - VTRUST: Validator trust score indicating the network's trust in the neuron as a validator. - - VAL: Validator status of the neuron. - - UPDATED: Number of blocks since the neuron's last update. - - ACTIVE: Activity status of the neuron. - - AXON: Network endpoint information of the neuron. - - HOTKEY: Partial hotkey (public key) of the neuron. - - COLDKEY: Partial coldkey (public key) of the neuron. - - The command also prints network-wide statistics such as total stake, issuance, and difficulty. - - Usage: - The user must specify the network UID to query the metagraph. If not specified, the default network UID is used. - - Example usage:: - - btcli subnet metagraph --netuid 0 # Root network - btcli subnet metagraph --netuid 1 --subtensor.network test - - Note: - This command provides a snapshot of the network's state at the time of calling. - It is useful for network analysis and diagnostics. It is intended to be used as - part of the Bittensor CLI and not as a standalone function within user code. - """ - - @staticmethod - def run(cli): - r"""Prints an entire metagraph.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - MetagraphCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Prints an entire metagraph.""" - console = bt_console - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - cli.config.subtensor.network - ) - ) - metagraph: Metagraph = subtensor.metagraph(netuid=cli.config.netuid) - metagraph.save() - difficulty = subtensor.difficulty(cli.config.netuid) - subnet_emission = Balance.from_tao( - subtensor.get_emission_value_by_subnet(cli.config.netuid) - ) - total_issuance = Balance.from_rao(subtensor.total_issuance().rao) - - TABLE_DATA = [] - total_stake = 0.0 - total_rank = 0.0 - total_validator_trust = 0.0 - total_trust = 0.0 - total_consensus = 0.0 - total_incentive = 0.0 - total_dividends = 0.0 - total_emission = 0 - for uid in metagraph.uids: - neuron = metagraph.neurons[uid] - ep = metagraph.axons[uid] - row = [ - str(neuron.uid), - "{:.5f}".format(metagraph.total_stake[uid]), - "{:.5f}".format(metagraph.ranks[uid]), - "{:.5f}".format(metagraph.trust[uid]), - "{:.5f}".format(metagraph.consensus[uid]), - "{:.5f}".format(metagraph.incentive[uid]), - "{:.5f}".format(metagraph.dividends[uid]), - "{}".format(int(metagraph.emission[uid] * 1000000000)), - "{:.5f}".format(metagraph.validator_trust[uid]), - "*" if metagraph.validator_permit[uid] else "", - str((metagraph.block.item() - metagraph.last_update[uid].item())), - str(metagraph.active[uid].item()), - ( - ep.ip + ":" + str(ep.port) - if ep.is_serving - else "[yellow]none[/yellow]" - ), - ep.hotkey[:10], - ep.coldkey[:10], - ] - total_stake += metagraph.total_stake[uid] - total_rank += metagraph.ranks[uid] - total_validator_trust += metagraph.validator_trust[uid] - total_trust += metagraph.trust[uid] - total_consensus += metagraph.consensus[uid] - total_incentive += metagraph.incentive[uid] - total_dividends += metagraph.dividends[uid] - total_emission += int(metagraph.emission[uid] * 1000000000) - TABLE_DATA.append(row) - total_neurons = len(metagraph.uids) - table = Table(show_footer=False) - table.title = "[white]Metagraph: net: {}:{}, block: {}, N: {}/{}, stake: {}, issuance: {}, difficulty: {}".format( - subtensor.network, - metagraph.netuid, - metagraph.block.item(), - sum(metagraph.active.tolist()), - metagraph.n.item(), - Balance.from_tao(total_stake), - total_issuance, - difficulty, - ) - table.add_column( - "[overline white]UID", - str(total_neurons), - footer_style="overline white", - style="yellow", - ) - table.add_column( - "[overline white]STAKE(\u03c4)", - "\u03c4{:.5f}".format(total_stake), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]RANK", - "{:.5f}".format(total_rank), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]TRUST", - "{:.5f}".format(total_trust), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]CONSENSUS", - "{:.5f}".format(total_consensus), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]INCENTIVE", - "{:.5f}".format(total_incentive), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]DIVIDENDS", - "{:.5f}".format(total_dividends), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]EMISSION(\u03c1)", - "\u03c1{}".format(int(total_emission)), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]VTRUST", - "{:.5f}".format(total_validator_trust), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]VAL", justify="right", style="green", no_wrap=True - ) - table.add_column("[overline white]UPDATED", justify="right", no_wrap=True) - table.add_column( - "[overline white]ACTIVE", justify="right", style="green", no_wrap=True - ) - table.add_column( - "[overline white]AXON", justify="left", style="dim blue", no_wrap=True - ) - table.add_column("[overline white]HOTKEY", style="dim blue", no_wrap=False) - table.add_column("[overline white]COLDKEY", style="dim purple", no_wrap=False) - table.show_footer = True - - for row in TABLE_DATA: - table.add_row(*row) - table.box = None - table.pad_edge = False - table.width = None - console.print(table) - - @staticmethod - def check_config(config: "Config"): - check_netuid_set( - config, subtensor=Subtensor(config=config, log_verbose=False) - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - metagraph_parser = parser.add_parser( - "metagraph", help="""View a subnet metagraph information.""" - ) - metagraph_parser.add_argument( - "--netuid", - dest="netuid", - type=int, - help="""Set the netuid to get the metagraph of""", - default=False, - ) - - Subtensor.add_args(metagraph_parser) diff --git a/bittensor/btcli/commands/misc.py b/bittensor/btcli/commands/misc.py deleted file mode 100644 index 8d6a34e8d..000000000 --- a/bittensor/btcli/commands/misc.py +++ /dev/null @@ -1,118 +0,0 @@ -# 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 os - -from rich.prompt import Prompt -from rich.table import Table - -from bittensor.core.config import Config -from bittensor.core.settings import bt_console -from bittensor.core.subtensor import Subtensor - - -class UpdateCommand: - """ - Executes the ``update`` command to update the local Bittensor package. - - This command performs a series of operations to ensure that the user's local Bittensor installation is updated to the latest version from the master branch of its GitHub repository. It primarily involves pulling the latest changes from the repository and reinstalling the package. - - Usage: - Upon invocation, the command first checks the user's configuration for the ``no_prompt`` setting. If ``no_prompt`` is set to ``True``, or if the user explicitly confirms with ``Y`` when prompted, the command proceeds to update the local Bittensor package. It changes the current directory to the Bittensor package directory, checks out the master branch of the Bittensor repository, pulls the latest changes, and then reinstalls the package using ``pip``. - - The command structure is as follows: - - 1. Change directory to the Bittensor package directory. - 2. Check out the master branch of the Bittensor GitHub repository. - 3. Pull the latest changes with the ``--ff-only`` option to ensure a fast-forward update. - 4. Reinstall the Bittensor package using pip. - - Example usage:: - - btcli legacy update - - Note: - This command is intended to be used within the Bittensor CLI to facilitate easy updates of the Bittensor package. It should be used with caution as it directly affects the local installation of the package. It is recommended to ensure that any important data or configurations are backed up before running this command. - """ - - @staticmethod - def run(cli): - if cli.config.no_prompt or cli.config.answer == "Y": - os.system( - " (cd ~/.bittensor/bittensor/ ; git checkout master ; git pull --ff-only )" - ) - os.system("pip install -e ~/.bittensor/bittensor/") - - @staticmethod - def check_config(config: "Config"): - if not config.no_prompt: - answer = Prompt.ask( - "This will update the local bittensor package", - choices=["Y", "N"], - default="Y", - ) - config.answer = answer - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - update_parser = parser.add_parser( - "update", add_help=False, help="""Update bittensor """ - ) - - Subtensor.add_args(update_parser) - - -class AutocompleteCommand: - """Show users how to install and run autocompletion for Bittensor CLI.""" - - @staticmethod - def run(cli): - shell_commands = { - "Bash": "btcli --print-completion bash >> ~/.bashrc", - "Zsh": "btcli --print-completion zsh >> ~/.zshrc", - "Tcsh": "btcli --print-completion tcsh >> ~/.tcshrc", - } - - table = Table(show_header=True, header_style="bold magenta") - table.add_column("Shell", style="dim", width=12) - table.add_column("Command to Enable Autocompletion", justify="left") - - for shell, command in shell_commands.items(): - table.add_row(shell, command) - - bt_console.print( - "To enable autocompletion for Bittensor CLI, run the appropriate command for your shell:" - ) - bt_console.print(table) - - bt_console.print( - "\n[bold]After running the command, execute the following to apply the changes:[/bold]" - ) - bt_console.print(" [yellow]source ~/.bashrc[/yellow] # For Bash and Zsh") - bt_console.print(" [yellow]source ~/.tcshrc[/yellow] # For Tcsh") - - @staticmethod - def add_args(parser): - parser.add_parser( - "autocomplete", - help="Instructions for enabling autocompletion for Bittensor CLI.", - ) - - @staticmethod - def check_config(config): - pass diff --git a/bittensor/btcli/commands/network.py b/bittensor/btcli/commands/network.py deleted file mode 100644 index 03c943c0a..000000000 --- a/bittensor/btcli/commands/network.py +++ /dev/null @@ -1,654 +0,0 @@ -# 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 -from typing import List, Optional, Dict, Union, Tuple - -from rich.prompt import Prompt -from rich.table import Table - -from ...core import settings -from .identity import SetIdentityCommand -from .utils import ( - get_delegates_details, - DelegatesDetails, - check_netuid_set, - normalize_hyperparameters, -) -from bittensor.core.subtensor import Subtensor -from bittensor.api.extrinsics.utils import HYPERPARAMS -from bittensor_wallet import Wallet -from bittensor.core.config import Config -from bittensor.core.settings import bt_console -from bittensor.utils.btlogging import logging -from bittensor.utils.balance import Balance -from bittensor.utils import formatting -from bittensor.utils import RAOPERTAO -from bittensor.core.chain_data import SubnetHyperparameters, SubnetInfo - - -class RegisterSubnetworkCommand: - """ - Executes the ``register_subnetwork`` command to register a new subnetwork on the Bittensor network. - - This command facilitates the creation and registration of a subnetwork, which involves interaction with the user's wallet and the Bittensor subtensor. It ensures that the user has the necessary credentials and configurations to successfully register a new subnetwork. - - Usage: - Upon invocation, the command performs several key steps to register a subnetwork: - - 1. It copies the user's current configuration settings. - 2. It accesses the user's wallet using the provided configuration. - 3. It initializes the Bittensor subtensor object with the user's configuration. - 4. It then calls the ``register_subnetwork`` function of the subtensor object, passing the user's wallet and a prompt setting based on the user's configuration. - - If the user's configuration does not specify a wallet name and ``no_prompt`` is not set, the command will prompt the user to enter a wallet name. This name is then used in the registration process. - - The command structure includes: - - - Copying the user's configuration. - - Accessing and preparing the user's wallet. - - Initializing the Bittensor subtensor. - - Registering the subnetwork with the necessary credentials. - - Example usage:: - - btcli subnets create - - Note: - This command is intended for advanced users of the Bittensor network who wish to contribute by adding new subnetworks. It requires a clear understanding of the network's functioning and the roles of subnetworks. Users should ensure that they have secured their wallet and are aware of the implications of adding a new subnetwork to the Bittensor ecosystem. - """ - - @staticmethod - def run(cli): - """Register a subnetwork""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - RegisterSubnetworkCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Register a subnetwork""" - wallet = Wallet(config=cli.config) - - # Call register command. - success = subtensor.register_subnetwork( - wallet=wallet, - prompt=not cli.config.no_prompt, - ) - if success and not cli.config.no_prompt: - # Prompt for user to set identity. - do_set_identity = Prompt.ask( - f"Subnetwork registered successfully. Would you like to set your identity? [y/n]", - choices=["y", "n"], - ) - - if do_set_identity.lower() == "y": - subtensor.close() - config = cli.config.copy() - SetIdentityCommand.check_config(config) - cli.config = config - SetIdentityCommand.run(cli) - - @classmethod - def check_config(cls, config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=settings.defaults.wallet.name) - config.wallet.name = str(wallet_name) - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - parser = parser.add_parser( - "create", - help="""Create a new bittensor subnetwork on this chain.""", - ) - - Wallet.add_args(parser) - Subtensor.add_args(parser) - - -class SubnetLockCostCommand: - """ - Executes the ``lock_cost`` command to view the locking cost required for creating a new subnetwork on the Bittensor network. - - This command is designed to provide users with the current cost of registering a new subnetwork, which is a critical piece of information for anyone considering expanding the network's infrastructure. - - The current implementation anneals the cost of creating a subnet over a period of two days. If the cost is unappealing currently, check back in a day or two to see if it has reached an amenble level. - - Usage: - Upon invocation, the command performs the following operations: - - 1. It copies the user's current Bittensor configuration. - 2. It initializes the Bittensor subtensor object with this configuration. - 3. It then retrieves the subnet lock cost using the ``get_subnet_burn_cost()`` method from the subtensor object. - 4. The cost is displayed to the user in a readable format, indicating the amount of Tao required to lock for registering a new subnetwork. - - In case of any errors during the process (e.g., network issues, configuration problems), the command will catch these exceptions and inform the user that it failed to retrieve the lock cost, along with the specific error encountered. - - The command structure includes: - - - Copying and using the user's configuration for Bittensor. - - Retrieving the current subnet lock cost from the Bittensor network. - - Displaying the cost in a user-friendly manner. - - Example usage:: - - btcli subnets lock_cost - - Note: - This command is particularly useful for users who are planning to contribute to the Bittensor network by adding new subnetworks. Understanding the lock cost is essential for these users to make informed decisions about their potential contributions and investments in the network. - """ - - @staticmethod - def run(cli): - r"""View locking cost of creating a new subnetwork""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - SubnetLockCostCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - """View locking cost of creating a new subnetwork""" - try: - bt_console.print( - f"Subnet lock cost: [green]{Balance( subtensor.get_subnet_burn_cost() )}[/green]" - ) - except Exception as e: - bt_console.print( - f"Subnet lock cost: [red]Failed to get subnet lock cost[/red]" - f"Error: {e}" - ) - - @classmethod - def check_config(cls, config: "Config"): - pass - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - parser = parser.add_parser( - "lock_cost", - help=""" Return the lock cost to register a subnet""", - ) - - Subtensor.add_args(parser) - - -class SubnetListCommand: - """ - Executes the ``list`` command to list all subnets and their detailed information on the Bittensor network. - - This command is designed to provide users with comprehensive information about each subnet within the - network, including its unique identifier (netuid), the number of neurons, maximum neuron capacity, - emission rate, tempo, recycle register cost (burn), proof of work (PoW) difficulty, and the name or - SS58 address of the subnet owner. - - Usage: - Upon invocation, the command performs the following actions: - - 1. It initializes the Bittensor subtensor object with the user's configuration. - 2. It retrieves a list of all subnets in the network along with their detailed information. - 3. The command compiles this data into a table format, displaying key information about each subnet. - - In addition to the basic subnet details, the command also fetches delegate information to provide the - name of the subnet owner where available. If the owner's name is not available, the owner's ``SS58`` - address is displayed. - - The command structure includes: - - - Initializing the Bittensor subtensor and retrieving subnet information. - - Calculating the total number of neurons across all subnets. - - Constructing a table that includes columns for ``NETUID``, ``N`` (current neurons), ``MAX_N`` (maximum neurons), ``EMISSION``, ``TEMPO``, ``BURN``, ``POW`` (proof of work difficulty), and ``SUDO`` (owner's name or ``SS58`` address). - - Displaying the table with a footer that summarizes the total number of subnets and neurons. - - Example usage:: - - btcli subnets list - - Note: - This command is particularly useful for users seeking an overview of the Bittensor network's structure and the distribution of its resources and ownership information for each subnet. - """ - - @staticmethod - def run(cli): - r"""List all subnet netuids in the network.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - SubnetListCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""List all subnet netuids in the network.""" - subnets: List["SubnetInfo"] = subtensor.get_all_subnets_info() - - rows = [] - total_neurons = 0 - delegate_info: Optional[Dict[str, "DelegatesDetails"]] = get_delegates_details( - url=settings.delegates_details_url - ) - - for subnet in subnets: - total_neurons += subnet.max_n - rows.append( - ( - str(subnet.netuid), - str(subnet.subnetwork_n), - str(formatting.millify(subnet.max_n)), - f"{subnet.emission_value / RAOPERTAO * 100:0.2f}%", - str(subnet.tempo), - f"{subnet.burn!s:8.8}", - str(formatting.millify(subnet.difficulty)), - f"{delegate_info[subnet.owner_ss58].name if subnet.owner_ss58 in delegate_info else subnet.owner_ss58}", - ) - ) - table = Table( - show_footer=True, - width=cli.config.get("width", None), - pad_edge=True, - box=None, - show_edge=True, - ) - table.title = "[white]Subnets - {}".format(subtensor.network) - table.add_column( - "[overline white]NETUID", - str(len(subnets)), - footer_style="overline white", - style="bold green", - justify="center", - ) - table.add_column( - "[overline white]N", - str(total_neurons), - footer_style="overline white", - style="green", - justify="center", - ) - table.add_column("[overline white]MAX_N", style="white", justify="center") - table.add_column("[overline white]EMISSION", style="white", justify="center") - table.add_column("[overline white]TEMPO", style="white", justify="center") - table.add_column("[overline white]RECYCLE", style="white", justify="center") - table.add_column("[overline white]POW", style="white", justify="center") - table.add_column("[overline white]SUDO", style="white") - for row in rows: - table.add_row(*row) - bt_console.print(table) - - @staticmethod - def check_config(config: "Config"): - pass - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - list_subnets_parser = parser.add_parser( - "list", help="""List all subnets on the network""" - ) - Subtensor.add_args(list_subnets_parser) - - -class SubnetSudoCommand: - """ - Executes the ``set`` command to set hyperparameters for a specific subnet on the Bittensor network. - - This command allows subnet owners to modify various hyperparameters of theirs subnet, such as its tempo, - emission rates, and other network-specific settings. - - Usage: - The command first prompts the user to enter the hyperparameter they wish to change and its new value. - It then uses the user's wallet and configuration settings to authenticate and send the hyperparameter update - to the specified subnet. - - Example usage:: - - btcli sudo set --netuid 1 --param 'tempo' --value '0.5' - - Note: - This command requires the user to specify the subnet identifier (``netuid``) and both the hyperparameter - and its new value. It is intended for advanced users who are familiar with the network's functioning - and the impact of changing these parameters. - """ - - @staticmethod - def run(cli): - r"""Set subnet hyperparameters.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - SubnetSudoCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run( - cli, - subtensor: "Subtensor", - ): - r"""Set subnet hyperparameters.""" - wallet = Wallet(config=cli.config) - print("\n") - SubnetHyperparamsCommand.run(cli) - if not cli.config.is_set("param") and not cli.config.no_prompt: - param = Prompt.ask("Enter hyperparameter", choices=HYPERPARAMS) - cli.config.param = str(param) - if not cli.config.is_set("value") and not cli.config.no_prompt: - value = Prompt.ask("Enter new value") - cli.config.value = value - - if ( - cli.config.param == "network_registration_allowed" - or cli.config.param == "network_pow_registration_allowed" - or cli.config.param == "commit_reveal_weights_enabled" - or cli.config.param == "liquid_alpha_enabled" - ): - cli.config.value = ( - True - if (cli.config.value.lower() == "true" or cli.config.value == "1") - else False - ) - - is_allowed_value, value = allowed_value(cli.config.param, cli.config.value) - if not is_allowed_value: - raise ValueError( - f"Hyperparameter {cli.config.param} value is not within bounds. Value is {cli.config.value} but must be {value}" - ) - - subtensor.set_hyperparameter( - wallet, - netuid=cli.config.netuid, - parameter=cli.config.param, - value=value, - prompt=not cli.config.no_prompt, - ) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=settings.defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("netuid") and not config.no_prompt: - check_netuid_set( - config, Subtensor(config=config, log_verbose=False) - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser("set", help="""Set hyperparameters for a subnet""") - parser.add_argument( - "--netuid", dest="netuid", type=int, required=False, default=False - ) - parser.add_argument("--param", dest="param", type=str, required=False) - parser.add_argument("--value", dest="value", type=str, required=False) - - Wallet.add_args(parser) - Subtensor.add_args(parser) - - -class SubnetHyperparamsCommand: - """ - Executes the '``hyperparameters``' command to view the current hyperparameters of a specific subnet on the Bittensor network. - - This command is useful for users who wish to understand the configuration and - operational parameters of a particular subnet. - - Usage: - Upon invocation, the command fetches and displays a list of all hyperparameters for the specified subnet. - These include settings like tempo, emission rates, and other critical network parameters that define - the subnet's behavior. - - Example usage:: - - $ btcli subnets hyperparameters --netuid 1 - - Subnet Hyperparameters - NETUID: 1 - finney - HYPERPARAMETER VALUE - rho 10 - kappa 32767 - immunity_period 7200 - min_allowed_weights 8 - max_weight_limit 455 - tempo 99 - min_difficulty 1000000000000000000 - max_difficulty 1000000000000000000 - weights_version 2013 - weights_rate_limit 100 - adjustment_interval 112 - activity_cutoff 5000 - registration_allowed True - target_regs_per_interval 2 - min_burn 1000000000 - max_burn 100000000000 - bonds_moving_avg 900000 - max_regs_per_block 1 - - Note: - The user must specify the subnet identifier (``netuid``) for which they want to view the hyperparameters. - This command is read-only and does not modify the network state or configurations. - """ - - @staticmethod - def run(cli): - """View hyperparameters of a subnetwork.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - SubnetHyperparamsCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""View hyperparameters of a subnetwork.""" - subnet: SubnetHyperparameters = subtensor.get_subnet_hyperparameters( - cli.config.netuid - ) - - table = Table( - show_footer=True, - width=cli.config.get("width", None), - pad_edge=True, - box=None, - show_edge=True, - ) - table.title = "[white]Subnet Hyperparameters - NETUID: {} - {}".format( - cli.config.netuid, subtensor.network - ) - table.add_column("[overline white]HYPERPARAMETER", style="white") - table.add_column("[overline white]VALUE", style="green") - table.add_column("[overline white]NORMALIZED", style="cyan") - - normalized_values = normalize_hyperparameters(subnet) - - for param, value, norm_value in normalized_values: - table.add_row(" " + param, value, norm_value) - - bt_console.print(table) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("netuid") and not config.no_prompt: - check_netuid_set( - config, Subtensor(config=config, log_verbose=False) - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "hyperparameters", help="""View subnet hyperparameters""" - ) - parser.add_argument( - "--netuid", dest="netuid", type=int, required=False, default=False - ) - Subtensor.add_args(parser) - - -class SubnetGetHyperparamsCommand: - """ - Executes the ``get`` command to retrieve the hyperparameters of a specific subnet on the Bittensor network. - - This command is similar to the ``hyperparameters`` command but may be used in different contexts within the CLI. - - Usage: - The command connects to the Bittensor network, queries the specified subnet, and returns a detailed list - of all its hyperparameters. This includes crucial operational parameters that determine the subnet's - performance and interaction within the network. - - Example usage:: - - $ btcli sudo get --netuid 1 - - Subnet Hyperparameters - NETUID: 1 - finney - HYPERPARAMETER VALUE - rho 10 - kappa 32767 - immunity_period 7200 - min_allowed_weights 8 - max_weight_limit 455 - tempo 99 - min_difficulty 1000000000000000000 - max_difficulty 1000000000000000000 - weights_version 2013 - weights_rate_limit 100 - adjustment_interval 112 - activity_cutoff 5000 - registration_allowed True - target_regs_per_interval 2 - min_burn 1000000000 - max_burn 100000000000 - bonds_moving_avg 900000 - max_regs_per_block 1 - - Note: - Users need to provide the ``netuid`` of the subnet whose hyperparameters they wish to view. This command is - designed for informational purposes and does not alter any network settings or configurations. - """ - - @staticmethod - def run(cli): - r"""View hyperparameters of a subnetwork.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - SubnetGetHyperparamsCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - """View hyperparameters of a subnetwork.""" - subnet: SubnetHyperparameters = subtensor.get_subnet_hyperparameters( - cli.config.netuid - ) - - table = Table( - show_footer=True, - width=cli.config.get("width", None), - pad_edge=True, - box=None, - show_edge=True, - ) - table.title = "[white]Subnet Hyperparameters - NETUID: {} - {}".format( - cli.config.netuid, subtensor.network - ) - table.add_column("[overline white]HYPERPARAMETER", style="white") - table.add_column("[overline white]VALUE", style="green") - table.add_column("[overline white]NORMALIZED", style="cyan") - - normalized_values = normalize_hyperparameters(subnet) - - for param, value, norm_value in normalized_values: - table.add_row(" " + param, value, norm_value) - - bt_console.print(table) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("netuid") and not config.no_prompt: - check_netuid_set( - config, Subtensor(config=config, log_verbose=False) - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser("get", help="""View subnet hyperparameters""") - parser.add_argument( - "--netuid", dest="netuid", type=int, required=False, default=False - ) - Subtensor.add_args(parser) - - -def allowed_value( - param: str, value: Union[str, bool, float] -) -> Tuple[bool, Union[str, list[float], float]]: - """ - Check the allowed values on hyperparameters. Return False if value is out of bounds. - """ - # Reminder error message ends like: Value is {value} but must be {error_message}. (the second part of return statement) - # Check if value is a boolean, only allow boolean and floats - try: - if not isinstance(value, bool): - if param == "alpha_values": - # Split the string into individual values - alpha_low_str, alpha_high_str = value.split(",") - alpha_high = float(alpha_high_str) - alpha_low = float(alpha_low_str) - - # Check alpha_high value - if alpha_high <= 52428 or alpha_high >= 65535: - return ( - False, - f"between 52428 and 65535 for alpha_high (but is {alpha_high})", - ) - - # Check alpha_low value - if alpha_low < 0 or alpha_low > 52428: - return ( - False, - f"between 0 and 52428 for alpha_low (but is {alpha_low})", - ) - - return True, [alpha_low, alpha_high] - except ValueError: - return False, "a number or a boolean" - - return True, value diff --git a/bittensor/btcli/commands/overview.py b/bittensor/btcli/commands/overview.py deleted file mode 100644 index 8daa56c42..000000000 --- a/bittensor/btcli/commands/overview.py +++ /dev/null @@ -1,784 +0,0 @@ -# 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 -from collections import defaultdict -from concurrent.futures import ProcessPoolExecutor -from typing import List, Optional, Dict, Tuple - -from bittensor_wallet import Wallet -from fuzzywuzzy import fuzz -from rich.align import Align -from rich.prompt import Prompt -from rich.table import Table -from tqdm import tqdm - -from bittensor.core.chain_data import NeuronInfoLite -from bittensor.core.settings import bt_console -from bittensor.core.subtensor import Subtensor -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging -from bittensor.utils.networking import int_to_ip -from . import defaults -from .utils import ( - get_hotkey_wallets_for_wallet, - get_coldkey_wallets_for_path, - get_all_wallets_for_path, - filter_netuids_by_registered_hotkeys, -) - - -class OverviewCommand: - """ - Executes the ``overview`` command to present a detailed overview of the user's registered accounts on the Bittensor network. - - This command compiles and displays comprehensive information about each neuron associated with the user's wallets, - including both hotkeys and coldkeys. It is especially useful for users managing multiple accounts or seeking a summary - of their network activities and stake distributions. - - Usage: - The command offers various options to customize the output. Users can filter the displayed data by specific netuids, - sort by different criteria, and choose to include all wallets in the user's configuration directory. The output is - presented in a tabular format with the following columns: - - - COLDKEY: The SS58 address of the coldkey. - - HOTKEY: The SS58 address of the hotkey. - - UID: Unique identifier of the neuron. - - ACTIVE: Indicates if the neuron is active. - - STAKE(τ): Amount of stake in the neuron, in Tao. - - RANK: The rank of the neuron within the network. - - TRUST: Trust score of the neuron. - - CONSENSUS: Consensus score of the neuron. - - INCENTIVE: Incentive score of the neuron. - - DIVIDENDS: Dividends earned by the neuron. - - EMISSION(p): Emission received by the neuron, in Rho. - - VTRUST: Validator trust score of the neuron. - - VPERMIT: Indicates if the neuron has a validator permit. - - UPDATED: Time since last update. - - AXON: IP address and port of the neuron. - - HOTKEY_SS58: Human-readable representation of the hotkey. - - Example usage:: - - btcli wallet overview - btcli wallet overview --all --sort_by stake --sort_order descending - - Note: - This command is read-only and does not modify the network state or account configurations. It provides a quick and - comprehensive view of the user's network presence, making it ideal for monitoring account status, stake distribution, - and overall contribution to the Bittensor network. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Prints an overview for the wallet's colkey.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - OverviewCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _get_total_balance( - total_balance: "Balance", - subtensor: "Subtensor", - cli: "bittensor.cli", - ) -> Tuple[List["Wallet"], "Balance"]: - if cli.config.get("all", d=None): - cold_wallets = get_coldkey_wallets_for_path(cli.config.wallet.path) - for cold_wallet in tqdm(cold_wallets, desc="Pulling balances"): - if ( - cold_wallet.coldkeypub_file.exists_on_device() - and not cold_wallet.coldkeypub_file.is_encrypted() - ): - total_balance = total_balance + subtensor.get_balance( - cold_wallet.coldkeypub.ss58_address - ) - all_hotkeys = get_all_wallets_for_path(cli.config.wallet.path) - else: - # We are only printing keys for a single coldkey - coldkey_wallet = Wallet(config=cli.config) - if ( - coldkey_wallet.coldkeypub_file.exists_on_device() - and not coldkey_wallet.coldkeypub_file.is_encrypted() - ): - total_balance = subtensor.get_balance( - coldkey_wallet.coldkeypub.ss58_address - ) - if not coldkey_wallet.coldkeypub_file.exists_on_device(): - bt_console.print("[bold red]No wallets found.") - return [], None - all_hotkeys = get_hotkey_wallets_for_wallet(coldkey_wallet) - - return all_hotkeys, total_balance - - @staticmethod - def _get_hotkeys( - cli: "bittensor.cli", all_hotkeys: List["Wallet"] - ) -> List["Wallet"]: - if not cli.config.get("all_hotkeys", False): - # We are only showing hotkeys that are specified. - all_hotkeys = [ - hotkey - for hotkey in all_hotkeys - if hotkey.hotkey_str in cli.config.hotkeys - ] - else: - # We are excluding the specified hotkeys from all_hotkeys. - all_hotkeys = [ - hotkey - for hotkey in all_hotkeys - if hotkey.hotkey_str not in cli.config.hotkeys - ] - return all_hotkeys - - @staticmethod - def _get_key_address(all_hotkeys: List["Wallet"]): - hotkey_coldkey_to_hotkey_wallet = {} - for hotkey_wallet in all_hotkeys: - if hotkey_wallet.hotkey.ss58_address not in hotkey_coldkey_to_hotkey_wallet: - hotkey_coldkey_to_hotkey_wallet[hotkey_wallet.hotkey.ss58_address] = {} - - hotkey_coldkey_to_hotkey_wallet[hotkey_wallet.hotkey.ss58_address][ - hotkey_wallet.coldkeypub.ss58_address - ] = hotkey_wallet - - all_hotkey_addresses = list(hotkey_coldkey_to_hotkey_wallet.keys()) - - return all_hotkey_addresses, hotkey_coldkey_to_hotkey_wallet - - @staticmethod - def _process_neuron_results( - results: List[Tuple[int, List["bittensor.NeuronInfoLite"], Optional[str]]], - neurons: Dict[str, List["bittensor.NeuronInfoLite"]], - netuids: List[int], - ) -> Dict[str, List["bittensor.NeuronInfoLite"]]: - for result in results: - netuid, neurons_result, err_msg = result - if err_msg is not None: - bt_console.print(f"netuid '{netuid}': {err_msg}") - - if len(neurons_result) == 0: - # Remove netuid from overview if no neurons are found. - netuids.remove(netuid) - del neurons[str(netuid)] - else: - # Add neurons to overview. - neurons[str(netuid)] = neurons_result - return neurons - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - """Prints an overview for the wallet's coldkey.""" - wallet = Wallet(config=cli.config) - - all_hotkeys = [] - total_balance = Balance(0) - - # We are printing for every coldkey. - all_hotkeys, total_balance = OverviewCommand._get_total_balance( - total_balance, subtensor, cli - ) - - # We are printing for a select number of hotkeys from all_hotkeys. - if cli.config.get("hotkeys"): - all_hotkeys = OverviewCommand._get_hotkeys(cli, all_hotkeys) - - # Check we have keys to display. - if len(all_hotkeys) == 0: - bt_console.print("[red]No wallets found.[/red]") - return - - # Pull neuron info for all keys. - neurons: Dict[str, List[NeuronInfoLite]] = {} - block = subtensor.block - - netuids = subtensor.get_all_subnet_netuids() - netuids = filter_netuids_by_registered_hotkeys( - cli, subtensor, netuids, all_hotkeys - ) - logging.debug(f"Netuids to check: {netuids}") - - for netuid in netuids: - neurons[str(netuid)] = [] - - all_wallet_names = {wallet.name for wallet in all_hotkeys} - all_coldkey_wallets = [ - Wallet(name=wallet_name) for wallet_name in all_wallet_names - ] - - ( - all_hotkey_addresses, - hotkey_coldkey_to_hotkey_wallet, - ) = OverviewCommand._get_key_address(all_hotkeys) - - with bt_console.status( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - cli.config.subtensor.get( - "network", defaults.subtensor.network - ) - ) - ): - # Create a copy of the config without the parser and formatter_class. - ## This is needed to pass to the ProcessPoolExecutor, which cannot pickle the parser. - copy_config = cli.config.copy() - copy_config["__parser"] = None - copy_config["formatter_class"] = None - - # Pull neuron info for all keys. - ## Max len(netuids) or 5 threads. - with ProcessPoolExecutor(max_workers=max(len(netuids), 5)) as executor: - results = executor.map( - OverviewCommand._get_neurons_for_netuid, - [(copy_config, netuid, all_hotkey_addresses) for netuid in netuids], - ) - executor.shutdown(wait=True) # wait for all complete - - neurons = OverviewCommand._process_neuron_results( - results, neurons, netuids - ) - - total_coldkey_stake_from_metagraph = defaultdict( - lambda: Balance(0.0) - ) - checked_hotkeys = set() - for neuron_list in neurons.values(): - for neuron in neuron_list: - if neuron.hotkey in checked_hotkeys: - continue - total_coldkey_stake_from_metagraph[neuron.coldkey] += ( - neuron.stake_dict[neuron.coldkey] - ) - checked_hotkeys.add(neuron.hotkey) - - alerts_table = Table(show_header=True, header_style="bold magenta") - alerts_table.add_column("🥩 alert!") - - coldkeys_to_check = [] - for coldkey_wallet in all_coldkey_wallets: - # Check if we have any stake with hotkeys that are not registered. - total_coldkey_stake_from_chain = subtensor.get_total_stake_for_coldkey( - ss58_address=coldkey_wallet.coldkeypub.ss58_address - ) - difference = ( - total_coldkey_stake_from_chain - - total_coldkey_stake_from_metagraph[ - coldkey_wallet.coldkeypub.ss58_address - ] - ) - if difference == 0: - continue # We have all our stake registered. - - coldkeys_to_check.append(coldkey_wallet) - alerts_table.add_row( - "Found {} stake with coldkey {} that is not registered.".format( - difference, coldkey_wallet.coldkeypub.ss58_address - ) - ) - - if coldkeys_to_check: - # We have some stake that is not with a registered hotkey. - if "-1" not in neurons: - neurons["-1"] = [] - - # Use process pool to check each coldkey wallet for de-registered stake. - with ProcessPoolExecutor( - max_workers=max(len(coldkeys_to_check), 5) - ) as executor: - results = executor.map( - OverviewCommand._get_de_registered_stake_for_coldkey_wallet, - [ - (cli.config, all_hotkey_addresses, coldkey_wallet) - for coldkey_wallet in coldkeys_to_check - ], - ) - executor.shutdown(wait=True) # wait for all complete - - for result in results: - coldkey_wallet, de_registered_stake, err_msg = result - if err_msg is not None: - bt_console.print(err_msg) - - if len(de_registered_stake) == 0: - continue # We have no de-registered stake with this coldkey. - - de_registered_neurons = [] - for hotkey_addr, our_stake in de_registered_stake: - # Make a neuron info lite for this hotkey and coldkey. - de_registered_neuron = NeuronInfoLite.get_null_neuron() - de_registered_neuron.hotkey = hotkey_addr - de_registered_neuron.coldkey = ( - coldkey_wallet.coldkeypub.ss58_address - ) - de_registered_neuron.total_stake = Balance(our_stake) - - de_registered_neurons.append(de_registered_neuron) - - # Add this hotkey to the wallets dict - wallet_ = Wallet( - name=wallet, - ) - wallet_.hotkey_ss58 = hotkey_addr - wallet.hotkey_str = hotkey_addr[:5] # Max length of 5 characters - # Indicates a hotkey not on local machine but exists in stake_info obj on-chain - if hotkey_coldkey_to_hotkey_wallet.get(hotkey_addr) is None: - hotkey_coldkey_to_hotkey_wallet[hotkey_addr] = {} - hotkey_coldkey_to_hotkey_wallet[hotkey_addr][ - coldkey_wallet.coldkeypub.ss58_address - ] = wallet_ - - # Add neurons to overview. - neurons["-1"].extend(de_registered_neurons) - - # Setup outer table. - grid = Table.grid(pad_edge=False) - - # If there are any alerts, add them to the grid - if len(alerts_table.rows) > 0: - grid.add_row(alerts_table) - - title: str = "" - if not cli.config.get("all", d=None): - title = "[bold white italic]Wallet - {}:{}".format( - cli.config.wallet.name, wallet.coldkeypub.ss58_address - ) - else: - title = "[bold whit italic]All Wallets:" - - # Add title - grid.add_row(Align(title, vertical="middle", align="center")) - - # Generate rows per netuid - hotkeys_seen = set() - total_neurons = 0 - total_stake = 0.0 - for netuid in netuids: - subnet_tempo = subtensor.tempo(netuid=netuid) - last_subnet = netuid == netuids[-1] - TABLE_DATA = [] - total_rank = 0.0 - total_trust = 0.0 - total_consensus = 0.0 - total_validator_trust = 0.0 - total_incentive = 0.0 - total_dividends = 0.0 - total_emission = 0 - - for nn in neurons[str(netuid)]: - hotwallet = hotkey_coldkey_to_hotkey_wallet.get(nn.hotkey, {}).get( - nn.coldkey, None - ) - if not hotwallet: - # Indicates a mismatch between what the chain says the coldkey - # is for this hotkey and the local wallet coldkey-hotkey pair - hotwallet = argparse.Namespace() - hotwallet.name = nn.coldkey[:7] - hotwallet.hotkey_str = nn.hotkey[:7] - nn: NeuronInfoLite - uid = nn.uid - active = nn.active - stake = nn.total_stake.tao - rank = nn.rank - trust = nn.trust - consensus = nn.consensus - validator_trust = nn.validator_trust - incentive = nn.incentive - dividends = nn.dividends - emission = int(nn.emission / (subnet_tempo + 1) * 1e9) - last_update = int(block - nn.last_update) - validator_permit = nn.validator_permit - row = [ - hotwallet.name, - hotwallet.hotkey_str, - str(uid), - str(active), - "{:.5f}".format(stake), - "{:.5f}".format(rank), - "{:.5f}".format(trust), - "{:.5f}".format(consensus), - "{:.5f}".format(incentive), - "{:.5f}".format(dividends), - "{:_}".format(emission), - "{:.5f}".format(validator_trust), - "*" if validator_permit else "", - str(last_update), - ( - int_to_ip(nn.axon_info.ip) - + ":" - + str(nn.axon_info.port) - if nn.axon_info.port != 0 - else "[yellow]none[/yellow]" - ), - nn.hotkey, - ] - - total_rank += rank - total_trust += trust - total_consensus += consensus - total_incentive += incentive - total_dividends += dividends - total_emission += emission - total_validator_trust += validator_trust - - if not (nn.hotkey, nn.coldkey) in hotkeys_seen: - # Don't double count stake on hotkey-coldkey pairs. - hotkeys_seen.add((nn.hotkey, nn.coldkey)) - total_stake += stake - - # netuid -1 are neurons that are de-registered. - if netuid != "-1": - total_neurons += 1 - - TABLE_DATA.append(row) - - # Add subnet header - if netuid == "-1": - grid.add_row(f"Deregistered Neurons") - else: - grid.add_row(f"Subnet: [bold white]{netuid}[/bold white]") - - table = Table( - show_footer=False, - width=cli.config.get("width", None), - pad_edge=False, - box=None, - ) - if last_subnet: - table.add_column( - "[overline white]COLDKEY", - str(total_neurons), - footer_style="overline white", - style="bold white", - ) - table.add_column( - "[overline white]HOTKEY", - str(total_neurons), - footer_style="overline white", - style="white", - ) - else: - # No footer for non-last subnet. - table.add_column("[overline white]COLDKEY", style="bold white") - table.add_column("[overline white]HOTKEY", style="white") - table.add_column( - "[overline white]UID", - str(total_neurons), - footer_style="overline white", - style="yellow", - ) - table.add_column( - "[overline white]ACTIVE", justify="right", style="green", no_wrap=True - ) - if last_subnet: - table.add_column( - "[overline white]STAKE(\u03c4)", - "\u03c4{:.5f}".format(total_stake), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - else: - # No footer for non-last subnet. - table.add_column( - "[overline white]STAKE(\u03c4)", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]RANK", - "{:.5f}".format(total_rank), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]TRUST", - "{:.5f}".format(total_trust), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]CONSENSUS", - "{:.5f}".format(total_consensus), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]INCENTIVE", - "{:.5f}".format(total_incentive), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]DIVIDENDS", - "{:.5f}".format(total_dividends), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]EMISSION(\u03c1)", - "\u03c1{:_}".format(total_emission), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]VTRUST", - "{:.5f}".format(total_validator_trust), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column("[overline white]VPERMIT", justify="right", no_wrap=True) - table.add_column("[overline white]UPDATED", justify="right", no_wrap=True) - table.add_column( - "[overline white]AXON", justify="left", style="dim blue", no_wrap=True - ) - table.add_column( - "[overline white]HOTKEY_SS58", style="dim blue", no_wrap=False - ) - table.show_footer = True - - sort_by: Optional[str] = cli.config.get("sort_by", None) - sort_order: Optional[str] = cli.config.get("sort_order", None) - - if sort_by is not None and sort_by != "": - column_to_sort_by: int = 0 - highest_matching_ratio: int = 0 - sort_descending: bool = False # Default sort_order to ascending - - for index, column in zip(range(len(table.columns)), table.columns): - # Fuzzy match the column name. Default to the first column. - column_name = column.header.lower().replace("[overline white]", "") - match_ratio = fuzz.ratio(sort_by.lower(), column_name) - # Finds the best matching column - if match_ratio > highest_matching_ratio: - highest_matching_ratio = match_ratio - column_to_sort_by = index - - if sort_order.lower() in {"desc", "descending", "reverse"}: - # Sort descending if the sort_order matches desc, descending, or reverse - sort_descending = True - - def overview_sort_function(row): - data = row[column_to_sort_by] - # Try to convert to number if possible - try: - data = float(data) - except ValueError: - pass - return data - - TABLE_DATA.sort(key=overview_sort_function, reverse=sort_descending) - - for row in TABLE_DATA: - table.add_row(*row) - - grid.add_row(table) - - bt_console.clear() - - caption = "[italic][dim][white]Wallet balance: [green]\u03c4" + str( - total_balance.tao - ) - grid.add_row(Align(caption, vertical="middle", align="center")) - - # Print the entire table/grid - bt_console.print(grid, width=cli.config.get("width", None)) - - @staticmethod - def _get_neurons_for_netuid( - args_tuple: Tuple["bittensor.Config", int, List[str]], - ) -> Tuple[int, List["bittensor.NeuronInfoLite"], Optional[str]]: - subtensor_config, netuid, hot_wallets = args_tuple - - result: List["bittensor.NeuronInfoLite"] = [] - - try: - subtensor = Subtensor(config=subtensor_config, log_verbose=False) - - all_neurons: List["bittensor.NeuronInfoLite"] = subtensor.neurons_lite( - netuid=netuid - ) - # Map the hotkeys to uids - hotkey_to_neurons = {n.hotkey: n.uid for n in all_neurons} - for hot_wallet_addr in hot_wallets: - uid = hotkey_to_neurons.get(hot_wallet_addr) - if uid is not None: - nn = all_neurons[uid] - result.append(nn) - except Exception as e: - return netuid, [], "Error: {}".format(e) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - return netuid, result, None - - @staticmethod - def _get_de_registered_stake_for_coldkey_wallet( - args_tuple, - ) -> Tuple[ - "bittensor.Wallet", List[Tuple[str, "Balance"]], Optional[str] - ]: - subtensor_config, all_hotkey_addresses, coldkey_wallet = args_tuple - - # List of (hotkey_addr, our_stake) tuples. - result: List[Tuple[str, "Balance"]] = [] - - try: - subtensor = Subtensor(config=subtensor_config, log_verbose=False) - - # Pull all stake for our coldkey - all_stake_info_for_coldkey = subtensor.get_stake_info_for_coldkey( - coldkey_ss58=coldkey_wallet.coldkeypub.ss58_address - ) - - ## Filter out hotkeys that are in our wallets - ## Filter out hotkeys that are delegates. - def _filter_stake_info(stake_info: "bittensor.StakeInfo") -> bool: - if stake_info.stake == 0: - return False # Skip hotkeys that we have no stake with. - if stake_info.hotkey_ss58 in all_hotkey_addresses: - return False # Skip hotkeys that are in our wallets. - if subtensor.is_hotkey_delegate(hotkey_ss58=stake_info.hotkey_ss58): - return False # Skip hotkeys that are delegates, they show up in btcli my_delegates table. - - return True - - all_staked_hotkeys = filter(_filter_stake_info, all_stake_info_for_coldkey) - result = [ - ( - stake_info.hotkey_ss58, - stake_info.stake.tao, - ) # stake is a Balance object - for stake_info in all_staked_hotkeys - ] - - except Exception as e: - return coldkey_wallet, [], "Error: {}".format(e) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - return coldkey_wallet, result, None - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - overview_parser = parser.add_parser( - "overview", help="""Show registered account overview.""" - ) - overview_parser.add_argument( - "--all", - dest="all", - action="store_true", - help="""View overview for all wallets.""", - default=False, - ) - overview_parser.add_argument( - "--width", - dest="width", - action="store", - type=int, - help="""Set the output width of the overview. Defaults to automatic width from terminal.""", - default=None, - ) - overview_parser.add_argument( - "--sort_by", - "--wallet.sort_by", - dest="sort_by", - required=False, - action="store", - default="", - type=str, - help="""Sort the hotkeys by the specified column title (e.g. name, uid, axon).""", - ) - overview_parser.add_argument( - "--sort_order", - "--wallet.sort_order", - dest="sort_order", - required=False, - action="store", - default="ascending", - type=str, - help="""Sort the hotkeys in the specified ordering. (ascending/asc or descending/desc/reverse)""", - ) - overview_parser.add_argument( - "--hotkeys", - "--exclude_hotkeys", - "--wallet.hotkeys", - "--wallet.exclude_hotkeys", - required=False, - action="store", - default=[], - type=str, - nargs="*", - help="""Specify the hotkeys by name or ss58 address. (e.g. hk1 hk2 hk3)""", - ) - overview_parser.add_argument( - "--all_hotkeys", - "--wallet.all_hotkeys", - required=False, - action="store_true", - default=False, - help="""To specify all hotkeys. Specifying hotkeys will exclude them from this all.""", - ) - overview_parser.add_argument( - "--netuids", - dest="netuids", - type=int, - nargs="*", - help="""Set the netuid(s) to filter by.""", - default=None, - ) - Wallet.add_args(overview_parser) - Subtensor.add_args(overview_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if ( - not config.is_set("wallet.name") - and not config.no_prompt - and not config.get("all", d=None) - ): - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if config.netuids != [] and config.netuids != None: - if not isinstance(config.netuids, list): - config.netuids = [int(config.netuids)] - else: - config.netuids = [int(netuid) for netuid in config.netuids] diff --git a/bittensor/btcli/commands/register.py b/bittensor/btcli/commands/register.py deleted file mode 100644 index f11b3bb0b..000000000 --- a/bittensor/btcli/commands/register.py +++ /dev/null @@ -1,619 +0,0 @@ -# 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 sys -from copy import deepcopy - -from rich.console import Console -from rich.prompt import Prompt, Confirm - -from . import defaults -from .utils import check_netuid_set, check_for_cuda_reg_config -from bittensor.core.settings import networks, bt_console -from bittensor_wallet import Wallet -from bittensor.utils.btlogging import logging -from bittensor.core.subtensor import Subtensor -from bittensor.core.config import Config - -console = Console() - - -class RegisterCommand: - """ - Executes the ``register`` command to register a neuron on the Bittensor network by recycling some TAO (the network's native token). - - This command is used to add a new neuron to a specified subnet within the network, contributing to the decentralization and robustness of Bittensor. - - Usage: - Before registering, the command checks if the specified subnet exists and whether the user's balance is sufficient to cover the registration cost. - - The registration cost is determined by the current recycle amount for the specified subnet. If the balance is insufficient or the subnet does not exist, the command will exit with an appropriate error message. - - If the preconditions are met, and the user confirms the transaction (if ``no_prompt`` is not set), the command proceeds to register the neuron by recycling the required amount of TAO. - - The command structure includes: - - - Verification of subnet existence. - - Checking the user's balance against the current recycle amount for the subnet. - - User confirmation prompt for proceeding with registration. - - Execution of the registration process. - - Columns Displayed in the confirmation prompt: - - - Balance: The current balance of the user's wallet in TAO. - - Cost to Register: The required amount of TAO needed to register on the specified subnet. - - Example usage:: - - btcli subnets register --netuid 1 - - Note: - This command is critical for users who wish to contribute a new neuron to the network. It requires careful consideration of the subnet selection and an understanding of the registration costs. Users should ensure their wallet is sufficiently funded before attempting to register a neuron. - """ - - @staticmethod - def run(cli): - r"""Register neuron by recycling some TAO.""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - RegisterCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Register neuron by recycling some TAO.""" - wallet = Wallet(config=cli.config) - - # Verify subnet exists - if not subtensor.subnet_exists(netuid=cli.config.netuid): - bt_console.print( - f"[red]Subnet {cli.config.netuid} does not exist[/red]" - ) - sys.exit(1) - - # Check current recycle amount - current_recycle = subtensor.recycle(netuid=cli.config.netuid) - balance = subtensor.get_balance(address=wallet.coldkeypub.ss58_address) - - # Check balance is sufficient - if balance < current_recycle: - bt_console.print( - f"[red]Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO[/red]" - ) - sys.exit(1) - - if not cli.config.no_prompt: - if ( - Confirm.ask( - f"Your balance is: [bold green]{balance}[/bold green]\nThe cost to register by recycle is [bold red]{current_recycle}[/bold red]\nDo you want to continue?", - default=False, - ) - == False - ): - sys.exit(1) - - subtensor.burned_register( - wallet=wallet, netuid=cli.config.netuid, prompt=not cli.config.no_prompt - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - register_parser = parser.add_parser( - "register", help="""Register a wallet to a network.""" - ) - register_parser.add_argument( - "--netuid", - type=int, - help="netuid for subnet to serve this neuron on", - default=argparse.SUPPRESS, - ) - - Wallet.add_args(register_parser) - Subtensor.add_args(register_parser) - - @staticmethod - def check_config(config: "Config"): - if ( - not config.is_set("subtensor.network") - and not config.is_set("subtensor.chain_endpoint") - and not config.no_prompt - ): - config.subtensor.network = Prompt.ask( - "Enter subtensor network", - choices=networks, - default=defaults.subtensor.network, - ) - _, endpoint = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - config.subtensor.chain_endpoint = endpoint - - check_netuid_set( - config, subtensor=Subtensor(config=config, log_verbose=False) - ) - - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - -class PowRegisterCommand: - """ - Executes the ``pow_register`` command to register a neuron on the Bittensor network using Proof of Work (PoW). - - This method is an alternative registration process that leverages computational work for securing a neuron's place on the network. - - Usage: - The command starts by verifying the existence of the specified subnet. If the subnet does not exist, it terminates with an error message. - On successful verification, the PoW registration process is initiated, which requires solving computational puzzles. - - Optional arguments: - - ``--netuid`` (int): The netuid for the subnet on which to serve the neuron. Mandatory for specifying the target subnet. - - ``--pow_register.num_processes`` (int): The number of processors to use for PoW registration. Defaults to the system's default setting. - - ``--pow_register.update_interval`` (int): The number of nonces to process before checking for the next block during registration. Affects the frequency of update checks. - - ``--pow_register.no_output_in_place`` (bool): When set, disables the output of registration statistics in place. Useful for cleaner logs. - - ``--pow_register.verbose`` (bool): Enables verbose output of registration statistics for detailed information. - - ``--pow_register.cuda.use_cuda`` (bool): Enables the use of CUDA for GPU-accelerated PoW calculations. Requires a CUDA-compatible GPU. - - ``--pow_register.cuda.no_cuda`` (bool): Disables the use of CUDA, defaulting to CPU-based calculations. - - ``--pow_register.cuda.dev_id`` (int): Specifies the CUDA device ID, useful for systems with multiple CUDA-compatible GPUs. - - ``--pow_register.cuda.tpb`` (int): Sets the number of Threads Per Block for CUDA operations, affecting the GPU calculation dynamics. - - The command also supports additional wallet and subtensor arguments, enabling further customization of the registration process. - - Example usage:: - - btcli pow_register --netuid 1 --pow_register.num_processes 4 --cuda.use_cuda - - Note: - This command is suited for users with adequate computational resources to participate in PoW registration. It requires a sound understanding - of the network's operations and PoW mechanics. Users should ensure their systems meet the necessary hardware and software requirements, - particularly when opting for CUDA-based GPU acceleration. - - This command may be disabled according on the subnet owner's directive. For example, on netuid 1 this is permanently disabled. - """ - - @staticmethod - def run(cli): - r"""Register neuron.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - PowRegisterCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Register neuron.""" - wallet = Wallet(config=cli.config) - - # Verify subnet exists - if not subtensor.subnet_exists(netuid=cli.config.netuid): - bt_console.print( - f"[red]Subnet {cli.config.netuid} does not exist[/red]" - ) - sys.exit(1) - - registered = subtensor.register( - wallet=wallet, - netuid=cli.config.netuid, - prompt=not cli.config.no_prompt, - tpb=cli.config.pow_register.cuda.get("tpb", None), - update_interval=cli.config.pow_register.get("update_interval", None), - num_processes=cli.config.pow_register.get("num_processes", None), - cuda=cli.config.pow_register.cuda.get( - "use_cuda", defaults.pow_register.cuda.use_cuda - ), - dev_id=cli.config.pow_register.cuda.get("dev_id", None), - output_in_place=cli.config.pow_register.get( - "output_in_place", defaults.pow_register.output_in_place - ), - log_verbose=cli.config.pow_register.get( - "verbose", defaults.pow_register.verbose - ), - ) - if not registered: - sys.exit(1) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - register_parser = parser.add_parser( - "pow_register", help="""Register a wallet to a network using PoW.""" - ) - register_parser.add_argument( - "--netuid", - type=int, - help="netuid for subnet to serve this neuron on", - default=argparse.SUPPRESS, - ) - register_parser.add_argument( - "--pow_register.num_processes", - "-n", - dest="pow_register.num_processes", - help="Number of processors to use for POW registration", - type=int, - default=defaults.pow_register.num_processes, - ) - register_parser.add_argument( - "--pow_register.update_interval", - "--pow_register.cuda.update_interval", - "--cuda.update_interval", - "-u", - help="The number of nonces to process before checking for next block during registration", - type=int, - default=defaults.pow_register.update_interval, - ) - register_parser.add_argument( - "--pow_register.no_output_in_place", - "--no_output_in_place", - dest="pow_register.output_in_place", - help="Whether to not ouput the registration statistics in-place. Set flag to disable output in-place.", - action="store_false", - required=False, - default=defaults.pow_register.output_in_place, - ) - register_parser.add_argument( - "--pow_register.verbose", - help="Whether to ouput the registration statistics verbosely.", - action="store_true", - required=False, - default=defaults.pow_register.verbose, - ) - - ## Registration args for CUDA registration. - register_parser.add_argument( - "--pow_register.cuda.use_cuda", - "--cuda", - "--cuda.use_cuda", - dest="pow_register.cuda.use_cuda", - default=defaults.pow_register.cuda.use_cuda, - help="""Set flag to use CUDA to register.""", - action="store_true", - required=False, - ) - register_parser.add_argument( - "--pow_register.cuda.no_cuda", - "--no_cuda", - "--cuda.no_cuda", - dest="pow_register.cuda.use_cuda", - default=not defaults.pow_register.cuda.use_cuda, - help="""Set flag to not use CUDA for registration""", - action="store_false", - required=False, - ) - - register_parser.add_argument( - "--pow_register.cuda.dev_id", - "--cuda.dev_id", - type=int, - nargs="+", - default=defaults.pow_register.cuda.dev_id, - help="""Set the CUDA device id(s). Goes by the order of speed. (i.e. 0 is the fastest).""", - required=False, - ) - register_parser.add_argument( - "--pow_register.cuda.tpb", - "--cuda.tpb", - type=int, - default=defaults.pow_register.cuda.tpb, - help="""Set the number of Threads Per Block for CUDA.""", - required=False, - ) - - Wallet.add_args(register_parser) - Subtensor.add_args(register_parser) - - @staticmethod - def check_config(config: "Config"): - if ( - not config.is_set("subtensor.network") - and not config.is_set("subtensor.chain_endpoint") - and not config.no_prompt - ): - config.subtensor.network = Prompt.ask( - "Enter subtensor network", - choices=networks, - default=defaults.subtensor.network, - ) - _, endpoint = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - config.subtensor.chain_endpoint = endpoint - - check_netuid_set( - config, subtensor=Subtensor(config=config, log_verbose=False) - ) - - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - if not config.no_prompt: - check_for_cuda_reg_config(config) - - -class RunFaucetCommand: - """ - Executes the ``faucet`` command to obtain test TAO tokens by performing Proof of Work (PoW). - - IMPORTANT: - **THIS COMMAND IS CURRENTLY DISABLED.** - - This command is particularly useful for users who need test tokens for operations on the Bittensor testnet. - - Usage: - The command uses the PoW mechanism to validate the user's effort and rewards them with test TAO tokens. It is typically used in testnet environments where real value transactions are not necessary. - - Optional arguments: - - ``--faucet.num_processes`` (int): Specifies the number of processors to use for the PoW operation. A higher number of processors may increase the chances of successful computation. - - ``--faucet.update_interval`` (int): Sets the frequency of nonce processing before checking for the next block, which impacts the PoW operation's responsiveness. - - ``--faucet.no_output_in_place`` (bool): When set, it disables in-place output of registration statistics for cleaner log visibility. - - ``--faucet.verbose`` (bool): Enables verbose output for detailed statistical information during the PoW process. - - ``--faucet.cuda.use_cuda`` (bool): Activates the use of CUDA for GPU acceleration in the PoW process, suitable for CUDA-compatible GPUs. - - ``--faucet.cuda.no_cuda`` (bool): Disables the use of CUDA, opting for CPU-based calculations. - - ``--faucet.cuda.dev_id`` (int[]): Allows selection of specific CUDA device IDs for the operation, useful in multi-GPU setups. - - ``--faucet.cuda.tpb`` (int): Determines the number of Threads Per Block for CUDA operations, affecting GPU calculation efficiency. - - These options provide flexibility in configuring the PoW process according to the user's hardware capabilities and preferences. - - Example usage:: - - btcli wallet faucet --faucet.num_processes 4 --faucet.cuda.use_cuda - - Note: - This command is meant for use in testnet environments where users can experiment with the network without using real TAO tokens. - It's important for users to have the necessary hardware setup, especially when opting for CUDA-based GPU calculations. - - **THIS COMMAND IS CURRENTLY DISABLED.** - """ - - @staticmethod - def run(cli): - r"""Register neuron.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - RunFaucetCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Register neuron.""" - wallet = Wallet(config=cli.config) - success = subtensor.run_faucet( - wallet=wallet, - prompt=not cli.config.no_prompt, - tpb=cli.config.pow_register.cuda.get("tpb", None), - update_interval=cli.config.pow_register.get("update_interval", None), - num_processes=cli.config.pow_register.get("num_processes", None), - cuda=cli.config.pow_register.cuda.get( - "use_cuda", defaults.pow_register.cuda.use_cuda - ), - dev_id=cli.config.pow_register.cuda.get("dev_id", None), - output_in_place=cli.config.pow_register.get( - "output_in_place", defaults.pow_register.output_in_place - ), - log_verbose=cli.config.pow_register.get( - "verbose", defaults.pow_register.verbose - ), - ) - if not success: - logging.error("Faucet run failed.") - sys.exit(1) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - run_faucet_parser = parser.add_parser( - "faucet", help="""Perform PoW to receieve test TAO in your wallet.""" - ) - run_faucet_parser.add_argument( - "--faucet.num_processes", - "-n", - dest="pow_register.num_processes", - help="Number of processors to use for POW registration", - type=int, - default=defaults.pow_register.num_processes, - ) - run_faucet_parser.add_argument( - "--faucet.update_interval", - "--faucet.cuda.update_interval", - "--cuda.update_interval", - "-u", - help="The number of nonces to process before checking for next block during registration", - type=int, - default=defaults.pow_register.update_interval, - ) - run_faucet_parser.add_argument( - "--faucet.no_output_in_place", - "--no_output_in_place", - dest="pow_register.output_in_place", - help="Whether to not ouput the registration statistics in-place. Set flag to disable output in-place.", - action="store_false", - required=False, - default=defaults.pow_register.output_in_place, - ) - run_faucet_parser.add_argument( - "--faucet.verbose", - help="Whether to ouput the registration statistics verbosely.", - action="store_true", - required=False, - default=defaults.pow_register.verbose, - ) - - ## Registration args for CUDA registration. - run_faucet_parser.add_argument( - "--faucet.cuda.use_cuda", - "--cuda", - "--cuda.use_cuda", - dest="pow_register.cuda.use_cuda", - default=defaults.pow_register.cuda.use_cuda, - help="""Set flag to use CUDA to pow_register.""", - action="store_true", - required=False, - ) - run_faucet_parser.add_argument( - "--faucet.cuda.no_cuda", - "--no_cuda", - "--cuda.no_cuda", - dest="pow_register.cuda.use_cuda", - default=not defaults.pow_register.cuda.use_cuda, - help="""Set flag to not use CUDA for registration""", - action="store_false", - required=False, - ) - run_faucet_parser.add_argument( - "--faucet.cuda.dev_id", - "--cuda.dev_id", - type=int, - nargs="+", - default=defaults.pow_register.cuda.dev_id, - help="""Set the CUDA device id(s). Goes by the order of speed. (i.e. 0 is the fastest).""", - required=False, - ) - run_faucet_parser.add_argument( - "--faucet.cuda.tpb", - "--cuda.tpb", - type=int, - default=defaults.pow_register.cuda.tpb, - help="""Set the number of Threads Per Block for CUDA.""", - required=False, - ) - Wallet.add_args(run_faucet_parser) - Subtensor.add_args(run_faucet_parser) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.no_prompt: - check_for_cuda_reg_config(config) - - -class SwapHotkeyCommand: - @staticmethod - def run(cli): - """ - Executes the ``swap_hotkey`` command to swap the hotkeys for a neuron on the network. - - Usage: - The command is used to swap the hotkey of a wallet for another hotkey on that same wallet. - - Optional arguments: - - ``--wallet.name`` (str): Specifies the wallet for which the hotkey is to be swapped. - - ``--wallet.hotkey`` (str): The original hotkey name that is getting swapped out. - - ``--wallet.hotkey_b`` (str): The new hotkey name for which the old is getting swapped out for. - - Example usage:: - - btcli wallet swap_hotkey --wallet.name your_wallet_name --wallet.hotkey original_hotkey --wallet.hotkey_b new_hotkey - """ - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - SwapHotkeyCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Swap your hotkey for all registered axons on the network.""" - wallet = Wallet(config=cli.config) - - # This creates an unnecessary amount of extra data, but simplifies implementation. - new_config = deepcopy(cli.config) - new_config.wallet.hotkey = new_config.wallet.hotkey_b - new_wallet = Wallet(config=new_config) - - subtensor.swap_hotkey( - wallet=wallet, - new_wallet=new_wallet, - wait_for_finalization=False, - wait_for_inclusion=True, - prompt=False, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - swap_hotkey_parser = parser.add_parser( - "swap_hotkey", help="""Swap your associated hotkey.""" - ) - - swap_hotkey_parser.add_argument( - "--wallet.hotkey_b", - type=str, - default=defaults.wallet.hotkey, - help="""Name of the new hotkey""", - required=False, - ) - - Wallet.add_args(swap_hotkey_parser) - Subtensor.add_args(swap_hotkey_parser) - - @staticmethod - def check_config(config: "Config"): - if ( - not config.is_set("subtensor.network") - and not config.is_set("subtensor.chain_endpoint") - and not config.no_prompt - ): - config.subtensor.network = Prompt.ask( - "Enter subtensor network", - choices=networks, - default=defaults.subtensor.network, - ) - _, endpoint = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - config.subtensor.chain_endpoint = endpoint - - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter old hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - if not config.is_set("wallet.hotkey_b") and not config.no_prompt: - hotkey = Prompt.ask("Enter new hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey_b = str(hotkey) diff --git a/bittensor/btcli/commands/root.py b/bittensor/btcli/commands/root.py deleted file mode 100644 index 12d8725a9..000000000 --- a/bittensor/btcli/commands/root.py +++ /dev/null @@ -1,688 +0,0 @@ -# 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 re -import typing -import argparse -import numpy as np - -from typing import List, Optional, Dict -from rich.console import Console -from rich.prompt import Prompt -from rich.table import Table -from .utils import get_delegates_details, DelegatesDetails - -from . import defaults -from bittensor.core.settings import bt_console, delegates_details_url -from bittensor.core.subtensor import Subtensor -from bittensor.core.config import Config -from bittensor_wallet import Wallet -from bittensor.utils.btlogging import logging -from bittensor.core.chain_data import NeuronInfoLite, SubnetInfo - -console = Console() - - -class RootRegisterCommand: - """ - Executes the ``register`` command to register a wallet to the root network of the Bittensor network. - - This command is used to formally acknowledge a wallet's participation in the network's root layer. - - Usage: - The command registers the user's wallet with the root network, which is a crucial step for participating in network governance and other advanced functions. - - Optional arguments: - - None. The command primarily uses the wallet and subtensor configurations. - - Example usage:: - - btcli root register - - Note: - This command is important for users seeking to engage deeply with the Bittensor network, particularly in aspects related to network governance and decision-making. - - It is a straightforward process but requires the user to have an initialized and configured wallet. - """ - - @staticmethod - def run(cli): - r"""Register to root network.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - RootRegisterCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Register to root network.""" - wallet = Wallet(config=cli.config) - - subtensor.root_register(wallet=wallet, prompt=not cli.config.no_prompt) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "register", help="""Register a wallet to the root network.""" - ) - - Wallet.add_args(parser) - Subtensor.add_args(parser) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - -class RootList: - """ - Executes the ``list`` command to display the members of the root network on the Bittensor network. - - This command provides an overview of the neurons that constitute the network's foundational layer. - - Usage: - Upon execution, the command fetches and lists the neurons in the root network, showing their unique identifiers (UIDs), names, addresses, stakes, and whether they are part of the senate (network governance body). - - Optional arguments: - - None. The command uses the subtensor configuration to retrieve data. - - Example usage:: - - $ btcli root list - - UID NAME ADDRESS STAKE(τ) SENATOR - 0 5CaCUPsSSdKWcMJbmdmJdnWVa15fJQuz5HsSGgVdZffpHAUa 27086.37070 Yes - 1 RaoK9 5GmaAk7frPXnAxjbQvXcoEzMGZfkrDee76eGmKoB3wxUburE 520.24199 No - 2 Openτensor Foundaτion 5F4tQyWrhfGVcNhoqeiNsR6KjD4wMZ2kfhLj4oHYuyHbZAc3 1275437.45895 Yes - 3 RoundTable21 5FFApaS75bv5pJHfAp2FVLBj9ZaXuFDjEypsaBNc1wCfe52v 84718.42095 Yes - 4 5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN 168897.40859 Yes - 5 Rizzo 5CXRfP2ekFhe62r7q3vppRajJmGhTi7vwvb2yr79jveZ282w 53383.34400 No - 6 τaosτaτs and BitAPAI 5Hddm3iBFD2GLT5ik7LZnT3XJUnRnN8PoeCFgGQgawUVKNm8 646944.73569 Yes - ... - - Note: - This command is useful for users interested in understanding the composition and governance structure of the Bittensor network's root layer. It provides insights into which neurons hold significant influence and responsibility within the network. - """ - - @staticmethod - def run(cli): - r"""List the root network""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - RootList._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""List the root network""" - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ) - - senate_members = subtensor.get_senate_members() - root_neurons: typing.List["NeuronInfoLite"] = subtensor.neurons_lite( - netuid=0 - ) - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - url=delegates_details_url - ) - - table = Table(show_footer=False) - table.title = "[white]Root Network" - table.add_column( - "[overline white]UID", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - table.add_column( - "[overline white]NAME", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - table.add_column( - "[overline white]ADDRESS", - footer_style="overline white", - style="yellow", - no_wrap=True, - ) - table.add_column( - "[overline white]STAKE(\u03c4)", - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]SENATOR", - footer_style="overline white", - style="green", - no_wrap=True, - ) - table.show_footer = True - - for neuron_data in root_neurons: - table.add_row( - str(neuron_data.uid), - ( - delegate_info[neuron_data.hotkey].name - if neuron_data.hotkey in delegate_info - else "" - ), - neuron_data.hotkey, - "{:.5f}".format( - float(subtensor.get_total_stake_for_hotkey(neuron_data.hotkey)) - ), - "Yes" if neuron_data.hotkey in senate_members else "No", - ) - - table.box = None - table.pad_edge = False - table.width = None - bt_console.print(table) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser("list", help="""List the root network""") - Subtensor.add_args(parser) - - @staticmethod - def check_config(config: "Config"): - pass - - -class RootSetBoostCommand: - """ - Executes the ``boost`` command to boost the weights for a specific subnet within the root network on the Bittensor network. - - Usage: - The command allows boosting the weights for different subnets within the root network. - - Optional arguments: - - ``--netuid`` (int): A single netuid for which weights are to be boosted. - - ``--increase`` (float): The cooresponding increase in the weight for this subnet. - - Example usage:: - - $ btcli root boost --netuid 1 --increase 0.01 - - Enter netuid (e.g. 1): 1 - Enter amount (e.g. 0.01): 0.1 - Boosting weight for subnet: 1 by amount: 0.1 - - Normalized weights: - tensor([ - 0.0000, 0.5455, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.4545, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]) -> tensor([0.0000, 0.5455, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.4545, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000] - ) - - Do you want to set the following root weights?: - weights: tensor([ - 0.0000, 0.5455, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.4545, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]) - uids: tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, - 36, 37, 38, 39, 40])? [y/n]: y - True None - ✅ Finalized - ⠙ 📡 Setting root weights on test ...2023-11-28 22:09:14.001 | SUCCESS | Set weights Finalized: True - - """ - - @staticmethod - def run(cli): - r"""Set weights for root network.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - RootSetBoostCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Set weights for root network.""" - wallet = Wallet(config=cli.config) - - root = subtensor.metagraph(0, lite=False) - try: - my_uid = root.hotkeys.index(wallet.hotkey.ss58_address) - except ValueError: - bt_console.print( - "Wallet hotkey: {} not found in root metagraph".format(wallet.hotkey) - ) - exit() - my_weights = root.weights[my_uid] - prev_weight = my_weights[cli.config.netuid] - new_weight = prev_weight + cli.config.amount - - bt_console.print( - f"Boosting weight for netuid {cli.config.netuid} from {prev_weight} -> {new_weight}" - ) - my_weights[cli.config.netuid] = new_weight - all_netuids = np.arange(len(my_weights)) - - bt_console.print("Setting root weights...") - subtensor.root_set_weights( - wallet=wallet, - netuids=all_netuids, - weights=my_weights, - version_key=0, - prompt=not cli.config.no_prompt, - wait_for_finalization=True, - wait_for_inclusion=True, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "boost", help="""Boost weight for a specific subnet by increase amount.""" - ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) - parser.add_argument("--increase", dest="amount", type=float, required=False) - - Wallet.add_args(parser) - Subtensor.add_args(parser) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - if not config.is_set("netuid") and not config.no_prompt: - config.netuid = int(Prompt.ask(f"Enter netuid (e.g. 1)")) - if not config.is_set("amount") and not config.no_prompt: - config.amount = float(Prompt.ask(f"Enter amount (e.g. 0.01)")) - - -class RootSetSlashCommand: - """ - Executes the ``slash`` command to decrease the weights for a specific subnet within the root network on the Bittensor network. - - Usage: - The command allows slashing (decreasing) the weights for different subnets within the root network. - - Optional arguments: - - ``--netuid`` (int): A single netuid for which weights are to be slashed. - - ``--decrease`` (float): The corresponding decrease in the weight for this subnet. - - Example usage:: - - $ btcli root slash --netuid 1 --decrease 0.01 - - Enter netuid (e.g. 1): 1 - Enter decrease amount (e.g. 0.01): 0.2 - Slashing weight for subnet: 1 by amount: 0.2 - - Normalized weights: - tensor([ - 0.0000, 0.4318, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.5682, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]) -> tensor([ - 0.0000, 0.4318, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.5682, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000] - ) - - Do you want to set the following root weights?: - weights: tensor([ - 0.0000, 0.4318, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.5682, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]) - uids: tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, - 36, 37, 38, 39, 40])? [y/n]: y - ⠙ 📡 Setting root weights on test ...2023-11-28 22:09:14.001 | SUCCESS | Set weights Finalized: True - """ - - @staticmethod - def run(cli): - """Set weights for root network with decreased values.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - RootSetSlashCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - wallet = Wallet(config=cli.config) - - bt_console.print( - "Slashing weight for subnet: {} by amount: {}".format( - cli.config.netuid, cli.config.amount - ) - ) - root = subtensor.metagraph(0, lite=False) - try: - my_uid = root.hotkeys.index(wallet.hotkey.ss58_address) - except ValueError: - bt_console.print( - "Wallet hotkey: {} not found in root metagraph".format(wallet.hotkey) - ) - exit() - my_weights = root.weights[my_uid] - my_weights[cli.config.netuid] -= cli.config.amount - my_weights[my_weights < 0] = 0 # Ensure weights don't go negative - all_netuids = np.arange(len(my_weights)) - - subtensor.root_set_weights( - wallet=wallet, - netuids=all_netuids, - weights=my_weights, - version_key=0, - prompt=not cli.config.no_prompt, - wait_for_finalization=True, - wait_for_inclusion=True, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "slash", help="""Slash weight for a specific subnet by decrease amount.""" - ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) - parser.add_argument("--decrease", dest="amount", type=float, required=False) - - Wallet.add_args(parser) - Subtensor.add_args(parser) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - if not config.is_set("netuid") and not config.no_prompt: - config.netuid = int(Prompt.ask(f"Enter netuid (e.g. 1)")) - if not config.is_set("amount") and not config.no_prompt: - config.amount = float(Prompt.ask(f"Enter decrease amount (e.g. 0.01)")) - - -class RootSetWeightsCommand: - """ - Executes the ``weights`` command to set the weights for the root network on the Bittensor network. - - This command is used by network senators to influence the distribution of network rewards and responsibilities. - - Usage: - The command allows setting weights for different subnets within the root network. Users need to specify the netuids (network unique identifiers) and corresponding weights they wish to assign. - - Optional arguments: - - ``--netuids`` (str): A comma-separated list of netuids for which weights are to be set. - - ``--weights`` (str): Corresponding weights for the specified netuids, in comma-separated format. - - Example usage:: - - btcli root weights --netuids 1,2,3 --weights 0.3,0.3,0.4 - - Note: - This command is particularly important for network senators and requires a comprehensive understanding of the network's dynamics. - It is a powerful tool that directly impacts the network's operational mechanics and reward distribution. - """ - - @staticmethod - def run(cli): - r"""Set weights for root network.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - RootSetWeightsCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Set weights for root network.""" - wallet = Wallet(config=cli.config) - subnets: List["SubnetInfo"] = subtensor.get_all_subnets_info() - - # Get values if not set. - if not cli.config.is_set("netuids"): - example = ( - ", ".join(map(str, [subnet.netuid for subnet in subnets][:3])) + " ..." - ) - cli.config.netuids = Prompt.ask(f"Enter netuids (e.g. {example})") - - if not cli.config.is_set("weights"): - example = ( - ", ".join( - map( - str, - [ - "{:.2f}".format(float(1 / len(subnets))) - for subnet in subnets - ][:3], - ) - ) - + " ..." - ) - cli.config.weights = Prompt.ask(f"Enter weights (e.g. {example})") - - # Parse from string - matched_netuids = list(map(int, re.split(r"[ ,]+", cli.config.netuids))) - netuids = np.array(matched_netuids, dtype=np.int64) - - matched_weights = [ - float(weight) for weight in re.split(r"[ ,]+", cli.config.weights) - ] - weights = np.array(matched_weights, dtype=np.float32) - - # Run the set weights operation. - subtensor.root_set_weights( - wallet=wallet, - netuids=netuids, - weights=weights, - version_key=0, - prompt=not cli.config.no_prompt, - wait_for_finalization=True, - wait_for_inclusion=True, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser("weights", help="""Set weights for root network.""") - parser.add_argument("--netuids", dest="netuids", type=str, required=False) - parser.add_argument("--weights", dest="weights", type=str, required=False) - - Wallet.add_args(parser) - Subtensor.add_args(parser) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - -class RootGetWeightsCommand: - """ - Executes the ``get_weights`` command to retrieve the weights set for the root network on the Bittensor network. - - This command provides visibility into how network responsibilities and rewards are distributed among various subnets. - - Usage: - The command outputs a table listing the weights assigned to each subnet within the root network. This information is crucial for understanding the current influence and reward distribution among the subnets. - - Optional arguments: - - None. The command fetches weight information based on the subtensor configuration. - - Example usage:: - - $ btcli root get_weights - - Root Network Weights - UID 0 1 2 3 4 5 8 9 11 13 18 19 - 1 100.00% - - - - - - - - - - - - 2 - 40.00% 5.00% 10.00% 10.00% 10.00% 10.00% 5.00% - - 10.00% - - 3 - - 25.00% - 25.00% - 25.00% - - - 25.00% - - 4 - - 7.00% 7.00% 20.00% 20.00% 20.00% - 6.00% - 20.00% - - 5 - 20.00% - 10.00% 15.00% 15.00% 15.00% 5.00% - - 10.00% 10.00% - 6 - - - - 10.00% 10.00% 25.00% 25.00% - - 30.00% - - 7 - 60.00% - - 20.00% - - - 20.00% - - - - 8 - 49.35% - 7.18% 13.59% 21.14% 1.53% 0.12% 7.06% 0.03% - - - 9 100.00% - - - - - - - - - - - - ... - - Note: - This command is essential for users interested in the governance and operational dynamics of the Bittensor network. It offers transparency into how network rewards and responsibilities are allocated across different subnets. - """ - - @staticmethod - def run(cli): - r"""Get weights for root network.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - RootGetWeightsCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - """Get weights for root network.""" - weights = subtensor.weights(0) - - table = Table(show_footer=False) - table.title = "[white]Root Network Weights" - table.add_column( - "[white]UID", - header_style="overline white", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - - uid_to_weights = {} - netuids = set() - for matrix in weights: - [uid, weights_data] = matrix - - if not len(weights_data): - uid_to_weights[uid] = {} - normalized_weights = [] - else: - normalized_weights = np.array(weights_data)[:, 1] / max( - np.sum(weights_data, axis=0)[1], 1 - ) - - for weight_data, normalized_weight in zip(weights_data, normalized_weights): - [netuid, _] = weight_data - netuids.add(netuid) - if uid not in uid_to_weights: - uid_to_weights[uid] = {} - - uid_to_weights[uid][netuid] = normalized_weight - - for netuid in netuids: - table.add_column( - f"[white]{netuid}", - header_style="overline white", - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - - for uid in uid_to_weights: - row = [str(uid)] - - uid_weights = uid_to_weights[uid] - for netuid in netuids: - if netuid in uid_weights: - normalized_weight = uid_weights[netuid] - row.append("{:0.2f}%".format(normalized_weight * 100)) - else: - row.append("-") - table.add_row(*row) - - table.show_footer = True - - table.box = None - table.pad_edge = False - table.width = None - bt_console.print(table) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "get_weights", help="""Get weights for root network.""" - ) - - Wallet.add_args(parser) - Subtensor.add_args(parser) - - @staticmethod - def check_config(config: "Config"): - pass diff --git a/bittensor/btcli/commands/senate.py b/bittensor/btcli/commands/senate.py deleted file mode 100644 index 0e198f28a..000000000 --- a/bittensor/btcli/commands/senate.py +++ /dev/null @@ -1,657 +0,0 @@ -# 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 -from typing import Optional, Dict - -from bittensor_wallet import Wallet -from rich.console import Console -from rich.prompt import Prompt, Confirm -from rich.table import Table - -from bittensor.btcli.commands.utils import get_delegates_details, DelegatesDetails -from bittensor.core.config import Config -from bittensor.core.settings import bt_console, delegates_details_url -from bittensor.core.subtensor import Subtensor -from bittensor.utils.btlogging import logging -from . import defaults - -console = Console() - - -class SenateCommand: - """ - Executes the ``senate`` command to view the members of Bittensor's governance protocol, known as the Senate. - - This command lists the delegates involved in the decision-making process of the Bittensor network. - - Usage: - The command retrieves and displays a list of Senate members, showing their names and wallet addresses. - This information is crucial for understanding who holds governance roles within the network. - - Example usage:: - - btcli root senate - - Note: - This command is particularly useful for users interested in the governance structure and participants of the Bittensor network. It provides transparency into the network's decision-making body. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""View Bittensor's governance protocol proposals""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - SenateCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "Subtensor"): - r"""View Bittensor's governance protocol proposals""" - console = bt_console - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - cli.config.subtensor.network - ) - ) - - senate_members = subtensor.get_senate_members() - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - url=delegates_details_url - ) - - table = Table(show_footer=False) - table.title = "[white]Senate" - table.add_column( - "[overline white]NAME", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - table.add_column( - "[overline white]ADDRESS", - footer_style="overline white", - style="yellow", - no_wrap=True, - ) - table.show_footer = True - - for ss58_address in senate_members: - table.add_row( - ( - delegate_info[ss58_address].name - if ss58_address in delegate_info - else "" - ), - ss58_address, - ) - - table.box = None - table.pad_edge = False - table.width = None - console.print(table) - - @classmethod - def check_config(cls, config: "Config"): - None - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - senate_parser = parser.add_parser( - "senate", help="""View senate and it's members""" - ) - - Wallet.add_args(senate_parser) - Subtensor.add_args(senate_parser) - - -def format_call_data(call_data: "bittensor.ProposalCallData") -> str: - human_call_data = list() - - for arg in call_data["call_args"]: - arg_value = arg["value"] - - # If this argument is a nested call - func_args = ( - format_call_data( - { - "call_function": arg_value["call_function"], - "call_args": arg_value["call_args"], - } - ) - if isinstance(arg_value, dict) and "call_function" in arg_value - else str(arg_value) - ) - - human_call_data.append("{}: {}".format(arg["name"], func_args)) - - return "{}({})".format(call_data["call_function"], ", ".join(human_call_data)) - - -def display_votes( - vote_data: "bittensor.ProposalVoteData", delegate_info: "bittensor.DelegateInfo" -) -> str: - vote_list = list() - - for address in vote_data["ayes"]: - vote_list.append( - "{}: {}".format( - delegate_info[address].name if address in delegate_info else address, - "[bold green]Aye[/bold green]", - ) - ) - - for address in vote_data["nays"]: - vote_list.append( - "{}: {}".format( - delegate_info[address].name if address in delegate_info else address, - "[bold red]Nay[/bold red]", - ) - ) - - return "\n".join(vote_list) - - -class ProposalsCommand: - """ - Executes the ``proposals`` command to view active proposals within Bittensor's governance protocol. - - This command displays the details of ongoing proposals, including votes, thresholds, and proposal data. - - Usage: - The command lists all active proposals, showing their hash, voting threshold, number of ayes and nays, detailed votes by address, end block number, and call data associated with each proposal. - - Example usage:: - - btcli root proposals - - Note: - This command is essential for users who are actively participating in or monitoring the governance of the Bittensor network. - It provides a detailed view of the proposals being considered, along with the community's response to each. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""View Bittensor's governance protocol proposals""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - ProposalsCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "Subtensor"): - r"""View Bittensor's governance protocol proposals""" - console = bt_console - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ) - - senate_members = subtensor.get_senate_members() - proposals = subtensor.get_proposals() - - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=delegates_details_url) - ) - - table = Table(show_footer=False) - table.title = ( - "[white]Proposals\t\tActive Proposals: {}\t\tSenate Size: {}".format( - len(proposals), len(senate_members) - ) - ) - table.add_column( - "[overline white]HASH", - footer_style="overline white", - style="yellow", - no_wrap=True, - ) - table.add_column( - "[overline white]THRESHOLD", footer_style="overline white", style="white" - ) - table.add_column( - "[overline white]AYES", footer_style="overline white", style="green" - ) - table.add_column( - "[overline white]NAYS", footer_style="overline white", style="red" - ) - table.add_column( - "[overline white]VOTES", - footer_style="overline white", - style="rgb(50,163,219)", - ) - table.add_column( - "[overline white]END", footer_style="overline white", style="blue" - ) - table.add_column( - "[overline white]CALLDATA", footer_style="overline white", style="white" - ) - table.show_footer = True - - for hash in proposals: - call_data, vote_data = proposals[hash] - - table.add_row( - hash, - str(vote_data["threshold"]), - str(len(vote_data["ayes"])), - str(len(vote_data["nays"])), - display_votes(vote_data, registered_delegate_info), - str(vote_data["end"]), - format_call_data(call_data), - ) - - table.box = None - table.pad_edge = False - table.width = None - console.print(table) - - @classmethod - def check_config(cls, config: "Config"): - None - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - proposals_parser = parser.add_parser( - "proposals", help="""View active triumvirate proposals and their status""" - ) - - Wallet.add_args(proposals_parser) - Subtensor.add_args(proposals_parser) - - -class ShowVotesCommand: - """ - Executes the ``proposal_votes`` command to view the votes for a specific proposal in Bittensor's governance protocol. - - IMPORTANT - **THIS COMMAND IS DEPRECATED**. Use ``btcli root proposals`` to see vote status. - - This command provides a detailed breakdown of the votes cast by the senators for a particular proposal. - - Usage: - Users need to specify the hash of the proposal they are interested in. The command then displays the voting addresses and their respective votes (Aye or Nay) for the specified proposal. - - Optional arguments: - - ``--proposal`` (str): The hash of the proposal for which votes need to be displayed. - - Example usage:: - - btcli root proposal_votes --proposal - - Note: - This command is crucial for users seeking detailed insights into the voting behavior of the Senate on specific governance proposals. - It helps in understanding the level of consensus or disagreement within the Senate on key decisions. - - **THIS COMMAND IS DEPRECATED**. Use ``btcli root proposals`` to see vote status. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""View Bittensor's governance protocol proposals active votes""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - ShowVotesCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "Subtensor"): - r"""View Bittensor's governance protocol proposals active votes""" - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - cli.config.subtensor.network - ) - ) - - proposal_hash = cli.config.proposal_hash - if len(proposal_hash) == 0: - console.print( - 'Aborting: Proposal hash not specified. View all proposals with the "proposals" command.' - ) - return - - proposal_vote_data = subtensor.get_vote_data(proposal_hash) - if proposal_vote_data is None: - console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") - return - - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=delegates_details_url) - ) - - table = Table(show_footer=False) - table.title = "[white]Votes for Proposal {}".format(proposal_hash) - table.add_column( - "[overline white]ADDRESS", - footer_style="overline white", - style="yellow", - no_wrap=True, - ) - table.add_column( - "[overline white]VOTE", footer_style="overline white", style="white" - ) - table.show_footer = True - - votes = display_votes(proposal_vote_data, registered_delegate_info).split("\n") - for vote in votes: - split_vote_data = vote.split(": ") # Nasty, but will work. - table.add_row(split_vote_data[0], split_vote_data[1]) - - table.box = None - table.pad_edge = False - table.min_width = 64 - console.print(table) - - @classmethod - def check_config(cls, config: "Config"): - if config.proposal_hash == "" and not config.no_prompt: - proposal_hash = Prompt.ask("Enter proposal hash") - config.proposal_hash = str(proposal_hash) - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - show_votes_parser = parser.add_parser( - "proposal_votes", help="""View an active proposal's votes by address.""" - ) - show_votes_parser.add_argument( - "--proposal", - dest="proposal_hash", - type=str, - nargs="?", - help="""Set the proposal to show votes for.""", - default="", - ) - Wallet.add_args(show_votes_parser) - Subtensor.add_args(show_votes_parser) - - -class SenateRegisterCommand: - """ - Executes the ``senate_register`` command to register as a member of the Senate in Bittensor's governance protocol. - - This command is used by delegates who wish to participate in the governance and decision-making process of the network. - - Usage: - The command checks if the user's hotkey is a delegate and not already a Senate member before registering them to the Senate. - Successful execution allows the user to participate in proposal voting and other governance activities. - - Example usage:: - - btcli root senate_register - - Note: - This command is intended for delegates who are interested in actively participating in the governance of the Bittensor network. - It is a significant step towards engaging in network decision-making processes. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Register to participate in Bittensor's governance protocol proposals""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - SenateRegisterCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "Subtensor"): - r"""Register to participate in Bittensor's governance protocol proposals""" - wallet = Wallet(config=cli.config) - - # Unlock the wallet. - wallet.hotkey - wallet.coldkey - - # Check if the hotkey is a delegate. - if not subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - console.print( - "Aborting: Hotkey {} isn't a delegate.".format( - wallet.hotkey.ss58_address - ) - ) - return - - if subtensor.is_senate_member(hotkey_ss58=wallet.hotkey.ss58_address): - console.print( - "Aborting: Hotkey {} is already a senate member.".format( - wallet.hotkey.ss58_address - ) - ) - return - - subtensor.register_senate(wallet=wallet, prompt=not cli.config.no_prompt) - - @classmethod - def check_config(cls, config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - senate_register_parser = parser.add_parser( - "senate_register", - help="""Register as a senate member to participate in proposals""", - ) - - Wallet.add_args(senate_register_parser) - Subtensor.add_args(senate_register_parser) - - -class SenateLeaveCommand: - """ - Executes the ``senate_leave`` command to discard membership in Bittensor's Senate. - - This command allows a Senate member to voluntarily leave the governance body. - - Usage: - The command checks if the user's hotkey is currently a Senate member before processing the request to leave the Senate. - It effectively removes the user from participating in future governance decisions. - - Example usage:: - - btcli root senate_leave - - Note: - This command is relevant for Senate members who wish to step down from their governance responsibilities within the Bittensor network. - It should be used when a member no longer desires to participate in the Senate activities. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Discard membership in Bittensor's governance protocol proposals""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - SenateLeaveCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.cli"): - r"""Discard membership in Bittensor's governance protocol proposals""" - wallet = Wallet(config=cli.config) - - # Unlock the wallet. - wallet.hotkey - wallet.coldkey - - if not subtensor.is_senate_member(hotkey_ss58=wallet.hotkey.ss58_address): - console.print( - "Aborting: Hotkey {} isn't a senate member.".format( - wallet.hotkey.ss58_address - ) - ) - return - - subtensor.leave_senate(wallet=wallet, prompt=not cli.config.no_prompt) - - @classmethod - def check_config(cls, config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - senate_leave_parser = parser.add_parser( - "senate_leave", - help="""Discard senate membership in the governance protocol""", - ) - - Wallet.add_args(senate_leave_parser) - Subtensor.add_args(senate_leave_parser) - - -class VoteCommand: - """ - Executes the ``senate_vote`` command to cast a vote on an active proposal in Bittensor's governance protocol. - - This command is used by Senate members to vote on various proposals that shape the network's future. - - Usage: - The user needs to specify the hash of the proposal they want to vote on. The command then allows the Senate member to cast an 'Aye' or 'Nay' vote, contributing to the decision-making process. - - Optional arguments: - - ``--proposal`` (str): The hash of the proposal to vote on. - - Example usage:: - - btcli root senate_vote --proposal - - Note: - This command is crucial for Senate members to exercise their voting rights on key proposals. It plays a vital role in the governance and evolution of the Bittensor network. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Vote in Bittensor's governance protocol proposals""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - VoteCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "Subtensor"): - r"""Vote in Bittensor's governance protocol proposals""" - wallet = Wallet(config=cli.config) - - proposal_hash = cli.config.proposal_hash - if len(proposal_hash) == 0: - console.print( - 'Aborting: Proposal hash not specified. View all proposals with the "proposals" command.' - ) - return - - if not subtensor.is_senate_member(hotkey_ss58=wallet.hotkey.ss58_address): - console.print( - "Aborting: Hotkey {} isn't a senate member.".format( - wallet.hotkey.ss58_address - ) - ) - return - - # Unlock the wallet. - wallet.hotkey - wallet.coldkey - - vote_data = subtensor.get_vote_data(proposal_hash) - if vote_data == None: - console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") - return - - vote = Confirm.ask("Desired vote for proposal") - subtensor.vote_senate( - wallet=wallet, - proposal_hash=proposal_hash, - proposal_idx=vote_data["index"], - vote=vote, - prompt=not cli.config.no_prompt, - ) - - @classmethod - def check_config(cls, config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - if config.proposal_hash == "" and not config.no_prompt: - proposal_hash = Prompt.ask("Enter proposal hash") - config.proposal_hash = str(proposal_hash) - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - vote_parser = parser.add_parser( - "senate_vote", help="""Vote on an active proposal by hash.""" - ) - vote_parser.add_argument( - "--proposal", - dest="proposal_hash", - type=str, - nargs="?", - help="""Set the proposal to show votes for.""", - default="", - ) - Wallet.add_args(vote_parser) - Subtensor.add_args(vote_parser) diff --git a/bittensor/btcli/commands/stake.py b/bittensor/btcli/commands/stake.py deleted file mode 100644 index 2e9d96c0c..000000000 --- a/bittensor/btcli/commands/stake.py +++ /dev/null @@ -1,571 +0,0 @@ -# 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 os -import sys -from typing import List, Union, Optional, Dict, Tuple - -from bittensor_wallet import Wallet -from rich.prompt import Confirm, Prompt -from rich.table import Table -from tqdm import tqdm - -from bittensor.core.config import Config -from bittensor.core.settings import bt_console, tao_symbol, delegates_details_url -from bittensor.core.subtensor import Subtensor -from bittensor.utils import is_valid_ss58_address -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging -from . import defaults -from .utils import ( - get_hotkey_wallets_for_wallet, - get_delegates_details, - DelegatesDetails, -) - - -class StakeCommand: - """ - Executes the ``add`` command to stake tokens to one or more hotkeys from a user's coldkey on the Bittensor network. - - This command is used to allocate tokens to different hotkeys, securing their position and influence on the network. - - Usage: - Users can specify the amount to stake, the hotkeys to stake to (either by name or ``SS58`` address), and whether to stake to all hotkeys. The command checks for sufficient balance and hotkey registration - before proceeding with the staking process. - - Optional arguments: - - ``--all`` (bool): When set, stakes all available tokens from the coldkey. - - ``--uid`` (int): The unique identifier of the neuron to which the stake is to be added. - - ``--amount`` (float): The amount of TAO tokens to stake. - - ``--max_stake`` (float): Sets the maximum amount of TAO to have staked in each hotkey. - - ``--hotkeys`` (list): Specifies hotkeys by name or SS58 address to stake to. - - ``--all_hotkeys`` (bool): When set, stakes to all hotkeys associated with the wallet, excluding any specified in --hotkeys. - - The command prompts for confirmation before executing the staking operation. - - Example usage:: - - btcli stake add --amount 100 --wallet.name --wallet.hotkey - - Note: - This command is critical for users who wish to distribute their stakes among different neurons (hotkeys) on the network. - It allows for a strategic allocation of tokens to enhance network participation and influence. - """ - - @staticmethod - def run(cli): - """Stake token of amount to hotkey(s).""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - StakeCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Stake token of amount to hotkey(s).""" - config = cli.config.copy() - wallet = Wallet(config=config) - - # Get the hotkey_names (if any) and the hotkey_ss58s. - hotkeys_to_stake_to: List[Tuple[Optional[str], str]] = [] - if config.get("all_hotkeys"): - # Stake to all hotkeys. - all_hotkeys: List[Wallet] = get_hotkey_wallets_for_wallet( - wallet=wallet - ) - # Get the hotkeys to exclude. (d)efault to no exclusions. - hotkeys_to_exclude: List[str] = cli.config.get("hotkeys", d=[]) - # Exclude hotkeys that are specified. - hotkeys_to_stake_to = [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys - if wallet.hotkey_str not in hotkeys_to_exclude - ] # definitely wallets - - elif config.get("hotkeys"): - # Stake to specific hotkeys. - for hotkey_ss58_or_hotkey_name in config.get("hotkeys"): - if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): - # If the hotkey is a valid ss58 address, we add it to the list. - hotkeys_to_stake_to.append((None, hotkey_ss58_or_hotkey_name)) - else: - # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. - # We then get the hotkey from the wallet and add it to the list. - wallet_ = Wallet( - config=config, hotkey=hotkey_ss58_or_hotkey_name - ) - hotkeys_to_stake_to.append( - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ) - elif config.wallet.get("hotkey"): - # Only config.wallet.hotkey is specified. - # so we stake to that single hotkey. - hotkey_ss58_or_name = config.wallet.get("hotkey") - if is_valid_ss58_address(hotkey_ss58_or_name): - hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] - else: - # Hotkey is not a valid ss58 address, so we assume it is a hotkey name. - wallet_ = Wallet(config=config, hotkey=hotkey_ss58_or_name) - hotkeys_to_stake_to = [ - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ] - else: - # Only config.wallet.hotkey is specified. - # so we stake to that single hotkey. - assert config.wallet.hotkey is not None - hotkeys_to_stake_to = [ - (None, Wallet(config=config).hotkey.ss58_address) - ] - - # Get coldkey balance - wallet_balance: Balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - final_hotkeys: List[Tuple[str, str]] = [] - final_amounts: List[Union[float, Balance]] = [] - for hotkey in tqdm(hotkeys_to_stake_to): - hotkey: Tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) - if not subtensor.is_hotkey_registered_any(hotkey_ss58=hotkey[1]): - # Hotkey is not registered. - if len(hotkeys_to_stake_to) == 1: - # Only one hotkey, error - bt_console.print( - f"[red]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Aborting.[/red]" - ) - return None - else: - # Otherwise, print warning and skip - bt_console.print( - f"[yellow]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Skipping.[/yellow]" - ) - continue - - stake_amount_tao: float = config.get("amount") - if config.get("max_stake"): - # Get the current stake of the hotkey from this coldkey. - hotkey_stake: Balance = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey[1], coldkey_ss58=wallet.coldkeypub.ss58_address - ) - stake_amount_tao: float = config.get("max_stake") - hotkey_stake.tao - - # If the max_stake is greater than the current wallet balance, stake the entire balance. - stake_amount_tao: float = min(stake_amount_tao, wallet_balance.tao) - if ( - stake_amount_tao <= 0.00001 - ): # Threshold because of fees, might create a loop otherwise - # Skip hotkey if max_stake is less than current stake. - continue - wallet_balance = Balance.from_tao(wallet_balance.tao - stake_amount_tao) - - if wallet_balance.tao < 0: - # No more balance to stake. - break - - final_amounts.append(stake_amount_tao) - final_hotkeys.append(hotkey) # add both the name and the ss58 address. - - if len(final_hotkeys) == 0: - # No hotkeys to stake to. - bt_console.print( - "Not enough balance to stake to any hotkeys or max_stake is less than current stake." - ) - return None - - # Ask to stake - if not config.no_prompt: - if not Confirm.ask( - f"Do you want to stake to the following keys from {wallet.name}:\n" - + "".join( - [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {tao_symbol}' if amount else 'All'}[/bold white]\n" - for hotkey, amount in zip(final_hotkeys, final_amounts) - ] - ) - ): - return None - - if len(final_hotkeys) == 1: - # do regular stake - return subtensor.add_stake( - wallet=wallet, - hotkey_ss58=final_hotkeys[0][1], - amount=None if config.get("stake_all") else final_amounts[0], - wait_for_inclusion=True, - prompt=not config.no_prompt, - ) - - subtensor.add_stake_multiple( - wallet=wallet, - hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], - amounts=None if config.get("stake_all") else final_amounts, - wait_for_inclusion=True, - prompt=False, - ) - - @classmethod - def check_config(cls, config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if ( - not config.is_set("wallet.hotkey") - and not config.no_prompt - and not config.wallet.get("all_hotkeys") - and not config.wallet.get("hotkeys") - ): - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - # Get amount. - if ( - not config.get("amount") - and not config.get("stake_all") - and not config.get("max_stake") - ): - if not Confirm.ask( - "Stake all Tao from account: [bold]'{}'[/bold]?".format( - config.wallet.get("name", defaults.wallet.name) - ) - ): - amount = Prompt.ask("Enter Tao amount to stake") - try: - config.amount = float(amount) - except ValueError: - bt_console.print( - ":cross_mark:[red]Invalid Tao amount[/red] [bold white]{}[/bold white]".format( - amount - ) - ) - sys.exit() - else: - config.stake_all = True - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - stake_parser = parser.add_parser( - "add", help="""Add stake to your hotkey accounts from your coldkey.""" - ) - stake_parser.add_argument("--all", dest="stake_all", action="store_true") - stake_parser.add_argument("--uid", dest="uid", type=int, required=False) - stake_parser.add_argument("--amount", dest="amount", type=float, required=False) - stake_parser.add_argument( - "--max_stake", - dest="max_stake", - type=float, - required=False, - action="store", - default=None, - help="""Specify the maximum amount of Tao to have staked in each hotkey.""", - ) - stake_parser.add_argument( - "--hotkeys", - "--exclude_hotkeys", - "--wallet.hotkeys", - "--wallet.exclude_hotkeys", - required=False, - action="store", - default=[], - type=str, - nargs="*", - help="""Specify the hotkeys by name or ss58 address. (e.g. hk1 hk2 hk3)""", - ) - stake_parser.add_argument( - "--all_hotkeys", - "--wallet.all_hotkeys", - required=False, - action="store_true", - default=False, - help="""To specify all hotkeys. Specifying hotkeys will exclude them from this all.""", - ) - Wallet.add_args(stake_parser) - Subtensor.add_args(stake_parser) - - -def _get_coldkey_wallets_for_path(path: str) -> List["Wallet"]: - try: - wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [Wallet(path=path, name=name) for name in wallet_names] - except StopIteration: - # No wallet files found. - wallets = [] - return wallets - - -def _get_hotkey_wallets_for_wallet(wallet) -> List["Wallet"]: - hotkey_wallets = [] - hotkeys_path = wallet.path + "/" + wallet.name + "/hotkeys" - try: - hotkey_files = next(os.walk(os.path.expanduser(hotkeys_path)))[2] - except StopIteration: - hotkey_files = [] - for hotkey_file_name in hotkey_files: - try: - hotkey_for_name = Wallet( - path=wallet.path, name=wallet.name, hotkey=hotkey_file_name - ) - if ( - hotkey_for_name.hotkey_file.exists_on_device() - and not hotkey_for_name.hotkey_file.is_encrypted() - ): - hotkey_wallets.append(hotkey_for_name) - except Exception: - pass - return hotkey_wallets - - -class StakeShow: - """ - Executes the ``show`` command to list all stake accounts associated with a user's wallet on the Bittensor network. - - This command provides a comprehensive view of the stakes associated with both hotkeys and delegates linked to the user's coldkey. - - Usage: - The command lists all stake accounts for a specified wallet or all wallets in the user's configuration directory. - It displays the coldkey, balance, account details (hotkey/delegate name), stake amount, and the rate of return. - - Optional arguments: - - ``--all`` (bool): When set, the command checks all coldkey wallets instead of just the specified wallet. - - The command compiles a table showing: - - - Coldkey: The coldkey associated with the wallet. - - Balance: The balance of the coldkey. - - Account: The name of the hotkey or delegate. - - Stake: The amount of TAO staked to the hotkey or delegate. - - Rate: The rate of return on the stake, typically shown in TAO per day. - - Example usage:: - - btcli stake show --all - - Note: - This command is essential for users who wish to monitor their stake distribution and returns across various accounts on the Bittensor network. - It provides a clear and detailed overview of the user's staking activities. - """ - - @staticmethod - def run(cli): - r"""Show all stake accounts.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - StakeShow._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - """Show all stake accounts.""" - if cli.config.get("all", d=False): - wallets = _get_coldkey_wallets_for_path(cli.config.wallet.path) - else: - wallets = [Wallet(config=cli.config)] - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(url=delegates_details_url) - ) - - def get_stake_accounts( - wallet, subtensor - ) -> Dict[str, Dict[str, Union[str, Balance]]]: - """Get stake account details for the given wallet. - - Args: - wallet: The wallet object to fetch the stake account details for. - - Returns: - A dictionary mapping SS58 addresses to their respective stake account details. - """ - - wallet_stake_accounts = {} - - # Get this wallet's coldkey balance. - cold_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - # Populate the stake accounts with local hotkeys data. - wallet_stake_accounts.update(get_stakes_from_hotkeys(subtensor, wallet)) - - # Populate the stake accounts with delegations data. - wallet_stake_accounts.update(get_stakes_from_delegates(subtensor, wallet)) - - return { - "name": wallet.name, - "balance": cold_balance, - "accounts": wallet_stake_accounts, - } - - def get_stakes_from_hotkeys( - subtensor, wallet - ) -> Dict[str, Dict[str, Union[str, Balance]]]: - """Fetch stakes from hotkeys for the provided wallet. - - Args: - wallet: The wallet object to fetch the stakes for. - - Returns: - A dictionary of stakes related to hotkeys. - """ - hotkeys = get_hotkey_wallets_for_wallet(wallet) - stakes = {} - for hot in hotkeys: - emission = sum( - [ - n.emission - for n in subtensor.get_all_neurons_for_pubkey( - hot.hotkey.ss58_address - ) - ] - ) - hotkey_stake = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hot.hotkey.ss58_address, - coldkey_ss58=wallet.coldkeypub.ss58_address, - ) - stakes[hot.hotkey.ss58_address] = { - "name": hot.hotkey_str, - "stake": hotkey_stake, - "rate": emission, - } - return stakes - - def get_stakes_from_delegates( - subtensor, wallet - ) -> Dict[str, Dict[str, Union[str, Balance]]]: - """Fetch stakes from delegates for the provided wallet. - - Args: - wallet: The wallet object to fetch the stakes for. - - Returns: - A dictionary of stakes related to delegates. - """ - delegates = subtensor.get_delegated( - coldkey_ss58=wallet.coldkeypub.ss58_address - ) - stakes = {} - for dele, staked in delegates: - for nom in dele.nominators: - if nom[0] == wallet.coldkeypub.ss58_address: - delegate_name = ( - registered_delegate_info[dele.hotkey_ss58].name - if dele.hotkey_ss58 in registered_delegate_info - else dele.hotkey_ss58 - ) - stakes[dele.hotkey_ss58] = { - "name": delegate_name, - "stake": nom[1], - "rate": dele.total_daily_return.tao - * (nom[1] / dele.total_stake.tao), - } - return stakes - - def get_all_wallet_accounts( - wallets, - subtensor, - ) -> List[Dict[str, Dict[str, Union[str, Balance]]]]: - """Fetch stake accounts for all provided wallets using a ThreadPool. - - Args: - wallets: List of wallets to fetch the stake accounts for. - - Returns: - A list of dictionaries, each dictionary containing stake account details for each wallet. - """ - - accounts = [] - # Create a progress bar using tqdm - with tqdm(total=len(wallets), desc="Fetching accounts", ncols=100) as pbar: - for wallet in wallets: - accounts.append(get_stake_accounts(wallet, subtensor)) - pbar.update() - return accounts - - accounts = get_all_wallet_accounts(wallets, subtensor) - - total_stake = 0 - total_balance = 0 - total_rate = 0 - for acc in accounts: - total_balance += acc["balance"].tao - for key, value in acc["accounts"].items(): - total_stake += value["stake"].tao - total_rate += float(value["rate"]) - table = Table(show_footer=True, pad_edge=False, box=None, expand=False) - table.add_column( - "[overline white]Coldkey", footer_style="overline white", style="bold white" - ) - table.add_column( - "[overline white]Balance", - "\u03c4{:.5f}".format(total_balance), - footer_style="overline white", - style="green", - ) - table.add_column( - "[overline white]Account", footer_style="overline white", style="blue" - ) - table.add_column( - "[overline white]Stake", - "\u03c4{:.5f}".format(total_stake), - footer_style="overline white", - style="green", - ) - table.add_column( - "[overline white]Rate", - "\u03c4{:.5f}/d".format(total_rate), - footer_style="overline white", - style="green", - ) - for acc in accounts: - table.add_row(acc["name"], acc["balance"], "", "") - for key, value in acc["accounts"].items(): - table.add_row( - "", "", value["name"], value["stake"], str(value["rate"]) + "/d" - ) - bt_console.print(table) - - @staticmethod - def check_config(config: "Config"): - if ( - not config.get("all", d=None) - and not config.is_set("wallet.name") - and not config.no_prompt - ): - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - list_parser = parser.add_parser( - "show", help="""List all stake accounts for wallet.""" - ) - list_parser.add_argument( - "--all", - action="store_true", - help="""Check all coldkey wallets.""", - default=False, - ) - - Wallet.add_args(list_parser) - Subtensor.add_args(list_parser) diff --git a/bittensor/btcli/commands/transfer.py b/bittensor/btcli/commands/transfer.py deleted file mode 100644 index ccc224094..000000000 --- a/bittensor/btcli/commands/transfer.py +++ /dev/null @@ -1,132 +0,0 @@ -# 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 sys -import argparse -from rich.prompt import Prompt - -from bittensor_wallet import Wallet -from bittensor.core.settings import bt_console -from bittensor.core.subtensor import Subtensor -from bittensor.utils.btlogging import logging -from bittensor.core.config import Config -from . import defaults -from bittensor.utils import is_valid_bittensor_address_or_public_key - - -class TransferCommand: - """ - Executes the ``transfer`` command to transfer TAO tokens from one account to another on the Bittensor network. - - This command is used for transactions between different accounts, enabling users to send tokens to other participants on the network. - - Usage: - The command requires specifying the destination address (public key) and the amount of TAO to be transferred. - It checks for sufficient balance and prompts for confirmation before proceeding with the transaction. - - Optional arguments: - - ``--dest`` (str): The destination address for the transfer. This can be in the form of an SS58 or ed2519 public key. - - ``--amount`` (float): The amount of TAO tokens to transfer. - - The command displays the user's current balance before prompting for the amount to transfer, ensuring transparency and accuracy in the transaction. - - Example usage:: - - btcli wallet transfer --dest 5Dp8... --amount 100 - - Note: - This command is crucial for executing token transfers within the Bittensor network. Users should verify the destination address and amount before confirming the transaction to avoid errors or loss of funds. - """ - - @staticmethod - def run(cli): - r"""Transfer token of amount to destination.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - TransferCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Transfer token of amount to destination.""" - wallet = Wallet(config=cli.config) - subtensor.transfer( - wallet=wallet, - dest=cli.config.dest, - amount=cli.config.amount, - wait_for_inclusion=True, - prompt=not cli.config.no_prompt, - ) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - # Get destination. - if not config.dest and not config.no_prompt: - dest = Prompt.ask("Enter destination public key: (ss58 or ed2519)") - if not is_valid_bittensor_address_or_public_key(dest): - sys.exit() - else: - config.dest = str(dest) - - # Get current balance and print to user. - if not config.no_prompt: - wallet = Wallet(config=config) - subtensor = Subtensor(config=config, log_verbose=False) - with bt_console.status(":satellite: Checking Balance..."): - account_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bt_console.print( - "Balance: [green]{}[/green]".format(account_balance) - ) - - # Get amount. - if not config.get("amount"): - if not config.no_prompt: - amount = Prompt.ask("Enter TAO amount to transfer") - try: - config.amount = float(amount) - except ValueError: - bt_console.print( - ":cross_mark:[red] Invalid TAO amount[/red] [bold white]{}[/bold white]".format( - amount - ) - ) - sys.exit() - else: - bt_console.print(":cross_mark:[red] Invalid TAO amount[/red]") - sys.exit(1) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - transfer_parser = parser.add_parser( - "transfer", help="""Transfer Tao between accounts.""" - ) - transfer_parser.add_argument("--dest", dest="dest", type=str, required=False) - transfer_parser.add_argument( - "--amount", dest="amount", type=float, required=False - ) - - Wallet.add_args(transfer_parser) - Subtensor.add_args(transfer_parser) diff --git a/bittensor/btcli/commands/unstake.py b/bittensor/btcli/commands/unstake.py deleted file mode 100644 index 6f7a280a8..000000000 --- a/bittensor/btcli/commands/unstake.py +++ /dev/null @@ -1,301 +0,0 @@ -# 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 sys -from typing import List, Union, Optional, Tuple - -from bittensor_wallet import Wallet -from rich.prompt import Confirm, Prompt -from tqdm import tqdm - -from bittensor.core.config import Config -from bittensor.core.settings import tao_symbol, bt_console -from bittensor.core.subtensor import Subtensor -from bittensor.utils import is_valid_ss58_address -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging -from . import defaults -from .utils import get_hotkey_wallets_for_wallet - - -class UnStakeCommand: - """ - Executes the ``remove`` command to unstake TAO tokens from one or more hotkeys and transfer them back to the user's coldkey on the Bittensor network. - - This command is used to withdraw tokens previously staked to different hotkeys. - - Usage: - Users can specify the amount to unstake, the hotkeys to unstake from (either by name or ``SS58`` address), and whether to unstake from all hotkeys. The command checks for sufficient stake and prompts for confirmation before proceeding with the unstaking process. - - Optional arguments: - - ``--all`` (bool): When set, unstakes all staked tokens from the specified hotkeys. - - ``--amount`` (float): The amount of TAO tokens to unstake. - - --hotkey_ss58address (str): The SS58 address of the hotkey to unstake from. - - ``--max_stake`` (float): Sets the maximum amount of TAO to remain staked in each hotkey. - - ``--hotkeys`` (list): Specifies hotkeys by name or SS58 address to unstake from. - - ``--all_hotkeys`` (bool): When set, unstakes from all hotkeys associated with the wallet, excluding any specified in --hotkeys. - - The command prompts for confirmation before executing the unstaking operation. - - Example usage:: - - btcli stake remove --amount 100 --hotkeys hk1,hk2 - - Note: - This command is important for users who wish to reallocate their stakes or withdraw them from the network. - It allows for flexible management of token stakes across different neurons (hotkeys) on the network. - """ - - @classmethod - def check_config(cls, config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if ( - not config.get("hotkey_ss58address", d=None) - and not config.is_set("wallet.hotkey") - and not config.no_prompt - and not config.get("all_hotkeys") - and not config.get("hotkeys") - ): - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - # Get amount. - if ( - not config.get("hotkey_ss58address") - and not config.get("amount") - and not config.get("unstake_all") - and not config.get("max_stake") - ): - hotkeys: str = "" - if config.get("all_hotkeys"): - hotkeys = "all hotkeys" - elif config.get("hotkeys"): - hotkeys = str(config.hotkeys).replace("[", "").replace("]", "") - else: - hotkeys = str(config.wallet.hotkey) - if config.no_prompt: - config.unstake_all = True - else: - # I really don't like this logic flow. It can be a bit confusing to read for something - # as serious as unstaking all. - if Confirm.ask(f"Unstake all Tao from: [bold]'{hotkeys}'[/bold]?"): - config.unstake_all = True - else: - config.unstake_all = False - amount = Prompt.ask("Enter Tao amount to unstake") - try: - config.amount = float(amount) - except ValueError: - bt_console.print( - f":cross_mark:[red] Invalid Tao amount[/red] [bold white]{amount}[/bold white]" - ) - sys.exit() - - @staticmethod - def add_args(command_parser): - unstake_parser = command_parser.add_parser( - "remove", - help="""Remove stake from the specified hotkey into the coldkey balance.""", - ) - unstake_parser.add_argument( - "--all", dest="unstake_all", action="store_true", default=False - ) - unstake_parser.add_argument( - "--amount", dest="amount", type=float, required=False - ) - unstake_parser.add_argument( - "--hotkey_ss58address", dest="hotkey_ss58address", type=str, required=False - ) - unstake_parser.add_argument( - "--max_stake", - dest="max_stake", - type=float, - required=False, - action="store", - default=None, - help="""Specify the maximum amount of Tao to have staked in each hotkey.""", - ) - unstake_parser.add_argument( - "--hotkeys", - "--exclude_hotkeys", - "--wallet.hotkeys", - "--wallet.exclude_hotkeys", - required=False, - action="store", - default=[], - type=str, - nargs="*", - help="""Specify the hotkeys by name or ss58 address. (e.g. hk1 hk2 hk3)""", - ) - unstake_parser.add_argument( - "--all_hotkeys", - "--wallet.all_hotkeys", - required=False, - action="store_true", - default=False, - help="""To specify all hotkeys. Specifying hotkeys will exclude them from this all.""", - ) - Wallet.add_args(unstake_parser) - Subtensor.add_args(unstake_parser) - - @staticmethod - def run(cli): - r"""Unstake token of amount from hotkey(s).""" - try: - config = cli.config.copy() - subtensor: "Subtensor" = Subtensor( - config=config, log_verbose=False - ) - UnStakeCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Unstake token of amount from hotkey(s).""" - config = cli.config.copy() - wallet = Wallet(config=config) - - # Get the hotkey_names (if any) and the hotkey_ss58s. - hotkeys_to_unstake_from: List[Tuple[Optional[str], str]] = [] - if cli.config.get("hotkey_ss58address"): - # Stake to specific hotkey. - hotkeys_to_unstake_from = [(None, cli.config.get("hotkey_ss58address"))] - elif cli.config.get("all_hotkeys"): - # Stake to all hotkeys. - all_hotkeys: List["Wallet"] = get_hotkey_wallets_for_wallet( - wallet=wallet - ) - # Get the hotkeys to exclude. (d)efault to no exclusions. - hotkeys_to_exclude: List[str] = cli.config.get("hotkeys", d=[]) - # Exclude hotkeys that are specified. - hotkeys_to_unstake_from = [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys - if wallet.hotkey_str not in hotkeys_to_exclude - ] # definitely wallets - - elif cli.config.get("hotkeys"): - # Stake to specific hotkeys. - for hotkey_ss58_or_hotkey_name in cli.config.get("hotkeys"): - if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): - # If the hotkey is a valid ss58 address, we add it to the list. - hotkeys_to_unstake_from.append((None, hotkey_ss58_or_hotkey_name)) - else: - # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. - # We then get the hotkey from the wallet and add it to the list. - wallet_ = Wallet( - config=cli.config, hotkey=hotkey_ss58_or_hotkey_name - ) - hotkeys_to_unstake_from.append( - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ) - elif cli.config.wallet.get("hotkey"): - # Only cli.config.wallet.hotkey is specified. - # so we stake to that single hotkey. - hotkey_ss58_or_name = cli.config.wallet.get("hotkey") - if is_valid_ss58_address(hotkey_ss58_or_name): - hotkeys_to_unstake_from = [(None, hotkey_ss58_or_name)] - else: - # Hotkey is not a valid ss58 address, so we assume it is a hotkey name. - wallet_ = Wallet( - config=cli.config, hotkey=hotkey_ss58_or_name - ) - hotkeys_to_unstake_from = [ - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ] - else: - # Only cli.config.wallet.hotkey is specified. - # so we stake to that single hotkey. - assert cli.config.wallet.hotkey is not None - hotkeys_to_unstake_from = [ - (None, Wallet(config=cli.config).hotkey.ss58_address) - ] - - final_hotkeys: List[Tuple[str, str]] = [] - final_amounts: List[Union[float, Balance]] = [] - for hotkey in tqdm(hotkeys_to_unstake_from): - hotkey: Tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) - unstake_amount_tao: float = cli.config.get( - "amount" - ) # The amount specified to unstake. - hotkey_stake: Balance = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey[1], coldkey_ss58=wallet.coldkeypub.ss58_address - ) - if unstake_amount_tao is None: - unstake_amount_tao = hotkey_stake.tao - if cli.config.get("max_stake"): - # Get the current stake of the hotkey from this coldkey. - unstake_amount_tao: float = hotkey_stake.tao - cli.config.get( - "max_stake" - ) - cli.config.amount = unstake_amount_tao - if unstake_amount_tao < 0: - # Skip if max_stake is greater than current stake. - continue - else: - if unstake_amount_tao is not None: - # There is a specified amount to unstake. - if unstake_amount_tao > hotkey_stake.tao: - # Skip if the specified amount is greater than the current stake. - continue - - final_amounts.append(unstake_amount_tao) - final_hotkeys.append(hotkey) # add both the name and the ss58 address. - - if len(final_hotkeys) == 0: - # No hotkeys to unstake from. - bt_console.print( - "Not enough stake to unstake from any hotkeys or max_stake is more than current stake." - ) - return None - - # Ask to unstake - if not cli.config.no_prompt: - if not Confirm.ask( - f"Do you want to unstake from the following keys to {wallet.name}:\n" - + "".join( - [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {tao_symbol}' if amount else 'All'}[/bold white]\n" - for hotkey, amount in zip(final_hotkeys, final_amounts) - ] - ) - ): - return None - - if len(final_hotkeys) == 1: - # do regular unstake - return subtensor.unstake( - wallet=wallet, - hotkey_ss58=final_hotkeys[0][1], - amount=None if cli.config.get("unstake_all") else final_amounts[0], - wait_for_inclusion=True, - prompt=not cli.config.no_prompt, - ) - - subtensor.unstake_multiple( - wallet=wallet, - hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], - amounts=None if cli.config.get("unstake_all") else final_amounts, - wait_for_inclusion=True, - prompt=False, - ) diff --git a/bittensor/btcli/commands/utils.py b/bittensor/btcli/commands/utils.py deleted file mode 100644 index c3d871ca6..000000000 --- a/bittensor/btcli/commands/utils.py +++ /dev/null @@ -1,288 +0,0 @@ -# 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 os -import sys -from dataclasses import dataclass -from typing import List, Dict, Any, Optional, Tuple - -import requests -from bittensor_wallet import Wallet -from rich.prompt import Confirm, PromptBase - -from bittensor.btcli.commands import defaults -from bittensor.core.chain_data import SubnetHyperparameters -from bittensor.core.config import Config -from bittensor.core.settings import bt_console -from bittensor.core.subtensor import Subtensor -from bittensor.utils import U64_NORMALIZED_FLOAT, U16_NORMALIZED_FLOAT -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging -from bittensor.utils.registration import torch - - -class IntListPrompt(PromptBase): - """Prompt for a list of integers.""" - - def check_choice(self, value: str) -> bool: - assert self.choices is not None - # check if value is a valid choice or all the values in a list of ints are valid choices - return ( - value == "All" - or value in self.choices - or all( - val.strip() in self.choices for val in value.replace(",", " ").split() - ) - ) - - -def check_netuid_set( - config: "Config", - subtensor: "Subtensor", - allow_none: bool = False, -): - if subtensor.network != "nakamoto": - all_netuids = [str(netuid) for netuid in subtensor.get_subnets()] - if len(all_netuids) == 0: - bt_console.print(":cross_mark:[red]There are no open networks.[/red]") - sys.exit() - - # Make sure netuid is set. - if not config.is_set("netuid"): - if not config.no_prompt: - netuid = IntListPrompt.ask( - "Enter netuid", choices=all_netuids, default=str(all_netuids[0]) - ) - else: - netuid = str(defaults.netuid) if not allow_none else "None" - else: - netuid = config.netuid - - if isinstance(netuid, str) and netuid.lower() in ["none"] and allow_none: - config.netuid = None - else: - if isinstance(netuid, list): - netuid = netuid[0] - try: - config.netuid = int(netuid) - except: - raise ValueError('netuid must be an integer or "None" (if applicable)') - - -def check_for_cuda_reg_config(config: "Config") -> None: - """Checks, when CUDA is available, if the user would like to register with their CUDA device.""" - if torch and torch.cuda.is_available(): - if not config.no_prompt: - if config.pow_register.cuda.get("use_cuda") is None: # flag not set - # Ask about cuda registration only if a CUDA device is available. - cuda = Confirm.ask("Detected CUDA device, use CUDA for registration?\n") - config.pow_register.cuda.use_cuda = cuda - - # Only ask about which CUDA device if the user has more than one CUDA device. - if ( - config.pow_register.cuda.use_cuda - and config.pow_register.cuda.get("dev_id") is None - ): - devices: List[str] = [str(x) for x in range(torch.cuda.device_count())] - device_names: List[str] = [ - torch.cuda.get_device_name(x) - for x in range(torch.cuda.device_count()) - ] - bt_console.print("Available CUDA devices:") - choices_str: str = "" - for i, device in enumerate(devices): - choices_str += " {}: {}\n".format(device, device_names[i]) - bt_console.print(choices_str) - dev_id = IntListPrompt.ask( - "Which GPU(s) would you like to use? Please list one, or comma-separated", - choices=devices, - default="All", - ) - if dev_id.lower() == "all": - dev_id = list(range(torch.cuda.device_count())) - else: - try: - # replace the commas with spaces then split over whitespace., - # then strip the whitespace and convert to ints. - dev_id = [ - int(dev_id.strip()) - for dev_id in dev_id.replace(",", " ").split() - ] - except ValueError: - bt_console.log( - ":cross_mark:[red]Invalid GPU device[/red] [bold white]{}[/bold white]\nAvailable CUDA devices:{}".format( - dev_id, choices_str - ) - ) - sys.exit(1) - config.pow_register.cuda.dev_id = dev_id - else: - # flag was not set, use default value. - if config.pow_register.cuda.get("use_cuda") is None: - config.pow_register.cuda.use_cuda = defaults.pow_register.cuda.use_cuda - - -def get_hotkey_wallets_for_wallet(wallet) -> List["Wallet"]: - hotkey_wallets = [] - hotkeys_path = wallet.path + "/" + wallet.name + "/hotkeys" - try: - hotkey_files = next(os.walk(os.path.expanduser(hotkeys_path)))[2] - except StopIteration: - hotkey_files = [] - for hotkey_file_name in hotkey_files: - try: - hotkey_for_name = Wallet( - path=wallet.path, name=wallet.name, hotkey=hotkey_file_name - ) - if ( - hotkey_for_name.hotkey_file.exists_on_device() - and not hotkey_for_name.hotkey_file.is_encrypted() - ): - hotkey_wallets.append(hotkey_for_name) - except Exception: - pass - return hotkey_wallets - - -def get_coldkey_wallets_for_path(path: str) -> List["Wallet"]: - try: - wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [Wallet(path=path, name=name) for name in wallet_names] - except StopIteration: - # No wallet files found. - wallets = [] - return wallets - - -def get_all_wallets_for_path(path: str) -> List["Wallet"]: - all_wallets = [] - cold_wallets = get_coldkey_wallets_for_path(path) - for cold_wallet in cold_wallets: - if ( - cold_wallet.coldkeypub_file.exists_on_device() - and not cold_wallet.coldkeypub_file.is_encrypted() - ): - all_wallets.extend(get_hotkey_wallets_for_wallet(cold_wallet)) - return all_wallets - - -def filter_netuids_by_registered_hotkeys( - cli, subtensor, netuids, all_hotkeys -) -> List[int]: - netuids_with_registered_hotkeys = [] - for wallet in all_hotkeys: - netuids_list = subtensor.get_netuids_for_hotkey(wallet.hotkey.ss58_address) - logging.debug( - f"Hotkey {wallet.hotkey.ss58_address} registered in netuids: {netuids_list}" - ) - netuids_with_registered_hotkeys.extend(netuids_list) - - if not cli.config.netuids: - netuids = netuids_with_registered_hotkeys - - else: - netuids = [netuid for netuid in netuids if netuid in cli.config.netuids] - netuids.extend(netuids_with_registered_hotkeys) - - return list(set(netuids)) - - -def normalize_hyperparameters( - subnet: "SubnetHyperparameters", -) -> List[Tuple[str, str, str]]: - """ - Normalizes the hyperparameters of a subnet. - - Args: - subnet: The subnet hyperparameters object. - - Returns: - A list of tuples containing the parameter name, value, and normalized value. - """ - param_mappings = { - "adjustment_alpha": U64_NORMALIZED_FLOAT, - "min_difficulty": U64_NORMALIZED_FLOAT, - "max_difficulty": U64_NORMALIZED_FLOAT, - "difficulty": U64_NORMALIZED_FLOAT, - "bonds_moving_avg": U64_NORMALIZED_FLOAT, - "max_weight_limit": U16_NORMALIZED_FLOAT, - "kappa": U16_NORMALIZED_FLOAT, - "alpha_high": U16_NORMALIZED_FLOAT, - "alpha_low": U16_NORMALIZED_FLOAT, - "min_burn": Balance.from_rao, - "max_burn": Balance.from_rao, - } - - normalized_values: List[Tuple[str, str, str]] = [] - subnet_dict = subnet.__dict__ - - for param, value in subnet_dict.items(): - try: - if param in param_mappings: - norm_value = param_mappings[param](value) - if isinstance(norm_value, float): - norm_value = f"{norm_value:.{10}g}" - else: - norm_value = value - except Exception as e: - logging.warning(f"Error normalizing parameter '{param}': {e}") - norm_value = "-" - - normalized_values.append((param, str(value), str(norm_value))) - - return normalized_values - - -@dataclass -class DelegatesDetails: - name: str - url: str - description: str - signature: str - - @classmethod - def from_json(cls, json: Dict[str, any]) -> "DelegatesDetails": - return cls( - name=json["name"], - url=json["url"], - description=json["description"], - signature=json["signature"], - ) - - -def _get_delegates_details_from_github( - requests_get, url: str -) -> Dict[str, DelegatesDetails]: - response = requests_get(url) - - if response.status_code == 200: - all_delegates: Dict[str, Any] = response.json() - all_delegates_details = {} - for delegate_hotkey, delegates_details in all_delegates.items(): - all_delegates_details[delegate_hotkey] = DelegatesDetails.from_json( - delegates_details - ) - return all_delegates_details - else: - return {} - - -def get_delegates_details(url: str) -> Optional[Dict[str, DelegatesDetails]]: - try: - return _get_delegates_details_from_github(requests.get, url) - except Exception: - return None # Fail silently diff --git a/bittensor/btcli/commands/wallets.py b/bittensor/btcli/commands/wallets.py deleted file mode 100644 index 59423f53f..000000000 --- a/bittensor/btcli/commands/wallets.py +++ /dev/null @@ -1,1112 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 os -import sys -from typing import List, Optional, Tuple - -import requests -from rich.prompt import Confirm, Prompt -from rich.table import Table - -from bittensor.utils import RAOPERTAO, is_valid_bittensor_address_or_public_key -from ...core.settings import networks - -from bittensor.core.config import Config -from bittensor.core.settings import bt_console -from bittensor.core.subtensor import Subtensor -from bittensor.utils.btlogging import logging -from . import defaults -from bittensor_wallet import Wallet -from bittensor_wallet.keyfile import Keyfile - - -class RegenColdkeyCommand: - """ - Executes the ``regen_coldkey`` command to regenerate a coldkey for a wallet on the Bittensor network. - - This command is used to create a new coldkey from an existing mnemonic, seed, or JSON file. - - Usage: - Users can specify a mnemonic, a seed string, or a JSON file path to regenerate a coldkey. - The command supports optional password protection for the generated key and can overwrite an existing coldkey. - - Optional arguments: - - ``--mnemonic`` (str): A mnemonic phrase used to regenerate the key. - - ``--seed`` (str): A seed hex string used for key regeneration. - - ``--json`` (str): Path to a JSON file containing an encrypted key backup. - - ``--json_password`` (str): Password to decrypt the JSON file. - - ``--use_password`` (bool): Enables password protection for the generated key. - - ``--overwrite_coldkey`` (bool): Overwrites the existing coldkey with the new one. - - Example usage:: - - btcli wallet regen_coldkey --mnemonic "word1 word2 ... word12" - - Note: - This command is critical for users who need to regenerate their coldkey, possibly for recovery or security reasons. - It should be used with caution to avoid overwriting existing keys unintentionally. - """ - - @staticmethod - def run(cli): - r"""Creates a new coldkey under this wallet.""" - wallet = Wallet(config=cli.config) - - json_str: Optional[str] = None - json_password: Optional[str] = None - if cli.config.get("json"): - file_name: str = cli.config.get("json") - if not os.path.exists(file_name) or not os.path.isfile(file_name): - raise ValueError("File {} does not exist".format(file_name)) - with open(cli.config.get("json"), "r") as f: - json_str = f.read() - # Password can be "", assume if None - json_password = cli.config.get("json_password", "") - wallet.regenerate_coldkey( - mnemonic=cli.config.mnemonic, - seed=cli.config.seed, - json=(json_str, json_password), - use_password=cli.config.use_password, - overwrite=cli.config.overwrite_coldkey, - ) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if ( - config.mnemonic == None - and config.get("seed", d=None) == None - and config.get("json", d=None) == None - ): - prompt_answer = Prompt.ask("Enter mnemonic, seed, or json file location") - if prompt_answer.startswith("0x"): - config.seed = prompt_answer - elif len(prompt_answer.split(" ")) > 1: - config.mnemonic = prompt_answer - else: - config.json = prompt_answer - - if config.get("json", d=None) and config.get("json_password", d=None) == None: - config.json_password = Prompt.ask( - "Enter json backup password", password=True - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - regen_coldkey_parser = parser.add_parser( - "regen_coldkey", help="""Regenerates a coldkey from a passed value""" - ) - regen_coldkey_parser.add_argument( - "--mnemonic", - required=False, - nargs="+", - help="Mnemonic used to regen your key i.e. horse cart dog ...", - ) - regen_coldkey_parser.add_argument( - "--seed", - required=False, - default=None, - help="Seed hex string used to regen your key i.e. 0x1234...", - ) - regen_coldkey_parser.add_argument( - "--json", - required=False, - default=None, - help="""Path to a json file containing the encrypted key backup. (e.g. from PolkadotJS)""", - ) - regen_coldkey_parser.add_argument( - "--json_password", - required=False, - default=None, - help="""Password to decrypt the json file.""", - ) - regen_coldkey_parser.add_argument( - "--use_password", - dest="use_password", - action="store_true", - help="""Set true to protect the generated bittensor key with a password.""", - default=True, - ) - regen_coldkey_parser.add_argument( - "--no_password", - dest="use_password", - action="store_false", - help="""Set off protects the generated bittensor key with a password.""", - ) - regen_coldkey_parser.add_argument( - "--overwrite_coldkey", - default=False, - action="store_true", - help="""Overwrite the old coldkey with the newly generated coldkey""", - ) - Wallet.add_args(regen_coldkey_parser) - Subtensor.add_args(regen_coldkey_parser) - - -class RegenColdkeypubCommand: - """ - Executes the ``regen_coldkeypub`` command to regenerate the public part of a coldkey (coldkeypub) for a wallet on the Bittensor network. - - This command is used when a user needs to recreate their coldkeypub from an existing public key or SS58 address. - - Usage: - The command requires either a public key in hexadecimal format or an ``SS58`` address to regenerate the coldkeypub. It optionally allows overwriting an existing coldkeypub file. - - Optional arguments: - - ``--public_key_hex`` (str): The public key in hex format. - - ``--ss58_address`` (str): The SS58 address of the coldkey. - - ``--overwrite_coldkeypub`` (bool): Overwrites the existing coldkeypub file with the new one. - - Example usage:: - - btcli wallet regen_coldkeypub --ss58_address 5DkQ4... - - Note: - This command is particularly useful for users who need to regenerate their coldkeypub, perhaps due to file corruption or loss. - It is a recovery-focused utility that ensures continued access to wallet functionalities. - """ - - @staticmethod - def run(cli): - r"""Creates a new coldkeypub under this wallet.""" - wallet = Wallet(config=cli.config) - wallet.regenerate_coldkeypub( - ss58_address=cli.config.get("ss58_address"), - public_key=cli.config.get("public_key_hex"), - overwrite=cli.config.overwrite_coldkeypub, - ) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if config.ss58_address is None and config.public_key_hex is None: - prompt_answer = Prompt.ask( - "Enter the ss58_address or the public key in hex" - ) - if prompt_answer.startswith("0x"): - config.public_key_hex = prompt_answer - else: - config.ss58_address = prompt_answer - if not is_valid_bittensor_address_or_public_key( - address=( - config.ss58_address if config.ss58_address else config.public_key_hex - ) - ): - sys.exit(1) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - regen_coldkeypub_parser = parser.add_parser( - "regen_coldkeypub", - help="""Regenerates a coldkeypub from the public part of the coldkey.""", - ) - regen_coldkeypub_parser.add_argument( - "--public_key", - "--pubkey", - dest="public_key_hex", - required=False, - default=None, - type=str, - help="The public key (in hex) of the coldkey to regen e.g. 0x1234 ...", - ) - regen_coldkeypub_parser.add_argument( - "--ss58_address", - "--addr", - "--ss58", - dest="ss58_address", - required=False, - default=None, - type=str, - help="The ss58 address of the coldkey to regen e.g. 5ABCD ...", - ) - regen_coldkeypub_parser.add_argument( - "--overwrite_coldkeypub", - default=False, - action="store_true", - help="""Overwrite the old coldkeypub file with the newly generated coldkeypub""", - ) - Wallet.add_args(regen_coldkeypub_parser) - Subtensor.add_args(regen_coldkeypub_parser) - - -class RegenHotkeyCommand: - """ - Executes the ``regen_hotkey`` command to regenerate a hotkey for a wallet on the Bittensor network. - - Similar to regenerating a coldkey, this command creates a new hotkey from a mnemonic, seed, or JSON file. - - Usage: - Users can provide a mnemonic, seed string, or a JSON file to regenerate the hotkey. - The command supports optional password protection and can overwrite an existing hotkey. - - Optional arguments: - - ``--mnemonic`` (str): A mnemonic phrase used to regenerate the key. - - ``--seed`` (str): A seed hex string used for key regeneration. - - ``--json`` (str): Path to a JSON file containing an encrypted key backup. - - ``--json_password`` (str): Password to decrypt the JSON file. - - ``--use_password`` (bool): Enables password protection for the generated key. - - ``--overwrite_hotkey`` (bool): Overwrites the existing hotkey with the new one. - - Example usage:: - - btcli wallet regen_hotkey - btcli wallet regen_hotkey --seed 0x1234... - - Note: - This command is essential for users who need to regenerate their hotkey, possibly for security upgrades or key recovery. - It should be used cautiously to avoid accidental overwrites of existing keys. - """ - - def run(cli): - """Creates a new coldkey under this wallet.""" - wallet = Wallet(config=cli.config) - - json_str: Optional[str] = None - json_password: Optional[str] = None - if cli.config.get("json"): - file_name: str = cli.config.get("json") - if not os.path.exists(file_name) or not os.path.isfile(file_name): - raise ValueError("File {} does not exist".format(file_name)) - with open(cli.config.get("json"), "r") as f: - json_str = f.read() - - # Password can be "", assume if None - json_password = cli.config.get("json_password", "") - - wallet.regenerate_hotkey( - mnemonic=cli.config.mnemonic, - seed=cli.config.seed, - json=(json_str, json_password), - use_password=cli.config.use_password, - overwrite=cli.config.overwrite_hotkey, - ) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - if ( - config.mnemonic is None - and config.get("seed", d=None) is None - and config.get("json", d=None) is None - ): - prompt_answer = Prompt.ask("Enter mnemonic, seed, or json file location") - if prompt_answer.startswith("0x"): - config.seed = prompt_answer - elif len(prompt_answer.split(" ")) > 1: - config.mnemonic = prompt_answer - else: - config.json = prompt_answer - - if config.get("json", d=None) and config.get("json_password", d=None) is None: - config.json_password = Prompt.ask( - "Enter json backup password", password=True - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - regen_hotkey_parser = parser.add_parser( - "regen_hotkey", help="""Regenerates a hotkey from a passed mnemonic""" - ) - regen_hotkey_parser.add_argument( - "--mnemonic", - required=False, - nargs="+", - help="Mnemonic used to regen your key i.e. horse cart dog ...", - ) - regen_hotkey_parser.add_argument( - "--seed", - required=False, - default=None, - help="Seed hex string used to regen your key i.e. 0x1234...", - ) - regen_hotkey_parser.add_argument( - "--json", - required=False, - default=None, - help="""Path to a json file containing the encrypted key backup. (e.g. from PolkadotJS)""", - ) - regen_hotkey_parser.add_argument( - "--json_password", - required=False, - default=None, - help="""Password to decrypt the json file.""", - ) - regen_hotkey_parser.add_argument( - "--use_password", - dest="use_password", - action="store_true", - help="""Set true to protect the generated bittensor key with a password.""", - default=False, - ) - regen_hotkey_parser.add_argument( - "--no_password", - dest="use_password", - action="store_false", - help="""Set off protects the generated bittensor key with a password.""", - ) - regen_hotkey_parser.add_argument( - "--overwrite_hotkey", - dest="overwrite_hotkey", - action="store_true", - default=False, - help="""Overwrite the old hotkey with the newly generated hotkey""", - ) - Wallet.add_args(regen_hotkey_parser) - Subtensor.add_args(regen_hotkey_parser) - - -class NewHotkeyCommand: - """ - Executes the ``new_hotkey`` command to create a new hotkey under a wallet on the Bittensor network. - - This command is used to generate a new hotkey for managing a neuron or participating in the network. - - Usage: - The command creates a new hotkey with an optional word count for the mnemonic and supports password protection. - It also allows overwriting an existing hotkey. - - Optional arguments: - - ``--n_words`` (int): The number of words in the mnemonic phrase. - - ``--use_password`` (bool): Enables password protection for the generated key. - - ``--overwrite_hotkey`` (bool): Overwrites the existing hotkey with the new one. - - Example usage:: - - btcli wallet new_hotkey --n_words 24 - - Note: - This command is useful for users who wish to create additional hotkeys for different purposes, - such as running multiple miners or separating operational roles within the network. - """ - - @staticmethod - def run(cli): - """Creates a new hotke under this wallet.""" - wallet = Wallet(config=cli.config) - wallet.create_new_hotkey( - n_words=cli.config.n_words, - use_password=cli.config.use_password, - overwrite=cli.config.overwrite_hotkey, - ) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - new_hotkey_parser = parser.add_parser( - "new_hotkey", - help="""Creates a new hotkey (for running a miner) under the specified path.""", - ) - new_hotkey_parser.add_argument( - "--n_words", - type=int, - choices=[12, 15, 18, 21, 24], - default=12, - help="""The number of words representing the mnemonic. i.e. horse cart dog ... x 24""", - ) - new_hotkey_parser.add_argument( - "--use_password", - dest="use_password", - action="store_true", - help="""Set true to protect the generated bittensor key with a password.""", - default=False, - ) - new_hotkey_parser.add_argument( - "--no_password", - dest="use_password", - action="store_false", - help="""Set off protects the generated bittensor key with a password.""", - ) - new_hotkey_parser.add_argument( - "--overwrite_hotkey", - action="store_true", - default=False, - help="""Overwrite the old hotkey with the newly generated hotkey""", - ) - Wallet.add_args(new_hotkey_parser) - Subtensor.add_args(new_hotkey_parser) - - -class NewColdkeyCommand: - """ - Executes the ``new_coldkey`` command to create a new coldkey under a wallet on the Bittensor network. - - This command generates a coldkey, which is essential for holding balances and performing high-value transactions. - - Usage: - The command creates a new coldkey with an optional word count for the mnemonic and supports password protection. - It also allows overwriting an existing coldkey. - - Optional arguments: - - ``--n_words`` (int): The number of words in the mnemonic phrase. - - ``--use_password`` (bool): Enables password protection for the generated key. - - ``--overwrite_coldkey`` (bool): Overwrites the existing coldkey with the new one. - - Example usage:: - - btcli wallet new_coldkey --n_words 15 - - Note: - This command is crucial for users who need to create a new coldkey for enhanced security or as part of setting up a new wallet. - It's a foundational step in establishing a secure presence on the Bittensor network. - """ - - @staticmethod - def run(cli): - r"""Creates a new coldkey under this wallet.""" - wallet = Wallet(config=cli.config) - wallet.create_new_coldkey( - n_words=cli.config.n_words, - use_password=cli.config.use_password, - overwrite=cli.config.overwrite_coldkey, - ) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - new_coldkey_parser = parser.add_parser( - "new_coldkey", - help="""Creates a new coldkey (for containing balance) under the specified path. """, - ) - new_coldkey_parser.add_argument( - "--n_words", - type=int, - choices=[12, 15, 18, 21, 24], - default=12, - help="""The number of words representing the mnemonic. i.e. horse cart dog ... x 24""", - ) - new_coldkey_parser.add_argument( - "--use_password", - dest="use_password", - action="store_true", - help="""Set true to protect the generated bittensor key with a password.""", - default=True, - ) - new_coldkey_parser.add_argument( - "--no_password", - dest="use_password", - action="store_false", - help="""Set off protects the generated bittensor key with a password.""", - ) - new_coldkey_parser.add_argument( - "--overwrite_coldkey", - action="store_true", - default=False, - help="""Overwrite the old coldkey with the newly generated coldkey""", - ) - Wallet.add_args(new_coldkey_parser) - Subtensor.add_args(new_coldkey_parser) - - -class WalletCreateCommand: - """ - Executes the ``create`` command to generate both a new coldkey and hotkey under a specified wallet on the Bittensor network. - - This command is a comprehensive utility for creating a complete wallet setup with both cold and hotkeys. - - Usage: - The command facilitates the creation of a new coldkey and hotkey with an optional word count for the mnemonics. - It supports password protection for the coldkey and allows overwriting of existing keys. - - Optional arguments: - - ``--n_words`` (int): The number of words in the mnemonic phrase for both keys. - - ``--use_password`` (bool): Enables password protection for the coldkey. - - ``--overwrite_coldkey`` (bool): Overwrites the existing coldkey with the new one. - - ``--overwrite_hotkey`` (bool): Overwrites the existing hotkey with the new one. - - Example usage:: - - btcli wallet create --n_words 21 - - Note: - This command is ideal for new users setting up their wallet for the first time or for those who wish to completely renew their wallet keys. - It ensures a fresh start with new keys for secure and effective participation in the network. - """ - - @staticmethod - def run(cli): - r"""Creates a new coldkey and hotkey under this wallet.""" - wallet = Wallet(config=cli.config) - wallet.create_new_coldkey( - n_words=cli.config.n_words, - use_password=cli.config.use_password, - overwrite=cli.config.overwrite_coldkey, - ) - wallet.create_new_hotkey( - n_words=cli.config.n_words, - use_password=False, - overwrite=cli.config.overwrite_hotkey, - ) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - new_coldkey_parser = parser.add_parser( - "create", - help="""Creates a new coldkey (for containing balance) under the specified path. """, - ) - new_coldkey_parser.add_argument( - "--n_words", - type=int, - choices=[12, 15, 18, 21, 24], - default=12, - help="""The number of words representing the mnemonic. i.e. horse cart dog ... x 24""", - ) - new_coldkey_parser.add_argument( - "--use_password", - dest="use_password", - action="store_true", - help="""Set true to protect the generated bittensor key with a password.""", - default=True, - ) - new_coldkey_parser.add_argument( - "--no_password", - dest="use_password", - action="store_false", - help="""Set off protects the generated bittensor key with a password.""", - ) - new_coldkey_parser.add_argument( - "--overwrite_coldkey", - action="store_true", - default=False, - help="""Overwrite the old coldkey with the newly generated coldkey""", - ) - new_coldkey_parser.add_argument( - "--overwrite_hotkey", - action="store_true", - default=False, - help="""Overwrite the old hotkey with the newly generated hotkey""", - ) - Wallet.add_args(new_coldkey_parser) - Subtensor.add_args(new_coldkey_parser) - - -def _get_coldkey_wallets_for_path(path: str) -> List["Wallet"]: - """Get all coldkey wallet names from path.""" - try: - wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [Wallet(path=path, name=name) for name in wallet_names] - except StopIteration: - # No wallet files found. - wallets = [] - return wallets - - -class UpdateWalletCommand: - """ - Executes the ``update`` command to check and potentially update the security of the wallets in the Bittensor network. - - This command is used to enhance wallet security using modern encryption standards. - - Usage: - The command checks if any of the wallets need an update in their security protocols. - It supports updating all legacy wallets or a specific one based on the user's choice. - - Optional arguments: - - ``--all`` (bool): When set, updates all legacy wallets. - - Example usage:: - - btcli wallet update --all - - Note: - This command is important for maintaining the highest security standards for users' wallets. - It is recommended to run this command periodically to ensure wallets are up-to-date with the latest security practices. - """ - - @staticmethod - def run(cli): - """Check if any of the wallets needs an update.""" - config = cli.config.copy() - if config.get("all", d=False) == True: - wallets = _get_coldkey_wallets_for_path(config.wallet.path) - else: - wallets = [Wallet(config=config)] - - for wallet in wallets: - print("\n===== ", wallet, " =====") - wallet.coldkey_file.check_and_update_encryption() - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - update_wallet_parser = parser.add_parser( - "update", - help="""Updates the wallet security using NaCL instead of ansible vault.""", - ) - update_wallet_parser.add_argument("--all", action="store_true") - Wallet.add_args(update_wallet_parser) - Subtensor.add_args(update_wallet_parser) - - @staticmethod - def check_config(config: "bittensor.Config"): - if config.get("all", d=False) == False: - if not config.no_prompt: - if Confirm.ask("Do you want to update all legacy wallets?"): - config["all"] = True - - # Ask the user to specify the wallet if the wallet name is not clear. - if ( - config.get("all", d=False) == False - and config.wallet.get("name") == defaults.wallet.name - and not config.no_prompt - ): - wallet_name = Prompt.ask( - "Enter wallet name", default=defaults.wallet.name - ) - config.wallet.name = str(wallet_name) - - -def _get_coldkey_ss58_addresses_for_path(path: str) -> Tuple[List[str], List[str]]: - """Get all coldkey ss58 addresses from path.""" - - def list_coldkeypub_files(dir_path): - abspath = os.path.abspath(os.path.expanduser(dir_path)) - coldkey_files = [] - wallet_names = [] - - for potential_wallet_name in os.listdir(abspath): - coldkey_path = os.path.join( - abspath, potential_wallet_name, "coldkeypub.txt" - ) - if os.path.isdir( - os.path.join(abspath, potential_wallet_name) - ) and os.path.exists(coldkey_path): - coldkey_files.append(coldkey_path) - wallet_names.append(potential_wallet_name) - else: - logging.warning( - f"{coldkey_path} does not exist. Excluding..." - ) - return coldkey_files, wallet_names - - coldkey_files, wallet_names = list_coldkeypub_files(path) - addresses = [ - Keyfile(coldkey_path).keypair.ss58_address - for coldkey_path in coldkey_files - ] - return addresses, wallet_names - - -class WalletBalanceCommand: - """ - Executes the ``balance`` command to check the balance of the wallet on the Bittensor network. - - This command provides a detailed view of the wallet's coldkey balances, including free and staked balances. - - Usage: - The command lists the balances of all wallets in the user's configuration directory, showing the wallet name, coldkey address, and the respective free and staked balances. - - Optional arguments: - None. The command uses the wallet and subtensor configurations to fetch balance data. - - Example usages: - - - To display the balance of a single wallet, use the command with the `--wallet.name` argument to specify the wallet name: - - ``` - btcli w balance --wallet.name WALLET - ``` - - - Alternatively, you can invoke the command without specifying a wallet name, which will prompt you to enter the wallets path: - - ``` - btcli w balance - ``` - - - To display the balances of all wallets, use the `--all` argument: - - ``` - btcli w balance --all - ``` - - Note: - When using `btcli`, `w` is used interchangeably with `wallet`. You may use either based on your preference for brevity or clarity. - This command is essential for users to monitor their financial status on the Bittensor network. - It helps in keeping track of assets and ensuring the wallet's financial health. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Check the balance of the wallet.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - WalletBalanceCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "Subtensor"): - wallet = Wallet(config=cli.config) - - wallet_names = [] - coldkeys = [] - free_balances = [] - staked_balances = [] - total_free_balance = 0 - total_staked_balance = 0 - balances = {} - - if cli.config.get("all", d=None): - coldkeys, wallet_names = _get_coldkey_ss58_addresses_for_path( - cli.config.wallet.path - ) - - free_balances = [ - subtensor.get_balance(coldkeys[i]) for i in range(len(coldkeys)) - ] - - staked_balances = [ - subtensor.get_total_stake_for_coldkey(coldkeys[i]) - for i in range(len(coldkeys)) - ] - - total_free_balance = sum(free_balances) - total_staked_balance = sum(staked_balances) - - balances = { - name: (coldkey, free, staked) - for name, coldkey, free, staked in sorted( - zip(wallet_names, coldkeys, free_balances, staked_balances) - ) - } - else: - coldkey_wallet = Wallet(config=cli.config) - if ( - coldkey_wallet.coldkeypub_file.exists_on_device() - and not coldkey_wallet.coldkeypub_file.is_encrypted() - ): - coldkeys = [coldkey_wallet.coldkeypub.ss58_address] - wallet_names = [coldkey_wallet.name] - - free_balances = [ - subtensor.get_balance(coldkeys[i]) for i in range(len(coldkeys)) - ] - - staked_balances = [ - subtensor.get_total_stake_for_coldkey(coldkeys[i]) - for i in range(len(coldkeys)) - ] - - total_free_balance = sum(free_balances) - total_staked_balance = sum(staked_balances) - - balances = { - name: (coldkey, free, staked) - for name, coldkey, free, staked in sorted( - zip(wallet_names, coldkeys, free_balances, staked_balances) - ) - } - - if not coldkey_wallet.coldkeypub_file.exists_on_device(): - bt_console.print("[bold red]No wallets found.") - return - - table = Table(show_footer=False) - table.title = "[white]Wallet Coldkey Balances" - table.add_column( - "[white]Wallet Name", - header_style="overline white", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - - table.add_column( - "[white]Coldkey Address", - header_style="overline white", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - - for typestr in ["Free", "Staked", "Total"]: - table.add_column( - f"[white]{typestr} Balance", - header_style="overline white", - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - - for name, (coldkey, free, staked) in balances.items(): - table.add_row( - name, - coldkey, - str(free), - str(staked), - str(free + staked), - ) - table.add_row() - table.add_row( - "Total Balance Across All Coldkeys", - "", - str(total_free_balance), - str(total_staked_balance), - str(total_free_balance + total_staked_balance), - ) - table.show_footer = True - - table.box = None - table.pad_edge = False - table.width = None - bt_console.print(table) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - balance_parser = parser.add_parser( - "balance", help="""Checks the balance of the wallet.""" - ) - balance_parser.add_argument( - "--all", - dest="all", - action="store_true", - help="""View balance for all wallets.""", - default=False, - ) - - Wallet.add_args(balance_parser) - Subtensor.add_args(balance_parser) - - @staticmethod - def check_config(config: "Config"): - if ( - not config.is_set("wallet.path") - and not config.no_prompt - and not config.get("all", d=None) - ): - path = Prompt.ask("Enter wallets path", default=defaults.wallet.path) - config.wallet.path = str(path) - - if ( - not config.is_set("wallet.name") - and not config.no_prompt - and not config.get("all", d=None) - ): - wallet_name = Prompt.ask( - "Enter wallet name", default=defaults.wallet.name - ) - config.wallet.name = str(wallet_name) - - if not config.is_set("subtensor.network") and not config.no_prompt: - network = Prompt.ask( - "Enter network", - default=defaults.subtensor.network, - choices=networks, - ) - config.subtensor.network = str(network) - ( - _, - config.subtensor.chain_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network(str(network)) - - -API_URL = "https://api.subquery.network/sq/TaoStats/bittensor-indexer" -MAX_TXN = 1000 -GRAPHQL_QUERY = """ -query ($first: Int!, $after: Cursor, $filter: TransferFilter, $order: [TransfersOrderBy!]!) { - transfers(first: $first, after: $after, filter: $filter, orderBy: $order) { - nodes { - id - from - to - amount - extrinsicId - blockNumber - } - pageInfo { - endCursor - hasNextPage - hasPreviousPage - } - totalCount - } -} -""" - - -class GetWalletHistoryCommand: - """ - Executes the ``history`` command to fetch the latest transfers of the provided wallet on the Bittensor network. - - This command provides a detailed view of the transfers carried out on the wallet. - - Usage: - The command lists the latest transfers of the provided wallet, showing the From, To, Amount, Extrinsic Id and Block Number. - - Optional arguments: - None. The command uses the wallet and subtensor configurations to fetch latest transfer data associated with a wallet. - - Example usage:: - - btcli wallet history - - Note: - This command is essential for users to monitor their financial status on the Bittensor network. - It helps in fetching info on all the transfers so that user can easily tally and cross check the transactions. - """ - - @staticmethod - def run(cli): - r"""Check the transfer history of the provided wallet.""" - wallet = Wallet(config=cli.config) - wallet_address = wallet.get_coldkeypub().ss58_address - # Fetch all transfers - transfers = get_wallet_transfers(wallet_address) - - # Create output table - table = create_transfer_history_table(transfers) - - bt_console.print(table) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - history_parser = parser.add_parser( - "history", - help="""Fetch transfer history associated with the provided wallet""", - ) - Wallet.add_args(history_parser) - Subtensor.add_args(history_parser) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - -def get_wallet_transfers(wallet_address) -> List[dict]: - """Get all transfers associated with the provided wallet address.""" - - variables = { - "first": MAX_TXN, - "filter": { - "or": [ - {"from": {"equalTo": wallet_address}}, - {"to": {"equalTo": wallet_address}}, - ] - }, - "order": "BLOCK_NUMBER_DESC", - } - - response = requests.post( - API_URL, json={"query": GRAPHQL_QUERY, "variables": variables} - ) - data = response.json() - - # Extract nodes and pageInfo from the response - transfer_data = data.get("data", {}).get("transfers", {}) - transfers = transfer_data.get("nodes", []) - - return transfers - - -def create_transfer_history_table(transfers): - """Get output transfer table""" - - table = Table(show_footer=False) - # Define the column names - column_names = [ - "Id", - "From", - "To", - "Amount (Tao)", - "Extrinsic Id", - "Block Number", - "URL (taostats)", - ] - taostats_url_base = "https://x.taostats.io/extrinsic" - - # Create a table - table = Table(show_footer=False) - table.title = "[white]Wallet Transfers" - - # Define the column styles - header_style = "overline white" - footer_style = "overline white" - column_style = "rgb(50,163,219)" - no_wrap = True - - # Add columns to the table - for column_name in column_names: - table.add_column( - f"[white]{column_name}", - header_style=header_style, - footer_style=footer_style, - style=column_style, - no_wrap=no_wrap, - justify="left" if column_name == "Id" else "right", - ) - - # Add rows to the table - for item in transfers: - try: - tao_amount = int(item["amount"]) / RAOPERTAO - except: - tao_amount = item["amount"] - table.add_row( - item["id"], - item["from"], - item["to"], - f"{tao_amount:.3f}", - str(item["extrinsicId"]), - item["blockNumber"], - f"{taostats_url_base}/{item['blockNumber']}-{item['extrinsicId']}", - ) - table.add_row() - table.show_footer = True - table.box = None - table.pad_edge = False - table.width = None - return table diff --git a/bittensor/btcli/commands/weights.py b/bittensor/btcli/commands/weights.py deleted file mode 100644 index ae311cf56..000000000 --- a/bittensor/btcli/commands/weights.py +++ /dev/null @@ -1,294 +0,0 @@ -# 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. - -"""Module that encapsulates the CommitWeightCommand and the RevealWeightCommand. Used to commit and reveal weights -for a specific subnet on the Bittensor Network.""" - -import argparse -import os -import re - -import numpy as np -from bittensor_wallet import Wallet -from rich.prompt import Prompt, Confirm - -from bittensor.utils import weight_utils -from bittensor.core.config import Config -from bittensor.core.settings import bt_console -from bittensor.core.settings import version_as_int -from bittensor.core.subtensor import Subtensor -from bittensor.utils.btlogging import logging -from . import defaults # type: ignore - - -class CommitWeightCommand: - """ - Executes the ``commit`` command to commit weights for specific subnet on the Bittensor network. - - Usage: - The command allows committing weights for a specific subnet. Users need to specify the netuid (network unique identifier), corresponding UIDs, and weights they wish to commit. - - Optional arguments: - - ``--netuid`` (int): The netuid of the subnet for which weights are to be commited. - - ``--uids`` (str): Corresponding UIDs for the specified netuid, in comma-separated format. - - ``--weights`` (str): Corresponding weights for the specified UIDs, in comma-separated format. - - Example usage: - $ btcli wt commit --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 - - Note: - This command is used to commit weights for a specific subnet and requires the user to have the necessary permissions. - """ - - @staticmethod - def run(cli): - r"""Commit weights for a specific subnet.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - CommitWeightCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Commit weights for a specific subnet""" - wallet = Wallet(config=cli.config) - - # Get values if not set - if not cli.config.is_set("netuid"): - cli.config.netuid = int(Prompt.ask(f"Enter netuid")) - - if not cli.config.is_set("uids"): - cli.config.uids = Prompt.ask(f"Enter UIDs (comma-separated)") - - if not cli.config.is_set("weights"): - cli.config.weights = Prompt.ask(f"Enter weights (comma-separated)") - - # Parse from string - netuid = cli.config.netuid - uids = np.array( - [int(x) for x in re.split(r"[ ,]+", cli.config.uids)], dtype=np.int64 - ) - weights = np.array( - [float(x) for x in re.split(r"[ ,]+", cli.config.weights)], dtype=np.float32 - ) - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - if not cli.config.is_set("salt"): - # Generate random salt - salt_length = 8 - salt = list(os.urandom(salt_length)) - - if not Confirm.ask( - f"Have you recorded the [red]salt[/red]: [bold white]'{salt}'[/bold white]? It will be " - f"required to reveal weights." - ): - return False, "User cancelled the operation." - else: - salt = np.array( - [int(x) for x in re.split(r"[ ,]+", cli.config.salt)], - dtype=np.int64, - ).tolist() - - # Run the commit weights operation - success, message = subtensor.commit_weights( - wallet=wallet, - netuid=netuid, - uids=weight_uids, - weights=weight_vals, - salt=salt, - wait_for_inclusion=cli.config.wait_for_inclusion, - wait_for_finalization=cli.config.wait_for_finalization, - prompt=cli.config.prompt, - ) - - # Result - if success: - bt_console.print(f"Weights committed successfully") - else: - bt_console.print(f"Failed to commit weights: {message}") - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "commit", help="""Commit weights for a specific subnet.""" - ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) - parser.add_argument("--uids", dest="uids", type=str, required=False) - parser.add_argument("--weights", dest="weights", type=str, required=False) - parser.add_argument("--salt", dest="salt", type=str, required=False) - parser.add_argument( - "--wait-for-inclusion", - dest="wait_for_inclusion", - action="store_true", - default=False, - ) - parser.add_argument( - "--wait-for-finalization", - dest="wait_for_finalization", - action="store_true", - default=True, - ) - parser.add_argument( - "--prompt", - dest="prompt", - action="store_true", - default=False, - ) - - Wallet.add_args(parser) - Subtensor.add_args(parser) - - @staticmethod - def check_config(config: "Config"): - if not config.no_prompt and not config.is_set("wallet.name"): - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.no_prompt and not config.is_set("wallet.hotkey"): - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - -class RevealWeightCommand: - """ - Executes the ``reveal`` command to reveal weights for a specific subnet on the Bittensor network. - Usage: - The command allows revealing weights for a specific subnet. Users need to specify the netuid (network unique identifier), corresponding UIDs, and weights they wish to reveal. - Optional arguments: - - ``--netuid`` (int): The netuid of the subnet for which weights are to be revealed. - - ``--uids`` (str): Corresponding UIDs for the specified netuid, in comma-separated format. - - ``--weights`` (str): Corresponding weights for the specified UIDs, in comma-separated format. - - ``--salt`` (str): Corresponding salt for the hash function, integers in comma-separated format. - Example usage:: - $ btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189 - Note: - This command is used to reveal weights for a specific subnet and requires the user to have the necessary permissions. - """ - - @staticmethod - def run(cli): - r"""Reveal weights for a specific subnet.""" - try: - subtensor: "Subtensor" = Subtensor( - config=cli.config, log_verbose=False - ) - RevealWeightCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli, subtensor: "Subtensor"): - r"""Reveal weights for a specific subnet.""" - wallet = Wallet(config=cli.config) - - # Get values if not set. - if not cli.config.is_set("netuid"): - cli.config.netuid = int(Prompt.ask(f"Enter netuid")) - - if not cli.config.is_set("uids"): - cli.config.uids = Prompt.ask(f"Enter UIDs (comma-separated)") - - if not cli.config.is_set("weights"): - cli.config.weights = Prompt.ask(f"Enter weights (comma-separated)") - - if not cli.config.is_set("salt"): - cli.config.salt = Prompt.ask(f"Enter salt (comma-separated)") - - # Parse from string - netuid = cli.config.netuid - version = version_as_int - uids = np.array( - [int(x) for x in re.split(r"[ ,]+", cli.config.uids)], - dtype=np.int64, - ) - weights = np.array( - [float(x) for x in re.split(r"[ ,]+", cli.config.weights)], - dtype=np.float32, - ) - salt = np.array( - [int(x) for x in re.split(r"[ ,]+", cli.config.salt)], - dtype=np.int64, - ) - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - # Run the reveal weights operation. - success, message = subtensor.reveal_weights( - wallet=wallet, - netuid=netuid, - uids=weight_uids, - weights=weight_vals, - salt=salt, - version_key=version, - wait_for_inclusion=cli.config.wait_for_inclusion, - wait_for_finalization=cli.config.wait_for_finalization, - prompt=cli.config.prompt, - ) - - if success: - bt_console.print(f"Weights revealed successfully") - else: - bt_console.print(f"Failed to reveal weights: {message}") - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "reveal", help="""Reveal weights for a specific subnet.""" - ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) - parser.add_argument("--uids", dest="uids", type=str, required=False) - parser.add_argument("--weights", dest="weights", type=str, required=False) - parser.add_argument("--salt", dest="salt", type=str, required=False) - parser.add_argument( - "--wait-for-inclusion", - dest="wait_for_inclusion", - action="store_true", - default=False, - ) - parser.add_argument( - "--wait-for-finalization", - dest="wait_for_finalization", - action="store_true", - default=True, - ) - parser.add_argument( - "--prompt", - dest="prompt", - action="store_true", - default=False, - ) - - Wallet.add_args(parser) - Subtensor.add_args(parser) - - @staticmethod - def check_config(config: "Config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) diff --git a/setup.py b/setup.py index 43cbabe83..37d3d07b2 100644 --- a/setup.py +++ b/setup.py @@ -15,18 +15,18 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from setuptools import setup, find_packages -from os import path -from io import open import codecs -import re import os import pathlib +import re +from io import open +from os import path + +from setuptools import setup, find_packages def read_requirements(path): requirements = [] - git_requirements = [] with pathlib.Path(path).open() as requirements_txt: for line in requirements_txt: @@ -77,7 +77,6 @@ def read_requirements(path): "dev": extra_requirements_dev, "torch": extra_requirements_torch, }, - scripts=["bin/btcli"], classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py index e69de29bb..640a13250 100644 --- a/tests/integration_tests/__init__.py +++ b/tests/integration_tests/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py deleted file mode 100644 index 174917042..000000000 --- a/tests/integration_tests/test_cli.py +++ /dev/null @@ -1,2741 +0,0 @@ -# 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 os -import random -import unittest -from copy import deepcopy -from types import SimpleNamespace -from typing import Dict -from unittest.mock import MagicMock, patch - -import pytest -from bittensor_wallet import Wallet -from bittensor_wallet.mock import get_mock_keypair, get_mock_wallet as generate_wallet - -from bittensor.btcli.cli import COMMANDS as ALL_COMMANDS -from bittensor.btcli.cli import cli as btcli -from bittensor.btcli.commands.delegates import _get_coldkey_wallets_for_path -from bittensor.btcli.commands.identity import SetIdentityCommand -from bittensor.btcli.commands.wallets import _get_coldkey_ss58_addresses_for_path -from bittensor.core.config import Config -from bittensor.core.subtensor import Subtensor -from bittensor.mock import MockSubtensor -from bittensor.utils.balance import Balance -from tests.helpers import is_running_in_circleci, MockConsole - -_subtensor_mock: MockSubtensor = MockSubtensor() - - -def setUpModule(): - _subtensor_mock.reset() - - _subtensor_mock.create_subnet(netuid=1) - _subtensor_mock.create_subnet(netuid=2) - _subtensor_mock.create_subnet(netuid=3) - - # Set diff 0 - _subtensor_mock.set_difficulty(netuid=1, difficulty=0) - _subtensor_mock.set_difficulty(netuid=2, difficulty=0) - _subtensor_mock.set_difficulty(netuid=3, difficulty=0) - - -def return_mock_sub(*args, **kwargs): - return MockSubtensor - - -@patch("bittensor.core.subtensor.Subtensor", new_callable=return_mock_sub) -class TestCLIWithNetworkAndConfig(unittest.TestCase): - def setUp(self): - self._config = TestCLIWithNetworkAndConfig.construct_config() - - @property - def config(self): - copy_ = deepcopy(self._config) - return copy_ - - @staticmethod - def construct_config(): - parser = btcli.__create_parser__() - defaults = Config(parser=parser, args=[]) - # Parse commands and subcommands - for command in ALL_COMMANDS: - if ( - command in ALL_COMMANDS - and "commands" in ALL_COMMANDS[command] - ): - for subcommand in ALL_COMMANDS[command]["commands"]: - defaults.merge( - Config(parser=parser, args=[command, subcommand]) - ) - else: - defaults.merge(Config(parser=parser, args=[command])) - - defaults.netuid = 1 - # Always use mock subtensor. - defaults.subtensor.network = "finney" - # Skip version checking. - defaults.no_version_checking = True - - return defaults - - def test_overview(self, _): - if is_running_in_circleci(): - config = self.config - config.wallet.path = "/tmp/test_cli_test_overview" - config.wallet.name = "mock_wallet" - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - - mock_hotkeys = ["hk0", "hk1", "hk2", "hk3", "hk4"] - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - coldkeypub_file=MagicMock( - exists_on_device=MagicMock(return_value=True) # Wallet exists - ), - ) - for idx, hk in enumerate(mock_hotkeys) - ] - - mock_registrations = [ - (1, mock_wallets[0]), - (1, mock_wallets[1]), - # (1, mock_wallets[2]), Not registered on netuid 1 - (2, mock_wallets[0]), - # (2, mock_wallets[1]), Not registered on netuid 2 - (2, mock_wallets[2]), - (3, mock_wallets[0]), - (3, mock_wallets[1]), - (3, mock_wallets[2]), # All registered on netuid 3 (but hk3) - (3, mock_wallets[4]), # hk4 is only on netuid 3 - ] # hk3 is not registered on any network - - # Register each wallet to it's subnet. - print("Registering wallets to mock subtensor...") - - for netuid, wallet in mock_registrations: - _ = _subtensor_mock.force_register_neuron( - netuid=netuid, - coldkey=wallet.coldkey.ss58_address, - hotkey=wallet.hotkey.ss58_address, - ) - - def mock_get_wallet(*args, **kwargs): - hk = kwargs.get("hotkey") - name_ = kwargs.get("name") - - if not hk and kwargs.get("config"): - hk = kwargs.get("config").wallet.hotkey - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_ and wallet.hotkey_str == hk: - return wallet - else: - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - return mock_wallets[0] - - mock_console = MockConsole() - with patch( - "bittensor.btcli.commands.overview.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - with patch("bittensor.core.settings.bt_console", mock_console): - cli.run() - - # Check that the overview was printed. - self.assertIsNotNone(mock_console.captured_print) - - output_no_syntax = mock_console.remove_rich_syntax( - mock_console.captured_print - ) - - # Check that each subnet was printed. - self.assertIn("Subnet: 1", output_no_syntax) - self.assertIn("Subnet: 2", output_no_syntax) - self.assertIn("Subnet: 3", output_no_syntax) - - # Check that only registered hotkeys are printed once for each subnet. - for wallet in mock_wallets: - expected = [ - wallet.hotkey_str for _, wallet in mock_registrations - ].count(wallet.hotkey_str) - occurrences = output_no_syntax.count(wallet.hotkey_str) - self.assertEqual(occurrences, expected) - - # Check that unregistered hotkeys are not printed. - for wallet in mock_wallets: - if wallet not in [w for _, w in mock_registrations]: - self.assertNotIn(wallet.hotkey_str, output_no_syntax) - - def test_overview_not_in_first_subnet(self, _): - if is_running_in_circleci(): - config = self.config - config.wallet.path = "/tmp/test_cli_test_overview" - config.wallet.name = "mock_wallet" - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - - mock_hotkeys = ["hk0", "hk1", "hk2", "hk3", "hk4"] - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - coldkeypub_file=MagicMock( - exists_on_device=MagicMock(return_value=True) # Wallet exists - ), - ) - for idx, hk in enumerate(mock_hotkeys) - ] - - mock_registrations = [ - # No registrations in subnet 1 or 2 - (3, mock_wallets[4]) # hk4 is on netuid 3 - ] - - # Register each wallet to it's subnet - print("Registering mock wallets to subnets...") - - for netuid, wallet in mock_registrations: - print( - "Registering wallet {} to subnet {}".format( - wallet.hotkey_str, netuid - ) - ) - _ = _subtensor_mock.force_register_neuron( - netuid=netuid, - coldkey=wallet.coldkey.ss58_address, - hotkey=wallet.hotkey.ss58_address, - ) - - def mock_get_wallet(*args, **kwargs): - hk = kwargs.get("hotkey") - name_ = kwargs.get("name") - - if not hk and kwargs.get("config"): - hk = kwargs.get("config").wallet.hotkey - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_ and wallet.hotkey_str == hk: - return wallet - else: - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - return mock_wallets[0] - - mock_console = MockConsole() - with patch( - "bittensor.btcli.commands.overview.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - with patch("bittensor.core.settings.bt_console", mock_console): - cli.run() - - # Check that the overview was printed. - self.assertIsNotNone(mock_console.captured_print) - - output_no_syntax = mock_console.remove_rich_syntax( - mock_console.captured_print - ) - - # Check that each subnet was printed except subnet 1 and 2. - # Subnet 1 and 2 are not printed because no wallet is registered to them. - self.assertNotIn("Subnet: 1", output_no_syntax) - self.assertNotIn("Subnet: 2", output_no_syntax) - self.assertIn("Subnet: 3", output_no_syntax) - - # Check that only registered hotkeys are printed once for each subnet. - for wallet in mock_wallets: - expected = [ - wallet.hotkey_str for _, wallet in mock_registrations - ].count(wallet.hotkey_str) - occurrences = output_no_syntax.count(wallet.hotkey_str) - self.assertEqual(occurrences, expected) - - # Check that unregistered hotkeys are not printed. - for wallet in mock_wallets: - if wallet not in [w for _, w in mock_registrations]: - self.assertNotIn(wallet.hotkey_str, output_no_syntax) - - def test_overview_with_hotkeys_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.hotkeys = ["some_hotkey"] - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - cli.run() - - def test_overview_without_hotkeys_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - cli.run() - - def test_overview_with_sort_by_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.wallet.sort_by = "rank" - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - cli.run() - - def test_overview_with_sort_by_bad_column_name(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.wallet.sort_by = "totallynotmatchingcolumnname" - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - cli.run() - - def test_overview_without_sort_by_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - cli.run() - - def test_overview_with_sort_order_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.wallet.sort_order = "desc" # Set descending sort order - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - cli.run() - - def test_overview_with_sort_order_config_bad_sort_type(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.wallet.sort_order = "nowaythisshouldmatchanyorderingchoice" - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - cli.run() - - def test_overview_without_sort_order_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - # Don't specify sort_order in config - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - cli.run() - - def test_overview_with_width_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.width = 100 - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - cli.run() - - def test_overview_without_width_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - # Don't specify width in config - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = btcli(config) - cli.run() - - def test_overview_all(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.netuid = [] # Don't set, so it tries all networks. - - config.all = True - cli = btcli(config) - cli.run() - - def test_unstake_with_specific_hotkeys(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0", "hk1", "hk2"] - config.all_hotkeys = False - # Notice no max_stake specified - - mock_stakes: Dict[str, Balance] = { - # All have more than 5.0 stake - "hk0": Balance.from_float(10.0), - "hk1": Balance.from_float(11.1), - "hk2": Balance.from_float(12.2), - } - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them stakes - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) - - cli.run() - - # Check stakes after unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertAlmostEqual( - stake.tao, - mock_stakes[wallet.hotkey_str].tao - config.amount, - places=4, - ) - - def test_unstake_with_all_hotkeys(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - # Notice wallet.hotkeys not specified - config.all_hotkeys = True - # Notice no max_stake specified - - mock_stakes: Dict[str, Balance] = { - # All have more than 5.0 stake - "hk0": Balance.from_float(10.0), - "hk1": Balance.from_float(11.1), - "hk2": Balance.from_float(12.2), - } - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(list(mock_stakes.keys())) - ] - - # Register mock wallets and give them stakes - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch( - "bittensor.btcli.commands.unstake.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) - - cli.run() - - # Check stakes after unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertAlmostEqual( - stake.tao, - mock_stakes[wallet.hotkey_str].tao - config.amount, - places=4, - ) - - def test_unstake_with_exclude_hotkeys_from_all(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk1"] # Exclude hk1 - config.all_hotkeys = True - - mock_stakes: Dict[str, Balance] = { - # All have more than 5.0 stake - "hk0": Balance.from_float(10.0), - "hk1": Balance.from_float(11.1), - "hk2": Balance.from_float(12.2), - } - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(list(mock_stakes.keys())) - ] - - # Register mock wallets and give them stakes - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch( - "bittensor.btcli.commands.unstake.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) - - cli.run() - - # Check stakes after unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - if wallet.hotkey_str == "hk1": - # hk1 should not have been unstaked - self.assertAlmostEqual( - stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 - ) - else: - self.assertAlmostEqual( - stake.tao, - mock_stakes[wallet.hotkey_str].tao - config.amount, - places=4, - ) - - def test_unstake_with_multiple_hotkeys_max_stake(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 5.0 # The keys should have at most 5.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0", "hk1", "hk2"] - config.all_hotkeys = False - - mock_stakes: Dict[str, Balance] = { - # All have more than 5.0 stake - "hk0": Balance.from_float(10.0), - "hk1": Balance.from_float(4.9), - "hk2": Balance.from_float(12.2), - } - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(list(mock_stakes.keys())) - ] - - # Register mock wallets and give them stakes - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch( - "bittensor.btcli.commands.unstake.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) - - cli.run() - - # Check stakes after unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # All should have been unstaked below or equal to max_stake - self.assertLessEqual( - stake.tao, config.max_stake + 0.0001 - ) # Add a small buffer for fp errors - - if wallet.hotkey_str == "hk1": - # hk1 should not have been unstaked because it was already below max_stake - self.assertAlmostEqual( - stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 - ) - - def test_unstake_with_thresholds(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - # as the minimum required stake may change, this method allows us to dynamically - # update the amount in the mock without updating the tests - min_stake: Balance = _subtensor_mock.get_minimum_required_stake() - # Must be a float - config.amount = min_stake.tao # Unstake below the minimum required stake - wallet_names = ["w0", "w1", "w2"] - config.all_hotkeys = False - # Notice no max_stake specified - - mock_stakes: Dict[str, Balance] = { - "w0": 2 * min_stake - 1, # remaining stake will be below the threshold - "w1": 2 * min_stake - 2, - "w2": 2 * min_stake - 5, - } - - mock_wallets = [ - SimpleNamespace( - name=wallet_name, - coldkey=get_mock_keypair(idx, self.id()), - coldkeypub=get_mock_keypair(idx, self.id()), - hotkey_str="hk{}".format(idx), # doesn't matter - hotkey=get_mock_keypair(idx + 100, self.id()), # doesn't matter - ) - for idx, wallet_name in enumerate(wallet_names) - ] - - delegate_hotkey = mock_wallets[0].hotkey.ss58_address - - # Register mock neuron, only for w0 - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=delegate_hotkey, - coldkey=mock_wallets[0].coldkey.ss58_address, - stake=mock_stakes["w0"], - ) - - # Become a delegate - _ = _subtensor_mock.nominate( - wallet=mock_wallets[0], - ) - - # Stake to the delegate with the other coldkeys - for wallet in mock_wallets[1:]: - # Give balance - _ = _subtensor_mock.force_set_balance( - ss58_address=wallet.coldkeypub.ss58_address, - balance=( - mock_stakes[wallet.name] + _subtensor_mock.get_existential_deposit() - ).tao - + 1.0, - ) - _ = _subtensor_mock.add_stake( - wallet=wallet, - hotkey_ss58=delegate_hotkey, - amount=mock_stakes[wallet.name], - ) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("config") and kwargs["config"].get("wallet"): - for wallet in mock_wallets: - if wallet.name == kwargs["config"].wallet.name: - return wallet - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - for wallet in mock_wallets: - # Check stakes before unstaking - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=delegate_hotkey, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.name].rao) - - config.wallet.name = wallet.name - config.hotkey_ss58address = delegate_hotkey # Single unstake - - cli = btcli(config) - with patch.object(_subtensor_mock, "_do_unstake") as mock_unstake: - with patch( - "bittensor.core.settings.bt_console.print" - ) as mock_print: # Catch console print - cli.run() - - # Filter for console print calls - console_prints = [ - call[0][0] for call in mock_print.call_args_list - ] - minimum_print = filter( - lambda x: "less than minimum of" in x, console_prints - ) - - unstake_calls = mock_unstake.call_args_list - self.assertEqual(len(unstake_calls), 1) # Only one unstake call - - _, kwargs = unstake_calls[0] - # Verify delegate was unstaked - self.assertEqual(kwargs["hotkey_ss58"], delegate_hotkey) - self.assertEqual(kwargs["wallet"].name, wallet.name) - - if wallet.name == "w0": - # This wallet owns the delegate - # Should unstake specified amount - self.assertEqual( - kwargs["amount"], Balance(config.amount) - ) - # No warning for w0 - self.assertRaises( - StopIteration, next, minimum_print - ) # No warning for w0 - else: - # Should unstake *all* the stake - staked = mock_stakes[wallet.name] - self.assertEqual(kwargs["amount"], staked) - - # Check warning was printed - _ = next( - minimum_print - ) # Doesn't raise, so the warning was printed - - def test_unstake_all(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - config.amount = 0.0 # 0 implies full unstake - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0"] - config.all_hotkeys = False - - mock_stakes: Dict[str, Balance] = {"hk0": Balance.from_float(10.0)} - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them stakes - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) - - cli.run() - - # Check stakes after unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # because the amount is less than the threshold, none of these should unstake - self.assertEqual(stake.tao, Balance.from_tao(0)) - - def test_stake_with_specific_hotkeys(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0", "hk1", "hk2"] - config.all_hotkeys = False - # Notice no max_stake specified - - mock_balance = Balance.from_float(22.2) - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - ) - - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, 0) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertAlmostEqual(stake.tao, config.amount, places=4) - - def test_stake_with_all_hotkeys(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - # Notice wallet.hotkeys is not specified - config.all_hotkeys = True - # Notice no max_stake specified - - mock_hotkeys = ["hk0", "hk1", "hk2"] - - mock_balance = Balance.from_float(22.0) - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(mock_hotkeys) - ] - - # Register mock wallets and give them no stake - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - # Set the coldkey balance - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - with patch( - "bittensor.btcli.commands.stake.get_hotkey_wallets_for_wallet" - ) as mock_get_hotkey_wallets_for_wallet: - mock_get_hotkey_wallets_for_wallet.return_value = mock_wallets - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 0 - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 5.0 - self.assertAlmostEqual(stake.tao, config.amount, places=4) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertAlmostEqual( - balance.tao, - mock_balance.tao - (config.amount * len(mock_wallets)), - places=4, - ) - - def test_stake_with_exclude_hotkeys_from_all(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk1"] # exclude hk1 - config.all_hotkeys = True - # Notice no max_stake specified - - mock_hotkeys = ["hk0", "hk1", "hk2"] - - mock_balance = Balance.from_float(25.0) - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(mock_hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - # Set the coldkey balance - _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch( - "bittensor.btcli.commands.stake.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 0 - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - if wallet.hotkey_str == "hk1": - # Check that hk1 stake is 0 - # We excluded it from staking - self.assertEqual(stake.tao, 0) - else: - # Check that all stakes are 5.0 - self.assertAlmostEqual(stake.tao, config.amount, places=4) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertAlmostEqual( - balance.tao, mock_balance.tao - (config.amount * 2), places=4 - ) - - def test_stake_with_multiple_hotkeys_max_stake(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0", "hk1", "hk2"] - config.all_hotkeys = False - - mock_balance = Balance.from_float(config.max_stake * 3) - - mock_stakes: Dict[str, Balance] = { - "hk0": Balance.from_float(0.0), - "hk1": Balance.from_float(config.max_stake * 2), - "hk2": Balance.from_float(0.0), - } - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - if wallet.hotkey_str == "hk1": - # Set the stake for hk1 - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - else: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are correct - if wallet.hotkey_str == "hk1": - self.assertAlmostEqual(stake.tao, config.max_stake * 2, places=4) - else: - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - # Check that all stakes at least 15.0 - self.assertGreaterEqual(stake.tao + 0.1, config.max_stake) - - if wallet.hotkey_str == "hk1": - # Check that hk1 stake was not changed - # It had more than max_stake already - self.assertAlmostEqual( - stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 - ) - - # Check that the balance decreased - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertLessEqual(balance.tao, mock_balance.tao) - - def test_stake_with_multiple_hotkeys_max_stake_not_enough_balance(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0", "hk1", "hk2"] - config.all_hotkeys = False - - mock_balance = Balance.from_float(15.0 * 2) # Not enough for all hotkeys - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 0 - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - if wallet.hotkey_str == "hk2": - # Check that the stake is still 0 - self.assertEqual(stake.tao, 0) - - else: - # Check that all stakes are maximum of 15.0 - self.assertLessEqual(stake.tao, config.max_stake) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertLessEqual(balance.tao, mock_balance.tao) - - def test_stake_with_single_hotkey_max_stake(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0"] - config.all_hotkeys = False - - mock_balance = Balance.from_float(15.0 * 3) - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 0 - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - # Check that all stakes are maximum of 15.0 - self.assertLessEqual(stake.tao, config.max_stake) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertLessEqual(balance.tao, mock_balance.tao) - - def test_stake_with_single_hotkey_max_stake_not_enough_balance(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0"] - config.all_hotkeys = False - - mock_balance = Balance.from_float(1.0) # Not enough balance to do max - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 0 - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - wallet = mock_wallets[0] - - # Check did not stake - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - # Check that stake is less than max_stake - 1.0 - self.assertLessEqual(stake.tao, config.max_stake - 1.0) - - # Check that the balance decreased by less than max_stake - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertGreaterEqual(balance.tao, mock_balance.tao - config.max_stake) - - def test_stake_with_single_hotkey_max_stake_enough_stake(self, _): - # tests max stake when stake >= max_stake already - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0"] - config.all_hotkeys = False - - mock_balance = Balance.from_float(config.max_stake * 3) - - mock_stakes: Dict[str, Balance] = { # has enough stake, more than max_stake - "hk0": Balance.from_float(config.max_stake * 2) - } - - mock_coldkey_kp = get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, # More than max_stake - ) - - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - wallet = mock_wallets[0] - - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that stake is correct - self.assertAlmostEqual( - stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 - ) - # Check that the stake is greater than or equal to max_stake - self.assertGreaterEqual(stake.tao, config.max_stake) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - wallet = mock_wallets[0] - - # Check did not stake, since stake >= max_stake - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - # Check that all stake is unchanged - self.assertAlmostEqual( - stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 - ) - - # Check that the balance is the same - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - def test_stake_with_thresholds(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - - min_stake: Balance = _subtensor_mock.get_minimum_required_stake() - # Must be a float - wallet_names = ["w0", "w1", "w2"] - config.all_hotkeys = False - # Notice no max_stake specified - - mock_stakes: Dict[str, Balance] = { - "w0": min_stake - 1, # new stake will be below the threshold - "w1": min_stake - 2, - "w2": min_stake - 5, - } - - mock_wallets = [ - SimpleNamespace( - name=wallet_name, - coldkey=get_mock_keypair(idx, self.id()), - coldkeypub=get_mock_keypair(idx, self.id()), - hotkey_str="hk{}".format(idx), # doesn't matter - hotkey=get_mock_keypair(idx + 100, self.id()), # doesn't matter - ) - for idx, wallet_name in enumerate(wallet_names) - ] - - delegate_hotkey = mock_wallets[0].hotkey.ss58_address - - # Register mock neuron, only for w0 - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=delegate_hotkey, - coldkey=mock_wallets[0].coldkey.ss58_address, - balance=(mock_stakes["w0"] + _subtensor_mock.get_existential_deposit()).tao - + 1.0, - ) # No stake, but enough balance - - # Become a delegate - _ = _subtensor_mock.nominate( - wallet=mock_wallets[0], - ) - - # Give enough balance - for wallet in mock_wallets[1:]: - # Give balance - _ = _subtensor_mock.force_set_balance( - ss58_address=wallet.coldkeypub.ss58_address, - balance=( - mock_stakes[wallet.name] + _subtensor_mock.get_existential_deposit() - ).tao - + 1.0, - ) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("config") and kwargs["config"].get("wallet"): - for wallet in mock_wallets: - if wallet.name == kwargs["config"].wallet.name: - return wallet - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - for wallet in mock_wallets: - # Check balances and stakes before staking - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=delegate_hotkey, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, 0) # No stake - - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertGreaterEqual( - balance, mock_stakes[wallet.name] - ) # Enough balance - - config.wallet.name = wallet.name - config.wallet.hotkey = delegate_hotkey # Single stake - config.amount = mock_stakes[ - wallet.name - ].tao # Stake an amount below the threshold - - cli = btcli(config) - with patch.object(_subtensor_mock, "_do_stake") as mock_stake: - with patch( - "bittensor.core.settings.bt_console.print" - ) as mock_print: # Catch console print - cli.run() - - # Filter for console print calls - console_prints = [ - call[0][0] for call in mock_print.call_args_list - ] - minimum_print = filter( - lambda x: "below the minimum required" in x, console_prints - ) - - if wallet.name == "w0": - # This wallet owns the delegate - stake_calls = mock_stake.call_args_list - # Can stake below the threshold - self.assertEqual(len(stake_calls), 1) - - _, kwargs = stake_calls[0] - - # Should stake specified amount - self.assertEqual( - kwargs["amount"], Balance(config.amount) - ) - # No error for w0 - self.assertRaises( - StopIteration, next, minimum_print - ) # No warning for w0 - else: - # Should not call stake - self.assertEqual(len(mock_stake.call_args_list), 0) - # Should print error - self.assertIsNotNone(next(minimum_print)) - - def test_nominate(self, _): - config = self.config - config.command = "root" - config.subcommand = "nominate" - config.no_prompt = True - config.wallet.name = "w0" - config.hotkey = "hk0" - - mock_balance = Balance.from_float(100.0) - - mock_wallet = SimpleNamespace( - name="w0", - coldkey=get_mock_keypair(0, self.id()), - coldkeypub=get_mock_keypair(0, self.id()), - hotkey_str="hk0", - hotkey=get_mock_keypair(0 + 100, self.id()), - ) - - # Register mock wallet and give it a balance - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=mock_wallet.hotkey.ss58_address, - coldkey=mock_wallet.coldkey.ss58_address, - balance=mock_balance.rao, - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - hk = kwargs.get("hotkey") - name_ = kwargs.get("name") - - if not hk and kwargs.get("config"): - hk = kwargs.get("config").wallet.hotkey - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - if mock_wallet.name == name_: - return mock_wallet - else: - raise ValueError("Mock wallet not found") - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - cli.run() - - # Check the nomination - is_delegate = _subtensor_mock.is_hotkey_delegate( - hotkey_ss58=mock_wallet.hotkey.ss58_address - ) - self.assertTrue(is_delegate) - - def test_delegate_stake(self, _): - config = self.config - config.command = "root" - config.subcommand = "delegate" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "w1" - - mock_balances: Dict[str, Balance] = { - # All have more than 5.0 stake - "w0": { - "hk0": Balance.from_float(10.0), - }, - "w1": {"hk1": Balance.from_float(11.1)}, - } - - mock_stake = Balance.from_float(5.0) - - mock_wallets = [] - for idx, wallet_name in enumerate(list(mock_balances.keys())): - for idx_hk, hk in enumerate(list(mock_balances[wallet_name].keys())): - wallet = SimpleNamespace( - name=wallet_name, - coldkey=get_mock_keypair(idx, self.id()), - coldkeypub=get_mock_keypair(idx, self.id()), - hotkey_str=hk, - hotkey=get_mock_keypair(idx * 100 + idx_hk, self.id()), - ) - mock_wallets.append(wallet) - - # Set hotkey to be the hotkey from the other wallet - config.delegate_ss58key: str = mock_wallets[0].hotkey.ss58_address - - # Register mock wallets and give them balance - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=mock_wallets[0].hotkey.ss58_address, - coldkey=mock_wallets[0].coldkey.ss58_address, - balance=mock_balances["w0"]["hk0"].rao, - stake=mock_stake.rao, # Needs set stake to be a validator - ) - - # Give w1 some balance - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_wallets[1].coldkey.ss58_address, - balance=mock_balances["w1"]["hk1"].rao, - ) - - # Make the first wallet a delegate - success = _subtensor_mock.nominate(wallet=mock_wallets[0]) - self.assertTrue(success) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - hk = kwargs.get("hotkey") - name_ = kwargs.get("name") - - if not hk and kwargs.get("config"): - hk = kwargs.get("config").wallet.hotkey - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_ and wallet.hotkey_str == hk: - return wallet - else: - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - cli.run() - - # Check the stake - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=mock_wallets[0].hotkey.ss58_address, - coldkey_ss58=mock_wallets[1].coldkey.ss58_address, - ) - self.assertAlmostEqual(stake.tao, config.amount, places=4) - - def test_undelegate_stake(self, _): - config = self.config - config.command = "root" - config.subcommand = "undelegate" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "w1" - - mock_balances: Dict[str, Balance] = { - # All have more than 5.0 stake - "w0": { - "hk0": Balance.from_float(10.0), - }, - "w1": {"hk1": Balance.from_float(11.1)}, - } - - mock_stake = Balance.from_float(5.0) - mock_delegated = Balance.from_float(6.0) - - mock_wallets = [] - for idx, wallet_name in enumerate(list(mock_balances.keys())): - for idx_hk, hk in enumerate(list(mock_balances[wallet_name].keys())): - wallet = SimpleNamespace( - name=wallet_name, - coldkey=get_mock_keypair(idx, self.id()), - coldkeypub=get_mock_keypair(idx, self.id()), - hotkey_str=hk, - hotkey=get_mock_keypair(idx * 100 + idx_hk, self.id()), - ) - mock_wallets.append(wallet) - - # Set hotkey to be the hotkey from the other wallet - config.delegate_ss58key: str = mock_wallets[0].hotkey.ss58_address - - # Register mock wallets and give them balance - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=mock_wallets[0].hotkey.ss58_address, - coldkey=mock_wallets[0].coldkey.ss58_address, - balance=mock_balances["w0"]["hk0"].rao, - stake=mock_stake.rao, # Needs set stake to be a validator - ) - - # Give w1 some balance - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_wallets[1].coldkey.ss58_address, - balance=mock_balances["w1"]["hk1"].rao, - ) - - # Make the first wallet a delegate - success = _subtensor_mock.nominate(wallet=mock_wallets[0]) - self.assertTrue(success) - - # Stake to the delegate - success = _subtensor_mock.delegate( - wallet=mock_wallets[1], - delegate_ss58=mock_wallets[0].hotkey.ss58_address, - amount=mock_delegated, - prompt=False, - ) - self.assertTrue(success) - - # Verify the stake - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=mock_wallets[0].hotkey.ss58_address, - coldkey_ss58=mock_wallets[1].coldkey.ss58_address, - ) - self.assertAlmostEqual(stake.tao, mock_delegated.tao, places=4) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - hk = kwargs.get("hotkey") - name_ = kwargs.get("name") - - if not hk and kwargs.get("config"): - hk = kwargs.get("config").wallet.hotkey - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_ and wallet.hotkey_str == hk: - return wallet - else: - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - return mock_wallets[0] - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - cli.run() - - # Check the stake - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=mock_wallets[0].hotkey.ss58_address, - coldkey_ss58=mock_wallets[1].coldkey.ss58_address, - ) - self.assertAlmostEqual( - stake.tao, mock_delegated.tao - config.amount, places=4 - ) - - def test_transfer(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "transfer" - config.no_prompt = True - config.amount = 3.2 - config.wallet.name = "w1" - - mock_balances: Dict[str, Balance] = { - "w0": Balance.from_float(10.0), - "w1": Balance.from_float(config.amount + 0.001), - } - - mock_wallets = [] - for idx, wallet_name in enumerate(list(mock_balances.keys())): - wallet = SimpleNamespace( - name=wallet_name, - coldkey=get_mock_keypair(idx, self.id()), - coldkeypub=get_mock_keypair(idx, self.id()), - ) - mock_wallets.append(wallet) - - # Set dest to w0 - config.dest = mock_wallets[0].coldkey.ss58_address - - # Give w0 and w1 balance - - for wallet in mock_wallets: - success, err = _subtensor_mock.force_set_balance( - ss58_address=wallet.coldkey.ss58_address, - balance=mock_balances[wallet.name].rao, - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - name_ = kwargs.get("name") - - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - raise ValueError(f"No mock wallet found with name: {name_}") - - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - cli.run() - - # Check the balance of w0 - balance = _subtensor_mock.get_balance( - address=mock_wallets[0].coldkey.ss58_address - ) - self.assertAlmostEqual( - balance.tao, mock_balances["w0"].tao + config.amount, places=4 - ) - - # Check the balance of w1 - balance = _subtensor_mock.get_balance( - address=mock_wallets[1].coldkey.ss58_address - ) - self.assertAlmostEqual( - balance.tao, mock_balances["w1"].tao - config.amount, places=4 - ) # no fees - - def test_transfer_not_enough_balance(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "transfer" - config.no_prompt = True - config.amount = 3.2 - config.wallet.name = "w1" - - mock_balances: Dict[str, Balance] = { - "w0": Balance.from_float(10.0), - "w1": Balance.from_float(config.amount - 0.1), # not enough balance - } - - mock_wallets = [] - for idx, wallet_name in enumerate(list(mock_balances.keys())): - wallet = SimpleNamespace( - name=wallet_name, - coldkey=get_mock_keypair(idx, self.id()), - coldkeypub=get_mock_keypair(idx, self.id()), - ) - mock_wallets.append(wallet) - - # Set dest to w0 - config.dest = mock_wallets[0].coldkey.ss58_address - - # Give w0 and w1 balance - - for wallet in mock_wallets: - success, err = _subtensor_mock.force_set_balance( - ss58_address=wallet.coldkey.ss58_address, - balance=mock_balances[wallet.name].rao, - ) - - cli = btcli(config) - - def mock_get_wallet(*args, **kwargs): - name_ = kwargs.get("name") - - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - raise ValueError(f"No mock wallet found with name: {name_}") - - mock_console = MockConsole() - with patch("bittensor_wallet.Wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - with patch("bittensor.core.settings.bt_console", mock_console): - cli.run() - - # Check that the overview was printed. - self.assertIsNotNone(mock_console.captured_print) - - output_no_syntax = mock_console.remove_rich_syntax( - mock_console.captured_print - ) - - self.assertIn("Not enough balance", output_no_syntax) - - # Check the balance of w0 - balance = _subtensor_mock.get_balance( - address=mock_wallets[0].coldkey.ss58_address - ) - self.assertAlmostEqual( - balance.tao, mock_balances["w0"].tao, places=4 - ) # did not transfer - - # Check the balance of w1 - balance = _subtensor_mock.get_balance( - address=mock_wallets[1].coldkey.ss58_address - ) - self.assertAlmostEqual( - balance.tao, mock_balances["w1"].tao, places=4 - ) # did not transfer - - def test_register(self, _): - config = self.config - config.command = "subnets" - config.subcommand = "register" - config.no_prompt = True - - mock_wallet = generate_wallet(hotkey=get_mock_keypair(100, self.id())) - - # Give the wallet some balance for burning - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_wallet.coldkeypub.ss58_address, - balance=Balance.from_float(200.0), - ) - - with patch("bittensor_wallet.Wallet", return_value=mock_wallet) as mock_create_wallet: - cli = btcli(config) - cli.run() - mock_create_wallet.assert_called_once() - - # Verify that the wallet was registered - subtensor = Subtensor(config) - registered = subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=mock_wallet.hotkey.ss58_address, netuid=1 - ) - - self.assertTrue(registered) - - def test_pow_register(self, _): - # Not the best way to do this, but I need to finish these tests, and unittest doesn't make this - # as simple as pytest - config = self.config - config.command = "subnets" - config.subcommand = "pow_register" - config.pow_register.num_processes = 1 - config.pow_register.update_interval = 50_000 - config.no_prompt = True - - mock_wallet = generate_wallet(hotkey=get_mock_keypair(100, self.id())) - - class MockException(Exception): - pass - - with patch("bittensor_wallet.Wallet", return_value=mock_wallet) as mock_create_wallet: - with patch( - "bittensor.utils.registration.POWSolution.is_stale", - side_effect=MockException, - ) as mock_is_stale: - with pytest.raises(MockException): - cli = btcli(config) - cli.run() - mock_create_wallet.assert_called_once() - - self.assertEqual(mock_is_stale.call_count, 1) - - def test_stake(self, _): - amount_to_stake: Balance = Balance.from_tao(0.5) - config = self.config - config.no_prompt = True - config.command = "stake" - config.subcommand = "add" - config.amount = amount_to_stake.tao - config.stake_all = False - config.use_password = False - config.model = "core_server" - config.hotkey = "hk0" - - subtensor = Subtensor(config) - - mock_wallet = generate_wallet(hotkey=get_mock_keypair(100, self.id())) - - # Register the hotkey and give it some balance - _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=mock_wallet.hotkey.ss58_address, - coldkey=mock_wallet.coldkey.ss58_address, - balance=( - amount_to_stake + Balance.from_tao(1.0) - ).rao, # 1.0 tao extra for fees, etc - ) - - with patch("bittensor_wallet.Wallet", return_value=mock_wallet) as mock_create_wallet: - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=mock_wallet.hotkey.ss58_address, - coldkey_ss58=mock_wallet.coldkey.ss58_address, - ) - - cli = btcli(config) - cli.run() - mock_create_wallet.assert_called() - self.assertEqual(mock_create_wallet.call_count, 2) - - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=mock_wallet.hotkey.ss58_address, - coldkey_ss58=mock_wallet.coldkey.ss58_address, - ) - - self.assertGreater(new_stake, old_stake) - - def test_metagraph(self, _): - config = self.config - config.wallet.name = "metagraph_testwallet" - config.command = "subnets" - config.subcommand = "metagraph" - config.no_prompt = True - - # Add some neurons to the metagraph - mock_nn = [] - - def register_mock_neuron(i: int) -> int: - mock_nn.append( - SimpleNamespace( - hotkey=get_mock_keypair(i + 100, self.id()).ss58_address, - coldkey=get_mock_keypair(i, self.id()).ss58_address, - balance=Balance.from_rao(random.randint(0, 2**45)).rao, - stake=Balance.from_rao(random.randint(0, 2**45)).rao, - ) - ) - uid = _subtensor_mock.force_register_neuron( - netuid=config.netuid, - hotkey=mock_nn[i].hotkey, - coldkey=mock_nn[i].coldkey, - balance=mock_nn[i].balance, - stake=mock_nn[i].stake, - ) - return uid - - for i in range(5): - _ = register_mock_neuron(i) - - _subtensor_mock.neurons_lite(netuid=config.netuid) - - cli = btcli(config) - - mock_console = MockConsole() - with patch("bittensor.core.settings.bt_console", mock_console): - cli.run() - - # Check that the overview was printed. - self.assertIsNotNone(mock_console.captured_print) - - output_no_syntax = mock_console.remove_rich_syntax(mock_console.captured_print) - - self.assertIn("Metagraph", output_no_syntax) - nn = _subtensor_mock.neurons_lite(netuid=config.netuid) - self.assertIn( - str(len(nn) - 1), output_no_syntax - ) # Check that the number of neurons is output - # Check each uid is in the output - for neuron in nn: - self.assertIn(str(neuron.uid), output_no_syntax) - - def test_inspect(self, _): - config = self.config - config.wallet.name = "inspect_testwallet" - config.no_prompt = True - config.n_words = 12 - config.use_password = False - config.overwrite_coldkey = True - config.overwrite_hotkey = True - - # First create a new coldkey - config.command = "wallet" - config.subcommand = "new_coldkey" - cli = btcli(config) - cli.run() - - # Now let's give it a hotkey - config.command = "wallet" - config.subcommand = "new_hotkey" - cli.config = config - cli.run() - - # Now inspect it - config.command = "wallet" - cli.config.subcommand = "inspect" - cli.config = config - cli.run() - - config.command = "wallet" - cli.config.subcommand = "list" - cli.config = config - cli.run() - - # Run History Command to get list of transfers - config.command = "wallet" - cli.config.subcommand = "history" - cli.config = config - cli.run() - - -@patch("bittensor.core.subtensor.Subtensor", new_callable=return_mock_sub) -class TestCLIWithNetworkUsingArgs(unittest.TestCase): - """ - Test the CLI by passing args directly to the bittensor.cli factory - """ - - @unittest.mock.patch.object(MockSubtensor, "get_delegates") - def test_list_delegates(self, mocked_get_delegates, _): - # Call - cli = btcli(args=["root", "list_delegates"]) - cli.run() - - # Assertions - # make sure get_delegates called once without previous state (current only) - self.assertEqual(mocked_get_delegates.call_count, 2) - - def test_list_subnets(self, _): - cli = btcli( - args=[ - "subnets", - "list", - ] - ) - cli.run() - - def test_delegate(self, _): - """ - Test delegate add command - """ - mock_wallet = generate_wallet(hotkey=get_mock_keypair(100, self.id())) - delegate_wallet = generate_wallet(hotkey=get_mock_keypair(100 + 1, self.id())) - - # register the wallet - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=mock_wallet.hotkey.ss58_address, - coldkey=mock_wallet.coldkey.ss58_address, - ) - - # register the delegate - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=delegate_wallet.hotkey.ss58_address, - coldkey=delegate_wallet.coldkey.ss58_address, - ) - - # make the delegate a delegate - _subtensor_mock.nominate(delegate_wallet, wait_for_finalization=True) - self.assertTrue( - _subtensor_mock.is_hotkey_delegate(delegate_wallet.hotkey.ss58_address) - ) - - # Give the wallet some TAO - _, err = _subtensor_mock.force_set_balance( - ss58_address=mock_wallet.coldkey.ss58_address, - balance=Balance.from_tao(20.0), - ) - self.assertEqual(err, None) - - # Check balance - old_balance = _subtensor_mock.get_balance(mock_wallet.coldkey.ss58_address) - self.assertEqual(old_balance.tao, 20.0) - - # Check delegate stake - old_delegate_stake = _subtensor_mock.get_total_stake_for_hotkey( - delegate_wallet.hotkey.ss58_address - ) - - # Check wallet stake - old_wallet_stake = _subtensor_mock.get_total_stake_for_coldkey( - mock_wallet.coldkey.ss58_address - ) - - with patch( - "bittensor_wallet.Wallet", return_value=mock_wallet - ): # Mock wallet creation. SHOULD NOT BE REGISTERED - cli = btcli( - args=[ - "root", - "delegate", - "--subtensor.network", - "mock", # Mock network - "--wallet.name", - "mock", - "--delegate_ss58key", - delegate_wallet.hotkey.ss58_address, - "--amount", - "10.0", # Delegate 10 TAO - "--no_prompt", - ] - ) - cli.run() - - # Check delegate stake - new_delegate_stake = _subtensor_mock.get_total_stake_for_hotkey( - delegate_wallet.hotkey.ss58_address - ) - - # Check wallet stake - new_wallet_stake = _subtensor_mock.get_total_stake_for_coldkey( - mock_wallet.coldkey.ss58_address - ) - - # Check that the delegate stake increased by 10 TAO - self.assertAlmostEqual( - new_delegate_stake.tao, old_delegate_stake.tao + 10.0, delta=1e-6 - ) - - # Check that the wallet stake increased by 10 TAO - self.assertAlmostEqual( - new_wallet_stake.tao, old_wallet_stake.tao + 10.0, delta=1e-6 - ) - - new_balance = _subtensor_mock.get_balance(mock_wallet.coldkey.ss58_address) - self.assertAlmostEqual(new_balance.tao, old_balance.tao - 10.0, delta=1e-6) - - -@pytest.fixture(scope="function") -def wallets_dir_path(tmp_path): - wallets_dir = tmp_path / "wallets" - wallets_dir.mkdir() - yield wallets_dir - - -@pytest.mark.parametrize( - "test_id, wallet_names, expected_wallet_count", - [ - ("happy_path_single_wallet", ["wallet1"], 1), # Single wallet - ( - "happy_path_multiple_wallets", - ["wallet1", "wallet2", "wallet3"], - 3, - ), # Multiple wallets - ("happy_path_no_wallets", [], 0), # No wallets - ], -) -def test_get_coldkey_wallets_for_path( - test_id, wallet_names, expected_wallet_count, wallets_dir_path -): - # Arrange: Create mock wallet directories - for name in wallet_names: - (wallets_dir_path / name).mkdir() - - # Act: Call the function with the test directory - wallets = _get_coldkey_wallets_for_path(str(wallets_dir_path)) - - # Assert: Check if the correct number of wallet objects are returned - assert len(wallets) == expected_wallet_count - for wallet in wallets: - assert isinstance( - wallet, Wallet - ), "The returned object should be an instance of bittensor.wallet" - - -@pytest.mark.parametrize( - "test_id, exception, mock_path, expected_result", - [ - ( - "error_case_invalid_path", - StopIteration, - "/invalid/path", - [], - ), # Invalid path causing StopIteration - ], -) -def test_get_coldkey_wallets_for_path_errors( - test_id, exception, mock_path, expected_result -): - # Arrange: Patch os.walk to raise an exception - with patch("os.walk", side_effect=exception): - # Act: Call the function with an invalid path - wallets = _get_coldkey_wallets_for_path(mock_path) - - # Assert: Check if an empty list is returned - assert ( - wallets == expected_result - ), "Function should return an empty list on error" - - -@pytest.mark.parametrize( - "test_id, display, legal, web, riot, email, pgp_fingerprint, image, info, twitter, expected_exception, expected_message", - [ - ( - "test-run-happy-path-1", - "Alice", - "Alice Doe", - "https://alice.example.com", - "@alice:matrix", - "alice@example.com", - "ABCD1234ABCD1234ABCD", - "https://alice.image", - "Alice in Wonderland", - "@liceTwitter", - None, - "", - ), - # Edge cases - ( - "test_run_edge_case_002", - "", - "", - "", - "", - "", - "", - "", - "", - "", - None, - "", - ), # Empty strings as input - # Error cases - # Each field has a maximum size of 64 bytes, PGP fingerprint has a maximum size of 20 bytes - ( - "test_run_error_case_003", - "A" * 65, - "B" * 65, - "C" * 65, - "D" * 65, - "E" * 65, - "F" * 21, - "G" * 65, - "H" * 65, - "I" * 65, - ValueError, - "Identity value `display` must be <= 64 raw bytes", - ), - ], -) -def test_set_identity_command( - test_id, - display, - legal, - web, - riot, - email, - pgp_fingerprint, - image, - info, - twitter, - expected_exception, - expected_message, -): - # Arrange - mock_cli = MagicMock() - mock_cli.config = MagicMock( - display=display, - legal=legal, - web=web, - riot=riot, - email=email, - pgp_fingerprint=pgp_fingerprint, - image=image, - info=info, - twitter=twitter, - ) - mock_subtensor = MagicMock() - mock_subtensor.update_identity = MagicMock() - mock_subtensor.query_identity = MagicMock(return_value={}) - mock_subtensor.close = MagicMock() - mock_wallet = MagicMock() - mock_wallet.hotkey.ss58_address = "fake_ss58_address" - mock_wallet.coldkey.ss58_address = "fake_coldkey_ss58_address" - mock_wallet.coldkey = MagicMock() - - with patch("bittensor.core.subtensor.Subtensor", return_value=mock_subtensor), patch( - "bittensor_wallet.Wallet", return_value=mock_wallet - ), patch("bittensor.core.settings.bt_console", MagicMock()), patch( - "rich.prompt.Prompt.ask", side_effect=["y", "y"] - ), patch("sys.exit") as mock_exit: - # Act - if expected_exception: - with pytest.raises(expected_exception) as exc_info: - SetIdentityCommand._run(mock_cli, mock_subtensor) - # Assert - assert str(exc_info.value) == expected_message - else: - SetIdentityCommand._run(mock_cli, mock_subtensor) - # Assert - mock_subtensor.update_identity.assert_called_once() - assert mock_exit.call_count == 0 - - -@pytest.fixture -def setup_files(tmp_path): - def _setup_files(files): - for file_path, content in files.items(): - full_path = tmp_path / file_path - os.makedirs(full_path.parent, exist_ok=True) - with open(full_path, "w") as f: - f.write(content) - return tmp_path - - return _setup_files - - -@pytest.mark.parametrize( - "test_id, setup_data, expected", - [ - # Error cases - ( - "error_case_nonexistent_dir", - {"just_a_file.txt": ""}, - ([], []), - ), # Nonexistent dir - ], -) -def test_get_coldkey_ss58_addresses_for_path( - setup_files, test_id, setup_data, expected -): - path = setup_files(setup_data) - - # Arrange - # Setup done in setup_files fixture and parametrize - - # Act - result = _get_coldkey_ss58_addresses_for_path(str(path)) - - # Assert - assert ( - result == expected - ), f"Test ID: {test_id} failed. Expected {expected}, got {result}" - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/integration_tests/test_cli_no_network.py b/tests/integration_tests/test_cli_no_network.py deleted file mode 100644 index f3f4f0bdb..000000000 --- a/tests/integration_tests/test_cli_no_network.py +++ /dev/null @@ -1,1533 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2022 Yuma Rao -# Copyright © 2022-2023 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 unittest -from unittest.mock import MagicMock, patch -from typing import Any, Optional -import pytest -from copy import deepcopy -import re - -from tests.helpers import _get_mock_coldkey, __mock_wallet_factory__, MockConsole - -# import bittensor -from bittensor.utils.balance import Balance -from rich.table import Table - - -class MockException(Exception): - pass - - -mock_delegate_info = { - "hotkey_ss58": "", - "total_stake": Balance.from_rao(0), - "nominators": [], - "owner_ss58": "", - "take": 0.18, - "validator_permits": [], - "registrations": [], - "return_per_1000": Balance.from_rao(0), - "total_daily_return": Balance.from_rao(0), -} - - -def return_mock_sub_1(*args, **kwargs): - return MagicMock( - return_value=MagicMock( - get_subnets=MagicMock(return_value=[1]), # Mock subnet 1 ONLY. - block=10_000, - get_delegates=MagicMock( - return_value=[bittensor.DelegateInfo(**mock_delegate_info)] - ), - ) - ) - - -def return_mock_wallet_factory(*args, **kwargs): - return MagicMock( - return_value=__mock_wallet_factory__(*args, **kwargs), - add_args=bittensor.wallet.add_args, - ) - - -@patch( - "bittensor.subtensor", - new_callable=return_mock_sub_1, -) -@patch("bittensor.wallet", new_callable=return_mock_wallet_factory) -class TestCLINoNetwork(unittest.TestCase): - def setUp(self): - self._config = TestCLINoNetwork.construct_config() - - def config(self): - copy_ = deepcopy(self._config) - return copy_ - - @staticmethod - def construct_config(): - parser = bittensor.cli.__create_parser__() - defaults = bittensor.config(parser=parser, args=["subnets", "metagraph"]) - - # Parse commands and subcommands - for command in bittensor.ALL_COMMANDS: - if ( - command in bittensor.ALL_COMMANDS - and "commands" in bittensor.ALL_COMMANDS[command] - ): - for subcommand in bittensor.ALL_COMMANDS[command]["commands"]: - defaults.merge( - bittensor.config(parser=parser, args=[command, subcommand]) - ) - else: - defaults.merge(bittensor.config(parser=parser, args=[command])) - - defaults.netuid = 1 - defaults.subtensor.network = "mock" - defaults.no_version_checking = True - - return defaults - - def test_check_configs(self, _, __): - config = self.config() - config.no_prompt = True - config.model = "core_server" - config.dest = "no_prompt" - config.amount = 1 - config.mnemonic = "this is a mnemonic" - config.seed = None - config.uids = [1, 2, 3] - config.weights = [0.25, 0.25, 0.25, 0.25] - config.no_version_checking = True - config.ss58_address = bittensor.Keypair.create_from_seed(b"0" * 32).ss58_address - config.public_key_hex = None - config.proposal_hash = "" - - cli_instance = bittensor.cli - - # Define the response function for rich.prompt.Prompt.ask - def ask_response(prompt: str) -> Any: - if "delegate index" in prompt: - return 0 - elif "wallet name" in prompt: - return "mock" - elif "hotkey" in prompt: - return "mock" - - # Patch the ask response - with patch("rich.prompt.Prompt.ask", ask_response): - # Loop through all commands and their subcommands - for command, command_data in bittensor.ALL_COMMANDS.items(): - config.command = command - if isinstance(command_data, dict): - for subcommand in command_data["commands"].keys(): - config.subcommand = subcommand - cli_instance.check_config(config) - else: - config.subcommand = None - cli_instance.check_config(config) - - def test_new_coldkey(self, _, __): - config = self.config() - config.wallet.name = "new_coldkey_testwallet" - - config.command = "wallet" - config.subcommand = "new_coldkey" - config.amount = 1 - config.dest = "no_prompt" - config.model = "core_server" - config.n_words = 12 - config.use_password = False - config.no_prompt = True - config.overwrite_coldkey = True - - cli = bittensor.cli(config) - cli.run() - - def test_new_hotkey(self, _, __): - config = self.config() - config.wallet.name = "new_hotkey_testwallet" - config.command = "wallet" - config.subcommand = "new_hotkey" - config.amount = 1 - config.dest = "no_prompt" - config.model = "core_server" - config.n_words = 12 - config.use_password = False - config.no_prompt = True - config.overwrite_hotkey = True - - cli = bittensor.cli(config) - cli.run() - - def test_regen_coldkey(self, _, __): - config = self.config() - config.wallet.name = "regen_coldkey_testwallet" - config.command = "wallet" - config.subcommand = "regen_coldkey" - config.amount = 1 - config.dest = "no_prompt" - config.model = "core_server" - config.mnemonic = "faculty decade seven jelly gospel axis next radio grain radio remain gentle" - config.seed = None - config.n_words = 12 - config.use_password = False - config.no_prompt = True - config.overwrite_coldkey = True - - cli = bittensor.cli(config) - cli.run() - - def test_regen_coldkeypub(self, _, __): - config = self.config() - config.wallet.name = "regen_coldkeypub_testwallet" - config.command = "wallet" - config.subcommand = "regen_coldkeypub" - config.ss58_address = "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - config.public_key = None - config.use_password = False - config.no_prompt = True - config.overwrite_coldkeypub = True - - cli = bittensor.cli(config) - cli.run() - - def test_regen_hotkey(self, _, __): - config = self.config() - config.wallet.name = "regen_hotkey_testwallet" - config.command = "wallet" - config.subcommand = "regen_hotkey" - config.amount = 1 - config.model = "core_server" - config.mnemonic = "faculty decade seven jelly gospel axis next radio grain radio remain gentle" - config.seed = None - config.n_words = 12 - config.use_password = False - config.no_prompt = True - config.overwrite_hotkey = True - - cli = bittensor.cli(config) - cli.run() - - def test_list(self, _, __): - # Mock IO for wallet - with patch( - "bittensor.wallet", - side_effect=[ - MagicMock( - coldkeypub_file=MagicMock( - exists_on_device=MagicMock(return_value=True), # Wallet exists - is_encrypted=MagicMock( - return_value=False # Wallet is not encrypted - ), - ), - coldkeypub=MagicMock( - ss58_address=bittensor.Keypair.create_from_mnemonic( - bittensor.Keypair.generate_mnemonic() - ).ss58_address - ), - ), - MagicMock( - hotkey_file=MagicMock( - exists_on_device=MagicMock(return_value=True), # Wallet exists - is_encrypted=MagicMock( - return_value=False # Wallet is not encrypted - ), - ), - hotkey=MagicMock( - ss58_address=bittensor.Keypair.create_from_mnemonic( - bittensor.Keypair.generate_mnemonic() - ).ss58_address - ), - ), - ], - ): - config = self.config() - config.wallet.path = "tmp/walletpath" - config.wallet.name = "mock_wallet" - config.no_prompt = True - config.command = "wallet" - config.subcommand = "list" - - cli = bittensor.cli(config) - with patch( - "os.walk", - side_effect=[ - iter([("/tmp/walletpath", ["mock_wallet"], [])]), # 1 wallet dir - iter( - [ - ("/tmp/walletpath/mock_wallet/hotkeys", [], ["hk0"]) - ] # 1 hotkey file - ), - ], - ): - cli.run() - - def test_list_no_wallet(self, _, __): - with patch( - "bittensor.wallet", - side_effect=[ - MagicMock( - coldkeypub_file=MagicMock( - exists_on_device=MagicMock(return_value=True) - ) - ) - ], - ): - config = self.config() - config.wallet.path = "/tmp/test_cli_test_list_no_wallet" - config.no_prompt = True - config.command = "wallet" - config.subcommand = "list" - - cli = bittensor.cli(config) - # This shouldn't raise an error anymore - cli.run() - - def test_btcli_help(self, _, __): - with pytest.raises(SystemExit) as pytest_wrapped_e: - with patch( - "argparse.ArgumentParser._print_message", return_value=None - ) as mock_print_message: - args = ["--help"] - bittensor.cli(args=args).run() - - mock_print_message.assert_called_once() - - call_args = mock_print_message.call_args - help_out = call_args[0][0] - - # Extract commands from the help text. - commands_section = re.search( - r"positional arguments:.*?{(.+?)}", help_out, re.DOTALL - ).group(1) - extracted_commands = [cmd.strip() for cmd in commands_section.split(",")] - - # Get expected commands - parser = bittensor.cli.__create_parser__() - expected_commands = [command for command in parser._actions[-1].choices] - - # Validate each expected command is in extracted commands - for command in expected_commands: - assert ( - command in extracted_commands - ), f"Command {command} not found in help output" - - # Check for duplicates - assert len(extracted_commands) == len( - set(extracted_commands) - ), "Duplicate commands found in help output" - - @patch("torch.cuda.is_available", return_value=True) - def test_register_cuda_use_cuda_flag(self, _, __, patched_sub): - base_args = [ - "subnets", - "pow_register", - "--wallet.path", - "tmp/walletpath", - "--wallet.name", - "mock", - "--wallet.hotkey", - "hk0", - "--no_prompt", - "--cuda.dev_id", - "0", - ] - - patched_sub.return_value = MagicMock( - get_subnets=MagicMock(return_value=[1]), - subnet_exists=MagicMock(return_value=True), - register=MagicMock(side_effect=MockException), - ) - - # Should be able to set true without argument - args = base_args + [ - "--pow_register.cuda.use_cuda", # should be True without any arugment - ] - with pytest.raises(MockException): - cli = bittensor.cli(args=args) - cli.run() - - self.assertEqual(cli.config.pow_register.cuda.get("use_cuda"), True) - - # Should be able to set to false with no argument - - args = base_args + [ - "--pow_register.cuda.no_cuda", - ] - with pytest.raises(MockException): - cli = bittensor.cli(args=args) - cli.run() - - self.assertEqual(cli.config.pow_register.cuda.get("use_cuda"), False) - - -def return_mock_sub_2(*args, **kwargs): - return MagicMock( - return_value=MagicMock( - get_subnet_burn_cost=MagicMock(return_value=0.1), - get_subnets=MagicMock(return_value=[1]), # Need to pass check config - get_delegates=MagicMock( - return_value=[ - bittensor.DelegateInfo( - hotkey_ss58="", - total_stake=Balance.from_rao(0), - nominators=[], - owner_ss58="", - take=0.18, - validator_permits=[], - registrations=[], - return_per_1000=Balance(0.0), - total_daily_return=Balance(0.0), - ) - ] - ), - block=10_000, - ), - add_args=bittensor.subtensor.add_args, - ) - - -@patch("bittensor.wallet", new_callable=return_mock_wallet_factory) -@patch("bittensor.subtensor", new_callable=return_mock_sub_2) -class TestEmptyArgs(unittest.TestCase): - """ - Test that the CLI doesn't crash when no args are passed - """ - - @patch("rich.prompt.PromptBase.ask", side_effect=MockException) - def test_command_no_args(self, _, __, patched_prompt_ask): - # Get argparser - parser = bittensor.cli.__create_parser__() - # Get all commands from argparser - commands = [ - command - for command in parser._actions[-1].choices # extract correct subparser keys - if len(command) > 1 # Skip singleton aliases - and command - not in [ - "subnet", - "sudos", - "stakes", - "roots", - "wallets", - "weight", - "st", - "wt", - "su", - ] # Skip duplicate aliases - ] - # Test that each command and its subcommands can be run with no args - for command in commands: - command_data = bittensor.ALL_COMMANDS.get(command) - - # If command is dictionary, it means it has subcommands - if isinstance(command_data, dict): - for subcommand in command_data["commands"].keys(): - try: - # Run each subcommand - bittensor.cli(args=[command, subcommand]).run() - except MockException: - pass # Expected exception - else: - try: - # If no subcommands, just run the command - bittensor.cli(args=[command]).run() - except MockException: - pass # Expected exception - - # Should not raise any other exceptions - - -mock_delegate_info = { - "hotkey_ss58": "", - "total_stake": Balance.from_rao(0), - "nominators": [], - "owner_ss58": "", - "take": 0.18, - "validator_permits": [], - "registrations": [], - "return_per_1000": Balance.from_rao(0), - "total_daily_return": Balance.from_rao(0), -} - - -def return_mock_sub_3(*args, **kwargs): - return MagicMock( - return_value=MagicMock( - get_subnets=MagicMock(return_value=[1]), # Mock subnet 1 ONLY. - block=10_000, - get_delegates=MagicMock( - return_value=[bittensor.DelegateInfo(**mock_delegate_info)] - ), - ), - block=10_000, - ) - - -@patch("bittensor.subtensor", new_callable=return_mock_sub_3) -class TestCLIDefaultsNoNetwork(unittest.TestCase): - def test_inspect_prompt_wallet_name(self, _): - # Patch command to exit early - with patch("bittensor.btcli.commands.inspect.InspectCommand.run", return_value=None): - # Test prompt happens when no wallet name is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "inspect", - # '--wallet.name', 'mock', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called_once() - - # Test NO prompt happens when wallet name is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "inspect", - "--wallet.name", - "coolwalletname", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - # Test NO prompt happens when wallet name 'default' is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "inspect", - "--wallet.name", - "default", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_overview_prompt_wallet_name(self, _): - # Patch command to exit early - with patch( - "bittensor.btcli.commands.overview.OverviewCommand.run", return_value=None - ): - # Test prompt happens when no wallet name is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "overview", - # '--wallet.name', 'mock', - "--netuid", - "1", - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called_once() - - # Test NO prompt happens when wallet name is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "overview", - "--wallet.name", - "coolwalletname", - "--netuid", - "1", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - # Test NO prompt happens when wallet name 'default' is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "overview", - "--wallet.name", - "default", - "--netuid", - "1", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_stake_prompt_wallet_name_and_hotkey_name(self, _): - base_args = [ - "stake", - "add", - "--all", - ] - # Patch command to exit early - with patch("bittensor.btcli.commands.stake.StakeCommand.run", return_value=None): - # Test prompt happens when - # - wallet name IS NOT passed, AND - # - hotkey name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - # '--wallet.name', 'mock', - #'--wallet.hotkey', 'mock_hotkey', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 2, - msg="Prompt should have been called twice", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in [val for val in kwargs0.values()] - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - args1, kwargs1 = mock_ask_prompt.call_args_list[1] - combined_args_kwargs1 = [arg for arg in args1] + [ - val for val in kwargs1.values() - ] - # check that prompt was called for hotkey - - self.assertTrue( - any(filter(lambda x: "hotkey" in x.lower(), combined_args_kwargs1)), - msg=f"Prompt should have been called for hotkey: {combined_args_kwargs1}", - ) - - # Test prompt happens when - # - wallet name IS NOT passed, AND - # - hotkey name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - #'--wallet.name', 'mock', - "--wallet.hotkey", - "mock_hotkey", - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - # Test prompt happens when - # - wallet name IS passed, AND - # - hotkey name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "mock", - #'--wallet.hotkey', 'mock_hotkey', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for hotkey - self.assertTrue( - any(filter(lambda x: "hotkey" in x.lower(), combined_args_kwargs0)), - msg=f"Prompt should have been called for hotkey {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - wallet name IS passed, AND - # - hotkey name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "coolwalletname", - "--wallet.hotkey", - "coolwalletname_hotkey", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - # Test NO prompt happens when - # - wallet name 'default' IS passed, AND - # - hotkey name 'default' IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_unstake_prompt_wallet_name_and_hotkey_name(self, _): - base_args = [ - "stake", - "remove", - "--all", - ] - # Patch command to exit early - with patch("bittensor.btcli.commands.unstake.UnStakeCommand.run", return_value=None): - # Test prompt happens when - # - wallet name IS NOT passed, AND - # - hotkey name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - # '--wallet.name', 'mock', - #'--wallet.hotkey', 'mock_hotkey', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 2, - msg="Prompt should have been called twice", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - args1, kwargs1 = mock_ask_prompt.call_args_list[1] - combined_args_kwargs1 = [arg for arg in args1] + [ - val for val in kwargs1.values() - ] - # check that prompt was called for hotkey - self.assertTrue( - any(filter(lambda x: "hotkey" in x.lower(), combined_args_kwargs1)), - msg=f"Prompt should have been called for hotkey {combined_args_kwargs1}", - ) - - # Test prompt happens when - # - wallet name IS NOT passed, AND - # - hotkey name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - #'--wallet.name', 'mock', - "--wallet.hotkey", - "mock_hotkey", - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - # Test prompt happens when - # - wallet name IS passed, AND - # - hotkey name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "mock", - #'--wallet.hotkey', 'mock_hotkey', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for hotkey - self.assertTrue( - any(filter(lambda x: "hotkey" in x.lower(), combined_args_kwargs0)), - msg=f"Prompt should have been called for hotkey {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - wallet name IS passed, AND - # - hotkey name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "coolwalletname", - "--wallet.hotkey", - "coolwalletname_hotkey", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - # Test NO prompt happens when - # - wallet name 'default' IS passed, AND - # - hotkey name 'default' IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_delegate_prompt_wallet_name(self, _): - base_args = [ - "root", - "delegate", - "--all", - "--delegate_ss58key", - _get_mock_coldkey(0), - ] - # Patch command to exit early - with patch( - "bittensor.btcli.commands.delegates.DelegateStakeCommand.run", return_value=None - ): - # Test prompt happens when - # - wallet name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock"] - - cli = bittensor.cli( - args=base_args - + [ - # '--wallet.name', 'mock', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - wallet name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "coolwalletname", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_undelegate_prompt_wallet_name(self, _): - base_args = [ - "root", - "undelegate", - "--all", - "--delegate_ss58key", - _get_mock_coldkey(0), - ] - # Patch command to exit early - with patch( - "bittensor.btcli.commands.delegates.DelegateUnstakeCommand.run", return_value=None - ): - # Test prompt happens when - # - wallet name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock"] - - cli = bittensor.cli( - args=base_args - + [ - # '--wallet.name', 'mock', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - wallet name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "coolwalletname", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_history_prompt_wallet_name(self, _): - base_args = [ - "wallet", - "history", - ] - # Patch command to exit early - with patch( - "bittensor.btcli.commands.wallets.GetWalletHistoryCommand.run", return_value=None - ): - # Test prompt happens when - # - wallet name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock"] - - cli = bittensor.cli( - args=base_args - + [ - # '--wallet.name', 'mock', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - wallet name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "coolwalletname", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_delegate_prompt_hotkey(self, _): - # Tests when - # - wallet name IS passed, AND - # - delegate hotkey IS NOT passed - base_args = [ - "root", - "delegate", - "--all", - "--wallet.name", - "mock", - ] - - delegate_ss58 = _get_mock_coldkey(0) - with patch("bittensor.btcli.commands.delegates.show_delegates"): - with patch( - "bittensor.subtensor.Subtensor.get_delegates", - return_value=[ - bittensor.DelegateInfo( - hotkey_ss58=delegate_ss58, # return delegate with mock coldkey - total_stake=Balance.from_float(0.1), - nominators=[], - owner_ss58="", - take=0.18, - validator_permits=[], - registrations=[], - return_per_1000=Balance.from_float(0.1), - total_daily_return=Balance.from_float(0.1), - ) - ], - ): - # Patch command to exit early - with patch( - "bittensor.btcli.commands.delegates.DelegateStakeCommand.run", - return_value=None, - ): - # Test prompt happens when - # - delegate hotkey IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = [ - "0" - ] # select delegate with mock coldkey - - cli = bittensor.cli( - args=base_args - + [ - # '--delegate_ss58key', delegate_ss58, - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for delegate hotkey - self.assertTrue( - any( - filter( - lambda x: "delegate" in x.lower(), - combined_args_kwargs0, - ) - ), - msg=f"Prompt should have been called for delegate: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - delegate hotkey IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--delegate_ss58key", - delegate_ss58, - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_undelegate_prompt_hotkey(self, _): - # Tests when - # - wallet name IS passed, AND - # - delegate hotkey IS NOT passed - base_args = [ - "root", - "undelegate", - "--all", - "--wallet.name", - "mock", - ] - - delegate_ss58 = _get_mock_coldkey(0) - with patch("bittensor.btcli.commands.delegates.show_delegates"): - with patch( - "bittensor.subtensor.Subtensor.get_delegates", - return_value=[ - bittensor.DelegateInfo( - hotkey_ss58=delegate_ss58, # return delegate with mock coldkey - total_stake=Balance.from_float(0.1), - nominators=[], - owner_ss58="", - take=0.18, - validator_permits=[], - registrations=[], - return_per_1000=Balance.from_float(0.1), - total_daily_return=Balance.from_float(0.1), - ) - ], - ): - # Patch command to exit early - with patch( - "bittensor.btcli.commands.delegates.DelegateUnstakeCommand.run", - return_value=None, - ): - # Test prompt happens when - # - delegate hotkey IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = [ - "0" - ] # select delegate with mock coldkey - - cli = bittensor.cli( - args=base_args - + [ - # '--delegate_ss58key', delegate_ss58, - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for delegate hotkey - self.assertTrue( - any( - filter( - lambda x: "delegate" in x.lower(), - combined_args_kwargs0, - ) - ), - msg=f"Prompt should have been called for delegate: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - delegate hotkey IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--delegate_ss58key", - delegate_ss58, - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_vote_command_prompt_proposal_hash(self, _): - """Test that the vote command prompts for proposal_hash when it is not passed""" - base_args = [ - "root", - "senate_vote", - "--wallet.name", - "mock", - "--wallet.hotkey", - "mock_hotkey", - ] - - mock_proposal_hash = "mock_proposal_hash" - - with patch("bittensor.subtensor.Subtensor.is_senate_member", return_value=True): - with patch( - "bittensor.subtensor.Subtensor.get_vote_data", - return_value={"index": 1}, - ): - # Patch command to exit early - with patch( - "bittensor.btcli.commands.senate.VoteCommand.run", - return_value=None, - ): - # Test prompt happens when - # - proposal_hash IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = [ - mock_proposal_hash # Proposal hash - ] - - cli = bittensor.cli( - args=base_args - # proposal_hash not added - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called once", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for proposal_hash - self.assertTrue( - any( - filter( - lambda x: "proposal" in x.lower(), - combined_args_kwargs0, - ) - ), - msg=f"Prompt should have been called for proposal: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - proposal_hash IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--proposal_hash", - mock_proposal_hash, - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) - def test_commit_reveal_weights_enabled_parse_boolean_argument(self, mock_sub, __): - param = "commit_reveal_weights_enabled" - - def _test_value_parsing(parsed_value: bool, modified: str): - cli = bittensor.cli( - args=[ - "sudo", - "set", - "--netuid", - "1", - "--param", - param, - "--value", - modified, - "--wallet.name", - "mock", - ] - ) - cli.run() - - _, kwargs = mock_sub.call_args - passed_config = kwargs["config"] - self.assertEqual(passed_config.param, param, msg="Incorrect param") - self.assertEqual( - passed_config.value, - parsed_value, - msg=f"Boolean argument not correctly for {modified}", - ) - - for boolean_value in [True, False, 1, 0]: - as_str = str(boolean_value) - - _test_value_parsing(boolean_value, as_str) - _test_value_parsing(boolean_value, as_str.capitalize()) - _test_value_parsing(boolean_value, as_str.upper()) - _test_value_parsing(boolean_value, as_str.lower()) - - @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) - def test_hyperparameter_allowed_values( - self, - mock_sub, - __, - ): - params = ["alpha_values"] - - def _test_value_parsing(param: str, value: str): - cli = bittensor.cli( - args=[ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--param", - param, - "--value", - value, - "--wallet.name", - "mock", - ] - ) - should_raise_error = False - error_message = "" - - try: - alpha_low_str, alpha_high_str = value.strip("[]").split(",") - alpha_high = float(alpha_high_str) - alpha_low = float(alpha_low_str) - if alpha_high <= 52428 or alpha_high >= 65535: - should_raise_error = True - error_message = "between 52428 and 65535" - elif alpha_low < 0 or alpha_low > 52428: - should_raise_error = True - error_message = "between 0 and 52428" - except ValueError: - should_raise_error = True - error_message = "a number or a boolean" - except TypeError: - should_raise_error = True - error_message = "a number or a boolean" - - if isinstance(value, bool): - should_raise_error = True - error_message = "a number or a boolean" - - if should_raise_error: - with pytest.raises(ValueError) as exc_info: - cli.run() - assert ( - f"Hyperparameter {param} value is not within bounds. Value is {value} but must be {error_message}" - in str(exc_info.value) - ) - else: - cli.run() - _, kwargs = mock_sub.call_args - passed_config = kwargs["config"] - self.assertEqual(passed_config.param, param, msg="Incorrect param") - self.assertEqual( - passed_config.value, - value, - msg=f"Value argument not set correctly for {param}", - ) - - for param in params: - for value in [ - [0.8, 11], - [52429, 52428], - [52427, 53083], - [6553, 53083], - [-123, None], - [1, 0], - [True, "Some string"], - ]: - as_str = str(value).strip("[]") - _test_value_parsing(param, as_str) - - @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) - def test_network_registration_allowed_parse_boolean_argument(self, mock_sub, __): - param = "network_registration_allowed" - - def _test_value_parsing(parsed_value: bool, modified: str): - cli = bittensor.cli( - args=[ - "sudo", - "set", - "--netuid", - "1", - "--param", - param, - "--value", - modified, - "--wallet.name", - "mock", - ] - ) - cli.run() - - _, kwargs = mock_sub.call_args - passed_config = kwargs["config"] - self.assertEqual(passed_config.param, param, msg="Incorrect param") - self.assertEqual( - passed_config.value, - parsed_value, - msg=f"Boolean argument not correctly for {modified}", - ) - - for boolean_value in [True, False, 1, 0]: - as_str = str(boolean_value) - - _test_value_parsing(boolean_value, as_str) - _test_value_parsing(boolean_value, as_str.capitalize()) - _test_value_parsing(boolean_value, as_str.upper()) - _test_value_parsing(boolean_value, as_str.lower()) - - @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) - def test_network_pow_registration_allowed_parse_boolean_argument( - self, mock_sub, __ - ): - param = "network_pow_registration_allowed" - - def _test_value_parsing(parsed_value: bool, modified: str): - cli = bittensor.cli( - args=[ - "sudo", - "set", - "--netuid", - "1", - "--param", - param, - "--value", - modified, - "--wallet.name", - "mock", - ] - ) - cli.run() - - _, kwargs = mock_sub.call_args - passed_config = kwargs["config"] - self.assertEqual(passed_config.param, param, msg="Incorrect param") - self.assertEqual( - passed_config.value, - parsed_value, - msg=f"Boolean argument not correctly for {modified}", - ) - - for boolean_value in [True, False, 1, 0]: - as_str = str(boolean_value) - - _test_value_parsing(boolean_value, as_str) - _test_value_parsing(boolean_value, as_str.capitalize()) - _test_value_parsing(boolean_value, as_str.upper()) - _test_value_parsing(boolean_value, as_str.lower()) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py index d5c2043f1..07a13df70 100644 --- a/tests/integration_tests/test_metagraph_integration.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index ecc4ace19..9046f1f12 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Technologies Inc - +# 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 @@ -56,7 +55,7 @@ def setUp(self): def setUpClass(cls) -> None: # mock rich console status mock_console = MockConsole() - cls._mock_console_patcher = patch("bittensor.__console__", mock_console) + cls._mock_console_patcher = patch("bittensor.core.settings.bt_console", mock_console) cls._mock_console_patcher.start() # Keeps the same mock network for all tests. This stops the network from being re-setup for each test. @@ -711,7 +710,7 @@ def test_registration_multiprocessed_already_registered(self): ) self.subtensor._do_pow_register = MagicMock(return_value=(True, None)) - with patch("bittensor.__console__.status") as mock_set_status: + with patch("bittensor.core.settings.bt_console.status") as mock_set_status: # Need to patch the console status to avoid opening a parallel live display mock_set_status.__enter__ = MagicMock(return_value=True) mock_set_status.__exit__ = MagicMock(return_value=True) From a74c5c577bed5dead8ecde8d8ce22005b9db7e20 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 5 Aug 2024 09:46:48 -0700 Subject: [PATCH 061/260] Fix tests. --- tests/unit_tests/test_overview.py | 265 ----------------------------- tests/unit_tests/test_subtensor.py | 49 +++++- 2 files changed, 48 insertions(+), 266 deletions(-) delete mode 100644 tests/unit_tests/test_overview.py diff --git a/tests/unit_tests/test_overview.py b/tests/unit_tests/test_overview.py deleted file mode 100644 index 597d76669..000000000 --- a/tests/unit_tests/test_overview.py +++ /dev/null @@ -1,265 +0,0 @@ - -from copy import deepcopy -from unittest.mock import MagicMock, patch - -import pytest - -from bittensor.btcli.cli import cli as btcli, COMMANDS as ALL_COMMANDS -from bittensor.btcli.commands import OverviewCommand -from bittensor.core.config import Config -from tests.unit_tests.factories.neuron_factory import NeuronInfoLiteFactory - - -@pytest.fixture -def mock_subtensor(): - mock = MagicMock() - mock.get_balance = MagicMock(return_value=100) - return mock - - -def fake_config(**kwargs): - config = deepcopy(construct_config()) - for key, value in kwargs.items(): - setattr(config, key, value) - return config - - -def construct_config(): - parser = btcli.__create_parser__() - defaults = Config(parser=parser, args=[]) - # Parse commands and subcommands - for command in ALL_COMMANDS: - if ( - command in ALL_COMMANDS - and "commands" in ALL_COMMANDS[command] - ): - for subcommand in ALL_COMMANDS[command]["commands"]: - defaults.merge( - Config(parser=parser, args=[command, subcommand]) - ) - else: - defaults.merge(Config(parser=parser, args=[command])) - - defaults.netuid = 1 - # Always use mock subtensor. - defaults.subtensor.network = "finney" - # Skip version checking. - defaults.no_version_checking = True - - return defaults - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock() - mock.coldkeypub_file.exists_on_device = MagicMock(return_value=True) - mock.coldkeypub_file.is_encrypted = MagicMock(return_value=False) - mock.coldkeypub.ss58_address = "fake_address" - return mock - - -class MockHotkey: - def __init__(self, hotkey_str): - self.hotkey_str = hotkey_str - - -class MockCli: - def __init__(self, config): - self.config = config - - -@pytest.mark.parametrize( - "config_all, exists_on_device, is_encrypted, expected_balance, test_id", - [ - (True, True, False, 100, "happy_path_all_wallets"), - (False, True, False, 100, "happy_path_single_wallet"), - (True, False, False, 0, "edge_case_no_wallets_found"), - (True, True, True, 0, "edge_case_encrypted_wallet"), - ], -) -def test_get_total_balance( - mock_subtensor, - mock_wallet, - config_all, - exists_on_device, - is_encrypted, - expected_balance, - test_id, -): - # Arrange - cli = MockCli(fake_config(all=config_all)) - mock_wallet.coldkeypub_file.exists_on_device.return_value = exists_on_device - mock_wallet.coldkeypub_file.is_encrypted.return_value = is_encrypted - - with patch( - "bittensor_wallet.Wallet", return_value=mock_wallet - ) as mock_wallet_constructor, patch( - "bittensor.btcli.commands.overview.get_coldkey_wallets_for_path", - return_value=[mock_wallet] if config_all else [], - ), patch( - "bittensor.btcli.commands.overview.get_all_wallets_for_path", - return_value=[mock_wallet], - ), patch( - "bittensor.btcli.commands.overview.get_hotkey_wallets_for_wallet", - return_value=[mock_wallet], - ): - # Act - result_hotkeys, result_balance = OverviewCommand._get_total_balance( - 0, mock_subtensor, cli - ) - - # Assert - assert result_balance == expected_balance, f"Test ID: {test_id}" - assert all( - isinstance(hotkey, MagicMock) for hotkey in result_hotkeys - ), f"Test ID: {test_id}" - - -@pytest.mark.parametrize( - "config, all_hotkeys, expected_result, test_id", - [ - # Happy path tests - ( - {"all_hotkeys": False, "hotkeys": ["abc123", "xyz456"]}, - [MockHotkey("abc123"), MockHotkey("xyz456"), MockHotkey("mno567")], - ["abc123", "xyz456"], - "test_happy_path_included", - ), - ( - {"all_hotkeys": True, "hotkeys": ["abc123", "xyz456"]}, - [MockHotkey("abc123"), MockHotkey("xyz456"), MockHotkey("mno567")], - ["mno567"], - "test_happy_path_excluded", - ), - # Edge cases - ( - {"all_hotkeys": False, "hotkeys": []}, - [MockHotkey("abc123"), MockHotkey("xyz456")], - [], - "test_edge_no_hotkeys_specified", - ), - ( - {"all_hotkeys": True, "hotkeys": []}, - [MockHotkey("abc123"), MockHotkey("xyz456")], - ["abc123", "xyz456"], - "test_edge_all_hotkeys_excluded", - ), - ( - {"all_hotkeys": False, "hotkeys": ["abc123", "xyz456"]}, - [], - [], - "test_edge_no_hotkeys_available", - ), - ( - {"all_hotkeys": True, "hotkeys": ["abc123", "xyz456"]}, - [], - [], - "test_edge_no_hotkeys_available_excluded", - ), - ], -) -def test_get_hotkeys(config, all_hotkeys, expected_result, test_id): - # Arrange - cli = MockCli( - fake_config( - hotkeys=config.get("hotkeys"), all_hotkeys=config.get("all_hotkeys") - ) - ) - - # Act - result = OverviewCommand._get_hotkeys(cli, all_hotkeys) - - # Assert - assert [ - hotkey.hotkey_str for hotkey in result - ] == expected_result, f"Failed {test_id}" - - -def test_get_hotkeys_error(): - # Arrange - cli = MockCli(fake_config(hotkeys=["abc123", "xyz456"], all_hotkeys=False)) - all_hotkeys = None - - # Act - with pytest.raises(TypeError): - OverviewCommand._get_hotkeys(cli, all_hotkeys) - - -@pytest.fixture -def neuron_info(): - return [ - (1, [NeuronInfoLiteFactory(netuid=1)], None), - (2, [NeuronInfoLiteFactory(netuid=2)], None), - ] - - -@pytest.fixture -def neurons_dict(): - return { - "1": [NeuronInfoLiteFactory(netuid=1)], - "2": [NeuronInfoLiteFactory(netuid=2)], - } - - -@pytest.fixture -def netuids_list(): - return [1, 2] - - -# Test cases -@pytest.mark.parametrize( - "test_id, results, expected_neurons, expected_netuids", - [ - # Test ID: 01 - Happy path, all neurons processed correctly - ( - "01", - [ - (1, [NeuronInfoLiteFactory(netuid=1)], None), - (2, [NeuronInfoLiteFactory(netuid=2)], None), - ], - { - "1": [NeuronInfoLiteFactory(netuid=1)], - "2": [NeuronInfoLiteFactory(netuid=2)], - }, - [1, 2], - ), - # Test ID: 02 - Error message present, should skip processing for that netuid - ( - "02", - [ - (1, [NeuronInfoLiteFactory(netuid=1)], None), - (2, [], "Error fetching data"), - ], - {"1": [NeuronInfoLiteFactory()]}, - [1], - ), - # Test ID: 03 - No neurons found for a netuid, should remove the netuid - ( - "03", - [(1, [NeuronInfoLiteFactory()], None), (2, [], None)], - {"1": [NeuronInfoLiteFactory()]}, - [1], - ), - # Test ID: 04 - Mixed conditions - ( - "04", - [ - (1, [NeuronInfoLiteFactory(netuid=1)], None), - (2, [], None), - ], - {"1": [NeuronInfoLiteFactory()]}, - [1], - ), - ], -) -def test_process_neuron_results( - test_id, results, expected_neurons, expected_netuids, neurons_dict, netuids_list -): - # Act - actual_neurons = OverviewCommand._process_neuron_results( - results, neurons_dict, netuids_list - ) - - # Assert - assert actual_neurons.keys() == expected_neurons.keys(), f"Failed test {test_id}" - assert netuids_list == expected_netuids, f"Failed test {test_id}" diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 6461b2b74..08a919ff7 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -17,16 +17,17 @@ import argparse import unittest.mock as mock +from typing import List, Tuple from unittest.mock import MagicMock import pytest from bittensor_wallet import Wallet -from bittensor.btcli.commands.utils import normalize_hyperparameters from bittensor.core import subtensor as subtensor_module, settings from bittensor.core.axon import Axon from bittensor.core.chain_data import SubnetHyperparameters from bittensor.core.subtensor import Subtensor, logging +from bittensor.utils import U16_NORMALIZED_FLOAT, U64_NORMALIZED_FLOAT from bittensor.utils.balance import Balance U16_MAX = 65535 @@ -461,6 +462,52 @@ def sample_hyperparameters(): return MagicMock(spec=SubnetHyperparameters) +def normalize_hyperparameters( + subnet: "SubnetHyperparameters", +) -> List[Tuple[str, str, str]]: + """ + Normalizes the hyperparameters of a subnet. + + Args: + subnet: The subnet hyperparameters object. + + Returns: + A list of tuples containing the parameter name, value, and normalized value. + """ + param_mappings = { + "adjustment_alpha": U64_NORMALIZED_FLOAT, + "min_difficulty": U64_NORMALIZED_FLOAT, + "max_difficulty": U64_NORMALIZED_FLOAT, + "difficulty": U64_NORMALIZED_FLOAT, + "bonds_moving_avg": U64_NORMALIZED_FLOAT, + "max_weight_limit": U16_NORMALIZED_FLOAT, + "kappa": U16_NORMALIZED_FLOAT, + "alpha_high": U16_NORMALIZED_FLOAT, + "alpha_low": U16_NORMALIZED_FLOAT, + "min_burn": Balance.from_rao, + "max_burn": Balance.from_rao, + } + + normalized_values: List[Tuple[str, str, str]] = [] + subnet_dict = subnet.__dict__ + + for param, value in subnet_dict.items(): + try: + if param in param_mappings: + norm_value = param_mappings[param](value) + if isinstance(norm_value, float): + norm_value = f"{norm_value:.{10}g}" + else: + norm_value = value + except Exception as e: + logging.warning(f"Error normalizing parameter '{param}': {e}") + norm_value = "-" + + normalized_values.append((param, str(value), str(norm_value))) + + return normalized_values + + def get_normalized_value(normalized_data, param_name): return next( ( From f25417243588199aa9e6abb492d6675c351afe20 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 5 Aug 2024 09:55:38 -0700 Subject: [PATCH 062/260] Makes the `bittensor.utils.mock` subpackage available as `bittensor.mock` for backwards compatibility. --- bittensor/utils/backwards_compatibility.py | 6 +++++- bittensor/{ => utils}/mock/__init__.py | 2 +- bittensor/{ => utils}/mock/subtensor_mock.py | 0 tests/integration_tests/test_metagraph_integration.py | 2 +- tests/integration_tests/test_subtensor_integration.py | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) rename bittensor/{ => utils}/mock/__init__.py (95%) rename bittensor/{ => utils}/mock/subtensor_mock.py (100%) diff --git a/bittensor/utils/backwards_compatibility.py b/bittensor/utils/backwards_compatibility.py index a1780d888..274ccb8d5 100644 --- a/bittensor/utils/backwards_compatibility.py +++ b/bittensor/utils/backwards_compatibility.py @@ -97,7 +97,7 @@ from bittensor.core.synapse import TerminalInfo, Synapse # noqa: F401 from bittensor.core.tensor import tensor, Tensor # noqa: F401 from bittensor.core.threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor # noqa: F401 -from bittensor.mock.subtensor_mock import MockSubtensor as MockSubtensor # noqa: F401 +from bittensor.utils.mock.subtensor_mock import MockSubtensor as MockSubtensor # noqa: F401 from bittensor.utils import ( # noqa: F401 ss58_to_vec_u8, unbiased_topk, @@ -146,3 +146,7 @@ # Makes the `bittensor.api.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. extrinsics = importlib.import_module('bittensor.api.extrinsics') sys.modules['bittensor.extrinsics'] = extrinsics + +# Makes the `bittensor.utils.mock` subpackage available as `bittensor.mock` for backwards compatibility. +extrinsics = importlib.import_module('bittensor.utils.mock') +sys.modules['bittensor.mock'] = extrinsics diff --git a/bittensor/mock/__init__.py b/bittensor/utils/mock/__init__.py similarity index 95% rename from bittensor/mock/__init__.py rename to bittensor/utils/mock/__init__.py index b4f0efd5c..218579a15 100644 --- a/bittensor/mock/__init__.py +++ b/bittensor/utils/mock/__init__.py @@ -15,4 +15,4 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from .subtensor_mock import MockSubtensor as MockSubtensor +from .subtensor_mock import MockSubtensor diff --git a/bittensor/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py similarity index 100% rename from bittensor/mock/subtensor_mock.py rename to bittensor/utils/mock/subtensor_mock.py diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py index 07a13df70..e1cf924cd 100644 --- a/tests/integration_tests/test_metagraph_integration.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -18,7 +18,7 @@ import bittensor import torch import os -from bittensor.mock import MockSubtensor +from bittensor.utils.mock import MockSubtensor from bittensor.core.metagraph import METAGRAPH_STATE_DICT_NDARRAY_KEYS, get_save_dir _subtensor_mock: MockSubtensor = MockSubtensor() diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 9046f1f12..5c2da3e70 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -25,7 +25,7 @@ from substrateinterface import Keypair import bittensor -from bittensor.mock import MockSubtensor +from bittensor.utils.mock import MockSubtensor from bittensor.utils import weight_utils from bittensor.utils.balance import Balance from tests.helpers import ( From f55982c5f71fae9c902130499be8d96ecdfdb464 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 5 Aug 2024 10:00:04 -0700 Subject: [PATCH 063/260] Check for participation before nomination call --- bittensor/api/extrinsics/delegation.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bittensor/api/extrinsics/delegation.py b/bittensor/api/extrinsics/delegation.py index 81f3d89c9..b4de766cb 100644 --- a/bittensor/api/extrinsics/delegation.py +++ b/bittensor/api/extrinsics/delegation.py @@ -62,6 +62,14 @@ def nominate_extrinsic( ) return False + if not subtensor.is_hotkey_registered_any(wallet.hotkey.ss58_address): + logging.error( + "Hotkey {} is not registered to any network".format( + wallet.hotkey.ss58_address + ) + ) + return False + with bt_console.status( ":satellite: Sending nominate call on [white]{}[/white] ...".format( subtensor.network From da89cd38fa70de6002e51557fb3f094b601dcd98 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 5 Aug 2024 11:10:52 -0700 Subject: [PATCH 064/260] Ruff formatting. Fix ruff's complaints. --- bittensor/__init__.py | 3 +- bittensor/api/extrinsics/commit_weights.py | 28 ++-- bittensor/api/extrinsics/delegation.py | 100 +++++-------- bittensor/api/extrinsics/network.py | 59 ++++---- bittensor/api/extrinsics/prometheus.py | 7 +- bittensor/api/extrinsics/registration.py | 131 +++++++----------- bittensor/api/extrinsics/root.py | 83 +++++------ bittensor/api/extrinsics/senate.py | 53 ++++--- bittensor/api/extrinsics/serving.py | 10 +- bittensor/api/extrinsics/set_weights.py | 51 +++---- bittensor/api/extrinsics/staking.py | 75 ++++------ bittensor/api/extrinsics/transfer.py | 47 +++---- bittensor/api/extrinsics/unstaking.py | 129 +++++++---------- bittensor/core/axon.py | 10 +- bittensor/core/chain_data.py | 16 +-- bittensor/core/config.py | 22 +-- bittensor/core/dendrite.py | 32 ++--- bittensor/core/errors.py | 4 +- bittensor/core/metagraph.py | 54 +++++--- bittensor/core/settings.py | 68 ++++----- bittensor/core/subtensor.py | 31 +++-- bittensor/core/synapse.py | 25 ++-- bittensor/core/threadpool.py | 16 +-- bittensor/core/types.py | 1 + bittensor/utils/__init__.py | 4 +- bittensor/utils/backwards_compatibility.py | 25 ++-- bittensor/utils/registration.py | 4 +- bittensor/utils/subnets.py | 1 + bittensor/utils/version.py | 4 +- bittensor/utils/weight_utils.py | 5 +- tests/helpers/helpers.py | 6 +- .../test_subtensor_integration.py | 8 +- tests/unit_tests/extrinsics/test_root.py | 2 +- tests/unit_tests/extrinsics/test_staking.py | 4 +- tests/unit_tests/extrinsics/test_unstaking.py | 9 +- tests/unit_tests/test_axon.py | 7 +- tests/unit_tests/test_chain_data.py | 5 +- tests/unit_tests/test_dendrite.py | 6 +- tests/unit_tests/test_logging.py | 9 +- tests/unit_tests/utils/test_weight_utils.py | 2 +- 40 files changed, 529 insertions(+), 627 deletions(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index f4717dd25..ceb797943 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -60,9 +60,8 @@ def __apply_nest_asyncio(): ) # Install and apply nest asyncio to allow the async functions to run in a .ipynb import nest_asyncio + nest_asyncio.apply() __apply_nest_asyncio() - - diff --git a/bittensor/api/extrinsics/commit_weights.py b/bittensor/api/extrinsics/commit_weights.py index 0078af973..e9f545d59 100644 --- a/bittensor/api/extrinsics/commit_weights.py +++ b/bittensor/api/extrinsics/commit_weights.py @@ -17,7 +17,7 @@ """Module commit weights and reveal weights extrinsic.""" -from typing import Tuple, List +from typing import Tuple, List, TYPE_CHECKING from bittensor_wallet import Wallet from rich.prompt import Confirm @@ -25,9 +25,13 @@ from bittensor.utils import format_error_message from bittensor.utils.btlogging import logging +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + def commit_weights_extrinsic( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", netuid: int, commit_hash: str, @@ -38,6 +42,7 @@ def commit_weights_extrinsic( """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. This function is a wrapper around the `_do_commit_weights` method, handling user prompts and error messages. + Args: subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. @@ -46,11 +51,11 @@ def commit_weights_extrinsic( 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. prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + Returns: - Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. + Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. """ if prompt and not Confirm.ask(f"Would you like to commit weights?"): return False, "User cancelled the operation." @@ -72,7 +77,7 @@ def commit_weights_extrinsic( def reveal_weights_extrinsic( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", netuid: int, uids: List[int], @@ -86,6 +91,7 @@ def reveal_weights_extrinsic( """ 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, handling user prompts and error messages. + Args: subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. @@ -97,11 +103,11 @@ def reveal_weights_extrinsic( 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. prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + Returns: - Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, 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. + Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, 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. """ if prompt and not Confirm.ask(f"Would you like to reveal weights?"): diff --git a/bittensor/api/extrinsics/delegation.py b/bittensor/api/extrinsics/delegation.py index b4de766cb..8b0aeb361 100644 --- a/bittensor/api/extrinsics/delegation.py +++ b/bittensor/api/extrinsics/delegation.py @@ -15,7 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from typing import Union, Optional +from typing import Union, Optional, TYPE_CHECKING from bittensor_wallet import Wallet from rich.prompt import Confirm @@ -30,8 +30,9 @@ from bittensor.core.settings import bt_console from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -import typing -if typing.TYPE_CHECKING: + +# For annotation purposes +if TYPE_CHECKING: from bittensor.core.subtensor import Subtensor @@ -44,10 +45,11 @@ def nominate_extrinsic( """Becomes a delegate for the hotkey. Args: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (Wallet): The wallet to become a delegate for. wait_for_inclusion: wait_for_finalization: - subtensor: - wallet (Wallet): The wallet to become a delegate for. + Returns: success (bool): ``True`` if the transaction was successful. """ @@ -83,9 +85,7 @@ def nominate_extrinsic( ) if success is True: - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") logging.success( prefix="Become Delegate", suffix="Finalized: " + str(success), @@ -95,19 +95,11 @@ def nominate_extrinsic( return success except Exception as e: - bt_console.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) + bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) + logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) except NominationError as e: - bt_console.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) + bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) + logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) return False @@ -124,13 +116,14 @@ def delegate_extrinsic( """Delegates the specified amount of stake to the passed delegate. Args: - subtensor: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. wallet (Wallet): Bittensor wallet object. delegate_ss58 (Optional[str]): The ``ss58`` address of the delegate. amount (Union[Balance, float]): Amount to stake as bittensor balance, or ``float`` interpreted as Tao. 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. prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. @@ -203,9 +196,7 @@ def delegate_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") with bt_console.status( ":satellite: Checking Balance on: [white]{}[/white] ...".format( subtensor.network @@ -231,9 +222,7 @@ def delegate_extrinsic( ) return True else: - bt_console.print( - ":cross_mark: [red]Failed[/red]: Error unknown." - ) + bt_console.print(":cross_mark: [red]Failed[/red]: Error unknown.") return False except NotRegisteredError: @@ -260,13 +249,14 @@ def undelegate_extrinsic( """Un-delegates stake from the passed delegate. Args: - subtensor: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. wallet (Wallet): Bittensor wallet object. delegate_ss58 (Optional[str]): The ``ss58`` address of the delegate. amount (Union[Balance, float]): Amount to unstake as bittensor balance, or ``float`` interpreted as Tao. 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. prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. @@ -335,9 +325,7 @@ def undelegate_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") with bt_console.status( ":satellite: Checking Balance on: [white]{}[/white] ...".format( subtensor.network @@ -363,9 +351,7 @@ def undelegate_extrinsic( ) return True else: - bt_console.print( - ":cross_mark: [red]Failed[/red]: Error unknown." - ) + bt_console.print(":cross_mark: [red]Failed[/red]: Error unknown.") return False except NotRegisteredError: @@ -391,15 +377,13 @@ def decrease_take_extrinsic( """Decrease delegate take for the hotkey. Args: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (Wallet): Bittensor wallet object. + hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. + take (float): The ``take`` of the hotkey. wait_for_inclusion: wait_for_finalization: - subtensor: - wallet (Wallet): - Bittensor wallet object. - hotkey_ss58 (Optional[str]): - The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. - take (float): - The ``take`` of the hotkey. + Returns: success (bool): ``True`` if the transaction was successful. """ @@ -422,9 +406,7 @@ def decrease_take_extrinsic( ) if success is True: - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") logging.success( prefix="Decrease Delegate Take", suffix="Finalized: " + str(success), @@ -433,12 +415,8 @@ def decrease_take_extrinsic( return success except (TakeError, Exception) as e: - bt_console.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) + bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) + logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) return False @@ -454,7 +432,7 @@ def increase_take_extrinsic( """Increase delegate take for the hotkey. Args: - subtensor: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. wallet (Wallet): Bittensor wallet object. hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. take (float): The ``take`` of the hotkey. @@ -483,9 +461,7 @@ def increase_take_extrinsic( ) if success is True: - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") logging.success( prefix="Increase Delegate Take", suffix="Finalized: " + str(success), @@ -494,18 +470,10 @@ def increase_take_extrinsic( return success except Exception as e: - bt_console.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) + bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) + logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) except TakeError as e: - bt_console.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) + bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) + logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) return False diff --git a/bittensor/api/extrinsics/network.py b/bittensor/api/extrinsics/network.py index 03017fe75..4703cb285 100644 --- a/bittensor/api/extrinsics/network.py +++ b/bittensor/api/extrinsics/network.py @@ -16,8 +16,10 @@ # DEALINGS IN THE SOFTWARE. import time +from typing import TYPE_CHECKING import substrateinterface +from bittensor_wallet import Wallet from rich.prompt import Confirm from bittensor.api.extrinsics.utils import HYPERPARAMS @@ -25,6 +27,10 @@ from bittensor.utils import format_error_message from bittensor.utils.balance import Balance +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + def _find_event_attributes_in_extrinsic_receipt( response: "substrateinterface.base.ExtrinsicReceipt", event_name: str @@ -50,23 +56,21 @@ def _find_event_attributes_in_extrinsic_receipt( def register_subnetwork_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - r"""Registers a new subnetwork. + """Registers a new subnetwork. Args: - wallet (bittensor.wallet): - bittensor wallet object. - 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. - prompt (bool): - If true, the call waits for confirmation from the user before proceeding. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (bittensor.wallet): bittensor wallet object. + 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. + prompt (bool): If true, the call waits for confirmation from the user before proceeding. + Returns: success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. @@ -130,8 +134,8 @@ def register_subnetwork_extrinsic( def set_hyperparameter_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", netuid: int, parameter: str, value, @@ -139,23 +143,18 @@ def set_hyperparameter_extrinsic( wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - r"""Sets a hyperparameter for a specific subnetwork. + """Sets a hyperparameter for a specific subnetwork. Args: - wallet (bittensor.wallet): - bittensor wallet object. - netuid (int): - Subnetwork ``uid``. - parameter (str): - Hyperparameter name. - value (any): - New hyperparameter value. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (bittensor.wallet): bittensor wallet object. + netuid (int): Subnetwork ``uid``. + parameter (str): Hyperparameter name. + value (any): New hyperparameter value. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. @@ -171,9 +170,7 @@ def set_hyperparameter_extrinsic( extrinsic = HYPERPARAMS.get(parameter) if extrinsic is None: - bt_console.print( - ":cross_mark: [red]Invalid hyperparameter specified.[/red]" - ) + bt_console.print(":cross_mark: [red]Invalid hyperparameter specified.[/red]") return False with bt_console.status( diff --git a/bittensor/api/extrinsics/prometheus.py b/bittensor/api/extrinsics/prometheus.py index dbfd3d92f..6bf58a882 100644 --- a/bittensor/api/extrinsics/prometheus.py +++ b/bittensor/api/extrinsics/prometheus.py @@ -16,6 +16,7 @@ # DEALINGS IN THE SOFTWARE. import json +from typing import TYPE_CHECKING from bittensor_wallet import Wallet @@ -24,9 +25,13 @@ from bittensor.utils import networking as net from bittensor.utils.btlogging import logging +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + def prometheus_extrinsic( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", port: int, netuid: int, diff --git a/bittensor/api/extrinsics/registration.py b/bittensor/api/extrinsics/registration.py index f40637a4f..d616eda2d 100644 --- a/bittensor/api/extrinsics/registration.py +++ b/bittensor/api/extrinsics/registration.py @@ -16,26 +16,28 @@ # DEALINGS IN THE SOFTWARE. import time -from typing import List, Union, Optional, Tuple +from typing import List, Union, Optional, Tuple, TYPE_CHECKING +from bittensor_wallet import Wallet from rich.prompt import Confirm - +from bittensor.core.settings import bt_console from bittensor.utils import format_error_message - +from bittensor.utils.btlogging import logging from bittensor.utils.registration import ( POWSolution, create_pow, torch, log_no_torch_error, ) -from bittensor.core.settings import bt_console -from bittensor.utils.btlogging import logging -from bittensor_wallet import Wallet + +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor def register_extrinsic( - subtensor: "bittensor.subtensor", + subtensor: "Subtensor", wallet: "Wallet", netuid: int, wait_for_inclusion: bool = False, @@ -50,38 +52,26 @@ def register_extrinsic( update_interval: Optional[int] = None, log_verbose: bool = False, ) -> bool: - r"""Registers the wallet to the chain. + """Registers the wallet to the chain. Args: - subtensor: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (Wallet): Bittensor wallet object. + netuid (int): The ``netuid`` of the subnet to register on. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + max_allowed_attempts (int): Maximum number of attempts to register the wallet. output_in_place: - wallet (Wallet): - Bittensor wallet object. - netuid (int): - The ``netuid`` of the subnet to register on. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - max_allowed_attempts (int): - Maximum number of attempts to register the wallet. - cuda (bool): - If ``true``, the wallet should be registered using CUDA device(s). - dev_id (Union[List[int], int]): - The CUDA device id to use, or a list of device ids. - tpb (int): - The number of threads per block (CUDA). - num_processes (int): - The number of processes to use to register. - update_interval (int): - The number of nonces to solve between updates. - log_verbose (bool): - If ``true``, the registration process will log more information. + cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). + dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. + tpb (int): The number of threads per block (CUDA). + num_processes (int): The number of processes to use to register. + update_interval (int): The number of nonces to solve between updates. + log_verbose (bool): If ``true``, the registration process will log more information. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ if not subtensor.subnet_exists(netuid): bt_console.print( @@ -188,9 +178,7 @@ def register_extrinsic( ) return True - bt_console.print( - f":cross_mark: [red]Failed[/red]: {err_msg}" - ) + bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") time.sleep(0.5) # Successful registration, final check for neuron and pubkey @@ -231,7 +219,7 @@ def register_extrinsic( def burned_register_extrinsic( - subtensor: "bittensor.subtensor", + subtensor: "Subtensor", wallet: "Wallet", netuid: int, wait_for_inclusion: bool = False, @@ -241,17 +229,13 @@ def burned_register_extrinsic( """Registers the wallet to chain by recycling TAO. Args: - subtensor: - wallet (Wallet): - Bittensor wallet object. - netuid (int): - The ``netuid`` of the subnet to register on. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (Wallet): Bittensor wallet object. + netuid (int): The ``netuid`` of the subnet to register on. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. @@ -321,9 +305,7 @@ def burned_register_extrinsic( netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address ) if is_registered: - bt_console.print( - ":white_heavy_check_mark: [green]Registered[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Registered[/green]") return True else: # neuron not found, try again @@ -342,7 +324,7 @@ class MaxAttemptsException(Exception): def run_faucet_extrinsic( - subtensor: "bittensor.subtensor", + subtensor: "Subtensor", wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, @@ -359,33 +341,22 @@ def run_faucet_extrinsic( """Runs a continual POW to get a faucet of TAO on the test net. Args: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (Wallet): Bittensor wallet object. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + 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. + max_allowed_attempts (int): Maximum number of attempts to register the wallet. + cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). + dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. + tpb (int): The number of threads per block (CUDA). + num_processes (int): The number of processes to use to register. + update_interval (int): The number of nonces to solve between updates. + log_verbose (bool): If ``true``, the registration process will log more information. output_in_place: - subtensor: - wallet (Wallet): - Bittensor wallet object. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - 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. - max_allowed_attempts (int): - Maximum number of attempts to register the wallet. - cuda (bool): - If ``true``, the wallet should be registered using CUDA device(s). - dev_id (Union[List[int], int]): - The CUDA device id to use, or a list of device ids. - tpb (int): - The number of threads per block (CUDA). - num_processes (int): - The number of processes to use to register. - update_interval (int): - The number of nonces to solve between updates. - log_verbose (bool): - If ``true``, the registration process will log more information. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ if prompt: if not Confirm.ask( @@ -497,7 +468,7 @@ def run_faucet_extrinsic( def swap_hotkey_extrinsic( - subtensor: "bittensor.subtensor", + subtensor: "Subtensor", wallet: "Wallet", new_wallet: "Wallet", wait_for_inclusion: bool = False, diff --git a/bittensor/api/extrinsics/root.py b/bittensor/api/extrinsics/root.py index 28ae10cc3..3bd950c55 100644 --- a/bittensor/api/extrinsics/root.py +++ b/bittensor/api/extrinsics/root.py @@ -17,22 +17,26 @@ import logging import time -from typing import Union, List +from typing import Union, List, TYPE_CHECKING import numpy as np from bittensor_wallet import Wallet from numpy.typing import NDArray from rich.prompt import Confirm -from bittensor.utils import weight_utils from bittensor.core.settings import bt_console +from bittensor.utils import weight_utils from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, legacy_torch_api_compat +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + def root_register_extrinsic( - subtensor, - wallet, + subtensor: "Subtensor", + wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, @@ -40,18 +44,14 @@ def root_register_extrinsic( """Registers the wallet to root network. Args: - subtensor: - wallet (Wallet): - Bittensor wallet object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (bittensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (Wallet): Bittensor wallet object. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ wallet.coldkey # unlock coldkey @@ -87,9 +87,7 @@ def root_register_extrinsic( netuid=0, hotkey_ss58=wallet.hotkey.ss58_address ) if is_registered: - bt_console.print( - ":white_heavy_check_mark: [green]Registered[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Registered[/green]") return True else: # neuron not found, try again @@ -100,8 +98,8 @@ def root_register_extrinsic( @legacy_torch_api_compat def set_root_weights_extrinsic( - subtensor, - wallet, + subtensor: "Subtensor", + wallet: "Wallet", netuids: Union[NDArray[np.int64], "torch.LongTensor", List[int]], weights: Union[NDArray[np.float32], "torch.FloatTensor", List[float]], version_key: int = 0, @@ -109,27 +107,20 @@ def set_root_weights_extrinsic( wait_for_finalization: bool = False, prompt: bool = False, ) -> bool: - r"""Sets the given weights and values on chain for wallet hotkey account. + """Sets the given weights and values on chain for wallet hotkey account. Args: - subtensor: - wallet (Wallet): - Bittensor wallet object. - netuids (Union[NDArray[np.int64], torch.LongTensor, List[int]]): - The ``netuid`` of the subnet to set weights for. - weights (Union[NDArray[np.float32], torch.FloatTensor, list]): - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (bittensor.core.subtensor.Subtensor: The Subtensor instance object. + wallet (Wallet): Bittensor wallet object. + netuids (Union[NDArray[np.int64], torch.LongTensor, List[int]]): The ``netuid`` of the subnet to set weights for. + weights (Union[NDArray[np.float32], torch.FloatTensor, list]): 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ wallet.coldkey # unlock coldkey @@ -197,18 +188,14 @@ def set_root_weights_extrinsic( return True if success is True: - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") logging.success( prefix="Set weights", suffix="Finalized: " + str(success), ) return True else: - bt_console.print( - f":cross_mark: [red]Failed[/red]: {error_message}" - ) + bt_console.print(f":cross_mark: [red]Failed[/red]: {error_message}") logging.warning( prefix="Set weights", suffix="Failed: " + str(error_message), @@ -217,10 +204,6 @@ def set_root_weights_extrinsic( except Exception as e: # TODO( devs ): lets remove all of the bt_console calls and replace with the bittensor logger. - bt_console.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) + bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) + logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) return False diff --git a/bittensor/api/extrinsics/senate.py b/bittensor/api/extrinsics/senate.py index 03e284f9d..9ceebf9ec 100644 --- a/bittensor/api/extrinsics/senate.py +++ b/bittensor/api/extrinsics/senate.py @@ -16,16 +16,21 @@ # DEALINGS IN THE SOFTWARE. import time +from typing import TYPE_CHECKING +from bittensor_wallet import Wallet from rich.prompt import Confirm from bittensor.core.settings import bt_console from bittensor.utils import format_error_message -from bittensor_wallet import Wallet + +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor def register_senate_extrinsic( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, @@ -34,7 +39,7 @@ def register_senate_extrinsic( """Registers the wallet to chain for senate voting. Args: - subtensor: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. wallet (bittensor.wallet): Bittensor wallet object. 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. @@ -98,27 +103,23 @@ def register_senate_extrinsic( def leave_senate_extrinsic( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - r"""Removes the wallet from chain for senate voting. + """Removes the wallet from chain for senate voting. Args: - subtensor: - wallet (bittensor.wallet): - Bittensor wallet object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (bittensor.wallet): Bittensor wallet object. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + 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``. + 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``. """ wallet.coldkey # unlock coldkey wallet.hotkey # unlock hotkey @@ -174,7 +175,7 @@ def leave_senate_extrinsic( def vote_senate_extrinsic( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", proposal_hash: str, proposal_idx: int, @@ -183,21 +184,19 @@ def vote_senate_extrinsic( wait_for_finalization: bool = True, prompt: bool = False, ) -> bool: - """Votes ayes or nays on proposals. + """ + Votes ayes or nays on proposals. Args: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (bittensor.wallet): Bittensor wallet object. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. vote: proposal_idx: proposal_hash: - subtensor: - wallet (bittensor.wallet): - Bittensor wallet object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + 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``. diff --git a/bittensor/api/extrinsics/serving.py b/bittensor/api/extrinsics/serving.py index 8aa4939cc..9790c25ed 100644 --- a/bittensor/api/extrinsics/serving.py +++ b/bittensor/api/extrinsics/serving.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. import json -from typing import Optional +from typing import Optional, TYPE_CHECKING from bittensor_wallet import Wallet from retry import retry @@ -29,9 +29,13 @@ from bittensor.utils import format_error_message, networking as net from bittensor.utils.btlogging import logging +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + def serve_extrinsic( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", ip: str, port: int, @@ -135,7 +139,7 @@ def serve_extrinsic( def serve_axon_extrinsic( - subtensor, + subtensor: "Subtensor", netuid: int, axon: "Axon", wait_for_inclusion: bool = False, diff --git a/bittensor/api/extrinsics/set_weights.py b/bittensor/api/extrinsics/set_weights.py index 4312d8a85..80a51f763 100644 --- a/bittensor/api/extrinsics/set_weights.py +++ b/bittensor/api/extrinsics/set_weights.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. import logging -from typing import Union, Tuple +from typing import Union, Tuple, TYPE_CHECKING import numpy as np from bittensor_wallet import Wallet @@ -28,9 +28,13 @@ from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + def set_weights_extrinsic( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", netuid: int, uids: Union[NDArray[np.int64], "torch.LongTensor", list], @@ -43,27 +47,18 @@ def set_weights_extrinsic( """Sets the given weights and values on chain for wallet hotkey account. Args: - subtensor (bittensor.subtensor): - Subtensor endpoint to use. - wallet (bittensor.wallet): - Bittensor wallet object. - netuid (int): - The ``netuid`` of the subnet to set weights for. - 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. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (bittensor.subtensor): Subtensor endpoint to use. + wallet (bittensor.wallet): Bittensor wallet object. + netuid (int): The ``netuid`` of the subnet to set weights for. + 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. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ # First convert types. if use_torch(): @@ -109,9 +104,7 @@ def set_weights_extrinsic( return True, "Not waiting for finalization or inclusion." if success is True: - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") logging.success( prefix="Set weights", suffix="Finalized: " + str(success), @@ -126,10 +119,6 @@ def set_weights_extrinsic( return False, error_message except Exception as e: - bt_console.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) + bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) + logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) return False, str(e) diff --git a/bittensor/api/extrinsics/staking.py b/bittensor/api/extrinsics/staking.py index c647efce3..556a8cd40 100644 --- a/bittensor/api/extrinsics/staking.py +++ b/bittensor/api/extrinsics/staking.py @@ -17,26 +17,29 @@ from rich.prompt import Confirm from time import sleep -from typing import List, Union, Optional, Tuple +from typing import List, Union, Optional, Tuple, TYPE_CHECKING from bittensor.core.errors import NotDelegateError, StakeError, NotRegisteredError from bittensor.core.settings import __console__ as bt_console from bittensor.utils.balance import Balance from bittensor_wallet import Wallet +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor -def _check_threshold_amount(subtensor, stake_balance: "Balance") -> Tuple[bool, "Balance"]: + +def _check_threshold_amount( + subtensor: "Subtensor", stake_balance: "Balance" +) -> Tuple[bool, "Balance"]: """ Checks if the new stake balance will be above the minimum required stake threshold. Args: - stake_balance (Balance): - the balance to check for threshold limits. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + stake_balance (Balance): the balance to check for threshold limits. Returns: - success, threshold (bool, Balance): - ``true`` if the staking balance is above the threshold, or ``false`` if the - staking balance is below the threshold. - The threshold balance required to stake. + success, threshold (bool, Balance): ``true`` if the staking balance is above the threshold, or ``false`` if the staking balance is below the threshold. The threshold balance required to stake. """ min_req_stake: "Balance" = subtensor.get_minimum_required_stake() @@ -58,6 +61,7 @@ def add_stake_extrinsic( """Adds the specified amount of stake to passed hotkey ``uid``. Args: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. wallet (Wallet): Bittensor wallet object. hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. @@ -184,9 +188,7 @@ def add_stake_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") with bt_console.status( ":satellite: Checking Balance on: [white]{}[/white] ...".format( subtensor.network @@ -214,9 +216,7 @@ def add_stake_extrinsic( ) return True else: - bt_console.print( - ":cross_mark: [red]Failed[/red]: Error unknown." - ) + bt_console.print(":cross_mark: [red]Failed[/red]: Error unknown.") return False except NotRegisteredError: @@ -240,24 +240,19 @@ def add_stake_multiple_extrinsic( wait_for_finalization: bool = False, prompt: bool = False, ) -> bool: - r"""Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. + """Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. Args: - wallet (bittensor_wallet.Wallet): - Bittensor wallet object for the coldkey. - hotkey_ss58s (List[str]): - List of hotkeys to stake to. - amounts (List[Union[Balance, float]]): - List of amounts to stake. If ``None``, stake all to the first hotkey. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (bittensor_wallet.Wallet): Bittensor wallet object for the coldkey. + hotkey_ss58s (List[str]): List of hotkeys to stake to. + amounts (List[Union[Balance, float]]): List of amounts to stake. If ``None``, stake all to the first hotkey. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any wallet was staked. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any wallet was staked. If we did not wait for finalization / inclusion, the response is ``true``. """ if not isinstance(hotkey_ss58s, list) or not all( isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s @@ -273,9 +268,7 @@ def add_stake_multiple_extrinsic( if amounts is not None and not all( isinstance(amount, (Balance, float)) for amount in amounts ): - raise TypeError( - "amounts must be a [list of Balance or float] or None" - ) + raise TypeError("amounts must be a [list of Balance or float] or None") if amounts is None: amounts = [None] * len(hotkey_ss58s) @@ -396,9 +389,7 @@ def add_stake_multiple_extrinsic( continue - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") block = subtensor.get_current_block() new_stake = subtensor.get_stake_for_coldkey_and_hotkey( @@ -421,9 +412,7 @@ def add_stake_multiple_extrinsic( break else: - bt_console.print( - ":cross_mark: [red]Failed[/red]: Error unknown." - ) + bt_console.print(":cross_mark: [red]Failed[/red]: Error unknown.") continue except NotRegisteredError: @@ -434,9 +423,7 @@ def add_stake_multiple_extrinsic( ) continue except StakeError as e: - bt_console.print( - ":cross_mark: [red]Stake Error: {}[/red]".format(e) - ) + bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) continue if successful_stakes != 0: @@ -468,12 +455,12 @@ def __do_add_stake_single( Executes a stake call to the chain using the wallet and the amount specified. Args: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. wallet (bittensor_wallet.Wallet): Bittensor wallet object. hotkey_ss58 (str): Hotkey to stake to. amount (Balance): Amount to stake as Bittensor balance object. 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. Returns: success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. @@ -492,9 +479,7 @@ def __do_add_stake_single( # We are delegating. # Verify that the hotkey is a delegate. if not subtensor.is_hotkey_delegate(hotkey_ss58=hotkey_ss58): - raise NotDelegateError( - "Hotkey: {} is not a delegate.".format(hotkey_ss58) - ) + raise NotDelegateError("Hotkey: {} is not a delegate.".format(hotkey_ss58)) success = subtensor._do_stake( wallet=wallet, diff --git a/bittensor/api/extrinsics/transfer.py b/bittensor/api/extrinsics/transfer.py index a58cf6c40..f7362443b 100644 --- a/bittensor/api/extrinsics/transfer.py +++ b/bittensor/api/extrinsics/transfer.py @@ -15,7 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from typing import Union +from typing import Union, TYPE_CHECKING from rich.prompt import Confirm @@ -23,11 +23,16 @@ from bittensor.utils import get_explorer_url_for_network from bittensor.utils import is_valid_bittensor_address_or_public_key from bittensor.utils.balance import Balance +from bittensor_wallet import Wallet + +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor def transfer_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", dest: str, amount: Union[Balance, float], wait_for_inclusion: bool = True, @@ -35,26 +40,20 @@ def transfer_extrinsic( keep_alive: bool = True, prompt: bool = False, ) -> bool: - r"""Transfers funds from this wallet to the destination public key address. + """Transfers funds from this wallet to the destination public key address. Args: - wallet (bittensor.wallet): - Bittensor wallet object to make transfer from. - dest (str, ss58_address or ed25519): - Destination public key address of reciever. - amount (Union[Balance, int]): - Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - 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. - keep_alive (bool): - If set, keeps the account alive by keeping the balance above the existential deposit. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (bittensor.wallet): Bittensor wallet object to make transfer from. + dest (str, ss58_address or ed25519): Destination public key address of receiver. + amount (Union[Balance, int]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. + 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. + keep_alive (bool): If set, keeps the account alive by keeping the balance above the existential deposit. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ # Validate destination address. if not is_valid_bittensor_address_or_public_key(dest): @@ -121,12 +120,8 @@ def transfer_extrinsic( ) if success: - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - bt_console.print( - "[green]Block Hash: {}[/green]".format(block_hash) - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") + bt_console.print("[green]Block Hash: {}[/green]".format(block_hash)) explorer_urls = get_explorer_url_for_network( subtensor.network, block_hash, network_explorer_map diff --git a/bittensor/api/extrinsics/unstaking.py b/bittensor/api/extrinsics/unstaking.py index 4d1517d47..a16e4660f 100644 --- a/bittensor/api/extrinsics/unstaking.py +++ b/bittensor/api/extrinsics/unstaking.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. from time import sleep -from typing import List, Union, Optional +from typing import List, Union, Optional, TYPE_CHECKING from bittensor_wallet import Wallet from rich.prompt import Confirm @@ -25,39 +25,36 @@ from bittensor.core.settings import bt_console from bittensor.utils.balance import Balance +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + def __do_remove_stake_single( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", hotkey_ss58: str, amount: "Balance", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, ) -> bool: - r""" + """ Executes an unstake call to the chain using the wallet and the amount specified. Args: - wallet (Wallet): - Bittensor wallet object. - hotkey_ss58 (str): - Hotkey address to unstake from. - amount (bittensor.Balance): - Amount to unstake as Bittensor balance object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (Wallet): Bittensor wallet object. + hotkey_ss58 (str): Hotkey address to unstake from. + amount (bittensor.Balance): Amount to unstake as Bittensor balance object. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. Raises: - bittensor.core.errors.StakeError: - If the extrinsic fails to be finalized or included in the block. - bittensor.core.errors.NotRegisteredError: - If the hotkey is not registered in any subnets. + bittensor.core.errors.StakeError: If the extrinsic fails to be finalized or included in the block. + bittensor.core.errors.NotRegisteredError: If the hotkey is not registered in any subnets. """ # Decrypt keys, @@ -74,20 +71,16 @@ def __do_remove_stake_single( return success -def check_threshold_amount( - subtensor, stake_balance: Balance -) -> bool: +def check_threshold_amount(subtensor: "Subtensor", stake_balance: Balance) -> bool: """ Checks if the remaining stake balance is above the minimum required stake threshold. Args: - stake_balance (Balance): - the balance to check for threshold limits. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + stake_balance (Balance): the balance to check for threshold limits. Returns: - success (bool): - ``true`` if the unstaking is above the threshold or 0, or ``false`` if the - unstaking is below the threshold, but not 0. + success (bool): ``true`` if the unstaking is above the threshold or 0, or ``false`` if the unstaking is below the threshold, but not 0. """ min_req_stake: Balance = subtensor.get_minimum_required_stake() @@ -101,7 +94,7 @@ def check_threshold_amount( def unstake_extrinsic( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", hotkey_ss58: Optional[str] = None, amount: Optional[Union[Balance, float]] = None, @@ -109,24 +102,19 @@ def unstake_extrinsic( wait_for_finalization: bool = False, prompt: bool = False, ) -> bool: - r"""Removes stake into the wallet coldkey from the specified hotkey ``uid``. + """Removes stake into the wallet coldkey from the specified hotkey ``uid``. Args: - wallet (Wallet): - Bittensor wallet object. - hotkey_ss58 (Optional[str]): - The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey is used. - amount (Union[Balance, float]): - Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (Wallet): Bittensor wallet object. + hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey is used. + amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ # Decrypt keys, wallet.coldkey @@ -204,9 +192,7 @@ def unstake_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") with bt_console.status( ":satellite: Checking Balance on: [white]{}[/white] ...".format( subtensor.network @@ -230,9 +216,7 @@ def unstake_extrinsic( ) return True else: - bt_console.print( - ":cross_mark: [red]Failed[/red]: Unknown Error." - ) + bt_console.print(":cross_mark: [red]Failed[/red]: Unknown Error.") return False except NotRegisteredError as e: @@ -248,7 +232,7 @@ def unstake_extrinsic( def unstake_multiple_extrinsic( - subtensor, + subtensor: "Subtensor", wallet: "Wallet", hotkey_ss58s: List[str], amounts: Optional[List[Union[Balance, float]]] = None, @@ -256,24 +240,19 @@ def unstake_multiple_extrinsic( wait_for_finalization: bool = False, prompt: bool = False, ) -> bool: - r"""Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. + """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. Args: - wallet (Wallet): - The wallet with the coldkey to unstake to. - hotkey_ss58s (List[str]): - List of hotkeys to unstake from. - amounts (List[Union[Balance, float]]): - List of amounts to unstake. If ``None``, unstake all. - wait_for_inclusion (bool): - If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): - If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (Wallet): The wallet with the coldkey to unstake to. + hotkey_ss58s (List[str]): List of hotkeys to unstake from. + amounts (List[Union[Balance, float]]): List of amounts to unstake. If ``None``, unstake all. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``true``. + success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``true``. """ if not isinstance(hotkey_ss58s, list) or not all( isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s @@ -289,9 +268,7 @@ def unstake_multiple_extrinsic( if amounts is not None and not all( isinstance(amount, (Balance, float)) for amount in amounts ): - raise TypeError( - "amounts must be a [list of Balance or float] or None" - ) + raise TypeError("amounts must be a [list of Balance or float] or None") if amounts is None: amounts = [None] * len(hotkey_ss58s) @@ -401,9 +378,7 @@ def unstake_multiple_extrinsic( successful_unstakes += 1 continue - bt_console.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") with bt_console.status( ":satellite: Checking Balance on: [white]{}[/white] ...".format( subtensor.network @@ -422,9 +397,7 @@ def unstake_multiple_extrinsic( ) successful_unstakes += 1 else: - bt_console.print( - ":cross_mark: [red]Failed[/red]: Unknown Error." - ) + bt_console.print(":cross_mark: [red]Failed[/red]: Unknown Error.") continue except NotRegisteredError as e: @@ -433,9 +406,7 @@ def unstake_multiple_extrinsic( ) continue except StakeError as e: - bt_console.print( - ":cross_mark: [red]Stake Error: {}[/red]".format(e) - ) + bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) continue if successful_unstakes != 0: diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 2f914da9e..48a4ee908 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -61,6 +61,10 @@ from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds from bittensor.utils.btlogging import logging +# Just for annotation checker +if typing.TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + V_7_2_0 = 7002000 @@ -314,7 +318,7 @@ def __init__( # Build and check config. if config is None: config = Axon.config() - config: "Config" = copy.deepcopy(config) + config = copy.deepcopy(config) config.axon.ip = ip or defaults.axon.ip config.axon.port = port or defaults.axon.port config.axon.external_ip = external_ip or defaults.axon.external_ip @@ -681,11 +685,11 @@ def check_config(cls, config: "Config"): AssertionError: If the axon or external ports are not in range [1024, 65535] """ assert ( - 1024 < config.axon.port < 65535 + 1024 < config.axon.port < 65535 ), "Axon port must be in range [1024, 65535]" assert config.axon.external_port is None or ( - 1024 < config.axon.external_port < 65535 + 1024 < config.axon.external_port < 65535 ), "External port must be in range [1024, 65535]" def to_string(self): diff --git a/bittensor/core/chain_data.py b/bittensor/core/chain_data.py index 0cc5a6e06..9228afce0 100644 --- a/bittensor/core/chain_data.py +++ b/bittensor/core/chain_data.py @@ -432,9 +432,7 @@ def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfo": neuron_info_decoded["coldkey"], ss58_format ) stake_dict = { - ss58_encode(coldkey, ss58_format): Balance.from_rao( - int(stake) - ) + ss58_encode(coldkey, ss58_format): Balance.from_rao(int(stake)) for coldkey, stake in neuron_info_decoded["stake"] } neuron_info_decoded["stake_dict"] = stake_dict @@ -579,9 +577,7 @@ def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfoLite": neuron_info_decoded["coldkey"], ss58_format ) stake_dict = { - ss58_encode(coldkey, ss58_format): Balance.from_rao( - int(stake) - ) + ss58_encode(coldkey, ss58_format): Balance.from_rao(int(stake)) for coldkey, stake in neuron_info_decoded["stake"] } neuron_info_decoded["stake_dict"] = stake_dict @@ -752,9 +748,7 @@ def fix_decoded_values(cls, decoded: Any) -> "DelegateInfo": """Fixes the decoded values.""" return cls( - hotkey_ss58=ss58_encode( - decoded["delegate_ss58"], ss58_format - ), + hotkey_ss58=ss58_encode(decoded["delegate_ss58"], ss58_format), owner_ss58=ss58_encode(decoded["owner_ss58"], ss58_format), take=U16_NORMALIZED_FLOAT(decoded["take"]), nominators=[ @@ -1201,6 +1195,4 @@ def decode_account_id_list(cls, vec_u8: List[int]) -> Optional[List[str]]: ) if decoded is None: return None - return [ - ss58_encode(account_id, ss58_format) for account_id in decoded - ] + return [ss58_encode(account_id, ss58_format) for account_id in decoded] diff --git a/bittensor/core/config.py b/bittensor/core/config.py index b18db2f40..86f46bdc9 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -61,11 +61,11 @@ class config(DefaultMunch): """ def __init__( - self, - parser: argparse.ArgumentParser = None, - args: Optional[List[str]] = None, - strict: bool = False, - default: Optional[Any] = None, + self, + parser: argparse.ArgumentParser = None, + args: Optional[List[str]] = None, + strict: bool = False, + default: Optional[Any] = None, ) -> None: super().__init__(default) @@ -134,9 +134,9 @@ def __init__( # 1.1 Optionally load defaults if the --config is set. try: config_file_path = ( - str(os.getcwd()) - + "/" - + vars(parser.parse_known_args(args)[0])["config"] + str(os.getcwd()) + + "/" + + vars(parser.parse_known_args(args)[0])["config"] ) except Exception as e: config_file_path = None @@ -240,7 +240,7 @@ def __split_params__(params: argparse.Namespace, _config: "config"): keys = split_keys while len(keys) > 1: if ( - hasattr(head, keys[0]) and head[keys[0]] is not None + hasattr(head, keys[0]) and head[keys[0]] is not None ): # Needs to be Config head = getattr(head, keys[0]) keys = keys[1:] @@ -253,7 +253,7 @@ def __split_params__(params: argparse.Namespace, _config: "config"): @staticmethod def __parse_args__( - args: List[str], parser: argparse.ArgumentParser = None, strict: bool = False + args: List[str], parser: argparse.ArgumentParser = None, strict: bool = False ) -> argparse.Namespace: """Parses the passed args use the passed parser. @@ -382,7 +382,7 @@ def is_set(self, param_name: str) -> bool: return self.get("__is_set")[param_name] def __check_for_missing_required_args( - self, parser: argparse.ArgumentParser, args: List[str] + self, parser: argparse.ArgumentParser, args: List[str] ) -> List[str]: required_args = self.__get_required_args_from_parser(parser) missing_args = [arg for arg in required_args if not any(arg in s for s in args)] diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 7bd618acd..0d7483100 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -114,9 +114,7 @@ class DendriteMixin: d( bittensor.core.axon.Axon(), bittensor.core.synapse.Synapse ) """ - def __init__( - self, wallet: Optional[Union["Wallet", Keypair]] = None - ): + def __init__(self, wallet: Optional[Union["Wallet", Keypair]] = None): """ Initializes the Dendrite object, setting up essential properties. @@ -340,7 +338,9 @@ def _log_incoming_response(self, synapse): f"dendrite | <-- | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | {synapse.dendrite.status_code} | {synapse.dendrite.status_message}" ) - def query(self, *args, **kwargs) -> "List[Union[AsyncGenerator[Any, Any], Synapse, StreamingSynapse]]": + def query( + self, *args, **kwargs + ) -> "List[Union[AsyncGenerator[Any, Any], Synapse, StreamingSynapse]]": """ Makes a synchronous request to multiple target Axons and returns the server responses. @@ -430,9 +430,7 @@ async def forward( axons = [axons] # Check if synapse is an instance of the StreamingSynapse class or if streaming flag is set. - is_streaming_subclass = issubclass( - synapse.__class__, StreamingSynapse - ) + is_streaming_subclass = issubclass(synapse.__class__, StreamingSynapse) if streaming != is_streaming_subclass: logging.warning( f"Argument streaming is {streaming} while issubclass(synapse, StreamingSynapse) is {synapse.__class__.__name__}. This may cause unexpected behavior." @@ -441,9 +439,7 @@ async def forward( async def query_all_axons( is_stream: bool, - ) -> Union[ - AsyncGenerator[Any, Any], Synapse, StreamingSynapse - ]: + ) -> Union[AsyncGenerator[Any, Any], Synapse, StreamingSynapse]: """ Handles the processing of requests to all targeted axons, accommodating both streaming and non-streaming responses. @@ -540,9 +536,7 @@ async def call( # Record start time start_time = time.time() target_axon = ( - target_axon.info() - if isinstance(target_axon, Axon) - else target_axon + target_axon.info() if isinstance(target_axon, Axon) else target_axon ) # Build request endpoint from the synapse class @@ -578,9 +572,7 @@ async def call( self._log_incoming_response(synapse) # Log synapse event history - self.synapse_history.append( - Synapse.from_headers(synapse.to_headers()) - ) + self.synapse_history.append(Synapse.from_headers(synapse.to_headers())) # Return the updated synapse object after deserializing if requested return synapse.deserialize() if deserialize else synapse @@ -614,9 +606,7 @@ async def call_stream( # Record start time start_time = time.time() target_axon = ( - target_axon.info() - if isinstance(target_axon, Axon) - else target_axon + target_axon.info() if isinstance(target_axon, Axon) else target_axon ) # Build request endpoint from the synapse class @@ -660,9 +650,7 @@ async def call_stream( self._log_incoming_response(synapse) # Log synapse event history - self.synapse_history.append( - Synapse.from_headers(synapse.to_headers()) - ) + self.synapse_history.append(Synapse.from_headers(synapse.to_headers())) # Return the updated synapse object after deserializing if requested if deserialize: diff --git a/bittensor/core/errors.py b/bittensor/core/errors.py index 5b5c3d5cf..6fd9729e8 100644 --- a/bittensor/core/errors.py +++ b/bittensor/core/errors.py @@ -81,9 +81,7 @@ class InvalidRequestNameError(Exception): class SynapseException(Exception): - def __init__( - self, message="Synapse Exception", synapse: "Synapse" | None = None - ): + def __init__(self, message="Synapse Exception", synapse: "Synapse" | None = None): self.message = message self.synapse = synapse super().__init__(self.message) diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 659b994df..a550a2bd3 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -17,6 +17,7 @@ import os import pickle +import typing from abc import ABC, abstractmethod from os import listdir from os.path import join @@ -25,15 +26,20 @@ import numpy as np from numpy.typing import NDArray -from . import settings -from .chain_data import AxonInfo from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch from bittensor.utils.weight_utils import ( convert_weight_uids_and_vals_to_tensor, convert_bond_uids_and_vals_to_tensor, - convert_root_weight_uids_and_vals_to_tensor + convert_root_weight_uids_and_vals_to_tensor, ) +from . import settings +from .chain_data import AxonInfo + +# For annotation purposes +if typing.TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + METAGRAPH_STATE_DICT_NDARRAY_KEYS = [ "version", @@ -371,7 +377,9 @@ def addresses(self) -> List[str]: return [axon.ip_str() for axon in self.axons] @abstractmethod - def __init__(self, netuid: int, network: str = "finney", lite: bool = True, sync: bool = True): + def __init__( + self, netuid: int, network: str = "finney", lite: bool = True, sync: bool = True + ): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. This method is the entry point for creating a metagraph object, @@ -514,7 +522,10 @@ def sync( # Initialize subtensor subtensor = self._initialize_subtensor(subtensor) - if subtensor.chain_endpoint != settings.archive_entrypoint or subtensor.network != settings.networks[3]: + if ( + subtensor.chain_endpoint != settings.archive_entrypoint + or subtensor.network != settings.networks[3] + ): cur_block = subtensor.get_current_block() if block and block < (cur_block - 300): logging.warning( @@ -555,6 +566,7 @@ def _initialize_subtensor(self, subtensor): # TODO: Check and test the initialization of the new subtensor # Lazy import due to circular import (subtensor -> metagraph, metagraph -> subtensor) from bittensor.core.subtensor import Subtensor + subtensor = Subtensor(network=self.network) return subtensor @@ -622,7 +634,7 @@ def _set_weights_and_bonds(self, subtensor: "Optional[Subtensor]" = None): self.weights = self._process_root_weights( [neuron.weights for neuron in self.neurons], "weights", - subtensor, # type: ignore + subtensor, ) else: self.weights = self._process_weights_or_bonds( @@ -654,9 +666,9 @@ def _process_weights_or_bonds( for item in data: if len(item) == 0: if use_torch(): - data_array.append(torch.zeros(len(self.neurons))) # type: ignore + data_array.append(torch.zeros(len(self.neurons))) else: - data_array.append(np.zeros(len(self.neurons), dtype=np.float32)) # type: ignore + data_array.append(np.zeros(len(self.neurons), dtype=np.float32)) else: uids, values = zip(*item) # TODO: Validate and test the conversion of uids and values to tensor @@ -665,12 +677,12 @@ def _process_weights_or_bonds( convert_weight_uids_and_vals_to_tensor( len(self.neurons), list(uids), - list(values), # type: ignore + list(values), ) ) else: data_array.append( - convert_bond_uids_and_vals_to_tensor( # type: ignore + convert_bond_uids_and_vals_to_tensor( len(self.neurons), list(uids), list(values) ).astype(np.float32) ) @@ -724,12 +736,12 @@ def _process_root_weights( if use_torch(): data_array.append(torch.zeros(n_subnets)) else: - data_array.append(np.zeros(n_subnets, dtype=np.float32)) # type: ignore + data_array.append(np.zeros(n_subnets, dtype=np.float32)) else: uids, values = zip(*item) # TODO: Validate and test the conversion of uids and values to tensor data_array.append( - convert_root_weight_uids_and_vals_to_tensor( # type: ignore + convert_root_weight_uids_and_vals_to_tensor( n_subnets, list(uids), list(values), subnets ) ) @@ -753,7 +765,7 @@ def _process_root_weights( ) return tensor_param - def save(self) -> "metagraph": # type: ignore + def save(self) -> "Metagraph": """ Saves the current state of the metagraph to a file on disk. This function is crucial for persisting the current state of the network's metagraph, which can later be reloaded or analyzed. The save operation includes all neuron attributes and parameters, ensuring a complete snapshot of the metagraph's state. @@ -782,9 +794,7 @@ def save(self) -> "metagraph": # type: ignore state_dict = self.state_dict() state_dict["axons"] = self.axons torch.save(state_dict, graph_filename) - torch.load( - graph_filename - ) # verifies that the file can be loaded correctly + torch.load(graph_filename) # verifies that the file can be loaded correctly else: graph_filename = f"{save_directory}/block-{self.block.item()}.pt" state_dict = self.state_dict() @@ -818,7 +828,7 @@ def load(self): self.load_from_path(get_save_dir(self.network, self.netuid)) @abstractmethod - def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore + def load_from_path(self, dir_path: str) -> "Metagraph": """ Loads the state of the metagraph from a specified directory path. This method is crucial for restoring the metagraph to a specific state based on saved data. It locates the latest block file in the given directory and loads all metagraph parameters from it. This is particularly useful for analyses that require historical states of the network or for restoring previous states of the metagraph in different @@ -851,7 +861,7 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore BaseClass: Union["torch.nn.Module", object] = torch.nn.Module if use_torch() else object -class TorchMetaGraph(MetagraphMixin, BaseClass): # type: ignore +class TorchMetaGraph(MetagraphMixin, BaseClass): def __init__( self, netuid: int, network: str = "finney", lite: bool = True, sync: bool = True ): @@ -992,7 +1002,7 @@ def _set_metagraph_attributes(self, block, subtensor): ) self.axons = [n.axon_info for n in self.neurons] - def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore + def load_from_path(self, dir_path: str) -> "Metagraph": graph_file = latest_block_path(dir_path) state_dict = torch.load(graph_file) self.n = torch.nn.Parameter(state_dict["n"], requires_grad=False) @@ -1083,9 +1093,7 @@ def _set_metagraph_attributes(self, block, subtensor): """ # TODO: Check and test the setting of each attribute self.n = self._create_tensor(len(self.neurons), dtype=np.int64) - self.version = self._create_tensor( - [settings.version_as_int], dtype=np.int64 - ) + self.version = self._create_tensor([settings.version_as_int], dtype=np.int64) self.block = self._create_tensor( block if block else subtensor.block, dtype=np.int64 ) @@ -1130,7 +1138,7 @@ def _set_metagraph_attributes(self, block, subtensor): ) self.axons = [n.axon_info for n in self.neurons] - def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore + def load_from_path(self, dir_path: str) -> "Metagraph": graph_filename = latest_block_path(dir_path) try: with open(graph_filename, "rb") as graph_file: diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 9c26d3330..75781d500 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -111,7 +111,7 @@ def turn_console_on(): } # --- Type Registry --- -type_registry = { +type_registry: dict = { "types": { "Balance": "u64", # Need to override default u128 }, @@ -227,35 +227,37 @@ def turn_console_on(): }, } -defaults = Munch = munchify({ - "axon": { - "port": os.getenv("BT_AXON_PORT") or 8091, - "ip": os.getenv("BT_AXON_IP") or "[::]", - "external_port": os.getenv("BT_AXON_EXTERNAL_PORT") or None, - "external_ip": os.getenv("BT_AXON_EXTERNAL_IP") or None, - "max_workers": os.getenv("BT_AXON_MAX_WORKERS") or 10, - }, - "logging": { - "debug": os.getenv("BT_LOGGING_DEBUG") or False, - "trace": os.getenv("BT_LOGGING_TRACE") or False, - "record_log": os.getenv("BT_LOGGING_RECORD_LOG") or False, - "logging_dir": os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR), - }, - "priority": { - "max_workers": os.getenv("BT_PRIORITY_MAX_WORKERS") or 5, - "maxsize": os.getenv("BT_PRIORITY_MAXSIZE") or 10 - }, - "subtensor": { - "chain_endpoint": DEFAULT_ENDPOINT, - "network": DEFAULT_NETWORK, - "_mock": False - }, - "wallet": { - "name": "default", - "hotkey": "default", - "path": str(WALLETS_DIR), - }, -}) +defaults = Munch = munchify( + { + "axon": { + "port": os.getenv("BT_AXON_PORT") or 8091, + "ip": os.getenv("BT_AXON_IP") or "[::]", + "external_port": os.getenv("BT_AXON_EXTERNAL_PORT") or None, + "external_ip": os.getenv("BT_AXON_EXTERNAL_IP") or None, + "max_workers": os.getenv("BT_AXON_MAX_WORKERS") or 10, + }, + "logging": { + "debug": os.getenv("BT_LOGGING_DEBUG") or False, + "trace": os.getenv("BT_LOGGING_TRACE") or False, + "record_log": os.getenv("BT_LOGGING_RECORD_LOG") or False, + "logging_dir": os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR), + }, + "priority": { + "max_workers": os.getenv("BT_PRIORITY_MAX_WORKERS") or 5, + "maxsize": os.getenv("BT_PRIORITY_MAXSIZE") or 10, + }, + "subtensor": { + "chain_endpoint": DEFAULT_ENDPOINT, + "network": DEFAULT_NETWORK, + "_mock": False, + }, + "wallet": { + "name": "default", + "hotkey": "default", + "path": str(WALLETS_DIR), + }, + } +) # Parsing version without any literals. @@ -266,5 +268,7 @@ def turn_console_on(): _version_int_base = 1000 assert max(_version_info) < _version_int_base -version_as_int: int = sum(e * (_version_int_base**i) for i, e in enumerate(reversed(_version_info))) -assert version_as_int < 2 ** 31 # fits in int32 +version_as_int: int = sum( + e * (_version_int_base**i) for i, e in enumerate(reversed(_version_info)) +) +assert version_as_int < 2**31 # fits in int32 diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 7d4ff1582..5f914d68f 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -61,7 +61,10 @@ run_faucet_extrinsic, swap_hotkey_extrinsic, ) -from bittensor.api.extrinsics.root import root_register_extrinsic, set_root_weights_extrinsic +from bittensor.api.extrinsics.root import ( + root_register_extrinsic, + set_root_weights_extrinsic, +) from bittensor.api.extrinsics.senate import ( register_senate_extrinsic, leave_senate_extrinsic, @@ -74,9 +77,15 @@ get_metadata, ) from bittensor.api.extrinsics.set_weights import set_weights_extrinsic -from bittensor.api.extrinsics.staking import add_stake_extrinsic, add_stake_multiple_extrinsic +from bittensor.api.extrinsics.staking import ( + add_stake_extrinsic, + add_stake_multiple_extrinsic, +) from bittensor.api.extrinsics.transfer import transfer_extrinsic -from bittensor.api.extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic +from bittensor.api.extrinsics.unstaking import ( + unstake_extrinsic, + unstake_multiple_extrinsic, +) from bittensor.core import settings from bittensor.core.axon import Axon from bittensor.core.chain_data import ( @@ -103,7 +112,13 @@ U64_NORMALIZED_FLOAT, networking, ) -from bittensor.utils import torch, weight_utils, format_error_message, create_identity_dict, decode_hex_identity_dict +from bittensor.utils import ( + torch, + weight_utils, + format_error_message, + create_identity_dict, + decode_hex_identity_dict, +) from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging from bittensor.utils.registration import POWSolution @@ -2778,9 +2793,7 @@ def make_substrate_call_with_retry() -> "ScaleType": identity_info = make_substrate_call_with_retry() - return decode_hex_identity_dict( - identity_info.value["info"] - ) + return decode_hex_identity_dict(identity_info.value["info"]) def update_identity( self, @@ -3125,7 +3138,9 @@ def query_runtime_api( This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. """ - call_definition = settings.type_registry["runtime_api"][runtime_api]["methods"][method] + call_definition = settings.type_registry["runtime_api"][runtime_api]["methods"][ + method + ] json_result = self.state_call( method=f"{runtime_api}_{method}", diff --git a/bittensor/core/synapse.py b/bittensor/core/synapse.py index e252edf3b..9cfd292aa 100644 --- a/bittensor/core/synapse.py +++ b/bittensor/core/synapse.py @@ -19,7 +19,7 @@ import json import sys import warnings -from typing import Any, ClassVar, Dict, Optional, Tuple, Union +from typing import cast, Any, ClassVar, Dict, Optional, Tuple, Union from pydantic import ( BaseModel, @@ -756,7 +756,10 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: """ # Initialize the input dictionary with empty sub-dictionaries for 'axon' and 'dendrite' - inputs_dict: Dict[str, Union[Dict, Optional[str]]] = {"axon": {}, "dendrite": {}} + inputs_dict: Dict[str, Union[Dict, Optional[str]]] = { + "axon": {}, + "dendrite": {}, + } # Iterate over each item in the headers for key, value in headers.items(): @@ -764,21 +767,19 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: if "bt_header_axon_" in key: try: new_key = key.split("bt_header_axon_")[1] - inputs_dict["axon"][new_key] = value + axon_dict = cast(dict, inputs_dict["axon"]) + axon_dict[new_key] = value except Exception as e: - logging.error( - f"Error while parsing 'axon' header {key}: {e}" - ) + logging.error(f"Error while parsing 'axon' header {key}: {str(e)}") continue # Handle 'dendrite' headers elif "bt_header_dendrite_" in key: try: new_key = key.split("bt_header_dendrite_")[1] - inputs_dict["dendrite"][new_key] = value + dendrite_dict = cast(dict, inputs_dict["dendrite"]) + dendrite_dict[new_key] = value except Exception as e: - logging.error( - f"Error while parsing 'dendrite' header {key}: {e}" - ) + logging.error(f"Error while parsing 'dendrite' header {key}: {e}") continue # Handle 'input_obj' headers elif "bt_header_input_obj" in key: @@ -797,9 +798,7 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: ) continue except Exception as e: - logging.error( - f"Error while parsing 'input_obj' header {key}: {e}" - ) + logging.error(f"Error while parsing 'input_obj' header {key}: {e}") continue else: pass # TODO: log unexpected keys diff --git a/bittensor/core/threadpool.py b/bittensor/core/threadpool.py index 7182c660f..0b10a622a 100644 --- a/bittensor/core/threadpool.py +++ b/bittensor/core/threadpool.py @@ -55,7 +55,7 @@ def run(self): """Run the given work item""" # Checks if future is canceled or if work item is stale if (not self.future.set_running_or_notify_cancel()) or ( - time.time() - self.start_time > blocktime + time.time() - self.start_time > blocktime ): return @@ -127,12 +127,12 @@ class PriorityThreadPoolExecutor(_base.Executor): _counter = itertools.count().__next__ def __init__( - self, - maxsize=-1, - max_workers=None, - thread_name_prefix="", - initializer=None, - initargs=(), + self, + maxsize=-1, + max_workers=None, + thread_name_prefix="", + initializer=None, + initargs=(), ): """Initializes a new `ThreadPoolExecutor `_ instance. @@ -161,7 +161,7 @@ def __init__( self._shutdown = False self._shutdown_lock = threading.Lock() self._thread_name_prefix = thread_name_prefix or ( - "ThreadPoolExecutor-%d" % self._counter() + "ThreadPoolExecutor-%d" % self._counter() ) self._initializer = initializer self._initargs = initargs diff --git a/bittensor/core/types.py b/bittensor/core/types.py index 5fff1140c..9fd2b4d05 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -30,6 +30,7 @@ class AxonServeCallParams(TypedDict): class PrometheusServeCallParams(TypedDict): """Prometheus serve chain call parameters.""" + version: int ip: int port: int diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 3f67321fa..80f478fe9 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -217,9 +217,7 @@ def get_explorer_url_for_network( def ss58_address_to_bytes(ss58_address: str) -> bytes: """Converts a ss58 address to a bytes object.""" - account_id_hex: str = scalecodec.ss58_decode( - ss58_address, ss58_format - ) + account_id_hex: str = scalecodec.ss58_decode(ss58_address, ss58_format) return bytes.fromhex(account_id_hex) diff --git a/bittensor/utils/backwards_compatibility.py b/bittensor/utils/backwards_compatibility.py index 274ccb8d5..ca45a4599 100644 --- a/bittensor/utils/backwards_compatibility.py +++ b/bittensor/utils/backwards_compatibility.py @@ -20,6 +20,7 @@ the Bittensor framework, specifically up to and including version 7.3.0. This module addresses changes and deprecated features in recent versions, allowing users to maintain compatibility with older systems and projects. """ + import sys import importlib @@ -92,12 +93,14 @@ ) 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 # noqa: F401 from bittensor.core.subtensor import Subtensor -from bittensor.core.synapse import TerminalInfo, Synapse # noqa: F401 -from bittensor.core.tensor import tensor, Tensor # noqa: F401 -from bittensor.core.threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor # noqa: F401 -from bittensor.utils.mock.subtensor_mock import MockSubtensor as MockSubtensor # noqa: F401 +from bittensor.core.synapse import TerminalInfo, Synapse # noqa: F401 +from bittensor.core.tensor import tensor, Tensor # noqa: F401 +from bittensor.core.threadpool import ( # noqa: F401 + PriorityThreadPoolExecutor as PriorityThreadPoolExecutor, +) +from bittensor.utils.mock.subtensor_mock import MockSubtensor as MockSubtensor # noqa: F401 from bittensor.utils import ( # noqa: F401 ss58_to_vec_u8, unbiased_topk, @@ -111,9 +114,9 @@ U16_NORMALIZED_FLOAT, U64_NORMALIZED_FLOAT, u8_key_to_ss58, - get_hash + get_hash, ) -from bittensor.utils.balance import Balance as Balance # noqa: F401 +from bittensor.utils.balance import Balance as Balance # noqa: F401 from bittensor.utils.subnets import SubnetsAPI # noqa: F401 @@ -144,9 +147,9 @@ __rao_symbol__ = settings.rao_symbol # Makes the `bittensor.api.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. -extrinsics = importlib.import_module('bittensor.api.extrinsics') -sys.modules['bittensor.extrinsics'] = extrinsics +extrinsics = importlib.import_module("bittensor.api.extrinsics") +sys.modules["bittensor.extrinsics"] = extrinsics # Makes the `bittensor.utils.mock` subpackage available as `bittensor.mock` for backwards compatibility. -extrinsics = importlib.import_module('bittensor.utils.mock') -sys.modules['bittensor.mock'] = extrinsics +extrinsics = importlib.import_module("bittensor.utils.mock") +sys.modules["bittensor.mock"] = extrinsics diff --git a/bittensor/utils/registration.py b/bittensor/utils/registration.py index 6ba28b5a9..83406ff70 100644 --- a/bittensor/utils/registration.py +++ b/bittensor/utils/registration.py @@ -124,7 +124,7 @@ class CUDAException(Exception): def _hex_bytes_to_u8_list(hex_bytes: bytes): - hex_chunks = [int(hex_bytes[i: i + 2], 16) for i in range(0, len(hex_bytes), 2)] + hex_chunks = [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)] return hex_chunks @@ -202,6 +202,7 @@ class _SolverBase(multiprocessing.Process): limit: int The limit of the pow solve for a valid solution. """ + proc_num: int num_proc: int update_interval: int @@ -479,6 +480,7 @@ def get_cpu_count() -> int: @dataclass class RegistrationStatistics: """Statistics for a registration.""" + time_spent_total: float rounds_total: int time_average: float diff --git a/bittensor/utils/subnets.py b/bittensor/utils/subnets.py index e4dae0b23..e550f4c77 100644 --- a/bittensor/utils/subnets.py +++ b/bittensor/utils/subnets.py @@ -28,6 +28,7 @@ class SubnetsAPI(ABC): """This class is not used within the bittensor package, but is actively used by the community.""" + def __init__(self, wallet: "Wallet"): self.wallet = wallet self.dendrite = Dendrite(wallet=wallet) diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index 44d8e1198..f6dada5ae 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -56,9 +56,7 @@ def _get_version_from_file(version_file: Path) -> Optional[str]: def _get_version_from_pypi(timeout: int = 15) -> str: - logging.debug( - f"Checking latest Bittensor version at: {pipaddress}" - ) + logging.debug(f"Checking latest Bittensor version at: {pipaddress}") try: response = requests.get(pipaddress, timeout=timeout) latest_version = response.json()["info"]["version"] diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 0b9aefc49..36ef6f1c8 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -230,7 +230,10 @@ def process_weights_for_netuid( subtensor: "Subtensor", metagraph: "Metagraph" = None, exclude_quantile: int = 0, -) -> Union[Tuple["torch.Tensor", "torch.FloatTensor"], Tuple[NDArray[np.int64], NDArray[np.float32]]]: +) -> Union[ + Tuple["torch.Tensor", "torch.FloatTensor"], + Tuple[NDArray[np.int64], NDArray[np.float32]], +]: logging.debug("process_weights_for_netuid()") logging.debug("weights", weights) logging.debug("netuid", netuid) diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index 8d05baa40..c2ff458f2 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -54,10 +54,8 @@ def __eq__(self, __o: Union[float, int, Balance]) -> bool: # True if __o \in [value - tolerance, value + tolerance] # or if value \in [__o - tolerance, __o + tolerance] return ( - (self.value - self.tolerance) <= __o <= (self.value + self.tolerance) - ) or ( - (__o - self.tolerance) <= self.value <= (__o + self.tolerance) - ) + (self.value - self.tolerance) <= __o <= (self.value + self.tolerance) + ) or ((__o - self.tolerance) <= self.value <= (__o + self.tolerance)) def get_mock_neuron(**kwargs) -> NeuronInfo: diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 5c2da3e70..3152d74d5 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -55,7 +55,9 @@ def setUp(self): def setUpClass(cls) -> None: # mock rich console status mock_console = MockConsole() - cls._mock_console_patcher = patch("bittensor.core.settings.bt_console", mock_console) + cls._mock_console_patcher = patch( + "bittensor.core.settings.bt_console", mock_console + ) cls._mock_console_patcher.start() # Keeps the same mock network for all tests. This stops the network from being re-setup for each test. @@ -710,7 +712,9 @@ def test_registration_multiprocessed_already_registered(self): ) self.subtensor._do_pow_register = MagicMock(return_value=(True, None)) - with patch("bittensor.core.settings.bt_console.status") as mock_set_status: + with patch( + "bittensor.core.settings.bt_console.status" + ) as mock_set_status: # Need to patch the console status to avoid opening a parallel live display mock_set_status.__enter__ = MagicMock(return_value=True) mock_set_status.__exit__ = MagicMock(return_value=True) diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py index b3927ea70..7e1137b7b 100644 --- a/tests/unit_tests/extrinsics/test_root.py +++ b/tests/unit_tests/extrinsics/test_root.py @@ -295,7 +295,7 @@ def test_set_root_weights_extrinsic_torch( prompt, user_response, expected_success, - force_legacy_torch_compatible_api, + force_legacy_torch_compatible_api, ): test_set_root_weights_extrinsic( mock_subtensor, diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py index 1b9da07e3..c91fda171 100644 --- a/tests/unit_tests/extrinsics/test_staking.py +++ b/tests/unit_tests/extrinsics/test_staking.py @@ -127,9 +127,7 @@ def test_add_stake_extrinsic( staking_balance = amount if amount else Balance.from_tao(100) else: staking_balance = ( - Balance.from_tao(amount) - if not isinstance(amount, Balance) - else amount + Balance.from_tao(amount) if not isinstance(amount, Balance) else amount ) with patch.object( diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py index bc96b0256..107757fd0 100644 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ b/tests/unit_tests/extrinsics/test_unstaking.py @@ -3,7 +3,10 @@ import pytest from bittensor_wallet import Wallet -from bittensor.api.extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic +from bittensor.api.extrinsics.unstaking import ( + unstake_extrinsic, + unstake_multiple_extrinsic, +) from bittensor.core.subtensor import Subtensor from bittensor.utils.balance import Balance @@ -106,9 +109,7 @@ def test_unstake_extrinsic( mock_subtensor._do_unstake.assert_called_once_with( wallet=mock_wallet, hotkey_ss58=hotkey_ss58 or mock_wallet.hotkey.ss58_address, - amount=Balance.from_tao(amount) - if amount - else mock_current_stake, + amount=Balance.from_tao(amount) if amount else mock_current_stake, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index 314c896b8..8d450c4c6 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -33,7 +33,12 @@ from bittensor.core.settings import version_as_int from bittensor.core.synapse import Synapse from bittensor.core.threadpool import PriorityThreadPoolExecutor -from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds, ALLOWED_DELTA, NANOSECONDS_IN_SECOND +from bittensor.utils.axon_utils import ( + allowed_nonce_window_ns, + calculate_diff_seconds, + ALLOWED_DELTA, + NANOSECONDS_IN_SECOND, +) def test_attach_initial(): diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index 9256a5213..e2d761645 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -240,7 +240,7 @@ def test_to_parameter_dict(axon_info, test_case): def test_to_parameter_dict_torch( axon_info, test_case, - force_legacy_torch_compatible_api, + force_legacy_torch_compatible_api, ): result = axon_info.to_parameter_dict() @@ -533,7 +533,8 @@ def mock_from_scale_encoding(mocker): @pytest.fixture def mock_fix_decoded_values(mocker): return mocker.patch( - "bittensor.core.chain_data.DelegateInfo.fix_decoded_values", side_effect=lambda x: x + "bittensor.core.chain_data.DelegateInfo.fix_decoded_values", + side_effect=lambda x: x, ) diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index 4aeb08664..e7ccc691f 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -25,7 +25,11 @@ import pytest from bittensor.core.axon import Axon -from bittensor.core.dendrite import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR, Dendrite +from bittensor.core.dendrite import ( + DENDRITE_ERROR_MAPPING, + DENDRITE_DEFAULT_ERROR, + Dendrite, +) from bittensor.core.synapse import TerminalInfo from tests.helpers import _get_mock_wallet from bittensor.core.synapse import Synapse diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 1ac772481..6c39d7b5f 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -3,7 +3,10 @@ import logging as stdlogging from unittest.mock import MagicMock, patch from bittensor.utils.btlogging import LoggingMachine -from bittensor.utils.btlogging.defines import DEFAULT_LOG_FILE_NAME, BITTENSOR_LOGGER_NAME +from bittensor.utils.btlogging.defines import ( + DEFAULT_LOG_FILE_NAME, + BITTENSOR_LOGGER_NAME, +) from bittensor.utils.btlogging.loggingmachine import LoggingConfig @@ -75,7 +78,9 @@ def test_state_transitions(logging_machine, mock_config): Test state transitions and the associated logging level changes. """ config, log_file_path = mock_config - with patch("bittensor.utils.btlogging.loggingmachine.all_loggers") as mocked_all_loggers: + with patch( + "bittensor.utils.btlogging.loggingmachine.all_loggers" + ) as mocked_all_loggers: # mock the main bittensor logger, identified by its `name` field mocked_bt_logger = MagicMock() mocked_bt_logger.name = BITTENSOR_LOGGER_NAME diff --git a/tests/unit_tests/utils/test_weight_utils.py b/tests/unit_tests/utils/test_weight_utils.py index 77f7fad0f..fccf2276b 100644 --- a/tests/unit_tests/utils/test_weight_utils.py +++ b/tests/unit_tests/utils/test_weight_utils.py @@ -144,7 +144,7 @@ def test_normalize_with_max_weight(): def test_normalize_with_max_weight__legacy_torch_api_compat( - force_legacy_torch_compatible_api, + force_legacy_torch_compatible_api, ): weights = torch.rand(1000) wn = weight_utils.normalize_max_weight(weights, limit=0.01) From 79a4393d893f950f64f49d46a3c41487f367eff6 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 6 Aug 2024 11:50:28 -0700 Subject: [PATCH 065/260] Removing obsolete, unused (community and bittensor) code --- bittensor/__init__.py | 2 +- bittensor/api/extrinsics/commit_weights.py | 132 - bittensor/api/extrinsics/delegation.py | 479 -- bittensor/api/extrinsics/network.py | 241 - bittensor/api/extrinsics/prometheus.py | 5 +- bittensor/api/extrinsics/registration.py | 503 -- bittensor/api/extrinsics/root.py | 209 - bittensor/api/extrinsics/senate.py | 263 - bittensor/api/extrinsics/serving.py | 16 +- bittensor/api/extrinsics/set_weights.py | 7 +- bittensor/api/extrinsics/staking.py | 492 -- bittensor/api/extrinsics/transfer.py | 9 +- bittensor/api/extrinsics/unstaking.py | 426 -- bittensor/api/extrinsics/utils.py | 41 - bittensor/core/axon.py | 24 +- bittensor/core/chain_data.py | 64 +- bittensor/core/config.py | 43 +- bittensor/core/dendrite.py | 77 +- bittensor/core/metagraph.py | 6 +- bittensor/core/settings.py | 103 +- bittensor/core/subtensor.py | 5802 +++-------------- bittensor/core/synapse.py | 4 +- bittensor/core/tensor.py | 4 +- bittensor/core/threadpool.py | 4 +- bittensor/utils/__init__.py | 60 +- bittensor/utils/_register_cuda.py | 126 - .../__init__.py} | 39 +- .../{ => backwards_compatibility}/subnets.py | 20 +- bittensor/utils/balance.py | 34 +- bittensor/utils/formatting.py | 36 - bittensor/utils/mock/subtensor_mock.py | 582 +- bittensor/utils/networking.py | 20 +- bittensor/utils/registration.py | 1044 +-- bittensor/utils/version.py | 32 +- bittensor/utils/weight_utils.py | 78 +- tests/helpers/__init__.py | 12 +- tests/helpers/helpers.py | 11 +- .../test_metagraph_integration.py | 5 +- .../test_subtensor_integration.py | 670 +- .../unit_tests/extrinsics/test_delegation.py | 478 -- tests/unit_tests/extrinsics/test_network.py | 176 - .../unit_tests/extrinsics/test_prometheus.py | 6 +- .../extrinsics/test_registration.py | 420 -- tests/unit_tests/extrinsics/test_root.py | 310 - tests/unit_tests/extrinsics/test_senate.py | 238 - tests/unit_tests/extrinsics/test_serving.py | 11 +- .../unit_tests/extrinsics/test_set_weights.py | 2 +- tests/unit_tests/extrinsics/test_staking.py | 567 -- tests/unit_tests/extrinsics/test_unstaking.py | 334 - tests/unit_tests/test_axon.py | 1 - tests/unit_tests/test_dendrite.py | 18 +- tests/unit_tests/test_metagraph.py | 2 +- tests/unit_tests/test_subtensor.py | 1795 +---- 53 files changed, 1553 insertions(+), 14530 deletions(-) delete mode 100644 bittensor/api/extrinsics/commit_weights.py delete mode 100644 bittensor/api/extrinsics/delegation.py delete mode 100644 bittensor/api/extrinsics/network.py delete mode 100644 bittensor/api/extrinsics/registration.py delete mode 100644 bittensor/api/extrinsics/root.py delete mode 100644 bittensor/api/extrinsics/senate.py delete mode 100644 bittensor/api/extrinsics/staking.py delete mode 100644 bittensor/api/extrinsics/unstaking.py delete mode 100644 bittensor/api/extrinsics/utils.py delete mode 100644 bittensor/utils/_register_cuda.py rename bittensor/utils/{backwards_compatibility.py => backwards_compatibility/__init__.py} (86%) rename bittensor/utils/{ => backwards_compatibility}/subnets.py (90%) delete mode 100644 bittensor/utils/formatting.py delete mode 100644 tests/unit_tests/extrinsics/test_delegation.py delete mode 100644 tests/unit_tests/extrinsics/test_network.py delete mode 100644 tests/unit_tests/extrinsics/test_registration.py delete mode 100644 tests/unit_tests/extrinsics/test_root.py delete mode 100644 tests/unit_tests/extrinsics/test_senate.py delete mode 100644 tests/unit_tests/extrinsics/test_staking.py delete mode 100644 tests/unit_tests/extrinsics/test_unstaking.py diff --git a/bittensor/__init__.py b/bittensor/__init__.py index ceb797943..932bc996f 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -18,7 +18,7 @@ import os import warnings -from .core.settings import __version__, version_split, defaults +from .core.settings import __version__, version_split, DEFAULTS from .utils.backwards_compatibility import * from .utils.btlogging import logging diff --git a/bittensor/api/extrinsics/commit_weights.py b/bittensor/api/extrinsics/commit_weights.py deleted file mode 100644 index e9f545d59..000000000 --- a/bittensor/api/extrinsics/commit_weights.py +++ /dev/null @@ -1,132 +0,0 @@ -# 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. - -"""Module commit weights and reveal weights extrinsic.""" - -from typing import Tuple, List, TYPE_CHECKING - -from bittensor_wallet import Wallet -from rich.prompt import Confirm - -from bittensor.utils import format_error_message -from bittensor.utils.btlogging import logging - -# For annotation purposes -if TYPE_CHECKING: - from bittensor.core.subtensor import Subtensor - - -def commit_weights_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - commit_hash: str, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> Tuple[bool, str]: - """ - Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. - This function is a wrapper around the `_do_commit_weights` method, handling user prompts and error messages. - - Args: - subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor.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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. - """ - if prompt and not Confirm.ask(f"Would you like to commit weights?"): - return False, "User cancelled the operation." - - success, error_message = subtensor._do_commit_weights( - wallet=wallet, - netuid=netuid, - commit_hash=commit_hash, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success: - logging.info("Successfully committed weights.") - return True, "Successfully committed weights." - else: - logging.error(f"Failed to commit weights: {error_message}") - return False, format_error_message(error_message) - - -def reveal_weights_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - uids: List[int], - weights: List[int], - salt: List[int], - version_key: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> 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, handling user prompts and error messages. - - Args: - subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor.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. - weights (List[int]): List of weight values corresponding to each UID. - salt (List[int]): List of salt values corresponding to the hash function. - version_key (int): 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, 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. - """ - - if prompt and not Confirm.ask(f"Would you like to reveal weights?"): - return False, "User cancelled the operation." - - success, error_message = subtensor._do_reveal_weights( - wallet=wallet, - netuid=netuid, - uids=uids, - values=weights, - salt=salt, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success: - logging.info("Successfully revealed weights.") - return True, "Successfully revealed weights." - else: - logging.error(f"Failed to reveal weights: {error_message}") - return False, error_message diff --git a/bittensor/api/extrinsics/delegation.py b/bittensor/api/extrinsics/delegation.py deleted file mode 100644 index 8b0aeb361..000000000 --- a/bittensor/api/extrinsics/delegation.py +++ /dev/null @@ -1,479 +0,0 @@ -# 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. - -from typing import Union, Optional, TYPE_CHECKING - -from bittensor_wallet import Wallet -from rich.prompt import Confirm - -from bittensor.core.errors import ( - NominationError, - NotDelegateError, - NotRegisteredError, - StakeError, - TakeError, -) -from bittensor.core.settings import bt_console -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging - -# For annotation purposes -if TYPE_CHECKING: - from bittensor.core.subtensor import Subtensor - - -def nominate_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - wait_for_finalization: bool = False, - wait_for_inclusion: bool = True, -) -> bool: - """Becomes a delegate for the hotkey. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): The wallet to become a delegate for. - wait_for_inclusion: - wait_for_finalization: - - Returns: - success (bool): ``True`` if the transaction was successful. - """ - # Unlock the coldkey. - wallet.coldkey - wallet.hotkey - - # Check if the hotkey is already a delegate. - if subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - logging.error( - "Hotkey {} is already a delegate.".format(wallet.hotkey.ss58_address) - ) - return False - - if not subtensor.is_hotkey_registered_any(wallet.hotkey.ss58_address): - logging.error( - "Hotkey {} is not registered to any network".format( - wallet.hotkey.ss58_address - ) - ) - return False - - with bt_console.status( - ":satellite: Sending nominate call on [white]{}[/white] ...".format( - subtensor.network - ) - ): - try: - success = subtensor._do_nominate( - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success is True: - bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - logging.success( - prefix="Become Delegate", - suffix="Finalized: " + str(success), - ) - - # Raises NominationError if False - return success - - except Exception as e: - bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) - logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) - except NominationError as e: - bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) - logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) - - return False - - -def delegate_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - delegate_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """Delegates the specified amount of stake to the passed delegate. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - delegate_ss58 (Optional[str]): The ``ss58`` address of the delegate. - amount (Union[Balance, float]): Amount to stake as bittensor balance, or ``float`` interpreted as Tao. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - - Raises: - NotRegisteredError: If the wallet is not registered on the chain. - NotDelegateError: If the hotkey is not a delegate on the chain. - """ - # Decrypt keys, - wallet.coldkey - if not subtensor.is_hotkey_delegate(delegate_ss58): - raise NotDelegateError("Hotkey: {} is not a delegate.".format(delegate_ss58)) - - # Get state. - my_prev_coldkey_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - delegate_take = subtensor.get_delegate_take(delegate_ss58) - delegate_owner = subtensor.get_hotkey_owner(delegate_ss58) - my_prev_delegated_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=delegate_ss58 - ) - - # Convert to Balance - if amount is None: - # Stake it all. - staking_balance = Balance.from_tao(my_prev_coldkey_balance.tao) - elif not isinstance(amount, Balance): - staking_balance = Balance.from_tao(amount) - else: - staking_balance = amount - - # Remove existential balance to keep key alive. - if staking_balance > Balance.from_rao(1000): - staking_balance = staking_balance - Balance.from_rao(1000) - else: - staking_balance = staking_balance - - # Check enough balance to stake. - if staking_balance > my_prev_coldkey_balance: - bt_console.print( - ":cross_mark: [red]Not enough balance[/red]:[bold white]\n balance:{}\n amount: {}\n coldkey: {}[/bold white]".format( - my_prev_coldkey_balance, staking_balance, wallet.name - ) - ) - return False - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to delegate:[bold white]\n amount: {}\n to: {}\n owner: {}[/bold white]".format( - staking_balance, delegate_ss58, delegate_owner - ) - ): - return False - - try: - with bt_console.status( - ":satellite: Staking to: [bold white]{}[/bold white] ...".format( - subtensor.network - ) - ): - staking_response: bool = subtensor._do_delegation( - wallet=wallet, - delegate_ss58=delegate_ss58, - amount=staking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response 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 - - bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - with bt_console.status( - ":satellite: Checking Balance on: [white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance(address=wallet.coldkey.ss58_address) - block = subtensor.get_current_block() - new_delegate_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=delegate_ss58, - block=block, - ) # Get current stake - - bt_console.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - my_prev_coldkey_balance, new_balance - ) - ) - bt_console.print( - "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - my_prev_delegated_stake, new_delegate_stake - ) - ) - return True - else: - bt_console.print(":cross_mark: [red]Failed[/red]: Error unknown.") - return False - - except NotRegisteredError: - bt_console.print( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) - ) - return False - except StakeError as e: - bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - return False - - -def undelegate_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - delegate_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """Un-delegates stake from the passed delegate. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - delegate_ss58 (Optional[str]): The ``ss58`` address of the delegate. - amount (Union[Balance, float]): Amount to unstake as bittensor balance, or ``float`` interpreted as Tao. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - - Raises: - NotRegisteredError: If the wallet is not registered on the chain. - NotDelegateError: If the hotkey is not a delegate on the chain. - """ - # Decrypt keys, - wallet.coldkey - if not subtensor.is_hotkey_delegate(delegate_ss58): - raise NotDelegateError("Hotkey: {} is not a delegate.".format(delegate_ss58)) - - # Get state. - my_prev_coldkey_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - delegate_take = subtensor.get_delegate_take(delegate_ss58) - delegate_owner = subtensor.get_hotkey_owner(delegate_ss58) - my_prev_delegated_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=delegate_ss58 - ) - - # Convert to Balance - if amount is None: - # Stake it all. - unstaking_balance = Balance.from_tao(my_prev_delegated_stake.tao) - - elif not isinstance(amount, Balance): - unstaking_balance = Balance.from_tao(amount) - - else: - unstaking_balance = amount - - # Check enough stake to unstake. - if unstaking_balance > my_prev_delegated_stake: - bt_console.print( - ":cross_mark: [red]Not enough delegated stake[/red]:[bold white]\n stake:{}\n amount: {}\n coldkey: {}[/bold white]".format( - my_prev_delegated_stake, unstaking_balance, wallet.name - ) - ) - return False - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to un-delegate:[bold white]\n amount: {}\n from: {}\n owner: {}[/bold white]".format( - unstaking_balance, delegate_ss58, delegate_owner - ) - ): - return False - - try: - with bt_console.status( - ":satellite: Unstaking from: [bold white]{}[/bold white] ...".format( - subtensor.network - ) - ): - staking_response: bool = subtensor._do_undelegation( - wallet=wallet, - delegate_ss58=delegate_ss58, - amount=unstaking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response 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 - - bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - with bt_console.status( - ":satellite: Checking Balance on: [white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance(address=wallet.coldkey.ss58_address) - block = subtensor.get_current_block() - new_delegate_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=delegate_ss58, - block=block, - ) # Get current stake - - bt_console.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - my_prev_coldkey_balance, new_balance - ) - ) - bt_console.print( - "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - my_prev_delegated_stake, new_delegate_stake - ) - ) - return True - else: - bt_console.print(":cross_mark: [red]Failed[/red]: Error unknown.") - return False - - except NotRegisteredError: - bt_console.print( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) - ) - return False - except StakeError as e: - bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - return False - - -def decrease_take_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - hotkey_ss58: Optional[str] = None, - take: int = 0, - wait_for_finalization: bool = False, - wait_for_inclusion: bool = True, -) -> bool: - """Decrease delegate take for the hotkey. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. - take (float): The ``take`` of the hotkey. - wait_for_inclusion: - wait_for_finalization: - - Returns: - success (bool): ``True`` if the transaction was successful. - """ - # Unlock the coldkey. - wallet.coldkey - wallet.hotkey - - with bt_console.status( - ":satellite: Sending decrease_take_extrinsic call on [white]{}[/white] ...".format( - subtensor.network - ) - ): - try: - success = subtensor._do_decrease_take( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - take=take, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success is True: - bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - logging.success( - prefix="Decrease Delegate Take", - suffix="Finalized: " + str(success), - ) - - return success - - except (TakeError, Exception) as e: - bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) - logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) - - return False - - -def increase_take_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - hotkey_ss58: Optional[str] = None, - take: int = 0, - wait_for_finalization: bool = False, - wait_for_inclusion: bool = True, -) -> bool: - """Increase delegate take for the hotkey. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. - take (float): The ``take`` of the hotkey. - wait_for_inclusion: - wait_for_finalization: - - Returns: - success (bool): ``True`` if the transaction was successful. - """ - # Unlock the coldkey. - wallet.coldkey - wallet.hotkey - - with bt_console.status( - ":satellite: Sending increase_take_extrinsic call on [white]{}[/white] ...".format( - subtensor.network - ) - ): - try: - success = subtensor._do_increase_take( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - take=take, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success is True: - bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - logging.success( - prefix="Increase Delegate Take", - suffix="Finalized: " + str(success), - ) - - return success - - except Exception as e: - bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) - logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) - except TakeError as e: - bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) - logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) - - return False diff --git a/bittensor/api/extrinsics/network.py b/bittensor/api/extrinsics/network.py deleted file mode 100644 index 4703cb285..000000000 --- a/bittensor/api/extrinsics/network.py +++ /dev/null @@ -1,241 +0,0 @@ -# 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 time -from typing import TYPE_CHECKING - -import substrateinterface -from bittensor_wallet import Wallet -from rich.prompt import Confirm - -from bittensor.api.extrinsics.utils import HYPERPARAMS -from bittensor.core.settings import bt_console -from bittensor.utils import format_error_message -from bittensor.utils.balance import Balance - -# For annotation purposes -if TYPE_CHECKING: - from bittensor.core.subtensor import Subtensor - - -def _find_event_attributes_in_extrinsic_receipt( - response: "substrateinterface.base.ExtrinsicReceipt", event_name: str -) -> list: - """ - Searches for the attributes of a specified event within an extrinsic receipt. - - Args: - response (substrateinterface.base.ExtrinsicReceipt): The receipt of the extrinsic to be searched. - event_name (str): The name of the event to search for. - - Returns: - list: A list of attributes for the specified event. Returns [-1] if the event is not found. - """ - for event in response.triggered_events: - # Access the event details - event_details = event.value["event"] - # Check if the event_id is 'NetworkAdded' - if event_details["event_id"] == event_name: - # Once found, you can access the attributes of the event_name - return event_details["attributes"] - return [-1] - - -def register_subnetwork_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """Registers a new subnetwork. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (bittensor.wallet): bittensor wallet object. - 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. - prompt (bool): If true, the call waits for confirmation from the user before proceeding. - - 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``. - """ - your_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - burn_cost = Balance(subtensor.get_subnet_burn_cost()) - if burn_cost > your_balance: - bt_console.print( - f"Your balance of: [green]{your_balance}[/green] is not enough to pay the subnet lock cost of: [green]{burn_cost}[/green]" - ) - return False - - if prompt: - bt_console.print(f"Your balance is: [green]{your_balance}[/green]") - if not Confirm.ask( - f"Do you want to register a subnet for [green]{ burn_cost }[/green]?" - ): - return False - - wallet.coldkey # unlock coldkey - - with bt_console.status(":satellite: Registering subnet..."): - with subtensor.substrate as substrate: - # create extrinsic call - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="register_network", - call_params={"immunity_period": 0, "reg_allowed": True}, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if registration successful - response.process_events() - if not response.is_success: - bt_console.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" - ) - time.sleep(0.5) - - # Successful registration, final check for membership - else: - attributes = _find_event_attributes_in_extrinsic_receipt( - response, "NetworkAdded" - ) - bt_console.print( - f":white_heavy_check_mark: [green]Registered subnetwork with netuid: {attributes[0]}[/green]" - ) - return True - - -def set_hyperparameter_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - parameter: str, - value, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """Sets a hyperparameter for a specific subnetwork. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (bittensor.wallet): bittensor wallet object. - netuid (int): Subnetwork ``uid``. - parameter (str): Hyperparameter name. - value (any): New hyperparameter value. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - 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``. - """ - if subtensor.get_subnet_owner(netuid) != wallet.coldkeypub.ss58_address: - bt_console.print( - ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" - ) - return False - - wallet.coldkey # unlock coldkey - - extrinsic = HYPERPARAMS.get(parameter) - if extrinsic is None: - bt_console.print(":cross_mark: [red]Invalid hyperparameter specified.[/red]") - return False - - with bt_console.status( - f":satellite: Setting hyperparameter {parameter} to {value} on subnet: {netuid} ..." - ): - with subtensor.substrate as substrate: - extrinsic_params = substrate.get_metadata_call_function( - "AdminUtils", extrinsic - ) - call_params = {"netuid": netuid} - - # if input value is a list, iterate through the list and assign values - if isinstance(value, list): - # Create an iterator for the list of values - value_iterator = iter(value) - # Iterate over all value arguments and add them to the call_params dictionary - for value_argument in extrinsic_params["fields"]: - if "netuid" not in str(value_argument["name"]): - # Assign the next value from the iterator - try: - call_params[str(value_argument["name"])] = next( - value_iterator - ) - except StopIteration: - raise ValueError( - "Not enough values provided in the list for all parameters" - ) - - else: - value_argument = extrinsic_params["fields"][ - len(extrinsic_params["fields"]) - 1 - ] - call_params[str(value_argument["name"])] = value - - # create extrinsic call - call = substrate.compose_call( - call_module="AdminUtils", - call_function=extrinsic, - call_params=call_params, - ) - - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if registration successful - response.process_events() - if not response.is_success: - bt_console.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" - ) - time.sleep(0.5) - - # Successful registration, final check for membership - else: - bt_console.print( - f":white_heavy_check_mark: [green]Hyper parameter {parameter} changed to {value}[/green]" - ) - return True diff --git a/bittensor/api/extrinsics/prometheus.py b/bittensor/api/extrinsics/prometheus.py index 6bf58a882..2dd668bda 100644 --- a/bittensor/api/extrinsics/prometheus.py +++ b/bittensor/api/extrinsics/prometheus.py @@ -30,6 +30,7 @@ from bittensor.core.subtensor import Subtensor +# Community uses this extrinsic def prometheus_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -97,7 +98,7 @@ def prometheus_extrinsic( bt_console.print( f":white_heavy_check_mark: [green]Prometheus already Served[/green]\n" f"[green not bold]- Status: [/green not bold] |" - f"[green not bold] ip: [/green not bold][white not bold]{net.int_to_ip(neuron.prometheus_info.ip)}[/white not bold] |" + f"[green not bold] ip: [/green not bold][white not bold]{neuron.prometheus_info.ip}[/white not bold] |" f"[green not bold] ip_type: [/green not bold][white not bold]{neuron.prometheus_info.ip_type}[/white not bold] |" f"[green not bold] port: [/green not bold][white not bold]{neuron.prometheus_info.port}[/white not bold] | " f"[green not bold] version: [/green not bold][white not bold]{neuron.prometheus_info.version}[/white not bold] |" @@ -118,7 +119,7 @@ def prometheus_extrinsic( subtensor.network, netuid ) ): - success, err = subtensor._do_serve_prometheus( + success, err = subtensor.do_serve_prometheus( wallet=wallet, call_params=call_params, wait_for_finalization=wait_for_finalization, diff --git a/bittensor/api/extrinsics/registration.py b/bittensor/api/extrinsics/registration.py deleted file mode 100644 index d616eda2d..000000000 --- a/bittensor/api/extrinsics/registration.py +++ /dev/null @@ -1,503 +0,0 @@ -# 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 time -from typing import List, Union, Optional, Tuple, TYPE_CHECKING - -from bittensor_wallet import Wallet -from rich.prompt import Confirm - -from bittensor.core.settings import bt_console -from bittensor.utils import format_error_message -from bittensor.utils.btlogging import logging -from bittensor.utils.registration import ( - POWSolution, - create_pow, - torch, - log_no_torch_error, -) - -# For annotation purposes -if TYPE_CHECKING: - from bittensor.core.subtensor import Subtensor - - -def register_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[List[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, -) -> bool: - """Registers the wallet to the chain. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - netuid (int): The ``netuid`` of the subnet to register on. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - max_allowed_attempts (int): Maximum number of attempts to register the wallet. - output_in_place: - cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). - dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. - tpb (int): The number of threads per block (CUDA). - num_processes (int): The number of processes to use to register. - update_interval (int): The number of nonces to solve between updates. - log_verbose (bool): If ``true``, the registration process will log more information. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not subtensor.subnet_exists(netuid): - bt_console.print( - ":cross_mark: [red]Failed[/red]: error: [bold white]subnet:{}[/bold white] does not exist.".format( - netuid - ) - ) - return False - - with bt_console.status( - f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]..." - ): - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid - ) - if not neuron.is_null: - logging.debug( - f"Wallet {wallet} is already registered on {neuron.netuid} with {neuron.uid}" - ) - return True - - if prompt: - if not Confirm.ask( - "Continue Registration?\n hotkey: [bold white]{}[/bold white]\n coldkey: [bold white]{}[/bold white]\n network: [bold white]{}[/bold white]".format( - wallet.hotkey.ss58_address, - wallet.coldkeypub.ss58_address, - subtensor.network, - ) - ): - return False - - if not torch: - log_no_torch_error() - return False - - # Attempt rolling registration. - attempts = 1 - while True: - bt_console.print( - ":satellite: Registering...({}/{})".format(attempts, max_allowed_attempts) - ) - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - if prompt: - bt_console.print("CUDA is not available.") - return False - pow_result: Optional[POWSolution] = create_pow( - subtensor, - wallet, - netuid, - output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - pow_result: Optional[POWSolution] = create_pow( - subtensor, - wallet, - netuid, - output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # pow failed - if not pow_result: - # might be registered already on this subnet - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - bt_console.print( - f":white_heavy_check_mark: [green]Already registered on netuid:{netuid}[/green]" - ) - return True - - # pow successful, proceed to submit pow to chain for registration - else: - with bt_console.status(":satellite: Submitting POW..."): - # check if pow result is still valid - while not pow_result.is_stale(subtensor=subtensor): - result: Tuple[bool, Optional[str]] = subtensor._do_pow_register( - netuid=netuid, - wallet=wallet, - pow_result=pow_result, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - success, err_msg = result - - if not success: - # Look error here - # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs - if "HotKeyAlreadyRegisteredInSubNet" in err_msg: - bt_console.print( - f":white_heavy_check_mark: [green]Already Registered on [bold]subnet:{netuid}[/bold][/green]" - ) - return True - - bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - time.sleep(0.5) - - # Successful registration, final check for neuron and pubkey - else: - bt_console.print(":satellite: Checking Balance...") - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - bt_console.print( - ":white_heavy_check_mark: [green]Registered[/green]" - ) - return True - else: - # neuron not found, try again - bt_console.print( - ":cross_mark: [red]Unknown error. Neuron not found.[/red]" - ) - continue - else: - # Exited loop because pow is no longer valid. - bt_console.print("[red]POW is stale.[/red]") - # Try again. - continue - - if attempts < max_allowed_attempts: - # Failed registration, retry pow - attempts += 1 - bt_console.print( - ":satellite: Failed registration, retrying pow ...({}/{})".format( - attempts, max_allowed_attempts - ) - ) - else: - # Failed to register after max attempts. - bt_console.print("[red]No more attempts.[/red]") - return False - - -def burned_register_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """Registers the wallet to chain by recycling TAO. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - netuid (int): The ``netuid`` of the subnet to register on. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not subtensor.subnet_exists(netuid): - bt_console.print( - ":cross_mark: [red]Failed[/red]: error: [bold white]subnet:{}[/bold white] does not exist.".format( - netuid - ) - ) - return False - - wallet.coldkey # unlock coldkey - with bt_console.status( - f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]..." - ): - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid - ) - - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - recycle_amount = subtensor.recycle(netuid=netuid) - if not neuron.is_null: - bt_console.print( - ":white_heavy_check_mark: [green]Already Registered[/green]:\n" - "uid: [bold white]{}[/bold white]\n" - "netuid: [bold white]{}[/bold white]\n" - "hotkey: [bold white]{}[/bold white]\n" - "coldkey: [bold white]{}[/bold white]".format( - neuron.uid, neuron.netuid, neuron.hotkey, neuron.coldkey - ) - ) - return True - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask(f"Recycle {recycle_amount} to register on subnet:{netuid}?"): - return False - - with bt_console.status(":satellite: Recycling TAO for Registration..."): - success, err_msg = subtensor._do_burned_register( - netuid=netuid, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not success: - bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - time.sleep(0.5) - return False - # Successful registration, final check for neuron and pubkey - else: - bt_console.print(":satellite: Checking Balance...") - block = subtensor.get_current_block() - new_balance = subtensor.get_balance( - wallet.coldkeypub.ss58_address, block=block - ) - - bt_console.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_balance, new_balance - ) - ) - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - bt_console.print(":white_heavy_check_mark: [green]Registered[/green]") - return True - else: - # neuron not found, try again - bt_console.print( - ":cross_mark: [red]Unknown error. Neuron not found.[/red]" - ) - return False - - -class MaxSuccessException(Exception): - pass - - -class MaxAttemptsException(Exception): - pass - - -def run_faucet_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[List[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, -) -> Tuple[bool, str]: - """Runs a continual POW to get a faucet of TAO on the test net. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - 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. - max_allowed_attempts (int): Maximum number of attempts to register the wallet. - cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). - dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. - tpb (int): The number of threads per block (CUDA). - num_processes (int): The number of processes to use to register. - update_interval (int): The number of nonces to solve between updates. - log_verbose (bool): If ``true``, the registration process will log more information. - output_in_place: - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if prompt: - if not Confirm.ask( - "Run Faucet ?\n coldkey: [bold white]{}[/bold white]\n network: [bold white]{}[/bold white]".format( - wallet.coldkeypub.ss58_address, - subtensor.network, - ) - ): - return False, "" - - if not torch: - log_no_torch_error() - return False, "Requires torch" - - # Unlock coldkey - wallet.coldkey - - # Get previous balance. - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - # Attempt rolling registration. - attempts = 1 - successes = 1 - while True: - try: - pow_result = None - while pow_result is None or pow_result.is_stale(subtensor=subtensor): - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - if prompt: - bt_console.print("CUDA is not available.") - return False, "CUDA is not available." - pow_result: Optional[POWSolution] = create_pow( - subtensor, - wallet, - -1, - output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - pow_result: Optional[POWSolution] = create_pow( - subtensor, - wallet, - -1, - output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - call = subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="faucet", - call_params={ - "block_number": pow_result.block_number, - "nonce": pow_result.nonce, - "work": [int(byte_) for byte_ in pow_result.seal], - }, - ) - extrinsic = subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - bt_console.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" - ) - if attempts == max_allowed_attempts: - raise MaxAttemptsException - attempts += 1 - # Wait a bit before trying again - time.sleep(1) - - # Successful registration - else: - new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bt_console.print( - f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" - ) - old_balance = new_balance - - if successes == 3: - raise MaxSuccessException - - attempts = 1 # Reset attempts on success - successes += 1 - - except KeyboardInterrupt: - return True, "Done" - - except MaxSuccessException: - return True, f"Max successes reached: {3}" - - except MaxAttemptsException: - return False, f"Max attempts reached: {max_allowed_attempts}" - - -def swap_hotkey_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - new_wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - wallet.coldkey # unlock coldkey - if prompt: - # Prompt user for confirmation. - if not Confirm.ask( - f"Swap {wallet.hotkey} for new hotkey: {new_wallet.hotkey}?" - ): - return False - - with bt_console.status(":satellite: Swapping hotkeys..."): - success, err_msg = subtensor._do_swap_hotkey( - wallet=wallet, - new_wallet=new_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not success: - bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - time.sleep(0.5) - return False - - else: - bt_console.print( - f"Hotkey {wallet.hotkey} swapped for new hotkey: {new_wallet.hotkey}" - ) - return True diff --git a/bittensor/api/extrinsics/root.py b/bittensor/api/extrinsics/root.py deleted file mode 100644 index 3bd950c55..000000000 --- a/bittensor/api/extrinsics/root.py +++ /dev/null @@ -1,209 +0,0 @@ -# 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 logging -import time -from typing import Union, List, TYPE_CHECKING - -import numpy as np -from bittensor_wallet import Wallet -from numpy.typing import NDArray -from rich.prompt import Confirm - -from bittensor.core.settings import bt_console -from bittensor.utils import weight_utils -from bittensor.utils.btlogging import logging -from bittensor.utils.registration import torch, legacy_torch_api_compat - -# For annotation purposes -if TYPE_CHECKING: - from bittensor.core.subtensor import Subtensor - - -def root_register_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """Registers the wallet to root network. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - - wallet.coldkey # unlock coldkey - - is_registered = subtensor.is_hotkey_registered( - netuid=0, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - bt_console.print( - ":white_heavy_check_mark: [green]Already registered on root network.[/green]" - ) - return True - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask("Register to root network?"): - return False - - with bt_console.status(":satellite: Registering to root network..."): - success, err_msg = subtensor._do_root_register( - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not success: - bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - time.sleep(0.5) - - # Successful registration, final check for neuron and pubkey - else: - is_registered = subtensor.is_hotkey_registered( - netuid=0, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - bt_console.print(":white_heavy_check_mark: [green]Registered[/green]") - return True - else: - # neuron not found, try again - bt_console.print( - ":cross_mark: [red]Unknown error. Neuron not found.[/red]" - ) - - -@legacy_torch_api_compat -def set_root_weights_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - netuids: Union[NDArray[np.int64], "torch.LongTensor", List[int]], - weights: Union[NDArray[np.float32], "torch.FloatTensor", List[float]], - version_key: int = 0, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """Sets the given weights and values on chain for wallet hotkey account. - - Args: - subtensor (bittensor.core.subtensor.Subtensor: The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - netuids (Union[NDArray[np.int64], torch.LongTensor, List[int]]): The ``netuid`` of the subnet to set weights for. - weights (Union[NDArray[np.float32], torch.FloatTensor, list]): 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - - wallet.coldkey # unlock coldkey - - # 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) - - # Get weight restrictions. - min_allowed_weights = subtensor.min_allowed_weights(netuid=0) - max_weight_limit = subtensor.max_weight_limit(netuid=0) - - # Get non zero values. - non_zero_weight_idx = np.argwhere(weights > 0).squeeze(axis=1) - non_zero_weight_uids = netuids[non_zero_weight_idx] - non_zero_weights = weights[non_zero_weight_idx] - if non_zero_weights.size < min_allowed_weights: - raise ValueError( - "The minimum number of weights required to set weights is {}, got {}".format( - min_allowed_weights, non_zero_weights.size - ) - ) - - # Normalize the weights to max value. - formatted_weights = weight_utils.normalize_max_weight( - x=weights, limit=max_weight_limit - ) - bt_console.print( - f"\nRaw Weights -> Normalized weights: \n\t{weights} -> \n\t{formatted_weights}\n" - ) - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to set the following root weights?:\n[bold white] weights: {}\n uids: {}[/bold white ]?".format( - formatted_weights, netuids - ) - ): - return False - - with bt_console.status( - ":satellite: Setting root weights on [white]{}[/white] ...".format( - subtensor.network - ) - ): - try: - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - netuids, weights - ) - success, error_message = subtensor._do_set_root_weights( - wallet=wallet, - netuid=0, - uids=weight_uids, - vals=weight_vals, - version_key=version_key, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - ) - - bt_console.print(success, error_message) - - if not wait_for_finalization and not wait_for_inclusion: - return True - - if success is True: - bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - logging.success( - prefix="Set weights", - suffix="Finalized: " + str(success), - ) - return True - else: - bt_console.print(f":cross_mark: [red]Failed[/red]: {error_message}") - logging.warning( - prefix="Set weights", - suffix="Failed: " + str(error_message), - ) - return False - - except Exception as e: - # TODO( devs ): lets remove all of the bt_console calls and replace with the bittensor logger. - bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) - logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) - return False diff --git a/bittensor/api/extrinsics/senate.py b/bittensor/api/extrinsics/senate.py deleted file mode 100644 index 9ceebf9ec..000000000 --- a/bittensor/api/extrinsics/senate.py +++ /dev/null @@ -1,263 +0,0 @@ -# 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 time -from typing import TYPE_CHECKING - -from bittensor_wallet import Wallet -from rich.prompt import Confirm - -from bittensor.core.settings import bt_console -from bittensor.utils import format_error_message - -# For annotation purposes -if TYPE_CHECKING: - from bittensor.core.subtensor import Subtensor - - -def register_senate_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """Registers the wallet to chain for senate voting. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (bittensor.wallet): Bittensor wallet object. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - 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``. - """ - - wallet.coldkey # unlock coldkey - wallet.hotkey # unlock hotkey - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask(f"Register delegate hotkey to senate?"): - return False - - with bt_console.status(":satellite: Registering with senate..."): - with subtensor.substrate as substrate: - # create extrinsic call - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="join_senate", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if registration successful - response.process_events() - if not response.is_success: - bt_console.print( - f":cross_mark: [red]Failed[/red]:{format_error_message(response.error_message)}" - ) - time.sleep(0.5) - - # Successful registration, final check for membership - else: - is_registered = subtensor.is_senate_member(wallet.hotkey.ss58_address) - - if is_registered: - bt_console.print( - ":white_heavy_check_mark: [green]Registered[/green]" - ) - return True - else: - # neuron not found, try again - bt_console.print( - ":cross_mark: [red]Unknown error. Senate membership not found.[/red]" - ) - - -def leave_senate_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """Removes the wallet from chain for senate voting. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (bittensor.wallet): Bittensor wallet object. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - 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``. - """ - wallet.coldkey # unlock coldkey - wallet.hotkey # unlock hotkey - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask(f"Remove delegate hotkey from senate?"): - return False - - with bt_console.status(":satellite: Leaving senate..."): - with subtensor.substrate as substrate: - # create extrinsic call - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="leave_senate", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if registration successful - response.process_events() - if not response.is_success: - bt_console.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" - ) - time.sleep(0.5) - - # Successful registration, final check for membership - else: - is_registered = subtensor.is_senate_member(wallet.hotkey.ss58_address) - - if not is_registered: - bt_console.print( - ":white_heavy_check_mark: [green]Left senate[/green]" - ) - return True - else: - # neuron not found, try again - bt_console.print( - ":cross_mark: [red]Unknown error. Senate membership still found.[/red]" - ) - - -def vote_senate_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - proposal_hash: str, - proposal_idx: int, - vote: bool, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """ - Votes ayes or nays on proposals. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (bittensor.wallet): Bittensor wallet object. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - vote: - proposal_idx: - proposal_hash: - - 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``. - """ - wallet.coldkey # unlock coldkey - wallet.hotkey # unlock hotkey - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask("Cast a vote of {}?".format(vote)): - return False - - with bt_console.status(":satellite: Casting vote.."): - with subtensor.substrate as substrate: - # create extrinsic call - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="vote", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "proposal": proposal_hash, - "index": proposal_idx, - "approve": vote, - }, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if vote successful - response.process_events() - if not response.is_success: - bt_console.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" - ) - time.sleep(0.5) - - # Successful vote, final check for data - else: - vote_data = subtensor.get_vote_data(proposal_hash) - has_voted = ( - vote_data["ayes"].count(wallet.hotkey.ss58_address) > 0 - or vote_data["nays"].count(wallet.hotkey.ss58_address) > 0 - ) - - if has_voted: - bt_console.print( - ":white_heavy_check_mark: [green]Vote cast.[/green]" - ) - return True - else: - # hotkey not found in ayes/nays - bt_console.print( - ":cross_mark: [red]Unknown error. Couldn't find vote.[/red]" - ) diff --git a/bittensor/api/extrinsics/serving.py b/bittensor/api/extrinsics/serving.py index 9790c25ed..ab2421566 100644 --- a/bittensor/api/extrinsics/serving.py +++ b/bittensor/api/extrinsics/serving.py @@ -34,6 +34,7 @@ from bittensor.core.subtensor import Subtensor +# Community uses this extrinsic via `subtensor.serve` def serve_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -66,7 +67,7 @@ def serve_extrinsic( success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ # Decrypt hotkey - wallet.hotkey + wallet.unlock_hotkey() params: "AxonServeCallParams" = { "version": version_as_int, "ip": net.ip_to_int(ip), @@ -118,7 +119,7 @@ def serve_extrinsic( logging.debug( f"Serving axon with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) -> {subtensor.network}:{netuid}" ) - success, error_message = subtensor._do_serve_axon( + success, error_message = subtensor.do_serve_axon( wallet=wallet, call_params=params, wait_for_finalization=wait_for_finalization, @@ -138,13 +139,13 @@ def serve_extrinsic( return True +# Community uses this extrinsic via `subtensor.set_weights` def serve_axon_extrinsic( subtensor: "Subtensor", netuid: int, axon: "Axon", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, - prompt: bool = False, ) -> bool: """Serves the axon to the network. @@ -154,13 +155,12 @@ def serve_axon_extrinsic( axon (bittensor.core.axon.Axon): Axon to serve. 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. Returns: success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. """ - axon.wallet.hotkey - axon.wallet.coldkeypub + axon.wallet.unlock_hotkey() + axon.wallet.unlock_coldkeypub() external_port = axon.external_port # ---- Get external ip ---- @@ -197,6 +197,7 @@ def serve_axon_extrinsic( return serve_success +# Community uses this extrinsic directly and via `subtensor.commit` def publish_metadata( subtensor, wallet: "Wallet", @@ -225,7 +226,7 @@ def publish_metadata( MetadataError: If there is an error in submitting the extrinsic or if the response from the blockchain indicates failure. """ - wallet.hotkey + wallet.unlock_hotkey() with subtensor.substrate as substrate: call = substrate.compose_call( @@ -253,6 +254,7 @@ def publish_metadata( raise MetadataError(format_error_message(response.error_message)) +# Community uses this function directly def get_metadata(self, netuid: int, hotkey: str, block: Optional[int] = None) -> str: @retry(delay=2, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): diff --git a/bittensor/api/extrinsics/set_weights.py b/bittensor/api/extrinsics/set_weights.py index 80a51f763..8848f38b6 100644 --- a/bittensor/api/extrinsics/set_weights.py +++ b/bittensor/api/extrinsics/set_weights.py @@ -33,6 +33,7 @@ from bittensor.core.subtensor import Subtensor +# Community uses this extrinsic directly and via `subtensor.set_weights` def set_weights_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -90,7 +91,7 @@ def set_weights_extrinsic( ":satellite: Setting weights on [white]{}[/white] ...".format(subtensor.network) ): try: - success, error_message = subtensor._do_set_weights( + success, error_message = subtensor.do_set_weights( wallet=wallet, netuid=netuid, uids=weight_uids, @@ -120,5 +121,7 @@ def set_weights_extrinsic( except Exception as e: bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) - logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) + logging.warning( + msg=str(e), prefix="Set weights", suffix="Failed: " + ) return False, str(e) diff --git a/bittensor/api/extrinsics/staking.py b/bittensor/api/extrinsics/staking.py deleted file mode 100644 index 556a8cd40..000000000 --- a/bittensor/api/extrinsics/staking.py +++ /dev/null @@ -1,492 +0,0 @@ -# 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. - -from rich.prompt import Confirm -from time import sleep -from typing import List, Union, Optional, Tuple, TYPE_CHECKING -from bittensor.core.errors import NotDelegateError, StakeError, NotRegisteredError -from bittensor.core.settings import __console__ as bt_console -from bittensor.utils.balance import Balance -from bittensor_wallet import Wallet - -# For annotation purposes -if TYPE_CHECKING: - from bittensor.core.subtensor import Subtensor - - -def _check_threshold_amount( - subtensor: "Subtensor", stake_balance: "Balance" -) -> Tuple[bool, "Balance"]: - """ - Checks if the new stake balance will be above the minimum required stake threshold. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - stake_balance (Balance): the balance to check for threshold limits. - - Returns: - success, threshold (bool, Balance): ``true`` if the staking balance is above the threshold, or ``false`` if the staking balance is below the threshold. The threshold balance required to stake. - """ - min_req_stake: "Balance" = subtensor.get_minimum_required_stake() - - if min_req_stake > stake_balance: - return False, min_req_stake - else: - return True, min_req_stake - - -def add_stake_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - hotkey_ss58: Optional[str] = None, - amount: Optional[Union["Balance", float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """Adds the specified amount of stake to passed hotkey ``uid``. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. - amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - - Raises: - bittensor.core.errors.NotRegisteredError: If the wallet is not registered on the chain. - bittensor.core.errors.NotDelegateError: If the hotkey is not a delegate on the chain. - """ - # Decrypt keys, - wallet.coldkey - - # Default to wallet's own hotkey if the value is not passed. - if hotkey_ss58 is None: - hotkey_ss58 = wallet.hotkey.ss58_address - - # Flag to indicate if we are using the wallet's own hotkey. - own_hotkey: bool - - with bt_console.status( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - # Get hotkey owner - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner - if not own_hotkey: - # This is not the wallet's own hotkey, so we are delegating. - if not subtensor.is_hotkey_delegate(hotkey_ss58): - raise NotDelegateError( - "Hotkey: {} is not a delegate.".format(hotkey_ss58) - ) - - # Get hotkey take - hotkey_take = subtensor.get_delegate_take(hotkey_ss58) - - # Get current stake - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) - - # Grab the existential deposit. - existential_deposit = subtensor.get_existential_deposit() - - # Convert to Balance - if amount is None: - # Stake it all. - staking_balance = Balance.from_tao(old_balance.tao) - elif not isinstance(amount, Balance): - staking_balance = Balance.from_tao(amount) - else: - staking_balance = amount - - # Leave existential balance to keep key alive. - if staking_balance > old_balance - existential_deposit: - # If we are staking all, we need to leave at least the existential deposit. - staking_balance = old_balance - existential_deposit - else: - staking_balance = staking_balance - - # Check enough to stake. - if staking_balance > old_balance: - bt_console.print( - ":cross_mark: [red]Not enough stake[/red]:[bold white]\n balance:{}\n amount: {}\n coldkey: {}[/bold white]".format( - old_balance, staking_balance, wallet.name - ) - ) - return False - - # If nominating, we need to check if the new stake balance will be above the minimum required stake threshold. - if not own_hotkey: - new_stake_balance = old_stake + staking_balance - is_above_threshold, threshold = _check_threshold_amount( - subtensor, new_stake_balance - ) - if not is_above_threshold: - bt_console.print( - f":cross_mark: [red]New stake balance of {new_stake_balance} is below the minimum required nomination stake threshold {threshold}.[/red]" - ) - return False - - # Ask before moving on. - if prompt: - if not own_hotkey: - # We are delegating. - if not Confirm.ask( - "Do you want to delegate:[bold white]\n amount: {}\n to: {}\n take: {}\n owner: {}[/bold white]".format( - staking_balance, wallet.hotkey_str, hotkey_take, hotkey_owner - ) - ): - return False - else: - if not Confirm.ask( - "Do you want to stake:[bold white]\n amount: {}\n to: {}[/bold white]".format( - staking_balance, wallet.hotkey_str - ) - ): - return False - - try: - with bt_console.status( - ":satellite: Staking to: [bold white]{}[/bold white] ...".format( - subtensor.network - ) - ): - staking_response: bool = __do_add_stake_single( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=staking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response 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 - - bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - with bt_console.status( - ":satellite: Checking Balance on: [white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance( - address=wallet.coldkeypub.ss58_address - ) - block = subtensor.get_current_block() - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block=block, - ) # Get current stake - - bt_console.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_balance, new_balance - ) - ) - bt_console.print( - "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_stake, new_stake - ) - ) - return True - else: - bt_console.print(":cross_mark: [red]Failed[/red]: Error unknown.") - return False - - except NotRegisteredError: - bt_console.print( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) - ) - return False - except StakeError as e: - bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - return False - - -def add_stake_multiple_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - hotkey_ss58s: List[str], - amounts: Optional[List[Union["Balance", float]]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (bittensor_wallet.Wallet): Bittensor wallet object for the coldkey. - hotkey_ss58s (List[str]): List of hotkeys to stake to. - amounts (List[Union[Balance, float]]): List of amounts to stake. If ``None``, stake all to the first hotkey. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any wallet was staked. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not isinstance(hotkey_ss58s, list) or not all( - isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s - ): - raise TypeError("hotkey_ss58s must be a list of str") - - if len(hotkey_ss58s) == 0: - return True - - if amounts is not None and len(amounts) != len(hotkey_ss58s): - raise ValueError("amounts must be a list of the same length as hotkey_ss58s") - - if amounts is not None and not all( - isinstance(amount, (Balance, float)) for amount in amounts - ): - raise TypeError("amounts must be a [list of Balance or float] or None") - - if amounts is None: - amounts = [None] * len(hotkey_ss58s) - else: - # Convert to Balance - amounts = [ - Balance.from_tao(amount) if isinstance(amount, float) else amount - for amount in amounts - ] - - if sum(amount.tao for amount in amounts) == 0: - # Staking 0 tao - return True - - # Decrypt coldkey. - wallet.coldkey - - old_stakes = [] - with bt_console.status( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - # Get the old stakes. - for hotkey_ss58 in hotkey_ss58s: - old_stakes.append( - subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) - ) - - # Remove existential balance to keep key alive. - # Keys must maintain a balance of at least 1000 rao to stay alive. - total_staking_rao = sum( - [amount.rao if amount is not None else 0 for amount in amounts] - ) - if total_staking_rao == 0: - # Staking all to the first wallet. - if old_balance.rao > 1000: - old_balance -= Balance.from_rao(1000) - - elif total_staking_rao < 1000: - # Staking less than 1000 rao to the wallets. - pass - else: - # Staking more than 1000 rao to the wallets. - # Reduce the amount to stake to each wallet to keep the balance above 1000 rao. - percent_reduction = 1 - (1000 / total_staking_rao) - amounts = [ - Balance.from_tao(amount.tao * percent_reduction) for amount in amounts - ] - - successful_stakes = 0 - for idx, (hotkey_ss58, amount, old_stake) in enumerate( - zip(hotkey_ss58s, amounts, old_stakes) - ): - staking_all = False - # Convert to Balance - if amount is None: - # Stake it all. - staking_balance = Balance.from_tao(old_balance.tao) - staking_all = True - else: - # Amounts are cast to balance earlier in the function - assert isinstance(amount, Balance) - staking_balance = amount - - # Check enough to stake - if staking_balance > old_balance: - bt_console.print( - ":cross_mark: [red]Not enough balance[/red]: [green]{}[/green] to stake: [blue]{}[/blue] from coldkey: [white]{}[/white]".format( - old_balance, staking_balance, wallet.name - ) - ) - continue - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to stake:\n[bold white] amount: {}\n hotkey: {}[/bold white ]?".format( - staking_balance, wallet.hotkey_str - ) - ): - continue - - try: - staking_response: bool = __do_add_stake_single( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=staking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response is True: # If we successfully staked. - # We only wait here if we expect finalization. - - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_rate_limit_blocks = subtensor.tx_rate_limit() - if tx_rate_limit_blocks > 0: - bt_console.print( - ":hourglass: [yellow]Waiting for tx rate limit: [white]{}[/white] blocks[/yellow]".format( - tx_rate_limit_blocks - ) - ) - sleep(tx_rate_limit_blocks * 12) # 12 seconds per block - - if not wait_for_finalization and not wait_for_inclusion: - old_balance -= staking_balance - successful_stakes += 1 - if staking_all: - # If staked all, no need to continue - break - - continue - - bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - - block = subtensor.get_current_block() - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block=block, - ) - new_balance = subtensor.get_balance( - wallet.coldkeypub.ss58_address, block=block - ) - bt_console.print( - "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - hotkey_ss58, old_stake, new_stake - ) - ) - old_balance = new_balance - successful_stakes += 1 - if staking_all: - # If staked all, no need to continue - break - - else: - bt_console.print(":cross_mark: [red]Failed[/red]: Error unknown.") - continue - - except NotRegisteredError: - bt_console.print( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - hotkey_ss58 - ) - ) - continue - except StakeError as e: - bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - continue - - if successful_stakes != 0: - with bt_console.status( - ":satellite: Checking Balance on: ([white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bt_console.print( - "Balance: [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_balance, new_balance - ) - ) - return True - - return False - - -def __do_add_stake_single( - subtensor: "Subtensor", - wallet: "Wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> bool: - """ - Executes a stake call to the chain using the wallet and the amount specified. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (bittensor_wallet.Wallet): Bittensor wallet object. - hotkey_ss58 (str): Hotkey to stake to. - amount (Balance): Amount to stake as Bittensor balance object. - 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. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - - Raises: - bittensor.core.errors.StakeError: If the extrinsic fails to be finalized or included in the block. - bittensor.core.errors.NotDelegateError: If the hotkey is not a delegate. - bittensor.core.errors.NotRegisteredError: If the hotkey is not registered in any subnets. - """ - # Decrypt keys, - wallet.coldkey - - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner - if not own_hotkey: - # We are delegating. - # Verify that the hotkey is a delegate. - if not subtensor.is_hotkey_delegate(hotkey_ss58=hotkey_ss58): - raise NotDelegateError("Hotkey: {} is not a delegate.".format(hotkey_ss58)) - - success = subtensor._do_stake( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - return success diff --git a/bittensor/api/extrinsics/transfer.py b/bittensor/api/extrinsics/transfer.py index f7362443b..e56245895 100644 --- a/bittensor/api/extrinsics/transfer.py +++ b/bittensor/api/extrinsics/transfer.py @@ -19,7 +19,7 @@ from rich.prompt import Confirm -from bittensor.core.settings import bt_console, network_explorer_map +from bittensor.core.settings import bt_console, NETWORK_EXPLORER_MAP from bittensor.utils import get_explorer_url_for_network from bittensor.utils import is_valid_bittensor_address_or_public_key from bittensor.utils.balance import Balance @@ -30,6 +30,7 @@ from bittensor.core.subtensor import Subtensor +# Community uses this extrinsic directly and via `subtensor.transfer` def transfer_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -69,7 +70,7 @@ def transfer_extrinsic( dest = "0x" + dest.hex() # Unlock wallet coldkey. - wallet.coldkey + wallet.unlock_coldkey() # Convert to bittensor.Balance if not isinstance(amount, Balance): @@ -111,7 +112,7 @@ def transfer_extrinsic( return False with bt_console.status(":satellite: Transferring..."): - success, block_hash, err_msg = subtensor._do_transfer( + success, block_hash, err_msg = subtensor.do_transfer( wallet, dest, transfer_balance, @@ -124,7 +125,7 @@ def transfer_extrinsic( bt_console.print("[green]Block Hash: {}[/green]".format(block_hash)) explorer_urls = get_explorer_url_for_network( - subtensor.network, block_hash, network_explorer_map + subtensor.network, block_hash, NETWORK_EXPLORER_MAP ) if explorer_urls != {} and explorer_urls: bt_console.print( diff --git a/bittensor/api/extrinsics/unstaking.py b/bittensor/api/extrinsics/unstaking.py deleted file mode 100644 index a16e4660f..000000000 --- a/bittensor/api/extrinsics/unstaking.py +++ /dev/null @@ -1,426 +0,0 @@ -# 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. - -from time import sleep -from typing import List, Union, Optional, TYPE_CHECKING - -from bittensor_wallet import Wallet -from rich.prompt import Confirm - -from bittensor.core.errors import NotRegisteredError, StakeError -from bittensor.core.settings import bt_console -from bittensor.utils.balance import Balance - -# For annotation purposes -if TYPE_CHECKING: - from bittensor.core.subtensor import Subtensor - - -def __do_remove_stake_single( - subtensor: "Subtensor", - wallet: "Wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> bool: - """ - Executes an unstake call to the chain using the wallet and the amount specified. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - hotkey_ss58 (str): Hotkey address to unstake from. - amount (bittensor.Balance): Amount to unstake as Bittensor balance object. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - Raises: - bittensor.core.errors.StakeError: If the extrinsic fails to be finalized or included in the block. - bittensor.core.errors.NotRegisteredError: If the hotkey is not registered in any subnets. - - """ - # Decrypt keys, - wallet.coldkey - - success = subtensor._do_unstake( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - return success - - -def check_threshold_amount(subtensor: "Subtensor", stake_balance: Balance) -> bool: - """ - Checks if the remaining stake balance is above the minimum required stake threshold. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - stake_balance (Balance): the balance to check for threshold limits. - - Returns: - success (bool): ``true`` if the unstaking is above the threshold or 0, or ``false`` if the unstaking is below the threshold, but not 0. - """ - min_req_stake: Balance = subtensor.get_minimum_required_stake() - - if min_req_stake > stake_balance > 0: - bt_console.print( - f":cross_mark: [yellow]Remaining stake balance of {stake_balance} less than minimum of {min_req_stake} TAO[/yellow]" - ) - return False - else: - return True - - -def unstake_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - hotkey_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """Removes stake into the wallet coldkey from the specified hotkey ``uid``. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): Bittensor wallet object. - hotkey_ss58 (Optional[str]): The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey is used. - amount (Union[Balance, float]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - # Decrypt keys, - wallet.coldkey - - if hotkey_ss58 is None: - hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. - - with bt_console.status( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) - - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkey: bool = wallet.coldkeypub.ss58_address == hotkey_owner - - # Convert to bittensor.Balance - if amount is None: - # Unstake it all. - unstaking_balance = old_stake - elif not isinstance(amount, Balance): - unstaking_balance = Balance.from_tao(amount) - else: - unstaking_balance = amount - - # Check enough to unstake. - stake_on_uid = old_stake - if unstaking_balance > stake_on_uid: - bt_console.print( - ":cross_mark: [red]Not enough stake[/red]: [green]{}[/green] to unstake: [blue]{}[/blue] from hotkey: [white]{}[/white]".format( - stake_on_uid, unstaking_balance, wallet.hotkey_str - ) - ) - return False - - # If nomination stake, check threshold. - if not own_hotkey and not check_threshold_amount( - subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) - ): - bt_console.print( - f":warning: [yellow]This action will unstake the entire staked balance![/yellow]" - ) - unstaking_balance = stake_on_uid - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to unstake:\n[bold white] amount: {}\n hotkey: {}[/bold white ]?".format( - unstaking_balance, wallet.hotkey_str - ) - ): - return False - - try: - with bt_console.status( - ":satellite: Unstaking from chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - staking_response: bool = __do_remove_stake_single( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=unstaking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response 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 - - bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - with bt_console.status( - ":satellite: Checking Balance on: [white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance( - address=wallet.coldkeypub.ss58_address - ) - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) # Get stake on hotkey. - bt_console.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_balance, new_balance - ) - ) - bt_console.print( - "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_stake, new_stake - ) - ) - return True - else: - bt_console.print(":cross_mark: [red]Failed[/red]: Unknown Error.") - return False - - except NotRegisteredError as e: - bt_console.print( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) - ) - return False - except StakeError as e: - bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - return False - - -def unstake_multiple_extrinsic( - subtensor: "Subtensor", - wallet: "Wallet", - hotkey_ss58s: List[str], - amounts: Optional[List[Union[Balance, float]]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. - - Args: - subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (Wallet): The wallet with the coldkey to unstake to. - hotkey_ss58s (List[str]): List of hotkeys to unstake from. - amounts (List[Union[Balance, float]]): List of amounts to unstake. If ``None``, unstake all. - wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not isinstance(hotkey_ss58s, list) or not all( - isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s - ): - raise TypeError("hotkey_ss58s must be a list of str") - - if len(hotkey_ss58s) == 0: - return True - - if amounts is not None and len(amounts) != len(hotkey_ss58s): - raise ValueError("amounts must be a list of the same length as hotkey_ss58s") - - if amounts is not None and not all( - isinstance(amount, (Balance, float)) for amount in amounts - ): - raise TypeError("amounts must be a [list of Balance or float] or None") - - if amounts is None: - amounts = [None] * len(hotkey_ss58s) - else: - # Convert to Balance - amounts = [ - Balance.from_tao(amount) if isinstance(amount, float) else amount - for amount in amounts - ] - - if sum(amount.tao for amount in amounts) == 0: - # Staking 0 tao - return True - - # Unlock coldkey. - wallet.coldkey - - old_stakes = [] - own_hotkeys = [] - with bt_console.status( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - for hotkey_ss58 in hotkey_ss58s: - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) # Get stake on hotkey. - old_stakes.append(old_stake) # None if not registered. - - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkeys.append(wallet.coldkeypub.ss58_address == hotkey_owner) - - successful_unstakes = 0 - for idx, (hotkey_ss58, amount, old_stake, own_hotkey) in enumerate( - zip(hotkey_ss58s, amounts, old_stakes, own_hotkeys) - ): - # Covert to bittensor.Balance - if amount is None: - # Unstake it all. - unstaking_balance = old_stake - elif not isinstance(amount, Balance): - unstaking_balance = Balance.from_tao(amount) - else: - unstaking_balance = amount - - # Check enough to unstake. - stake_on_uid = old_stake - if unstaking_balance > stake_on_uid: - bt_console.print( - ":cross_mark: [red]Not enough stake[/red]: [green]{}[/green] to unstake: [blue]{}[/blue] from hotkey: [white]{}[/white]".format( - stake_on_uid, unstaking_balance, wallet.hotkey_str - ) - ) - continue - - # If nomination stake, check threshold. - if not own_hotkey and not check_threshold_amount( - subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) - ): - bt_console.print( - f":warning: [yellow]This action will unstake the entire staked balance![/yellow]" - ) - unstaking_balance = stake_on_uid - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to unstake:\n[bold white] amount: {}\n hotkey: {}[/bold white ]?".format( - unstaking_balance, wallet.hotkey_str - ) - ): - continue - - try: - with bt_console.status( - ":satellite: Unstaking from chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - staking_response: bool = __do_remove_stake_single( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=unstaking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response is True: # If we successfully unstaked. - # We only wait here if we expect finalization. - - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_rate_limit_blocks = subtensor.tx_rate_limit() - if tx_rate_limit_blocks > 0: - bt_console.print( - ":hourglass: [yellow]Waiting for tx rate limit: [white]{}[/white] blocks[/yellow]".format( - tx_rate_limit_blocks - ) - ) - sleep(tx_rate_limit_blocks * 12) # 12 seconds per block - - if not wait_for_finalization and not wait_for_inclusion: - successful_unstakes += 1 - continue - - bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - with bt_console.status( - ":satellite: Checking Balance on: [white]{}[/white] ...".format( - subtensor.network - ) - ): - block = subtensor.get_current_block() - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block=block, - ) - bt_console.print( - "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - hotkey_ss58, stake_on_uid, new_stake - ) - ) - successful_unstakes += 1 - else: - bt_console.print(":cross_mark: [red]Failed[/red]: Unknown Error.") - continue - - except NotRegisteredError as e: - bt_console.print( - ":cross_mark: [red]{} is not registered.[/red]".format(hotkey_ss58) - ) - continue - except StakeError as e: - bt_console.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - continue - - if successful_unstakes != 0: - with bt_console.status( - ":satellite: Checking Balance on: ([white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bt_console.print( - "Balance: [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_balance, new_balance - ) - ) - return True - - return False diff --git a/bittensor/api/extrinsics/utils.py b/bittensor/api/extrinsics/utils.py deleted file mode 100644 index 16466e9c6..000000000 --- a/bittensor/api/extrinsics/utils.py +++ /dev/null @@ -1,41 +0,0 @@ -# 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. - -HYPERPARAMS = { - "serving_rate_limit": "sudo_set_serving_rate_limit", - "min_difficulty": "sudo_set_min_difficulty", - "max_difficulty": "sudo_set_max_difficulty", - "weights_version": "sudo_set_weights_version_key", - "weights_rate_limit": "sudo_set_weights_set_rate_limit", - "max_weight_limit": "sudo_set_max_weight_limit", - "immunity_period": "sudo_set_immunity_period", - "min_allowed_weights": "sudo_set_min_allowed_weights", - "activity_cutoff": "sudo_set_activity_cutoff", - "network_registration_allowed": "sudo_set_network_registration_allowed", - "network_pow_registration_allowed": "sudo_set_network_pow_registration_allowed", - "min_burn": "sudo_set_min_burn", - "max_burn": "sudo_set_max_burn", - "adjustment_alpha": "sudo_set_adjustment_alpha", - "rho": "sudo_set_rho", - "kappa": "sudo_set_kappa", - "difficulty": "sudo_set_difficulty", - "bonds_moving_avg": "sudo_set_bonds_moving_average", - "commit_reveal_weights_interval": "sudo_set_commit_reveal_weights_interval", - "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", - "alpha_values": "sudo_set_alpha_values", - "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", -} diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 48a4ee908..8a4a21c2f 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -54,7 +54,7 @@ SynapseParsingError, UnknownSynapseError, ) -from bittensor.core.settings import defaults, version_as_int +from bittensor.core.settings import DEFAULTS, version_as_int from bittensor.core.synapse import Synapse, TerminalInfo from bittensor.core.threadpool import PriorityThreadPoolExecutor from bittensor.utils import networking @@ -319,11 +319,11 @@ def __init__( if config is None: config = Axon.config() config = copy.deepcopy(config) - config.axon.ip = ip or defaults.axon.ip - config.axon.port = port or defaults.axon.port - config.axon.external_ip = external_ip or defaults.axon.external_ip - config.axon.external_port = external_port or defaults.axon.external_port - config.axon.max_workers = max_workers or defaults.axon.max_workers + config.axon.ip = ip or DEFAULTS.axon.ip + config.axon.port = port or DEFAULTS.axon.port + config.axon.external_ip = external_ip or DEFAULTS.axon.external_ip + config.axon.external_port = external_port or DEFAULTS.axon.external_port + config.axon.max_workers = max_workers or DEFAULTS.axon.max_workers Axon.check_config(config) self.config = config # type: ignore @@ -586,34 +586,34 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: Optional[str] = None) "--" + prefix_str + "axon.port", type=int, help="The local port this axon endpoint is bound to. i.e. 8091", - default=defaults.axon.port, + default=DEFAULTS.axon.port, ) parser.add_argument( "--" + prefix_str + "axon.ip", type=str, help="""The local ip this axon binds to. ie. [::]""", - default=defaults.axon.ip, + default=DEFAULTS.axon.ip, ) parser.add_argument( "--" + prefix_str + "axon.external_port", type=int, required=False, help="""The public port this axon broadcasts to the network. i.e. 8091""", - default=defaults.axon.external_port, + default=DEFAULTS.axon.external_port, ) parser.add_argument( "--" + prefix_str + "axon.external_ip", type=str, required=False, help="""The external ip this axon broadcasts to the network to. ie. [::]""", - default=defaults.axon.external_ip, + default=DEFAULTS.axon.external_ip, ) parser.add_argument( "--" + prefix_str + "axon.max_workers", type=int, help="""The maximum number connection handler threads working simultaneously on this endpoint. The grpc server distributes new worker threads to service requests up to this number.""", - default=defaults.axon.max_workers, + default=DEFAULTS.axon.max_workers, ) except argparse.ArgumentError: @@ -1340,7 +1340,7 @@ async def submit_task( """ loop = asyncio.get_event_loop() future = loop.run_in_executor(executor, lambda: priority) - await future + result = await future return priority, result # If a priority function exists for the request's name diff --git a/bittensor/core/chain_data.py b/bittensor/core/chain_data.py index 9228afce0..e96dbd2ba 100644 --- a/bittensor/core/chain_data.py +++ b/bittensor/core/chain_data.py @@ -30,8 +30,8 @@ from scalecodec.types import GenericCall from scalecodec.utils.ss58 import ss58_encode -from .settings import ss58_format -from bittensor.utils import networking as net, RAOPERTAO, U16_NORMALIZED_FLOAT +from .settings import SS58_FORMAT +from bittensor.utils import networking as net, RAOPERTAO, u16_normalized_float from bittensor.utils.balance import Balance from bittensor.utils.registration import torch, use_torch from bittensor.utils.btlogging import logging @@ -426,13 +426,13 @@ class NeuronInfo: def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfo": """Fixes the values of the NeuronInfo object.""" neuron_info_decoded["hotkey"] = ss58_encode( - neuron_info_decoded["hotkey"], ss58_format + neuron_info_decoded["hotkey"], SS58_FORMAT ) neuron_info_decoded["coldkey"] = ss58_encode( - neuron_info_decoded["coldkey"], ss58_format + neuron_info_decoded["coldkey"], SS58_FORMAT ) stake_dict = { - ss58_encode(coldkey, ss58_format): Balance.from_rao(int(stake)) + ss58_encode(coldkey, SS58_FORMAT): Balance.from_rao(int(stake)) for coldkey, stake in neuron_info_decoded["stake"] } neuron_info_decoded["stake_dict"] = stake_dict @@ -445,21 +445,21 @@ def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfo": neuron_info_decoded["bonds"] = [ [int(bond[0]), int(bond[1])] for bond in neuron_info_decoded["bonds"] ] - neuron_info_decoded["rank"] = U16_NORMALIZED_FLOAT(neuron_info_decoded["rank"]) + neuron_info_decoded["rank"] = u16_normalized_float(neuron_info_decoded["rank"]) neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO - neuron_info_decoded["incentive"] = U16_NORMALIZED_FLOAT( + neuron_info_decoded["incentive"] = u16_normalized_float( neuron_info_decoded["incentive"] ) - neuron_info_decoded["consensus"] = U16_NORMALIZED_FLOAT( + neuron_info_decoded["consensus"] = u16_normalized_float( neuron_info_decoded["consensus"] ) - neuron_info_decoded["trust"] = U16_NORMALIZED_FLOAT( + neuron_info_decoded["trust"] = u16_normalized_float( neuron_info_decoded["trust"] ) - neuron_info_decoded["validator_trust"] = U16_NORMALIZED_FLOAT( + neuron_info_decoded["validator_trust"] = u16_normalized_float( neuron_info_decoded["validator_trust"] ) - neuron_info_decoded["dividends"] = U16_NORMALIZED_FLOAT( + neuron_info_decoded["dividends"] = u16_normalized_float( neuron_info_decoded["dividends"] ) neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values( @@ -571,33 +571,33 @@ class NeuronInfoLite: def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfoLite": """Fixes the values of the NeuronInfoLite object.""" neuron_info_decoded["hotkey"] = ss58_encode( - neuron_info_decoded["hotkey"], ss58_format + neuron_info_decoded["hotkey"], SS58_FORMAT ) neuron_info_decoded["coldkey"] = ss58_encode( - neuron_info_decoded["coldkey"], ss58_format + neuron_info_decoded["coldkey"], SS58_FORMAT ) stake_dict = { - ss58_encode(coldkey, ss58_format): Balance.from_rao(int(stake)) + ss58_encode(coldkey, SS58_FORMAT): Balance.from_rao(int(stake)) for coldkey, stake in neuron_info_decoded["stake"] } neuron_info_decoded["stake_dict"] = stake_dict neuron_info_decoded["stake"] = sum(stake_dict.values()) neuron_info_decoded["total_stake"] = neuron_info_decoded["stake"] - neuron_info_decoded["rank"] = U16_NORMALIZED_FLOAT(neuron_info_decoded["rank"]) + neuron_info_decoded["rank"] = u16_normalized_float(neuron_info_decoded["rank"]) neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO - neuron_info_decoded["incentive"] = U16_NORMALIZED_FLOAT( + neuron_info_decoded["incentive"] = u16_normalized_float( neuron_info_decoded["incentive"] ) - neuron_info_decoded["consensus"] = U16_NORMALIZED_FLOAT( + neuron_info_decoded["consensus"] = u16_normalized_float( neuron_info_decoded["consensus"] ) - neuron_info_decoded["trust"] = U16_NORMALIZED_FLOAT( + neuron_info_decoded["trust"] = u16_normalized_float( neuron_info_decoded["trust"] ) - neuron_info_decoded["validator_trust"] = U16_NORMALIZED_FLOAT( + neuron_info_decoded["validator_trust"] = u16_normalized_float( neuron_info_decoded["validator_trust"] ) - neuron_info_decoded["dividends"] = U16_NORMALIZED_FLOAT( + neuron_info_decoded["dividends"] = u16_normalized_float( neuron_info_decoded["dividends"] ) neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values( @@ -748,12 +748,12 @@ def fix_decoded_values(cls, decoded: Any) -> "DelegateInfo": """Fixes the decoded values.""" return cls( - hotkey_ss58=ss58_encode(decoded["delegate_ss58"], ss58_format), - owner_ss58=ss58_encode(decoded["owner_ss58"], ss58_format), - take=U16_NORMALIZED_FLOAT(decoded["take"]), + hotkey_ss58=ss58_encode(decoded["delegate_ss58"], SS58_FORMAT), + owner_ss58=ss58_encode(decoded["owner_ss58"], SS58_FORMAT), + take=u16_normalized_float(decoded["take"]), nominators=[ ( - ss58_encode(nom[0], ss58_format), + ss58_encode(nom[0], SS58_FORMAT), Balance.from_rao(nom[1]), ) for nom in decoded["nominators"] @@ -819,8 +819,8 @@ class StakeInfo: def fix_decoded_values(cls, decoded: Any) -> "StakeInfo": """Fixes the decoded values.""" return cls( - hotkey_ss58=ss58_encode(decoded["hotkey"], ss58_format), - coldkey_ss58=ss58_encode(decoded["coldkey"], ss58_format), + hotkey_ss58=ss58_encode(decoded["hotkey"], SS58_FORMAT), + coldkey_ss58=ss58_encode(decoded["coldkey"], SS58_FORMAT), stake=Balance.from_rao(decoded["stake"]), ) @@ -851,7 +851,7 @@ def list_of_tuple_from_vec_u8( return {} return { - ss58_encode(address=account_id, ss58_format=ss58_format): [ + ss58_encode(address=account_id, ss58_format=SS58_FORMAT): [ StakeInfo.fix_decoded_values(d) for d in stake_info ] for account_id, stake_info in decoded @@ -936,12 +936,12 @@ def fix_decoded_values(cls, decoded: Dict) -> "SubnetInfo": tempo=decoded["tempo"], modality=decoded["network_modality"], connection_requirements={ - str(int(netuid)): U16_NORMALIZED_FLOAT(int(req)) + str(int(netuid)): u16_normalized_float(int(req)) for netuid, req in decoded["network_connect"] }, emission_value=decoded["emission_values"], burn=Balance.from_rao(decoded["burn"]), - owner_ss58=ss58_encode(decoded["owner"], ss58_format), + owner_ss58=ss58_encode(decoded["owner"], SS58_FORMAT), ) def to_parameter_dict(self) -> Union[dict[str, Any], "torch.nn.ParameterDict"]: @@ -1159,8 +1159,8 @@ class ScheduledColdkeySwapInfo: def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": """Fixes the decoded values.""" return cls( - old_coldkey=ss58_encode(decoded["old_coldkey"], ss58_format), - new_coldkey=ss58_encode(decoded["new_coldkey"], ss58_format), + old_coldkey=ss58_encode(decoded["old_coldkey"], SS58_FORMAT), + new_coldkey=ss58_encode(decoded["new_coldkey"], SS58_FORMAT), arbitration_block=decoded["arbitration_block"], ) @@ -1195,4 +1195,4 @@ def decode_account_id_list(cls, vec_u8: List[int]) -> Optional[List[str]]: ) if decoded is None: return None - return [ss58_encode(account_id, ss58_format) for account_id in decoded] + return [ss58_encode(account_id, SS58_FORMAT) for account_id in decoded] diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 86f46bdc9..79eda9c90 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -43,7 +43,7 @@ class config(DefaultMunch): __is_set: Dict[str, bool] - r""" Translates the passed parser into a nested Bittensor config. + """ Translates the passed parser into a nested Bittensor config. Args: parser (argparse.ArgumentParser): @@ -55,6 +55,7 @@ class config(DefaultMunch): default (Optional[Any]): Default value for the Config. Defaults to ``None``. This default will be returned for attributes that are undefined. + Returns: config (bittensor.config): Nested config object created from parser arguments. @@ -71,8 +72,8 @@ def __init__( self["__is_set"] = {} - if parser == None: - return None + if parser is None: + return # Optionally add config specific arguments try: @@ -120,7 +121,7 @@ def __init__( pass # Get args from argv if not passed in. - if args == None: + if args is None: args = sys.argv[1:] # Check for missing required arguments before proceeding @@ -145,10 +146,10 @@ def __init__( config_params = config.__parse_args__(args=args, parser=parser, strict=False) # 2. Optionally check for --strict - ## strict=True when passed in OR when --strict is set + # strict=True when passed in OR when --strict is set strict = config_params.strict or strict - if config_file_path != None: + if config_file_path is not None: config_file_path = os.path.expanduser(config_file_path) try: with open(config_file_path) as f: @@ -169,36 +170,36 @@ def __init__( # Make the is_set map _config["__is_set"] = {} - ## Reparse args using default of unset + # Reparse args using default of unset parser_no_defaults = copy.deepcopy(parser) # Only command as the arg, else no args default_param_args = ( [_config.get("command")] - if _config.get("command") != None and _config.get("subcommand") == None + if _config.get("command") is not None and _config.get("subcommand") is None else [] ) - if _config.get("command") != None and _config.get("subcommand") != None: + if _config.get("command") is not None and _config.get("subcommand") is not None: default_param_args = [_config.get("command"), _config.get("subcommand")] - ## Get all args by name + # Get all args by name default_params = parser.parse_args(args=default_param_args) all_default_args = default_params.__dict__.keys() | [] - ## Make a dict with keys as args and values as argparse.SUPPRESS + # Make a dict with keys as args and values as argparse.SUPPRESS defaults_as_suppress = {key: argparse.SUPPRESS for key in all_default_args} - ## Set the defaults to argparse.SUPPRESS, should remove them from the namespace + # Set the defaults to argparse.SUPPRESS, should remove them from the namespace parser_no_defaults.set_defaults(**defaults_as_suppress) parser_no_defaults._defaults.clear() # Needed for quirk of argparse - ### Check for subparsers and do the same - if parser_no_defaults._subparsers != None: + # Check for subparsers and do the same + if parser_no_defaults._subparsers is not None: for action in parser_no_defaults._subparsers._actions: # Should only be the "command" subparser action if isinstance(action, argparse._SubParsersAction): # Set the defaults to argparse.SUPPRESS, should remove them from the namespace # Each choice is the keyword for a command, we need to set the defaults for each of these - ## Note: we also need to clear the _defaults dict for each, this is a quirk of argparse + # Note: we also need to clear the _defaults dict for each, this is a quirk of argparse cmd_parser: argparse.ArgumentParser for cmd_parser in action.choices.values(): # If this choice is also a subparser, set defaults recursively @@ -264,6 +265,7 @@ def __parse_args__( Command line parser object. strict (bool): If ``true``, the command line arguments are strictly parsed. + Returns: Namespace: Namespace object created from parser arguments. @@ -331,7 +333,8 @@ def update_with_kwargs(self, kwargs): @classmethod def _merge(cls, a, b): - """Merge two configurations recursively. + """ + Merge two configurations recursively. If there is a conflict, the value from the second configuration will take precedence. """ for key in b: @@ -403,15 +406,11 @@ def __get_required_args_from_parser(parser: argparse.ArgumentParser) -> List[str class DefaultConfig(config): - """ - A Config with a set of default values. - """ + """A Config with a set of default values.""" @classmethod def default(cls: Type[T]) -> T: - """ - Get default config. - """ + """Get default config.""" raise NotImplementedError("Function default is not implemented.") diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 0d7483100..47b5fe945 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -68,29 +68,14 @@ class DendriteMixin: Methods: __str__(): Returns a string representation of the Dendrite object. __repr__(): Returns a string representation of the Dendrite object, acting as a fallback for __str__(). - query(self, *args, **kwargs) -> Union[Synapse, List[Synapse]]: - Makes synchronous requests to one or multiple target Axons and returns responses. - - forward(self, axons, synapse=Synapse(), timeout=12, deserialize=True, run_async=True, streaming=False) -> Synapse: - Asynchronously sends requests to one or multiple Axons and collates their responses. - - call(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> Synapse: - Asynchronously sends a request to a specified Axon and processes the response. - - call_stream(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> AsyncGenerator[Synapse, None]: - Sends a request to a specified Axon and yields an AsyncGenerator that contains streaming response chunks before finally yielding the filled Synapse as the final element. - - preprocess_synapse_for_request(self, target_axon_info, synapse, timeout=12.0) -> Synapse: - Preprocesses the synapse for making a request, including building headers and signing. - - process_server_response(self, server_response, json_response, local_synapse): - Processes the server response, updates the local synapse state, and merges headers. - - close_session(self): - Synchronously closes the internal aiohttp client session. - - aclose_session(self): - Asynchronously closes the internal aiohttp client session. + query(self, *args, **kwargs) -> Union[Synapse, List[Synapse]]: Makes synchronous requests to one or multiple target Axons and returns responses. + forward(self, axons, synapse=Synapse(), timeout=12, deserialize=True, run_async=True, streaming=False) -> Synapse: Asynchronously sends requests to one or multiple Axons and collates their responses. + call(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> Synapse: Asynchronously sends a request to a specified Axon and processes the response. + call_stream(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> AsyncGenerator[Synapse, None]: Sends a request to a specified Axon and yields an AsyncGenerator that contains streaming response chunks before finally yielding the filled Synapse as the final element. + preprocess_synapse_for_request(self, target_axon_info, synapse, timeout=12.0) -> Synapse: Preprocesses the synapse for making a request, including building headers and signing. + process_server_response(self, server_response, json_response, local_synapse): Processes the server response, updates the local synapse state, and merges headers. + close_session(self): Synchronously closes the internal aiohttp client session. + aclose_session(self): Asynchronously closes the internal aiohttp client session. NOTE: When working with async `aiohttp `_ client sessions, it is recommended to use a context manager. @@ -347,11 +332,10 @@ def query( Cleanup is automatically handled and sessions are closed upon completed requests. Args: - axons (Union[List[Union['bittensor.AxonInfo', 'bittensor.axon']], Union['bittensor.AxonInfo', 'bittensor.axon']]): - The list of target Axon information. + axons (Union[List[Union['bittensor.AxonInfo', 'bittensor.axon']], Union['bittensor.AxonInfo', 'bittensor.axon']]): The list of target Axon information. synapse (Synapse, optional): The Synapse object. Defaults to :func:`Synapse()`. - timeout (float, optional): The request timeout duration in seconds. - Defaults to ``12.0`` seconds. + timeout (float, optional): The request timeout duration in seconds. Defaults to ``12.0`` seconds. + Returns: Union[Synapse, List[Synapse]]: If a single target axon is provided, returns the response from that axon. If multiple target axons are provided, returns a list of responses from all target axons. """ @@ -463,19 +447,13 @@ async def single_axon_response( """ Manages the request and response process for a single axon, supporting both streaming and non-streaming modes. - This function is responsible for initiating a request to a single axon. Depending on the - ``is_stream`` flag, it either uses ``call_stream`` for streaming responses or ``call`` for - standard responses. The function handles the response processing, catering to the specifics - of streaming or non-streaming data. + This function is responsible for initiating a request to a single axon. Depending on the ``is_stream`` flag, it either uses ``call_stream`` for streaming responses or ``call`` for standard responses. The function handles the response processing, catering to the specifics of streaming or non-streaming data. Args: target_axon: The target axon object to which the request is to be sent. This object contains the necessary information like IP address and port to formulate the request. Returns: - Union[AsyncGenerator, Synapse, bittensor.StreamingSynapse]: The response - from the targeted axon. In streaming mode, an AsyncGenerator is returned, yielding - data chunks. In non-streaming mode, a Synapse or StreamingSynapse object is returned - containing the response. + Union[AsyncGenerator, Synapse, bittensor.StreamingSynapse]: The response from the targeted axon. In streaming mode, an AsyncGenerator is returned, yielding data chunks. In non-streaming mode, a Synapse or StreamingSynapse object is returned containing the response. """ if is_stream: # If in streaming mode, return the async_generator @@ -519,9 +497,7 @@ async def call( """ Asynchronously sends a request to a specified Axon and processes the response. - This function establishes a connection with a specified Axon, sends the encapsulated - data through the Synapse object, waits for a response, processes it, and then - returns the updated Synapse object. + This function establishes a connection with a specified Axon, sends the encapsulated data through the Synapse object, waits for a response, processes it, and then returns the updated Synapse object. Args: target_axon (Union['bittensor.AxonInfo', 'bittensor.axon']): The target Axon to send the request to. @@ -665,14 +641,12 @@ def preprocess_synapse_for_request( timeout: float = 12.0, ) -> "Synapse": """ - Preprocesses the synapse for making a request. This includes building - headers for Dendrite and Axon and signing the request. + Preprocesses the synapse for making a request. This includes building headers for Dendrite and Axon and signing the request. Args: target_axon_info (bittensor.AxonInfo): The target axon information. synapse (Synapse): The synapse object to be preprocessed. - timeout (float, optional): The request timeout duration in seconds. - Defaults to ``12.0`` seconds. + timeout (float, optional): The request timeout duration in seconds. Defaults to ``12.0`` seconds. Returns: Synapse: The preprocessed synapse. @@ -707,8 +681,7 @@ def process_server_response( local_synapse: Synapse, ): """ - Processes the server response, updates the local synapse state with the - server's state and merges headers set by the server. + Processes the server response, updates the local synapse state with the server's state and merges headers set by the server. Args: server_response (object): The `aiohttp `_ response object from the server. @@ -784,16 +757,13 @@ async def __aenter__(self): """ Asynchronous context manager entry method. - Enables the use of the ``async with`` statement with the Dendrite instance. When entering the context, - the current instance of the class is returned, making it accessible within the asynchronous context. + Enables the use of the ``async with`` statement with the Dendrite instance. When entering the context, the current instance of the class is returned, making it accessible within the asynchronous context. Returns: Dendrite: The current instance of the Dendrite class. Usage:: - - async with Dendrite() as dendrite: - await dendrite.some_async_method() + async with Dendrite() as dendrite: await dendrite.some_async_method() """ return self @@ -801,8 +771,7 @@ async def __aexit__(self, exc_type, exc_value, traceback): """ Asynchronous context manager exit method. - Ensures proper cleanup when exiting the ``async with`` context. This method will close the `aiohttp `_ client session - asynchronously, releasing any tied resources. + Ensures proper cleanup when exiting the ``async with`` context. This method will close the `aiohttp `_ client session asynchronously, releasing any tied resources. Args: exc_type (Type[BaseException], optional): The type of exception that was raised. @@ -811,8 +780,7 @@ async def __aexit__(self, exc_type, exc_value, traceback): Usage:: - async with bt.dendrite( wallet ) as dendrite: - await dendrite.some_async_method() + async with bt.dendrite( wallet ) as dendrite: await dendrite.some_async_method() Note: This automatically closes the session by calling :func:`__aexit__` after the context closes. @@ -823,8 +791,7 @@ def __del__(self): """ Dendrite destructor. - This method is invoked when the Dendrite instance is about to be destroyed. The destructor ensures that the - aiohttp client session is closed before the instance is fully destroyed, releasing any remaining resources. + This method is invoked when the Dendrite instance is about to be destroyed. The destructor ensures that the aiohttp client session is closed before the instance is fully destroyed, releasing any remaining resources. Note: Relying on the destructor for cleanup can be unpredictable. It is recommended to explicitly close sessions using the provided methods or the ``async with`` context manager. diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index a550a2bd3..fdb46dcbe 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -523,8 +523,8 @@ def sync( subtensor = self._initialize_subtensor(subtensor) if ( - subtensor.chain_endpoint != settings.archive_entrypoint - or subtensor.network != settings.networks[3] + subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT + or subtensor.network != settings.NETWORKS[3] ): cur_block = subtensor.get_current_block() if block and block < (cur_block - 300): @@ -941,7 +941,7 @@ def __init__( if sync: self.sync(block=None, lite=lite) - def _set_metagraph_attributes(self, block, subtensor): + def _set_metagraph_attributes(self, block, subtensor: "Subtensor"): """ Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 75781d500..5ae9a888e 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -15,7 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -__version__ = "7.3.0" +__version__ = "7.5.0" import os import re @@ -60,44 +60,45 @@ def turn_console_on(): WALLETS_DIR = USER_BITTENSOR_DIR / "wallets" MINERS_DIR = USER_BITTENSOR_DIR / "miners" +# Bittensor networks name +NETWORKS = ["local", "finney", "test", "archive"] + DEFAULT_ENDPOINT = "wss://entrypoint-finney.opentensor.ai:443" -DEFAULT_NETWORK = "finney" +DEFAULT_NETWORK = NETWORKS[1] # Create dirs if they don't exist WALLETS_DIR.mkdir(parents=True, exist_ok=True) MINERS_DIR.mkdir(parents=True, exist_ok=True) -# Bittensor networks name -networks = ["local", "finney", "test", "archive"] # Bittensor endpoints (Needs to use wss://) -finney_entrypoint = "wss://entrypoint-finney.opentensor.ai:443" -finney_test_entrypoint = "wss://test.finney.opentensor.ai:443/" -archive_entrypoint = "wss://archive.chain.opentensor.ai:443/" -local_entrypoint = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or "ws://127.0.0.1:9944" +FINNEY_ENTRYPOINT = "wss://entrypoint-finney.opentensor.ai:443" +FINNEY_TEST_ENTRYPOINT = "wss://test.finney.opentensor.ai:443/" +ARCHIVE_ENTRYPOINT = "wss://archive.chain.opentensor.ai:443/" +LOCAL_ENTRYPOINT = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or "ws://127.0.0.1:9944" # Currency Symbols Bittensor -tao_symbol: str = chr(0x03C4) -rao_symbol: str = chr(0x03C1) +TAO_SYMBOL: str = chr(0x03C4) +RAO_SYMBOL: str = chr(0x03C1) # Pip address for versioning -pipaddress = "https://pypi.org/pypi/bittensor/json" +PIPADDRESS = "https://pypi.org/pypi/bittensor/json" # Substrate chain block time (seconds). -blocktime = 12 +BLOCKTIME = 12 # Substrate ss58_format -ss58_format = 42 +SS58_FORMAT = 42 # Wallet ss58 address length -ss58_address_length = 48 +SS58_ADDRESS_LENGTH = 48 # Raw GitHub url for delegates registry file -delegates_details_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" +DELEGATES_DETAILS_URL = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" # Block Explorers map network to explorer url # Must all be polkadotjs explorer urls -network_explorer_map = { +NETWORK_EXPLORER_MAP = { "opentensor": { "local": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", "endpoint": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", @@ -111,7 +112,7 @@ def turn_console_on(): } # --- Type Registry --- -type_registry: dict = { +TYPE_REGISTRY: dict = { "types": { "Balance": "u64", # Need to override default u128 }, @@ -142,41 +143,6 @@ def turn_console_on(): }, } }, - "StakeInfoRuntimeApi": { - "methods": { - "get_stake_info_for_coldkey": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_stake_info_for_coldkeys": { - "params": [ - { - "name": "coldkey_account_vecs", - "type": "Vec>", - }, - ], - "type": "Vec", - }, - }, - }, - "ValidatorIPRuntimeApi": { - "methods": { - "get_associated_validator_ip_info_for_subnet": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - }, - }, "SubnetInfoRuntimeApi": { "methods": { "get_subnet_hyperparams": { @@ -193,41 +159,10 @@ def turn_console_on(): "SubnetRegistrationRuntimeApi": { "methods": {"get_network_registration_cost": {"params": [], "type": "u64"}} }, - "ColdkeySwapRuntimeApi": { - "methods": { - "get_scheduled_coldkey_swap": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_remaining_arbitration_period": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_coldkey_swap_destinations": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - } - }, }, } -defaults = Munch = munchify( +DEFAULTS = munchify( { "axon": { "port": os.getenv("BT_AXON_PORT") or 8091, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 5f914d68f..ce69640cb 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -24,7 +24,6 @@ import copy import socket import sys -import time from typing import List, Dict, Union, Optional, Tuple, TypedDict, Any import numpy as np @@ -35,41 +34,10 @@ from scalecodec.base import RuntimeConfiguration from scalecodec.exceptions import RemainingScaleBytesNotEmptyException from scalecodec.type_registry import load_type_registry_preset -from scalecodec.types import GenericCall, ScaleType -from substrateinterface.base import QueryMapResult, SubstrateInterface, ExtrinsicReceipt -from substrateinterface.exceptions import SubstrateRequestException +from scalecodec.types import ScaleType +from substrateinterface.base import QueryMapResult, SubstrateInterface -from bittensor.api.extrinsics.commit_weights import ( - commit_weights_extrinsic, - reveal_weights_extrinsic, -) -from bittensor.api.extrinsics.delegation import ( - delegate_extrinsic, - nominate_extrinsic, - undelegate_extrinsic, - increase_take_extrinsic, - decrease_take_extrinsic, -) -from bittensor.api.extrinsics.network import ( - register_subnetwork_extrinsic, - set_hyperparameter_extrinsic, -) from bittensor.api.extrinsics.prometheus import prometheus_extrinsic -from bittensor.api.extrinsics.registration import ( - register_extrinsic, - burned_register_extrinsic, - run_faucet_extrinsic, - swap_hotkey_extrinsic, -) -from bittensor.api.extrinsics.root import ( - root_register_extrinsic, - set_root_weights_extrinsic, -) -from bittensor.api.extrinsics.senate import ( - register_senate_extrinsic, - leave_senate_extrinsic, - vote_senate_extrinsic, -) from bittensor.api.extrinsics.serving import ( serve_extrinsic, serve_axon_extrinsic, @@ -77,52 +45,24 @@ get_metadata, ) from bittensor.api.extrinsics.set_weights import set_weights_extrinsic -from bittensor.api.extrinsics.staking import ( - add_stake_extrinsic, - add_stake_multiple_extrinsic, -) from bittensor.api.extrinsics.transfer import transfer_extrinsic -from bittensor.api.extrinsics.unstaking import ( - unstake_extrinsic, - unstake_multiple_extrinsic, -) from bittensor.core import settings from bittensor.core.axon import Axon from bittensor.core.chain_data import ( - DelegateInfoLite, NeuronInfo, - DelegateInfo, PrometheusInfo, - SubnetInfo, SubnetHyperparameters, - StakeInfo, NeuronInfoLite, - AxonInfo, - ProposalVoteData, - IPInfo, custom_rpc_type_registry, ) from bittensor.core.config import Config -from bittensor.core.errors import IdentityError, NominationError, StakeError, TakeError from bittensor.core.metagraph import Metagraph from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams -from bittensor.utils import ( - U16_NORMALIZED_FLOAT, - ss58_to_vec_u8, - U64_NORMALIZED_FLOAT, - networking, -) -from bittensor.utils import ( - torch, - weight_utils, - format_error_message, - create_identity_dict, - decode_hex_identity_dict, -) +from bittensor.utils import u16_normalized_float, networking +from bittensor.utils import torch, format_error_message from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging -from bittensor.utils.registration import POWSolution -from bittensor.utils.registration import legacy_torch_api_compat + KEY_NONCE: Dict[str, int] = {} @@ -198,10 +138,7 @@ def __init__( NOTE: Currently subtensor defaults to the ``finney`` network. This will change in a future release. - We strongly encourage users to run their own local subtensor node whenever possible. This increases - decentralization and resilience of the network. In a future release, local subtensor will become the - default and the fallback to ``finney`` removed. Please plan ahead for this change. We will provide detailed - instructions on how to run a local subtensor node in the documentation in a subsequent release. + We strongly encourage users to run their own local subtensor node whenever possible. This increases decentralization and resilience of the network. In a future release, local subtensor will become the default and the fallback to ``finney`` removed. Please plan ahead for this change. We will provide detailed instructions on how to run a local subtensor node in the documentation in a subsequent release. Args: network (str, optional): The network name to connect to (e.g., ``finney``, ``local``). This can also be the chain endpoint (e.g., ``wss://entrypoint-finney.opentensor.ai:443``) and will be correctly parsed into the network and chain endpoint. If not specified, defaults to the main Bittensor network. @@ -209,7 +146,6 @@ def __init__( _mock (bool, optional): If set to ``True``, uses a mocked connection for testing purposes. This initialization sets up the connection to the specified Bittensor network, allowing for various blockchain operations such as neuron registration, stake management, and setting weights. - """ # Determine config.subtensor.chain_endpoint and config.subtensor.network config. # If chain_endpoint is set, we override the network flag, otherwise, the chain_endpoint is assigned by the @@ -218,14 +154,16 @@ def __init__( if config is None: config = Subtensor.config() - self.config = copy.deepcopy(config) # type: ignore + self._config = copy.deepcopy(config) # Setup config.subtensor.network and config.subtensor.chain_endpoint - self.chain_endpoint, self.network = Subtensor.setup_config(network, config) # type: ignore + self.chain_endpoint, self.network = Subtensor.setup_config( + network, self._config + ) if ( self.network == "finney" - or self.chain_endpoint == settings.finney_entrypoint + or self.chain_endpoint == settings.FINNEY_ENTRYPOINT ) and log_verbose: logging.info( f"You are connecting to {self.network} network with endpoint {self.chain_endpoint}." @@ -243,10 +181,10 @@ def __init__( try: # Set up params. self.substrate = SubstrateInterface( - ss58_format=settings.ss58_format, + ss58_format=settings.SS58_FORMAT, use_remote_preset=True, url=self.chain_endpoint, - type_registry=settings.type_registry, + type_registry=settings.TYPE_REGISTRY, ) except ConnectionRefusedError: logging.error( @@ -286,128 +224,24 @@ def __str__(self) -> str: def __repr__(self) -> str: return self.__str__() + def close(self): + """Cleans up resources for this subtensor instance like active websocket connection and active extensions.""" + self.substrate.close() + @staticmethod def config() -> "Config": """ Creates and returns a Bittensor configuration object. Returns: - config (bittensor.core.config.Config): A Bittensor configuration object configured with arguments added by the - `subtensor.add_args` method. + config (bittensor.core.config.Config): A Bittensor configuration object configured with arguments added by the `subtensor.add_args` method. """ parser = argparse.ArgumentParser() Subtensor.add_args(parser) return Config(parser, args=[]) - @classmethod - def help(cls): - """Print help to stdout.""" - parser = argparse.ArgumentParser() - cls.add_args(parser) - print(cls.__new__.__doc__) - parser.print_help() - - @classmethod - def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = None): - """ - Adds command-line arguments to the provided ArgumentParser for configuring the Subtensor settings. - - Args: - parser (argparse.ArgumentParser): The ArgumentParser object to which the Subtensor arguments will be added. - prefix (Optional[str]): An optional prefix for the argument names. If provided, the prefix is prepended to - each argument name. - - Arguments added: - --subtensor.network: The Subtensor network flag. Possible values are 'finney', 'test', 'archive', and - 'local'. Overrides the chain endpoint if set. - --subtensor.chain_endpoint: The Subtensor chain endpoint flag. If set, it overrides the network flag. - --subtensor._mock: If true, uses a mocked connection to the chain. - - Example: - parser = argparse.ArgumentParser() - Subtensor.add_args(parser) - """ - prefix_str = "" if prefix is None else f"{prefix}." - try: - default_network = settings.networks[1] - default_chain_endpoint = settings.finney_entrypoint - - parser.add_argument( - f"--{prefix_str}subtensor.network", - default=default_network, - type=str, - help="""The subtensor network flag. The likely choices are: - -- finney (main network) - -- test (test network) - -- archive (archive network +300 blocks) - -- local (local running network) - If this option is set it overloads subtensor.chain_endpoint with - an entry point node from that network. - """, - ) - parser.add_argument( - f"--{prefix_str}subtensor.chain_endpoint", - default=default_chain_endpoint, - type=str, - help="""The subtensor endpoint flag. If set, overrides the --network flag.""", - ) - parser.add_argument( - f"--{prefix_str}subtensor._mock", - default=False, - type=bool, - help="""If true, uses a mocked connection to the chain.""", - ) - - except argparse.ArgumentError: - # re-parsing arguments. - pass - - @staticmethod - def determine_chain_endpoint_and_network(network: str): - """Determines the chain endpoint and network from the passed network or chain_endpoint. - - Args: - network (str): The network flag. The choices are: ``-- finney`` (main network), ``-- archive`` - (archive network +300 blocks), ``-- local`` (local running network), ``-- test`` (test network). - Returns: - network (str): The network flag. - chain_endpoint (str): The chain endpoint flag. If set, overrides the ``network`` argument. - """ - if network is None: - return None, None - if network in ["finney", "local", "test", "archive"]: - if network == "finney": - # Kiru Finney staging network. - return network, settings.finney_entrypoint - elif network == "local": - return network, settings.local_entrypoint - elif network == "test": - return network, settings.finney_test_entrypoint - elif network == "archive": - return network, settings.archive_entrypoint - else: - if ( - network == settings.finney_entrypoint - or "entrypoint-finney.opentensor.ai" in network - ): - return "finney", settings.finney_entrypoint - elif ( - network == settings.finney_test_entrypoint - or "test.finney.opentensor.ai" in network - ): - return "test", settings.finney_test_entrypoint - elif ( - network == settings.archive_entrypoint - or "archive.chain.opentensor.ai" in network - ): - return "archive", settings.archive_entrypoint - elif "127.0.0.1" in network or "localhost" in network: - return "local", network - else: - return "unknown", network - @staticmethod - def setup_config(network: str, config: "Config"): + def setup_config(network: Optional[str], config: "Config"): """ Sets up and returns the configuration for the Subtensor network and endpoint. @@ -420,8 +254,7 @@ def setup_config(network: str, config: "Config"): 5. Default network. Args: - network (str): The name of the Subtensor network. If None, the network and endpoint will be determined from - the `config` object. + network (str): The name of the Subtensor network. If None, the network and endpoint will be determined from the `config` object. config (bittensor.core.config.Config): The configuration object containing the network and chain endpoint settings. Returns: @@ -470,7 +303,7 @@ def setup_config(network: str, config: "Config"): evaluated_network, evaluated_endpoint, ) = Subtensor.determine_chain_endpoint_and_network( - settings.defaults.subtensor.network + settings.DEFAULTS.subtensor.network ) return ( @@ -478,4250 +311,838 @@ def setup_config(network: str, config: "Config"): evaluated_network, ) - def close(self): - """Cleans up resources for this subtensor instance like active websocket connection and active extensions.""" - self.substrate.close() + @classmethod + def help(cls): + """Print help to stdout.""" + parser = argparse.ArgumentParser() + cls.add_args(parser) + print(cls.__new__.__doc__) + parser.print_help() - ############## - # Delegation # - ############## - def nominate( - self, - wallet: "Wallet", - wait_for_finalization: bool = False, - wait_for_inclusion: bool = True, - ) -> bool: + @classmethod + def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = None): """ - Becomes a delegate for the hotkey associated with the given wallet. This method is used to nominate - a neuron (identified by the hotkey in the wallet) as a delegate on the Bittensor network, allowing it - to participate in consensus and validation processes. + Adds command-line arguments to the provided ArgumentParser for configuring the Subtensor settings. Args: - wallet (bittensor_wallet.Wallet): The wallet containing the hotkey to be nominated. - wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the blockchain. - wait_for_inclusion (bool, optional): If ``True``, waits until the transaction is included in a block. + parser (argparse.ArgumentParser): The ArgumentParser object to which the Subtensor arguments will be added. + prefix (Optional[str]): An optional prefix for the argument names. If provided, the prefix is prepended to each argument name. - Returns: - bool: ``True`` if the nomination process is successful, ``False`` otherwise. + Arguments added: + --subtensor.network: The Subtensor network flag. Possible values are 'finney', 'test', 'archive', and 'local'. Overrides the chain endpoint if set. + --subtensor.chain_endpoint: The Subtensor chain endpoint flag. If set, it overrides the network flag. + --subtensor._mock: If true, uses a mocked connection to the chain. - This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the - dynamic selection and participation of validators in the network's consensus process. + Example: + parser = argparse.ArgumentParser() + Subtensor.add_args(parser) """ - return nominate_extrinsic( - subtensor=self, - wallet=wallet, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - ) + prefix_str = "" if prefix is None else f"{prefix}." + try: + default_network = settings.DEFAULT_NETWORK + default_chain_endpoint = settings.FINNEY_ENTRYPOINT + + parser.add_argument( + f"--{prefix_str}subtensor.network", + default=default_network, + type=str, + help="""The subtensor network flag. The likely choices are: + -- finney (main network) + -- test (test network) + -- archive (archive network +300 blocks) + -- local (local running network) + If this option is set it overloads subtensor.chain_endpoint with + an entry point node from that network. + """, + ) + parser.add_argument( + f"--{prefix_str}subtensor.chain_endpoint", + default=default_chain_endpoint, + type=str, + help="""The subtensor endpoint flag. If set, overrides the --network flag.""", + ) + parser.add_argument( + f"--{prefix_str}subtensor._mock", + default=False, + type=bool, + help="""If true, uses a mocked connection to the chain.""", + ) + + except argparse.ArgumentError: + # re-parsing arguments. + pass - def delegate( + # Inner private functions + def _encode_params( self, - wallet: "Wallet", - delegate_ss58: Optional[str] = None, - amount: Optional[Union["Balance", float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Becomes a delegate for the hotkey associated with the given wallet. This method is used to nominate - a neuron (identified by the hotkey in the wallet) as a delegate on the Bittensor network, allowing it - to participate in consensus and validation processes. + call_definition: List["ParamWithTypes"], + params: Union[List[Any], Dict[str, Any]], + ) -> str: + """Returns a hex encoded string of the params using their types.""" + param_data = scalecodec.ScaleBytes(b"") - Args: - wallet (bittensor_wallet.Wallet): The wallet containing the hotkey to be nominated. - delegate_ss58 (Optional[str]): The ``SS58`` address of the delegate neuron. - amount (Union[Balance, float]): The amount of TAO to undelegate. - wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the - blockchain. - wait_for_inclusion (bool, optional): If ``True``, waits until the transaction is included in a block. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + for i, param in enumerate(call_definition["params"]): # type: ignore + scale_obj = self.substrate.create_scale_object(param["type"]) + if type(params) is list: + param_data += scale_obj.encode(params[i]) + else: + if param["name"] not in params: + raise ValueError(f"Missing param {param['name']} in params dict.") - Returns: - bool: ``True`` if the nomination process is successful, False otherwise. + param_data += scale_obj.encode(params[param["name"]]) - This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the - dynamic selection and participation of validators in the network's consensus process. - """ - return delegate_extrinsic( - subtensor=self, - wallet=wallet, - delegate_ss58=delegate_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) + return param_data.to_hex() - def undelegate( - self, - wallet: "Wallet", - delegate_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: + def _get_hyperparameter( + self, param_name: str, netuid: int, block: Optional[int] = None + ) -> Optional[Any]: """ - Removes a specified amount of stake from a delegate neuron using the provided wallet. This action - reduces the staked amount on another neuron, effectively withdrawing support or speculation. + Retrieves a specified hyperparameter for a specific subnet. Args: - wallet (bittensor_wallet.Wallet): The wallet used for the undelegation process. - delegate_ss58 (Optional[str]): The ``SS58`` address of the delegate neuron. - amount (Union[Balance, float]): The amount of TAO to undelegate. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + param_name (str): The name of the hyperparameter to retrieve. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. Returns: - bool: ``True`` if the undelegation is successful, False otherwise. - - This function reflects the dynamic and speculative nature of the Bittensor network, allowing neurons - to adjust their stakes and investments based on changing perceptions and performances within the network. + Optional[Union[int, float]]: The value of the specified hyperparameter if the subnet exists, ``None`` otherwise. """ - return undelegate_extrinsic( - subtensor=self, - wallet=wallet, - delegate_ss58=delegate_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) + if not self.subnet_exists(netuid, block): + return None - def set_take( - self, - wallet: "Wallet", - delegate_ss58: Optional[str] = None, - take: float = 0.0, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: + result = self.query_subtensor(param_name, block, [netuid]) + if result is None or not hasattr(result, "value"): + return None + + return result.value + + # Calls methods + def query_subtensor( + self, name: str, block: Optional[int] = None, params: Optional[list] = None + ) -> "ScaleType": """ - Set delegate hotkey take + Queries named storage from the Subtensor module on the Bittensor blockchain. This function is used to retrieve specific data or parameters from the blockchain, such as stake, rank, or other neuron-specific attributes. + Args: - wallet (bittensor_wallet.Wallet): The wallet containing the hotkey to be nominated. - delegate_ss58 (str, optional): Hotkey - take (float): Delegate take on subnet ID - wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the - blockchain. - wait_for_inclusion (bool, optional): If ``True``, waits until the transaction is included in a block. + name (str): The name of the storage function to query. + block (Optional[int]): The blockchain block number at which to perform the query. + params (Optional[List[object]], optional): A list of parameters to pass to the query function. Returns: - bool: ``True`` if the process is successful, False otherwise. + query_response (ScaleType): An object containing the requested data. - This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the - dynamic selection and participation of validators in the network's consensus process. - """ - # Ensure delegate_ss58 is not None - if delegate_ss58 is None: - raise ValueError("delegate_ss58 cannot be None") - - # Calculate u16 representation of the take - takeu16 = int(take * 0xFFFF) - - # Check if the new take is greater or lower than existing take or if existing is set - delegate = self.get_delegate_by_hotkey(delegate_ss58) - current_take = None - if delegate is not None: - current_take = int(float(delegate.take) * 65535.0) - - if takeu16 == current_take: - settings.bt_console.print("Nothing to do, take hasn't changed") - return True - if current_take is None or current_take < takeu16: - settings.bt_console.print( - "Current take is either not set or is lower than the new one. Will use increase_take" - ) - return increase_take_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=delegate_ss58, - take=takeu16, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - else: - settings.bt_console.print( - "Current take is higher than the new one. Will use decrease_take" - ) - return decrease_take_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=delegate_ss58, - take=takeu16, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - def send_extrinsic( - self, - wallet: "Wallet", - module: str, - function: str, - params: dict, - period: int = 5, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - max_retries: int = 3, - wait_time: int = 3, - max_wait: int = 20, - ) -> Optional[ExtrinsicReceipt]: - """ - Sends an extrinsic to the Bittensor blockchain using the provided wallet and parameters. This method - constructs and submits the extrinsic, handling retries and blockchain communication. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the extrinsic. - module (str): The module name for the extrinsic. - function (str): The function name for the extrinsic. - params (dict): The parameters for the extrinsic. - period (int, optional): The number of blocks for the extrinsic to live in the mempool. Defaults to 5. - 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. - max_retries (int, optional): The maximum number of retries for the extrinsic. Defaults to 3. - wait_time (int, optional): The wait time between retries. Defaults to 3. - max_wait (int, optional): The maximum wait time for the extrinsic. Defaults to 20. - - Returns: - Optional[ExtrinsicReceipt]: The receipt of the extrinsic if successful, None otherwise. - """ - call = self.substrate.compose_call( - call_module=module, - call_function=function, - call_params=params, - ) - - hotkey = wallet.get_hotkey().ss58_address - # Periodically update the nonce cache - if hotkey not in KEY_NONCE or self.get_current_block() % 5 == 0: - KEY_NONCE[hotkey] = self.substrate.get_account_nonce(hotkey) - - nonce = KEY_NONCE[hotkey] - - # <3 parity tech - old_init_runtime = self.substrate.init_runtime - self.substrate.init_runtime = lambda: None - self.substrate.init_runtime = old_init_runtime - response = None - - for attempt in range(1, max_retries + 1): - try: - # Create the extrinsic with new nonce - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - era={"period": period}, - nonce=nonce, - ) - - # Submit the extrinsic - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # Return immediately if we don't wait - if not wait_for_inclusion and not wait_for_finalization: - KEY_NONCE[hotkey] = nonce + 1 # update the nonce cache - return response - - # If we wait for finalization or inclusion, check if it is successful - if response.is_success: - KEY_NONCE[hotkey] = nonce + 1 # update the nonce cache - return response - else: - # Wait for a while - wait = min(wait_time * attempt, max_wait) - time.sleep(wait) - # Incr the nonce and try again - nonce = nonce + 1 - continue - - # This dies because user is spamming... incr and try again - except SubstrateRequestException as e: - if "Priority is too low" in e.args[0]["message"]: - wait = min(wait_time * attempt, max_wait) - logging.warning( - f"Priority is too low, retrying with new nonce: {nonce} in {wait} seconds." - ) - nonce = nonce + 1 - time.sleep(wait) - continue - else: - logging.error(f"Error sending extrinsic: {e}") - response = None - - return response - - ############### - # Set Weights # - ############### - # TODO: still needed? Can't find any usage of this method. - def set_weights( - self, - wallet: "Wallet", - netuid: int, - uids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], - version_key: int = settings.version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, - max_retries: int = 5, - ) -> Tuple[bool, str]: - """ - Sets the inter-neuronal weights for the specified neuron. This process involves specifying the - influence or trust a neuron places on other neurons in the network, which is a fundamental aspect - of Bittensor's decentralized learning architecture. - - Args: - 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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_retries (int, optional): The number of maximum attempts to set weights. (Default: 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. - - 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】. - """ - uid = self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to set weights!" - while ( - self.blocks_since_last_update(netuid, uid) > self.weights_rate_limit(netuid) # type: ignore - and retries < max_retries - ): - try: - success, message = set_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - except Exception as e: - logging.error(f"Error setting weights: {e}") - finally: - retries += 1 - - return success, message - - def _do_set_weights( - self, - wallet: "Wallet", - uids: List[int], - vals: List[int], - netuid: int, - version_key: int = settings.version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[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. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. - uids (List[int]): List of neuron UIDs for which weights are being set. - vals (List[int]): List of weight values corresponding to each UID. - netuid (int): Unique identifier for the network. - 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. - - Returns: - Tuple[bool, Optional[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. + This query function is essential for accessing detailed information about the network and its neurons, providing valuable insights into the state and dynamics of the Bittensor ecosystem. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_weights", - call_params={ - "dests": uids, - "weights": vals, - "netuid": netuid, - "version_key": version_key, - }, - ) - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - era={"period": 5}, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + def make_substrate_call_with_retry() -> "ScaleType": + return self.substrate.query( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), ) - # 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." - - response.process_events() - if response.is_success: - return True, "Successfully set weights." - else: - return False, format_error_message(response.error_message) return make_substrate_call_with_retry() - ################## - # Commit Weights # - ################## - def commit_weights( - self, - wallet: "Wallet", - netuid: int, - salt: List[int], - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], - version_key: int = settings.version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, - max_retries: int = 5, - ) -> Tuple[bool, str]: - """ - Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. - This action serves as a commitment or snapshot of the neuron's current weight distribution. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the weights. - netuid (int): The unique identifier of the subnet. - salt (List[int]): list of randomly generated integers as salt to generated weighted hash. - 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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_retries (int, optional): The number of maximum attempts to commit weights. (Default: 5) - - Returns: - Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. - """ - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to commit weights!" - - logging.info( - "Committing weights with params: netuid={}, uids={}, weights={}, version_key={}".format( - netuid, uids, weights, version_key - ) - ) - - # Generate the hash of the weights - commit_hash = weight_utils.generate_weight_hash( - address=wallet.hotkey.ss58_address, - netuid=netuid, - uids=list(uids), - values=list(weights), - salt=salt, - version_key=version_key, - ) - - logging.info("Commit Hash: {}".format(commit_hash)) - - while retries < max_retries: - try: - success, message = commit_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - commit_hash=commit_hash, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - if success: - break - except Exception as e: - logging.error(f"Error committing weights: {e}") - finally: - retries += 1 - - return success, message - - def _do_commit_weights( - self, - wallet: "Wallet", - netuid: int, - commit_hash: str, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[str]]: + def query_map_subtensor( + self, name: str, block: Optional[int] = None, params: Optional[list] = None + ) -> "QueryMapResult": """ - 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. + Queries map storage from the Subtensor module on the Bittensor blockchain. This function is designed to retrieve a map-like data structure, which can include various neuron-specific details or network-wide attributes. Args: - 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, 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. + name (str): The name of the map storage function to query. + block (Optional[int]): The blockchain block number at which to perform the query. + params (Optional[List[object]], optional): A list of parameters to pass to the query function. Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + QueryMapResult: An object containing the map-like data structure, or ``None`` if not found. - 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. + This function is particularly useful for analyzing and understanding complex network structures and relationships within the Bittensor ecosystem, such as inter-neuronal connections and stake distributions. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="commit_weights", - call_params={ - "netuid": netuid, - "commit_hash": commit_hash, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + return self.substrate.query_map( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), ) - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message - return make_substrate_call_with_retry() - ################## - # Reveal Weights # - ################## - def reveal_weights( - self, - wallet: "Wallet", - netuid: int, - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], - salt: Union[NDArray[np.int64], list], - version_key: int = settings.version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, - max_retries: int = 5, - ) -> Tuple[bool, str]: - """ - Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. - This action serves as a revelation of the neuron's previously committed weight distribution. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. - netuid (int): The unique identifier of the subnet. - uids (np.ndarray): NumPy array of neuron UIDs for which weights are being revealed. - 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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_retries (int, optional): The number of maximum attempts to reveal weights. (Default: 5) - - Returns: - Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string - value describing the success or potential error. - - This function allows neurons to reveal their previously committed weight distribution, ensuring transparency - and accountability within the Bittensor network. - """ - - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to reveal weights!" - - while retries < max_retries: - try: - success, message = reveal_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=list(uids), - weights=list(weights), - salt=list(salt), - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - if success: - break - except Exception as e: - logging.error(f"Error revealing weights: {e}") - finally: - retries += 1 - - return success, message - - def _do_reveal_weights( - self, - wallet: "Wallet", - netuid: int, - uids: List[int], - values: List[int], - salt: List[int], - version_key: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[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. - - Args: - 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. - values (List[int]): List of weight values corresponding to each UID. - salt (List[int]): List of salt values corresponding to the hash function. - version_key (int): 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. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency - and accountability for the neuron's weight distribution. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="reveal_weights", - call_params={ - "netuid": netuid, - "uids": uids, - "values": values, - "salt": salt, - "version_key": version_key, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - response.process_events() - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - - return make_substrate_call_with_retry() - - ################ - # Registration # - ################ - def register( - self, - wallet: "Wallet", - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[List[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, - ) -> bool: - """ - Registers a neuron on the Bittensor network using the provided wallet. Registration - is a critical step for a neuron to become an active participant in the network, enabling - it to stake, set weights, and receive incentives. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered. - netuid (int): The unique identifier of the subnet. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. - Defaults to `False`. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. - Defaults to `True`. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_allowed_attempts (int): Maximum number of attempts to register the wallet. - output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning - the progress is printed on the same lines. Defaults to `True`. - cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). Defaults to `False`. - dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. Defaults to `0` (zero). - tpb (int): The number of threads per block (CUDA). Default to `256`. - 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`. - - Returns: - bool: ``True`` if the registration is successful, False otherwise. - - This function facilitates the entry of new neurons into the network, supporting the decentralized - growth and scalability of the Bittensor ecosystem. - """ - return register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - max_allowed_attempts=max_allowed_attempts, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - def swap_hotkey( - self, - wallet: "Wallet", - new_wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - ) -> bool: - """ - Swaps an old hotkey with a new hotkey for the specified wallet. - - This method initiates an extrinsic to change the hotkey associated with a wallet to a new hotkey. It provides - options to wait for inclusion and finalization of the transaction, and to prompt the user for confirmation. - - Args: - wallet (bittensor_wallet.Wallet): The wallet whose hotkey is to be swapped. - new_wallet (bittensor_wallet.Wallet): The new wallet with the hotkey to be set. - 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`. - prompt (bool): Whether to prompt the user for confirmation before proceeding. Default is `False`. - - Returns: - bool: True if the hotkey swap was successful, False otherwise. - """ - return swap_hotkey_extrinsic( - subtensor=self, - wallet=wallet, - new_wallet=new_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def run_faucet( - self, - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[List[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, - ) -> bool: - """ - Facilitates a faucet transaction, allowing new neurons to receive an initial amount of TAO - for participating in the network. This function is particularly useful for newcomers to the - Bittensor network, enabling them to start with a small stake on testnet only. - - Args: - wallet (bittensor_wallet.Wallet): The wallet for which the faucet transaction is to be run. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. - Defaults to `False`. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. - Defaults to `True`. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_allowed_attempts (int): Maximum number of attempts to register the wallet. - output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning - the progress is printed on the same lines. Defaults to `True`. - cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). Defaults to `False`. - dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. Defaults to `0` (zero). - tpb (int): The number of threads per block (CUDA). Default to `256`. - 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`. - - Returns: - bool: ``True`` if the faucet transaction is successful, False otherwise. - - This function is part of Bittensor's onboarding process, ensuring that new neurons have - the necessary resources to begin their journey in the decentralized AI network. - - Note: - This is for testnet ONLY and is disabled currently. You must build your own staging subtensor chain with the - ``--features pow-faucet`` argument to enable this. - """ - result, _ = run_faucet_extrinsic( - subtensor=self, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - max_allowed_attempts=max_allowed_attempts, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - return result - - def burned_register( - self, - wallet: "Wallet", - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - ) -> bool: - """ - Registers a neuron on the Bittensor network by recycling TAO. This method of registration - involves recycling TAO tokens, allowing them to be re-mined by performing work on the network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered. - netuid (int): The unique identifier of the subnet. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. - Defaults to `False`. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. - Defaults to `True`. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. Defaults to `False`. - - Returns: - bool: ``True`` if the registration is successful, False otherwise. - """ - return burned_register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def _do_pow_register( - self, - netuid: int, - wallet: "Wallet", - pow_result: POWSolution, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """Sends a (POW) register extrinsic to the chain. - - Args: - netuid (int): The subnet to register on. - wallet (bittensor_wallet.Wallet): The wallet to 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`. - - Returns: - success (bool): ``True`` if the extrinsic was included in a block. - error (Optional[str]): ``None`` on success or not waiting for inclusion/finalization, otherwise the error - message. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="register", - call_params={ - "netuid": netuid, - "block_number": pow_result.block_number, - "nonce": pow_result.nonce, - "work": [int(byte_) for byte_ in pow_result.seal], - "hotkey": wallet.hotkey.ss58_address, - "coldkey": wallet.coldkeypub.ss58_address, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - - return make_substrate_call_with_retry() - - def _do_burned_register( - self, - netuid: int, - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """ - Performs a burned register extrinsic call to the Subtensor chain. - - This method sends a registration transaction to the Subtensor blockchain using the burned register mechanism. It - retries the call up to three times with exponential backoff in case of failures. - - Args: - netuid (int): The network unique identifier to register on. - 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. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional error message. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="burned_register", - call_params={ - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - - return make_substrate_call_with_retry() - - def _do_swap_hotkey( - self, - wallet: "Wallet", - new_wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """ - Performs a hotkey swap extrinsic call to the Subtensor chain. - - Args: - wallet (bittensor_wallet.Wallet): The wallet whose hotkey is to be swapped. - new_wallet (bittensor_wallet.Wallet): The wallet with the new hotkey to be set. - 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`. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional - error message. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="swap_hotkey", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "new_hotkey": new_wallet.hotkey.ss58_address, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - - return make_substrate_call_with_retry() - - ############ - # Transfer # - ############ - def transfer( - self, - wallet: "Wallet", - dest: str, - amount: Union[Balance, float], - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Executes a transfer of funds from the provided wallet to the specified destination address. - This function is used to move TAO tokens within the Bittensor network, facilitating transactions - between neurons. - - Args: - wallet (bittensor_wallet.Wallet): The wallet from which funds are being transferred. - dest (str): The destination public key address. - amount (Union[Balance, float]): The amount of TAO to be transferred. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - transfer_extrinsic (bool): ``True`` if the transfer is successful, False otherwise. - - This function is essential for the fluid movement of tokens in the network, supporting - various economic activities such as staking, delegation, and reward distribution. - """ - return transfer_extrinsic( - subtensor=self, - wallet=wallet, - dest=dest, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def get_transfer_fee( - self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] - ) -> "Balance": - """ - Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. - This function simulates the transfer to estimate the associated cost, taking into account the current - network conditions and transaction complexity. - - Args: - wallet (bittensor_wallet.Wallet): The wallet from which the transfer is initiated. - dest (str): The ``SS58`` address of the destination account. - value (Union[Balance, float, int]): The amount of tokens to be transferred, specified as a Balance object, - or in Tao (float) or Rao (int) units. - - Returns: - Balance: The estimated transaction fee for the transfer, represented as a Balance object. - - Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the - wallet has sufficient funds to cover both the transfer amount and the associated costs. This function - provides a crucial tool for managing financial operations within the Bittensor network. - """ - if isinstance(value, float): - value = Balance.from_tao(value) - elif isinstance(value, int): - value = Balance.from_rao(value) - - if isinstance(value, Balance): - call = self.substrate.compose_call( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": dest, "value": value.rao}, - ) - - try: - payment_info = self.substrate.get_payment_info( - call=call, keypair=wallet.coldkeypub - ) - except Exception as e: - settings.bt_console.print( - ":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n {}[/bold white]".format( - e - ) - ) - payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao - - fee = Balance.from_rao(payment_info["partialFee"]) - return fee - else: - fee = Balance.from_rao(int(2e7)) - logging.error( - "To calculate the transaction fee, the value must be Balance, float, or int. Received type: %s. Fee " - "is %s", - type(value), - 2e7, - ) - return fee - - def _do_transfer( - self, - wallet: "Wallet", - dest: str, - transfer_balance: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[str], Optional[str]]: - """Sends a transfer extrinsic to the chain. - - Args: - wallet (bittensor_wallet.Wallet): Wallet object. - dest (str): Destination public key address. - transfer_balance (bittensor.utils.balance.Balance): Amount to transfer. - wait_for_inclusion (bool): If ``true``, waits for inclusion. - wait_for_finalization (bool): If ``true``, waits for finalization. - - Returns: - success (bool): ``True`` if transfer was successful. - block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is ``True``. - error (str): Error message if transfer failed. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": dest, "value": transfer_balance.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None, None - - # Otherwise continue with finalization. - response.process_events() - if response.is_success: - block_hash = response.block_hash - return True, block_hash, None - else: - return False, None, format_error_message(response.error_message) - - return make_substrate_call_with_retry() - - def get_existential_deposit( - self, block: Optional[int] = None - ) -> Optional["Balance"]: - """ - Retrieves the existential deposit amount for the Bittensor blockchain. The existential deposit - is the minimum amount of TAO required for an account to exist on the blockchain. Accounts with - balances below this threshold can be reaped to conserve network resources. - - Args: - block (Optional[int]): Block number at which to query the deposit amount. If ``None``, the current block is - used. - - Returns: - Optional[Balance]: The existential deposit amount, or ``None`` if the query fails. - - The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring - efficient use of storage and preventing the proliferation of dust accounts. - """ - result = self.query_constant( - module_name="Balances", constant_name="ExistentialDeposit", block=block - ) - - if result is None or not hasattr(result, "value"): - return None - - return Balance.from_rao(result.value) - - ########### - # Network # - ########### - def register_subnetwork( - self, - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization=True, - prompt: bool = False, - ) -> bool: - """ - Registers a new subnetwork on the Bittensor network using the provided wallet. This function - is used for the creation and registration of subnetworks, which are specialized segments of the - overall Bittensor network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet to be used for registration. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the subnetwork registration is successful, False otherwise. - - This function allows for the expansion and diversification of the Bittensor network, supporting - its decentralized and adaptable architecture. - """ - return register_subnetwork_extrinsic( - self, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def set_hyperparameter( - self, - wallet: "Wallet", - netuid: int, - parameter: str, - value, - wait_for_inclusion: bool = False, - wait_for_finalization=True, - prompt: bool = False, - ) -> bool: - """ - Sets a specific hyperparameter for a given subnetwork on the Bittensor blockchain. This action - involves adjusting network-level parameters, influencing the behavior and characteristics of the - subnetwork. - - Args: - wallet (bittensor_wallet.Wallet): The wallet used for setting the hyperparameter. - netuid (int): The unique identifier of the subnetwork. - parameter (str): The name of the hyperparameter to be set. - value: The new value for the hyperparameter. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the hyperparameter setting is successful, False otherwise. - - This function plays a critical role in the dynamic governance and adaptability of the Bittensor - network, allowing for fine-tuning of network operations and characteristics. - """ - return set_hyperparameter_extrinsic( - self, - wallet=wallet, - netuid=netuid, - parameter=parameter, - value=value, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - ########### - # Serving # - ########### - def serve( - self, - wallet: "Wallet", - ip: str, - port: int, - protocol: int, - netuid: int, - placeholder1: int = 0, - placeholder2: int = 0, - wait_for_inclusion: bool = False, - wait_for_finalization=True, - ) -> bool: - """ - Registers a neuron's serving endpoint on the Bittensor network. This function announces the - IP address and port where the neuron is available to serve requests, facilitating peer-to-peer - communication within the network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron being served. - ip (str): The IP address of the serving neuron. - port (int): The port number on which the neuron is serving. - protocol (int): The protocol type used by the neuron (e.g., GRPC, HTTP). - netuid (int): The unique identifier of the subnetwork. - placeholder1 (int, optional): Placeholder parameter for future extensions. Default is ``0``. - placeholder2 (int, optional): Placeholder parameter for future extensions. Default is ``0``. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Default is - ``False``. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Default - is ``True``. - - Returns: - bool: ``True`` if the serve registration is successful, False otherwise. - - This function is essential for establishing the neuron's presence in the network, enabling - it to participate in the decentralized machine learning processes of Bittensor. - """ - return serve_extrinsic( - self, - wallet, - ip, - port, - protocol, - netuid, - placeholder1, - placeholder2, - wait_for_inclusion, - wait_for_finalization, - ) - - def serve_axon( - self, - netuid: int, - axon: "Axon", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> bool: - """ - Registers an Axon serving endpoint on the Bittensor network for a specific neuron. This function - is used to set up the Axon, a key component of a neuron that handles incoming queries and data - processing tasks. - - Args: - netuid (int): The unique identifier of the subnetwork. - axon (bittensor.core.axon.Axon): The Axon instance to be registered for serving. - 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. - - Returns: - bool: ``True`` if the Axon serve registration is successful, False otherwise. - - By registering an Axon, the neuron becomes an active part of the network's distributed - computing infrastructure, contributing to the collective intelligence of Bittensor. - """ - return serve_axon_extrinsic( - self, netuid, axon, wait_for_inclusion, wait_for_finalization - ) - - def _do_serve_axon( - self, - wallet: "Wallet", - call_params: AxonServeCallParams, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[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. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. - call_params (AxonServeCallParams): Parameters required for the serve axon call. - 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. - - Returns: - Tuple[bool, Optional[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. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="serve_axon", - call_params=call_params, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - else: - return True, None - - return make_substrate_call_with_retry() - - def serve_prometheus( - self, - wallet: "Wallet", - port: int, - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> bool: - return prometheus_extrinsic( - self, - wallet=wallet, - port=port, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - def _do_serve_prometheus( - self, - wallet: "Wallet", - call_params: PrometheusServeCallParams, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """ - Sends a serve prometheus extrinsic to the chain. - Args: - wallet (:func:`bittensor_wallet.Wallet`): Wallet object. - call_params (:func:`PrometheusServeCallParams`): Prometheus serve call parameters. - wait_for_inclusion (bool): If ``true``, waits for inclusion. - wait_for_finalization (bool): If ``true``, waits for finalization. - Returns: - success (bool): ``True`` if serve prometheus was successful. - error (:func:`Optional[str]`): Error message if serve prometheus failed, ``None`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="serve_prometheus", - call_params=call_params, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - else: - return True, None - - return make_substrate_call_with_retry() - - def _do_associate_ips( - self, - wallet: "Wallet", - ip_info_list: List["IPInfo"], - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """ - Sends an associate IPs extrinsic to the chain. - - Args: - wallet (bittensor_wallet.Wallet): Wallet object. - ip_info_list (:func:`List[IPInfo]`): List of IPInfo objects. - netuid (int): Netuid to associate IPs to. - wait_for_inclusion (bool): If ``true``, waits for inclusion. - wait_for_finalization (bool): If ``true``, waits for finalization. - - Returns: - success (bool): ``True`` if associate IPs was successful. - error (:func:`Optional[str]`): Error message if associate IPs failed, None otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="associate_ips", - call_params={ - "ip_info_list": [ip_info.encode() for ip_info in ip_info_list], - "netuid": netuid, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message - else: - return True, None - - return make_substrate_call_with_retry() - - ########### - # Staking # - ########### - def add_stake( - self, - wallet: "Wallet", - hotkey_ss58: Optional[str] = None, - amount: Optional[Union["Balance", float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. Staking - is a fundamental process in the Bittensor network that enables neurons to participate actively - and earn incentives. - - Args: - wallet (bittensor_wallet.Wallet): The wallet to be used for staking. - hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey associated with the neuron. - amount (Union[Balance, float]): The amount of TAO to stake. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the staking is successful, False otherwise. - - This function enables neurons to increase their stake in the network, enhancing their influence - and potential rewards in line with Bittensor's consensus and reward mechanisms. - """ - return add_stake_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def add_stake_multiple( - self, - wallet: "Wallet", - hotkey_ss58s: List[str], - amounts: Optional[List[Union["Balance", float]]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Adds stakes to multiple neurons identified by their hotkey SS58 addresses. This bulk operation - allows for efficient staking across different neurons from a single wallet. - - Args: - wallet (bittensor_wallet.Wallet): The wallet used for staking. - hotkey_ss58s (List[str]): List of ``SS58`` addresses of hotkeys to stake to. - amounts (List[Union[Balance, float]], optional): Corresponding amounts of TAO to stake for each hotkey. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the staking is successful for all specified neurons, False otherwise. - - This function is essential for managing stakes across multiple neurons, reflecting the dynamic - and collaborative nature of the Bittensor network. - """ - return add_stake_multiple_extrinsic( - self, - wallet, - hotkey_ss58s, - amounts, - wait_for_inclusion, - wait_for_finalization, - prompt, - ) - - def _do_stake( - self, - wallet: "Wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """Sends a stake extrinsic to the chain. - - Args: - wallet (bittensor_wallet.Wallet): Wallet object that can sign the extrinsic. - hotkey_ss58 (str): Hotkey ``ss58`` address to stake to. - amount (:func:`Balance`): Amount to stake. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. - - Returns: - success (bool): ``True`` if the extrinsic was successful. - - Raises: - StakeError: If the extrinsic failed. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={"hotkey": hotkey_ss58, "amount_staked": amount.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - response.process_events() - if response.is_success: - return True - else: - raise StakeError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() - - ############# - # Unstaking # - ############# - def unstake_multiple( - self, - wallet: "Wallet", - hotkey_ss58s: List[str], - amounts: Optional[List[Union["Balance", float]]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts - efficiently. This function is useful for managing the distribution of stakes across multiple neurons. - - Args: - wallet (bittensor_wallet.Wallet): The wallet linked to the coldkey from which the stakes are being withdrawn. - hotkey_ss58s (List[str]): A list of hotkey ``SS58`` addresses to unstake from. - amounts (List[Union[Balance, float]], optional): The amounts of TAO to unstake from each hotkey. If not - provided, unstakes all available stakes. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the batch unstaking is successful, False otherwise. - - This function allows for strategic reallocation or withdrawal of stakes, aligning with the dynamic - stake management aspect of the Bittensor network. - """ - return unstake_multiple_extrinsic( - self, - wallet, - hotkey_ss58s, - amounts, - wait_for_inclusion, - wait_for_finalization, - prompt, - ) - - def unstake( - self, - wallet: "Wallet", - hotkey_ss58: Optional[str] = None, - amount: Optional[Union["Balance", float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting - individual neuron stakes within the Bittensor network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron from which the stake is being removed. - hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. - amount (Union[Balance, float], optional): The amount of TAO to unstake. If not specified, unstakes all. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the unstaking process is successful, False otherwise. - - This function supports flexible stake management, allowing neurons to adjust their network participation - and potential reward accruals. - """ - return unstake_extrinsic( - self, - wallet, - hotkey_ss58, - amount, - wait_for_inclusion, - wait_for_finalization, - prompt, - ) - - def _do_unstake( - self, - wallet: "Wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """Sends an unstake extrinsic to the chain. - - Args: - wallet (:func:`Wallet`): Wallet object that can sign the extrinsic. - hotkey_ss58 (str): Hotkey ``ss58`` address to unstake from. - amount (:func:`Balance`): Amount to unstake. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. - Returns: - success (bool): ``True`` if the extrinsic was successful. - Raises: - StakeError: If the extrinsic failed. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={"hotkey": hotkey_ss58, "amount_unstaked": amount.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - response.process_events() - if response.is_success: - return True - else: - raise StakeError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() - - ################## - # Coldkey Swap # - ################## - - def check_in_arbitration(self, ss58_address: str) -> int: - """ - Checks storage function to see if the provided coldkey is in arbitration. - If 0, `swap` has not been called on this key. If 1, swap has been called once, so - the key is not in arbitration. If >1, `swap` has been called with multiple destinations, and - the key is thus in arbitration. - """ - return self.query_module( - "SubtensorModule", "ColdkeySwapDestinations", params=[ss58_address] - ).decode() - - def get_remaining_arbitration_period( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the remaining arbitration period for a given coldkey. - Args: - coldkey_ss58 (str): The SS58 address of the coldkey. - block (Optional[int], optional): The block number to query. If None, uses the latest block. - Returns: - Optional[int]: The remaining arbitration period in blocks, or 0 if not found. - """ - arbitration_block = self.query_subtensor( - name="ColdkeyArbitrationBlock", - block=block, - params=[coldkey_ss58], - ) - - if block is None: - block = self.block - - if arbitration_block.value > block: - return arbitration_block.value - block - else: - return 0 - - ########## - # Senate # - ########## - - def register_senate( - self, - wallet: "Wallet", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting - individual neuron stakes within the Bittensor network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron from which the stake is being removed. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the unstaking process is successful, False otherwise. - - This function supports flexible stake management, allowing neurons to adjust their network participation - and potential reward accruals. - """ - return register_senate_extrinsic( - self, wallet, wait_for_inclusion, wait_for_finalization, prompt - ) - - def leave_senate( - self, - wallet: "Wallet", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting - individual neuron stakes within the Bittensor network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron from which the stake is being removed. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the unstaking process is successful, False otherwise. - - This function supports flexible stake management, allowing neurons to adjust their network participation - and potential reward accruals. - """ - return leave_senate_extrinsic( - self, wallet, wait_for_inclusion, wait_for_finalization, prompt - ) - - def vote_senate( - self, - wallet: "Wallet", - proposal_hash: str, - proposal_idx: int, - vote: bool, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting - individual neuron stakes within the Bittensor network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron from which the stake is being removed. - proposal_hash (str): The hash of the proposal being voted on. - proposal_idx (int): The index of the proposal being voted on. - vote (bool): The vote to be cast (True for yes, False for no). - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the unstaking process is successful, False otherwise. - - This function supports flexible stake management, allowing neurons to adjust their network participation - and potential reward accruals. - """ - return vote_senate_extrinsic( - self, - wallet, - proposal_hash, - proposal_idx, - vote, - wait_for_inclusion, - wait_for_finalization, - prompt, - ) - - def is_senate_member(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - """ - Checks if a given neuron (identified by its hotkey SS58 address) is a member of the Bittensor senate. - The senate is a key governance body within the Bittensor network, responsible for overseeing and - approving various network operations and proposals. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number at which to check senate membership. - - Returns: - bool: ``True`` if the neuron is a senate member at the given block, False otherwise. - - This function is crucial for understanding the governance dynamics of the Bittensor network and for - identifying the neurons that hold decision-making power within the network. - """ - senate_members = self.query_module( - module="SenateMembers", name="Members", block=block - ) - if not hasattr(senate_members, "serialize"): - return False - senate_members_serialized = senate_members.serialize() - - if not hasattr(senate_members_serialized, "count"): - return False - - return senate_members_serialized.count(hotkey_ss58) > 0 - - def get_vote_data( - self, proposal_hash: str, block: Optional[int] = None - ) -> Optional[ProposalVoteData]: - """ - Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes - information about how senate members have voted on the proposal. - - Args: - proposal_hash (str): The hash of the proposal for which voting data is requested. - block (Optional[int]): The blockchain block number to query the voting data. - - Returns: - Optional[ProposalVoteData]: An object containing the proposal's voting data, or ``None`` if not found. - - This function is important for tracking and understanding the decision-making processes within - the Bittensor network, particularly how proposals are received and acted upon by the governing body. - """ - vote_data = self.query_module( - module="Triumvirate", name="Voting", block=block, params=[proposal_hash] - ) - if not hasattr(vote_data, "serialize"): - return None - return vote_data.serialize() if vote_data is not None else None - - get_proposal_vote_data = get_vote_data - - def get_senate_members(self, block: Optional[int] = None) -> Optional[List[str]]: - """ - Retrieves the list of current senate members from the Bittensor blockchain. Senate members are - responsible for governance and decision-making within the network. - - Args: - block (Optional[int]): The blockchain block number at which to retrieve the senate members. - - Returns: - Optional[List[str]]: A list of ``SS58`` addresses of current senate members, or ``None`` if not available. - - Understanding the composition of the senate is key to grasping the governance structure and - decision-making authority within the Bittensor network. - """ - senate_members = self.query_module("SenateMembers", "Members", block=block) - if not hasattr(senate_members, "serialize"): - return None - return senate_members.serialize() if senate_members is not None else None - - def get_proposal_call_data( - self, proposal_hash: str, block: Optional[int] = None - ) -> Optional["GenericCall"]: - """ - Retrieves the call data of a specific proposal on the Bittensor blockchain. This data provides - detailed information about the proposal, including its purpose and specifications. - - Args: - proposal_hash (str): The hash of the proposal. - block (Optional[int]): The blockchain block number at which to query the proposal call data. - - Returns: - Optional[GenericCall]: An object containing the proposal's call data, or ``None`` if not found. - - This function is crucial for analyzing the types of proposals made within the network and the - specific changes or actions they intend to implement or address. - """ - proposal_data = self.query_module( - module="Triumvirate", name="ProposalOf", block=block, params=[proposal_hash] - ) - if not hasattr(proposal_data, "serialize"): - return None - - return proposal_data.serialize() if proposal_data is not None else None - - def get_proposal_hashes(self, block: Optional[int] = None) -> Optional[List[str]]: - """ - Retrieves the list of proposal hashes currently present on the Bittensor blockchain. Each hash - uniquely identifies a proposal made within the network. - - Args: - block (Optional[int]): The blockchain block number to query the proposal hashes. - - Returns: - Optional[List[str]]: A list of proposal hashes, or ``None`` if not available. - - This function enables tracking and reviewing the proposals made in the network, offering insights - into the active governance and decision-making processes. - """ - proposal_hashes = self.query_module( - module="Triumvirate", name="Proposals", block=block - ) - if not hasattr(proposal_hashes, "serialize"): - return None - - return proposal_hashes.serialize() if proposal_hashes is not None else None - - def get_proposals( - self, block: Optional[int] = None - ) -> Optional[Dict[str, Tuple["GenericCall", "ProposalVoteData"]]]: - """ - Retrieves all active proposals on the Bittensor blockchain, along with their call and voting data. - This comprehensive view allows for a thorough understanding of the proposals and their reception - by the senate. - - Args: - block (Optional[int]): The blockchain block number to query the proposals. - - Returns: - Optional[Dict[str, Tuple[bittensor.core.chain_data.ProposalCallData, bittensor.core.chain_data.ProposalVoteData]]]: A dictionary mapping proposal hashes to their corresponding call and vote data, or ``None`` if not available. - - This function is integral for analyzing the governance activity on the Bittensor network, providing a holistic view of the proposals and their impact or potential changes within the network. - """ - proposal_hashes: Optional[List[str]] = self.get_proposal_hashes(block=block) - if proposal_hashes is None: - return None - return { - proposal_hash: ( # type: ignore - self.get_proposal_call_data(proposal_hash, block=block), - self.get_proposal_vote_data(proposal_hash, block=block), - ) - for proposal_hash in proposal_hashes - } - - ######## - # Root # - ######## - - def root_register( - self, - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - ) -> bool: - """ - Registers the neuron associated with the wallet on the root network. This process is integral for participating in the highest layer of decision-making and governance within the Bittensor network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron to be registered on the root 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the registration on the root network is successful, False otherwise. - - This function enables neurons to engage in the most critical and influential aspects of the network's governance, signifying a high level of commitment and responsibility in the Bittensor ecosystem. - """ - return root_register_extrinsic( - subtensor=self, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def _do_root_register( - self, - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="root_register", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - - return make_substrate_call_with_retry() - - @legacy_torch_api_compat - def root_set_weights( - self, - wallet: "Wallet", - netuids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], - version_key: int = 0, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Sets the weights for neurons on the root network. This action is crucial for defining the influence - and interactions of neurons at the root level of the Bittensor network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. - netuids (Union[NDArray[np.int64], torch.LongTensor, list]): The list of neuron UIDs for which weights are - being set. - weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The corresponding weights to be set for each - UID. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the setting of root-level weights is successful, False otherwise. - - This function plays a pivotal role in shaping the root network's collective intelligence and decision-making - processes, reflecting the principles of decentralized governance and collaborative learning in Bittensor. - """ - return set_root_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuids=netuids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def _do_set_root_weights( - self, - wallet: "Wallet", - uids: List[int], - vals: List[int], - netuid: int = 0, - version_key: int = settings.version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[str]]: # (success, error_message) - """ - Internal method to send a transaction to the Bittensor blockchain, setting weights - for specified neurons on root. This method constructs and submits the transaction, handling - retries and blockchain communication. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. - uids (List[int]): List of neuron UIDs for which weights are being set. - vals (List[int]): List of weight values corresponding to each UID. - netuid (int): Unique identifier for the network. - 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. - - Returns: - Tuple[bool, Optional[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 to the root network. - """ - - @retry(delay=2, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_root_weights", - call_params={ - "dests": uids, - "weights": vals, - "netuid": netuid, - "version_key": version_key, - "hotkey": wallet.hotkey.ss58_address, - }, - ) - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.coldkey, - era={"period": 5}, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # 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." - - response.process_events() - if response.is_success: - return True, "Successfully set weights." - else: - return False, response.error_message - - return make_substrate_call_with_retry() - - ################## - # Registry Calls # - ################## - - # Queries subtensor registry named storage with params and block. - def query_identity( - self, - key: str, - block: Optional[int] = None, - ) -> dict: - """ - Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves - detailed identity information about a specific neuron, which is a crucial aspect of the network's decentralized - identity and governance system. - - NOTE: - See the `Bittensor CLI documentation `_ for supported identity - parameters. - - Args: - key (str): The key used to query the neuron's identity, typically the neuron's ``SS58`` address. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - result (dict): An object containing the identity information of the neuron if found, ``None`` otherwise. - - The identity information can include various attributes such as the neuron's stake, rank, and other - network-specific details, providing insights into the neuron's role and status within the Bittensor network. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry() -> "ScaleType": - return self.substrate.query( - module="Registry", - storage_function="IdentityOf", - params=[key], - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - identity_info = make_substrate_call_with_retry() - - return decode_hex_identity_dict(identity_info.value["info"]) - - def update_identity( - self, - wallet: "Wallet", - identified: Optional[str] = None, - params: Optional[dict] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """ - Updates the identity of a neuron on the Bittensor blockchain. This function allows neurons to modify their - identity attributes, reflecting changes in their roles, stakes, or other network-specific parameters. - - NOTE: - See the `Bittensor CLI documentation `_ for supported identity - parameters. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron whose identity is being updated. - identified (str, optional): The identified ``SS58`` address of the neuron. Defaults to the wallet's coldkey - address. - params (dict, optional): A dictionary of parameters to update in the neuron's identity. - 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. - - Returns: - bool: ``True`` if the identity update is successful, False otherwise. - - This function plays a vital role in maintaining the accuracy and currency of neuron identities in the - Bittensor network, ensuring that the network's governance and consensus mechanisms operate effectively. - """ - if identified is None: - identified = wallet.coldkey.ss58_address - - params = {} if params is None else params - - call_params = create_identity_dict(**params) - call_params["identified"] = identified - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry() -> bool: - call = self.substrate.compose_call( - call_module="Registry", - call_function="set_identity", - call_params=call_params, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise IdentityError(response.error_message) - - return make_substrate_call_with_retry() - - # Make some commitment on-chain about arbitrary data. - def commit(self, wallet, netuid: int, data: str): - """ - Commits arbitrary data to the Bittensor network by publishing metadata. - - Args: - 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. - """ - publish_metadata(self, wallet, netuid, f"Raw{len(data)}", data.encode()) - - def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: - """ - Retrieves the on-chain commitment for a specific neuron in the Bittensor network. - - Args: - netuid (int): The unique identifier of the subnetwork. - uid (int): The unique identifier of the neuron. - block (Optional[int]): The block number to retrieve the commitment from. If None, the latest block - is used. Default is ``None``. - - Returns: - str: The commitment data as a string. - """ - metagraph = self.metagraph(netuid) - hotkey = metagraph.hotkeys[uid] # type: ignore - - metadata = get_metadata(self, netuid, hotkey, block) - commitment = metadata["info"]["fields"][0] # type: ignore - hex_data = commitment[list(commitment.keys())[0]][2:] # type: ignore - - return bytes.fromhex(hex_data).decode() - - ################## - # Standard Calls # - ################## - - # Queries subtensor named storage with params and block. - def query_subtensor( - self, - name: str, - block: Optional[int] = None, - params: Optional[list] = None, - ) -> "ScaleType": - """ - Queries named storage from the Subtensor module on the Bittensor blockchain. This function is used to retrieve - specific data or parameters from the blockchain, such as stake, rank, or other neuron-specific attributes. - - Args: - name (str): The name of the storage function to query. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): A list of parameters to pass to the query function. - - Returns: - query_response (ScaleType): An object containing the requested data. - - This query function is essential for accessing detailed information about the network and its neurons, - providing valuable insights into the state and dynamics of the Bittensor ecosystem. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry() -> "ScaleType": - return self.substrate.query( - module="SubtensorModule", - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - return make_substrate_call_with_retry() - - # Queries subtensor map storage with params and block. - def query_map_subtensor( - self, - name: str, - block: Optional[int] = None, - params: Optional[list] = None, - ) -> "QueryMapResult": - """ - Queries map storage from the Subtensor module on the Bittensor blockchain. This function is designed to - retrieve a map-like data structure, which can include various neuron-specific details or network-wide - attributes. - - Args: - name (str): The name of the map storage function to query. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): A list of parameters to pass to the query function. - - Returns: - QueryMapResult: An object containing the map-like data structure, or ``None`` if not found. - - This function is particularly useful for analyzing and understanding complex network structures and - relationships within the Bittensor ecosystem, such as inter-neuronal connections and stake distributions. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - return self.substrate.query_map( - module="SubtensorModule", - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - return make_substrate_call_with_retry() - - def query_constant( - self, module_name: str, constant_name: str, block: Optional[int] = None - ) -> Optional["ScaleType"]: - """ - Retrieves a constant from the specified module on the Bittensor blockchain. This function is used to - access fixed parameters or values defined within the blockchain's modules, which are essential for - understanding the network's configuration and rules. - - Args: - module_name (str): The name of the module containing the constant. - constant_name (str): The name of the constant to retrieve. - block (Optional[int]): The blockchain block number at which to query the constant. - - Returns: - Optional[ScaleType]: The value of the constant if found, ``None`` otherwise. - - Constants queried through this function can include critical network parameters such as inflation rates, - consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor network's - operational parameters. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - return self.substrate.get_constant( - module_name=module_name, - constant_name=constant_name, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - return make_substrate_call_with_retry() - - # Queries any module storage with params and block. - def query_module( - self, - module: str, - name: str, - block: Optional[int] = None, - params: Optional[list] = None, - ) -> "ScaleType": - """ - Queries any module storage on the Bittensor blockchain with the specified parameters and block number. - This function is a generic query interface that allows for flexible and diverse data retrieval from - various blockchain modules. - - Args: - module (str): The name of the module from which to query data. - name (str): The name of the storage function within the module. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): A list of parameters to pass to the query function. - - Returns: - Optional[ScaleType]: An object containing the requested data if found, ``None`` otherwise. - - This versatile query function is key to accessing a wide range of data and insights from different - parts of the Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry() -> "ScaleType": - return self.substrate.query( - module=module, - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - return make_substrate_call_with_retry() - - # Queries any module map storage with params and block. - def query_map( - self, - module: str, - name: str, - block: Optional[int] = None, - params: Optional[list] = None, - ) -> QueryMapResult: - """ - Queries map storage from any module on the Bittensor blockchain. This function retrieves data structures - that represent key-value mappings, essential for accessing complex and structured data within the blockchain - modules. - - Args: - module (str): The name of the module from which to query the map storage. - name (str): The specific storage function within the module to query. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): Parameters to be passed to the query. - - Returns: - result (QueryMapResult): A data structure representing the map storage if found, ``None`` otherwise. - - This function is particularly useful for retrieving detailed and structured data from various blockchain - modules, offering insights into the network's state and the relationships between its different components. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry() -> "QueryMapResult": - return self.substrate.query_map( - module=module, - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - return make_substrate_call_with_retry() - - def state_call( - self, - method: str, - data: str, - block: Optional[int] = None, - ) -> Dict[Any, Any]: - """ - Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. - This function is typically used for advanced queries that require specific method calls and data inputs. - - Args: - method (str): The method name for the state call. - data (str): The data to be passed to the method. - block (Optional[int]): The blockchain block number at which to perform the state call. - - Returns: - result (Dict[Any, Any]): The result of the rpc call. - - The state call function provides a more direct and flexible way of querying blockchain data, - useful for specific use cases where standard queries are insufficient. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry() -> Dict[Any, Any]: - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="state_call", - params=[method, data, block_hash] if block_hash else [method, data], - ) - - return make_substrate_call_with_retry() - - def query_runtime_api( - self, - runtime_api: str, - method: str, - params: Optional[Union[List[int], Dict[str, int]]], - block: Optional[int] = None, - ) -> Optional[str]: - """ - Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying - runtime and retrieve data encoded in Scale Bytes format. This function is essential for advanced users - who need to interact with specific runtime methods and decode complex data types. - - Args: - runtime_api (str): The name of the runtime API to query. - method (str): The specific method within the runtime API to call. - params (Optional[List[ParamWithTypes]], optional): The parameters to pass to the method call. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - Optional[bytes]: The Scale Bytes encoded result from the runtime API call, or ``None`` if the call fails. - - This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed - and specific interactions with the network's runtime environment. - """ - call_definition = settings.type_registry["runtime_api"][runtime_api]["methods"][ - method - ] - - json_result = self.state_call( - method=f"{runtime_api}_{method}", - data=( - "0x" - if params is None - else self._encode_params(call_definition=call_definition, params=params) - ), - block=block, - ) - - if json_result is None: - return None - - return_type = call_definition["type"] - - as_scale_bytes = scalecodec.ScaleBytes(json_result["result"]) # type: ignore - - rpc_runtime_config = RuntimeConfiguration() - rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) - rpc_runtime_config.update_type_registry(custom_rpc_type_registry) - - obj = rpc_runtime_config.create_scale_object(return_type, as_scale_bytes) - if obj.data.to_hex() == "0x0400": # RPC returned None result - return None - - return obj.decode() - - def _encode_params( - self, - call_definition: List["ParamWithTypes"], - params: Union[List[Any], Dict[str, Any]], - ) -> str: - """Returns a hex encoded string of the params using their types.""" - param_data = scalecodec.ScaleBytes(b"") - - for i, param in enumerate(call_definition["params"]): # type: ignore - scale_obj = self.substrate.create_scale_object(param["type"]) - if type(params) is list: - param_data += scale_obj.encode(params[i]) - else: - if param["name"] not in params: - raise ValueError(f"Missing param {param['name']} in params dict.") - - param_data += scale_obj.encode(params[param["name"]]) - - return param_data.to_hex() - - ########################## - # Hyper parameter calls. # - ########################## - - def _get_hyperparameter( - self, param_name: str, netuid: int, block: Optional[int] = None - ) -> Optional[Any]: - """ - Retrieves a specified hyperparameter for a specific subnet. - - Args: - param_name (str): The name of the hyperparameter to retrieve. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[Union[int, float]]: The value of the specified hyperparameter if the subnet exists, ``None`` - otherwise. - """ - if not self.subnet_exists(netuid, block): - return None - - result = self.query_subtensor(param_name, block, [netuid]) - if result is None or not hasattr(result, "value"): - return None - - return result.value - - def rho(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the 'Rho' hyperparameter for a specified subnet within the Bittensor network. 'Rho' represents the - global inflation rate, which directly influences the network's token emission rate and economic model. - - Note: - This is currently fixed such that the Bittensor blockchain emmits 7200 Tao per day. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to query the parameter. - - Returns: - Optional[int]: The value of the 'Rho' hyperparameter if the subnet exists, ``None`` otherwise. - - Mathematical Context: - Rho (p) is calculated based on the network's target inflation and actual neuron staking. - It adjusts the emission rate of the TAO token to balance the network's economy and dynamics. - The formula for Rho is defined as: p = (Staking_Target / Staking_Actual) * Inflation_Target. - Here, Staking_Target and Staking_Actual represent the desired and actual total stakes in the network, - while Inflation_Target is the predefined inflation rate goal. - - 'Rho' is essential for understanding the network's economic dynamics, affecting the reward distribution - and incentive structures across the network's neurons. - """ - call = self._get_hyperparameter(param_name="Rho", netuid=netuid, block=block) - return None if call is None else int(call) - - def kappa(self, netuid: int, block: Optional[int] = None) -> Optional[float]: - """ - Retrieves the 'Kappa' hyperparameter for a specified subnet. 'Kappa' is a critical parameter in - the Bittensor network that controls the distribution of stake weights among neurons, impacting their - rankings and incentive allocations. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[float]: The value of the 'Kappa' hyperparameter if the subnet exists, None otherwise. - - Mathematical Context: - Kappa (κ) is used in the calculation of neuron ranks, which determine their share of network incentives. - It is derived from the softmax function applied to the inter-neuronal weights set by each neuron. - The formula for Kappa is: κ_i = exp(w_i) / Σ(exp(w_j)), where w_i represents the weight set by neuron i, - and the denominator is the sum of exponential weights set by all neurons. - This mechanism ensures a normalized and probabilistic distribution of ranks based on relative weights. - - Understanding 'Kappa' is crucial for analyzing stake dynamics and the consensus mechanism within the network, - as it plays a significant role in neuron ranking and incentive allocation processes. - """ - call = self._get_hyperparameter(param_name="Kappa", netuid=netuid, block=block) - return None if call is None else U16_NORMALIZED_FLOAT(int(call)) - - def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. - This parameter is instrumental in determining the computational challenge required for neurons - to participate in consensus and validation processes. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[int]: The value of the 'Difficulty' hyperparameter if the subnet exists, ``None`` otherwise. - - The 'Difficulty' parameter directly impacts the network's security and integrity by setting the - computational effort required for validating transactions and participating in the network's consensus - mechanism. - """ - call = self._get_hyperparameter( - param_name="Difficulty", netuid=netuid, block=block - ) - if call is None: - return None - return int(call) - - def recycle(self, netuid: int, block: Optional[int] = None) -> Optional["Balance"]: - """ - Retrieves the 'Burn' hyperparameter for a specified subnet. The 'Burn' parameter represents the - amount of Tao that is effectively recycled within the Bittensor network. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[Balance]: The value of the 'Burn' hyperparameter if the subnet exists, None otherwise. - - Understanding the 'Burn' rate is essential for analyzing the network registration usage, particularly - how it is correlated with user activity and the overall cost of participation in a given subnet. - """ - call = self._get_hyperparameter(param_name="Burn", netuid=netuid, block=block) - return None if call is None else Balance.from_rao(int(call)) - - # Returns network ImmunityPeriod hyper parameter. - def immunity_period( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the - duration during which new neurons are protected from certain network penalties or restrictions. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[int]: The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, ``None`` otherwise. - - The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new - participants have a grace period to establish themselves and contribute to the network without facing - immediate punitive actions. - """ - call = self._get_hyperparameter( - param_name="ImmunityPeriod", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_batch_size( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorBatchSize hyper parameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int]): The block number to retrieve the parameter from. If None, the latest block - is used. Default is ``None``. - - Returns: - Optional[int]: The value of the ValidatorBatchSize hyperparameter, or None if the subnetwork does not exist - or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorBatchSize", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_prune_len( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorPruneLen hyper parameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int]): The block number to retrieve the parameter from. If None, the latest block - is used. Default is ``None``. - - Returns: - Optional[int]: The value of the ValidatorPruneLen hyperparameter, or None if the subnetwork does not exist - or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorPruneLen", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_logits_divergence( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network ValidatorLogitsDivergence hyper parameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int]): The block number to retrieve the parameter from. If None, the latest block - is used. Default is ``None``. - - Returns: - Optional[float]: The value of the ValidatorLogitsDivergence hyperparameter, or None if the subnetwork does - not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorLogitsDivergence", netuid=netuid, block=block - ) - return None if call is None else U16_NORMALIZED_FLOAT(int(call)) - - def validator_sequence_length( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorSequenceLength hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the ValidatorSequenceLength hyperparameter, or ``None`` if the subnetwork does - not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorSequenceLength", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_epochs_per_reset( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorEpochsPerReset hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the ValidatorEpochsPerReset hyperparameter, or ``None`` if the subnetwork does - not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorEpochsPerReset", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_epoch_length( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorEpochLen hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the ValidatorEpochLen hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorEpochLen", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_exclude_quantile( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network ValidatorExcludeQuantile hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the ValidatorExcludeQuantile hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorExcludeQuantile", netuid=netuid, block=block - ) - return None if call is None else U16_NORMALIZED_FLOAT(int(call)) - - def max_allowed_validators( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorExcludeQuantile hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the ValidatorExcludeQuantile hyperparameter, or ``None`` if the subnetwork - does not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="MaxAllowedValidators", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def min_allowed_weights( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network MinAllowedWeights hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the MinAllowedWeights hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="MinAllowedWeights", block=block, netuid=netuid - ) - return None if call is None else int(call) - - def max_weight_limit( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network MaxWeightsLimit hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the MaxWeightsLimit hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="MaxWeightsLimit", block=block, netuid=netuid - ) - return None if call is None else U16_NORMALIZED_FLOAT(int(call)) - - def adjustment_alpha( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network AdjustmentAlpha hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the AdjustmentAlpha hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="AdjustmentAlpha", block=block, netuid=netuid - ) - return None if call is None else U64_NORMALIZED_FLOAT(int(call)) - - def bonds_moving_avg( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network BondsMovingAverage hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the BondsMovingAverage hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="BondsMovingAverage", netuid=netuid, block=block - ) - return None if call is None else U64_NORMALIZED_FLOAT(int(call)) - - def scaling_law_power( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """Returns network ScalingLawPower hyper parameter""" - call = self._get_hyperparameter( - param_name="ScalingLawPower", netuid=netuid, block=block - ) - return None if call is None else int(call) / 100.0 - - def synergy_scaling_law_power( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network ScalingLawPower hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the ScalingLawPower hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="SynergyScalingLawPower", netuid=netuid, block=block - ) - return None if call is None else int(call) / 100.0 - - def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Returns network SubnetworkN hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the SubnetworkN hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="SubnetworkN", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def max_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Returns network MaxAllowedUids hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the MaxAllowedUids hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="MaxAllowedUids", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def blocks_since_epoch( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network BlocksSinceEpoch hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the BlocksSinceEpoch hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="BlocksSinceEpoch", netuid=netuid, block=block - ) - return None if call is None else int(call) - - 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. - - Args: - netuid (int): The unique identifier of the subnetwork. - uid (int): The unique identifier of the neuron. - - Returns: - Optional[int]: The number of blocks since the last update, or ``None`` if the subnetwork or UID does not - exist. - """ - call = self._get_hyperparameter(param_name="LastUpdate", netuid=netuid) - return None if call is None else self.get_current_block() - int(call[uid]) - - def weights_rate_limit(self, netuid: int) -> Optional[int]: - """ - Returns network WeightsSetRateLimit hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - - Returns: - Optional[int]: The value of the WeightsSetRateLimit hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter(param_name="WeightsSetRateLimit", netuid=netuid) - return None if call is None else int(call) - - def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Returns network Tempo hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the Tempo hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter(param_name="Tempo", netuid=netuid, block=block) - return None if call is None else int(call) - - ##################### - # Account functions # - ##################### - - def get_total_stake_for_hotkey( - self, ss58_address: str, block: Optional[int] = None - ) -> Optional["Balance"]: - """ - Returns the total stake held on a hotkey including delegative. - - Args: - ss58_address (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to retrieve the stake from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[Balance]: The total stake held on the hotkey, or ``None`` if the hotkey does not - exist or the stake is not found. - """ - _result = self.query_subtensor("TotalHotkeyStake", block, [ss58_address]) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def get_total_stake_for_coldkey( - self, ss58_address: str, block: Optional[int] = None - ) -> Optional["Balance"]: - """ - Returns the total stake held on a coldkey. - - Args: - ss58_address (str): The SS58 address of the coldkey. - block (Optional[int], optional): The block number to retrieve the stake from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[Balance]: The total stake held on the coldkey, or ``None`` if the coldkey does not - exist or the stake is not found. - """ - _result = self.query_subtensor("TotalColdkeyStake", block, [ss58_address]) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def get_stake_for_coldkey_and_hotkey( - self, hotkey_ss58: str, coldkey_ss58: str, block: Optional[int] = None - ) -> Optional["Balance"]: - """ - Returns the stake under a coldkey - hotkey pairing. - - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - coldkey_ss58 (str): The SS58 address of the coldkey. - block (Optional[int], optional): The block number to retrieve the stake from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[Balance]: The stake under the coldkey - hotkey pairing, or ``None`` if the pairing does not - exist or the stake is not found. - """ - _result = self.query_subtensor("Stake", block, [hotkey_ss58, coldkey_ss58]) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def get_stake( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> List[Tuple[str, "Balance"]]: - """ - Returns a list of stake tuples (coldkey, balance) for each delegating coldkey including the owner. - - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to retrieve the stakes from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - List[Tuple[str, Balance]]: A list of tuples, each containing a coldkey SS58 address and the corresponding - balance staked by that coldkey. - """ - return [ - (r[0].value, Balance.from_rao(r[1].value)) - for r in self.query_map_subtensor("Stake", block, [hotkey_ss58]) - ] - - def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - """ - Returns true if the hotkey is known by the chain and there are accounts. - - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to check the hotkey against. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - bool: ``True`` if the hotkey is known by the chain and there are accounts, ``False`` otherwise. - """ - _result = self.query_subtensor("Owner", block, [hotkey_ss58]) - return ( - False - if getattr(_result, "value", None) is None - else _result.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ) - - def get_hotkey_owner( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[str]: - """ - Returns the coldkey owner of the passed hotkey. - - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to check the hotkey owner against. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[str]: The SS58 address of the coldkey owner, or ``None`` if the hotkey does not exist or the owner - is not found. - """ - _result = self.query_subtensor("Owner", block, [hotkey_ss58]) - return ( - None - if getattr(_result, "value", None) is None - or not self.does_hotkey_exist(hotkey_ss58, block) - else _result.value - ) - - # TODO: check if someone still use this method. bittensor not. - def get_axon_info( - self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[AxonInfo]: - """ - Returns the axon information for this hotkey account. - - Args: - netuid (int): The unique identifier of the subnetwork. - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to retrieve the axon information from. If ``None``, the - latest block is used. Default is ``None``. - - Returns: - Optional[AxonInfo]: An AxonInfo object containing the axon information, or ``None`` if the axon information - is not found. - """ - result = self.query_subtensor("Axons", block, [netuid, hotkey_ss58]) - if result is not None and hasattr(result, "value"): - return AxonInfo( - ip=networking.int_to_ip(result.value["ip"]), - ip_type=result.value["ip_type"], - port=result.value["port"], - protocol=result.value["protocol"], - version=result.value["version"], - placeholder1=result.value["placeholder1"], - placeholder2=result.value["placeholder2"], - hotkey=hotkey_ss58, - coldkey="", - ) - return None - - # It is used in subtensor in neuron_info, and serving - def get_prometheus_info( - self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[PrometheusInfo]: - """ - Returns the prometheus information for this hotkey account. - - Args: - netuid (int): The unique identifier of the subnetwork. - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to retrieve the prometheus information from. If ``None``, - the latest block is used. Default is ``None``. - - Returns: - Optional[PrometheusInfo]: A PrometheusInfo object containing the prometheus information, or ``None`` if the - prometheus information is not found. - """ - result = self.query_subtensor("Prometheus", block, [netuid, hotkey_ss58]) - if result is not None and hasattr(result, "value"): - return PrometheusInfo( - ip=networking.int_to_ip(result.value["ip"]), - ip_type=result.value["ip_type"], - port=result.value["port"], - version=result.value["version"], - block=result.value["block"], - ) - return None - - ##################### - # Global Parameters # - ##################### - - @property - def block(self) -> int: - r"""Returns current chain block. - Returns: - block (int): - Current chain block. - """ - return self.get_current_block() - - def total_issuance(self, block: Optional[int] = None) -> Optional[Balance]: - """ - Retrieves the total issuance of the Bittensor network's native token (Tao) as of a specific - blockchain block. This represents the total amount of currency that has been issued or mined on the network. - - Args: - block (Optional[int], optional): The blockchain block number at which to perform the query. - - Returns: - Balance: The total issuance of TAO, represented as a Balance object. - - The total issuance is a key economic indicator in the Bittensor network, reflecting the overall supply - of the currency and providing insights into the network's economic health and inflationary trends. - """ - _result = self.query_subtensor("TotalIssuance", block) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def total_stake(self, block: Optional[int] = None) -> Optional[Balance]: - """ - Retrieves the total amount of TAO staked on the Bittensor network as of a specific blockchain block. - This represents the cumulative stake across all neurons in the network, indicating the overall level - of participation and investment by the network's participants. - - Args: - block (Optional[int], optional): The blockchain block number at which to perform the query. - - Returns: - Balance: The total amount of TAO staked on the network, represented as a Balance object. - - The total stake is an important metric for understanding the network's security, governance dynamics, - and the level of commitment by its participants. It is also a critical factor in the network's - consensus and incentive mechanisms. - """ - _result = self.query_subtensor("TotalStake", block) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def serving_rate_limit( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the serving rate limit for a specific subnet within the Bittensor network. - This rate limit determines how often you can change your node's IP address on the blockchain. Expressed in - number of blocks. Applies to both subnet validator and subnet miner nodes. Used when you move your node to a new - machine. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number at which to perform the query. - - Returns: - Optional[int]: The serving rate limit of the subnet if it exists, ``None`` otherwise. - - The serving rate limit is a crucial parameter for maintaining network efficiency and preventing - overuse of resources by individual neurons. It helps ensure a balanced distribution of service - requests across the network. - """ - call = self._get_hyperparameter( - param_name="ServingRateLimit", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def tx_rate_limit(self, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the transaction rate limit for the Bittensor network as of a specific blockchain block. - This rate limit sets the maximum number of transactions that can be processed within a given time frame. - - Args: - block (Optional[int], optional): The blockchain block number at which to perform the query. - - Returns: - Optional[int]: The transaction rate limit of the network, None if not available. - - The transaction rate limit is an essential parameter for ensuring the stability and scalability - of the Bittensor network. It helps in managing network load and preventing congestion, thereby - maintaining efficient and timely transaction processing. - """ - _result = self.query_subtensor("TxRateLimit", block) - return getattr(_result, "value", None) - - ###################### - # Network Parameters # - ###################### - - def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: - """ - Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number at which to check the subnet's existence. - - Returns: - bool: ``True`` if the subnet exists, False otherwise. - - This function is critical for verifying the presence of specific subnets in the network, - enabling a deeper understanding of the network's structure and composition. - """ - _result = self.query_subtensor("NetworksAdded", block, [netuid]) - return getattr(_result, "value", False) - - def get_all_subnet_netuids(self, block: Optional[int] = None) -> List[int]: - """ - Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. - - Args: - block (Optional[int], optional): The blockchain block number at which to retrieve the subnet netuids. - - Returns: - List[int]: A list of subnet netuids. - - This function provides a comprehensive view of the subnets within the Bittensor network, - offering insights into its diversity and scale. - """ - result = self.query_map_subtensor("NetworksAdded", block) - return ( - [] - if result is None or not hasattr(result, "records") - else [netuid.value for netuid, exists in result if exists] - ) - - def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. - - Args: - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - int: The total number of subnets in the network. - - Understanding the total number of subnets is essential for assessing the network's growth and - the extent of its decentralized infrastructure. - """ - _result = self.query_subtensor("TotalNetworks", block) - return getattr(_result, "value", None) - - def get_subnet_modality( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns the NetworkModality hyperparameter for a specific subnetwork. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the NetworkModality hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. - """ - _result = self.query_subtensor("NetworkModality", block, [netuid]) - return getattr(_result, "value", None) - - def get_subnet_connection_requirement( - self, netuid_0: int, netuid_1: int, block: Optional[int] = None - ) -> Optional[int]: - _result = self.query_subtensor("NetworkConnect", block, [netuid_0, netuid_1]) - return getattr(_result, "value", None) - - def get_emission_value_by_subnet( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Retrieves the emission value of a specific subnet within the Bittensor network. The emission value - represents the rate at which the subnet emits or distributes the network's native token (Tao). - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Optional[float]: The emission value of the subnet, None if not available. - - The emission value is a critical economic parameter, influencing the incentive distribution and - reward mechanisms within the subnet. - """ - _result = self.query_subtensor("EmissionValues", block, [netuid]) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def get_subnet_connection_requirements( - self, netuid: int, block: Optional[int] = None - ) -> Dict[str, int]: + def query_runtime_api( + self, + runtime_api: str, + method: str, + params: Optional[Union[List[int], Dict[str, int]]], + block: Optional[int] = None, + ) -> Optional[str]: """ - Retrieves the connection requirements for a specific subnet within the Bittensor network. This - function provides details on the criteria that must be met for neurons to connect to the subnet. + Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying runtime and retrieve data encoded in Scale Bytes format. This function is essential for advanced users who need to interact with specific runtime methods and decode complex data types. Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int], optional): The blockchain block number for the query. + runtime_api (str): The name of the runtime API to query. + method (str): The specific method within the runtime API to call. + params (Optional[List[ParamWithTypes]], optional): The parameters to pass to the method call. + block (Optional[int]): The blockchain block number at which to perform the query. Returns: - Dict[str, int]: A dictionary detailing the connection requirements for the subnet. + Optional[bytes]: The Scale Bytes encoded result from the runtime API call, or ``None`` if the call fails. - Understanding these requirements is crucial for neurons looking to participate in or interact - with specific subnets, ensuring compliance with their connection standards. + This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. """ - result = self.query_map_subtensor("NetworkConnect", block, [netuid]) - return ( - {str(netuid.value): exists.value for netuid, exists in result.records} - if result and hasattr(result, "records") - else {} + call_definition = settings.TYPE_REGISTRY["runtime_api"][runtime_api]["methods"][ + method + ] + + json_result = self.state_call( + method=f"{runtime_api}_{method}", + data=( + "0x" + if params is None + else self._encode_params(call_definition=call_definition, params=params) + ), + block=block, ) - def get_subnets(self, block: Optional[int] = None) -> List[int]: - """ - Retrieves a list of all subnets currently active within the Bittensor network. This function - provides an overview of the various subnets and their identifiers. + if json_result is None: + return None - Args: - block (Optional[int], optional): The blockchain block number for the query. + return_type = call_definition["type"] - Returns: - List[int]: A list of network UIDs representing each active subnet. + as_scale_bytes = scalecodec.ScaleBytes(json_result["result"]) - This function is valuable for understanding the network's structure and the diversity of subnets - available for neuron participation and collaboration. - """ - result = self.query_map_subtensor("NetworksAdded", block) - return ( - [network[0].value for network in result.records] - if result and hasattr(result, "records") - else [] - ) + rpc_runtime_config = RuntimeConfiguration() + rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) + rpc_runtime_config.update_type_registry(custom_rpc_type_registry) + + obj = rpc_runtime_config.create_scale_object(return_type, as_scale_bytes) + if obj.data.to_hex() == "0x0400": # RPC returned None result + return None + + return obj.decode() - def get_all_subnets_info(self, block: Optional[int] = None) -> List[SubnetInfo]: + def state_call( + self, method: str, data: str, block: Optional[int] = None + ) -> Dict[Any, Any]: """ - Retrieves detailed information about all subnets within the Bittensor network. This function - provides comprehensive data on each subnet, including its characteristics and operational parameters. + Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. This function is typically used for advanced queries that require specific method calls and data inputs. Args: - block (Optional[int], optional): The blockchain block number for the query. + method (str): The method name for the state call. + data (str): The data to be passed to the method. + block (Optional[int]): The blockchain block number at which to perform the state call. Returns: - List[SubnetInfo]: A list of SubnetInfo objects, each containing detailed information about a subnet. + result (Dict[Any, Any]): The result of the rpc call. - Gaining insights into the subnets' details assists in understanding the network's composition, - the roles of different subnets, and their unique features. + The state call function provides a more direct and flexible way of querying blockchain data, useful for specific use cases where standard queries are insufficient. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): + def make_substrate_call_with_retry() -> Dict[Any, Any]: block_hash = None if block is None else self.substrate.get_block_hash(block) - return self.substrate.rpc_request( - method="subnetInfo_getSubnetsInfo", # custom rpc method - params=[block_hash] if block_hash else [], + method="state_call", + params=[method, data, block_hash] if block_hash else [method, data], ) - json_body = make_substrate_call_with_retry() - - if not (result := json_body.get("result", None)): - return [] - - return SubnetInfo.list_from_vec_u8(result) + return make_substrate_call_with_retry() - def get_subnet_info( - self, netuid: int, block: Optional[int] = None - ) -> Optional[SubnetInfo]: + def query_map( + self, + module: str, + name: str, + block: Optional[int] = None, + params: Optional[list] = None, + ) -> QueryMapResult: """ - Retrieves detailed information about a specific subnet within the Bittensor network. This function - provides key data on the subnet, including its operational parameters and network status. + Queries map storage from any module on the Bittensor blockchain. This function retrieves data structures that represent key-value mappings, essential for accessing complex and structured data within the blockchain modules. Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int], optional): The blockchain block number for the query. + module (str): The name of the module from which to query the map storage. + name (str): The specific storage function within the module to query. + block (Optional[int]): The blockchain block number at which to perform the query. + params (Optional[List[object]], optional): Parameters to be passed to the query. Returns: - Optional[SubnetInfo]: Detailed information about the subnet, or ``None`` if not found. + result (QueryMapResult): A data structure representing the map storage if found, ``None`` otherwise. - This function is essential for neurons and stakeholders interested in the specifics of a particular - subnet, including its governance, performance, and role within the broader network. + This function is particularly useful for retrieving detailed and structured data from various blockchain modules, offering insights into the network's state and the relationships between its different components. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="subnetInfo_getSubnetInfo", # custom rpc method - params=[netuid, block_hash] if block_hash else [netuid], + def make_substrate_call_with_retry() -> "QueryMapResult": + return self.substrate.query_map( + module=module, + storage_function=name, + params=params, + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), ) - json_body = make_substrate_call_with_retry() - - if not (result := json_body.get("result", None)): - return None - - return SubnetInfo.from_vec_u8(result) + return make_substrate_call_with_retry() - def get_subnet_hyperparameters( - self, netuid: int, block: Optional[int] = None - ) -> Optional[Union[List, SubnetHyperparameters]]: + def query_constant( + self, module_name: str, constant_name: str, block: Optional[int] = None + ) -> Optional["ScaleType"]: """ - Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters - define the operational settings and rules governing the subnet's behavior. + Retrieves a constant from the specified module on the Bittensor blockchain. This function is used to access fixed parameters or values defined within the blockchain's modules, which are essential for understanding the network's configuration and rules. Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int], optional): The blockchain block number for the query. + module_name (str): The name of the module containing the constant. + constant_name (str): The name of the constant to retrieve. + block (Optional[int]): The blockchain block number at which to query the constant. Returns: - Optional[SubnetHyperparameters]: The subnet's hyperparameters, or ``None`` if not available. + Optional[ScaleType]: The value of the constant if found, ``None`` otherwise. - Understanding the hyperparameters is crucial for comprehending how subnets are configured and - managed, and how they interact with the network's consensus and incentive mechanisms. + Constants queried through this function can include critical network parameters such as inflation rates, consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor network's operational parameters. """ - hex_bytes_result = self.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, - ) - - if hex_bytes_result is None: - return [] - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + return self.substrate.get_constant( + module_name=module_name, + constant_name=constant_name, + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), + ) - return SubnetHyperparameters.from_vec_u8(bytes_result) # type: ignore + return make_substrate_call_with_retry() - def get_subnet_owner( - self, netuid: int, block: Optional[int] = None - ) -> Optional[str]: + def query_module( + self, + module: str, + name: str, + block: Optional[int] = None, + params: Optional[list] = None, + ) -> "ScaleType": """ - Retrieves the owner's address of a specific subnet within the Bittensor network. The owner is - typically the entity responsible for the creation and maintenance of the subnet. + Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This function is a generic query interface that allows for flexible and diverse data retrieval from various blockchain modules. Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int], optional): The blockchain block number for the query. + module (str): The name of the module from which to query data. + name (str): The name of the storage function within the module. + block (Optional[int]): The blockchain block number at which to perform the query. + params (Optional[List[object]], optional): A list of parameters to pass to the query function. Returns: - Optional[str]: The SS58 address of the subnet's owner, or ``None`` if not available. + Optional[ScaleType]: An object containing the requested data if found, ``None`` otherwise. - Knowing the subnet owner provides insights into the governance and operational control of the subnet, - which can be important for decision-making and collaboration within the network. + This versatile query function is key to accessing a wide range of data and insights from different parts of the Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. """ - _result = self.query_subtensor("SubnetOwner", block, [netuid]) - return getattr(_result, "value", None) - ############## - # Nomination # - ############## - def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry() -> "ScaleType": + return self.substrate.query( + module=module, + storage_function=name, + params=params, + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), + ) + + return make_substrate_call_with_retry() + + # Common subtensor methods + def metagraph( + self, netuid: int, lite: bool = True, block: Optional[int] = None + ) -> "Metagraph": # type: ignore """ - Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function - checks if the neuron associated with the hotkey is part of the network's delegation system. + Returns a synced metagraph for a specified subnet within the Bittensor network. The metagraph represents the network's structure, including neuron connections and interactions. Args: - hotkey_ss58 (str): The SS58 address of the neuron's hotkey. - block (Optional[int], optional): The blockchain block number for the query. + netuid (int): The network UID of the subnet to query. + lite (bool, default=True): If true, returns a metagraph using a lightweight sync (no weights, no bonds). + block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. Returns: - bool: ``True`` if the hotkey is a delegate, ``False`` otherwise. + bittensor.core.metagraph.Metagraph: The metagraph representing the subnet's structure and neuron relationships. - Being a delegate is a significant status within the Bittensor network, indicating a neuron's - involvement in consensus and governance processes. + The metagraph is an essential tool for understanding the topology and dynamics of the Bittensor network's decentralized architecture, particularly in relation to neuron interconnectivity and consensus processes. """ - return hotkey_ss58 in [ - info.hotkey_ss58 for info in self.get_delegates(block=block) - ] + metagraph = Metagraph( + network=self.network, netuid=netuid, lite=lite, sync=False + ) + metagraph.sync(block=block, lite=lite, subtensor=self) - def get_delegate_take( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[float]: - """ - Retrieves 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. + return metagraph - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int], optional): The blockchain block number for the query. + @staticmethod + def determine_chain_endpoint_and_network(network: str): + """Determines the chain endpoint and network from the passed network or chain_endpoint. + Args: + network (str): The network flag. The choices are: ``-- finney`` (main network), ``-- archive`` + (archive network +300 blocks), ``-- local`` (local running network), ``-- test`` (test network). Returns: - Optional[float]: The delegate take percentage, None if not available. - - The delegate take is a critical parameter in the network's incentive structure, influencing - the distribution of rewards among neurons and their nominators. + network (str): The network flag. + chain_endpoint (str): The chain endpoint flag. If set, overrides the ``network`` argument. """ - _result = self.query_subtensor("Delegates", block, [hotkey_ss58]) - return ( - None - if getattr(_result, "value", None) is None - else U16_NORMALIZED_FLOAT(_result.value) - ) + if network is None: + return None, None + if network in ["finney", "local", "test", "archive"]: + if network == "finney": + # Kiru Finney staging network. + return network, settings.FINNEY_ENTRYPOINT + elif network == "local": + return network, settings.LOCAL_ENTRYPOINT + elif network == "test": + return network, settings.FINNEY_TEST_ENTRYPOINT + elif network == "archive": + return network, settings.ARCHIVE_ENTRYPOINT + else: + if ( + network == settings.FINNEY_ENTRYPOINT + or "entrypoint-finney.opentensor.ai" in network + ): + return "finney", settings.FINNEY_ENTRYPOINT + elif ( + network == settings.FINNEY_TEST_ENTRYPOINT + or "test.finney.opentensor.ai" in network + ): + return "test", settings.FINNEY_TEST_ENTRYPOINT + elif ( + network == settings.ARCHIVE_ENTRYPOINT + or "archive.chain.opentensor.ai" in network + ): + return "archive", settings.ARCHIVE_ENTRYPOINT + elif "127.0.0.1" in network or "localhost" in network: + return "local", network + else: + return "unknown", network - def get_nominators_for_hotkey( + def get_netuids_for_hotkey( self, hotkey_ss58: str, block: Optional[int] = None - ) -> Union[List[Tuple[str, Balance]], int]: + ) -> List[int]: """ - Retrieves a list of nominators and their stakes for a neuron identified by its hotkey. - Nominators are neurons that stake their tokens on a delegate to support its operations. + Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function identifies the specific subnets within the Bittensor network where the neuron associated with the hotkey is active. Args: hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int], optional): The blockchain block number for the query. + block (Optional[int]): The blockchain block number at which to perform the query. Returns: - Union[List[Tuple[str, Balance]], int]: A list of tuples containing each nominator's address and staked amount or 0. - - This function provides insights into the neuron's support network within the Bittensor ecosystem, - indicating its trust and collaboration relationships. + List[int]: A list of netuids where the neuron is a member. """ - result = self.query_map_subtensor("Stake", block, [hotkey_ss58]) + result = self.query_map_subtensor("IsNetworkMember", block, [hotkey_ss58]) return ( - [(record[0].value, record[1].value) for record in result.records] + [record[0].value for record in result.records if record[1]] if result and hasattr(result, "records") - else 0 + else [] ) - def get_delegate_by_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[DelegateInfo]: - """ - Retrieves detailed information about a delegate neuron based on its hotkey. This function provides - a comprehensive view of the delegate's status, including its stakes, nominators, and reward distribution. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the delegate's hotkey. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Optional[DelegateInfo]: Detailed information about the delegate neuron, ``None`` if not found. - - This function is essential for understanding the roles and influence of delegate neurons within - the Bittensor network's consensus and governance structures. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(encoded_hotkey_: List[int]): - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="delegateInfo_getDelegate", # custom rpc method - params=( - [encoded_hotkey_, block_hash] if block_hash else [encoded_hotkey_] - ), - ) - - encoded_hotkey = ss58_to_vec_u8(hotkey_ss58) - json_body = make_substrate_call_with_retry(encoded_hotkey) - - if not (result := json_body.get("result", None)): - return None - - return DelegateInfo.from_vec_u8(result) - - def get_delegates_lite(self, block: Optional[int] = None) -> List[DelegateInfoLite]: - """ - Retrieves a lighter list of all delegate neurons within the Bittensor network. This function provides an - overview of the neurons that are actively involved in the network's delegation system. - - Analyzing the delegate population offers insights into the network's governance dynamics and the distribution - of trust and responsibility among participating neurons. - - This is a lighter version of :func:`get_delegates`. - - Args: - block (Optional[int], optional): The blockchain block number for the query. - + def get_current_block(self) -> int: + """ + Returns the current block number on the Bittensor blockchain. This function provides the latest block number, indicating the most recent state of the blockchain. + Returns: - List[DelegateInfoLite]: A list of ``DelegateInfoLite`` objects detailing each delegate's characteristics. + int: The current chain block number. + Knowing the current block number is essential for querying real-time data and performing time-sensitive operations on the blockchain. It serves as a reference point for network activities and data synchronization. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): - block_hash = None if block is None else self.substrate.get_block_hash(block) + return self.substrate.get_block_number(None) # type: ignore - return self.substrate.rpc_request( - method="delegateInfo_getDelegatesLite", # custom rpc method - params=[block_hash] if block_hash else [], - ) + return make_substrate_call_with_retry() - json_body = make_substrate_call_with_retry() + def is_hotkey_registered_any( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> bool: + """ + Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. - if not (result := json_body.get("result", None)): - return [] + Args: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number at which to perform the check. - return [DelegateInfoLite(**d) for d in result] + Returns: + bool: ``True`` if the hotkey is registered on any subnet, False otherwise. - def get_delegates(self, block: Optional[int] = None) -> List[DelegateInfo]: + This function is essential for determining the network-wide presence and participation of a neuron. """ - Retrieves a list of all delegate neurons within the Bittensor network. This function provides an overview of the - neurons that are actively involved in the network's delegation system. + return len(self.get_netuids_for_hotkey(hotkey_ss58, block)) > 0 - Analyzing the delegate population offers insights into the network's governance dynamics and the distribution of - trust and responsibility among participating neurons. + def is_hotkey_registered_on_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> bool: + """ + Checks if a neuron's hotkey is registered on a specific subnet within the Bittensor network. Args: - block (Optional[int], optional): The blockchain block number for the query. + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number at which to perform the check. Returns: - List[DelegateInfo]: A list of DelegateInfo objects detailing each delegate's characteristics. + bool: ``True`` if the hotkey is registered on the specified subnet, False otherwise. + This function helps in assessing the participation of a neuron in a particular subnet, indicating its specific area of operation or influence within the network. """ + return self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block) is not None - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="delegateInfo_getDelegates", # custom rpc method - params=[block_hash] if block_hash else [], - ) + def is_hotkey_registered( + self, + hotkey_ss58: str, + netuid: Optional[int] = None, + block: Optional[int] = None, + ) -> bool: + """ + Determines whether a given hotkey (public key) is registered in the Bittensor network, either globally across any subnet or specifically on a specified subnet. This function checks the registration status of a neuron identified by its hotkey, which is crucial for validating its participation and activities within the network. - json_body = make_substrate_call_with_retry() + Args: + hotkey_ss58 (str): The SS58 address of the neuron's hotkey. + netuid (Optional[int]): The unique identifier of the subnet to check the registration. If ``None``, the registration is checked across all subnets. + block (Optional[int]): The blockchain block number at which to perform the query. - if not (result := json_body.get("result", None)): - return [] + Returns: + bool: ``True`` if the hotkey is registered in the specified context (either any subnet or a specific subnet), ``False`` otherwise. - return DelegateInfo.list_from_vec_u8(result) + This function is important for verifying the active status of neurons in the Bittensor network. It aids in understanding whether a neuron is eligible to participate in network processes such as consensus, validation, and incentive distribution based on its registration status. + """ + if netuid is None: + return self.is_hotkey_registered_any(hotkey_ss58, block) + else: + return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) - def get_delegated( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> List[Tuple[DelegateInfo, Balance]]: + def do_set_weights( + self, + wallet: "Wallet", + uids: List[int], + vals: List[int], + netuid: int, + version_key: int = settings.version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + ) -> Tuple[bool, Optional[str]]: # (success, error_message) """ - Retrieves a list of delegates and their associated stakes for a given coldkey. This function - identifies the delegates that a specific account has staked tokens on. + 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: - coldkey_ss58 (str): The ``SS58`` address of the account's coldkey. - block (Optional[int], optional): The blockchain block number for the query. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. + uids (List[int]): List of neuron UIDs for which weights are being set. + vals (List[int]): List of weight values corresponding to each UID. + netuid (int): Unique identifier for the network. + 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. Returns: - List[Tuple[DelegateInfo, Balance]]: A list of tuples, each containing a delegate's information and staked - amount. + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - This function is important for account holders to understand their stake allocations and their - involvement in the network's delegation and consensus mechanisms. + 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. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(encoded_coldkey_: List[int]): - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="delegateInfo_getDelegated", - params=( - [block_hash, encoded_coldkey_] if block_hash else [encoded_coldkey_] - ), + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_weights", + call_params={ + "dests": uids, + "weights": vals, + "netuid": netuid, + "version_key": version_key, + }, ) + # Period dictates how long the extrinsic will stay as part of waiting pool + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + era={"period": 5}, + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # 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." - encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - json_body = make_substrate_call_with_retry(encoded_coldkey) - - if not (result := json_body.get("result", None)): - return [] + response.process_events() + if response.is_success: + return True, "Successfully set weights." + else: + return False, format_error_message(response.error_message) - return DelegateInfo.delegated_list_from_vec_u8(result) + return make_substrate_call_with_retry() - ##################### - # Stake Information # - ##################### + # keep backwards compatibility for the community + _do_set_weights = do_set_weights - def get_stake_info_for_coldkey( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> Optional[List[StakeInfo]]: + # Not used in Bittensor, but is actively used by the community in almost all subnets + def set_weights( + self, + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + version_key: int = settings.version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, + max_retries: int = 5, + ) -> Tuple[bool, str]: """ - Retrieves stake information associated with a specific coldkey. This function provides details - about the stakes held by an account, including the staked amounts and associated delegates. + Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or trust a neuron places on other neurons in the network, which is a fundamental aspect of Bittensor's decentralized learning architecture. Args: - coldkey_ss58 (str): The ``SS58`` address of the account's coldkey. - block (Optional[int], optional): The blockchain block number for the query. + 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, 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. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + max_retries (int, optional): The number of maximum attempts to set weights. (Default: 5) Returns: - List[StakeInfo]: A list of StakeInfo objects detailing the stake allocations for the account. + Tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, a string value describing the success or potential error. - Stake information is vital for account holders to assess their investment and participation - in the network's delegation and consensus processes. + 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】. """ - encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - - hex_bytes_result = self.query_runtime_api( - runtime_api="StakeInfoRuntimeApi", - method="get_stake_info_for_coldkey", - params=[encoded_coldkey], # type: ignore - block=block, - ) - - if hex_bytes_result is None: - return None + uid = self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to set weights!" + while ( + self.blocks_since_last_update(netuid, uid) > self.weights_rate_limit(netuid) # type: ignore + and retries < max_retries + ): + try: + success, message = set_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + except Exception as e: + logging.error(f"Error setting weights: {e}") + finally: + retries += 1 - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) - # TODO: review if this is the correct type / works - return StakeInfo.list_from_vec_u8(bytes_result) # type: ignore + return success, message - def get_stake_info_for_coldkeys( - self, coldkey_ss58_list: List[str], block: Optional[int] = None - ) -> Optional[Dict[str, List[StakeInfo]]]: + def serve_axon( + self, + netuid: int, + axon: "Axon", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> bool: """ - Retrieves stake information for a list of coldkeys. This function aggregates stake data for multiple - accounts, providing a collective view of their stakes and delegations. + Registers an Axon serving endpoint on the Bittensor network for a specific neuron. This function is used to set up the Axon, a key component of a neuron that handles incoming queries and data processing tasks. Args: - coldkey_ss58_list (List[str]): A list of ``SS58`` addresses of the accounts' coldkeys. - block (Optional[int], optional): The blockchain block number for the query. + netuid (int): The unique identifier of the subnetwork. + axon (bittensor.core.axon.Axon): The Axon instance to be registered for serving. + 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. Returns: - Dict[str, List[StakeInfo]]: A dictionary mapping each coldkey to a list of its StakeInfo objects. + bool: ``True`` if the Axon serve registration is successful, False otherwise. - This function is useful for analyzing the stake distribution and delegation patterns of multiple - accounts simultaneously, offering a broader perspective on network participation and investment strategies. + By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, contributing to the collective intelligence of Bittensor. """ - # TODO: review - ss58_to_vec_u8 returns List[int] but the runtime api expects List[List[int]] - encoded_coldkeys = [ - ss58_to_vec_u8(coldkey_ss58) for coldkey_ss58 in coldkey_ss58_list - ] - - hex_bytes_result = self.query_runtime_api( - runtime_api="StakeInfoRuntimeApi", - method="get_stake_info_for_coldkeys", - params=[encoded_coldkeys], # type: ignore - block=block, + return serve_axon_extrinsic( + self, netuid, axon, wait_for_inclusion, wait_for_finalization ) - if hex_bytes_result is None: - return None - - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) + # metagraph + @property + def block(self) -> int: + """Returns current chain block. - return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore + Returns: + block (int): Current chain block. + """ + return self.get_current_block() - def get_minimum_required_stake( - self, - ) -> Balance: + def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: """ - Returns the minimum required stake for nominators in the Subtensor network. + Returns the number of blocks since the last update for a specific UID in the subnetwork. - This method retries the substrate call up to three times with exponential backoff in case of failures. + Args: + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. Returns: - Balance: The minimum required stake as a Balance object. - - Raises: - Exception: If the substrate call fails after the maximum number of retries. + Optional[int]: The number of blocks since the last update, or ``None`` if the subnetwork or UID does not exist. """ + call = self._get_hyperparameter(param_name="LastUpdate", netuid=netuid) + return None if call is None else self.get_current_block() - int(call[uid]) - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - return self.substrate.query( - module="SubtensorModule", storage_function="NominatorMinRequiredStake" - ) - - result = make_substrate_call_with_retry() - return Balance.from_rao(result.decode()) - - ################################# - # Neuron information per subnet # - ################################# - - def is_hotkey_registered_any( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> bool: + def get_block_hash(self, block_id: int) -> str: """ - Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. + Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique identifier representing the cryptographic hash of the block's content, ensuring its integrity and immutability. Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number at which to perform the check. + block_id (int): The block number for which the hash is to be retrieved. Returns: - bool: ``True`` if the hotkey is registered on any subnet, False otherwise. + str: The cryptographic hash of the specified block. - This function is essential for determining the network-wide presence and participation of a neuron. + The block hash is a fundamental aspect of blockchain technology, providing a secure reference to each block's data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the trustworthiness of the blockchain. """ - return len(self.get_netuids_for_hotkey(hotkey_ss58, block)) > 0 + return self.substrate.get_block_hash(block_id=block_id) - def is_hotkey_registered_on_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> bool: + def weights_rate_limit(self, netuid: int) -> Optional[int]: """ - Checks if a neuron's hotkey is registered on a specific subnet within the Bittensor network. + Returns network WeightsSetRateLimit hyperparameter. Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to perform the check. + netuid (int): The unique identifier of the subnetwork. Returns: - bool: ``True`` if the hotkey is registered on the specified subnet, False otherwise. - - This function helps in assessing the participation of a neuron in a particular subnet, - indicating its specific area of operation or influence within the network. + Optional[int]: The value of the WeightsSetRateLimit hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. """ - return self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block) is not None + call = self._get_hyperparameter(param_name="WeightsSetRateLimit", netuid=netuid) + return None if call is None else int(call) - def is_hotkey_registered( - self, - hotkey_ss58: str, - netuid: Optional[int] = None, - block: Optional[int] = None, - ) -> bool: + # Keep backwards compatibility for community usage. + # Make some commitment on-chain about arbitrary data. + def commit(self, wallet, netuid: int, data: str): """ - Determines whether a given hotkey (public key) is registered in the Bittensor network, either - globally across any subnet or specifically on a specified subnet. This function checks the registration - status of a neuron identified by its hotkey, which is crucial for validating its participation and - activities within the network. + Commits arbitrary data to the Bittensor network by publishing metadata. Args: - hotkey_ss58 (str): The SS58 address of the neuron's hotkey. - netuid (Optional[int]): The unique identifier of the subnet to check the registration. If ``None``, the - registration is checked across all subnets. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - bool: ``True`` if the hotkey is registered in the specified context (either any subnet or a specific - subnet), ``False`` otherwise. - - This function is important for verifying the active status of neurons in the Bittensor network. It aids - in understanding whether a neuron is eligible to participate in network processes such as consensus, - validation, and incentive distribution based on its registration status. + 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. """ - if netuid is None: - return self.is_hotkey_registered_any(hotkey_ss58, block) - else: - return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) + publish_metadata(self, wallet, netuid, f"Raw{len(data)}", data.encode()) - def get_uid_for_hotkey_on_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional[int]: + # Keep backwards compatibility for community usage. + def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: """ - Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. + Returns network SubnetworkN hyperparameter. Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. + netuid (int): The unique identifier of the subnetwork. + block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. Returns: - Optional[int]: The UID of the neuron if it is registered on the subnet, ``None`` otherwise. - - The UID is a critical identifier within the network, linking the neuron's hotkey to its - operational and governance activities on a particular subnet. + Optional[int]: The value of the SubnetworkN hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. """ - _result = self.query_subtensor("Uids", block, [netuid, hotkey_ss58]) - return getattr(_result, "value", None) + call = self._get_hyperparameter( + param_name="SubnetworkN", netuid=netuid, block=block + ) + return None if call is None else int(call) - def get_all_uids_for_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> List[int]: - """ - Retrieves all unique identifiers (UIDs) associated with a given hotkey across different subnets - within the Bittensor network. This function helps in identifying all the neuron instances that are - linked to a specific hotkey. + def do_transfer( + self, + wallet: "Wallet", + dest: str, + transfer_balance: "Balance", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> Tuple[bool, Optional[str], Optional[str]]: + """Sends a transfer extrinsic to the chain. Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number at which to perform the query. + wallet (bittensor_wallet.Wallet): Wallet object. + dest (str): Destination public key address. + transfer_balance (bittensor.utils.balance.Balance): Amount to transfer. + wait_for_inclusion (bool): If ``true``, waits for inclusion. + wait_for_finalization (bool): If ``true``, waits for finalization. Returns: - List[int]: A list of UIDs associated with the given hotkey across various subnets. - - This function is important for tracking a neuron's presence and activities across different - subnets within the Bittensor ecosystem. + success (bool): ``True`` if transfer was successful. + block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is ``True``. + error (str): Error message if transfer failed. """ - return [ - self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block) or 0 - for netuid in self.get_netuids_for_hotkey(hotkey_ss58, block) - ] - def get_netuids_for_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> List[int]: + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": dest, "value": transfer_balance.rao}, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, None, None + + # Otherwise continue with finalization. + response.process_events() + if response.is_success: + block_hash = response.block_hash + return True, block_hash, None + else: + return False, None, format_error_message(response.error_message) + + return make_substrate_call_with_retry() + + # Community uses this method + def transfer( + self, + wallet: "Wallet", + dest: str, + amount: Union[Balance, float], + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> bool: """ - Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function - identifies the specific subnets within the Bittensor network where the neuron associated with - the hotkey is active. + Executes a transfer of funds from the provided wallet to the specified destination address. This function is used to move TAO tokens within the Bittensor network, facilitating transactions between neurons. Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number at which to perform the query. + wallet (bittensor_wallet.Wallet): The wallet from which funds are being transferred. + dest (str): The destination public key address. + amount (Union[Balance, float]): The amount of TAO to be transferred. + 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. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. Returns: - List[int]: A list of netuids where the neuron is a member. + transfer_extrinsic (bool): ``True`` if the transfer is successful, False otherwise. + + This function is essential for the fluid movement of tokens in the network, supporting various economic activities such as staking, delegation, and reward distribution. """ - result = self.query_map_subtensor("IsNetworkMember", block, [hotkey_ss58]) - return ( - [record[0].value for record in result.records if record[1]] - if result and hasattr(result, "records") - else [] + return transfer_extrinsic( + subtensor=self, + wallet=wallet, + dest=dest, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, ) + # Community uses this method via `bittensor.api.extrinsics.prometheus.prometheus_extrinsic` def get_neuron_for_pubkey_and_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None ) -> Optional[NeuronInfo]: """ - Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific - subnet UID (netuid). This function provides detailed neuron information for a particular subnet within - the Bittensor network. + Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor network. Args: hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. @@ -4731,8 +1152,7 @@ def get_neuron_for_pubkey_and_subnet( Returns: Optional[NeuronInfo]: Detailed information about the neuron if found, ``None`` otherwise. - This function is crucial for accessing specific neuron data and understanding its status, stake, - and other attributes within a particular subnet of the Bittensor ecosystem. + This function is crucial for accessing specific neuron data and understanding its status, stake, and other attributes within a particular subnet of the Bittensor ecosystem. """ return self.neuron_for_uid( self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block=block), @@ -4740,655 +1160,567 @@ def get_neuron_for_pubkey_and_subnet( block=block, ) - def get_all_neurons_for_pubkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> List[NeuronInfo]: + def neuron_for_uid( + self, uid: Optional[int], netuid: int, block: Optional[int] = None + ) -> NeuronInfo: """ - Retrieves information about all neuron instances associated with a given public key (hotkey ``SS58`` - address) across different subnets of the Bittensor network. This function aggregates neuron data - from various subnets to provide a comprehensive view of a neuron's presence and status within the network. + Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a neuron's attributes, including its stake, rank, and operational status. Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number for the query. + uid (int): The unique identifier of the neuron. + netuid (int): The unique identifier of the subnet. + block (Optional[int], optional): The blockchain block number for the query. Returns: - List[NeuronInfo]: A list of NeuronInfo objects detailing the neuron's presence across various subnets. + NeuronInfo: Detailed information about the neuron if found, ``None`` otherwise. - This function is valuable for analyzing a neuron's overall participation, influence, and - contributions across the Bittensor network. + This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, offering insights into their roles in the network's consensus and validation mechanisms. """ - netuids = self.get_netuids_for_hotkey(hotkey_ss58, block) - uids = [self.get_uid_for_hotkey_on_subnet(hotkey_ss58, net) for net in netuids] - return [self.neuron_for_uid(uid, net) for uid, net in list(zip(uids, netuids))] + if uid is None: + return NeuronInfo.get_null_neuron() + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + block_hash = None if block is None else self.substrate.get_block_hash(block) + params = [netuid, uid] + if block_hash: + params = params + [block_hash] + return self.substrate.rpc_request( + method="neuronInfo_getNeuron", + params=params, # custom rpc method + ) + + json_body = make_substrate_call_with_retry() + + if not (result := json_body.get("result", None)): + return NeuronInfo.get_null_neuron() + + return NeuronInfo.from_vec_u8(result) - def neuron_has_validator_permit( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> Optional[bool]: + # Community uses this method via `bittensor.api.extrinsics.prometheus.prometheus_extrinsic` + def do_serve_prometheus( + self, + wallet: "Wallet", + call_params: PrometheusServeCallParams, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: """ - Checks if a neuron, identified by its unique identifier (UID), has a validator permit on a specific - subnet within the Bittensor network. This function determines whether the neuron is authorized to - participate in validation processes on the subnet. + Sends a serve prometheus extrinsic to the chain. Args: - uid (int): The unique identifier of the neuron. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. + wallet (:func:`bittensor_wallet.Wallet`): Wallet object. + call_params (:func:`PrometheusServeCallParams`): Prometheus serve call parameters. + wait_for_inclusion (bool): If ``true``, waits for inclusion. + wait_for_finalization (bool): If ``true``, waits for finalization. Returns: - Optional[bool]: ``True`` if the neuron has a validator permit, False otherwise. - - This function is essential for understanding a neuron's role and capabilities within a specific - subnet, particularly regarding its involvement in network validation and governance. + success (bool): ``True`` if serve prometheus was successful. + error (:func:`Optional[str]`): Error message if serve prometheus failed, ``None`` otherwise. """ - _result = self.query_subtensor("ValidatorPermit", block, [netuid, uid]) - return getattr(_result, "value", None) - def neuron_for_wallet( - self, wallet: "Wallet", netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfo]: + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="serve_prometheus", + call_params=call_params, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.hotkey + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if wait_for_inclusion or wait_for_finalization: + response.process_events() + if response.is_success: + return True, None + else: + return False, format_error_message(response.error_message) + else: + return True, None + + return make_substrate_call_with_retry() + + # Community uses this method name + _do_serve_prometheus = do_serve_prometheus + + # Community uses this method + def serve_prometheus( + self, + wallet: "Wallet", + port: int, + netuid: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> bool: """ - Retrieves information about a neuron associated with a given wallet on a specific subnet. - This function provides detailed data about the neuron's status, stake, and activities based on - the wallet's hotkey address. + Serves Prometheus metrics by submitting an extrinsic to a blockchain network via the specified wallet. The function allows configuring whether to wait for the transaction's inclusion in a block and its finalization. Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to perform the query. + wallet (bittensor_wallet.Wallet): Bittensor wallet instance used for submitting the extrinsic. + port (int): The port number on which Prometheus metrics are served. + netuid (int): The unique identifier of the subnetwork. + wait_for_inclusion (bool, optional): If True, waits for the transaction to be included in a block. Defaults to ``False``. + wait_for_finalization (bool, optional): If True, waits for the transaction to be finalized. Defaults to ``True``. Returns: - Optional[NeuronInfo]: Detailed information about the neuron if found, ``None`` otherwise. - - This function is important for wallet owners to understand and manage their neuron's presence - and activities within a particular subnet of the Bittensor network. + bool: Returns True if the Prometheus extrinsic is successfully processed, otherwise False. """ - return self.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid, block=block + return prometheus_extrinsic( + self, + wallet=wallet, + port=port, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) - def neuron_for_uid( - self, uid: Optional[int], netuid: int, block: Optional[int] = None - ) -> NeuronInfo: + # Community uses this method as part of `subtensor.serve_axon` + def do_serve_axon( + self, + wallet: "Wallet", + call_params: AxonServeCallParams, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> Tuple[bool, Optional[str]]: """ - Retrieves detailed information about a specific neuron identified by its unique identifier (UID) - within a specified subnet (netuid) of the Bittensor network. This function provides a comprehensive - view of a neuron's attributes, including its stake, rank, and operational status. + 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. Args: - uid (int): The unique identifier of the neuron. - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. + call_params (AxonServeCallParams): Parameters required for the serve axon call. + 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. Returns: - NeuronInfo: Detailed information about the neuron if found, ``None`` otherwise. + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - This function is crucial for analyzing individual neurons' contributions and status within a specific - subnet, offering insights into their roles in the network's consensus and validation mechanisms. + This function is crucial for initializing and announcing a neuron's Axon service on the network, enhancing the decentralized computation capabilities of Bittensor. """ - if uid is None: - return NeuronInfo.get_null_neuron() @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) def make_substrate_call_with_retry(): - block_hash = None if block is None else self.substrate.get_block_hash(block) - params = [netuid, uid] - if block_hash: - params = params + [block_hash] - return self.substrate.rpc_request( - method="neuronInfo_getNeuron", - params=params, # custom rpc method + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="serve_axon", + call_params=call_params, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.hotkey + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, ) + if wait_for_inclusion or wait_for_finalization: + response.process_events() + if response.is_success: + return True, None + else: + return False, format_error_message(response.error_message) + else: + return True, None - json_body = make_substrate_call_with_retry() - - if not (result := json_body.get("result", None)): - return NeuronInfo.get_null_neuron() + return make_substrate_call_with_retry() - return NeuronInfo.from_vec_u8(result) + # keep backwards compatibility for the community + _do_serve_axon = do_serve_axon - def neurons(self, netuid: int, block: Optional[int] = None) -> List[NeuronInfo]: + # Community uses this method + def serve( + self, + wallet: "Wallet", + ip: str, + port: int, + protocol: int, + netuid: int, + placeholder1: int = 0, + placeholder2: int = 0, + wait_for_inclusion: bool = False, + wait_for_finalization=True, + ) -> bool: """ - Retrieves a list of all neurons within a specified subnet of the Bittensor network. This function - provides a snapshot of the subnet's neuron population, including each neuron's attributes and network - interactions. + Registers a neuron's serving endpoint on the Bittensor network. This function announces the IP address and port where the neuron is available to serve requests, facilitating peer-to-peer communication within the network. Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron being served. + ip (str): The IP address of the serving neuron. + port (int): The port number on which the neuron is serving. + protocol (int): The protocol type used by the neuron (e.g., GRPC, HTTP). + netuid (int): The unique identifier of the subnetwork. + placeholder1 (int, optional): Placeholder parameter for future extensions. Default is ``0``. + placeholder2 (int, optional): Placeholder parameter for future extensions. Default is ``0``. + wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Default is ``False``. + wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Default is ``True``. Returns: - List[NeuronInfo]: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. + bool: ``True`` if the serve registration is successful, False otherwise. - Understanding the distribution and status of neurons within a subnet is key to comprehending the - network's decentralized structure and the dynamics of its consensus and governance processes. + This function is essential for establishing the neuron's presence in the network, enabling it to participate in the decentralized machine learning processes of Bittensor. """ - neurons_lite = self.neurons_lite(netuid=netuid, block=block) - weights = self.weights(block=block, netuid=netuid) - bonds = self.bonds(block=block, netuid=netuid) - - weights_as_dict = {uid: w for uid, w in weights} - bonds_as_dict = {uid: b for uid, b in bonds} - - neurons = [ - NeuronInfo.from_weights_bonds_and_neuron_lite( - neuron_lite, weights_as_dict, bonds_as_dict - ) - for neuron_lite in neurons_lite - ] - - return neurons + return serve_extrinsic( + self, + wallet, + ip, + port, + protocol, + netuid, + placeholder1, + placeholder2, + wait_for_inclusion, + wait_for_finalization, + ) - def neuron_for_uid_lite( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfoLite]: + # Community uses this method + def get_subnet_hyperparameters( + self, netuid: int, block: Optional[int] = None + ) -> Optional[Union[List, SubnetHyperparameters]]: """ - Retrieves a lightweight version of information about a neuron in a specific subnet, identified by - its UID. The 'lite' version focuses on essential attributes such as stake and network activity. + Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define the operational settings and rules governing the subnet's behavior. Args: - uid (int): The unique identifier of the neuron. - netuid (int): The unique identifier of the subnet. + netuid (int): The network UID of the subnet to query. block (Optional[int], optional): The blockchain block number for the query. Returns: - Optional[NeuronInfoLite]: A simplified version of neuron information if found, ``None`` otherwise. + Optional[SubnetHyperparameters]: The subnet's hyperparameters, or ``None`` if not available. - This function is useful for quick and efficient analyses of neuron status and activities within a - subnet without the need for comprehensive data retrieval. + Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how they interact with the network's consensus and incentive mechanisms. """ - if uid is None: - return NeuronInfoLite.get_null_neuron() - hex_bytes_result = self.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neuron_lite", - params={ - "netuid": netuid, - "uid": uid, - }, + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], block=block, ) if hex_bytes_result is None: - return NeuronInfoLite.get_null_neuron() + return [] if hex_bytes_result.startswith("0x"): bytes_result = bytes.fromhex(hex_bytes_result[2:]) else: bytes_result = bytes.fromhex(hex_bytes_result) - return NeuronInfoLite.from_vec_u8(bytes_result) # type: ignore + return SubnetHyperparameters.from_vec_u8(bytes_result) # type: ignore - def neurons_lite( + # Community uses this method + # Returns network ImmunityPeriod hyper parameter. + def immunity_period( self, netuid: int, block: Optional[int] = None - ) -> List[NeuronInfoLite]: + ) -> Optional[int]: """ - Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. - This function provides a streamlined view of the neurons, focusing on key attributes such as stake - and network participation. + Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the duration during which new neurons are protected from certain network penalties or restrictions. Args: netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. + block (Optional[int]): The blockchain block number for the query. Returns: - List[NeuronInfoLite]: A list of simplified neuron information for the subnet. + Optional[int]: The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, ``None`` otherwise. - This function offers a quick overview of the neuron population within a subnet, facilitating - efficient analysis of the network's decentralized structure and neuron dynamics. + The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new participants have a grace period to establish themselves and contribute to the network without facing immediate punitive actions. """ - hex_bytes_result = self.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neurons_lite", - params=[netuid], - block=block, + call = self._get_hyperparameter( + param_name="ImmunityPeriod", netuid=netuid, block=block ) + return None if call is None else int(call) - if hex_bytes_result is None: - return [] + # Community uses this method + def get_uid_for_hotkey_on_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) + Args: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. - return NeuronInfoLite.list_from_vec_u8(bytes_result) # type: ignore + Returns: + Optional[int]: The UID of the neuron if it is registered on the subnet, ``None`` otherwise. - def metagraph( - self, - netuid: int, - lite: bool = True, - block: Optional[int] = None, - ) -> "metagraph": # type: ignore + The UID is a critical identifier within the network, linking the neuron's hotkey to its operational and governance activities on a particular subnet. + """ + _result = self.query_subtensor("Uids", block, [netuid, hotkey_ss58]) + return getattr(_result, "value", None) + + # Community uses this method + def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: """ - Returns a synced metagraph for a specified subnet within the Bittensor network. The metagraph - represents the network's structure, including neuron connections and interactions. + Returns network Tempo hyperparameter. Args: - netuid (int): The network UID of the subnet to query. - lite (bool, default=True): If true, returns a metagraph using a lightweight sync (no weights, no bonds). - block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. + netuid (int): The unique identifier of the subnetwork. + block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. Returns: - bittensor.core.metagraph.Metagraph: The metagraph representing the subnet's structure and neuron relationships. - - The metagraph is an essential tool for understanding the topology and dynamics of the Bittensor - network's decentralized architecture, particularly in relation to neuron interconnectivity and consensus processes. + Optional[int]: The value of the Tempo hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. """ - metagraph_ = Metagraph( - network=self.network, netuid=netuid, lite=lite, sync=False - ) - metagraph_.sync(block=block, lite=lite, subtensor=self) - - return metagraph_ + call = self._get_hyperparameter(param_name="Tempo", netuid=netuid, block=block) + return None if call is None else int(call) - def incentive(self, netuid: int, block: Optional[int] = None) -> List[int]: + # Community uses this method + def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: """ - Retrieves the list of incentives for neurons within a specific subnet of the Bittensor network. - This function provides insights into the reward distribution mechanisms and the incentives allocated - to each neuron based on their contributions and activities. + Retrieves the on-chain commitment for a specific neuron in the Bittensor network. Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. + block (Optional[int]): The block number to retrieve the commitment from. If None, the latest block is used. Default is ``None``. Returns: - List[int]: The list of incentives for neurons within the subnet, indexed by UID. - - Understanding the incentive structure is crucial for analyzing the network's economic model and - the motivational drivers for neuron participation and collaboration. + str: The commitment data as a string. """ - i_map = [] - i_map_encoded = self.query_map_subtensor(name="Incentive", block=block) - if i_map_encoded.records: - for netuid_, incentives_map in i_map_encoded: - if netuid_ == netuid: - i_map = incentives_map.serialize() - break + metagraph = self.metagraph(netuid) + hotkey = metagraph.hotkeys[uid] # type: ignore + + metadata = get_metadata(self, netuid, hotkey, block) + commitment = metadata["info"]["fields"][0] # type: ignore + hex_data = commitment[list(commitment.keys())[0]][2:] # type: ignore - return i_map + return bytes.fromhex(hex_data).decode() - def weights( + # Community uses this via `bittensor.utils.weight_utils.process_weights_for_netuid` function. + def min_allowed_weights( self, netuid: int, block: Optional[int] = None - ) -> List[Tuple[int, List[Tuple[int, int]]]]: + ) -> Optional[int]: """ - Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. - This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the - network's trust and value assignment mechanisms. + Returns network MinAllowedWeights hyperparameter. Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. + netuid (int): The unique identifier of the subnetwork. + block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. Returns: - List[Tuple[int, List[Tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its assigned weights. - - The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, - influencing their influence and reward allocation within the subnet. + Optional[int]: The value of the MinAllowedWeights hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. """ - w_map = [] - w_map_encoded = self.query_map_subtensor( - name="Weights", block=block, params=[netuid] + call = self._get_hyperparameter( + param_name="MinAllowedWeights", block=block, netuid=netuid ) - if w_map_encoded.records: - for uid, w in w_map_encoded: - w_map.append((uid.serialize(), w.serialize())) - - return w_map + return None if call is None else int(call) - def bonds( + # Community uses this via `bittensor.utils.weight_utils.process_weights_for_netuid` function. + def max_weight_limit( self, netuid: int, block: Optional[int] = None - ) -> List[Tuple[int, List[Tuple[int, int]]]]: + ) -> Optional[float]: """ - Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. - Bonds represent the investments or commitments made by neurons in one another, indicating a level - of trust and perceived value. This bonding mechanism is integral to the network's market-based approach - to measuring and rewarding machine intelligence. + Returns network MaxWeightsLimit hyperparameter. Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. + netuid (int): The unique identifier of the subnetwork. + block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. Returns: - List[Tuple[int, List[Tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its bonds with other - neurons. - - Understanding bond distributions is crucial for analyzing the trust dynamics and market behavior - within the subnet. It reflects how neurons recognize and invest in each other's intelligence and - contributions, supporting diverse and niche systems within the Bittensor ecosystem. + Optional[float]: The value of the MaxWeightsLimit hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. """ - b_map = [] - b_map_encoded = self.query_map_subtensor( - name="Bonds", block=block, params=[netuid] + call = self._get_hyperparameter( + param_name="MaxWeightsLimit", block=block, netuid=netuid ) - if b_map_encoded.records: - for uid, b in b_map_encoded: - b_map.append((uid.serialize(), b.serialize())) - - return b_map + return None if call is None else u16_normalized_float(int(call)) - def associated_validator_ip_info( - self, netuid: int, block: Optional[int] = None - ) -> Optional[List["IPInfo"]]: + # # Community uses this method. It is used in subtensor in neuron_info, and serving. + def get_prometheus_info( + self, netuid: int, hotkey_ss58: str, block: Optional[int] = None + ) -> Optional[PrometheusInfo]: """ - Retrieves the list of all validator IP addresses associated with a specific subnet in the Bittensor network. This information is crucial for network communication and the identification of validator nodes. + Returns the prometheus information for this hotkey account. Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. + netuid (int): The unique identifier of the subnetwork. + hotkey_ss58 (str): The SS58 address of the hotkey. + block (Optional[int], optional): The block number to retrieve the prometheus information from. If ``None``, the latest block is used. Default is ``None``. Returns: - Optional[List[IPInfo]]: A list of IPInfo objects for validator nodes in the subnet, or ``None`` if no validators are associated. + Optional[PrometheusInfo]: A PrometheusInfo object containing the prometheus information, or ``None`` if the prometheus information is not found. + """ + result = self.query_subtensor("Prometheus", block, [netuid, hotkey_ss58]) + if result is not None and hasattr(result, "value"): + return PrometheusInfo( + ip=networking.int_to_ip(result.value["ip"]), + ip_type=result.value["ip_type"], + port=result.value["port"], + version=result.value["version"], + block=result.value["block"], + ) + return None - Validator IP information is key for establishing secure and reliable connections within the network, facilitating consensus and validation processes critical for the network's integrity and performance. + # Community uses this method + def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: """ - hex_bytes_result = self.query_runtime_api( - runtime_api="ValidatorIPRuntimeApi", - method="get_associated_validator_ip_info_for_subnet", - params=[netuid], # type: ignore - block=block, - ) + Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. - if hex_bytes_result is None: - return None + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int], optional): The blockchain block number at which to check the subnet's existence. - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) + Returns: + bool: ``True`` if the subnet exists, False otherwise. - return IPInfo.list_from_vec_u8(bytes_result) # type: ignore + This function is critical for verifying the presence of specific subnets in the network, enabling a deeper understanding of the network's structure and composition. + """ + _result = self.query_subtensor("NetworksAdded", block, [netuid]) + return getattr(_result, "value", False) - def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: + # Metagraph uses this method + def bonds( + self, netuid: int, block: Optional[int] = None + ) -> List[Tuple[int, List[Tuple[int, int]]]]: """ - Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost represents the amount of Tao that needs to be locked or burned to establish a new subnet. + Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. Bonds represent the investments or commitments made by neurons in one another, indicating a level of trust and perceived value. This bonding mechanism is integral to the network's market-based approach to measuring and rewarding machine intelligence. Args: + netuid (int): The network UID of the subnet to query. block (Optional[int]): The blockchain block number for the query. Returns: - int: The burn cost for subnet registration. + List[Tuple[int, List[Tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its bonds with other neurons. - The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for controlling the proliferation of subnets and ensuring their commitment to the network's long-term viability. + Understanding bond distributions is crucial for analyzing the trust dynamics and market behavior within the subnet. It reflects how neurons recognize and invest in each other's intelligence and contributions, supporting diverse and niche systems within the Bittensor ecosystem. """ - lock_cost = self.query_runtime_api( - runtime_api="SubnetRegistrationRuntimeApi", - method="get_network_registration_cost", - params=[], - block=block, + b_map = [] + b_map_encoded = self.query_map_subtensor( + name="Bonds", block=block, params=[netuid] ) + if b_map_encoded.records: + for uid, b in b_map_encoded: + b_map.append((uid.serialize(), b.serialize())) - if lock_cost is None: - return None - - return lock_cost - - ############## - # Extrinsics # - ############## - - def _do_delegation( - self, - wallet: "Wallet", - delegate_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """ - Delegates a specified amount of stake to a delegate's hotkey. + return b_map - This method sends a transaction to add stake to a delegate's hotkey and retries the call up to three times with exponential backoff in case of failures. + # Metagraph uses this method + def neurons(self, netuid: int, block: Optional[int] = None) -> List[NeuronInfo]: + """ + Retrieves a list of all neurons within a specified subnet of the Bittensor network. This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and network interactions. Args: - wallet (bittensor_wallet.Wallet): The wallet from which the stake will be delegated. - delegate_ss58 (str): The SS58 address of the delegate's hotkey. - amount (Balance): The amount of stake to be delegated. - wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. + netuid (int): The unique identifier of the subnet. + block (Optional[int], optional): The blockchain block number for the query. Returns: - bool: ``True`` if the delegation is successful, ``False`` otherwise. + List[NeuronInfo]: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. + + Understanding the distribution and status of neurons within a subnet is key to comprehending the network's decentralized structure and the dynamics of its consensus and governance processes. """ + neurons_lite = self.neurons_lite(netuid=netuid, block=block) + weights = self.weights(block=block, netuid=netuid) + bonds = self.bonds(block=block, netuid=netuid) - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={"hotkey": delegate_ss58, "amount_staked": amount.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, + weights_as_dict = {uid: w for uid, w in weights} + bonds_as_dict = {uid: b for uid, b in bonds} + + neurons = [ + NeuronInfo.from_weights_bonds_and_neuron_lite( + neuron_lite, weights_as_dict, bonds_as_dict ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise StakeError(format_error_message(response.error_message)) + for neuron_lite in neurons_lite + ] - return make_substrate_call_with_retry() + return neurons - def _do_undelegation( - self, - wallet: "Wallet", - delegate_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: + # Metagraph uses this method + def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: """ - Removes a specified amount of stake from a delegate's hotkey. - - This method sends a transaction to remove stake from a delegate's hotkey and retries the call up to three times with exponential backoff in case of failures. + Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. Args: - wallet (bittensor_wallet.Wallet): The wallet from which the stake will be removed. - delegate_ss58 (str): The SS58 address of the delegate's hotkey. - amount (Balance): The amount of stake to be removed. - wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. + block (Optional[int], optional): The blockchain block number for the query. Returns: - bool: ``True`` if the undelegation is successful, ``False`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": delegate_ss58, - "amount_unstaked": amount.rao, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise StakeError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() + int: The total number of subnets in the network. - def _do_nominate( - self, - wallet: "Wallet", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: + Understanding the total number of subnets is essential for assessing the network's growth and the extent of its decentralized infrastructure. """ - Nominates the wallet's hotkey to become a delegate. + _result = self.query_subtensor("TotalNetworks", block) + return getattr(_result, "value", None) - This method sends a transaction to nominate the wallet's hotkey to become a delegate and retries the call up to three times with exponential backoff in case of failures. + # Metagraph uses this method + def get_subnets(self, block: Optional[int] = None) -> List[int]: + """ + Retrieves a list of all subnets currently active within the Bittensor network. This function provides an overview of the various subnets and their identifiers. Args: - wallet (bittensor_wallet.Wallet): The wallet whose hotkey will be nominated. - wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. + block (Optional[int], optional): The blockchain block number for the query. Returns: - bool: ``True`` if the nomination is successful, ``False`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="become_delegate", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) # sign with coldkey - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise NominationError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() + List[int]: A list of network UIDs representing each active subnet. - def _do_increase_take( - self, - wallet: "Wallet", - hotkey_ss58: str, - take: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: + This function is valuable for understanding the network's structure and the diversity of subnets available for neuron participation and collaboration. """ - Increases the take rate for a delegate's hotkey. + result = self.query_map_subtensor("NetworksAdded", block) + return ( + [network[0].value for network in result.records] + if result and hasattr(result, "records") + else [] + ) - This method sends a transaction to increase the take rate for a delegate's hotkey and retries the call up to three times with exponential backoff in case of failures. + # Metagraph uses this method + def neurons_lite( + self, netuid: int, block: Optional[int] = None + ) -> List[NeuronInfoLite]: + """ + Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network participation. Args: - wallet (bittensor_wallet.Wallet): The wallet from which the transaction will be signed. - hotkey_ss58 (str): The SS58 address of the delegate's hotkey. - take (int): The new take rate to be set. - wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. + netuid (int): The unique identifier of the subnet. + block (Optional[int], optional): The blockchain block number for the query. Returns: - bool: ``True`` if the take rate increase is successful, ``False`` otherwise. + List[NeuronInfoLite]: A list of simplified neuron information for the subnet. + + This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis of the network's decentralized structure and neuron dynamics. """ + hex_bytes_result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons_lite", + params=[netuid], + block=block, + ) - @retry(delay=1, tries=3, backoff=2, max_delay=4) - def make_substrate_call_with_retry(): - with self.substrate as substrate: - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="increase_take", - call_params={ - "hotkey": hotkey_ss58, - "take": take, - }, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) # sign with coldkey - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise TakeError(format_error_message(response.error_message)) + if hex_bytes_result is None: + return [] - return make_substrate_call_with_retry() + if hex_bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + else: + bytes_result = bytes.fromhex(hex_bytes_result) - def _do_decrease_take( - self, - wallet: "Wallet", - hotkey_ss58: str, - take: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """ - Decreases the take rate for a delegate's hotkey. + return NeuronInfoLite.list_from_vec_u8(bytes_result) # type: ignore - This method sends a transaction to decrease the take rate for a delegate's hotkey and retries the call up to three times with exponential backoff in case of failures. + # Used in the `neurons` method which is used in metagraph.py + def weights( + self, netuid: int, block: Optional[int] = None + ) -> List[Tuple[int, List[Tuple[int, int]]]]: + """ + Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust and value assignment mechanisms. Args: - wallet (bittensor_wallet.Wallet): The wallet from which the transaction will be signed. - hotkey_ss58 (str): The SS58 address of the delegate's hotkey. - take (int): The new take rate to be set. - wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. + netuid (int): The network UID of the subnet to query. + block (Optional[int]): The blockchain block number for the query. Returns: - bool: ``True`` if the take rate decrease is successful, ``False`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4) - def make_substrate_call_with_retry(): - with self.substrate as substrate: - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="decrease_take", - call_params={ - "hotkey": hotkey_ss58, - "take": take, - }, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) # sign with coldkey - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise TakeError(format_error_message(response.error_message)) + List[Tuple[int, List[Tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its assigned weights. - return make_substrate_call_with_retry() + The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, influencing their influence and reward allocation within the subnet. + """ + w_map = [] + w_map_encoded = self.query_map_subtensor( + name="Weights", block=block, params=[netuid] + ) + if w_map_encoded.records: + for uid, w in w_map_encoded: + w_map.append((uid.serialize(), w.serialize())) - ########## - # Legacy # - ########## + return w_map + # Used by community via `transfer_extrinsic` def get_balance(self, address: str, block: Optional[int] = None) -> Balance: """ Retrieves the token balance of a specific address within the Bittensor network. This function queries the blockchain to determine the amount of Tao held by a given account. @@ -5416,6 +1748,7 @@ def make_substrate_call_with_retry(): ) result = make_substrate_call_with_retry() + except RemainingScaleBytesNotEmptyException: logging.error( "Received a corrupted message. This likely points to an error with the network or subnet." @@ -5423,92 +1756,77 @@ def make_substrate_call_with_retry(): return Balance(1000) return Balance(result.value["data"]["free"]) - def get_current_block(self) -> int: - """ - Returns the current block number on the Bittensor blockchain. This function provides the latest block number, indicating the most recent state of the blockchain. - - Returns: - int: The current chain block number. - - Knowing the current block number is essential for querying real-time data and performing time-sensitive operations on the blockchain. It serves as a reference point for network activities and data synchronization. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - return self.substrate.get_block_number(None) # type: ignore - - return make_substrate_call_with_retry() - - def get_balances(self, block: Optional[int] = None) -> Dict[str, Balance]: + # Used in community via `bittensor.core.subtensor.Subtensor.transfer` + def get_transfer_fee( + self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] + ) -> "Balance": """ - Retrieves the token balances of all accounts within the Bittensor network as of a specific blockchain block. - This function provides a comprehensive view of the token distribution among different accounts. + Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This function simulates the transfer to estimate the associated cost, taking into account the current network conditions and transaction complexity. Args: - block (int, optional): The blockchain block number at which to perform the query. + wallet (bittensor_wallet.Wallet): The wallet from which the transfer is initiated. + dest (str): The ``SS58`` address of the destination account. + value (Union[Balance, float, int]): The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao (int) units. Returns: - Dict[str, Balance]: A dictionary mapping each account's ``ss58`` address to its balance. + Balance: The estimated transaction fee for the transfer, represented as a Balance object. - This function is valuable for analyzing the overall economic landscape of the Bittensor network, including the distribution of financial resources and the financial status of network participants. + Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the wallet has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. """ + if isinstance(value, float): + value = Balance.from_tao(value) + elif isinstance(value, int): + value = Balance.from_rao(value) - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - return self.substrate.query_map( - module="System", - storage_function="Account", - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), + if isinstance(value, Balance): + call = self.substrate.compose_call( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": dest, "value": value.rao}, ) - result = make_substrate_call_with_retry() - return_dict = {} - for r in result: - bal = Balance(int(r[1]["data"]["free"].value)) - return_dict[r[0].value] = bal - return return_dict + try: + payment_info = self.substrate.get_payment_info( + call=call, keypair=wallet.coldkeypub + ) + except Exception as e: + settings.bt_console.print( + ":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n {}[/bold white]".format( + e + ) + ) + payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao - # TODO: check with the team if this is used anywhere externally. not in bittensor - @staticmethod - def _null_neuron() -> NeuronInfo: - neuron = NeuronInfo( - uid=0, - netuid=0, - active=0, - stake=Balance(0), - rank=0, - emission=0, - incentive=0, - consensus=0, - trust=0, - validator_trust=0, - dividends=0, - last_update=0, - validator_permit=False, - weights=[], - bonds=[], - prometheus_info=None, - axon_info=None, - is_null=True, - coldkey="000000000000000000000000000000000000000000000000", - hotkey="000000000000000000000000000000000000000000000000", - ) # type: ignore - return neuron + fee = Balance.from_rao(payment_info["partialFee"]) + return fee + else: + fee = Balance.from_rao(int(2e7)) + logging.error( + "To calculate the transaction fee, the value must be Balance, float, or int. Received type: %s. Fee " + "is %s", + type(value), + 2e7, + ) + return fee - def get_block_hash(self, block_id: int) -> str: + # Used in community via `bittensor.core.subtensor.Subtensor.transfer` + def get_existential_deposit( + self, block: Optional[int] = None + ) -> Optional["Balance"]: """ - Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique - identifier representing the cryptographic hash of the block's content, ensuring its integrity and - immutability. + Retrieves the existential deposit amount for the Bittensor blockchain. The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. Accounts with balances below this threshold can be reaped to conserve network resources. Args: - block_id (int): The block number for which the hash is to be retrieved. + block (Optional[int]): Block number at which to query the deposit amount. If ``None``, the current block is used. Returns: - str: The cryptographic hash of the specified block. + Optional[Balance]: The existential deposit amount, or ``None`` if the query fails. - The block hash is a fundamental aspect of blockchain technology, providing a secure reference to each block's data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the trustworthiness of the blockchain. + The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of storage and preventing the proliferation of dust accounts. """ - return self.substrate.get_block_hash(block_id=block_id) + result = self.query_constant( + module_name="Balances", constant_name="ExistentialDeposit", block=block + ) + if result is None or not hasattr(result, "value"): + return None + return Balance.from_rao(result.value) diff --git a/bittensor/core/synapse.py b/bittensor/core/synapse.py index 9cfd292aa..53fa8a14c 100644 --- a/bittensor/core/synapse.py +++ b/bittensor/core/synapse.py @@ -79,7 +79,7 @@ def cast_int(raw: str) -> int: int or None: The converted integer, or ``None`` if the input was ``None``. """ - return int(raw) if raw is not None else raw # type: ignore + return int(raw) if raw is not None else raw def cast_float(raw: str) -> float: @@ -95,7 +95,7 @@ def cast_float(raw: str) -> float: float or None: The converted float, or ``None`` if the input was ``None``. """ - return float(raw) if raw is not None else raw # type: ignore + return float(raw) if raw is not None else raw class TerminalInfo(BaseModel): diff --git a/bittensor/core/tensor.py b/bittensor/core/tensor.py index 3eebd404c..e50947e71 100644 --- a/bittensor/core/tensor.py +++ b/bittensor/core/tensor.py @@ -72,9 +72,7 @@ def _add_torch(self): def cast_dtype(raw: Union[None, np.dtype, "torch.dtype", str]) -> Optional[str]: """ - Casts the raw value to a string representing the - `numpy data type `_, or the - `torch data type `_ if using torch. + Casts the raw value to a string representing the `numpy data type `_, or the `torch data type `_ if using torch. Args: raw (Union[None, numpy.dtype, torch.dtype, str]): The raw value to cast. diff --git a/bittensor/core/threadpool.py b/bittensor/core/threadpool.py index 0b10a622a..4a8b1d302 100644 --- a/bittensor/core/threadpool.py +++ b/bittensor/core/threadpool.py @@ -21,7 +21,7 @@ from bittensor.core.config import Config from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME -from bittensor.core.settings import blocktime +from bittensor.core.settings import BLOCKTIME # Workers are created as daemon threads. This is done to allow the interpreter # to exit when there are still idle threads in a ThreadPoolExecutor's thread @@ -55,7 +55,7 @@ def run(self): """Run the given work item""" # Checks if future is canceled or if work item is stale if (not self.future.set_running_or_notify_cancel()) or ( - time.time() - self.start_time > blocktime + time.time() - self.start_time > BLOCKTIME ): return diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 80f478fe9..e4be85346 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -23,7 +23,7 @@ from substrateinterface import Keypair as Keypair from substrateinterface.utils import ss58 -from bittensor.core.settings import ss58_format +from bittensor.core.settings import SS58_FORMAT from .registration import torch, use_torch from .version import version_checking, check_version, VersionCheckError @@ -43,7 +43,7 @@ def _unbiased_topk( values: Union[np.ndarray, "torch.Tensor"], k: int, dim=0, - sorted=True, + sorted_=True, largest=True, axis=0, return_type: str = "numpy", @@ -53,7 +53,7 @@ def _unbiased_topk( values: (np.ndarray) if using numpy, (torch.Tensor) if using torch: Values to index into. k (int): Number to take. dim (int): Dimension to index into (used by Torch) - sorted (bool): Whether to sort indices. + sorted_ (bool): Whether to sort indices. largest (bool): Whether to take the largest value. axis (int): Axis along which to index into (used by Numpy) return_type (str): Whether or use torch or numpy approach @@ -66,7 +66,7 @@ def _unbiased_topk( permutation = torch.randperm(values.shape[dim]) permuted_values = values[permutation] topk, indices = torch.topk( - permuted_values, k, dim=dim, sorted=sorted, largest=largest + permuted_values, k, dim=dim, sorted=sorted_, largest=largest ) return topk, permutation[indices] else: @@ -77,7 +77,7 @@ def _unbiased_topk( permutation = np.random.permutation(values.shape[axis]) permuted_values = np.take(values, permutation, axis=axis) indices = np.argpartition(permuted_values, -k, axis=axis)[-k:] - if not sorted: + if not sorted_: indices = np.sort(indices, axis=axis) if not largest: indices = indices[::-1] @@ -89,38 +89,30 @@ def unbiased_topk( values: Union[np.ndarray, "torch.Tensor"], k: int, dim: int = 0, - sorted: bool = True, + sorted_: bool = True, largest: bool = True, axis: int = 0, ) -> Union[Tuple[np.ndarray, np.ndarray], Tuple["torch.Tensor", "torch.LongTensor"]]: """Selects topk as in torch.topk but does not bias lower indices when values are equal. Args: - values: (np.ndarray) if using numpy, (torch.Tensor) if using torch: - Values to index into. - k: (int): - Number to take. - dim: (int): - Dimension to index into (used by Torch) - sorted: (bool): - Whether to sort indices. - largest: (bool): - Whether to take the largest value. - axis: (int): - Axis along which to index into (used by Numpy) + values: (np.ndarray) if using numpy, (torch.Tensor) if using torch: Values to index into. + k: (int): Number to take. + dim: (int): Dimension to index into (used by Torch) + sorted_: (bool): Whether to sort indices. + largest: (bool): Whether to take the largest value. + axis: (int): Axis along which to index into (used by Numpy) Return: - topk: (np.ndarray) if using numpy, (torch.Tensor) if using torch: - topk k values. - indices: (np.ndarray) if using numpy, (torch.LongTensor) if using torch: - indices of the topk values. + topk: (np.ndarray) if using numpy, (torch.Tensor) if using torch: topk k values. + indices: (np.ndarray) if using numpy, (torch.LongTensor) if using torch: indices of the topk values. """ if use_torch(): return _unbiased_topk( - values, k, dim, sorted, largest, axis, return_type="torch" + values, k, dim, sorted_, largest, axis, return_type="torch" ) else: return _unbiased_topk( - values, k, dim, sorted, largest, axis, return_type="numpy" + values, k, dim, sorted_, largest, axis, return_type="numpy" ) @@ -160,7 +152,7 @@ def strtobool(val: str) -> Union[bool, Literal["==SUPRESS=="]]: def get_explorer_root_url_by_network_from_map( network: str, network_map: Dict[str, Dict[str, str]] ) -> Optional[Dict[str, str]]: - r""" + """ Returns the explorer root url for the given network name from the given network map. Args: @@ -182,7 +174,7 @@ def get_explorer_root_url_by_network_from_map( def get_explorer_url_for_network( network: str, block_hash: str, network_map: Dict[str, str] ) -> Optional[List[str]]: - r""" + """ Returns the explorer url for the given block hash and network. Args: @@ -217,27 +209,27 @@ def get_explorer_url_for_network( def ss58_address_to_bytes(ss58_address: str) -> bytes: """Converts a ss58 address to a bytes object.""" - account_id_hex: str = scalecodec.ss58_decode(ss58_address, ss58_format) + account_id_hex: str = scalecodec.ss58_decode(ss58_address, SS58_FORMAT) return bytes.fromhex(account_id_hex) -def U16_NORMALIZED_FLOAT(x: int) -> float: +def u16_normalized_float(x: int) -> float: return float(x) / float(U16_MAX) -def U64_NORMALIZED_FLOAT(x: int) -> float: +def u64_normalized_float(x: int) -> float: return float(x) / float(U64_MAX) def u8_key_to_ss58(u8_key: List[int]) -> str: - r""" + """ Converts a u8-encoded account key to an ss58 address. Args: u8_key (List[int]): The u8-encoded account key. """ # First byte is length, then 32 bytes of key. - return scalecodec.ss58_encode(bytes(u8_key).hex(), ss58_format) + return scalecodec.ss58_encode(bytes(u8_key).hex(), SS58_FORMAT) def get_hash(content, encoding="utf-8"): @@ -301,7 +293,7 @@ def create_identity_dict( dict: A dictionary with the specified structure and byte string conversions. Raises: - ValueError: If pgp_fingerprint is not exactly 20 bytes long when encoded. + ValueError: If pgp_fingerprint is not exactly 20 bytes long when encoded. """ if pgp_fingerprint and len(pgp_fingerprint.encode()) != 20: raise ValueError("pgp_fingerprint must be exactly 20 bytes long when encoded") @@ -348,7 +340,7 @@ def is_valid_ss58_address(address: str) -> bool: """ try: return ss58.is_valid_ss58_address( - address, valid_ss58_format=ss58_format + address, valid_ss58_format=SS58_FORMAT ) or ss58.is_valid_ss58_address( address, valid_ss58_format=42 ) # Default substrate ss58 format (legacy) @@ -377,7 +369,7 @@ def _is_valid_ed25519_pubkey(public_key: Union[str, bytes]) -> bool: else: raise ValueError("public_key must be a string or bytes") - keypair = Keypair(public_key=public_key, ss58_format=ss58_format) + keypair = Keypair(public_key=public_key, ss58_format=SS58_FORMAT) ss58_addr = keypair.ss58_address return ss58_addr is not None diff --git a/bittensor/utils/_register_cuda.py b/bittensor/utils/_register_cuda.py deleted file mode 100644 index 05619416e..000000000 --- a/bittensor/utils/_register_cuda.py +++ /dev/null @@ -1,126 +0,0 @@ -import binascii -import hashlib -import math -from typing import Tuple - -import numpy as np -from Crypto.Hash import keccak - -from contextlib import redirect_stdout -import io - - -def solve_cuda( - nonce_start: np.int64, - update_interval: np.int64, - tpb: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - dev_id: int = 0, -) -> Tuple[np.int64, bytes]: - """ - Solves the PoW problem using CUDA. - Args: - nonce_start: int64 - Starting nonce. - update_interval: int64 - Number of nonces to solve before updating block information. - tpb: int - Threads per block. - block_and_hotkey_hash_bytes: bytes - Keccak(Bytes of the block hash + bytes of the hotkey) 64 bytes. - difficulty: int256 - Difficulty of the PoW problem. - limit: int256 - Upper limit of the nonce. - dev_id: int (default=0) - The CUDA device ID - Returns: - Tuple[int64, bytes] - Tuple of the nonce and the seal corresponding to the solution. - Returns -1 for nonce if no solution is found. - """ - - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - - upper = int(limit // difficulty) - - upper_bytes = upper.to_bytes(32, byteorder="little", signed=False) - - def _hex_bytes_to_u8_list(hex_bytes: bytes): - hex_chunks = [ - int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2) - ] - return hex_chunks - - def _create_seal_hash(block_and_hotkey_hash_hex: bytes, nonce: int) -> bytes: - nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little")) - pre_seal = nonce_bytes + block_and_hotkey_hash_hex - seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest() - kec = keccak.new(digest_bits=256) - seal = kec.update(seal_sh256).digest() - return seal - - def _seal_meets_difficulty(seal: bytes, difficulty: int): - seal_number = int.from_bytes(seal, "big") - product = seal_number * difficulty - limit = int(math.pow(2, 256)) - 1 - - return product < limit - - # Call cython function - # int blockSize, uint64 nonce_start, uint64 update_interval, const unsigned char[:] limit, - # const unsigned char[:] block_bytes, int dev_id - block_and_hotkey_hash_hex = binascii.hexlify(block_and_hotkey_hash_bytes)[:64] - - solution = cubit.solve_cuda( - tpb, - nonce_start, - update_interval, - upper_bytes, - block_and_hotkey_hash_hex, - dev_id, - ) # 0 is first GPU - seal = None - if solution != -1: - seal = _create_seal_hash(block_and_hotkey_hash_hex, solution) - if _seal_meets_difficulty(seal, difficulty): - return solution, seal - else: - return -1, b"\x00" * 32 - - return solution, seal - - -def reset_cuda(): - """ - Resets the CUDA environment. - """ - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - - cubit.reset_cuda() - - -def log_cuda_errors() -> str: - """ - Logs any CUDA errors. - """ - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - - f = io.StringIO() - with redirect_stdout(f): - cubit.log_cuda_errors() - - s = f.getvalue() - - return s diff --git a/bittensor/utils/backwards_compatibility.py b/bittensor/utils/backwards_compatibility/__init__.py similarity index 86% rename from bittensor/utils/backwards_compatibility.py rename to bittensor/utils/backwards_compatibility/__init__.py index ca45a4599..872ef88a2 100644 --- a/bittensor/utils/backwards_compatibility.py +++ b/bittensor/utils/backwards_compatibility/__init__.py @@ -21,8 +21,8 @@ features in recent versions, allowing users to maintain compatibility with older systems and projects. """ -import sys import importlib +import sys from bittensor_wallet.errors import KeyFileError # noqa: F401 from bittensor_wallet.keyfile import ( # noqa: F401 @@ -92,7 +92,7 @@ UnstakeError, ) from bittensor.core.metagraph import Metagraph -from bittensor.core.settings import blocktime +from bittensor.core.settings import BLOCKTIME from bittensor.core.stream import StreamingSynapse # noqa: F401 from bittensor.core.subtensor import Subtensor from bittensor.core.synapse import TerminalInfo, Synapse # noqa: F401 @@ -100,7 +100,6 @@ from bittensor.core.threadpool import ( # noqa: F401 PriorityThreadPoolExecutor as PriorityThreadPoolExecutor, ) -from bittensor.utils.mock.subtensor_mock import MockSubtensor as MockSubtensor # noqa: F401 from bittensor.utils import ( # noqa: F401 ss58_to_vec_u8, unbiased_topk, @@ -111,14 +110,14 @@ get_explorer_root_url_by_network_from_map, get_explorer_url_for_network, ss58_address_to_bytes, - U16_NORMALIZED_FLOAT, - U64_NORMALIZED_FLOAT, + u16_normalized_float, + u64_normalized_float, u8_key_to_ss58, get_hash, ) from bittensor.utils.balance import Balance as Balance # noqa: F401 -from bittensor.utils.subnets import SubnetsAPI # noqa: F401 - +from bittensor.utils.mock.subtensor_mock import MockSubtensor as MockSubtensor # noqa: F401 +from .subnets import SubnetsAPI # noqa: F401 # Backwards compatibility with previous bittensor versions. axon = Axon @@ -129,22 +128,22 @@ 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 +__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 +__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 +__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 +__tao_symbol__ = settings.TAO_SYMBOL +__rao_symbol__ = settings.RAO_SYMBOL # Makes the `bittensor.api.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. extrinsics = importlib.import_module("bittensor.api.extrinsics") diff --git a/bittensor/utils/subnets.py b/bittensor/utils/backwards_compatibility/subnets.py similarity index 90% rename from bittensor/utils/subnets.py rename to bittensor/utils/backwards_compatibility/subnets.py index e550f4c77..8622379fa 100644 --- a/bittensor/utils/subnets.py +++ b/bittensor/utils/backwards_compatibility/subnets.py @@ -16,16 +16,18 @@ # DEALINGS IN THE SOFTWARE. from abc import ABC, abstractmethod -from typing import Any, List, Union, Optional - -from bittensor_wallet import Wallet +from typing import Any, List, Union, Optional, TYPE_CHECKING from bittensor.core.axon import Axon from bittensor.core.dendrite import Dendrite -from bittensor.core.synapse import Synapse from bittensor.utils.btlogging import logging +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.synapse import Synapse + +# Community uses this class class SubnetsAPI(ABC): """This class is not used within the bittensor package, but is actively used by the community.""" @@ -38,17 +40,11 @@ async def __call__(self, *args, **kwargs): @abstractmethod def prepare_synapse(self, *args, **kwargs) -> Any: - """ - Prepare the synapse-specific payload. - """ - ... + """Prepare the synapse-specific payload.""" @abstractmethod def process_responses(self, responses: List[Union["Synapse", Any]]) -> Any: - """ - Process the responses from the network. - """ - ... + """Process the responses from the network.""" async def query_api( self, diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index f95efbbc7..90c851814 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -1,16 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021-2022 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 @@ -19,7 +17,7 @@ from typing import Union -from ..core import settings +from bittensor.core import settings class Balance: @@ -35,8 +33,8 @@ class Balance: tao: A float property that gives the balance in tao units. """ - unit: str = settings.tao_symbol # This is the tao unit - rao_unit: str = settings.rao_symbol # This is the rao unit + unit: str = settings.TAO_SYMBOL # This is the tao unit + rao_unit: str = settings.RAO_SYMBOL # This is the rao unit rao: int tao: float @@ -61,21 +59,15 @@ def tao(self): return self.rao / pow(10, 9) def __int__(self): - """ - Convert the Balance object to an int. The resulting value is in rao. - """ + """Convert the Balance object to an int. The resulting value is in rao.""" return self.rao def __float__(self): - """ - Convert the Balance object to a float. The resulting value is in tao. - """ + """Convert the Balance object to a float. The resulting value is in tao.""" return self.tao def __str__(self): - """ - Returns the Balance object as a string in the format "symbolvalue", where the value is in tao. - """ + """Returns the Balance object as a string in the format "symbolvalue", where the value is in tao.""" return f"{self.unit}{float(self.tao):,.9f}" def __rich__(self): @@ -226,12 +218,6 @@ def __rfloordiv__(self, other: Union[int, float, "Balance"]): except (ValueError, TypeError): raise NotImplementedError("Unsupported type") - def __int__(self) -> int: - return self.rao - - def __float__(self) -> float: - return self.tao - def __nonzero__(self) -> bool: return bool(self.rao) diff --git a/bittensor/utils/formatting.py b/bittensor/utils/formatting.py deleted file mode 100644 index f0a22d094..000000000 --- a/bittensor/utils/formatting.py +++ /dev/null @@ -1,36 +0,0 @@ -import math - - -def get_human_readable(num, suffix="H"): - for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]: - if abs(num) < 1000.0: - return f"{num:3.1f}{unit}{suffix}" - num /= 1000.0 - return f"{num:.1f}Y{suffix}" - - -def millify(n: int): - millnames = ["", " K", " M", " B", " T"] - n = float(n) - millidx = max( - 0, - min( - len(millnames) - 1, int(math.floor(0 if n == 0 else math.log10(abs(n)) / 3)) - ), - ) - - return "{:.2f}{}".format(n / 10 ** (3 * millidx), millnames[millidx]) - - -def convert_blocks_to_time(blocks: int, block_time: int = 12) -> tuple[int, int, int]: - """ - Converts number of blocks into number of hours, minutes, seconds. - :param blocks: number of blocks - :param block_time: time per block, by default this is 12 - :return: tuple containing number of hours, number of minutes, number of seconds - """ - seconds = blocks * block_time - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - remaining_seconds = seconds % 60 - return hours, minutes, remaining_seconds diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py index dcc8112f8..0e4939efa 100644 --- a/bittensor/utils/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -15,11 +15,9 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from abc import abstractclassmethod 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, Dict, List, Optional, Tuple, Union from typing import TypedDict @@ -31,15 +29,12 @@ NeuronInfo, NeuronInfoLite, PrometheusInfo, - DelegateInfo, - SubnetInfo, AxonInfo, ) -from bittensor.core.subtensor import Subtensor from bittensor.core.errors import ChainQueryError -from bittensor.utils import RAOPERTAO, U16_NORMALIZED_FLOAT +from bittensor.core.subtensor import Subtensor +from bittensor.utils import RAOPERTAO, u16_normalized_float from bittensor.utils.balance import Balance -from bittensor.utils.registration import POWSolution # Mock Testing Constant __GLOBAL_MOCK_STATE__ = {} @@ -69,7 +64,7 @@ class PrometheusServeCallParams(TypedDict): class InfoDict(Mapping): - @abstractclassmethod + @classmethod def default(cls): raise NotImplementedError @@ -276,6 +271,7 @@ def setup(self) -> None: self.substrate = MagicMock() def __init__(self, *args, **kwargs) -> None: + super().__init__() self.__dict__ = __GLOBAL_MOCK_STATE__ if not hasattr(self, "chain_state") or getattr(self, "chain_state") is None: @@ -376,104 +372,6 @@ 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): @@ -484,37 +382,6 @@ 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]]: @@ -737,13 +604,6 @@ def get_balance(self, address: str, block: int = None) -> "Balance": else: return Balance(0) - def get_balances(self, block: int = None) -> Dict[str, "Balance"]: - balances = {} - for address in self.chain_state["System"]["Account"]: - balances[address] = self.get_balance(address, block) - - return balances - # ==== Neuron RPC methods ==== def neuron_for_uid( @@ -915,13 +775,13 @@ def _neuron_subnet_exists( weights = [[int(weight[0]), int(weight[1])] for weight in weights] bonds = [[int(bond[0]), int(bond[1])] for bond in bonds] - rank = U16_NORMALIZED_FLOAT(rank) + rank = u16_normalized_float(rank) emission = emission / RAOPERTAO - incentive = U16_NORMALIZED_FLOAT(incentive) - consensus = U16_NORMALIZED_FLOAT(consensus) - trust = U16_NORMALIZED_FLOAT(trust) - validator_trust = U16_NORMALIZED_FLOAT(validator_trust) - dividends = U16_NORMALIZED_FLOAT(dividends) + incentive = u16_normalized_float(incentive) + consensus = u16_normalized_float(consensus) + trust = u16_normalized_float(trust) + validator_trust = u16_normalized_float(validator_trust) + dividends = u16_normalized_float(dividends) prometheus_info = PrometheusInfo.fix_decoded_values(prometheus_info) axon_info_ = AxonInfo.from_neuron_info( {"hotkey": hotkey, "coldkey": coldkey, "axon_info": axon_info_} @@ -955,32 +815,6 @@ def _neuron_subnet_exists( return neuron_info - def neuron_for_uid_lite( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfoLite]: - if block: - if self.block_number < block: - raise Exception("Cannot query block in the future") - - else: - block = self.block_number - - if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: - raise Exception("Subnet does not exist") - - neuron_info = self._neuron_subnet_exists(uid, netuid, block) - if neuron_info is None: - return None - - else: - neuron_info_dict = neuron_info.__dict__ - del neuron_info - del neuron_info_dict["weights"] - del neuron_info_dict["bonds"] - - neuron_info_lite = NeuronInfoLite(**neuron_info_dict) - return neuron_info_lite - def neurons_lite( self, netuid: int, block: Optional[int] = None ) -> List[NeuronInfoLite]: @@ -998,78 +832,12 @@ def neurons_lite( return neurons - # Extrinsics - def _do_delegation( - self, - wallet: "Wallet", - delegate_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - # Check if delegate - if not self.is_hotkey_delegate(hotkey_ss58=delegate_ss58): - raise Exception("Not a delegate") - - # do stake - success = self._do_stake( - wallet=wallet, - hotkey_ss58=delegate_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - return success - - def _do_undelegation( - self, - wallet: "Wallet", - delegate_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - # Check if delegate - if not self.is_hotkey_delegate(hotkey_ss58=delegate_ss58): - raise Exception("Not a delegate") - - # do unstake - self._do_unstake( - wallet=wallet, - hotkey_ss58=delegate_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - def _do_nominate( - self, - wallet: "Wallet", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - hotkey_ss58 = wallet.hotkey.ss58_address - coldkey_ss58 = wallet.coldkeypub.ss58_address - - subtensor_state = self.chain_state["SubtensorModule"] - if self.is_hotkey_delegate(hotkey_ss58=hotkey_ss58): - return True - - else: - subtensor_state["Delegates"][hotkey_ss58] = {} - subtensor_state["Delegates"][hotkey_ss58][self.block_number] = ( - 0.18 # Constant for now - ) - - return True - def get_transfer_fee( self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] ) -> "Balance": return Balance(700) - def _do_transfer( + def do_transfer( self, wallet: "Wallet", dest: str, @@ -1101,204 +869,6 @@ def _do_transfer( return True, None, None - def _do_pow_register( - self, - netuid: int, - wallet: "Wallet", - pow_result: "POWSolution", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - # Assume pow result is valid - - subtensor_state = self.chain_state["SubtensorModule"] - if netuid not in subtensor_state["NetworksAdded"]: - raise Exception("Subnet does not exist") - - self._register_neuron( - netuid=netuid, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - return True, None - - def _do_burned_register( - self, - netuid: int, - wallet: "Wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - subtensor_state = self.chain_state["SubtensorModule"] - if netuid not in subtensor_state["NetworksAdded"]: - raise Exception("Subnet does not exist") - - bal = self.get_balance(wallet.coldkeypub.ss58_address) - burn = self.recycle(netuid=netuid) - existential_deposit = self.get_existential_deposit() - - if bal < burn + existential_deposit: - raise Exception("Insufficient funds") - - self._register_neuron( - netuid=netuid, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - # Burn the funds - self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"][ - "free" - ][self.block_number] = (bal - burn).rao - - return True, None - - def _do_stake( - self, - wallet: "Wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - subtensor_state = self.chain_state["SubtensorModule"] - - bal = self.get_balance(wallet.coldkeypub.ss58_address) - curr_stake = self.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address - ) - if curr_stake is None: - curr_stake = Balance(0) - existential_deposit = self.get_existential_deposit() - - if bal < amount + existential_deposit: - raise Exception("Insufficient funds") - - stake_state = subtensor_state["Stake"] - - # Stake the funds - if not hotkey_ss58 in stake_state: - stake_state[hotkey_ss58] = {} - if not wallet.coldkeypub.ss58_address in stake_state[hotkey_ss58]: - stake_state[hotkey_ss58][wallet.coldkeypub.ss58_address] = {} - - stake_state[hotkey_ss58][wallet.coldkeypub.ss58_address][self.block_number] = ( - amount.rao - ) - - # Add to total_stake storage - subtensor_state["TotalStake"][self.block_number] = ( - self._get_most_recent_storage(subtensor_state["TotalStake"]) + amount.rao - ) - - total_hotkey_stake_state = subtensor_state["TotalHotkeyStake"] - if not hotkey_ss58 in total_hotkey_stake_state: - total_hotkey_stake_state[hotkey_ss58] = {} - - total_coldkey_stake_state = subtensor_state["TotalColdkeyStake"] - if not wallet.coldkeypub.ss58_address in total_coldkey_stake_state: - total_coldkey_stake_state[wallet.coldkeypub.ss58_address] = {} - - curr_total_hotkey_stake = self.query_subtensor( - name="TotalHotkeyStake", - params=[hotkey_ss58], - block=min(self.block_number - 1, 0), - ) - curr_total_coldkey_stake = self.query_subtensor( - name="TotalColdkeyStake", - params=[wallet.coldkeypub.ss58_address], - block=min(self.block_number - 1, 0), - ) - - total_hotkey_stake_state[hotkey_ss58][self.block_number] = ( - curr_total_hotkey_stake.value + amount.rao - ) - total_coldkey_stake_state[wallet.coldkeypub.ss58_address][self.block_number] = ( - curr_total_coldkey_stake.value + amount.rao - ) - - # Remove from free balance - self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"][ - "free" - ][self.block_number] = (bal - amount).rao - - return True - - def _do_unstake( - self, - wallet: "Wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - subtensor_state = self.chain_state["SubtensorModule"] - - bal = self.get_balance(wallet.coldkeypub.ss58_address) - curr_stake = self.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address - ) - if curr_stake is None: - curr_stake = Balance(0) - - if curr_stake < amount: - raise Exception("Insufficient funds") - - stake_state = subtensor_state["Stake"] - - if curr_stake.rao == 0: - return True - - # Unstake the funds - # We know that the hotkey has stake, so we can just remove it - stake_state[hotkey_ss58][wallet.coldkeypub.ss58_address][self.block_number] = ( - curr_stake - amount - ).rao - # Add to the free balance - if wallet.coldkeypub.ss58_address not in self.chain_state["System"]["Account"]: - self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address] = { - "data": {"free": {}} - } - - # Remove from total stake storage - subtensor_state["TotalStake"][self.block_number] = ( - self._get_most_recent_storage(subtensor_state["TotalStake"]) - amount.rao - ) - - total_hotkey_stake_state = subtensor_state["TotalHotkeyStake"] - if not hotkey_ss58 in total_hotkey_stake_state: - total_hotkey_stake_state[hotkey_ss58] = {} - total_hotkey_stake_state[hotkey_ss58][self.block_number] = ( - 0 # Shouldn't happen - ) - - total_coldkey_stake_state = subtensor_state["TotalColdkeyStake"] - if not wallet.coldkeypub.ss58_address in total_coldkey_stake_state: - total_coldkey_stake_state[wallet.coldkeypub.ss58_address] = {} - total_coldkey_stake_state[wallet.coldkeypub.ss58_address][ - self.block_number - ] = amount.rao # Shouldn't happen - - total_hotkey_stake_state[hotkey_ss58][self.block_number] = ( - self._get_most_recent_storage( - subtensor_state["TotalHotkeyStake"][hotkey_ss58] - ) - - amount.rao - ) - total_coldkey_stake_state[wallet.coldkeypub.ss58_address][self.block_number] = ( - self._get_most_recent_storage( - subtensor_state["TotalColdkeyStake"][wallet.coldkeypub.ss58_address] - ) - - amount.rao - ) - - self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"][ - "free" - ][self.block_number] = (bal + amount).rao - - return True - @staticmethod def min_required_stake(): """ @@ -1308,131 +878,7 @@ def min_required_stake(): # valid minimum threshold as of 2024/05/01 return 100_000_000 # RAO - def get_minimum_required_stake(self): - return Balance.from_rao(self.min_required_stake()) - - def get_delegate_by_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional["DelegateInfo"]: - subtensor_state = self.chain_state["SubtensorModule"] - - if hotkey_ss58 not in subtensor_state["Delegates"]: - return None - - newest_state = self._get_most_recent_storage( - subtensor_state["Delegates"][hotkey_ss58], block - ) - if newest_state is None: - return None - - nom_result = [] - nominators = subtensor_state["Stake"][hotkey_ss58] - for nominator in nominators: - nom_amount = self.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey_ss58, coldkey_ss58=nominator, block=block - ) - if nom_amount is not None and nom_amount.rao > 0: - nom_result.append((nominator, nom_amount)) - - registered_subnets = [] - for subnet in self.get_all_subnet_netuids(block=block): - uid = self.get_uid_for_hotkey_on_subnet( - hotkey_ss58=hotkey_ss58, netuid=subnet, block=block - ) - - if uid is not None: - registered_subnets.append((subnet, uid)) - - info = DelegateInfo( - hotkey_ss58=hotkey_ss58, - total_stake=self.get_total_stake_for_hotkey(ss58_address=hotkey_ss58) - or Balance(0), - nominators=nom_result, - owner_ss58=self.get_hotkey_owner(hotkey_ss58=hotkey_ss58, block=block), - take=0.18, - validator_permits=[ - subnet - for subnet, uid in registered_subnets - if self.neuron_has_validator_permit(uid=uid, netuid=subnet, block=block) - ], - registrations=[subnet for subnet, _ in registered_subnets], - return_per_1000=Balance.from_tao(1234567), # Doesn't matter for mock? - total_daily_return=Balance.from_tao(1234567), # Doesn't matter for mock? - ) - - return info - - def get_delegates(self, block: Optional[int] = None) -> List["DelegateInfo"]: - subtensor_state = self.chain_state["SubtensorModule"] - delegates_info = [] - for hotkey in subtensor_state["Delegates"]: - info = self.get_delegate_by_hotkey(hotkey_ss58=hotkey, block=block) - if info is not None: - delegates_info.append(info) - - return delegates_info - - def get_delegated( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> List[Tuple["DelegateInfo", "Balance"]]: - """Returns the list of delegates that a given coldkey is staked to.""" - delegates = self.get_delegates(block=block) - - result = [] - for delegate in delegates: - if coldkey_ss58 in delegate.nominators: - result.append((delegate, delegate.nominators[coldkey_ss58])) - - return result - - def get_all_subnets_info(self, block: Optional[int] = None) -> List[SubnetInfo]: - subtensor_state = self.chain_state["SubtensorModule"] - result = [] - for subnet in subtensor_state["NetworksAdded"]: - info = self.get_subnet_info(netuid=subnet, block=block) - if info is not None: - result.append(info) - - return result - - def get_subnet_info( - self, netuid: int, block: Optional[int] = None - ) -> Optional[SubnetInfo]: - if not self.subnet_exists(netuid=netuid, block=block): - return None - - def query_subnet_info(name: str) -> Optional[object]: - return self.query_subtensor(name=name, block=block, params=[netuid]).value - - info = SubnetInfo( - netuid=netuid, - rho=query_subnet_info(name="Rho"), - kappa=query_subnet_info(name="Kappa"), - difficulty=query_subnet_info(name="Difficulty"), - immunity_period=query_subnet_info(name="ImmunityPeriod"), - max_allowed_validators=query_subnet_info(name="MaxAllowedValidators"), - min_allowed_weights=query_subnet_info(name="MinAllowedWeights"), - max_weight_limit=query_subnet_info(name="MaxWeightLimit"), - scaling_law_power=query_subnet_info(name="ScalingLawPower"), - subnetwork_n=query_subnet_info(name="SubnetworkN"), - max_n=query_subnet_info(name="MaxAllowedUids"), - blocks_since_epoch=query_subnet_info(name="BlocksSinceLastStep"), - tempo=query_subnet_info(name="Tempo"), - modality=query_subnet_info(name="NetworkModality"), - connection_requirements={ - str(netuid_.value): percentile.value - for netuid_, percentile in self.query_map_subtensor( - name="NetworkConnect", block=block, params=[netuid] - ).records - }, - emission_value=query_subnet_info(name="EmissionValues"), - burn=query_subnet_info(name="Burn"), - owner_ss58=query_subnet_info(name="SubnetOwner"), - ) - - return info - - def _do_serve_prometheus( + def do_serve_prometheus( self, wallet: "Wallet", call_params: "PrometheusServeCallParams", @@ -1441,7 +887,7 @@ def _do_serve_prometheus( ) -> Tuple[bool, Optional[str]]: return True, None - def _do_set_weights( + def do_set_weights( self, wallet: "Wallet", netuid: int, @@ -1453,7 +899,7 @@ def _do_set_weights( ) -> Tuple[bool, Optional[str]]: return True, None - def _do_serve_axon( + def do_serve_axon( self, wallet: "Wallet", call_params: "AxonServeCallParams", diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index 4d1af585c..66f397114 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -1,31 +1,27 @@ -"""Utils for handling local network with ip and ports.""" - # The MIT License (MIT) -# Copyright © 2021-2022 Yuma Rao -# Copyright © 2022-2023 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies - +# 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. -# Standard Lib +"""Utils for handling local network with ip and ports.""" + +import json import os import urllib -import json -import netaddr -# 3rd party +import netaddr import requests diff --git a/bittensor/utils/registration.py b/bittensor/utils/registration.py index 83406ff70..f6cd201c1 100644 --- a/bittensor/utils/registration.py +++ b/bittensor/utils/registration.py @@ -16,34 +16,12 @@ # DEALINGS IN THE SOFTWARE. import functools -import hashlib -import multiprocessing -import multiprocessing.queues # this must be imported separately, or could break type annotations import os -import random -import time import typing -from dataclasses import dataclass -from datetime import timedelta -from queue import Empty, Full -from typing import Any, Callable, Dict, List, Optional, Tuple, Union -import backoff -import binascii -import math import numpy -from Crypto.Hash import keccak -from rich import console as rich_console -from rich import status as rich_status -from bittensor.core.settings import bt_console -from bittensor.utils._register_cuda import solve_cuda from bittensor.utils.btlogging import logging -from bittensor.utils.formatting import get_human_readable, millify - -if typing.TYPE_CHECKING: - from bittensor.core.subtensor import Subtensor - from bittensor_wallet import Wallet def use_torch() -> bool: @@ -102,6 +80,8 @@ def log_no_torch_error(): class LazyLoadedTorch: + """A lazy-loading proxy for the torch module.""" + def __bool__(self): return bool(_get_real_torch()) @@ -117,1023 +97,3 @@ def __getattr__(self, name): import torch else: torch = LazyLoadedTorch() - - -class CUDAException(Exception): - """An exception raised when an error occurs in the CUDA environment.""" - - -def _hex_bytes_to_u8_list(hex_bytes: bytes): - hex_chunks = [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)] - return hex_chunks - - -def _create_seal_hash(block_and_hotkey_hash_bytes: bytes, nonce: int) -> bytes: - nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little")) - pre_seal = nonce_bytes + binascii.hexlify(block_and_hotkey_hash_bytes)[:64] - seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest() - kec = keccak.new(digest_bits=256) - seal = kec.update(seal_sh256).digest() - return seal - - -def _seal_meets_difficulty(seal: bytes, difficulty: int, limit: int): - seal_number = int.from_bytes(seal, "big") - product = seal_number * difficulty - return product < limit - - -@dataclass -class POWSolution: - """A solution to the registration PoW problem.""" - - nonce: int - block_number: int - difficulty: int - seal: bytes - - def is_stale(self, subtensor: "Subtensor") -> bool: - """ - Returns True if the POW is stale. This means the block the POW is solved for is within 3 blocks of the current - block. - """ - return self.block_number < subtensor.get_current_block() - 3 - - -class _SolverBase(multiprocessing.Process): - """ - A process that solves the registration PoW problem. - - Args: - proc_num: int - The number of the process being created. - num_proc: int - The total number of processes running. - update_interval: int - The number of nonces to try to solve before checking for a new block. - finished_queue: multiprocessing.Queue - The queue to put the process number when a process finishes each update_interval. - Used for calculating the average time per update_interval across all processes. - solution_queue: multiprocessing.Queue - The queue to put the solution the process has found during the pow solve. - newBlockEvent: multiprocessing.Event - The event to set by the main process when a new block is finalized in the network. - The solver process will check for the event after each update_interval. - The solver process will get the new block hash and difficulty and start solving for a new nonce. - stopEvent: multiprocessing.Event - The event to set by the main process when all the solver processes should stop. - The solver process will check for the event after each update_interval. - The solver process will stop when the event is set. - Used to stop the solver processes when a solution is found. - curr_block: multiprocessing.Array - The array containing this process's current block hash. - The main process will set the array to the new block hash when a new block is finalized in the network. - The solver process will get the new block hash from this array when newBlockEvent is set. - curr_block_num: multiprocessing.Value - The value containing this process's current block number. - The main process will set the value to the new block number when a new block is finalized in the network. - The solver process will get the new block number from this value when newBlockEvent is set. - curr_diff: multiprocessing.Array - The array containing this process's current difficulty. - The main process will set the array to the new difficulty when a new block is finalized in the network. - The solver process will get the new difficulty from this array when newBlockEvent is set. - check_block: multiprocessing.Lock - The lock to prevent this process from getting the new block data while the main process is updating the data. - limit: int - The limit of the pow solve for a valid solution. - """ - - proc_num: int - num_proc: int - update_interval: int - finished_queue: multiprocessing.Queue - solution_queue: multiprocessing.Queue - newBlockEvent: multiprocessing.Event - stopEvent: multiprocessing.Event - hotkey_bytes: bytes - curr_block: multiprocessing.Array - curr_block_num: multiprocessing.Value - curr_diff: multiprocessing.Array - check_block: multiprocessing.Lock - limit: int - - def __init__( - self, - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ): - multiprocessing.Process.__init__(self, daemon=True) - self.proc_num = proc_num - self.num_proc = num_proc - self.update_interval = update_interval - self.finished_queue = finished_queue - self.solution_queue = solution_queue - self.newBlockEvent = multiprocessing.Event() - self.newBlockEvent.clear() - self.curr_block = curr_block - self.curr_block_num = curr_block_num - self.curr_diff = curr_diff - self.check_block = check_block - self.stopEvent = stopEvent - self.limit = limit - - def run(self): - raise NotImplementedError("_SolverBase is an abstract class") - - @staticmethod - def create_shared_memory() -> ( - Tuple[multiprocessing.Array, multiprocessing.Value, multiprocessing.Array] - ): - """Creates shared memory for the solver processes to use.""" - curr_block = multiprocessing.Array("h", 32, lock=True) # byte array - curr_block_num = multiprocessing.Value("i", 0, lock=True) # int - curr_diff = multiprocessing.Array("Q", [0, 0], lock=True) # [high, low] - - return curr_block, curr_block_num, curr_diff - - -class _Solver(_SolverBase): - def run(self): - block_number: int - block_and_hotkey_hash_bytes: bytes - block_difficulty: int - nonce_limit = int(math.pow(2, 64)) - 1 - - # Start at random nonce - nonce_start = random.randint(0, nonce_limit) - nonce_end = nonce_start + self.update_interval - while not self.stopEvent.is_set(): - if self.newBlockEvent.is_set(): - with self.check_block: - block_number = self.curr_block_num.value - block_and_hotkey_hash_bytes = bytes(self.curr_block) - block_difficulty = _registration_diff_unpack(self.curr_diff) - - self.newBlockEvent.clear() - - # Do a block of nonces - solution = _solve_for_nonce_block( - nonce_start, - nonce_end, - block_and_hotkey_hash_bytes, - block_difficulty, - self.limit, - block_number, - ) - if solution is not None: - self.solution_queue.put(solution) - - try: - # Send time - self.finished_queue.put_nowait(self.proc_num) - except Full: - pass - - nonce_start = random.randint(0, nonce_limit) - nonce_start = nonce_start % nonce_limit - nonce_end = nonce_start + self.update_interval - - -class _CUDASolver(_SolverBase): - dev_id: int - tpb: int - - def __init__( - self, - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id: int, - tpb: int, - ): - super().__init__( - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - self.dev_id = dev_id - self.tpb = tpb - - def run(self): - block_number: int = 0 # dummy value - block_and_hotkey_hash_bytes: bytes = b"0" * 32 # dummy value - block_difficulty: int = int(math.pow(2, 64)) - 1 # dummy value - nonce_limit = int(math.pow(2, 64)) - 1 # U64MAX - - # Start at random nonce - nonce_start = random.randint(0, nonce_limit) - while not self.stopEvent.is_set(): - if self.newBlockEvent.is_set(): - with self.check_block: - block_number = self.curr_block_num.value - block_and_hotkey_hash_bytes = bytes(self.curr_block) - block_difficulty = _registration_diff_unpack(self.curr_diff) - - self.newBlockEvent.clear() - - # Do a block of nonces - solution = _solve_for_nonce_block_cuda( - nonce_start, - self.update_interval, - block_and_hotkey_hash_bytes, - block_difficulty, - self.limit, - block_number, - self.dev_id, - self.tpb, - ) - if solution is not None: - self.solution_queue.put(solution) - - try: - # Signal that a nonce_block was finished using queue - # send our proc_num - self.finished_queue.put(self.proc_num) - except Full: - pass - - # increase nonce by number of nonces processed - nonce_start += self.update_interval * self.tpb - nonce_start = nonce_start % nonce_limit - - -def _solve_for_nonce_block_cuda( - nonce_start: int, - update_interval: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - block_number: int, - dev_id: int, - tpb: int, -) -> Optional[POWSolution]: - """Tries to solve the POW on a CUDA device for a block of nonces (nonce_start, nonce_start + update_interval * tpb""" - solution, seal = solve_cuda( - nonce_start, - update_interval, - tpb, - block_and_hotkey_hash_bytes, - difficulty, - limit, - dev_id, - ) - - if solution != -1: - # Check if solution is valid (i.e. not -1) - return POWSolution(solution, block_number, difficulty, seal) - - return None - - -def _solve_for_nonce_block( - nonce_start: int, - nonce_end: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - block_number: int, -) -> Optional[POWSolution]: - """Tries to solve the POW for a block of nonces (nonce_start, nonce_end)""" - for nonce in range(nonce_start, nonce_end): - # Create seal. - seal = _create_seal_hash(block_and_hotkey_hash_bytes, nonce) - - # Check if seal meets difficulty - if _seal_meets_difficulty(seal, difficulty, limit): - # Found a solution, save it. - return POWSolution(nonce, block_number, difficulty, seal) - - return None - - -def _registration_diff_unpack(packed_diff: multiprocessing.Array) -> int: - """Unpacks the packed two 32-bit integers into one 64-bit integer. Little endian.""" - return int(packed_diff[0] << 32 | packed_diff[1]) - - -def _registration_diff_pack(diff: int, packed_diff: multiprocessing.Array): - """Packs the difficulty into two 32-bit integers. Little endian.""" - packed_diff[0] = diff >> 32 - packed_diff[1] = diff & 0xFFFFFFFF # low 32 bits - - -def _hash_block_with_hotkey(block_bytes: bytes, hotkey_bytes: bytes) -> bytes: - """Hashes the block with the hotkey using Keccak-256 to get 32 bytes""" - kec = keccak.new(digest_bits=256) - kec = kec.update(bytearray(block_bytes + hotkey_bytes)) - block_and_hotkey_hash_bytes = kec.digest() - return block_and_hotkey_hash_bytes - - -def _update_curr_block( - curr_diff: multiprocessing.Array, - curr_block: multiprocessing.Array, - curr_block_num: multiprocessing.Value, - block_number: int, - block_bytes: bytes, - diff: int, - hotkey_bytes: bytes, - lock: multiprocessing.Lock, -): - with lock: - curr_block_num.value = block_number - # Hash the block with the hotkey - block_and_hotkey_hash_bytes = _hash_block_with_hotkey(block_bytes, hotkey_bytes) - for i in range(32): - curr_block[i] = block_and_hotkey_hash_bytes[i] - _registration_diff_pack(diff, curr_diff) - - -def get_cpu_count() -> int: - try: - return len(os.sched_getaffinity(0)) - except AttributeError: - # OSX does not have sched_getaffinity - return os.cpu_count() - - -@dataclass -class RegistrationStatistics: - """Statistics for a registration.""" - - time_spent_total: float - rounds_total: int - time_average: float - time_spent: float - hash_rate_perpetual: float - hash_rate: float - difficulty: int - block_number: int - block_hash: bytes - - -class RegistrationStatisticsLogger: - """Logs statistics for a registration.""" - - console: rich_console.Console - status: Optional[rich_status.Status] - - def __init__( - self, console: rich_console.Console, output_in_place: bool = True - ) -> None: - self.console = console - - if output_in_place: - self.status = self.console.status("Solving") - else: - self.status = None - - def start(self) -> None: - if self.status is not None: - self.status.start() - - def stop(self) -> None: - if self.status is not None: - self.status.stop() - - def get_status_message( - cls, stats: RegistrationStatistics, verbose: bool = False - ) -> str: - message = ( - "Solving\n" - + f"Time Spent (total): [bold white]{timedelta(seconds=stats.time_spent_total)}[/bold white]\n" - + ( - f"Time Spent This Round: {timedelta(seconds=stats.time_spent)}\n" - + f"Time Spent Average: {timedelta(seconds=stats.time_average)}\n" - if verbose - else "" - ) - + f"Registration Difficulty: [bold white]{millify(stats.difficulty)}[/bold white]\n" - + f"Iters (Inst/Perp): [bold white]{get_human_readable(stats.hash_rate, 'H')}/s / " - + f"{get_human_readable(stats.hash_rate_perpetual, 'H')}/s[/bold white]\n" - + f"Block Number: [bold white]{stats.block_number}[/bold white]\n" - + f"Block Hash: [bold white]{stats.block_hash.encode('utf-8')}[/bold white]\n" - ) - return message - - def update(self, stats: RegistrationStatistics, verbose: bool = False) -> None: - if self.status is not None: - self.status.update(self.get_status_message(stats, verbose=verbose)) - else: - self.console.log(self.get_status_message(stats, verbose=verbose)) - - -def _solve_for_difficulty_fast( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional[POWSolution]: - """ - Solves the POW for registration using multiprocessing. - Args: - subtensor (bittensor.core.subtensor.Subtensor): Subtensor to connect to for block information and to submit. - wallet (bittensor_wallet.Wallet): wallet to use for registration. - netuid (int): The netuid of the subnet to register to. - output_in_place (bool): If true, prints the status in place. Otherwise, prints the status on a new line. - num_processes (int): Number of processes to use. - update_interval (int): Number of nonces to solve before updating block information. - n_samples (int): The number of samples of the hash_rate to keep for the EWMA. - alpha_ (float): The alpha for the EWMA for the hash_rate calculation. - log_verbose (bool): If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more - robust. - Note: - - We can also modify the update interval to do smaller blocks of work, while still updating the block - information after a different number of nonces, to increase the transparency of the process while still keeping - the speed. - """ - if num_processes is None: - # get the number of allowed processes for this process - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - limit = int(math.pow(2, 256)) - 1 - - curr_block, curr_block_num, curr_diff = _Solver.create_shared_memory() - - # Establish communication queues - # See the _Solver class for more information on the queues. - stopEvent = multiprocessing.Event() - stopEvent.clear() - - solution_queue = multiprocessing.Queue() - finished_queues = [multiprocessing.Queue() for _ in range(num_processes)] - check_block = multiprocessing.Lock() - - hotkey_bytes = ( - wallet.coldkeypub.public_key if netuid == -1 else wallet.hotkey.public_key - ) - # Start consumers - solvers = [ - _Solver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - # Set to current block - _update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(bt_console, output_in_place) - logger.start() - - solution = None - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - while netuid == -1 or not subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=0.25) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - old_block_number=old_block_number, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_stats=curr_stats, - update_curr_block=_update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - for finished_queue in finished_queues: - try: - proc_num = finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - hash_rate_ = (num_time * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * update_interval - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, solution contains the nonce or wallet is registered - stopEvent.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - _terminate_workers_and_wait_for_exit(solvers) - - return solution - - -@backoff.on_exception(backoff.constant, Exception, interval=1, max_tries=3) -def _get_block_with_retry( - subtensor: "Subtensor", netuid: int -) -> Tuple[int, int, bytes]: - """ - Gets the current block number, difficulty, and block hash from the substrate node. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): The subtensor object to use to get the block number, difficulty, and block hash. - netuid (int): The netuid of the network to get the block number, difficulty, and block hash from. - - Returns: - block_number (int): The current block number. - difficulty (int): The current difficulty of the subnet. - block_hash (bytes): The current block hash. - - Raises: - Exception: If the block hash is None. - ValueError: If the difficulty is None. - """ - block_number = subtensor.get_current_block() - difficulty = 1_000_000 if netuid == -1 else subtensor.difficulty(netuid=netuid) - block_hash = subtensor.get_block_hash(block_number) - if block_hash is None: - raise Exception( - "Network error. Could not connect to substrate to get block hash" - ) - if difficulty is None: - raise ValueError("Chain error. Difficulty is None") - return block_number, difficulty, block_hash - - -class _UsingSpawnStartMethod: - def __init__(self, force: bool = False): - self._old_start_method = None - self._force = force - - def __enter__(self): - self._old_start_method = multiprocessing.get_start_method(allow_none=True) - if self._old_start_method is None: - self._old_start_method = "spawn" # default to spawn - - multiprocessing.set_start_method("spawn", force=self._force) - - def __exit__(self, *args): - # restore the old start method - multiprocessing.set_start_method(self._old_start_method, force=True) - - -def _check_for_newest_block_and_update( - subtensor: "Subtensor", - netuid: int, - old_block_number: int, - hotkey_bytes: bytes, - curr_diff: multiprocessing.Array, - curr_block: multiprocessing.Array, - curr_block_num: multiprocessing.Value, - update_curr_block: Callable, - check_block: "multiprocessing.Lock", - solvers: List[_Solver], - curr_stats: RegistrationStatistics, -) -> int: - """ - Checks for a new block and updates the current block information if a new block is found. - - Args: - subtensor (bittensor.core.subtensor.Subtensor): The subtensor object to use for getting the current block. - netuid (int): The netuid to use for retrieving the difficulty. - old_block_number (int): The old block number to check against. - hotkey_bytes (bytes): The bytes of the hotkey's pubkey. - curr_diff (multiprocessing.Array): The current difficulty as a multiprocessing array. - curr_block (multiprocessing.Array): Where the current block is stored as a multiprocessing array. - curr_block_num (multiprocessing.Value): Where the current block number is stored as a multiprocessing value. - update_curr_block (Callable): A function that updates the current block. - check_block (multiprocessing.Lock): A mp lock that is used to check for a new block. - solvers (List[_Solver]): A list of solvers to update the current block for. - curr_stats (RegistrationStatistics): The current registration statistics to update. - - Returns: - (int) The current block number. - """ - block_number = subtensor.get_current_block() - if block_number != old_block_number: - old_block_number = block_number - # update block information - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - block_bytes = bytes.fromhex(block_hash[2:]) - - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - # Set new block events for each solver - - for worker in solvers: - worker.newBlockEvent.set() - - # update stats - curr_stats.block_number = block_number - curr_stats.block_hash = block_hash - curr_stats.difficulty = difficulty - - return old_block_number - - -def _solve_for_difficulty_fast_cuda( - subtensor: "Subtensor", - wallet: "Wallet", - netuid: int, - output_in_place: bool = True, - update_interval: int = 50_000, - tpb: int = 512, - dev_id: Union[List[int], int] = 0, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional[POWSolution]: - """ - Solves the registration fast using CUDA - Args: - subtensor (bittensor.core.subtensor.Subtensor): The subtensor node to grab blocks. - wallet (bittensor_wallet.Wallet): The wallet to register. - netuid (int): The netuid of the subnet to register to. - output_in_place (bool): If true, prints the output in place, otherwise prints to new lines. - update_interval (int): The number of nonces to try before checking for more blocks. - tpb (int): The number of threads per block. CUDA param that should match the GPU capability. - dev_id (Union[List[int], int]): The CUDA device IDs to execute the registration on, either a single device or a list of devices. - n_samples (int): The number of samples of the hash_rate to keep for the EWMA. - alpha_ (float): The alpha for the EWMA for the hash_rate calculation. - log_verbose (bool): If true, prints more verbose logging of the registration metrics. - - Note: - The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - """ - if isinstance(dev_id, int): - dev_id = [dev_id] - elif dev_id is None: - dev_id = [0] - - if update_interval is None: - update_interval = 50_000 - - if not torch.cuda.is_available(): - raise Exception("CUDA not available") - - limit = int(math.pow(2, 256)) - 1 - - # Set mp start to use spawn so CUDA doesn't complain - with _UsingSpawnStartMethod(force=True): - curr_block, curr_block_num, curr_diff = _CUDASolver.create_shared_memory() - - # Create a worker per CUDA device - num_processes = len(dev_id) - - # Establish communication queues - stopEvent = multiprocessing.Event() - stopEvent.clear() - solution_queue = multiprocessing.Queue() - finished_queues = [multiprocessing.Queue() for _ in range(num_processes)] - check_block = multiprocessing.Lock() - - hotkey_bytes = wallet.hotkey.public_key - # Start workers - solvers = [ - _CUDASolver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id[i], - tpb, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - - # Set to current block - _update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, # EWMA hash_rate (H/s) - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - logger = RegistrationStatisticsLogger(bt_console, output_in_place) - logger.start() - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - solution = None - while netuid == -1 or not subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=0.15) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - old_block_number=old_block_number, - curr_stats=curr_stats, - update_curr_block=_update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - # Get times for each solver - for finished_queue in finished_queues: - try: - proc_num = finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - hash_rate_ = (num_time * tpb * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * (tpb * update_interval) - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, found_solution contains the nonce or wallet is registered - - stopEvent.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - _terminate_workers_and_wait_for_exit(solvers) - - return solution - - -def _terminate_workers_and_wait_for_exit( - workers: List[Union[multiprocessing.Process, multiprocessing.queues.Queue]], -) -> None: - for worker in workers: - if isinstance(worker, multiprocessing.queues.Queue): - worker.join_thread() - else: - worker.terminate() - worker.join() - worker.close() - - -def create_pow( - subtensor, - wallet, - netuid: int, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[List[int], int] = 0, - tpb: int = 256, - num_processes: int = None, - update_interval: int = None, - log_verbose: bool = False, -) -> Optional[Dict[str, Any]]: - """ - Creates a proof of work for the given subtensor and wallet. - Args: - subtensor (bittensor.core.subtensor.Subtensor): The subtensor to create a proof of work for. - wallet (bittensor_wallet.Wallet): The wallet to create a proof of work for. - netuid (int): The netuid for the subnet to create a proof of work for. - output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning the progress is printed on the same lines. - cuda (bool): If true, uses CUDA to solve the proof of work. - dev_id (Union[List[int], int]): The CUDA device id(s) to use. If cuda is true and dev_id is a list, then multiple CUDA devices will be used to solve the proof of work. - tpb (int): The number of threads per block to use when solving the proof of work. Should be a multiple of 32. - num_processes (int): The number of processes to use when solving the proof of work. If None, then the number of processes is equal to the number of CPU cores. - update_interval (int): The number of nonces to run before checking for a new block. - log_verbose (bool): If true, prints the progress of the proof of work more verbosely. - - Returns: - Optional[Dict[str, Any]] : The proof of work solution or None if the wallet is already registered or there is a different error. - - Raises: - ValueError: If the subnet does not exist. - """ - if netuid != -1: - if not subtensor.subnet_exists(netuid=netuid): - raise ValueError(f"Subnet {netuid} does not exist") - - if cuda: - solution: Optional[POWSolution] = _solve_for_difficulty_fast_cuda( - subtensor, - wallet, - netuid=netuid, - output_in_place=output_in_place, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - solution: Optional[POWSolution] = _solve_for_difficulty_fast( - subtensor, - wallet, - netuid=netuid, - output_in_place=output_in_place, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - return solution diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index f6dada5ae..c79fb6492 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -15,21 +15,22 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from typing import Optional -from pathlib import Path import time -from packaging.version import Version +from pathlib import Path +from typing import Optional import requests -from .btlogging import logging -from ..core.settings import __version__ -from ..core.settings import pipaddress +from packaging.version import Version + +from bittensor.core.settings import __version__ +from bittensor.core.settings import PIPADDRESS +from bittensor.utils.btlogging import logging VERSION_CHECK_THRESHOLD = 86400 class VersionCheckError(Exception): - pass + """Exception raised for errors in the version check process.""" def _get_version_file_path() -> Path: @@ -56,9 +57,9 @@ def _get_version_from_file(version_file: Path) -> Optional[str]: def _get_version_from_pypi(timeout: int = 15) -> str: - logging.debug(f"Checking latest Bittensor version at: {pipaddress}") + logging.debug(f"Checking latest Bittensor version at: {PIPADDRESS}") try: - response = requests.get(pipaddress, timeout=timeout) + response = requests.get(PIPADDRESS, timeout=timeout) latest_version = response.json()["info"]["version"] return latest_version except requests.exceptions.RequestException: @@ -67,6 +68,15 @@ def _get_version_from_pypi(timeout: int = 15) -> str: def get_and_save_latest_version(timeout: int = 15) -> str: + """ + Retrieves and saves the latest version of Bittensor. + + Args: + timeout (int, optional): The timeout for the request to PyPI in seconds. Default is ``15``. + + Returns: + str: The latest version of Bittensor. + """ version_file = _get_version_file_path() if last_known_version := _get_version_from_file(version_file): @@ -104,9 +114,7 @@ def check_version(timeout: int = 15): def version_checking(timeout: int = 15): - """ - Deprecated, kept for backwards compatibility. Use check_version() instead. - """ + """Deprecated, kept for backwards compatibility. Use check_version() instead.""" from warnings import warn diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 36ef6f1c8..333310836 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -17,15 +17,12 @@ """Conversion for weight between chain representation and np.array or torch.Tensor""" -import hashlib import logging import typing from typing import Tuple, List, Union import numpy as np from numpy.typing import NDArray -from scalecodec import ScaleBytes, U16, Vec -from substrateinterface import Keypair from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch, legacy_torch_api_compat @@ -39,6 +36,7 @@ U16_MAX = 65535 +# Uses in `bittensor.utils.weight_utils.process_weights_for_netuid` @legacy_torch_api_compat def normalize_max_weight( x: Union[NDArray[np.float32], "torch.FloatTensor"], limit: float = 0.1 @@ -87,6 +85,7 @@ def normalize_max_weight( return y +# Metagraph uses this function. def convert_weight_uids_and_vals_to_tensor( n: int, uids: List[int], weights: List[int] ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: @@ -114,6 +113,7 @@ def convert_weight_uids_and_vals_to_tensor( return row_weights +# Metagraph uses this function. def convert_root_weight_uids_and_vals_to_tensor( n: int, uids: List[int], weights: List[int], subnets: List[int] ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: @@ -149,10 +149,12 @@ def convert_root_weight_uids_and_vals_to_tensor( return row_weights +# Metagraph uses this function. def convert_bond_uids_and_vals_to_tensor( n: int, uids: List[int], bonds: List[int] ) -> Union[NDArray[np.int64], "torch.LongTensor"]: """Converts bond and uids from chain representation into a np.array. + Args: n (int): number of neurons on network. uids (List[int]): Tensor of uids as destinations for passed bonds. @@ -171,11 +173,13 @@ def convert_bond_uids_and_vals_to_tensor( return row_bonds +# This is used by the community via `bittensor.api.extrinsics.set_weights.set_weights_extrinsic` def convert_weights_and_uids_for_emit( uids: Union[NDArray[np.int64], "torch.LongTensor"], weights: Union[NDArray[np.float32], "torch.FloatTensor"], ) -> Tuple[List[int], List[int]]: """Converts weights into integer u32 representation that sum to MAX_INT_WEIGHT. + Args: uids (np.int64):Tensor of uids as destinations for passed weights. weights (np.float32):Tensor of weights. @@ -234,6 +238,21 @@ def process_weights_for_netuid( Tuple["torch.Tensor", "torch.FloatTensor"], Tuple[NDArray[np.int64], NDArray[np.float32]], ]: + """ + Processes weight tensors for a given subnet id using the provided weight and UID arrays, applying constraints and normalization based on the subtensor and metagraph data. This function can handle both NumPy arrays and PyTorch tensors. + + Args: + uids (Union[NDArray[np.int64], "torch.Tensor"]): Array of unique identifiers of the neurons. + weights (Union[NDArray[np.float32], "torch.Tensor"]): Array of weights associated with the user IDs. + netuid (int): The network uid to process weights for. + subtensor (Subtensor): Subtensor instance to access blockchain data. + metagraph (Metagraph, optional): Metagraph instance for additional network data. If None, it is fetched from the subtensor using the netuid. + exclude_quantile (int, optional): Quantile threshold for excluding lower weights. Defaults to ``0``. + + Returns: + Union[Tuple["torch.Tensor", "torch.FloatTensor"], Tuple[NDArray[np.int64], NDArray[np.float32]]]: Tuple containing the array of user IDs and the corresponding normalized weights. The data type of the return matches the type of the input weights (NumPy or PyTorch). + """ + logging.debug("process_weights_for_netuid()") logging.debug("weights", weights) logging.debug("netuid", netuid) @@ -338,56 +357,3 @@ def process_weights_for_netuid( logging.debug("final_weights", normalized_weights) return non_zero_weight_uids, normalized_weights - - -def generate_weight_hash( - address: str, - netuid: int, - uids: List[int], - values: List[int], - version_key: int, - salt: List[int], -) -> str: - """ - Generate a valid commit hash from the provided weights. - - Args: - address (str): The account identifier. Wallet ss58_address. - netuid (int): The network unique identifier. - uids (List[int]): The list of UIDs. - salt (List[int]): The salt to add to hash. - values (List[int]): The list of weight values. - version_key (int): The version key. - - Returns: - str: The generated commit hash. - """ - # Encode data using SCALE codec - wallet_address = ScaleBytes(Keypair(ss58_address=address).public_key) - netuid = ScaleBytes(netuid.to_bytes(2, "little")) - - vec_uids = Vec(data=None, sub_type="U16") - vec_uids.value = [U16(ScaleBytes(uid.to_bytes(2, "little"))) for uid in uids] - uids = ScaleBytes(vec_uids.encode().data) - - vec_values = Vec(data=None, sub_type="U16") - vec_values.value = [ - U16(ScaleBytes(value.to_bytes(2, "little"))) for value in values - ] - values = ScaleBytes(vec_values.encode().data) - - version_key = ScaleBytes(version_key.to_bytes(8, "little")) - - vec_salt = Vec(data=None, sub_type="U16") - vec_salt.value = [U16(ScaleBytes(salts.to_bytes(2, "little"))) for salts in salt] - salt = ScaleBytes(vec_salt.encode().data) - - data = wallet_address + netuid + uids + values + salt + version_key - - # Generate Blake2b hash of the data tuple - blake2b_hash = hashlib.blake2b(data.data, digest_size=32) - - # Convert the hash to hex string and add "0x" prefix - commit_hash = "0x" + blake2b_hash.hexdigest() - - return commit_hash diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index fc9e8ad9d..f876d249b 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -16,15 +16,17 @@ # DEALINGS IN THE SOFTWARE. import os -from .helpers import ( - _get_mock_coldkey, - _get_mock_hotkey, - _get_mock_keypair, - _get_mock_wallet, +from .helpers import ( # noqa: F401 CLOSE_IN_VALUE, MockConsole, __mock_wallet_factory__, ) +from bittensor_wallet.mock.wallet_mock import ( # noqa: F401 + get_mock_coldkey, + get_mock_hotkey, + get_mock_keypair, + get_mock_wallet, +) def is_running_in_circleci(): diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index c2ff458f2..417bd643b 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -18,10 +18,9 @@ from typing import Union from bittensor_wallet.mock.wallet_mock import MockWallet as _MockWallet -from bittensor_wallet.mock.wallet_mock import get_mock_coldkey as _get_mock_coldkey -from bittensor_wallet.mock.wallet_mock import get_mock_hotkey as _get_mock_hotkey -from bittensor_wallet.mock.wallet_mock import get_mock_keypair as _get_mock_keypair -from bittensor_wallet.mock.wallet_mock import get_mock_wallet as _get_mock_wallet +from bittensor_wallet.mock.wallet_mock import get_mock_coldkey +from bittensor_wallet.mock.wallet_mock import get_mock_hotkey +from bittensor_wallet.mock.wallet_mock import get_mock_wallet from rich.console import Console from rich.text import Text @@ -33,7 +32,7 @@ def __mock_wallet_factory__(*args, **kwargs) -> _MockWallet: """Returns a mock wallet object.""" - mock_wallet = _get_mock_wallet() + mock_wallet = get_mock_wallet() return mock_wallet @@ -117,7 +116,7 @@ def get_mock_neuron(**kwargs) -> NeuronInfo: def get_mock_neuron_by_uid(uid: int, **kwargs) -> NeuronInfo: return get_mock_neuron( - uid=uid, hotkey=_get_mock_hotkey(uid), coldkey=_get_mock_coldkey(uid), **kwargs + uid=uid, hotkey=get_mock_hotkey(uid), coldkey=get_mock_coldkey(uid), **kwargs ) diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py index e1cf924cd..366d3fa85 100644 --- a/tests/integration_tests/test_metagraph_integration.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -26,11 +26,8 @@ def setUpModule(): _subtensor_mock.reset() - _subtensor_mock.create_subnet(netuid=3) - - # Set diff 0 - _subtensor_mock.set_difficulty(netuid=3, difficulty=0) + _subtensor_mock.set_difficulty(netuid=3, difficulty=0) # Set diff 0 class TestMetagraph: diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 3152d74d5..65f939dfa 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -15,26 +15,21 @@ # 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 random import unittest -from queue import Empty as QueueEmpty from unittest.mock import MagicMock, patch -import numpy as np -import pytest from substrateinterface import Keypair import bittensor -from bittensor.utils.mock import MockSubtensor -from bittensor.utils import weight_utils +from bittensor.core import settings from bittensor.utils.balance import Balance +from bittensor.utils.mock import MockSubtensor from tests.helpers import ( - _get_mock_coldkey, + get_mock_coldkey, MockConsole, - _get_mock_keypair, - _get_mock_wallet, + get_mock_keypair, + get_mock_wallet, ) -from bittensor.core import settings class TestSubtensor(unittest.TestCase): @@ -43,9 +38,9 @@ class TestSubtensor(unittest.TestCase): subtensor: MockSubtensor def setUp(self): - self.wallet = _get_mock_wallet( - hotkey=_get_mock_keypair(0, self.id()), - coldkey=_get_mock_keypair(1, self.id()), + self.wallet = get_mock_wallet( + hotkey=get_mock_keypair(0, self.id()), + coldkey=get_mock_keypair(1, self.id()), ) self.balance = Balance.from_tao(1000) self.mock_neuron = MagicMock() # NOTE: this might need more sophistication @@ -59,10 +54,8 @@ def setUpClass(cls) -> None: "bittensor.core.settings.bt_console", mock_console ) cls._mock_console_patcher.start() - # Keeps the same mock network for all tests. This stops the network from being re-setup for each test. cls._mock_subtensor = MockSubtensor() - cls._do_setup_subnet() @classmethod @@ -81,8 +74,8 @@ def test_network_overrides(self): # Argument importance: chain_endpoint (arg) > network (arg) > config.subtensor.chain_endpoint > config.subtensor.network config0 = bittensor.subtensor.config() config0.subtensor.network = "finney" - config0.subtensor.chain_endpoint = "wss://finney.subtensor.io" # Should not match bittensor.core.settings.finney_entrypoint - assert config0.subtensor.chain_endpoint != settings.finney_entrypoint + config0.subtensor.chain_endpoint = "wss://finney.subtensor.io" # Should not match bittensor.core.settings.FINNEY_ENTRYPOINT + assert config0.subtensor.chain_endpoint != settings.FINNEY_ENTRYPOINT config1 = bittensor.subtensor.config() config1.subtensor.network = "local" @@ -96,7 +89,7 @@ def test_network_overrides(self): sub1 = bittensor.subtensor(config=config1, network="local") self.assertEqual( sub1.chain_endpoint, - settings.local_entrypoint, + settings.LOCAL_ENTRYPOINT, msg="Explicit network arg should override config.network", ) @@ -104,157 +97,33 @@ def test_network_overrides(self): sub2 = bittensor.subtensor(config=config0) self.assertNotEqual( sub2.chain_endpoint, - settings.finney_entrypoint, # Here we expect the endpoint corresponding to the network "finney" + settings.FINNEY_ENTRYPOINT, # Here we expect the endpoint corresponding to the network "finney" msg="config.network should override config.chain_endpoint", ) sub3 = bittensor.subtensor(config=config1) # Should pick local instead of finney (default) assert sub3.network == "local" - assert sub3.chain_endpoint == settings.local_entrypoint + assert sub3.chain_endpoint == settings.LOCAL_ENTRYPOINT def test_get_current_block(self): block = self.subtensor.get_current_block() - assert type(block) == int + assert type(block) is int def test_do_block_step(self): self.subtensor.do_block_step() block = self.subtensor.get_current_block() - assert type(block) == int + assert type(block) is int def test_do_block_step_query_previous_block(self): self.subtensor.do_block_step() block = self.subtensor.get_current_block() self.subtensor.query_subtensor("NetworksAdded", block) - def test_unstake(self): - self.subtensor._do_unstake = MagicMock(return_value=True) - - self.subtensor.substrate.get_payment_info = MagicMock( - return_value={"partialFee": 100} - ) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - success = self.subtensor.unstake(self.wallet, amount=200) - self.assertTrue(success, msg="Unstake should succeed") - - def test_unstake_inclusion(self): - self.subtensor._do_unstake = MagicMock(return_value=True) - - self.subtensor.substrate.get_payment_info = MagicMock( - return_value={"partialFee": 100} - ) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - success = self.subtensor.unstake( - self.wallet, amount=200, wait_for_inclusion=True - ) - self.assertTrue(success, msg="Unstake should succeed") - - def test_unstake_failed(self): - self.subtensor._do_unstake = MagicMock(return_value=False) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - fail = self.subtensor.unstake(self.wallet, amount=200, wait_for_inclusion=True) - self.assertFalse(fail, msg="Unstake should fail") - - def test_stake(self): - self.subtensor._do_stake = MagicMock(return_value=True) - - self.subtensor.substrate.get_payment_info = MagicMock( - return_value={"partialFee": 100} - ) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - self.subtensor.get_hotkey_owner = MagicMock( - return_value=self.wallet.coldkeypub.ss58_address - ) - success = self.subtensor.add_stake(self.wallet, amount=200) - self.assertTrue(success, msg="Stake should succeed") - - def test_stake_inclusion(self): - self.subtensor._do_stake = MagicMock(return_value=True) - - self.subtensor.substrate.get_payment_info = MagicMock( - return_value={"partialFee": 100} - ) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - self.subtensor.get_hotkey_owner = MagicMock( - return_value=self.wallet.coldkeypub.ss58_address - ) - success = self.subtensor.add_stake( - self.wallet, amount=200, wait_for_inclusion=True - ) - self.assertTrue(success, msg="Stake should succeed") - - def test_stake_failed(self): - self.subtensor._do_stake = MagicMock(return_value=False) - - self.subtensor.substrate.get_payment_info = MagicMock( - return_value={"partialFee": 100} - ) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=Balance.from_rao(0)) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - self.subtensor.get_hotkey_owner = MagicMock( - return_value=self.wallet.coldkeypub.ss58_address - ) - fail = self.subtensor.add_stake( - self.wallet, amount=200, wait_for_inclusion=True - ) - self.assertFalse(fail, msg="Stake should fail") - def test_transfer(self): - fake_coldkey = _get_mock_coldkey(1) + fake_coldkey = get_mock_coldkey(1) - self.subtensor._do_transfer = MagicMock(return_value=(True, "0x", None)) + self.subtensor.do_transfer = MagicMock(return_value=(True, "0x", None)) self.subtensor.register = MagicMock(return_value=True) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( return_value=self.mock_neuron @@ -268,8 +137,8 @@ def test_transfer(self): self.assertTrue(success, msg="Transfer should succeed") def test_transfer_inclusion(self): - fake_coldkey = _get_mock_coldkey(1) - self.subtensor._do_transfer = MagicMock(return_value=(True, "0x", None)) + fake_coldkey = get_mock_coldkey(1) + self.subtensor.do_transfer = MagicMock(return_value=(True, "0x", None)) self.subtensor.register = MagicMock(return_value=True) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( return_value=self.mock_neuron @@ -282,8 +151,8 @@ def test_transfer_inclusion(self): self.assertTrue(success, msg="Transfer should succeed") def test_transfer_failed(self): - fake_coldkey = _get_mock_coldkey(1) - self.subtensor._do_transfer = MagicMock( + fake_coldkey = get_mock_coldkey(1) + self.subtensor.do_transfer = MagicMock( return_value=(False, None, "Mock failure message") ) @@ -293,7 +162,7 @@ def test_transfer_failed(self): self.assertFalse(fail, msg="Transfer should fail") def test_transfer_invalid_dest(self): - fake_coldkey = _get_mock_coldkey(1) + fake_coldkey = get_mock_coldkey(1) fail = self.subtensor.transfer( self.wallet, @@ -304,8 +173,8 @@ def test_transfer_invalid_dest(self): self.assertFalse(fail, msg="Transfer should fail because of invalid dest") def test_transfer_dest_as_bytes(self): - fake_coldkey = _get_mock_coldkey(1) - self.subtensor._do_transfer = MagicMock(return_value=(True, "0x", None)) + fake_coldkey = get_mock_coldkey(1) + self.subtensor.do_transfer = MagicMock(return_value=(True, "0x", None)) self.subtensor.register = MagicMock(return_value=True) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( @@ -325,15 +194,8 @@ def test_transfer_dest_as_bytes(self): def test_set_weights(self): chain_weights = [0] - class success: - def __init__(self): - self.is_success = True - - def process_events(self): - return True - self.subtensor.set_weights = MagicMock(return_value=True) - self.subtensor._do_set_weights = MagicMock(return_value=(True, None)) + self.subtensor.do_set_weights = MagicMock(return_value=(True, None)) success = self.subtensor.set_weights( wallet=self.wallet, @@ -341,11 +203,11 @@ def process_events(self): uids=[1], weights=chain_weights, ) - assert success == True + assert success is True def test_set_weights_inclusion(self): chain_weights = [0] - self.subtensor._do_set_weights = MagicMock(return_value=(True, None)) + self.subtensor.do_set_weights = MagicMock(return_value=(True, None)) self.subtensor.set_weights = MagicMock(return_value=True) success = self.subtensor.set_weights( @@ -355,11 +217,11 @@ def test_set_weights_inclusion(self): weights=chain_weights, wait_for_inclusion=True, ) - assert success == True + assert success is True def test_set_weights_failed(self): chain_weights = [0] - self.subtensor._do_set_weights = MagicMock( + self.subtensor.do_set_weights = MagicMock( return_value=(False, "Mock failure message") ) self.subtensor.set_weights = MagicMock(return_value=False) @@ -371,483 +233,17 @@ def test_set_weights_failed(self): weights=chain_weights, wait_for_inclusion=True, ) - assert fail == False - - def test_commit_weights(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - commit_hash = bittensor.utils.weight_utils.generate_weight_hash( - address=self.wallet.hotkey.ss58_address, - netuid=3, - uids=weight_uids, - values=weight_vals, - salt=salt.tolist(), - version_key=0, - ) - - self.subtensor.commit_weights = MagicMock( - return_value=(True, "Successfully committed weights.") - ) - self.subtensor._do_commit_weights = MagicMock(return_value=(True, None)) - - success, message = self.subtensor.commit_weights( - wallet=self.wallet, netuid=3, uids=uids, weights=weights, salt=salt - ) - assert success is True - assert message == "Successfully committed weights." - - def test_commit_weights_inclusion(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - commit_hash = bittensor.utils.weight_utils.generate_weight_hash( - address=self.wallet.hotkey.ss58_address, - netuid=1, - uids=weight_uids, - values=weight_vals, - salt=salt.tolist(), - version_key=0, - ) - - self.subtensor._do_commit_weights = MagicMock(return_value=(True, None)) - self.subtensor.commit_weights = MagicMock( - return_value=(True, "Successfully committed weights.") - ) - - success, message = self.subtensor.commit_weights( - wallet=self.wallet, - netuid=1, - uids=uids, - weights=weights, - salt=salt, - wait_for_inclusion=True, - ) - assert success is True - assert message == "Successfully committed weights." - - def test_commit_weights_failed(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - commit_hash = bittensor.utils.weight_utils.generate_weight_hash( - address=self.wallet.hotkey.ss58_address, - netuid=3, - uids=weight_uids, - values=weight_vals, - salt=salt.tolist(), - version_key=0, - ) - - self.subtensor._do_commit_weights = MagicMock( - return_value=(False, "Mock failure message") - ) - self.subtensor.commit_weights = MagicMock( - return_value=(False, "Mock failure message") - ) - - success, message = self.subtensor.commit_weights( - wallet=self.wallet, - netuid=3, - uids=uids, - weights=weights, - salt=salt, - wait_for_inclusion=True, - ) - assert success is False - assert message == "Mock failure message" - - def test_reveal_weights(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - - self.subtensor.reveal_weights = MagicMock( - return_value=(True, "Successfully revealed weights.") - ) - self.subtensor._do_reveal_weights = MagicMock(return_value=(True, None)) - - success, message = self.subtensor.reveal_weights( - wallet=self.wallet, - netuid=3, - uids=uids, - weights=weights, - salt=salt, - version_key=0, - ) - assert success is True - assert message == "Successfully revealed weights." - - def test_reveal_weights_inclusion(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - - self.subtensor._do_reveal_weights = MagicMock(return_value=(True, None)) - self.subtensor.reveal_weights = MagicMock( - return_value=(True, "Successfully revealed weights.") - ) - - success, message = self.subtensor.reveal_weights( - wallet=self.wallet, - netuid=1, - uids=uids, - weights=weights, - salt=salt, - version_key=0, - wait_for_inclusion=True, - ) - assert success is True - assert message == "Successfully revealed weights." - - def test_reveal_weights_failed(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - - self.subtensor._do_reveal_weights = MagicMock( - return_value=(False, "Mock failure message") - ) - self.subtensor.reveal_weights = MagicMock( - return_value=(False, "Mock failure message") - ) - - success, message = self.subtensor.reveal_weights( - wallet=self.wallet, - netuid=3, - uids=uids, - weights=weights, - salt=salt, - version_key=0, - wait_for_inclusion=True, - ) - assert success is False - assert message == "Mock failure message" - - def test_commit_and_reveal_weights(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - version_key = 0 - - # Mock the commit_weights and reveal_weights functions - self.subtensor.commit_weights = MagicMock( - return_value=(True, "Successfully committed weights.") - ) - self.subtensor._do_commit_weights = MagicMock(return_value=(True, None)) - self.subtensor.reveal_weights = MagicMock( - return_value=(True, "Successfully revealed weights.") - ) - self.subtensor._do_reveal_weights = MagicMock(return_value=(True, None)) - - # Commit weights - commit_success, commit_message = self.subtensor.commit_weights( - wallet=self.wallet, - netuid=3, - uids=uids, - weights=weights, - salt=salt, - ) - assert commit_success is True - assert commit_message == "Successfully committed weights." - - # Reveal weights - reveal_success, reveal_message = self.subtensor.reveal_weights( - wallet=self.wallet, - netuid=3, - uids=uids, - weights=weights, - salt=salt, - version_key=version_key, - ) - assert reveal_success is True - assert reveal_message == "Successfully revealed weights." - - def test_commit_and_reveal_weights_inclusion(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - version_key = 0 - - # Mock the commit_weights and reveal_weights functions - self.subtensor.commit_weights = MagicMock( - return_value=(True, "Successfully committed weights.") - ) - self.subtensor._do_commit_weights = MagicMock(return_value=(True, None)) - self.subtensor.reveal_weights = MagicMock( - return_value=(True, "Successfully revealed weights.") - ) - self.subtensor._do_reveal_weights = MagicMock(return_value=(True, None)) - - # Commit weights with wait_for_inclusion - commit_success, commit_message = self.subtensor.commit_weights( - wallet=self.wallet, - netuid=1, - uids=uids, - weights=weights, - salt=salt, - wait_for_inclusion=True, - ) - assert commit_success is True - assert commit_message == "Successfully committed weights." - - # Reveal weights with wait_for_inclusion - reveal_success, reveal_message = self.subtensor.reveal_weights( - wallet=self.wallet, - netuid=1, - uids=uids, - weights=weights, - salt=salt, - version_key=version_key, - wait_for_inclusion=True, - ) - assert reveal_success is True - assert reveal_message == "Successfully revealed weights." + assert fail is False def test_get_balance(self): - fake_coldkey = _get_mock_coldkey(0) + fake_coldkey = get_mock_coldkey(0) balance = self.subtensor.get_balance(address=fake_coldkey) - assert type(balance) == bittensor.utils.balance.Balance - - def test_get_balances(self): - balances = self.subtensor.get_balances() - assert type(balances) == dict - for i in balances: - assert type(balances[i]) == bittensor.utils.balance.Balance - - def test_get_uid_by_hotkey_on_subnet(self): - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - mock_hotkey_kp = _get_mock_keypair(100, self.id()) - - # Register on subnet 3 - mock_uid = self.subtensor.force_register_neuron( - netuid=3, - hotkey=mock_hotkey_kp.ss58_address, - coldkey=mock_coldkey_kp.ss58_address, - ) - - uid = self.subtensor.get_uid_for_hotkey_on_subnet( - mock_hotkey_kp.ss58_address, netuid=3 - ) - self.assertIsInstance( - uid, int, msg="get_uid_for_hotkey_on_subnet should return an int" - ) - self.assertEqual( - uid, - mock_uid, - msg="get_uid_for_hotkey_on_subnet should return the correct uid", - ) - - def test_is_hotkey_registered(self): - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - mock_hotkey_kp = _get_mock_keypair(100, self.id()) - - # Register on subnet 3 - _ = self.subtensor.force_register_neuron( - netuid=3, - hotkey=mock_hotkey_kp.ss58_address, - coldkey=mock_coldkey_kp.ss58_address, - ) - - registered = self.subtensor.is_hotkey_registered( - mock_hotkey_kp.ss58_address, netuid=3 - ) - self.assertTrue(registered, msg="Hotkey should be registered") - - def test_is_hotkey_registered_not_registered(self): - mock_hotkey_kp = _get_mock_keypair(100, self.id()) - - # Do not register on subnet 3 - - registered = self.subtensor.is_hotkey_registered( - mock_hotkey_kp.ss58_address, netuid=3 - ) - self.assertFalse(registered, msg="Hotkey should not be registered") - - def test_registration_multiprocessed_already_registered(self): - workblocks_before_is_registered = random.randint(5, 10) - # return False each work block but return True after a random number of blocks - is_registered_return_values = ( - [False for _ in range(workblocks_before_is_registered)] - + [True] - + [True, False] - ) - # this should pass the initial False check in the subtensor class and then return True because the neuron is already registered - - mock_neuron = MagicMock() - mock_neuron.is_null = True - - # patch solution queue to return None - with patch( - "multiprocessing.queues.Queue.get", return_value=None - ) as mock_queue_get: - # patch time queue get to raise Empty exception - with patch( - "multiprocessing.queues.Queue.get_nowait", side_effect=QueueEmpty - ) as mock_queue_get_nowait: - wallet = _get_mock_wallet( - hotkey=_get_mock_keypair(0, self.id()), - coldkey=_get_mock_keypair(1, self.id()), - ) - self.subtensor.is_hotkey_registered = MagicMock( - side_effect=is_registered_return_values - ) - - self.subtensor.difficulty = MagicMock(return_value=1) - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - side_effect=mock_neuron - ) - self.subtensor._do_pow_register = MagicMock(return_value=(True, None)) - - with patch( - "bittensor.core.settings.bt_console.status" - ) as mock_set_status: - # Need to patch the console status to avoid opening a parallel live display - mock_set_status.__enter__ = MagicMock(return_value=True) - mock_set_status.__exit__ = MagicMock(return_value=True) - - # should return True - assert self.subtensor.register( - wallet=wallet, netuid=3, num_processes=3, update_interval=5 - ) - - # calls until True and once again before exiting subtensor class - # This assertion is currently broken when difficulty is too low - assert ( - self.subtensor.is_hotkey_registered.call_count - == workblocks_before_is_registered + 2 - ) - - def test_registration_partly_failed(self): - do_pow_register_mock = MagicMock( - side_effect=[(False, "Failed"), (False, "Failed"), (True, None)] - ) - - def is_registered_side_effect(*args, **kwargs): - nonlocal do_pow_register_mock - return do_pow_register_mock.call_count < 3 - - current_block = [i for i in range(0, 100)] - - wallet = _get_mock_wallet( - hotkey=_get_mock_keypair(0, self.id()), - coldkey=_get_mock_keypair(1, self.id()), - ) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=bittensor.NeuronInfo.get_null_neuron() - ) - self.subtensor.is_hotkey_registered = MagicMock( - side_effect=is_registered_side_effect - ) - - self.subtensor.difficulty = MagicMock(return_value=1) - self.subtensor.get_current_block = MagicMock(side_effect=current_block) - self.subtensor._do_pow_register = do_pow_register_mock - - # should return True - self.assertTrue( - self.subtensor.register( - wallet=wallet, netuid=3, num_processes=3, update_interval=5 - ), - msg="Registration should succeed", - ) - - def test_registration_failed(self): - is_registered_return_values = [False for _ in range(100)] - current_block = [i for i in range(0, 100)] - mock_neuron = MagicMock() - mock_neuron.is_null = True - - with patch( - "bittensor.api.extrinsics.registration.create_pow", return_value=None - ) as mock_create_pow: - wallet = _get_mock_wallet( - hotkey=_get_mock_keypair(0, self.id()), - coldkey=_get_mock_keypair(1, self.id()), - ) - - self.subtensor.is_hotkey_registered = MagicMock( - side_effect=is_registered_return_values - ) - - self.subtensor.get_current_block = MagicMock(side_effect=current_block) - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=mock_neuron - ) - self.subtensor.substrate.get_block_hash = MagicMock( - return_value="0x" + "0" * 64 - ) - self.subtensor._do_pow_register = MagicMock(return_value=(False, "Failed")) - - # should return True - self.assertIsNot( - self.subtensor.register(wallet=wallet, netuid=3), - True, - msg="Registration should fail", - ) - self.assertEqual(mock_create_pow.call_count, 3) - - def test_registration_stale_then_continue(self): - # verify that after a stale solution, the solve will continue without exiting - - class ExitEarly(Exception): - pass - - mock_is_stale = MagicMock(side_effect=[True, False]) - - mock_do_pow_register = MagicMock(side_effect=ExitEarly()) - - mock_subtensor_self = MagicMock( - neuron_for_pubkey=MagicMock( - return_value=MagicMock(is_null=True) - ), # not registered - _do_pow_register=mock_do_pow_register, - substrate=MagicMock( - get_block_hash=MagicMock(return_value="0x" + "0" * 64), - ), - ) - - mock_wallet = MagicMock() - - mock_create_pow = MagicMock(return_value=MagicMock(is_stale=mock_is_stale)) - - with patch("bittensor.api.extrinsics.registration.create_pow", mock_create_pow): - # should create a pow and check if it is stale - # then should create a new pow and check if it is stale - # then should enter substrate and exit early because of test - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=bittensor.NeuronInfo.get_null_neuron() - ) - with pytest.raises(ExitEarly): - bittensor.subtensor.register(mock_subtensor_self, mock_wallet, netuid=3) - self.assertEqual( - mock_create_pow.call_count, 2, msg="must try another pow after stale" - ) - self.assertEqual(mock_is_stale.call_count, 2) - self.assertEqual( - mock_do_pow_register.call_count, - 1, - msg="only tries to submit once, then exits", - ) + assert type(balance) is bittensor.utils.balance.Balance def test_defaults_to_finney(self): sub = bittensor.subtensor() assert sub.network == "finney" - assert sub.chain_endpoint == settings.finney_entrypoint + assert sub.chain_endpoint == settings.FINNEY_ENTRYPOINT if __name__ == "__main__": diff --git a/tests/unit_tests/extrinsics/test_delegation.py b/tests/unit_tests/extrinsics/test_delegation.py deleted file mode 100644 index 1b1200f7a..000000000 --- a/tests/unit_tests/extrinsics/test_delegation.py +++ /dev/null @@ -1,478 +0,0 @@ -# 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. - -from unittest.mock import MagicMock, patch - -import pytest -from bittensor_wallet.wallet import Wallet - -from bittensor.core.errors import ( - NominationError, - NotDelegateError, - NotRegisteredError, - StakeError, -) -from bittensor.api.extrinsics.delegation import ( - nominate_extrinsic, - delegate_extrinsic, - undelegate_extrinsic, -) -from bittensor.core.subtensor import Subtensor -from bittensor.utils.balance import Balance - - -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=Subtensor) - mock.network = "magic_mock" - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=Wallet) - mock.hotkey.ss58_address = "fake_hotkey_address" - mock.coldkey.ss58_address = "fake_coldkey_address" - mock.coldkey = MagicMock() - mock.hotkey = MagicMock() - mock.name = "fake_wallet_name" - mock.hotkey_str = "fake_hotkey_str" - return mock - - -@pytest.mark.parametrize( - "already_delegate, nomination_success, raises_exception, expected_result", - [ - (False, True, None, True), # Successful nomination - (True, None, None, False), # Already a delegate - (False, None, NominationError, False), # Failure - Nomination error - (False, None, ValueError, False), # Failure - ValueError - ], - ids=[ - "success-nomination-done", - "failure-already-delegate", - "failure-nomination-error", - "failure-value-error", - ], -) -def test_nominate_extrinsic( - mock_subtensor, - mock_wallet, - already_delegate, - nomination_success, - raises_exception, - expected_result, -): - # Arrange - with patch.object( - mock_subtensor, "is_hotkey_delegate", return_value=already_delegate - ), patch.object( - mock_subtensor, "_do_nominate", return_value=nomination_success - ) as mock_nominate: - if raises_exception: - mock_subtensor._do_nominate.side_effect = raises_exception - - # Act - result = nominate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - wait_for_finalization=False, - wait_for_inclusion=True, - ) - # Assert - assert result == expected_result - - if not already_delegate and nomination_success is not None: - mock_nominate.assert_called_once_with( - wallet=mock_wallet, wait_for_inclusion=True, wait_for_finalization=False - ) - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, is_delegate, prompt_response, stake_amount, balance_sufficient, transaction_success, raises_error, expected_result, delegate_called", - [ - (True, False, True, True, 100, True, True, None, True, True), # Success case - ( - False, - False, - True, - True, - 100, - True, - True, - None, - True, - True, - ), # Success case - no wait - ( - True, - False, - True, - True, - None, - True, - True, - None, - True, - True, - ), # Success case - all stake - ( - True, - False, - True, - True, - 0.000000100, - True, - True, - None, - True, - True, - ), # Success case - below cutoff threshold - ( - True, - False, - True, - True, - Balance.from_tao(1), - True, - True, - None, - True, - True, - ), # Success case - from Tao - ( - True, - False, - False, - None, - 100, - True, - False, - NotDelegateError, - False, - False, - ), # Not a delegate error - ( - True, - False, - True, - True, - 200, - False, - False, - None, - False, - False, - ), # Insufficient balance - ( - True, - False, - True, - False, - 100, - True, - True, - None, - False, - False, - ), # User declines prompt - ( - True, - False, - True, - True, - 100, - True, - False, - None, - False, - True, - ), # Transaction fails - ( - True, - False, - True, - True, - 100, - True, - False, - NotRegisteredError, - False, - True, - ), # Raises a NotRegisteredError - ( - True, - False, - True, - True, - 100, - True, - False, - StakeError, - False, - True, - ), # Raises a StakeError - ], - ids=[ - "success-delegate", - "success-no-wait", - "success-all-stake", - "success-below-existential-threshold", - "success-from-tao", - "failure-not-delegate", - "failure-low-balance", - "failure-prompt-declined", - "failure-transaction-failed", - "failure-NotRegisteredError", - "failure-StakeError", - ], -) -def test_delegate_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - is_delegate, - prompt_response, - stake_amount, - balance_sufficient, - transaction_success, - raises_error, - expected_result, - delegate_called, -): - # Arrange - wallet_balance = Balance.from_tao(500) - wallet_insufficient_balance = Balance.from_tao(0.002) - - with patch("rich.prompt.Confirm.ask", return_value=prompt_response), patch.object( - mock_subtensor, - "get_balance", - return_value=wallet_balance - if balance_sufficient - else wallet_insufficient_balance, - ), patch.object( - mock_subtensor, "is_hotkey_delegate", return_value=is_delegate - ), patch.object( - mock_subtensor, "_do_delegation", return_value=transaction_success - ) as mock_delegate: - if raises_error: - mock_delegate.side_effect = raises_error - - # Act - if raises_error == NotDelegateError: - with pytest.raises(raises_error): - result = delegate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=stake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=True, - ) - else: - result = delegate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=stake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=True, - ) - # Assert - assert result == expected_result - - if delegate_called: - if stake_amount is None: - called_stake_amount = wallet_balance - elif isinstance(stake_amount, Balance): - called_stake_amount = stake_amount - else: - called_stake_amount = Balance.from_tao(stake_amount) - - if called_stake_amount > Balance.from_rao(1000): - called_stake_amount -= Balance.from_rao(1000) - - mock_delegate.assert_called_once_with( - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=called_stake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, is_delegate, prompt_response, unstake_amount, current_stake, transaction_success, raises_error, expected_result", - [ - (True, False, True, True, 50, 100, True, None, True), # Success case - (False, False, True, True, 50, 100, True, None, True), # Success case - no wait - ( - False, - False, - True, - True, - Balance.from_tao(1), - 100, - True, - None, - True, - ), # Success case - from tao - (True, False, True, True, None, 100, True, None, True), # Success - unstake all - ( - True, - False, - True, - True, - 1000, - 1000, - False, - None, - False, - ), # Failure - transaction fails - ( - True, - False, - False, - None, - 100, - 120, - True, - NotDelegateError, - False, - ), # Not a delegate - (True, False, True, False, 100, 111, True, None, False), # User declines prompt - ( - True, - False, - True, - True, - 100, - 90, - True, - None, - False, - ), # Insufficient stake to unstake - ( - True, - False, - True, - True, - 100, - 100, - False, - StakeError, - False, - ), # StakeError raised - ( - True, - False, - True, - True, - 100, - 100, - False, - NotRegisteredError, - False, - ), # NotRegisteredError raised - ], - ids=[ - "success-undelegate", - "success-undelegate-no-wait", - "success-from-tao", - "success-undelegate-all", - "failure-transaction-failed", - "failure-NotDelegateError", - "failure-prompt-declined", - "failure-insufficient-stake", - "failure--StakeError", - "failure-NotRegisteredError", - ], -) -def test_undelegate_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - is_delegate, - prompt_response, - unstake_amount, - current_stake, - transaction_success, - raises_error, - expected_result, -): - # Arrange - wallet_balance = Balance.from_tao(500) - - with patch("rich.prompt.Confirm.ask", return_value=prompt_response), patch.object( - mock_subtensor, "is_hotkey_delegate", return_value=is_delegate - ), patch.object( - mock_subtensor, "get_balance", return_value=wallet_balance - ), patch.object( - mock_subtensor, - "get_stake_for_coldkey_and_hotkey", - return_value=Balance.from_tao(current_stake), - ), patch.object( - mock_subtensor, "_do_undelegation", return_value=transaction_success - ) as mock_undelegate: - if raises_error: - mock_undelegate.side_effect = raises_error - - # Act - if raises_error == NotDelegateError: - with pytest.raises(raises_error): - result = undelegate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=unstake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=True, - ) - else: - result = undelegate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=unstake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=True, - ) - - # Assert - assert result == expected_result - - if expected_result and prompt_response: - if unstake_amount is None: - called_unstake_amount = Balance.from_tao(current_stake) - elif isinstance(unstake_amount, Balance): - called_unstake_amount = unstake_amount - else: - called_unstake_amount = Balance.from_tao(unstake_amount) - - mock_undelegate.assert_called_once_with( - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=called_unstake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) diff --git a/tests/unit_tests/extrinsics/test_network.py b/tests/unit_tests/extrinsics/test_network.py deleted file mode 100644 index e4589bc1b..000000000 --- a/tests/unit_tests/extrinsics/test_network.py +++ /dev/null @@ -1,176 +0,0 @@ -# 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. - -from unittest.mock import MagicMock, patch - -import pytest -from bittensor_wallet import Wallet - -from bittensor.api.extrinsics.network import ( - set_hyperparameter_extrinsic, - register_subnetwork_extrinsic, -) -from bittensor.core.subtensor import Subtensor - - -# Mock the bittensor and related modules to avoid real network calls and wallet operations -@pytest.fixture -def mock_subtensor(): - subtensor = MagicMock(spec=Subtensor) - subtensor.get_balance.return_value = 100 - subtensor.get_subnet_burn_cost.return_value = 10 - subtensor.substrate = MagicMock() - subtensor.substrate.get_block_hash = MagicMock(return_value="0x" + "0" * 64) - return subtensor - - -@pytest.fixture -def mock_wallet(): - wallet = MagicMock(spec=Wallet) - wallet.coldkeypub.ss58_address = "fake_address" - wallet.coldkey = MagicMock() - return wallet - - -@pytest.fixture -def mock_other_owner_wallet(): - wallet = MagicMock(spec=Wallet) - wallet.coldkeypub.ss58_address = "fake_other_owner" - return wallet - - -@pytest.mark.parametrize( - "test_id, wait_for_inclusion, wait_for_finalization, prompt, expected", - [ - ("happy-path-01", False, True, False, True), - ("happy-path-02", True, False, False, True), - ("happy-path-03", False, False, False, True), - ("happy-path-04", True, True, False, True), - ], -) -def test_register_subnetwork_extrinsic_happy_path( - mock_subtensor, - mock_wallet, - test_id, - wait_for_inclusion, - wait_for_finalization, - prompt, - expected, -): - # Arrange - mock_subtensor.substrate.submit_extrinsic.return_value.is_success = True - - # Act - result = register_subnetwork_extrinsic( - mock_subtensor, mock_wallet, wait_for_inclusion, wait_for_finalization, prompt - ) - - # Assert - assert result == expected - - -# Edge cases -@pytest.mark.parametrize( - "test_id, balance, burn_cost, prompt_input, expected", - [ - ("edge-case-01", 0, 10, False, False), # Balance is zero - ("edge-case-02", 10, 10, False, False), # Balance equals burn cost - ("edge-case-03", 9, 10, False, False), # Balance less than burn cost - ("edge-case-04", 100, 10, True, True), # User declines prompt - ], -) -def test_register_subnetwork_extrinsic_edge_cases( - mock_subtensor, - mock_wallet, - test_id, - balance, - burn_cost, - prompt_input, - expected, - monkeypatch, -): - # Arrange - mock_subtensor.get_balance.return_value = balance - mock_subtensor.get_subnet_burn_cost.return_value = burn_cost - monkeypatch.setattr("rich.prompt.Confirm.ask", lambda x: prompt_input) - - # Act - result = register_subnetwork_extrinsic(mock_subtensor, mock_wallet, prompt=True) - - # Assert - assert result == expected - - -@pytest.mark.parametrize( - "netuid, parameter, value, is_owner, wait_for_inclusion, wait_for_finalization, prompt, extrinsic_success, expected_result", - [ - # Success - no wait - (1, "serving_rate_limit", 49, True, False, False, False, True, True), - # Success - with wait - (1, "serving_rate_limit", 50, True, True, True, False, True, True), - # Failure - wallet doesn't own subnet - (1, "serving_rate_limit", 50, False, True, True, False, True, False), - # Failure - invalid hyperparameter - (1, None, 50, True, True, False, False, False, False), - ], - ids=[ - "success-no-wait", - "success-with-wait", - "failure-not-owner", - "failure-invalid-hyperparameter", - ], -) -def test_set_hyperparameter_extrinsic( - mock_subtensor, - mock_wallet, - mock_other_owner_wallet, - netuid, - parameter, - value, - is_owner, - wait_for_inclusion, - wait_for_finalization, - prompt, - extrinsic_success, - expected_result, -): - # Arrange - with patch.object( - mock_subtensor, - "get_subnet_owner", - return_value=mock_wallet.coldkeypub.ss58_address - if is_owner - else mock_other_owner_wallet.coldkeypub.ss58_address, - ), patch.object( - mock_subtensor.substrate, - "submit_extrinsic", - return_value=MagicMock(is_success=extrinsic_success), - ): - # Act - result = set_hyperparameter_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=netuid, - parameter=parameter, - value=value, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_result diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index 6b45b7fc2..516020c8c 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -79,7 +79,7 @@ def test_prometheus_extrinsic_happy_path( neuron.prometheus_info.port = port neuron.prometheus_info.ip_type = 4 subtensor.get_neuron_for_pubkey_and_subnet.return_value = neuron - subtensor._do_serve_prometheus.return_value = (True, None) + subtensor.do_serve_prometheus.return_value = (True, None) # Act result = prometheus_extrinsic( @@ -117,7 +117,7 @@ def test_prometheus_extrinsic_edge_cases( neuron = MagicMock() neuron.is_null = True subtensor.get_neuron_for_pubkey_and_subnet.return_value = neuron - subtensor._do_serve_prometheus.return_value = (True, None) + subtensor.do_serve_prometheus.return_value = (True, None) # Act result = prometheus_extrinsic( @@ -131,7 +131,7 @@ def test_prometheus_extrinsic_edge_cases( ) # Assert - assert result == True, f"Test ID: {test_id}" + assert result is True, f"Test ID: {test_id}" # Error cases diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py deleted file mode 100644 index 00addfd5a..000000000 --- a/tests/unit_tests/extrinsics/test_registration.py +++ /dev/null @@ -1,420 +0,0 @@ -# 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. - -from unittest.mock import MagicMock, patch - -import pytest -from bittensor_wallet import Wallet - -from bittensor.api.extrinsics.registration import ( - MaxSuccessException, - MaxAttemptsException, - swap_hotkey_extrinsic, - burned_register_extrinsic, - register_extrinsic, -) -from bittensor.core.subtensor import Subtensor -from bittensor.utils.registration import POWSolution - - -# Mocking external dependencies -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=Subtensor) - mock.network = "mock_network" - mock.substrate = MagicMock() - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=Wallet) - mock.coldkeypub.ss58_address = "mock_address" - mock.coldkey = MagicMock() - mock.hotkey = MagicMock() - mock.hotkey.ss58_address = "fake_ss58_address" - return mock - - -@pytest.fixture -def mock_pow_solution(): - mock = MagicMock(spec=POWSolution) - mock.block_number = 123 - mock.nonce = 456 - mock.seal = [0, 1, 2, 3] - mock.is_stale.return_value = False - return mock - - -@pytest.fixture -def mock_new_wallet(): - mock = MagicMock(spec=Wallet) - mock.coldkeypub.ss58_address = "mock_address" - mock.coldkey = MagicMock() - mock.hotkey = MagicMock() - return mock - - -@pytest.mark.parametrize( - "wait_for_inclusion,wait_for_finalization,prompt,cuda,dev_id,tpb,num_processes,update_interval,log_verbose,expected", - [ - (False, True, False, False, 0, 256, None, None, False, True), - (True, False, False, True, [0], 256, 1, 100, True, False), - (False, False, False, True, 1, 512, 2, 200, False, False), - ], - ids=["happy-path-1", "happy-path-2", "happy-path-3"], -) -def test_run_faucet_extrinsic_happy_path( - mock_subtensor, - mock_wallet, - mock_pow_solution, - wait_for_inclusion, - wait_for_finalization, - prompt, - cuda, - dev_id, - tpb, - num_processes, - update_interval, - log_verbose, - expected, -): - with patch( - "bittensor.utils.registration._solve_for_difficulty_fast", - return_value=mock_pow_solution, - ) as mock_create_pow, patch("rich.prompt.Confirm.ask", return_value=True): - from bittensor.api.extrinsics.registration import run_faucet_extrinsic - - # Arrange - mock_subtensor.get_balance.return_value = 100 - mock_subtensor.substrate.submit_extrinsic.return_value.is_success = True - - # Act - result = run_faucet_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # Assert - if isinstance(result, tuple): - assert result[0] == expected - if result[0] is True: - # Checks only if successful - mock_subtensor.substrate.submit_extrinsic.assert_called() - else: - assert result == expected - mock_subtensor.get_balance.assert_called_with("mock_address") - - -@pytest.mark.parametrize( - "cuda,torch_cuda_available,prompt_response,expected", - [ - ( - True, - False, - False, - False, - ), # ID: edge-case-1: CUDA required but not available, user declines prompt - ( - True, - False, - True, - False, - ), # ID: edge-case-2: CUDA required but not available, user accepts prompt but fails due to CUDA unavailability - ], - ids=["edge-case-1", "edge-case-2"], -) -def test_run_faucet_extrinsic_edge_cases( - mock_subtensor, mock_wallet, cuda, torch_cuda_available, prompt_response, expected -): - with patch("torch.cuda.is_available", return_value=torch_cuda_available), patch( - "rich.prompt.Confirm.ask", return_value=prompt_response - ): - from bittensor.api.extrinsics.registration import run_faucet_extrinsic - - # Act - result = run_faucet_extrinsic( - subtensor=mock_subtensor, wallet=mock_wallet, cuda=cuda - ) - - # Assert - assert result[0] == expected - - -@pytest.mark.parametrize( - "exception,expected", - [ - (KeyboardInterrupt, (True, "Done")), # ID: error-1: User interrupts the process - ( - MaxSuccessException, - (True, "Max successes reached: 3"), - ), # ID: error-2: Maximum successes reached - ( - MaxAttemptsException, - (False, "Max attempts reached: 3"), - ), # ID: error-3: Maximum attempts reached - ], - ids=["error-1", "error-2", "error-3"], -) -@pytest.mark.skip(reason="Waiting for fix to MaxAttemptedException") -def test_run_faucet_extrinsic_error_cases( - mock_subtensor, mock_wallet, mock_pow_solution, exception, expected -): - with patch( - "bittensor.utils.registration.create_pow", - side_effect=[mock_pow_solution, exception], - ): - from bittensor.api.extrinsics.registration import run_faucet_extrinsic - - # Act - result = run_faucet_extrinsic( - subtensor=mock_subtensor, wallet=mock_wallet, max_allowed_attempts=3 - ) - - # Assert - assert result == expected - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, prompt, swap_success, prompt_response, expected_result, test_id", - [ - # Happy paths - (False, True, False, True, None, True, "happy-path-finalization-true"), - (True, False, False, True, None, True, "happy-path-inclusion-true"), - (True, True, False, True, None, True, "edge-both-waits-true"), - # Error paths - (False, True, False, False, None, False, "swap_failed"), - (False, True, True, True, False, False, "error-prompt-declined"), - ], -) -def test_swap_hotkey_extrinsic( - mock_subtensor, - mock_wallet, - mock_new_wallet, - wait_for_inclusion, - wait_for_finalization, - prompt, - swap_success, - prompt_response, - expected_result, - test_id, -): - # Arrange - with patch.object( - mock_subtensor, - "_do_swap_hotkey", - return_value=(swap_success, "Mock error message"), - ): - with patch( - "rich.prompt.Confirm.ask", return_value=prompt_response - ) as mock_confirm: - # Act - result = swap_hotkey_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - new_wallet=mock_new_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_result, f"Test failed for test_id: {test_id}" - - if prompt: - mock_confirm.assert_called_once() - else: - mock_confirm.assert_not_called() - - -@pytest.mark.parametrize( - "subnet_exists, neuron_is_null, recycle_success, prompt, prompt_response, is_registered, expected_result, test_id", - [ - # Happy paths - (True, False, None, False, None, None, True, "neuron-not-null"), - (True, True, True, True, True, True, True, "happy-path-wallet-registered"), - # Error paths - (False, True, False, False, None, None, False, "subnet-non-existence"), - (True, True, True, True, False, None, False, "prompt-declined"), - (True, True, False, True, True, False, False, "error-path-recycling-failed"), - (True, True, True, True, True, False, False, "error-path-not-registered"), - ], -) -def test_burned_register_extrinsic( - mock_subtensor, - mock_wallet, - subnet_exists, - neuron_is_null, - recycle_success, - prompt, - prompt_response, - is_registered, - expected_result, - test_id, -): - # Arrange - with patch.object( - mock_subtensor, "subnet_exists", return_value=subnet_exists - ), patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=MagicMock(is_null=neuron_is_null), - ), patch.object( - mock_subtensor, - "_do_burned_register", - return_value=(recycle_success, "Mock error message"), - ), patch.object( - mock_subtensor, "is_hotkey_registered", return_value=is_registered - ), patch("rich.prompt.Confirm.ask", return_value=prompt_response) as mock_confirm: - # Act - result = burned_register_extrinsic( - subtensor=mock_subtensor, wallet=mock_wallet, netuid=123, prompt=True - ) - - # Assert - assert result == expected_result, f"Test failed for test_id: {test_id}" - - if prompt: - mock_confirm.assert_called_once() - else: - mock_confirm.assert_not_called() - - -@pytest.mark.parametrize( - "subnet_exists, neuron_is_null, prompt, prompt_response, cuda_available, expected_result, test_id", - [ - (False, True, True, True, True, False, "subnet-does-not-exist"), - (True, False, True, True, True, True, "neuron-already-registered"), - (True, True, True, False, True, False, "user-declines-prompt"), - (True, True, False, None, False, False, "cuda-unavailable"), - ], -) -def test_register_extrinsic_without_pow( - mock_subtensor, - mock_wallet, - subnet_exists, - neuron_is_null, - prompt, - prompt_response, - cuda_available, - expected_result, - test_id, -): - # Arrange - with patch.object( - mock_subtensor, "subnet_exists", return_value=subnet_exists - ), patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=MagicMock(is_null=neuron_is_null), - ), patch("rich.prompt.Confirm.ask", return_value=prompt_response), patch( - "torch.cuda.is_available", return_value=cuda_available - ): - # Act - result = register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=prompt, - 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( - "pow_success, pow_stale, registration_success, cuda, hotkey_registered, expected_result, test_id", - [ - (True, False, True, False, False, True, "successful-with-valid-pow"), - (True, False, True, True, False, True, "successful-with-valid-cuda-pow"), - # Pow failed but key was registered already - (False, False, False, False, True, True, "hotkey-registered"), - # Pow was a success but registration failed with error 'key already registered' - (True, False, False, False, False, True, "registration-fail-key-registered"), - ], -) -def test_register_extrinsic_with_pow( - mock_subtensor, - mock_wallet, - mock_pow_solution, - pow_success, - pow_stale, - registration_success, - cuda, - hotkey_registered, - expected_result, - test_id, -): - # Arrange - with patch( - "bittensor.utils.registration._solve_for_difficulty_fast", - return_value=mock_pow_solution if pow_success else None, - ), patch( - "bittensor.utils.registration._solve_for_difficulty_fast_cuda", - return_value=mock_pow_solution if pow_success else None, - ), patch.object( - mock_subtensor, - "_do_pow_register", - return_value=(registration_success, "HotKeyAlreadyRegisteredInSubNet"), - ), 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 = MagicMock( - return_value=hotkey_registered - ) - - result = register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=False, - 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}" diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py deleted file mode 100644 index 7e1137b7b..000000000 --- a/tests/unit_tests/extrinsics/test_root.py +++ /dev/null @@ -1,310 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pytest - -from bittensor.api.extrinsics.root import ( - root_register_extrinsic, - set_root_weights_extrinsic, -) -from bittensor.core.subtensor import Subtensor - - -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=Subtensor) - mock.network = "magic_mock" - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock() - mock.hotkey.ss58_address = "fake_hotkey_address" - return mock - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, hotkey_registered, registration_success, prompt, user_response, expected_result", - [ - ( - False, - True, - [True, None], - True, - True, - True, - True, - ), # Already registered after attempt - ( - False, - True, - [False, True], - True, - True, - True, - True, - ), # Registration succeeds with user confirmation - (False, True, [False, False], False, False, None, None), # Registration fails - ( - False, - True, - [False, False], - True, - False, - None, - None, - ), # Registration succeeds but neuron not found - ( - False, - True, - [False, False], - True, - True, - False, - False, - ), # User declines registration - ], - ids=[ - "success-already-registered", - "success-registration-succeeds", - "failure-registration-failed", - "failure-neuron-not-found", - "failure-prompt-declined", - ], -) -def test_root_register_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - hotkey_registered, - registration_success, - prompt, - user_response, - expected_result, -): - # Arrange - mock_subtensor.is_hotkey_registered.side_effect = hotkey_registered - - with patch.object( - mock_subtensor, - "_do_root_register", - return_value=(registration_success, "Error registering"), - ) as mock_register, patch("rich.prompt.Confirm.ask", return_value=user_response): - # Act - result = root_register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - # Assert - assert result == expected_result - - if not hotkey_registered[0] and user_response: - mock_register.assert_called_once() - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, netuids, weights, prompt, user_response, expected_success", - [ - (True, False, [1, 2], [0.5, 0.5], True, True, True), # Success - weights set - ( - False, - False, - [1, 2], - [0.5, 0.5], - False, - None, - True, - ), # Success - weights set no wait - ( - True, - False, - [1, 2], - [2000, 20], - True, - True, - True, - ), # Success - large value to be normalized - ( - True, - False, - [1, 2], - [2000, 0], - True, - True, - True, - ), # Success - single large value - ( - True, - False, - [1, 2], - [0.5, 0.5], - True, - False, - False, - ), # Failure - prompt declined - ( - True, - False, - [1, 2], - [0.5, 0.5], - False, - None, - False, - ), # Failure - setting weights failed - ( - True, - False, - [], - [], - None, - False, - False, - ), # Exception catched - ValueError 'min() arg is an empty sequence' - ], - ids=[ - "success-weights-set", - "success-not-wait", - "success-large-value", - "success-single-value", - "failure-user-declines", - "failure-setting-weights", - "failure-value-error-exception", - ], -) -def test_set_root_weights_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - netuids, - weights, - prompt, - user_response, - expected_success, -): - # Arrange - with patch.object( - mock_subtensor, - "_do_set_root_weights", - return_value=(expected_success, "Mock error"), - ), patch.object( - mock_subtensor, "min_allowed_weights", return_value=0 - ), patch.object(mock_subtensor, "max_weight_limit", return_value=1), patch( - "rich.prompt.Confirm.ask", return_value=user_response - ) as mock_confirm: - # Act - result = set_root_weights_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuids=netuids, - weights=weights, - version_key=0, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_success - if prompt: - mock_confirm.assert_called_once() - else: - mock_confirm.assert_not_called() - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, netuids, weights, prompt, user_response, expected_success", - [ - (True, False, [1, 2], [0.5, 0.5], True, True, True), # Success - weights set - ( - False, - False, - [1, 2], - [0.5, 0.5], - False, - None, - True, - ), # Success - weights set no wait - ( - True, - False, - [1, 2], - [2000, 20], - True, - True, - True, - ), # Success - large value to be normalized - ( - True, - False, - [1, 2], - [2000, 0], - True, - True, - True, - ), # Success - single large value - ( - True, - False, - [1, 2], - [0.5, 0.5], - True, - False, - False, - ), # Failure - prompt declined - ( - True, - False, - [1, 2], - [0.5, 0.5], - False, - None, - False, - ), # Failure - setting weights failed - ( - True, - False, - [], - [], - None, - False, - False, - ), # Exception catched - ValueError 'min() arg is an empty sequence' - ], - ids=[ - "success-weights-set", - "success-not-wait", - "success-large-value", - "success-single-value", - "failure-user-declines", - "failure-setting-weights", - "failure-value-error-exception", - ], -) -def test_set_root_weights_extrinsic_torch( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - netuids, - weights, - prompt, - user_response, - expected_success, - force_legacy_torch_compatible_api, -): - test_set_root_weights_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - netuids, - weights, - prompt, - user_response, - expected_success, - ) diff --git a/tests/unit_tests/extrinsics/test_senate.py b/tests/unit_tests/extrinsics/test_senate.py deleted file mode 100644 index 32bb7bdff..000000000 --- a/tests/unit_tests/extrinsics/test_senate.py +++ /dev/null @@ -1,238 +0,0 @@ -import pytest -from unittest.mock import MagicMock, patch -from bittensor.core.subtensor import Subtensor -from bittensor_wallet import Wallet -from bittensor.api.extrinsics.senate import ( - leave_senate_extrinsic, - register_senate_extrinsic, - vote_senate_extrinsic, -) - - -# Mocking external dependencies -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=Subtensor) - mock.substrate = MagicMock() - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=Wallet) - mock.coldkey = MagicMock() - mock.hotkey = MagicMock() - mock.hotkey.ss58_address = "fake_hotkey_address" - mock.is_senate_member = None - return mock - - -# Parametrized test cases -@pytest.mark.parametrize( - "wait_for_inclusion,wait_for_finalization,prompt,response_success,is_registered,expected_result, test_id", - [ - # Happy path tests - (False, True, False, True, True, True, "happy-path-finalization-true"), - (True, False, False, True, True, True, "happy-path-inclusion-true"), - (False, False, False, True, True, True, "happy-path-no_wait"), - # Edge cases - (True, True, False, True, True, True, "edge-both-waits-true"), - # Error cases - (False, True, False, False, False, None, "error-finalization-failed"), - (True, False, False, False, False, None, "error-inclusion-failed"), - (False, True, True, True, False, False, "error-prompt-declined"), - ], -) -def test_register_senate_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - prompt, - response_success, - is_registered, - expected_result, - test_id, -): - # Arrange - with patch( - "bittensor.api.extrinsics.senate.Confirm.ask", return_value=not prompt - ), patch("bittensor.api.extrinsics.senate.time.sleep"), 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", - ), - ) as mock_submit_extrinsic, patch.object( - mock_subtensor, "is_senate_member", return_value=is_registered - ): - # Act - result = register_senate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_result, f"Test ID: {test_id}" - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, prompt, response_success, \ - vote, vote_in_ayes, vote_in_nays, expected_result, test_id", - [ - # Happy path tests - (False, True, False, True, True, True, False, True, "happy-finalization-aye"), - (True, False, False, True, False, False, True, True, "happy-inclusion-nay"), - (False, False, False, True, True, True, False, True, "happy-no-wait-aye"), - # Edge cases - (True, True, False, True, True, True, False, True, "edge-both-waits-true-aye"), - # Error cases - (True, False, False, False, True, False, False, None, "error-inclusion-failed"), - (True, False, True, True, True, True, False, False, "error-prompt-declined"), - ( - True, - False, - False, - True, - True, - False, - False, - None, - "error-no-vote-registered-aye", - ), - ( - False, - True, - False, - True, - False, - False, - False, - None, - "error-no-vote-registered-nay", - ), - ( - False, - True, - False, - False, - True, - False, - False, - None, - "error-finalization-failed", - ), - ], -) -def test_vote_senate_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - prompt, - vote, - response_success, - vote_in_ayes, - vote_in_nays, - expected_result, - test_id, -): - # Arrange - proposal_hash = "mock_hash" - proposal_idx = 123 - - with patch( - "bittensor.api.extrinsics.senate.Confirm.ask", return_value=not prompt - ), patch("bittensor.api.extrinsics.senate.time.sleep"), 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", - ), - ), patch.object( - mock_subtensor, - "get_vote_data", - return_value={ - "ayes": [mock_wallet.hotkey.ss58_address] if vote_in_ayes else [], - "nays": [mock_wallet.hotkey.ss58_address] if vote_in_nays else [], - }, - ): - # Act - result = vote_senate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - proposal_hash=proposal_hash, - proposal_idx=proposal_idx, - vote=vote, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_result, f"Test ID: {test_id}" - - -# Parametrized test cases -@pytest.mark.parametrize( - "wait_for_inclusion,wait_for_finalization,prompt,response_success,is_registered,expected_result, test_id", - [ - # Happy path tests - (False, True, False, True, False, True, "happy-path-finalization-true"), - (True, False, False, True, False, True, "happy-path-inclusion-true"), - (False, False, False, True, False, True, "happy-path-no_wait"), - # Edge cases - (True, True, False, True, False, True, "edge-both-waits-true"), - # Error cases - (False, True, False, False, True, None, "error-finalization-failed"), - (True, False, False, False, True, None, "error-inclusion-failed"), - (False, True, True, True, False, False, "error-prompt-declined"), - ], -) -def test_leave_senate_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - prompt, - response_success, - is_registered, - expected_result, - test_id, -): - # Arrange - with patch( - "bittensor.api.extrinsics.senate.Confirm.ask", return_value=not prompt - ), patch("bittensor.api.extrinsics.senate.time.sleep"), 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", - ), - ), patch.object(mock_subtensor, "is_senate_member", return_value=is_registered): - # Act - result = leave_senate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_result, f"Test ID: {test_id}" diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 26f25af0b..7dd065844 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -118,7 +118,7 @@ def test_serve_extrinsic_happy_path( test_id, ): # Arrange - mock_subtensor._do_serve_axon.return_value = (True, "") + mock_subtensor.do_serve_axon.return_value = (True, "") with patch("bittensor.api.extrinsics.serving.Confirm.ask", return_value=True): # Act result = serve_extrinsic( @@ -175,7 +175,7 @@ def test_serve_extrinsic_edge_cases( test_id, ): # Arrange - mock_subtensor._do_serve_axon.return_value = (True, "") + mock_subtensor.do_serve_axon.return_value = (True, "") with patch("bittensor.api.extrinsics.serving.Confirm.ask", return_value=True): # Act result = serve_extrinsic( @@ -232,7 +232,7 @@ def test_serve_extrinsic_error_cases( test_id, ): # Arrange - mock_subtensor._do_serve_axon.return_value = (False, "Error serving axon") + mock_subtensor.do_serve_axon.return_value = (False, "Error serving axon") with patch("bittensor.api.extrinsics.serving.Confirm.ask", return_value=True): # Act result = serve_extrinsic( @@ -307,13 +307,12 @@ def test_serve_axon_extrinsic( # Act if not external_ip_success: with pytest.raises(RuntimeError): - result = serve_axon_extrinsic( + serve_axon_extrinsic( mock_subtensor, netuid, mock_axon, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - prompt=prompt, ) else: result = serve_axon_extrinsic( @@ -322,8 +321,8 @@ def test_serve_axon_extrinsic( mock_axon, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - prompt=prompt, ) + # Assert assert result == expected_result, f"Test ID: {test_id}" diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 38d4a6944..6c12db75d 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -75,7 +75,7 @@ def test_set_weights_extrinsic( return_value=(uids_tensor, weights_tensor), ), patch("rich.prompt.Confirm.ask", return_value=user_accepts), patch.object( mock_subtensor, - "_do_set_weights", + "do_set_weights", return_value=(expected_success, "Mock error message"), ) as mock_do_set_weights: result, message = set_weights_extrinsic( diff --git a/tests/unit_tests/extrinsics/test_staking.py b/tests/unit_tests/extrinsics/test_staking.py deleted file mode 100644 index c91fda171..000000000 --- a/tests/unit_tests/extrinsics/test_staking.py +++ /dev/null @@ -1,567 +0,0 @@ -# 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 pytest -from unittest.mock import patch, MagicMock -from bittensor.utils.balance import Balance -from bittensor.api.extrinsics.staking import ( - add_stake_extrinsic, - add_stake_multiple_extrinsic, -) -from bittensor.core.errors import NotDelegateError -from bittensor.core.subtensor import Subtensor -from bittensor_wallet import Wallet - - -# Mocking external dependencies -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=Subtensor) - mock.network = "mock_network" - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=Wallet) - mock.hotkey.ss58_address = "5FHneW46..." - mock.coldkeypub.ss58_address = "5Gv8YYFu8..." - mock.hotkey_str = "mock_hotkey_str" - mock.name = "mock_wallet" - return mock - - -@pytest.fixture -def mock_other_owner_wallet(): - mock = MagicMock(spec=Wallet) - mock.hotkey.ss58_address = "11HneC46..." - mock.coldkeypub.ss58_address = "6Gv9ZZFu8..." - mock.hotkey_str = "mock_hotkey_str_other_owner" - mock.name = "mock_wallet_other_owner" - return mock - - -# Parametrized test cases -@pytest.mark.parametrize( - "hotkey_ss58, hotkey_owner, hotkey_delegate, amount, wait_for_inclusion, wait_for_finalization, prompt, user_accepts, expected_success, exception", - [ - # Simple staking to own hotkey, float - (None, True, None, 10.0, True, False, False, None, True, None), - # Simple staking to own hotkey, int - (None, True, None, 10, True, False, False, None, True, None), - # Not waiting for inclusion & finalization, own hotkey - ("5FHneW46...", True, None, 10.0, False, False, False, None, True, None), - # Prompt refused - (None, True, None, 10.0, True, False, True, False, False, None), - # Stake all - (None, True, None, None, True, False, False, None, True, None), - # Insufficient balance - (None, True, None, 110, True, False, False, None, False, None), - # No deduction scenario - (None, True, None, 0.000000100, True, False, False, None, True, None), - # Not owner but Delegate - ("5FHneW46...", False, True, 10.0, True, False, False, None, True, None), - # Not owner but Delegate and prompt refused - ("5FHneW46...", False, True, 10.0, True, False, True, False, False, None), - # Not owner and not delegate - ( - "5FHneW46...", - False, - False, - 10.0, - True, - False, - False, - None, - False, - NotDelegateError, - ), - # Staking failed - (None, True, None, 10.0, True, False, False, None, False, None), - ], - ids=[ - "success-own-hotkey-float", - "success-own-hotkey-int", - "success-own-hotkey-no-wait", - "prompt-refused", - "success-staking-all", - "failure-insufficient-balance", - "success-no-deduction", - "success-delegate", - "failure-delegate-prompt-refused", - "failure-not-delegate", - "failure-staking", - ], -) -def test_add_stake_extrinsic( - mock_subtensor, - mock_wallet, - mock_other_owner_wallet, - hotkey_ss58, - hotkey_owner, - hotkey_delegate, - amount, - wait_for_inclusion, - wait_for_finalization, - prompt, - user_accepts, - expected_success, - exception, -): - # Arrange - if not amount: - staking_balance = amount if amount else Balance.from_tao(100) - else: - staking_balance = ( - Balance.from_tao(amount) if not isinstance(amount, Balance) else amount - ) - - with patch.object( - mock_subtensor, "_do_stake", return_value=expected_success - ) as mock_add_stake, patch.object( - mock_subtensor, "get_balance", return_value=Balance.from_tao(100) - ), patch.object( - mock_subtensor, - "get_stake_for_coldkey_and_hotkey", - return_value=Balance.from_tao(50), - ), patch.object( - mock_subtensor, - "get_hotkey_owner", - return_value=mock_wallet.coldkeypub.ss58_address - if hotkey_owner - else mock_other_owner_wallet.coldkeypub.ss58_address, - ), patch.object( - mock_subtensor, "is_hotkey_delegate", return_value=hotkey_delegate - ), patch.object(mock_subtensor, "get_delegate_take", return_value=0.01), patch( - "rich.prompt.Confirm.ask", return_value=user_accepts - ) as mock_confirm, patch.object( - mock_subtensor, - "get_minimum_required_stake", - return_value=Balance.from_tao(0.01), - ), patch.object( - mock_subtensor, - "get_existential_deposit", - return_value=Balance.from_rao(100_000), - ): - mock_balance = mock_subtensor.get_balance() - existential_deposit = mock_subtensor.get_existential_deposit() - if staking_balance > mock_balance - existential_deposit: - staking_balance = mock_balance - existential_deposit - - # Act - if not hotkey_owner and not hotkey_delegate: - with pytest.raises(exception): - result = add_stake_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - else: - result = add_stake_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert ( - result == expected_success - ), f"Expected {expected_success}, but got {result}" - - if prompt: - mock_confirm.assert_called_once() - - if expected_success: - if not hotkey_ss58: - hotkey_ss58 = mock_wallet.hotkey.ss58_address - - mock_add_stake.assert_called_once_with( - wallet=mock_wallet, - hotkey_ss58=hotkey_ss58, - amount=staking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -# Parametrized test cases -@pytest.mark.parametrize( - "hotkey_ss58s, amounts, hotkey_owner, hotkey_delegates ,wallet_balance, wait_for_inclusion, wait_for_finalization, prompt, prompt_response, stake_responses, expected_success, stake_attempted, exception, exception_msg", - [ - # Successful stake - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, True], - [None, None], - 100.0, - True, - False, - False, - None, - [True, True], - True, - 2, - None, - None, - ), - # Successful stake with prompt - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, True], - [None, None], - 100.0, - True, - True, - True, - True, - [True, True], - True, - 2, - None, - None, - ), - # Successful stake, no deduction scenario - ( - ["5FHneW46...", "11HneC46..."], - [0.000000100, 0.000000100], - [True, True], - [None, None], - 100.0, - True, - False, - False, - None, - [True, True], - True, - 2, - None, - None, - ), - # Successful stake, not waiting for finalization & inclusion - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, True], - [None, None], - 100.0, - False, - False, - False, - None, - [True, True], - True, - 2, - None, - None, - ), - # Successful stake, one key is a delegate - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, False], - [True, True], - 100.0, - True, - False, - False, - None, - [True, True], - True, - 2, - None, - None, - ), - # Partial successful stake, one key is not a delegate - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, False], - [True, False], - 100.0, - True, - False, - False, - None, - [True, False], - True, - 1, - None, - None, - ), - # Successful, staking all tao to first wallet, not waiting for finalization + inclusion - ( - ["5FHneW46...", "11HneC46..."], - None, - [True, True], - [None, None], - 100.0, - False, - False, - False, - None, - [True, False], - True, - 1, - None, - None, - ), - # Successful, staking all tao to first wallet - ( - ["5FHneW46...", "11HneC46..."], - None, - [True, True], - [None, None], - 100.0, - True, - False, - False, - None, - [True, False], - True, - 1, - None, - None, - ), - # Success, staking 0 tao - ( - ["5FHneW46...", "11HneC46..."], - [0.0, 0.0], - [True, True], - [None, None], - 100.0, - True, - False, - False, - None, - [None, None], - True, - 0, - None, - None, - ), - # Complete failure to stake for both keys - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, True], - [None, None], - 100.0, - True, - False, - False, - None, - [False, False], - False, - 2, - None, - None, - ), - # Complete failure, both keys are not delegates - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [False, False], - [False, False], - 100.0, - True, - False, - False, - None, - [False, False], - False, - 0, - None, - None, - ), - # Unsuccessful stake with prompt declined both times - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, True], - [None, None], - 100.0, - True, - True, - True, - False, - [None, None], - False, - 0, - None, - None, - ), - # Exception, TypeError for incorrect hotkey_ss58s - ( - [123, "11HneC46..."], - [10.0, 20.0], - [False, False], - [False, False], - 100.0, - True, - False, - False, - None, - [None, None], - None, - 0, - TypeError, - "hotkey_ss58s must be a list of str", - ), - # Exception, ValueError for mismatch between hotkeys and amounts - ( - ["5FHneW46...", "11HneC46..."], - [10.0], - [False, False], - [False, False], - 100.0, - True, - False, - False, - None, - [None, None], - None, - 0, - ValueError, - "amounts must be a list of the same length as hotkey_ss58s", - ), - # Exception, TypeError for incorrect amounts - ( - ["5FHneW46...", "11HneC46..."], - ["abc", 12], - [False, False], - [False, False], - 100.0, - True, - False, - False, - None, - [None, None], - None, - 0, - TypeError, - "amounts must be a [list of Balance or float] or None", - ), - ], - ids=[ - "success-basic-path", - "success-with-prompt", - "success-no-deduction", - "success-no-wait", - "success-one-delegate", - "partial-success-one-not-delegate", - "success-all-tao-no-wait", - "success-all-tao", - "success-0-tao", - "failure-both-keys", - "failure-both-not-delegates", - "failure-prompt-declined", - "failure-type-error-hotkeys", - "failure-value-error-amount", - "failure-type-error-amount", - ], -) -def test_add_stake_multiple_extrinsic( - mock_subtensor, - mock_wallet, - mock_other_owner_wallet, - hotkey_ss58s, - amounts, - hotkey_owner, - hotkey_delegates, - wallet_balance, - wait_for_inclusion, - wait_for_finalization, - prompt, - prompt_response, - stake_responses, - expected_success, - stake_attempted, - exception, - exception_msg, -): - # Arrange - def hotkey_delegate_side_effect(hotkey_ss58): - index = hotkey_ss58s.index(hotkey_ss58) - return hotkey_delegates[index] - - def owner_side_effect(hotkey_ss58): - index = hotkey_ss58s.index(hotkey_ss58) - return ( - mock_wallet.coldkeypub.ss58_address - if hotkey_owner[index] - else mock_other_owner_wallet.coldkeypub.ss58_address - ) - - def stake_side_effect(hotkey_ss58, *args, **kwargs): - index = hotkey_ss58s.index(hotkey_ss58) - return stake_responses[index] - - with patch.object( - mock_subtensor, "get_balance", return_value=Balance.from_tao(wallet_balance) - ), patch.object( - mock_subtensor, "is_hotkey_delegate", side_effect=hotkey_delegate_side_effect - ), patch.object( - mock_subtensor, "get_hotkey_owner", side_effect=owner_side_effect - ), patch.object( - mock_subtensor, "_do_stake", side_effect=stake_side_effect - ) as mock_do_stake, patch.object( - mock_subtensor, "tx_rate_limit", return_value=0 - ), patch("rich.prompt.Confirm.ask", return_value=prompt_response) as mock_confirm: - # Act - if exception: - with pytest.raises(exception) as exc_info: - result = add_stake_multiple_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - # Assert - assert str(exc_info.value) == exception_msg - - # Act - else: - result = add_stake_multiple_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert ( - result == expected_success - ), f"Expected {expected_success}, but got {result}" - if prompt: - assert mock_confirm.called - assert mock_do_stake.call_count == stake_attempted diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py deleted file mode 100644 index 107757fd0..000000000 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ /dev/null @@ -1,334 +0,0 @@ -from unittest.mock import patch, MagicMock - -import pytest -from bittensor_wallet import Wallet - -from bittensor.api.extrinsics.unstaking import ( - unstake_extrinsic, - unstake_multiple_extrinsic, -) -from bittensor.core.subtensor import Subtensor -from bittensor.utils.balance import Balance - - -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=Subtensor) - mock.network = "mock_network" - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=Wallet) - mock.hotkey.ss58_address = "5FHneW46..." - mock.coldkeypub.ss58_address = "5Gv8YYFu8..." - mock.hotkey_str = "mock_hotkey_str" - return mock - - -def mock_get_minimum_required_stake(): - # Valid minimum threshold as of 2024/05/01 - return Balance.from_rao(100_000_000) - - -@pytest.mark.parametrize( - "hotkey_ss58, amount, wait_for_inclusion, wait_for_finalization, prompt, user_accepts, expected_success, unstake_attempted", - [ - # Successful unstake without waiting for inclusion or finalization - (None, 10.0, False, False, False, None, True, True), - # Successful unstake with prompt accepted - ("5FHneW46...", 10.0, True, True, True, True, True, True), - # Prompt declined - ("5FHneW46...", 10.0, True, True, True, False, False, False), - # Not enough stake to unstake - ("5FHneW46...", 1000.0, True, True, False, None, False, False), - # Successful - unstake threshold not reached - (None, 0.01, True, True, False, None, True, True), - # Successful unstaking all - (None, None, False, False, False, None, True, True), - # Failure - unstaking failed - (None, 10.0, False, False, False, None, False, True), - ], - ids=[ - "successful-no-wait", - "successful-with-prompt", - "failure-prompt-declined", - "failure-not-enough-stake", - "success-threshold-not-reached", - "success-unstake-all", - "failure-unstake-failed", - ], -) -def test_unstake_extrinsic( - mock_subtensor, - mock_wallet, - hotkey_ss58, - amount, - wait_for_inclusion, - wait_for_finalization, - prompt, - user_accepts, - expected_success, - unstake_attempted, -): - mock_current_stake = Balance.from_tao(50) - mock_current_balance = Balance.from_tao(100) - - with patch.object( - mock_subtensor, "_do_unstake", return_value=(expected_success) - ), patch.object( - mock_subtensor, "get_balance", return_value=mock_current_balance - ), patch.object( - mock_subtensor, - "get_minimum_required_stake", - side_effect=mock_get_minimum_required_stake, - ), patch.object( - mock_subtensor, - "get_stake_for_coldkey_and_hotkey", - return_value=mock_current_stake, - ), patch("rich.prompt.Confirm.ask", return_value=user_accepts) as mock_confirm: - result = unstake_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - assert ( - result == expected_success - ), f"Expected result {expected_success}, but got {result}" - - if prompt: - mock_confirm.assert_called_once() - - if unstake_attempted: - mock_subtensor._do_unstake.assert_called_once_with( - wallet=mock_wallet, - hotkey_ss58=hotkey_ss58 or mock_wallet.hotkey.ss58_address, - amount=Balance.from_tao(amount) if amount else mock_current_stake, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - else: - mock_subtensor._do_unstake.assert_not_called() - - -@pytest.mark.parametrize( - # TODO: Write dynamic test to test for amount = None with multiple hotkeys - "hotkey_ss58s, amounts, wallet_balance, wait_for_inclusion, wait_for_finalization, prompt, prompt_response, unstake_responses, expected_success, unstake_attempted, exception, exception_msg", - [ - # Successful unstake - no wait - ( - ["5FHneW46...", "5FHneW47..."], - [10.0, 20.0], - 100, - False, - False, - True, - True, - [True, True], - True, - 2, - None, - None, - ), - # Partial-success unstake - one unstake fails - ( - ["5FHneW46...", "5FHneW47..."], - [10.0, 20.0], - 100, - True, - False, - True, - True, - [True, False], - True, - 2, - None, - None, - ), - # Success, based on no hotkeys - func to be confirmed - ([], [], 100, True, True, False, None, [None], True, 0, None, None), - # Unsuccessful unstake - not enough stake - ( - ["5FHneW46..."], - [1000.0], - 100, - True, - True, - False, - True, - [None], - False, - 0, - None, - None, - ), - # Successful unstake - new stake below threshold - ( - ["5FHneW46..."], - [ - 100 - mock_get_minimum_required_stake() + 0.01 - ], # New stake just below threshold - 100, - True, - True, - False, - True, - [True], - True, # Sucessful unstake - 1, - None, - None, - ), - # Unsuccessful unstake with prompt declined both times - ( - ["5FHneW46...", "5FHneW48..."], - [10.0, 10.0], - 100, - True, - True, - True, - False, - [None, None], - False, - 0, - None, - None, - ), - # Exception, TypeError for incorrect hotkey_ss58s - ( - ["5FHneW46...", 123], - [10.0, 20.0], - 100, - True, - False, - False, - None, - [None, None], - None, - 0, - TypeError, - "hotkey_ss58s must be a list of str", - ), - # Exception, ValueError for mismatch between hotkeys and amounts - ( - ["5FHneW46...", "5FHneW48..."], - [10.0], - 100, - True, - False, - False, - None, - [None, None], - None, - 0, - ValueError, - "amounts must be a list of the same length as hotkey_ss58s", - ), - # Exception, TypeError for incorrect amounts - ( - ["5FHneW46...", "5FHneW48..."], - [10.0, "tao"], - 100, - True, - False, - False, - None, - [None, None], - None, - 0, - TypeError, - "amounts must be a [list of Balance or float] or None", - ), - ], - ids=[ - "success-no-wait", - "partial-success-one-fail", - "success-no-hotkey", - "failure-not-enough-stake", - "success-threshold-not-reached", - "failure-prompt-declined", - "failure-type-error-hotkeys", - "failure-value-error-amounts", - "failure-type-error-amounts", - ], -) -def test_unstake_multiple_extrinsic( - mock_subtensor, - mock_wallet, - hotkey_ss58s, - amounts, - wallet_balance, - wait_for_inclusion, - wait_for_finalization, - prompt, - prompt_response, - unstake_responses, - expected_success, - unstake_attempted, - exception, - exception_msg, -): - # Arrange - mock_current_stake = Balance.from_tao(100) - amounts_in_balances = [ - Balance.from_tao(amount) if isinstance(amount, float) else amount - for amount in amounts - ] - - def unstake_side_effect(hotkey_ss58, *args, **kwargs): - index = hotkey_ss58s.index(hotkey_ss58) - return unstake_responses[index] - - with patch.object( - mock_subtensor, "_do_unstake", side_effect=unstake_side_effect - ) as mock_unstake, patch.object( - mock_subtensor, - "get_minimum_required_stake", - side_effect=mock_get_minimum_required_stake, - ), patch.object( - mock_subtensor, "get_balance", return_value=Balance.from_tao(wallet_balance) - ), patch.object(mock_subtensor, "tx_rate_limit", return_value=0), patch.object( - mock_subtensor, - "get_stake_for_coldkey_and_hotkey", - return_value=mock_current_stake, - ), patch("rich.prompt.Confirm.ask", return_value=prompt_response) as mock_confirm: - # Act - if exception: - with pytest.raises(exception) as exc_info: - result = unstake_multiple_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - # Assert - assert str(exc_info.value) == exception_msg - - # Act - else: - result = unstake_multiple_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts_in_balances, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert ( - result == expected_success - ), f"Expected {expected_success}, but got {result}" - if prompt: - assert mock_confirm.called - assert mock_unstake.call_count == unstake_attempted diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index 8d450c4c6..976a7a4b5 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -263,7 +263,6 @@ async def test_blacklist_fail(middleware): @pytest.mark.asyncio -@pytest.mark.skip("middleware.priority runs infinitely") async def test_priority_pass(middleware): synapse = SynapseMock() middleware.axon.priority_fns = {"SynapseMock": priority_fn_pass} diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index e7ccc691f..f6b101ac6 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -31,7 +31,7 @@ Dendrite, ) from bittensor.core.synapse import TerminalInfo -from tests.helpers import _get_mock_wallet +from tests.helpers import get_mock_wallet from bittensor.core.synapse import Synapse from bittensor.core.chain_data import AxonInfo @@ -49,7 +49,7 @@ def dummy(synapse: SynapseDummy) -> SynapseDummy: @pytest.fixture def setup_dendrite(): # Assuming bittensor.wallet() returns a wallet object - user_wallet = _get_mock_wallet() + user_wallet = get_mock_wallet() dendrite_obj = Dendrite(user_wallet) return dendrite_obj @@ -109,7 +109,7 @@ async def test_aclose(dendrite_obj, setup_axon): axon = setup_axon # Use context manager to open an async session async with dendrite_obj: - resp = await dendrite_obj([axon], SynapseDummy(input=1), deserialize=False) + await dendrite_obj([axon], SynapseDummy(input=1), deserialize=False) # Close should automatically be called on the session after context manager scope assert dendrite_obj._session is None @@ -128,16 +128,16 @@ def __await__(self): def test_dendrite_create_wallet(): - d = Dendrite(_get_mock_wallet()) - d = Dendrite(_get_mock_wallet().hotkey) - d = Dendrite(_get_mock_wallet().coldkeypub) + d = Dendrite(get_mock_wallet()) + d = Dendrite(get_mock_wallet().hotkey) + d = Dendrite(get_mock_wallet().coldkeypub) assert d.__str__() == d.__repr__() @pytest.mark.asyncio async def test_forward_many(): n = 10 - d = Dendrite(wallet=_get_mock_wallet()) + d = Dendrite(wallet=get_mock_wallet()) d.call = AsyncMock() axons = [MagicMock() for _ in range(n)] @@ -153,10 +153,10 @@ async def test_forward_many(): def test_pre_process_synapse(): - d = Dendrite(wallet=_get_mock_wallet()) + d = Dendrite(wallet=get_mock_wallet()) s = Synapse() synapse = d.preprocess_synapse_for_request( - target_axon_info=Axon(wallet=_get_mock_wallet()).info(), + target_axon_info=Axon(wallet=get_mock_wallet()).info(), synapse=s, timeout=12, ) diff --git a/tests/unit_tests/test_metagraph.py b/tests/unit_tests/test_metagraph.py index 826fa527f..c04490df8 100644 --- a/tests/unit_tests/test_metagraph.py +++ b/tests/unit_tests/test_metagraph.py @@ -129,7 +129,7 @@ def test_process_weights_or_bonds(mock_environment): @pytest.fixture def mock_subtensor(): subtensor = MagicMock() - subtensor.chain_endpoint = settings.finney_entrypoint + subtensor.chain_endpoint = settings.FINNEY_ENTRYPOINT subtensor.network = "finney" subtensor.get_current_block.return_value = 601 return subtensor diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 08a919ff7..8c9ace9b8 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -27,7 +27,7 @@ from bittensor.core.axon import Axon from bittensor.core.chain_data import SubnetHyperparameters from bittensor.core.subtensor import Subtensor, logging -from bittensor.utils import U16_NORMALIZED_FLOAT, U64_NORMALIZED_FLOAT +from bittensor.utils import u16_normalized_float, u64_normalized_float from bittensor.utils.balance import Balance U16_MAX = 65535 @@ -135,58 +135,6 @@ class ExitEarly(Exception): pass -def test_stake_multiple(): - mock_amount: Balance = Balance.from_tao(1.0) - - mock_wallet = MagicMock( - spec=Wallet, - coldkey=MagicMock(), - coldkeypub=MagicMock( - # mock ss58 address - ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - ), - hotkey=MagicMock( - ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" - ), - ) - - mock_hotkey_ss58s = ["5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg"] - - mock_amounts = [mock_amount] # more than 1000 RAO - - mock_neuron = MagicMock( - is_null=False, - ) - - mock_do_stake = MagicMock(side_effect=ExitEarly) - - mock_subtensor = MagicMock( - spec=Subtensor, - network="mock_net", - get_balance=MagicMock( - return_value=Balance.from_tao(mock_amount.tao + 20.0) - ), # enough balance to stake - get_neuron_for_pubkey_and_subnet=MagicMock(return_value=mock_neuron), - _do_stake=mock_do_stake, - ) - - with pytest.raises(ExitEarly): - Subtensor.add_stake_multiple( - mock_subtensor, - wallet=mock_wallet, - hotkey_ss58s=mock_hotkey_ss58s, - amounts=mock_amounts, - ) - - mock_do_stake.assert_called_once() - # args, kwargs - _, kwargs = mock_do_stake.call_args - - assert kwargs["amount"] == pytest.approx( - mock_amount.rao, rel=1e9 - ) # delta of 1.0 TAO - - @pytest.mark.parametrize( "test_id, expected_output", [ @@ -225,40 +173,40 @@ def mock_add_argument(*args, **kwargs): "network, expected_network, expected_endpoint", [ # Happy path tests - ("finney", "finney", settings.finney_entrypoint), - ("local", "local", settings.local_entrypoint), - ("test", "test", settings.finney_test_entrypoint), - ("archive", "archive", settings.archive_entrypoint), + ("finney", "finney", settings.FINNEY_ENTRYPOINT), + ("local", "local", settings.LOCAL_ENTRYPOINT), + ("test", "test", settings.FINNEY_TEST_ENTRYPOINT), + ("archive", "archive", settings.ARCHIVE_ENTRYPOINT), # Endpoint override tests ( - settings.finney_entrypoint, + settings.FINNEY_ENTRYPOINT, "finney", - settings.finney_entrypoint, + settings.FINNEY_ENTRYPOINT, ), ( "entrypoint-finney.opentensor.ai", "finney", - settings.finney_entrypoint, + settings.FINNEY_ENTRYPOINT, ), ( - settings.finney_test_entrypoint, + settings.FINNEY_TEST_ENTRYPOINT, "test", - settings.finney_test_entrypoint, + settings.FINNEY_TEST_ENTRYPOINT, ), ( "test.finney.opentensor.ai", "test", - settings.finney_test_entrypoint, + settings.FINNEY_TEST_ENTRYPOINT, ), ( - settings.archive_entrypoint, + settings.ARCHIVE_ENTRYPOINT, "archive", - settings.archive_entrypoint, + settings.ARCHIVE_ENTRYPOINT, ), ( "archive.chain.opentensor.ai", "archive", - settings.archive_entrypoint, + settings.ARCHIVE_ENTRYPOINT, ), ("127.0.0.1", "local", "127.0.0.1"), ("localhost", "local", "localhost"), @@ -349,76 +297,6 @@ def test_hyperparameter_success_float(subtensor, mocker): subtensor.query_subtensor.assert_called_once_with("Difficulty", None, [1]) -# Tests Hyper parameter calls -@pytest.mark.parametrize( - "method, param_name, value, expected_result_type", - [ - ("rho", "Rho", 1, int), - ("kappa", "Kappa", 1.0, float), - ("difficulty", "Difficulty", 1, int), - ("recycle", "Burn", 1, Balance), - ("immunity_period", "ImmunityPeriod", 1, int), - ("validator_batch_size", "ValidatorBatchSize", 1, int), - ("validator_prune_len", "ValidatorPruneLen", 1, int), - ("validator_logits_divergence", "ValidatorLogitsDivergence", 1.0, float), - ("validator_sequence_length", "ValidatorSequenceLength", 1, int), - ("validator_epochs_per_reset", "ValidatorEpochsPerReset", 1, int), - ("validator_epoch_length", "ValidatorEpochLen", 1, int), - ("validator_exclude_quantile", "ValidatorExcludeQuantile", 1.0, float), - ("max_allowed_validators", "MaxAllowedValidators", 1, int), - ("min_allowed_weights", "MinAllowedWeights", 1, int), - ("max_weight_limit", "MaxWeightsLimit", 1, float), - ("adjustment_alpha", "AdjustmentAlpha", 1, float), - ("bonds_moving_avg", "BondsMovingAverage", 1, float), - ("scaling_law_power", "ScalingLawPower", 1, float), - ("synergy_scaling_law_power", "SynergyScalingLawPower", 1, float), - ("subnetwork_n", "SubnetworkN", 1, int), - ("max_n", "MaxAllowedUids", 1, int), - ("blocks_since_epoch", "BlocksSinceEpoch", 1, int), - ("tempo", "Tempo", 1, int), - ], -) -def test_hyper_parameter_success_calls( - subtensor, mocker, method, param_name, value, expected_result_type -): - """ - Tests various hyperparameter methods to ensure they correctly fetch their respective hyperparameters and return the - expected values. - """ - # Prep - subtensor._get_hyperparameter = mocker.MagicMock(return_value=value) - - spy_u16_normalized_float = mocker.spy(subtensor_module, "U16_NORMALIZED_FLOAT") - spy_u64_normalized_float = mocker.spy(subtensor_module, "U64_NORMALIZED_FLOAT") - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - subtensor_method = getattr(subtensor, method) - result = subtensor_method(netuid=7, block=707) - - # Assertions - subtensor._get_hyperparameter.assert_called_once_with( - block=707, netuid=7, param_name=param_name - ) - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, expected_result_type) - - # Special cases - if method in [ - "kappa", - "validator_logits_divergence", - "validator_exclude_quantile", - "max_weight_limit", - ]: - spy_u16_normalized_float.assert_called_once() - - if method in ["adjustment_alpha", "bonds_moving_avg"]: - spy_u64_normalized_float.assert_called_once() - - if method in ["recycle"]: - spy_balance_from_rao.assert_called_once() - - def test_blocks_since_last_update_success_calls(subtensor, mocker): """Tests the weights_rate_limit method to ensure it correctly fetches the LastUpdate hyperparameter.""" # Prep @@ -475,15 +353,15 @@ def normalize_hyperparameters( A list of tuples containing the parameter name, value, and normalized value. """ param_mappings = { - "adjustment_alpha": U64_NORMALIZED_FLOAT, - "min_difficulty": U64_NORMALIZED_FLOAT, - "max_difficulty": U64_NORMALIZED_FLOAT, - "difficulty": U64_NORMALIZED_FLOAT, - "bonds_moving_avg": U64_NORMALIZED_FLOAT, - "max_weight_limit": U16_NORMALIZED_FLOAT, - "kappa": U16_NORMALIZED_FLOAT, - "alpha_high": U16_NORMALIZED_FLOAT, - "alpha_low": U16_NORMALIZED_FLOAT, + "adjustment_alpha": u64_normalized_float, + "min_difficulty": u64_normalized_float, + "max_difficulty": u64_normalized_float, + "difficulty": u64_normalized_float, + "bonds_moving_avg": u64_normalized_float, + "max_weight_limit": u16_normalized_float, + "kappa": u16_normalized_float, + "alpha_high": u16_normalized_float, + "alpha_low": u16_normalized_float, "min_burn": Balance.from_rao, "max_burn": Balance.from_rao, } @@ -551,7 +429,7 @@ def test_hyperparameter_normalization( # Mid-value test if is_balance: - numeric_value = float(str(norm_value).lstrip(settings.tao_symbol)) + numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) expected_tao = mid_value / 1e9 assert ( numeric_value == expected_tao @@ -565,7 +443,7 @@ def test_hyperparameter_normalization( norm_value = get_normalized_value(normalized, param_name) if is_balance: - numeric_value = float(str(norm_value).lstrip(settings.tao_symbol)) + numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) expected_tao = max_value / 1e9 assert ( numeric_value == expected_tao @@ -579,7 +457,7 @@ def test_hyperparameter_normalization( norm_value = get_normalized_value(normalized, param_name) if is_balance: - numeric_value = float(str(norm_value).lstrip(settings.tao_symbol)) + numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) expected_tao = zero_value / 1e9 assert ( numeric_value == expected_tao @@ -593,1274 +471,251 @@ def test_hyperparameter_normalization( ########################### -# `get_total_stake_for_hotkey` tests -def test_get_total_stake_for_hotkey_success(subtensor, mocker): - """Tests successful retrieval of total stake for hotkey.""" - # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=mocker.MagicMock(value=1)) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.get_total_stake_for_hotkey(ss58_address=fake_ss58_address) - - # Assertions - subtensor.query_subtensor.assert_called_once_with( - "TotalHotkeyStake", None, [fake_ss58_address] - ) - spy_balance_from_rao.assert_called_once() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, Balance) - - -def test_get_total_stake_for_hotkey_not_result(subtensor, mocker): - """Tests retrieval of total stake for hotkey when no result is returned.""" - # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=None) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.get_total_stake_for_hotkey(ss58_address=fake_ss58_address) - - # Assertions - subtensor.query_subtensor.assert_called_once_with( - "TotalHotkeyStake", None, [fake_ss58_address] - ) - spy_balance_from_rao.assert_not_called() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, type(None)) - - -def test_get_total_stake_for_hotkey_not_value(subtensor, mocker): - """Tests retrieval of total stake for hotkey when no value attribute is present.""" +# get_prometheus_info tests +def test_get_prometheus_info_success(mocker, subtensor): + """Test get_prometheus_info returns correct data when information is found.""" # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=object) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.get_total_stake_for_hotkey(ss58_address=fake_ss58_address) - - # Assertions - subtensor.query_subtensor.assert_called_once_with( - "TotalHotkeyStake", None, [fake_ss58_address] + netuid = 1 + hotkey_ss58 = "test_hotkey" + block = 123 + mock_result = mocker.MagicMock( + value={ + "ip": 3232235777, # 192.168.1.1 + "ip_type": 4, + "port": 9090, + "version": "1.0", + "block": 1000, + } ) - spy_balance_from_rao.assert_not_called() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(subtensor.query_subtensor.return_value, object) - assert not hasattr(result, "value") - - -# `get_total_stake_for_coldkey` tests -def test_get_total_stake_for_coldkey_success(subtensor, mocker): - """Tests successful retrieval of total stake for coldkey.""" - # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=mocker.MagicMock(value=1)) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.get_total_stake_for_coldkey(ss58_address=fake_ss58_address) + result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - # Assertions + # Asserts + assert result is not None + assert result.ip == "192.168.1.1" + assert result.ip_type == 4 + assert result.port == 9090 + assert result.version == "1.0" + assert result.block == 1000 subtensor.query_subtensor.assert_called_once_with( - "TotalColdkeyStake", None, [fake_ss58_address] + "Prometheus", block, [netuid, hotkey_ss58] ) - spy_balance_from_rao.assert_called_once() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, Balance) -def test_get_total_stake_for_coldkey_not_result(subtensor, mocker): - """Tests retrieval of total stake for coldkey when no result is returned.""" +def test_get_prometheus_info_no_data(mocker, subtensor): + """Test get_prometheus_info returns None when no information is found.""" # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=None) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + netuid = 1 + hotkey_ss58 = "test_hotkey" + block = 123 + mocker.patch.object(subtensor, "query_subtensor", return_value=None) # Call - result = subtensor.get_total_stake_for_coldkey(ss58_address=fake_ss58_address) + result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - # Assertions + # Asserts + assert result is None subtensor.query_subtensor.assert_called_once_with( - "TotalColdkeyStake", None, [fake_ss58_address] + "Prometheus", block, [netuid, hotkey_ss58] ) - spy_balance_from_rao.assert_not_called() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, type(None)) -def test_get_total_stake_for_coldkey_not_value(subtensor, mocker): - """Tests retrieval of total stake for coldkey when no value attribute is present.""" +def test_get_prometheus_info_no_value_attribute(mocker, subtensor): + """Test get_prometheus_info returns None when result has no value attribute.""" # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=object) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + netuid = 1 + hotkey_ss58 = "test_hotkey" + block = 123 + mock_result = mocker.MagicMock() + del mock_result.value + mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.get_total_stake_for_coldkey(ss58_address=fake_ss58_address) + result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - # Assertions + # Asserts + assert result is None subtensor.query_subtensor.assert_called_once_with( - "TotalColdkeyStake", None, [fake_ss58_address] + "Prometheus", block, [netuid, hotkey_ss58] ) - spy_balance_from_rao.assert_not_called() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(subtensor.query_subtensor.return_value, object) - assert not hasattr(result, "value") -# `get_stake` tests -def test_get_stake_returns_correct_data(mocker, subtensor): - """Tests that get_stake returns correct data.""" +def test_get_prometheus_info_no_block(mocker, subtensor): + """Test get_prometheus_info with no block specified.""" # Prep + netuid = 1 hotkey_ss58 = "test_hotkey" - block = 123 - expected_query_result = [ - (mocker.MagicMock(value="coldkey1"), mocker.MagicMock(value=100)), - (mocker.MagicMock(value="coldkey2"), mocker.MagicMock(value=200)), - ] - mocker.patch.object( - subtensor, "query_map_subtensor", return_value=expected_query_result + mock_result = MagicMock( + value={ + "ip": "192.168.1.1", + "ip_type": 4, + "port": 9090, + "version": "1.0", + "block": 1000, + } ) + mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.get_stake(hotkey_ss58, block) - - # Assertion - assert result == [ - ("coldkey1", Balance.from_rao(100)), - ("coldkey2", Balance.from_rao(200)), - ] - subtensor.query_map_subtensor.assert_called_once_with("Stake", block, [hotkey_ss58]) - + result = subtensor.get_prometheus_info(netuid, hotkey_ss58) -def test_get_stake_no_block(mocker, subtensor): - """Tests get_stake with no block specified.""" - # Prep - hotkey_ss58 = "test_hotkey" - expected_query_result = [ - (MagicMock(value="coldkey1"), MagicMock(value=100)), - ] - mocker.patch.object( - subtensor, "query_map_subtensor", return_value=expected_query_result + # Asserts + assert result is not None + assert result.ip == "192.168.1.1" + assert result.ip_type == 4 + assert result.port == 9090 + assert result.version == "1.0" + assert result.block == 1000 + subtensor.query_subtensor.assert_called_once_with( + "Prometheus", None, [netuid, hotkey_ss58] ) - # Call - result = subtensor.get_stake(hotkey_ss58) - # Assertion - assert result == [("coldkey1", Balance.from_rao(100))] - subtensor.query_map_subtensor.assert_called_once_with("Stake", None, [hotkey_ss58]) +########################### +# Global Parameters tests # +########################### -def test_get_stake_empty_result(mocker, subtensor): - """Tests get_stake with an empty result.""" - # Prep - hotkey_ss58 = "test_hotkey" - block = 123 - expected_query_result = [] - mocker.patch.object( - subtensor, "query_map_subtensor", return_value=expected_query_result - ) +# `block` property test +def test_block_property(mocker, subtensor): + """Test block property returns the correct block number.""" + expected_block = 123 + mocker.patch.object(subtensor, "get_current_block", return_value=expected_block) - # Call - result = subtensor.get_stake(hotkey_ss58, block) + result = subtensor.block - # Assertion - assert result == [] - subtensor.query_map_subtensor.assert_called_once_with("Stake", block, [hotkey_ss58]) + assert result == expected_block + subtensor.get_current_block.assert_called_once() -# `does_hotkey_exist` tests -def test_does_hotkey_exist_true(mocker, subtensor): - """Test does_hotkey_exist returns True when hotkey exists and is valid.""" +# `subnet_exists` tests +def test_subnet_exists_success(mocker, subtensor): + """Test subnet_exists returns True when subnet exists.""" # Prep - hotkey_ss58 = "test_hotkey" + netuid = 1 block = 123 - mock_result = mocker.MagicMock(value="valid_coldkey") + mock_result = mocker.MagicMock(value=True) mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.does_hotkey_exist(hotkey_ss58, block) + result = subtensor.subnet_exists(netuid, block) - # Assertions + # Asserts assert result is True - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) + subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) -def test_does_hotkey_exist_false_special_value(mocker, subtensor): - """Test does_hotkey_exist returns False when result value is the special value.""" +def test_subnet_exists_no_data(mocker, subtensor): + """Test subnet_exists returns False when no subnet information is found.""" # Prep - hotkey_ss58 = "test_hotkey" + netuid = 1 block = 123 - special_value = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - mock_result = MagicMock(value=special_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) + mocker.patch.object(subtensor, "query_subtensor", return_value=None) # Call - result = subtensor.does_hotkey_exist(hotkey_ss58, block) + result = subtensor.subnet_exists(netuid, block) - # Assertions + # Asserts assert result is False - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) + subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) -def test_does_hotkey_exist_false_no_value(mocker, subtensor): - """Test does_hotkey_exist returns False when result has no value attribute.""" +def test_subnet_exists_no_value_attribute(mocker, subtensor): + """Test subnet_exists returns False when result has no value attribute.""" # Prep - hotkey_ss58 = "test_hotkey" + netuid = 1 block = 123 mock_result = mocker.MagicMock() del mock_result.value mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.does_hotkey_exist(hotkey_ss58, block) - - # Assertions - assert result is False - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) - - -def test_does_hotkey_exist_false_no_result(mocker, subtensor): - """Test does_hotkey_exist returns False when query_subtensor returns None.""" - # Prep - hotkey_ss58 = "test_hotkey" - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.does_hotkey_exist(hotkey_ss58, block) + result = subtensor.subnet_exists(netuid, block) - # Assertions + # Asserts assert result is False - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) + subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) -def test_does_hotkey_exist_no_block(mocker, subtensor): - """Test does_hotkey_exist with no block specified.""" +def test_subnet_exists_no_block(mocker, subtensor): + """Test subnet_exists with no block specified.""" # Prep - hotkey_ss58 = "test_hotkey" - mock_result = mocker.MagicMock(value="valid_coldkey") + netuid = 1 + mock_result = mocker.MagicMock(value=True) mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.does_hotkey_exist(hotkey_ss58) + result = subtensor.subnet_exists(netuid) - # Assertions + # Asserts assert result is True - subtensor.query_subtensor.assert_called_once_with("Owner", None, [hotkey_ss58]) + subtensor.query_subtensor.assert_called_once_with("NetworksAdded", None, [netuid]) -# `get_hotkey_owner` tests -def test_get_hotkey_owner_exists(mocker, subtensor): - """Test get_hotkey_owner when the hotkey exists.""" +# `get_total_subnets` tests +def test_get_total_subnets_success(mocker, subtensor): + """Test get_total_subnets returns correct data when total subnet information is found.""" # Prep - hotkey_ss58 = "test_hotkey" block = 123 - expected_owner = "coldkey_owner" - mock_result = mocker.MagicMock(value=expected_owner) + total_subnets_value = 10 + mock_result = mocker.MagicMock(value=total_subnets_value) mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - mocker.patch.object(subtensor, "does_hotkey_exist", return_value=True) # Call - result = subtensor.get_hotkey_owner(hotkey_ss58, block) + result = subtensor.get_total_subnets(block) - # Assertions - assert result == expected_owner - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) - subtensor.does_hotkey_exist.assert_called_once_with(hotkey_ss58, block) + # Asserts + assert result is not None + assert result == total_subnets_value + subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) -def test_get_hotkey_owner_does_not_exist(mocker, subtensor): - """Test get_hotkey_owner when the hotkey does not exist.""" +def test_get_total_subnets_no_data(mocker, subtensor): + """Test get_total_subnets returns None when no total subnet information is found.""" # Prep - hotkey_ss58 = "test_hotkey" block = 123 mocker.patch.object(subtensor, "query_subtensor", return_value=None) - mocker.patch.object(subtensor, "does_hotkey_exist", return_value=False) # Call - result = subtensor.get_hotkey_owner(hotkey_ss58, block) + result = subtensor.get_total_subnets(block) - # Assertions + # Asserts assert result is None - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) - subtensor.does_hotkey_exist.assert_not_called() + subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) -def test_get_hotkey_owner_no_block(mocker, subtensor): - """Test get_hotkey_owner with no block specified.""" +def test_get_total_subnets_no_value_attribute(mocker, subtensor): + """Test get_total_subnets returns None when result has no value attribute.""" # Prep - hotkey_ss58 = "test_hotkey" - expected_owner = "coldkey_owner" - mock_result = mocker.MagicMock(value=expected_owner) + block = 123 + mock_result = mocker.MagicMock() + del mock_result.value # Simulating a missing value attribute mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - mocker.patch.object(subtensor, "does_hotkey_exist", return_value=True) # Call - result = subtensor.get_hotkey_owner(hotkey_ss58) - - # Assertions - assert result == expected_owner - subtensor.query_subtensor.assert_called_once_with("Owner", None, [hotkey_ss58]) - subtensor.does_hotkey_exist.assert_called_once_with(hotkey_ss58, None) - - -def test_get_hotkey_owner_no_value_attribute(mocker, subtensor): - """Test get_hotkey_owner when the result has no value attribute.""" - # Prep - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - mocker.patch.object(subtensor, "does_hotkey_exist", return_value=True) - - # Call - result = subtensor.get_hotkey_owner(hotkey_ss58, block) - - # Assertions - assert result is None - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) - subtensor.does_hotkey_exist.assert_not_called() - - -# `get_axon_info` tests -def test_get_axon_info_success(mocker, subtensor): - """Test get_axon_info returns correct data when axon information is found.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock( - value={ - "ip": "192.168.1.1", - "ip_type": 4, - "port": 8080, - "protocol": "tcp", - "version": "1.0", - "placeholder1": "data1", - "placeholder2": "data2", - } - ) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_axon_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is not None - assert result.ip == "192.168.1.1" - assert result.ip_type == 4 - assert result.port == 8080 - assert result.protocol == "tcp" - assert result.version == "1.0" - assert result.placeholder1 == "data1" - assert result.placeholder2 == "data2" - assert result.hotkey == hotkey_ss58 - assert result.coldkey == "" - subtensor.query_subtensor.assert_called_once_with( - "Axons", block, [netuid, hotkey_ss58] - ) - - -def test_get_axon_info_no_data(mocker, subtensor): - """Test get_axon_info returns None when no axon information is found.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.get_axon_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "Axons", block, [netuid, hotkey_ss58] - ) - - -def test_get_axon_info_no_value_attribute(mocker, subtensor): - """Test get_axon_info returns None when result has no value attribute.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_axon_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "Axons", block, [netuid, hotkey_ss58] - ) - - -def test_get_axon_info_no_block(mocker, subtensor): - """Test get_axon_info with no block specified.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - mock_result = mocker.MagicMock( - value={ - "ip": 3232235777, # 192.168.1.1 - "ip_type": 4, - "port": 8080, - "protocol": "tcp", - "version": "1.0", - "placeholder1": "data1", - "placeholder2": "data2", - } - ) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_axon_info(netuid, hotkey_ss58) - - # Asserts - assert result is not None - assert result.ip == "192.168.1.1" - assert result.ip_type == 4 - assert result.port == 8080 - assert result.protocol == "tcp" - assert result.version == "1.0" - assert result.placeholder1 == "data1" - assert result.placeholder2 == "data2" - assert result.hotkey == hotkey_ss58 - assert result.coldkey == "" - subtensor.query_subtensor.assert_called_once_with( - "Axons", None, [netuid, hotkey_ss58] - ) - - -# get_prometheus_info tests -def test_get_prometheus_info_success(mocker, subtensor): - """Test get_prometheus_info returns correct data when information is found.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock( - value={ - "ip": 3232235777, # 192.168.1.1 - "ip_type": 4, - "port": 9090, - "version": "1.0", - "block": 1000, - } - ) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is not None - assert result.ip == "192.168.1.1" - assert result.ip_type == 4 - assert result.port == 9090 - assert result.version == "1.0" - assert result.block == 1000 - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", block, [netuid, hotkey_ss58] - ) - - -def test_get_prometheus_info_no_data(mocker, subtensor): - """Test get_prometheus_info returns None when no information is found.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", block, [netuid, hotkey_ss58] - ) - - -def test_get_prometheus_info_no_value_attribute(mocker, subtensor): - """Test get_prometheus_info returns None when result has no value attribute.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", block, [netuid, hotkey_ss58] - ) - - -def test_get_prometheus_info_no_block(mocker, subtensor): - """Test get_prometheus_info with no block specified.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - mock_result = MagicMock( - value={ - "ip": "192.168.1.1", - "ip_type": 4, - "port": 9090, - "version": "1.0", - "block": 1000, - } - ) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58) - - # Asserts - assert result is not None - assert result.ip == "192.168.1.1" - assert result.ip_type == 4 - assert result.port == 9090 - assert result.version == "1.0" - assert result.block == 1000 - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", None, [netuid, hotkey_ss58] - ) - - -########################### -# Global Parameters tests # -########################### - - -# `block` property test -def test_block_property(mocker, subtensor): - """Test block property returns the correct block number.""" - expected_block = 123 - mocker.patch.object(subtensor, "get_current_block", return_value=expected_block) - - result = subtensor.block - - assert result == expected_block - subtensor.get_current_block.assert_called_once() - - -# `total_issuance` tests -def test_total_issuance_success(mocker, subtensor): - """Test total_issuance returns correct data when issuance information is found.""" - # Prep - block = 123 - issuance_value = 1000 - mock_result = mocker.MagicMock(value=issuance_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_issuance(block) - - # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("TotalIssuance", block) - spy_balance_from_rao.assert_called_once_with( - subtensor.query_subtensor.return_value.value - ) - - -def test_total_issuance_no_data(mocker, subtensor): - """Test total_issuance returns None when no issuance information is found.""" - # Prep - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_issuance(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalIssuance", block) - spy_balance_from_rao.assert_not_called() - - -def test_total_issuance_no_value_attribute(mocker, subtensor): - """Test total_issuance returns None when result has no value attribute.""" - # Prep - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_issuance(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalIssuance", block) - spy_balance_from_rao.assert_not_called() - - -def test_total_issuance_no_block(mocker, subtensor): - """Test total_issuance with no block specified.""" - # Prep - issuance_value = 1000 - mock_result = mocker.MagicMock(value=issuance_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_issuance() - - # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("TotalIssuance", None) - spy_balance_from_rao.assert_called_once_with( - subtensor.query_subtensor.return_value.value - ) - - -# `total_stake` method tests -def test_total_stake_success(mocker, subtensor): - """Test total_stake returns correct data when stake information is found.""" - # Prep - block = 123 - stake_value = 5000 - mock_result = mocker.MagicMock(value=stake_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_stake(block) - - # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("TotalStake", block) - spy_balance_from_rao.assert_called_once_with( - subtensor.query_subtensor.return_value.value - ) - - -def test_total_stake_no_data(mocker, subtensor): - """Test total_stake returns None when no stake information is found.""" - # Prep - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_stake(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalStake", block) - spy_balance_from_rao.assert_not_called() - - -def test_total_stake_no_value_attribute(mocker, subtensor): - """Test total_stake returns None when result has no value attribute.""" - # Prep - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_stake(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalStake", block) - spy_balance_from_rao.assert_not_called() - - -def test_total_stake_no_block(mocker, subtensor): - """Test total_stake with no block specified.""" - # Prep - stake_value = 5000 - mock_result = mocker.MagicMock(value=stake_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_stake() - - # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("TotalStake", None) - ( - spy_balance_from_rao.assert_called_once_with( - subtensor.query_subtensor.return_value.value - ), - ) - - -# `serving_rate_limit` method tests -def test_serving_rate_limit_success(mocker, subtensor): - """Test serving_rate_limit returns correct data when rate limit information is found.""" - # Prep - netuid = 1 - block = 123 - rate_limit_value = "10" - mocker.patch.object(subtensor, "_get_hyperparameter", return_value=rate_limit_value) - - # Call - result = subtensor.serving_rate_limit(netuid, block) - - # Asserts - assert result is not None - assert result == int(rate_limit_value) - subtensor._get_hyperparameter.assert_called_once_with( - param_name="ServingRateLimit", netuid=netuid, block=block - ) - - -def test_serving_rate_limit_no_data(mocker, subtensor): - """Test serving_rate_limit returns None when no rate limit information is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "_get_hyperparameter", return_value=None) - - # Call - result = subtensor.serving_rate_limit(netuid, block) - - # Asserts - assert result is None - subtensor._get_hyperparameter.assert_called_once_with( - param_name="ServingRateLimit", netuid=netuid, block=block - ) - - -def test_serving_rate_limit_no_block(mocker, subtensor): - """Test serving_rate_limit with no block specified.""" - # Prep - netuid = 1 - rate_limit_value = "10" - mocker.patch.object(subtensor, "_get_hyperparameter", return_value=rate_limit_value) - - # Call - result = subtensor.serving_rate_limit(netuid) - - # Asserts - assert result is not None - assert result == int(rate_limit_value) - subtensor._get_hyperparameter.assert_called_once_with( - param_name="ServingRateLimit", netuid=netuid, block=None - ) - - -# `tx_rate_limit` tests -def test_tx_rate_limit_success(mocker, subtensor): - """Test tx_rate_limit returns correct data when rate limit information is found.""" - # Prep - block = 123 - rate_limit_value = 100 - mock_result = mocker.MagicMock(value=rate_limit_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.tx_rate_limit(block) - - # Asserts - assert result is not None - assert result == rate_limit_value - subtensor.query_subtensor.assert_called_once_with("TxRateLimit", block) - - -def test_tx_rate_limit_no_data(mocker, subtensor): - """Test tx_rate_limit returns None when no rate limit information is found.""" - # Prep - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.tx_rate_limit(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TxRateLimit", block) - - -def test_tx_rate_limit_no_value_attribute(mocker, subtensor): - """Test tx_rate_limit returns None when result has no value attribute.""" - # Prep - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.tx_rate_limit(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TxRateLimit", block) - - -def test_tx_rate_limit_no_block(mocker, subtensor): - """Test tx_rate_limit with no block specified.""" - # Prep - rate_limit_value = 100 - mock_result = mocker.MagicMock(value=rate_limit_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.tx_rate_limit() - - # Asserts - assert result is not None - assert result == rate_limit_value - subtensor.query_subtensor.assert_called_once_with("TxRateLimit", None) - - -############################ -# Network Parameters tests # -############################ - - -# `subnet_exists` tests -def test_subnet_exists_success(mocker, subtensor): - """Test subnet_exists returns True when subnet exists.""" - # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock(value=True) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.subnet_exists(netuid, block) - - # Asserts - assert result is True - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) - - -def test_subnet_exists_no_data(mocker, subtensor): - """Test subnet_exists returns False when no subnet information is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.subnet_exists(netuid, block) - - # Asserts - assert result is False - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) - - -def test_subnet_exists_no_value_attribute(mocker, subtensor): - """Test subnet_exists returns False when result has no value attribute.""" - # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.subnet_exists(netuid, block) - - # Asserts - assert result is False - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) - - -def test_subnet_exists_no_block(mocker, subtensor): - """Test subnet_exists with no block specified.""" - # Prep - netuid = 1 - mock_result = mocker.MagicMock(value=True) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.subnet_exists(netuid) - - # Asserts - assert result is True - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", None, [netuid]) - - -# `get_all_subnet_netuids` tests -def test_get_all_subnet_netuids_success(mocker, subtensor): - """Test get_all_subnet_netuids returns correct list when netuid information is found.""" - # Prep - block = 123 - mock_netuid1 = mocker.MagicMock(value=1) - mock_netuid2 = mocker.MagicMock(value=2) - mock_result = mocker.MagicMock() - mock_result.records = True - mock_result.__iter__.return_value = [(mock_netuid1, True), (mock_netuid2, True)] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_all_subnet_netuids(block) - - # Asserts - assert result == [1, 2] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) - - -def test_get_all_subnet_netuids_no_data(mocker, subtensor): - """Test get_all_subnet_netuids returns empty list when no netuid information is found.""" - # Prep - block = 123 - mocker.patch.object(subtensor, "query_map_subtensor", return_value=None) - - # Call - result = subtensor.get_all_subnet_netuids(block) - - # Asserts - assert result == [] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) - - -def test_get_all_subnet_netuids_no_records_attribute(mocker, subtensor): - """Test get_all_subnet_netuids returns empty list when result has no records attribute.""" - # Prep - block = 123 - mock_result = mocker.MagicMock() - del mock_result.records - mock_result.__iter__.return_value = [] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_all_subnet_netuids(block) - - # Asserts - assert result == [] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) - - -def test_get_all_subnet_netuids_no_block(mocker, subtensor): - """Test get_all_subnet_netuids with no block specified.""" - # Prep - mock_netuid1 = mocker.MagicMock(value=1) - mock_netuid2 = mocker.MagicMock(value=2) - mock_result = mocker.MagicMock() - mock_result.records = True - mock_result.__iter__.return_value = [(mock_netuid1, True), (mock_netuid2, True)] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_all_subnet_netuids() - - # Asserts - assert result == [1, 2] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", None) - - -# `get_total_subnets` tests -def test_get_total_subnets_success(mocker, subtensor): - """Test get_total_subnets returns correct data when total subnet information is found.""" - # Prep - block = 123 - total_subnets_value = 10 - mock_result = mocker.MagicMock(value=total_subnets_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_total_subnets(block) - - # Asserts - assert result is not None - assert result == total_subnets_value - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) - - -def test_get_total_subnets_no_data(mocker, subtensor): - """Test get_total_subnets returns None when no total subnet information is found.""" - # Prep - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.get_total_subnets(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) - - -def test_get_total_subnets_no_value_attribute(mocker, subtensor): - """Test get_total_subnets returns None when result has no value attribute.""" - # Prep - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value # Simulating a missing value attribute - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_total_subnets(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) - - -def test_get_total_subnets_no_block(mocker, subtensor): - """Test get_total_subnets with no block specified.""" - # Prep - total_subnets_value = 10 - mock_result = mocker.MagicMock(value=total_subnets_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_total_subnets() - - # Asserts - assert result is not None - assert result == total_subnets_value - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", None) - - -# `get_subnet_modality` tests -def test_get_subnet_modality_success(mocker, subtensor): - """Test get_subnet_modality returns correct data when modality information is found.""" - # Prep - netuid = 1 - block = 123 - modality_value = 42 - mock_result = mocker.MagicMock(value=modality_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnet_modality(netuid, block) - - # Asserts - assert result is not None - assert result == modality_value - subtensor.query_subtensor.assert_called_once_with( - "NetworkModality", block, [netuid] - ) - - -def test_get_subnet_modality_no_data(mocker, subtensor): - """Test get_subnet_modality returns None when no modality information is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.get_subnet_modality(netuid, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "NetworkModality", block, [netuid] - ) - - -def test_get_subnet_modality_no_value_attribute(mocker, subtensor): - """Test get_subnet_modality returns None when result has no value attribute.""" - # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value # Simulating a missing value attribute - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnet_modality(netuid, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "NetworkModality", block, [netuid] - ) - - -def test_get_subnet_modality_no_block_specified(mocker, subtensor): - """Test get_subnet_modality with no block specified.""" - # Prep - netuid = 1 - modality_value = 42 - mock_result = mocker.MagicMock(value=modality_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnet_modality(netuid) - - # Asserts - assert result is not None - assert result == modality_value - subtensor.query_subtensor.assert_called_once_with("NetworkModality", None, [netuid]) - - -# `get_emission_value_by_subnet` tests -def test_get_emission_value_by_subnet_success(mocker, subtensor): - """Test get_emission_value_by_subnet returns correct data when emission value is found.""" - # Prep - netuid = 1 - block = 123 - emission_value = 1000 - mock_result = mocker.MagicMock(value=emission_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.get_emission_value_by_subnet(netuid, block) - - # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("EmissionValues", block, [netuid]) - spy_balance_from_rao.assert_called_once_with(emission_value) - assert result == Balance.from_rao(emission_value) - - -def test_get_emission_value_by_subnet_no_data(mocker, subtensor): - """Test get_emission_value_by_subnet returns None when no emission value is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.get_emission_value_by_subnet(netuid, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("EmissionValues", block, [netuid]) - spy_balance_from_rao.assert_not_called() - - -def test_get_emission_value_by_subnet_no_value_attribute(mocker, subtensor): - """Test get_emission_value_by_subnet returns None when result has no value attribute.""" - # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value # Simulating a missing value attribute - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.get_emission_value_by_subnet(netuid, block) + result = subtensor.get_total_subnets(block) # Asserts assert result is None - subtensor.query_subtensor.assert_called_once_with("EmissionValues", block, [netuid]) - spy_balance_from_rao.assert_not_called() + subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) -def test_get_emission_value_by_subnet_no_block_specified(mocker, subtensor): - """Test get_emission_value_by_subnet with no block specified.""" +def test_get_total_subnets_no_block(mocker, subtensor): + """Test get_total_subnets with no block specified.""" # Prep - netuid = 1 - emission_value = 1000 - mock_result = mocker.MagicMock(value=emission_value) + total_subnets_value = 10 + mock_result = mocker.MagicMock(value=total_subnets_value) mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") # Call - result = subtensor.get_emission_value_by_subnet(netuid) + result = subtensor.get_total_subnets() # Asserts assert result is not None - subtensor.query_subtensor.assert_called_once_with("EmissionValues", None, [netuid]) - spy_balance_from_rao.assert_called_once_with(emission_value) - assert result == Balance.from_rao(emission_value) - - -# `get_subnet_connection_requirements` tests -def test_get_subnet_connection_requirements_success(mocker, subtensor): - """Test get_subnet_connection_requirements returns correct data when requirements are found.""" - # Prep - netuid = 1 - block = 123 - mock_tuple1 = (mocker.MagicMock(value="requirement1"), mocker.MagicMock(value=10)) - mock_tuple2 = (mocker.MagicMock(value="requirement2"), mocker.MagicMock(value=20)) - mock_result = mocker.MagicMock() - mock_result.records = [mock_tuple1, mock_tuple2] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnet_connection_requirements(netuid, block) - - # Asserts - assert result == {"requirement1": 10, "requirement2": 20} - subtensor.query_map_subtensor.assert_called_once_with( - "NetworkConnect", block, [netuid] - ) - - -def test_get_subnet_connection_requirements_no_data(mocker, subtensor): - """Test get_subnet_connection_requirements returns empty dict when no data is found.""" - # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - mock_result.records = [] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnet_connection_requirements(netuid, block) - - # Asserts - assert result == {} - subtensor.query_map_subtensor.assert_called_once_with( - "NetworkConnect", block, [netuid] - ) - - -def test_get_subnet_connection_requirements_no_records_attribute(mocker, subtensor): - """Test get_subnet_connection_requirements returns empty dict when result has no records attribute.""" - # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - del mock_result.records # Simulating a missing records attribute - - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnet_connection_requirements(netuid, block) - - # Asserts - assert result == {} - subtensor.query_map_subtensor.assert_called_once_with( - "NetworkConnect", block, [netuid] - ) - - -def test_get_subnet_connection_requirements_no_block_specified(mocker, subtensor): - """Test get_subnet_connection_requirements with no block specified.""" - # Prep - netuid = 1 - mock_tuple1 = (mocker.MagicMock(value="requirement1"), mocker.MagicMock(value=10)) - mock_tuple2 = (mocker.MagicMock(value="requirement2"), mocker.MagicMock(value=20)) - mock_result = mocker.MagicMock() - mock_result.records = [mock_tuple1, mock_tuple2] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnet_connection_requirements(netuid) - - # Asserts - assert result == {"requirement1": 10, "requirement2": 20} - subtensor.query_map_subtensor.assert_called_once_with( - "NetworkConnect", None, [netuid] - ) + assert result == total_subnets_value + subtensor.query_subtensor.assert_called_once_with("TotalNetworks", None) # `get_subnets` tests @@ -1931,166 +786,6 @@ def test_get_subnets_no_block_specified(mocker, subtensor): subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", None) -# `get_all_subnets_info` tests -def test_get_all_subnets_info_success(mocker, subtensor): - """Test get_all_subnets_info returns correct data when subnet information is found.""" - # Prep - block = 123 - subnet_data = [1, 2, 3] # Mocked response data - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" - ) - mock_response = {"result": subnet_data} - mocker.patch.object(subtensor.substrate, "rpc_request", return_value=mock_response) - mocker.patch.object( - subtensor_module.SubnetInfo, - "list_from_vec_u8", - return_value="list_from_vec_u80", - ) - - # Call - result = subtensor.get_all_subnets_info(block) - - # Asserts - subtensor.substrate.get_block_hash.assert_called_once_with(block) - subtensor.substrate.rpc_request.assert_called_once_with( - method="subnetInfo_getSubnetsInfo", params=["mock_block_hash"] - ) - subtensor_module.SubnetInfo.list_from_vec_u8.assert_called_once_with(subnet_data) - - -@pytest.mark.parametrize("result_", [[], None]) -def test_get_all_subnets_info_no_data(mocker, subtensor, result_): - """Test get_all_subnets_info returns empty list when no subnet information is found.""" - # Prep - block = 123 - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" - ) - mock_response = {"result": result_} - mocker.patch.object(subtensor.substrate, "rpc_request", return_value=mock_response) - mocker.patch.object(subtensor_module.SubnetInfo, "list_from_vec_u8") - - # Call - result = subtensor.get_all_subnets_info(block) - - # Asserts - assert result == [] - subtensor.substrate.get_block_hash.assert_called_once_with(block) - subtensor.substrate.rpc_request.assert_called_once_with( - method="subnetInfo_getSubnetsInfo", params=["mock_block_hash"] - ) - subtensor_module.SubnetInfo.list_from_vec_u8.assert_not_called() - - -def test_get_all_subnets_info_retry(mocker, subtensor): - """Test get_all_subnets_info retries on failure.""" - # Prep - block = 123 - subnet_data = [1, 2, 3] - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" - ) - mock_response = {"result": subnet_data} - mock_rpc_request = mocker.patch.object( - subtensor.substrate, - "rpc_request", - side_effect=[Exception, Exception, mock_response], - ) - mocker.patch.object( - subtensor_module.SubnetInfo, "list_from_vec_u8", return_value=["some_data"] - ) - - # Call - result = subtensor.get_all_subnets_info(block) - - # Asserts - subtensor.substrate.get_block_hash.assert_called_with(block) - assert mock_rpc_request.call_count == 3 - subtensor_module.SubnetInfo.list_from_vec_u8.assert_called_once_with(subnet_data) - assert result == ["some_data"] - - -# `get_subnet_info` tests -def test_get_subnet_info_success(mocker, subtensor): - """Test get_subnet_info returns correct data when subnet information is found.""" - # Prep - netuid = 1 - block = 123 - subnet_data = [1, 2, 3] - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" - ) - mock_response = {"result": subnet_data} - mocker.patch.object(subtensor.substrate, "rpc_request", return_value=mock_response) - mocker.patch.object( - subtensor_module.SubnetInfo, "from_vec_u8", return_value=["from_vec_u8"] - ) - - # Call - result = subtensor.get_subnet_info(netuid, block) - - # Asserts - subtensor.substrate.get_block_hash.assert_called_once_with(block) - subtensor.substrate.rpc_request.assert_called_once_with( - method="subnetInfo_getSubnetInfo", params=[netuid, "mock_block_hash"] - ) - subtensor_module.SubnetInfo.from_vec_u8.assert_called_once_with(subnet_data) - - -@pytest.mark.parametrize("result_", [None, {}]) -def test_get_subnet_info_no_data(mocker, subtensor, result_): - """Test get_subnet_info returns None when no subnet information is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" - ) - mock_response = {"result": result_} - mocker.patch.object(subtensor.substrate, "rpc_request", return_value=mock_response) - mocker.patch.object(subtensor_module.SubnetInfo, "from_vec_u8") - - # Call - result = subtensor.get_subnet_info(netuid, block) - - # Asserts - assert result is None - subtensor.substrate.get_block_hash.assert_called_once_with(block) - subtensor.substrate.rpc_request.assert_called_once_with( - method="subnetInfo_getSubnetInfo", params=[netuid, "mock_block_hash"] - ) - subtensor_module.SubnetInfo.from_vec_u8.assert_not_called() - - -def test_get_subnet_info_retry(mocker, subtensor): - """Test get_subnet_info retries on failure.""" - # Prep - netuid = 1 - block = 123 - subnet_data = [1, 2, 3] - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" - ) - mock_response = {"result": subnet_data} - mock_rpc_request = mocker.patch.object( - subtensor.substrate, - "rpc_request", - side_effect=[Exception, Exception, mock_response], - ) - mocker.patch.object( - subtensor_module.SubnetInfo, "from_vec_u8", return_value=["from_vec_u8"] - ) - - # Call - result = subtensor.get_subnet_info(netuid, block) - - # Asserts - subtensor.substrate.get_block_hash.assert_called_with(block) - assert mock_rpc_request.call_count == 3 - subtensor_module.SubnetInfo.from_vec_u8.assert_called_once_with(subnet_data) - - # `get_subnet_hyperparameters` tests def test_get_subnet_hyperparameters_success(mocker, subtensor): """Test get_subnet_hyperparameters returns correct data when hyperparameters are found.""" @@ -2166,169 +861,3 @@ def test_get_subnet_hyperparameters_hex_without_prefix(mocker, subtensor): subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( bytes_result ) - - -# `get_subnet_owner` tests -def test_get_subnet_owner_success(mocker, subtensor): - """Test get_subnet_owner returns correct data when owner information is found.""" - # Prep - netuid = 1 - block = 123 - owner_address = "5F3sa2TJAWMqDhXG6jhV4N8ko9rXPM6twz9mG9m3rrgq3xiJ" - mock_result = mocker.MagicMock(value=owner_address) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnet_owner(netuid, block) - - # Asserts - subtensor.query_subtensor.assert_called_once_with("SubnetOwner", block, [netuid]) - assert result == owner_address - - -def test_get_subnet_owner_no_data(mocker, subtensor): - """Test get_subnet_owner returns None when no owner information is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.get_subnet_owner(netuid, block) - - # Asserts - subtensor.query_subtensor.assert_called_once_with("SubnetOwner", block, [netuid]) - assert result is None - - -def test_get_subnet_owner_no_value_attribute(mocker, subtensor): - """Test get_subnet_owner returns None when result has no value attribute.""" - # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value # Simulating a missing value attribute - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_subnet_owner(netuid, block) - - # Asserts - subtensor.query_subtensor.assert_called_once_with("SubnetOwner", block, [netuid]) - assert result is None - - -#################### -# Nomination tests # -#################### - - -# `is_hotkey_delegate` tests -def test_is_hotkey_delegate_success(mocker, subtensor): - """Test is_hotkey_delegate returns True when hotkey is a delegate.""" - # Prep - hotkey_ss58 = "hotkey_ss58" - block = 123 - mock_delegates = [ - mocker.MagicMock(hotkey_ss58=hotkey_ss58), - mocker.MagicMock(hotkey_ss58="hotkey_ss583"), - ] - mocker.patch.object(subtensor, "get_delegates", return_value=mock_delegates) - - # Call - result = subtensor.is_hotkey_delegate(hotkey_ss58, block) - - # Asserts - subtensor.get_delegates.assert_called_once_with(block=block) - assert result is True - - -def test_is_hotkey_delegate_not_found(mocker, subtensor): - """Test is_hotkey_delegate returns False when hotkey is not a delegate.""" - # Prep - hotkey_ss58 = "hotkey_ss58" - block = 123 - mock_delegates = [mocker.MagicMock(hotkey_ss58="hotkey_ss583")] - mocker.patch.object(subtensor, "get_delegates", return_value=mock_delegates) - - # Call - result = subtensor.is_hotkey_delegate(hotkey_ss58, block) - - # Asserts - subtensor.get_delegates.assert_called_once_with(block=block) - assert result is False - - -# `get_delegate_take` tests -def test_get_delegate_take_success(mocker, subtensor): - """Test get_delegate_take returns correct data when delegate take is found.""" - # Prep - hotkey_ss58 = "hotkey_ss58" - block = 123 - delegate_take_value = 32768 - mock_result = mocker.MagicMock(value=delegate_take_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_u16_normalized_float = mocker.spy(subtensor_module, "U16_NORMALIZED_FLOAT") - - # Call - subtensor.get_delegate_take(hotkey_ss58, block) - - # Asserts - subtensor.query_subtensor.assert_called_once_with("Delegates", block, [hotkey_ss58]) - spy_u16_normalized_float.assert_called_once_with(delegate_take_value) - - -def test_get_delegate_take_no_data(mocker, subtensor): - """Test get_delegate_take returns None when no delegate take is found.""" - # Prep - hotkey_ss58 = "hotkey_ss58" - block = 123 - delegate_take_value = 32768 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - spy_u16_normalized_float = mocker.spy(subtensor_module, "U16_NORMALIZED_FLOAT") - - # Call - result = subtensor.get_delegate_take(hotkey_ss58, block) - - # Asserts - subtensor.query_subtensor.assert_called_once_with("Delegates", block, [hotkey_ss58]) - spy_u16_normalized_float.assert_not_called() - assert result is None - - -def test_get_remaining_arbitration_period(subtensor, mocker): - """Tests successful retrieval of total stake for hotkey.""" - # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=mocker.MagicMock(value=0)) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - - # Call - result = subtensor.get_remaining_arbitration_period(coldkey_ss58=fake_ss58_address) - - # Assertions - subtensor.query_subtensor.assert_called_once_with( - name="ColdkeyArbitrationBlock", block=None, params=[fake_ss58_address] - ) - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert result == 0 - - -def test_get_remaining_arbitration_period_happy(subtensor, mocker): - """Tests successful retrieval of total stake for hotkey.""" - # Prep - subtensor.query_subtensor = mocker.MagicMock( - return_value=mocker.MagicMock(value=2000) - ) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - - # Call - result = subtensor.get_remaining_arbitration_period( - coldkey_ss58=fake_ss58_address, block=200 - ) - - # Assertions - subtensor.query_subtensor.assert_called_once_with( - name="ColdkeyArbitrationBlock", block=200, params=[fake_ss58_address] - ) - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert result == 1800 # 2000 - 200 From 70f154565e05489fca62d1414069b5fd27e85d0c Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 6 Aug 2024 12:33:05 -0700 Subject: [PATCH 066/260] Move backwards compatibility related content to `bittensor/utils/backwards_compatibility` subpackage --- bittensor/api/__init__.py | 0 bittensor/core/subtensor.py | 14 +++++-- .../utils/backwards_compatibility/__init__.py | 14 ++++--- .../extrinsics/__init__.py | 0 .../extrinsics/prometheus.py | 0 .../extrinsics/serving.py | 0 .../extrinsics/set_weights.py | 0 .../extrinsics/transfer.py | 0 .../test_backwards_compatibility.py | 38 +++++++++++++++++++ .../unit_tests/extrinsics/test_prometheus.py | 4 +- tests/unit_tests/extrinsics/test_serving.py | 17 +++++++-- .../unit_tests/extrinsics/test_set_weights.py | 4 +- 12 files changed, 75 insertions(+), 16 deletions(-) delete mode 100644 bittensor/api/__init__.py rename bittensor/{api => utils/backwards_compatibility}/extrinsics/__init__.py (100%) rename bittensor/{api => utils/backwards_compatibility}/extrinsics/prometheus.py (100%) rename bittensor/{api => utils/backwards_compatibility}/extrinsics/serving.py (100%) rename bittensor/{api => utils/backwards_compatibility}/extrinsics/set_weights.py (100%) rename bittensor/{api => utils/backwards_compatibility}/extrinsics/transfer.py (100%) create mode 100644 tests/unit_tests/extrinsics/test_backwards_compatibility.py diff --git a/bittensor/api/__init__.py b/bittensor/api/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index ce69640cb..e837dca1a 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -37,15 +37,21 @@ from scalecodec.types import ScaleType from substrateinterface.base import QueryMapResult, SubstrateInterface -from bittensor.api.extrinsics.prometheus import prometheus_extrinsic -from bittensor.api.extrinsics.serving import ( +from bittensor.utils.backwards_compatibility.extrinsics.prometheus import ( + prometheus_extrinsic, +) +from bittensor.utils.backwards_compatibility.extrinsics.serving import ( serve_extrinsic, serve_axon_extrinsic, publish_metadata, get_metadata, ) -from bittensor.api.extrinsics.set_weights import set_weights_extrinsic -from bittensor.api.extrinsics.transfer import transfer_extrinsic +from bittensor.utils.backwards_compatibility.extrinsics.set_weights import ( + set_weights_extrinsic, +) +from bittensor.utils.backwards_compatibility.extrinsics.transfer import ( + transfer_extrinsic, +) from bittensor.core import settings from bittensor.core.axon import Axon from bittensor.core.chain_data import ( diff --git a/bittensor/utils/backwards_compatibility/__init__.py b/bittensor/utils/backwards_compatibility/__init__.py index 872ef88a2..321c21ff8 100644 --- a/bittensor/utils/backwards_compatibility/__init__.py +++ b/bittensor/utils/backwards_compatibility/__init__.py @@ -145,10 +145,12 @@ __tao_symbol__ = settings.TAO_SYMBOL __rao_symbol__ = settings.RAO_SYMBOL -# Makes the `bittensor.api.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. -extrinsics = importlib.import_module("bittensor.api.extrinsics") -sys.modules["bittensor.extrinsics"] = extrinsics - # Makes the `bittensor.utils.mock` subpackage available as `bittensor.mock` for backwards compatibility. -extrinsics = importlib.import_module("bittensor.utils.mock") -sys.modules["bittensor.mock"] = extrinsics +mock_subpackage = importlib.import_module("bittensor.utils.mock") +sys.modules["bittensor.mock"] = mock_subpackage + +# Makes the `bittensor.utils.backwards_compatibility.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. +extrinsics_subpackage = importlib.import_module( + "bittensor.utils.backwards_compatibility.extrinsics" +) +sys.modules["bittensor.extrinsics"] = extrinsics_subpackage diff --git a/bittensor/api/extrinsics/__init__.py b/bittensor/utils/backwards_compatibility/extrinsics/__init__.py similarity index 100% rename from bittensor/api/extrinsics/__init__.py rename to bittensor/utils/backwards_compatibility/extrinsics/__init__.py diff --git a/bittensor/api/extrinsics/prometheus.py b/bittensor/utils/backwards_compatibility/extrinsics/prometheus.py similarity index 100% rename from bittensor/api/extrinsics/prometheus.py rename to bittensor/utils/backwards_compatibility/extrinsics/prometheus.py diff --git a/bittensor/api/extrinsics/serving.py b/bittensor/utils/backwards_compatibility/extrinsics/serving.py similarity index 100% rename from bittensor/api/extrinsics/serving.py rename to bittensor/utils/backwards_compatibility/extrinsics/serving.py diff --git a/bittensor/api/extrinsics/set_weights.py b/bittensor/utils/backwards_compatibility/extrinsics/set_weights.py similarity index 100% rename from bittensor/api/extrinsics/set_weights.py rename to bittensor/utils/backwards_compatibility/extrinsics/set_weights.py diff --git a/bittensor/api/extrinsics/transfer.py b/bittensor/utils/backwards_compatibility/extrinsics/transfer.py similarity index 100% rename from bittensor/api/extrinsics/transfer.py rename to bittensor/utils/backwards_compatibility/extrinsics/transfer.py diff --git a/tests/unit_tests/extrinsics/test_backwards_compatibility.py b/tests/unit_tests/extrinsics/test_backwards_compatibility.py new file mode 100644 index 000000000..7a73767d9 --- /dev/null +++ b/tests/unit_tests/extrinsics/test_backwards_compatibility.py @@ -0,0 +1,38 @@ +# 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 sys + + +def test_mock_import(): + """ + Tests that `bittensor.mock` can be imported and is the same as `bittensor.utils.mock`. + """ + import bittensor.mock as redirected_mock + import bittensor.utils.mock as real_mock + + assert "bittensor.mock" in sys.modules + assert redirected_mock is real_mock + + +def test_extrinsics_import(): + """Tests that `bittensor.extrinsics` can be imported and is the same as `bittensor.utils.backwards_compatibility.extrinsics`.""" + import bittensor.extrinsics as redirected_extrinsics + import bittensor.utils.backwards_compatibility.extrinsics as real_extrinsics + + assert "bittensor.extrinsics" in sys.modules + assert redirected_extrinsics is real_extrinsics diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index 516020c8c..5f93ab92a 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -20,7 +20,9 @@ import pytest from bittensor_wallet import Wallet -from bittensor.api.extrinsics.prometheus import prometheus_extrinsic +from bittensor.utils.backwards_compatibility.extrinsics.prometheus import ( + prometheus_extrinsic, +) from bittensor.core.subtensor import Subtensor from bittensor.core.settings import version_as_int diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 7dd065844..af6a056fe 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -21,7 +21,7 @@ from bittensor_wallet import Wallet from bittensor.core.axon import Axon -from bittensor.api.extrinsics.serving import ( +from bittensor.utils.backwards_compatibility.extrinsics.serving import ( serve_extrinsic, publish_metadata, serve_axon_extrinsic, @@ -119,7 +119,10 @@ def test_serve_extrinsic_happy_path( ): # Arrange mock_subtensor.do_serve_axon.return_value = (True, "") - with patch("bittensor.api.extrinsics.serving.Confirm.ask", return_value=True): + with patch( + "bittensor.utils.backwards_compatibility.extrinsics.serving.Confirm.ask", + return_value=True, + ): # Act result = serve_extrinsic( mock_subtensor, @@ -176,7 +179,10 @@ def test_serve_extrinsic_edge_cases( ): # Arrange mock_subtensor.do_serve_axon.return_value = (True, "") - with patch("bittensor.api.extrinsics.serving.Confirm.ask", return_value=True): + with patch( + "bittensor.utils.backwards_compatibility.extrinsics.serving.Confirm.ask", + return_value=True, + ): # Act result = serve_extrinsic( mock_subtensor, @@ -233,7 +239,10 @@ def test_serve_extrinsic_error_cases( ): # Arrange mock_subtensor.do_serve_axon.return_value = (False, "Error serving axon") - with patch("bittensor.api.extrinsics.serving.Confirm.ask", return_value=True): + with patch( + "bittensor.utils.backwards_compatibility.extrinsics.serving.Confirm.ask", + return_value=True, + ): # Act result = serve_extrinsic( mock_subtensor, diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 6c12db75d..5c6817684 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -4,7 +4,9 @@ from bittensor.core.subtensor import Subtensor from bittensor_wallet import Wallet -from bittensor.api.extrinsics.set_weights import set_weights_extrinsic +from bittensor.utils.backwards_compatibility.extrinsics.set_weights import ( + set_weights_extrinsic, +) @pytest.fixture From db7c0008f733fc82ba61113e94f1b5a882dcd1a2 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Aug 2024 10:28:02 -0700 Subject: [PATCH 067/260] btlogging/loggingmachine.py improvement from https://github.com/opentensor/bittensor/pull/2155 --- bittensor/utils/btlogging/loggingmachine.py | 63 +++++++++++++-------- tests/unit_tests/test_logging.py | 41 +++++++++++++- 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index 7c7ea3376..e40c4bd1c 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -361,45 +361,58 @@ def __trace_on__(self) -> bool: """ return self.current_state_value == "Trace" - def trace(self, msg="", prefix="", suffix="", *args, **kwargs): + @staticmethod + def _concat_msg(*args): + return " - ".join(str(el) for el in args if el != "") + + def trace(self, msg="", *args, prefix="", suffix="", **kwargs): """Wraps trace message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" - self._logger.trace(msg, *args, **kwargs) + msg = self._concat_msg(prefix, msg, suffix) + self._logger.trace(msg, *args, **kwargs, stacklevel=2) - def debug(self, msg="", prefix="", suffix="", *args, **kwargs): + def debug(self, msg="", *args, prefix="", suffix="", **kwargs): """Wraps debug message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" - self._logger.debug(msg, *args, **kwargs) + msg = self._concat_msg(prefix, msg, suffix) + self._logger.debug(msg, *args, **kwargs, stacklevel=2) - def info(self, msg="", prefix="", suffix="", *args, **kwargs): + def info(self, msg="", *args, prefix="", suffix="", **kwargs): """Wraps info message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" - self._logger.info(msg, *args, **kwargs) + msg = self._concat_msg(prefix, msg, suffix) + self._logger.info(msg, *args, **kwargs, stacklevel=2) - def success(self, msg="", prefix="", suffix="", *args, **kwargs): + def success(self, msg="", *args, prefix="", suffix="", **kwargs): """Wraps success message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" - self._logger.success(msg, *args, **kwargs) + msg = self._concat_msg(prefix, msg, suffix) + self._logger.success(msg, *args, **kwargs, stacklevel=2) - def warning(self, msg="", prefix="", suffix="", *args, **kwargs): + def warning(self, msg="", *args, prefix="", suffix="", **kwargs): """Wraps warning message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" - self._logger.warning(msg, *args, **kwargs) + msg = self._concat_msg(prefix, msg, suffix) + self._logger.warning(msg, *args, **kwargs, stacklevel=2) - def error(self, msg="", prefix="", suffix="", *args, **kwargs): + def error(self, msg="", *args, prefix="", suffix="", **kwargs): """Wraps error message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" - self._logger.error(msg, *args, **kwargs) + msg = self._concat_msg(prefix, msg, suffix) + self._logger.error(msg, *args, **kwargs, stacklevel=2) - def critical(self, msg="", prefix="", suffix="", *args, **kwargs): + def critical(self, msg="", *args, prefix="", suffix="", **kwargs): """Wraps critical message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" - self._logger.critical(msg, *args, **kwargs) + msg = self._concat_msg(prefix, msg, suffix) + self._logger.critical(msg, *args, **kwargs, stacklevel=2) - def exception(self, msg="", prefix="", suffix="", *args, **kwargs): + def exception(self, msg="", *args, prefix="", suffix="", **kwargs): """Wraps exception message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" - self._logger.exception(msg, *args, **kwargs) + msg = self._concat_msg(prefix, msg, suffix) + stacklevel = 2 + if ( + sys.implementation.name == "cpython" + and sys.version_info.major == 3 + and sys.version_info.minor < 11 + ): + # Note that, on CPython < 3.11, exception() calls through to + # error() without adjusting stacklevel, so we have to increment it. + stacklevel += 1 + self._logger.exception(msg, *args, **kwargs, stacklevel=stacklevel) def on(self): """Enable default state.""" @@ -508,4 +521,4 @@ def __call__( cfg = LoggingConfig( debug=debug, trace=trace, record_log=record_log, logging_dir=logging_dir ) - self.set_config(cfg) + self.set_config(cfg) \ No newline at end of file diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 6c39d7b5f..67520e218 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -1,7 +1,11 @@ -import pytest -import multiprocessing import logging as stdlogging +import multiprocessing +import os +import re from unittest.mock import MagicMock, patch + +import pytest + from bittensor.utils.btlogging import LoggingMachine from bittensor.utils.btlogging.defines import ( DEFAULT_LOG_FILE_NAME, @@ -173,3 +177,36 @@ def test_all_log_levels_output(logging_machine, caplog): assert "Test warning" in caplog.text assert "Test error" in caplog.text assert "Test critical" in caplog.text + + +def test_log_sanity(logging_machine, caplog): + """ + Test that logging is sane: + - prefix and suffix work + - format strings work + - reported filename is correct + Note that this is tested against caplog, which is not formatted the same as + stdout. + """ + basemsg = "logmsg #%d, cookie: %s" + cookie = "0ef852c74c777f8d8cc09d511323ce76" + nfixtests = [ + {}, + {"prefix": "pref"}, + {"suffix": "suff"}, + {"prefix": "pref", "suffix": "suff"}, + ] + cookiejar = {} + for i, nfix in enumerate(nfixtests): + prefix = nfix.get("prefix", "") + suffix = nfix.get("suffix", "") + use_cookie = f"{cookie} #{i}#" + logging_machine.info(basemsg, i, use_cookie, prefix=prefix, suffix=suffix) + # Check to see if all elements are present, regardless of downstream formatting. + expect = f"INFO.*{os.path.basename(__file__)}.* " + if prefix != "": + expect += prefix + " - " + expect += basemsg % (i, use_cookie) + if suffix != "": + expect += " - " + suffix + assert re.search(expect, caplog.text) \ No newline at end of file From f80c2b95a9407479741c5a25a7c4b8dd321f60a5 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Aug 2024 11:08:49 -0700 Subject: [PATCH 068/260] Fixes after `Merge streaming fix to staging` from https://github.com/opentensor/bittensor/pull/2183 --- bittensor/core/axon.py | 10 +++---- bittensor/core/stream.py | 5 +--- bittensor/extrinsics/delegation.py | 0 bittensor/utils/btlogging/loggingmachine.py | 2 +- .../e2e_tests/subcommands/subnet/test_list.py | 29 ------------------- tests/unit_tests/test_axon.py | 13 +++++---- 6 files changed, 14 insertions(+), 45 deletions(-) delete mode 100644 bittensor/extrinsics/delegation.py delete mode 100644 tests/e2e_tests/subcommands/subnet/test_list.py diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 6d30a32d2..e5ae679d1 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -23,7 +23,6 @@ import copy import inspect import json -import os import threading import time import traceback @@ -57,6 +56,7 @@ UnknownSynapseError, ) from bittensor.core.settings import DEFAULTS, version_as_int +from bittensor.core.stream import StreamingSynapse from bittensor.core.synapse import Synapse, TerminalInfo from bittensor.core.threadpool import PriorityThreadPoolExecutor from bittensor.utils import networking @@ -377,7 +377,7 @@ def __init__( self.app.add_middleware(self.middleware_cls, axon=self) # Attach default forward. - def ping(r: "Synapse") -> Synapse: + def ping(r: Synapse) -> Synapse: return r self.attach( @@ -482,7 +482,7 @@ async def endpoint(*args, **kwargs): response = forward_fn(*args, **kwargs) if isinstance(response, Awaitable): response = await response - if isinstance(response, bittensor.Synapse): + if isinstance(response, Synapse): return await self.middleware_cls.synapse_to_response( synapse=response, start_time=start_time ) @@ -507,11 +507,11 @@ async def endpoint(*args, **kwargs): return_annotation = forward_sig.return_annotation if isinstance(return_annotation, type) and issubclass( - return_annotation, bittensor.Synapse + return_annotation, Synapse ): if issubclass( return_annotation, - bittensor.StreamingSynapse, + StreamingSynapse, ): warnings.warn( "The forward_fn return annotation is a subclass of bittensor.StreamingSynapse. " diff --git a/bittensor/core/stream.py b/bittensor/core/stream.py index 3e6923f77..38d903699 100644 --- a/bittensor/core/stream.py +++ b/bittensor/core/stream.py @@ -39,9 +39,7 @@ class BTStreamingResponseModel(BaseModel): passing the token streamer to the BTStreamingResponse class. Attributes: - token_streamer: Callable[[Send], Awaitable[None]] - The token streamer callable, which takes a send function (provided by the ASGI server) and returns an awaitable. - It is responsible for generating the content of the streaming response. + token_streamer: Callable[[Send], Awaitable[None]] The token streamer callable, which takes a send function (provided by the ASGI server) and returns an awaitable. It is responsible for generating the content of the streaming response. """ token_streamer: Callable[[Send], Awaitable[None]] @@ -146,7 +144,6 @@ def extract_response_json(self, response: ClientResponse) -> dict: Args: response: The response object from which to extract JSON data. """ - ... def create_streaming_response( self, token_streamer: Callable[[Send], Awaitable[None]] diff --git a/bittensor/extrinsics/delegation.py b/bittensor/extrinsics/delegation.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index e40c4bd1c..adbb4e929 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -521,4 +521,4 @@ def __call__( cfg = LoggingConfig( debug=debug, trace=trace, record_log=record_log, logging_dir=logging_dir ) - self.set_config(cfg) \ No newline at end of file + self.set_config(cfg) diff --git a/tests/e2e_tests/subcommands/subnet/test_list.py b/tests/e2e_tests/subcommands/subnet/test_list.py deleted file mode 100644 index 74b79a2df..000000000 --- a/tests/e2e_tests/subcommands/subnet/test_list.py +++ /dev/null @@ -1,29 +0,0 @@ -import bittensor -from bittensor.commands import RegisterSubnetworkCommand -from tests.e2e_tests.utils import setup_wallet - -""" -Test the list command before and after registering subnets. - -Verify that: -* list of subnets gets displayed -------------------------- -* Register a subnets -* Ensure is visible in list cmd -""" - - -def test_list_command(local_chain, capsys): - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - - netuid = 0 - - assert local_chain.query("SubtensorModule", "NetworksAdded", [netuid]).serialize() - - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - netuid - 1 - - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [netuid]).serialize() diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index 3d77c1c27..f8bb912d6 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -30,12 +30,12 @@ from fastapi.testclient import TestClient from starlette.requests import Request -import bittensor -from bittensor.core.synapse import Synapse, RunException, StreamingSynapse from bittensor.core.axon import AxonMiddleware, Axon -from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds -from bittensor.constants import ALLOWED_DELTA, NANOSECONDS_IN_SECOND - +from bittensor.core.errors import RunException +from bittensor.core.settings import version_as_int +from bittensor.core.stream import StreamingSynapse +from bittensor.core.synapse import Synapse +from bittensor.core.threadpool import PriorityThreadPoolExecutor from bittensor.utils.axon_utils import ( allowed_nonce_window_ns, calculate_diff_seconds, @@ -43,6 +43,7 @@ NANOSECONDS_IN_SECOND, ) + def test_attach_initial(): # Create a mock AxonServer instance server = Axon() @@ -736,7 +737,7 @@ def test_nonce_within_allowed_window(nonce_offset_seconds, expected_result): [ None, fastapi.Response, - bittensor.StreamingSynapse, + StreamingSynapse, ], ) async def test_streaming_synapse( From 82f8dcab4a259545be059bb3331811d00ebe4cc2 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Aug 2024 13:22:08 -0700 Subject: [PATCH 069/260] Review fix. --- bittensor/core/dendrite.py | 6 ++++-- bittensor/utils/backwards_compatibility/subnets.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 47b5fe945..3daeca3f2 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -763,7 +763,8 @@ async def __aenter__(self): Dendrite: The current instance of the Dendrite class. Usage:: - async with Dendrite() as dendrite: await dendrite.some_async_method() + async with Dendrite() as dendrite: + await dendrite.some_async_method() """ return self @@ -780,7 +781,8 @@ async def __aexit__(self, exc_type, exc_value, traceback): Usage:: - async with bt.dendrite( wallet ) as dendrite: await dendrite.some_async_method() + async with bt.dendrite( wallet ) as dendrite: + await dendrite.some_async_method() Note: This automatically closes the session by calling :func:`__aexit__` after the context closes. diff --git a/bittensor/utils/backwards_compatibility/subnets.py b/bittensor/utils/backwards_compatibility/subnets.py index 8622379fa..0046df8f2 100644 --- a/bittensor/utils/backwards_compatibility/subnets.py +++ b/bittensor/utils/backwards_compatibility/subnets.py @@ -22,6 +22,7 @@ from bittensor.core.dendrite import Dendrite from bittensor.utils.btlogging import logging +# For annotation purposes if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.synapse import Synapse From 66f02452101e4a79e0a1596109bb069b29704f39 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Aug 2024 14:28:56 -0700 Subject: [PATCH 070/260] Remove black from dev requirements. --- requirements/dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index a9e1a1bc4..14d616b48 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,4 +1,3 @@ -black==24.3.0 pytest==7.2.0 pytest-asyncio==0.23.7 pytest-mock==3.12.0 From 78da7fc066df63400cb31c5d18a99f9d481547f9 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Aug 2024 14:51:33 -0700 Subject: [PATCH 071/260] black back --- requirements/dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/dev.txt b/requirements/dev.txt index 14d616b48..a9e1a1bc4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,3 +1,4 @@ +black==24.3.0 pytest==7.2.0 pytest-asyncio==0.23.7 pytest-mock==3.12.0 From 0b0c9abe183e2d30f593ed31daa9ff138157bdbd Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Aug 2024 15:02:49 -0700 Subject: [PATCH 072/260] Remove space from prod.txt. Remove black from dev.txt --- requirements/dev.txt | 1 - requirements/prod.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index a9e1a1bc4..14d616b48 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,4 +1,3 @@ -black==24.3.0 pytest==7.2.0 pytest-asyncio==0.23.7 pytest-mock==3.12.0 diff --git a/requirements/prod.txt b/requirements/prod.txt index b9cf7aecc..4ef4a6f5e 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -33,5 +33,4 @@ termcolor tqdm uvicorn wheel - git+https://github.com/opentensor/btwallet.git@main#egg=bittensor-wallet From 20b7fa6fe0f1f5c95bc7f735d7a6ebe898e8d943 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 8 Aug 2024 11:45:48 -0700 Subject: [PATCH 073/260] Add dendrite reference to backwords compatibility --- bittensor/utils/backwards_compatibility/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/utils/backwards_compatibility/__init__.py b/bittensor/utils/backwards_compatibility/__init__.py index 321c21ff8..2be69269f 100644 --- a/bittensor/utils/backwards_compatibility/__init__.py +++ b/bittensor/utils/backwards_compatibility/__init__.py @@ -122,6 +122,7 @@ # Backwards compatibility with previous bittensor versions. axon = Axon config = Config +dendrite = Dendrite keyfile = Keyfile metagraph = Metagraph wallet = Wallet From 49e10860d182465edd9e53f7cdb8eaf527f28805 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 8 Aug 2024 12:07:45 -0700 Subject: [PATCH 074/260] Fix the usage of env vars in default settings. --- bittensor/core/settings.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 5ae9a888e..0688d00ea 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -162,14 +162,20 @@ def turn_console_on(): }, } + +_BT_AXON_PORT = os.getenv("BT_AXON_PORT") +_BT_AXON_MAX_WORKERS = os.getenv("BT_AXON_MAX_WORKERS") +_BT_PRIORITY_MAX_WORKERS = os.getenv("BT_PRIORITY_MAX_WORKERS") +_BT_PRIORITY_MAXSIZE = os.getenv("BT_PRIORITY_MAXSIZE") + DEFAULTS = munchify( { "axon": { - "port": os.getenv("BT_AXON_PORT") or 8091, + "port": int(_BT_AXON_PORT) if _BT_AXON_PORT else 8091, "ip": os.getenv("BT_AXON_IP") or "[::]", "external_port": os.getenv("BT_AXON_EXTERNAL_PORT") or None, "external_ip": os.getenv("BT_AXON_EXTERNAL_IP") or None, - "max_workers": os.getenv("BT_AXON_MAX_WORKERS") or 10, + "max_workers": int(_BT_AXON_MAX_WORKERS) if _BT_AXON_MAX_WORKERS else 10, }, "logging": { "debug": os.getenv("BT_LOGGING_DEBUG") or False, @@ -178,8 +184,8 @@ def turn_console_on(): "logging_dir": os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR), }, "priority": { - "max_workers": os.getenv("BT_PRIORITY_MAX_WORKERS") or 5, - "maxsize": os.getenv("BT_PRIORITY_MAXSIZE") or 10, + "max_workers": int(_BT_PRIORITY_MAX_WORKERS) if _BT_PRIORITY_MAX_WORKERS else 5, + "maxsize": int(_BT_PRIORITY_MAXSIZE) if _BT_PRIORITY_MAXSIZE else 10, }, "subtensor": { "chain_endpoint": DEFAULT_ENDPOINT, From ed836e6e2f5c119e0859a9636cdf9f6a6e40427e Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 8 Aug 2024 12:09:13 -0700 Subject: [PATCH 075/260] ruff --- bittensor/core/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 0688d00ea..1c523cefc 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -184,7 +184,9 @@ def turn_console_on(): "logging_dir": os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR), }, "priority": { - "max_workers": int(_BT_PRIORITY_MAX_WORKERS) if _BT_PRIORITY_MAX_WORKERS else 5, + "max_workers": int(_BT_PRIORITY_MAX_WORKERS) + if _BT_PRIORITY_MAX_WORKERS + else 5, "maxsize": int(_BT_PRIORITY_MAXSIZE) if _BT_PRIORITY_MAXSIZE else 10, }, "subtensor": { From 7a0d94a5049b5822f228c2791c694af5b98da21c Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 9 Aug 2024 15:02:41 -0700 Subject: [PATCH 076/260] Change the subpackage name to `deprecated` --- bittensor/__init__.py | 2 +- bittensor/core/subtensor.py | 8 ++++---- .../{backwards_compatibility => deprecated}/__init__.py | 4 ++-- .../extrinsics/__init__.py | 0 .../extrinsics/prometheus.py | 0 .../extrinsics/serving.py | 0 .../extrinsics/set_weights.py | 0 .../extrinsics/transfer.py | 0 .../{backwards_compatibility => deprecated}/subnets.py | 0 .../unit_tests/extrinsics/test_backwards_compatibility.py | 4 ++-- tests/unit_tests/extrinsics/test_prometheus.py | 2 +- tests/unit_tests/extrinsics/test_serving.py | 8 ++++---- tests/unit_tests/extrinsics/test_set_weights.py | 2 +- 13 files changed, 15 insertions(+), 15 deletions(-) rename bittensor/utils/{backwards_compatibility => deprecated}/__init__.py (96%) rename bittensor/utils/{backwards_compatibility => deprecated}/extrinsics/__init__.py (100%) rename bittensor/utils/{backwards_compatibility => deprecated}/extrinsics/prometheus.py (100%) rename bittensor/utils/{backwards_compatibility => deprecated}/extrinsics/serving.py (100%) rename bittensor/utils/{backwards_compatibility => deprecated}/extrinsics/set_weights.py (100%) rename bittensor/utils/{backwards_compatibility => deprecated}/extrinsics/transfer.py (100%) rename bittensor/utils/{backwards_compatibility => deprecated}/subnets.py (100%) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 932bc996f..b131d13f0 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -19,7 +19,7 @@ import warnings from .core.settings import __version__, version_split, DEFAULTS -from .utils.backwards_compatibility import * +from .utils.deprecated import * from .utils.btlogging import logging diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index e837dca1a..062e02350 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -37,19 +37,19 @@ from scalecodec.types import ScaleType from substrateinterface.base import QueryMapResult, SubstrateInterface -from bittensor.utils.backwards_compatibility.extrinsics.prometheus import ( +from bittensor.utils.deprecated.extrinsics.prometheus import ( prometheus_extrinsic, ) -from bittensor.utils.backwards_compatibility.extrinsics.serving import ( +from bittensor.utils.deprecated.extrinsics.serving import ( serve_extrinsic, serve_axon_extrinsic, publish_metadata, get_metadata, ) -from bittensor.utils.backwards_compatibility.extrinsics.set_weights import ( +from bittensor.utils.deprecated.extrinsics.set_weights import ( set_weights_extrinsic, ) -from bittensor.utils.backwards_compatibility.extrinsics.transfer import ( +from bittensor.utils.deprecated.extrinsics.transfer import ( transfer_extrinsic, ) from bittensor.core import settings diff --git a/bittensor/utils/backwards_compatibility/__init__.py b/bittensor/utils/deprecated/__init__.py similarity index 96% rename from bittensor/utils/backwards_compatibility/__init__.py rename to bittensor/utils/deprecated/__init__.py index 2be69269f..6abd60457 100644 --- a/bittensor/utils/backwards_compatibility/__init__.py +++ b/bittensor/utils/deprecated/__init__.py @@ -150,8 +150,8 @@ mock_subpackage = importlib.import_module("bittensor.utils.mock") sys.modules["bittensor.mock"] = mock_subpackage -# Makes the `bittensor.utils.backwards_compatibility.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. +# Makes the `bittensor.utils.deprecated.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. extrinsics_subpackage = importlib.import_module( - "bittensor.utils.backwards_compatibility.extrinsics" + "bittensor.utils.deprecated.extrinsics" ) sys.modules["bittensor.extrinsics"] = extrinsics_subpackage diff --git a/bittensor/utils/backwards_compatibility/extrinsics/__init__.py b/bittensor/utils/deprecated/extrinsics/__init__.py similarity index 100% rename from bittensor/utils/backwards_compatibility/extrinsics/__init__.py rename to bittensor/utils/deprecated/extrinsics/__init__.py diff --git a/bittensor/utils/backwards_compatibility/extrinsics/prometheus.py b/bittensor/utils/deprecated/extrinsics/prometheus.py similarity index 100% rename from bittensor/utils/backwards_compatibility/extrinsics/prometheus.py rename to bittensor/utils/deprecated/extrinsics/prometheus.py diff --git a/bittensor/utils/backwards_compatibility/extrinsics/serving.py b/bittensor/utils/deprecated/extrinsics/serving.py similarity index 100% rename from bittensor/utils/backwards_compatibility/extrinsics/serving.py rename to bittensor/utils/deprecated/extrinsics/serving.py diff --git a/bittensor/utils/backwards_compatibility/extrinsics/set_weights.py b/bittensor/utils/deprecated/extrinsics/set_weights.py similarity index 100% rename from bittensor/utils/backwards_compatibility/extrinsics/set_weights.py rename to bittensor/utils/deprecated/extrinsics/set_weights.py diff --git a/bittensor/utils/backwards_compatibility/extrinsics/transfer.py b/bittensor/utils/deprecated/extrinsics/transfer.py similarity index 100% rename from bittensor/utils/backwards_compatibility/extrinsics/transfer.py rename to bittensor/utils/deprecated/extrinsics/transfer.py diff --git a/bittensor/utils/backwards_compatibility/subnets.py b/bittensor/utils/deprecated/subnets.py similarity index 100% rename from bittensor/utils/backwards_compatibility/subnets.py rename to bittensor/utils/deprecated/subnets.py diff --git a/tests/unit_tests/extrinsics/test_backwards_compatibility.py b/tests/unit_tests/extrinsics/test_backwards_compatibility.py index 7a73767d9..bea186dd7 100644 --- a/tests/unit_tests/extrinsics/test_backwards_compatibility.py +++ b/tests/unit_tests/extrinsics/test_backwards_compatibility.py @@ -30,9 +30,9 @@ def test_mock_import(): def test_extrinsics_import(): - """Tests that `bittensor.extrinsics` can be imported and is the same as `bittensor.utils.backwards_compatibility.extrinsics`.""" + """Tests that `bittensor.extrinsics` can be imported and is the same as `bittensor.utils.deprecated.extrinsics`.""" import bittensor.extrinsics as redirected_extrinsics - import bittensor.utils.backwards_compatibility.extrinsics as real_extrinsics + import bittensor.utils.deprecated.extrinsics as real_extrinsics assert "bittensor.extrinsics" in sys.modules assert redirected_extrinsics is real_extrinsics diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index 5f93ab92a..beb4ec42e 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -20,7 +20,7 @@ import pytest from bittensor_wallet import Wallet -from bittensor.utils.backwards_compatibility.extrinsics.prometheus import ( +from bittensor.utils.deprecated.extrinsics.prometheus import ( prometheus_extrinsic, ) from bittensor.core.subtensor import Subtensor diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index af6a056fe..beb401602 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -21,7 +21,7 @@ from bittensor_wallet import Wallet from bittensor.core.axon import Axon -from bittensor.utils.backwards_compatibility.extrinsics.serving import ( +from bittensor.utils.deprecated.extrinsics.serving import ( serve_extrinsic, publish_metadata, serve_axon_extrinsic, @@ -120,7 +120,7 @@ def test_serve_extrinsic_happy_path( # Arrange mock_subtensor.do_serve_axon.return_value = (True, "") with patch( - "bittensor.utils.backwards_compatibility.extrinsics.serving.Confirm.ask", + "bittensor.utils.deprecated.extrinsics.serving.Confirm.ask", return_value=True, ): # Act @@ -180,7 +180,7 @@ def test_serve_extrinsic_edge_cases( # Arrange mock_subtensor.do_serve_axon.return_value = (True, "") with patch( - "bittensor.utils.backwards_compatibility.extrinsics.serving.Confirm.ask", + "bittensor.utils.deprecated.extrinsics.serving.Confirm.ask", return_value=True, ): # Act @@ -240,7 +240,7 @@ def test_serve_extrinsic_error_cases( # Arrange mock_subtensor.do_serve_axon.return_value = (False, "Error serving axon") with patch( - "bittensor.utils.backwards_compatibility.extrinsics.serving.Confirm.ask", + "bittensor.utils.deprecated.extrinsics.serving.Confirm.ask", return_value=True, ): # Act diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 5c6817684..1c86f5a11 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -4,7 +4,7 @@ from bittensor.core.subtensor import Subtensor from bittensor_wallet import Wallet -from bittensor.utils.backwards_compatibility.extrinsics.set_weights import ( +from bittensor.utils.deprecated.extrinsics.set_weights import ( set_weights_extrinsic, ) From fcdd2832eada705baff981280569ca1f5c88a694 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 9 Aug 2024 15:06:19 -0700 Subject: [PATCH 077/260] Update __version__ until 8.0.0 --- bittensor/core/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 1c523cefc..254868b7f 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -15,7 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -__version__ = "7.5.0" +__version__ = "8.0.0" import os import re From 69e2ae98002bddc86659a71829ff1902ad999cac Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 9 Aug 2024 15:09:16 -0700 Subject: [PATCH 078/260] Move `subnets.py` to `bittensor/utils` as it is considered more utility than deprecated. --- bittensor/utils/deprecated/__init__.py | 2 +- bittensor/utils/{deprecated => }/subnets.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename bittensor/utils/{deprecated => }/subnets.py (100%) diff --git a/bittensor/utils/deprecated/__init__.py b/bittensor/utils/deprecated/__init__.py index 6abd60457..7973878fe 100644 --- a/bittensor/utils/deprecated/__init__.py +++ b/bittensor/utils/deprecated/__init__.py @@ -117,7 +117,7 @@ ) from bittensor.utils.balance import Balance as Balance # noqa: F401 from bittensor.utils.mock.subtensor_mock import MockSubtensor as MockSubtensor # noqa: F401 -from .subnets import SubnetsAPI # noqa: F401 +from bittensor.utils.subnets import SubnetsAPI # noqa: F401 # Backwards compatibility with previous bittensor versions. axon = Axon diff --git a/bittensor/utils/deprecated/subnets.py b/bittensor/utils/subnets.py similarity index 100% rename from bittensor/utils/deprecated/subnets.py rename to bittensor/utils/subnets.py From a6d1554dd131d5b4be91794da07f5beef8abafd9 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 9 Aug 2024 15:24:03 -0700 Subject: [PATCH 079/260] Rename test module --- .../test_backwards_compatibility.py => test_deprecated.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit_tests/{extrinsics/test_backwards_compatibility.py => test_deprecated.py} (100%) diff --git a/tests/unit_tests/extrinsics/test_backwards_compatibility.py b/tests/unit_tests/test_deprecated.py similarity index 100% rename from tests/unit_tests/extrinsics/test_backwards_compatibility.py rename to tests/unit_tests/test_deprecated.py From 141bbe89139ca485d664cd8b045df23f0badf132 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 9 Aug 2024 15:35:08 -0700 Subject: [PATCH 080/260] Add aliases mapping test --- tests/unit_tests/test_deprecated.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit_tests/test_deprecated.py b/tests/unit_tests/test_deprecated.py index bea186dd7..69f90a1fa 100644 --- a/tests/unit_tests/test_deprecated.py +++ b/tests/unit_tests/test_deprecated.py @@ -36,3 +36,17 @@ def test_extrinsics_import(): assert "bittensor.extrinsics" in sys.modules assert redirected_extrinsics is real_extrinsics + + +def test_object_aliases_are_correctly_mapped(): + """Ensures all object aliases correctly map to their respective classes in Bittensor package.""" + import bittensor + + assert issubclass(bittensor.axon, bittensor.Axon) + assert issubclass(bittensor.config, bittensor.Config) + assert issubclass(bittensor.dendrite, bittensor.Dendrite) + assert issubclass(bittensor.keyfile, bittensor.Keyfile) + assert issubclass(bittensor.metagraph, bittensor.Metagraph) + assert issubclass(bittensor.wallet, bittensor.Wallet) + assert issubclass(bittensor.synapse, bittensor.Synapse) + From 6dbb610b1f9cd8a8a82ce20f0284a6066a58d97d Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 9 Aug 2024 15:40:09 -0700 Subject: [PATCH 081/260] ruff --- bittensor/utils/deprecated/__init__.py | 4 +--- tests/unit_tests/test_deprecated.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/bittensor/utils/deprecated/__init__.py b/bittensor/utils/deprecated/__init__.py index 7973878fe..4dec76744 100644 --- a/bittensor/utils/deprecated/__init__.py +++ b/bittensor/utils/deprecated/__init__.py @@ -151,7 +151,5 @@ sys.modules["bittensor.mock"] = mock_subpackage # Makes the `bittensor.utils.deprecated.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. -extrinsics_subpackage = importlib.import_module( - "bittensor.utils.deprecated.extrinsics" -) +extrinsics_subpackage = importlib.import_module("bittensor.utils.deprecated.extrinsics") sys.modules["bittensor.extrinsics"] = extrinsics_subpackage diff --git a/tests/unit_tests/test_deprecated.py b/tests/unit_tests/test_deprecated.py index 69f90a1fa..239241407 100644 --- a/tests/unit_tests/test_deprecated.py +++ b/tests/unit_tests/test_deprecated.py @@ -49,4 +49,3 @@ def test_object_aliases_are_correctly_mapped(): assert issubclass(bittensor.metagraph, bittensor.Metagraph) assert issubclass(bittensor.wallet, bittensor.Wallet) assert issubclass(bittensor.synapse, bittensor.Synapse) - From 40a487ed29d2cfcc7a53e3dc60b1be8e34158410 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 9 Aug 2024 16:49:35 -0700 Subject: [PATCH 082/260] Init: Adds initial e2e tests --- tests/e2e_tests/__init__.py | 0 tests/e2e_tests/conftest.py | 84 +++++++++ tests/e2e_tests/test_axon.py | 128 ++++++++++++++ tests/e2e_tests/test_dendrite.py | 136 +++++++++++++++ tests/e2e_tests/test_incentive.py | 181 ++++++++++++++++++++ tests/e2e_tests/test_transfer.py | 52 ++++++ tests/e2e_tests/utils/chain_interactions.py | 121 +++++++++++++ tests/e2e_tests/utils/test_utils.py | 84 +++++++++ 8 files changed, 786 insertions(+) create mode 100644 tests/e2e_tests/__init__.py create mode 100644 tests/e2e_tests/conftest.py create mode 100644 tests/e2e_tests/test_axon.py create mode 100644 tests/e2e_tests/test_dendrite.py create mode 100644 tests/e2e_tests/test_incentive.py create mode 100644 tests/e2e_tests/test_transfer.py create mode 100644 tests/e2e_tests/utils/chain_interactions.py create mode 100644 tests/e2e_tests/utils/test_utils.py diff --git a/tests/e2e_tests/__init__.py b/tests/e2e_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py new file mode 100644 index 000000000..2a8507b53 --- /dev/null +++ b/tests/e2e_tests/conftest.py @@ -0,0 +1,84 @@ +import os +import re +import shlex +import signal +import subprocess +import time + +import pytest +from substrateinterface import SubstrateInterface + +from bittensor import logging +from tests.e2e_tests.utils.test_utils import ( + clone_or_update_templates, + install_templates, + template_path, + uninstall_templates, +) + + +# Fixture for setting up and tearing down a localnet.sh chain between tests +@pytest.fixture(scope="function") +def local_chain(request): + param = request.param if hasattr(request, "param") else None + # Get the environment variable for the script path + script_path = "/Users/ibraheem/Desktop/Bittensor/subtensor/scripts/localnet.sh" + + if not script_path: + # Skip the test if the localhost.sh path is not set + 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 + args = "" if param is None else f"{param}" + + # Compile commands to send to process + cmds = shlex.split(f"{script_path} {args}") + + # Start new node process + process = subprocess.Popen( + cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid + ) + + # Pattern match indicates node is compiled and ready + pattern = re.compile(r"Imported #1") + + # install neuron templates + logging.info("downloading and installing neuron templates from github") + templates_dir = clone_or_update_templates() + install_templates(templates_dir) + + timestamp = int(time.time()) + + def wait_for_node_start(process, pattern): + for line in process.stdout: + print(line.strip()) + # 10 min as timeout + if int(time.time()) - timestamp > 10 * 60: + print("Subtensor not started in time") + break + if pattern.search(line): + print("Node started!") + break + + wait_for_node_start(process, pattern) + + # Run the test, passing in substrate interface + yield SubstrateInterface(url="ws://127.0.0.1:9945") + + # Terminate the process group (includes all child processes) + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + + # Give some time for the process to terminate + time.sleep(1) + + # If the process is not terminated, send SIGKILL + if process.poll() is None: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + + # Ensure the process has terminated + process.wait() + + # uninstall templates + logging.info("uninstalling neuron templates") + uninstall_templates(template_path) diff --git a/tests/e2e_tests/test_axon.py b/tests/e2e_tests/test_axon.py new file mode 100644 index 000000000..ed6be9784 --- /dev/null +++ b/tests/e2e_tests/test_axon.py @@ -0,0 +1,128 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import logging +from bittensor.utils import networking +from tests.e2e_tests.utils.chain_interactions import register_neuron, register_subnet +from tests.e2e_tests.utils.test_utils import ( + setup_wallet, + template_path, + templates_repo, +) + + +@pytest.mark.asyncio +async def test_axon(local_chain): + """ + Test the Axon mechanism and successful registration on the network. + + 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 + 4. Check the metagraph again after running the miner and verify all attributes + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_axon") + + netuid = 1 + # Register root as Alice - the subnet owner + alice_keypair, wallet = setup_wallet("//Alice") + + # Register a subnet, netuid 1 + assert register_subnet(local_chain, wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" + + # Register Alice to the network + assert register_neuron( + local_chain, wallet, netuid + ), f"Neuron wasn't registered to subnet {netuid}" + + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + + # Validate current metagraph stats + old_axon = metagraph.axons[0] + assert len(metagraph.axons) == 1, f"Expected 1 axon, but got {len(metagraph.axons)}" + assert old_axon.hotkey == alice_keypair.ss58_address, "Hotkey mismatch for the axon" + assert ( + old_axon.coldkey == alice_keypair.ss58_address + ), "Coldkey mismatch for the axon" + assert old_axon.ip == "0.0.0.0", f"Expected IP 0.0.0.0, but got {old_axon.ip}" + assert old_axon.port == 0, f"Expected port 0, but got {old_axon.port}" + assert old_axon.ip_type == 0, f"Expected IP type 0, but got {old_axon.ip_type}" + + # Prepare to run the miner + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/miner.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + wallet.path, + "--wallet.name", + wallet.name, + "--wallet.hotkey", + "default", + ] + ) + + # Run the miner in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + logging.info("Neuron Alice is now mining") + + # Waiting for 5 seconds for metagraph to be updated + await asyncio.sleep(5) + + # Refresh the metagraph + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + updated_axon = metagraph.axons[0] + external_ip = networking.get_external_ip() + + # Assert updated attributes + assert ( + len(metagraph.axons) == 1 + ), f"Expected 1 axon, but got {len(metagraph.axons)} after mining" + + assert ( + len(metagraph.neurons) == 1 + ), f"Expected 1 neuron, but got {len(metagraph.neurons)}" + + assert ( + updated_axon.ip == external_ip + ), f"Expected IP {external_ip}, but got {updated_axon.ip}" + + assert ( + updated_axon.ip_type == networking.ip_version(external_ip) + ), f"Expected IP type {networking.ip_version(external_ip)}, but got {updated_axon.ip_type}" + + assert updated_axon.port == 8091, f"Expected port 8091, but got {updated_axon.port}" + + assert ( + updated_axon.hotkey == alice_keypair.ss58_address + ), "Hotkey mismatch after mining" + + assert ( + updated_axon.coldkey == alice_keypair.ss58_address + ), "Coldkey mismatch after mining" + + logging.info("✅ Passed test_axon") diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py new file mode 100644 index 000000000..fa481356e --- /dev/null +++ b/tests/e2e_tests/test_dendrite.py @@ -0,0 +1,136 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import logging, Subtensor + +from tests.e2e_tests.utils.test_utils import ( + setup_wallet, + template_path, + templates_repo, +) +from tests.e2e_tests.utils.chain_interactions import ( + register_neuron, + register_subnet, + add_stake, + wait_epoch, +) + + +@pytest.mark.asyncio +async def test_dendrite(local_chain): + """ + Test the Dendrite mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Bob as a validator + 3. Add stake to Bob and ensure neuron is not a validator yet + 4. Run Bob as a validator and wait epoch + 5. Ensure Bob's neuron has all correct attributes of a validator + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_dendrite") + netuid = 1 + + # Register root as Alice - the subnet owner + alice_keypair, alice_wallet = setup_wallet("//Alice") + + # Register a subnet, netuid 1 + assert register_subnet(local_chain, alice_wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" + + # Register Bob + bob_keypair, bob_wallet = setup_wallet("//Bob") + + # Register Bob to the network + assert register_neuron( + local_chain, bob_wallet, netuid + ), f"Neuron wasn't registered to subnet {netuid}" + + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + subtensor = Subtensor(network="ws://localhost:9945") + + # Assert one neuron is Bob + assert len(subtensor.neurons(netuid=netuid)) == 1 + neuron = metagraph.neurons[0] + assert neuron.hotkey == bob_keypair.ss58_address + assert neuron.coldkey == bob_keypair.ss58_address + + # Assert stake is 0 + assert neuron.stake.tao == 0 + + # Stake to become to top neuron after the first epoch + assert add_stake(local_chain, bob_wallet, bittensor.Balance.from_tao(10_000)) + + # Refresh metagraph + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + old_neuron = metagraph.neurons[0] + + # Assert stake is 10000 + assert ( + old_neuron.stake.tao == 10_000.0 + ), f"Expected 10_000.0 staked TAO, but got {neuron.stake.tao}" + + # Assert neuron is not a validator yet + assert old_neuron.active is True + assert old_neuron.validator_permit is False + assert old_neuron.validator_trust == 0.0 + assert old_neuron.pruning_score == 0 + + # Prepare to run the validator + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/validator.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + bob_wallet.path, + "--wallet.name", + bob_wallet.name, + "--wallet.hotkey", + "default", + ] + ) + + # Run the validator in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Alice is now validating") + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + + await wait_epoch(subtensor, netuid=netuid) + + # Refresh metagraph + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + + # Refresh validator neuron + updated_neuron = metagraph.neurons[0] + + assert len(metagraph.neurons) == 1 + assert updated_neuron.active is True + assert updated_neuron.validator_permit is True + assert updated_neuron.hotkey == bob_keypair.ss58_address + assert updated_neuron.coldkey == bob_keypair.ss58_address + assert updated_neuron.pruning_score != 0 + + logging.info("✅ Passed test_dendrite") diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py new file mode 100644 index 000000000..2c3946184 --- /dev/null +++ b/tests/e2e_tests/test_incentive.py @@ -0,0 +1,181 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import Subtensor, logging +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, + wait_epoch, +) +from tests.e2e_tests.utils.test_utils import ( + setup_wallet, + template_path, + templates_repo, +) + + +@pytest.mark.asyncio +async def test_incentive(local_chain): + """ + Test the incentive mechanism and interaction of miners/validators + + Steps: + 1. Register a subnet and register Alice & Bob + 2. Add Stake by Alice + 3. Run Alice as validator & Bob as miner. Wait Epoch + 4. Verify miner has correct: trust, rank, consensus, incentive + 5. Verify validator has correct: validator_permit, validator_trust, dividends, stake + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_incentive") + netuid = 1 + + # Register root as Alice - the subnet owner and validator + alice_keypair, alice_wallet = setup_wallet("//Alice") + register_subnet(local_chain, alice_wallet) + + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" + + # Register Bob as miner + bob_keypair, bob_wallet = setup_wallet("//Bob") + + # Register Alice as a neuron on the subnet + register_neuron(local_chain, alice_wallet, netuid) + + # Register Bob as a neuron on the subnet + register_neuron(local_chain, bob_wallet, netuid) + + subtensor = Subtensor(network="ws://localhost:9945") + # Assert two neurons are in network + assert ( + len(subtensor.neurons(netuid=netuid)) == 2 + ), "Alice & Bob not registered in the subnet" + + # Alice to stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(10_000)) + + # Prepare to run Bob as miner + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/miner.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + bob_wallet.path, + "--wallet.name", + bob_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + + # Run Bob as miner in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Bob is now mining") + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph to refresh with latest data + + # Prepare to run Alice as validator + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/validator.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + alice_wallet.path, + "--wallet.name", + alice_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + + # Run Alice as validator in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Alice is now validating") + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + + # Get latest metagraph + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + + # Get current miner/validator stats + bob_neuron = metagraph.neurons[1] + assert bob_neuron.incentive == 0 + assert bob_neuron.consensus == 0 + assert bob_neuron.rank == 0 + assert bob_neuron.trust == 0 + + alice_neuron = metagraph.neurons[0] + assert alice_neuron.validator_permit is False + assert alice_neuron.dividends == 0 + assert alice_neuron.stake.tao == 10_000.0 + assert alice_neuron.validator_trust == 0 + + # Wait until next epoch + await wait_epoch(subtensor) + + # Set weights by Alice on the subnet + subtensor._do_set_weights( + wallet=alice_wallet, + uids=[1], + vals=[65535], + netuid=netuid, + version_key=0, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + logging.info("Alice neuron set weights successfully") + + await wait_epoch(subtensor) + + # Refresh metagraph + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + + # Get current emissions and validate that Alice has gotten tao + bob_neuron = metagraph.neurons[1] + assert bob_neuron.incentive == 1 + assert bob_neuron.consensus == 1 + assert bob_neuron.rank == 1 + assert bob_neuron.trust == 1 + + alice_neuron = metagraph.neurons[0] + assert alice_neuron.validator_permit is True + assert alice_neuron.dividends == 1 + assert alice_neuron.stake.tao == 10_000.0 + assert alice_neuron.validator_trust == 1 + + logging.info("✅ Passed test_incentive") diff --git a/tests/e2e_tests/test_transfer.py b/tests/e2e_tests/test_transfer.py new file mode 100644 index 000000000..9ec501d5b --- /dev/null +++ b/tests/e2e_tests/test_transfer.py @@ -0,0 +1,52 @@ +from bittensor import Subtensor, logging +from bittensor.core.subtensor import transfer_extrinsic +from tests.e2e_tests.utils.test_utils import setup_wallet + + +def test_transfer(local_chain): + """ + Test the transfer mechanism on the chain + + Steps: + 1. Create a wallet for Alice + 2. Calculate existing balance and transfer 2 Tao + 3. Calculate balance after extrinsic call and verify calculations + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_transfer") + + # Set up Alice wallet + keypair, wallet = setup_wallet("//Alice") + + # Account details before transfer + acc_before = local_chain.query("System", "Account", [keypair.ss58_address]) + + # Transfer Tao using extrinsic + subtensor = Subtensor(network="ws://localhost:9945") + transfer_extrinsic( + subtensor=subtensor, + wallet=wallet, + dest="5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx", + amount=2, + wait_for_finalization=True, + wait_for_inclusion=True, + prompt=False, + ) + + # Account details after transfer + acc_after = local_chain.query("System", "Account", [keypair.ss58_address]) + + # Transfer calculation assertions + expected_transfer = 2_000_000_000 + tolerance = 200_000 # Tx fee tolerance + + actual_difference = ( + acc_before.value["data"]["free"] - acc_after.value["data"]["free"] + ) + assert ( + expected_transfer <= actual_difference <= expected_transfer + tolerance + ), f"Expected transfer with tolerance: {expected_transfer} <= {actual_difference} <= {expected_transfer + tolerance}" + + logging.info("✅ Passed test_transfer") diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py new file mode 100644 index 000000000..6da3a9c70 --- /dev/null +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -0,0 +1,121 @@ +import asyncio + +from substrateinterface import SubstrateInterface + +import bittensor +from bittensor import logging + + +def add_stake( + substrate: SubstrateInterface, wallet: bittensor.wallet, amount: bittensor.Balance +) -> bool: + """ + Adds stake to a hotkey using SubtensorModule. Mimics command of adding stake + """ + stake_call = substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={"hotkey": wallet.hotkey.ss58_address, "amount_staked": amount.rao}, + ) + extrinsic = substrate.create_signed_extrinsic( + call=stake_call, keypair=wallet.coldkey + ) + response = substrate.submit_extrinsic( + extrinsic, wait_for_finalization=True, wait_for_inclusion=True + ) + response.process_events() + return response.is_success + + +def register_subnet(substrate: SubstrateInterface, wallet: bittensor.wallet) -> bool: + """ + Registers a subnet on the chain using wallet. Mimics register subnet command. + """ + register_call = substrate.compose_call( + call_module="SubtensorModule", + call_function="register_network", + call_params={"immunity_period": 0, "reg_allowed": True}, + ) + extrinsic = substrate.create_signed_extrinsic( + call=register_call, keypair=wallet.coldkey + ) + response = substrate.submit_extrinsic( + extrinsic, wait_for_finalization=True, wait_for_inclusion=True + ) + response.process_events() + return response.is_success + + +def register_neuron( + substrate: SubstrateInterface, wallet: bittensor.wallet, netuid: int +) -> bool: + """ + Registers a neuron on a subnet. Mimics subnet register command. + """ + neuron_register_call = substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + }, + ) + extrinsic = substrate.create_signed_extrinsic( + call=neuron_register_call, keypair=wallet.coldkey + ) + response = substrate.submit_extrinsic( + extrinsic, wait_for_finalization=True, wait_for_inclusion=True + ) + response.process_events() + return response.is_success + + +async def wait_epoch(subtensor, netuid=1): + """ + Waits for the next epoch to start on a specific subnet. + + Queries the tempo value from the Subtensor module and calculates the + interval based on the tempo. Then waits for the next epoch to start + by monitoring the current block number. + + Raises: + Exception: If the tempo cannot be determined from the chain. + """ + q_tempo = [ + v.value + for [k, v] in subtensor.query_map_subtensor("Tempo") + if k.value == netuid + ] + if len(q_tempo) == 0: + raise Exception("could not determine tempo") + tempo = q_tempo[0] + logging.info(f"tempo = {tempo}") + await wait_interval(tempo, subtensor, netuid) + + +async def wait_interval(tempo, subtensor, netuid=1): + """ + Waits until the next tempo interval starts for a specific subnet. + + Calculates the next tempo block start based on the current block number + and the provided tempo, then enters a loop where it periodically checks + the current block number until the next tempo interval starts. + """ + interval = tempo + 1 + current_block = subtensor.get_current_block() + last_epoch = current_block - 1 - (current_block + netuid + 1) % interval + next_tempo_block_start = last_epoch + interval + last_reported = None + while current_block < next_tempo_block_start: + await asyncio.sleep( + 1 + ) # Wait for 1 second before checking the block number again + current_block = subtensor.get_current_block() + if last_reported is None or current_block - last_reported >= 10: + last_reported = current_block + print( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + logging.info( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) diff --git a/tests/e2e_tests/utils/test_utils.py b/tests/e2e_tests/utils/test_utils.py new file mode 100644 index 000000000..9061df791 --- /dev/null +++ b/tests/e2e_tests/utils/test_utils.py @@ -0,0 +1,84 @@ +import os +import shutil +import subprocess +import sys +from typing import Tuple + +from substrateinterface import Keypair + +import bittensor + +template_path = os.getcwd() + "/neurons/" +templates_repo = "templates repository" + + +def setup_wallet(uri: str) -> Tuple[Keypair, bittensor.wallet]: + """ + Sets up a wallet using the provided URI. + + This function creates a keypair from the given URI and initializes a wallet + at a temporary path. It sets the coldkey, coldkeypub, and hotkey for the wallet + using the generated keypair. + + Side Effects: + - Creates a wallet in a temporary directory. + - Sets keys in the wallet without encryption and with overwriting enabled. + """ + keypair = Keypair.create_from_uri(uri) + wallet_path = "/tmp/btcli-e2e-wallet-{}".format(uri.strip("/")) + wallet = bittensor.wallet(path=wallet_path) + wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True) + wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) + wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True) + return keypair, wallet + + +def clone_or_update_templates(specific_commit=None): + """ + Clones or updates the Bittensor subnet template repository. + + This function clones the Bittensor subnet template repository if it does not + already exist in the specified installation directory. If the repository already + exists, it updates it by pulling the latest changes. Optionally, it can check out + a specific commit if the `specific_commit` variable is set. + """ + install_dir = template_path + repo_mapping = { + templates_repo: "https://github.com/opentensor/bittensor-subnet-template.git", + } + + os.makedirs(install_dir, exist_ok=True) + os.chdir(install_dir) + + for repo, git_link in repo_mapping.items(): + if not os.path.exists(repo): + print(f"\033[94mCloning {repo}...\033[0m") + subprocess.run(["git", "clone", git_link, repo], check=True) + else: + print(f"\033[94mUpdating {repo}...\033[0m") + os.chdir(repo) + subprocess.run(["git", "pull"], check=True) + os.chdir("..") + + # For pulling specific commit versions of repo + if specific_commit: + os.chdir(templates_repo) + print( + f"\033[94mChecking out commit {specific_commit} in {templates_repo}...\033[0m" + ) + subprocess.run(["git", "checkout", specific_commit], check=True) + os.chdir("..") + + return install_dir + templates_repo + "/" + + +def install_templates(install_dir): + subprocess.check_call([sys.executable, "-m", "pip", "install", install_dir]) + + +def uninstall_templates(install_dir): + subprocess.check_call( + [sys.executable, "-m", "pip", "uninstall", "bittensor_subnet_template", "-y"] + ) + # Delete everything in directory + shutil.rmtree(install_dir) From 0d8a2563017c9996fac83c88eebacefc18fd76ad Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 9 Aug 2024 16:55:50 -0700 Subject: [PATCH 083/260] Ruff --- tests/e2e_tests/conftest.py | 2 +- tests/e2e_tests/test_dendrite.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 2a8507b53..710599190 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -34,7 +34,7 @@ def local_chain(request): # Compile commands to send to process cmds = shlex.split(f"{script_path} {args}") - + # Start new node process process = subprocess.Popen( cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py index fa481356e..d7e3e6ff6 100644 --- a/tests/e2e_tests/test_dendrite.py +++ b/tests/e2e_tests/test_dendrite.py @@ -33,7 +33,7 @@ async def test_dendrite(local_chain): Raises: AssertionError: If any of the checks or verifications fail """ - + logging.info("Testing test_dendrite") netuid = 1 From 240088463cd477a50fb783a928aff4498d986957 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 9 Aug 2024 17:00:43 -0700 Subject: [PATCH 084/260] Fixes LOCALNET_SH_PATH env_var --- tests/e2e_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 710599190..9fc9faec6 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -22,7 +22,7 @@ def local_chain(request): param = request.param if hasattr(request, "param") else None # Get the environment variable for the script path - script_path = "/Users/ibraheem/Desktop/Bittensor/subtensor/scripts/localnet.sh" + script_path = os.getenv("LOCALNET_SH_PATH") if not script_path: # Skip the test if the localhost.sh path is not set From 3f48492542fcb277f000faaeb81d181d4a8b6bb7 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 9 Aug 2024 17:29:04 -0700 Subject: [PATCH 085/260] Adds module description --- tests/e2e_tests/utils/chain_interactions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 6da3a9c70..f0797770d 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -1,3 +1,8 @@ +""" +This module provides functions interacting with the chain for end to end testing; +these are not present in btsdk but are required for e2e tests +""" + import asyncio from substrateinterface import SubstrateInterface From bbe6c8af9424064df1fbb33b20a34736b56b80cf Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 12 Aug 2024 18:05:54 -0700 Subject: [PATCH 086/260] Adds commit-reveal back and adds e2e for liquid alpha & commit reveal --- bittensor/core/subtensor.py | 270 ++++++++++++++++++ .../deprecated/extrinsics/commit_weights.py | 126 ++++++++ bittensor/utils/weight_utils.py | 60 +++- tests/e2e_tests/test_commit_weights.py | 161 +++++++++++ tests/e2e_tests/test_liquid_alpha.py | 187 ++++++++++++ tests/e2e_tests/utils/chain_interactions.py | 55 ++++ 6 files changed, 857 insertions(+), 2 deletions(-) create mode 100644 bittensor/utils/deprecated/extrinsics/commit_weights.py create mode 100644 tests/e2e_tests/test_commit_weights.py create mode 100644 tests/e2e_tests/test_liquid_alpha.py diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 062e02350..3ffd92d5a 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -49,9 +49,14 @@ from bittensor.utils.deprecated.extrinsics.set_weights import ( set_weights_extrinsic, ) +from bittensor.utils.deprecated.extrinsics.commit_weights import ( + commit_weights_extrinsic, + reveal_weights_extrinsic, +) from bittensor.utils.deprecated.extrinsics.transfer import ( transfer_extrinsic, ) +from bittensor.utils.weight_utils import generate_weight_hash from bittensor.core import settings from bittensor.core.axon import Axon from bittensor.core.chain_data import ( @@ -1836,3 +1841,268 @@ def get_existential_deposit( if result is None or not hasattr(result, "value"): return None return Balance.from_rao(result.value) + + def commit_weights( + self, + wallet: "Wallet", + netuid: int, + salt: List[int], + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + version_key: int = settings.version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, + max_retries: int = 5, + ) -> Tuple[bool, str]: + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This action serves as a commitment or snapshot of the neuron's current weight distribution. + + Args: + wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. + netuid (int): The unique identifier of the subnet. + salt (List[int]): list of randomly generated integers as salt to generated weighted hash. + 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, 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. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + max_retries (int, optional): The number of maximum attempts to commit weights. (Default: 5) + + Returns: + Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. + """ + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to commit weights!" + + logging.info( + "Committing weights with params: netuid={}, uids={}, weights={}, version_key={}".format( + netuid, uids, weights, version_key + ) + ) + + # Generate the hash of the weights + commit_hash = generate_weight_hash( + address=wallet.hotkey.ss58_address, + netuid=netuid, + uids=list(uids), + values=list(weights), + salt=salt, + version_key=version_key, + ) + + logging.info("Commit Hash: {}".format(commit_hash)) + + while retries < max_retries: + try: + success, message = commit_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + if success: + break + except Exception as e: + logging.error(f"Error committing weights: {e}") + finally: + retries += 1 + + return success, message + + def _do_commit_weights( + self, + wallet: "Wallet", + netuid: int, + commit_hash: str, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + ) -> Tuple[bool, Optional[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: + wallet (bittensor.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, 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. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + 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. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + + return make_substrate_call_with_retry() + + def reveal_weights( + self, + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + salt: Union[NDArray[np.int64], list], + version_key: int = settings.version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, + max_retries: int = 5, + ) -> Tuple[bool, str]: + """ + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This action serves as a revelation of the neuron's previously committed weight distribution. + + Args: + wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. + netuid (int): The unique identifier of the subnet. + uids (np.ndarray): NumPy array of neuron UIDs for which weights are being revealed. + 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, 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. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + max_retries (int, optional): The number of maximum attempts to reveal weights. (Default: 5) + + Returns: + Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string + value describing the success or potential error. + + This function allows neurons to reveal their previously committed weight distribution, ensuring transparency + and accountability within the Bittensor network. + """ + + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to reveal weights!" + + while retries < max_retries: + try: + success, message = reveal_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=list(uids), + weights=list(weights), + salt=list(salt), + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + if success: + break + except Exception as e: + logging.error(f"Error revealing weights: {e}") + finally: + retries += 1 + + return success, message + + def _do_reveal_weights( + self, + wallet: "Wallet", + netuid: int, + uids: List[int], + values: List[int], + salt: List[int], + version_key: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + ) -> Tuple[bool, Optional[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. + + Args: + wallet (bittensor.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. + values (List[int]): List of weight values corresponding to each UID. + salt (List[int]): List of salt values corresponding to the hash function. + version_key (int): 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. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency + and accountability for the neuron's weight distribution. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": values, + "salt": salt, + "version_key": version_key, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if response.is_success: + return True, None + else: + return False, format_error_message(response.error_message) + + return make_substrate_call_with_retry() diff --git a/bittensor/utils/deprecated/extrinsics/commit_weights.py b/bittensor/utils/deprecated/extrinsics/commit_weights.py new file mode 100644 index 000000000..9fe6ae7ff --- /dev/null +++ b/bittensor/utils/deprecated/extrinsics/commit_weights.py @@ -0,0 +1,126 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao +# Copyright © 2023 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. + +"""Module commit weights and reveal weights extrinsic.""" + +from typing import List, Tuple + +from rich.prompt import Confirm + +import bittensor +from bittensor.utils import format_error_message + + +def commit_weights_extrinsic( + subtensor: "bittensor.subtensor", + wallet: "bittensor.wallet", + netuid: int, + commit_hash: str, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, +) -> Tuple[bool, str]: + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This function is a wrapper around the `_do_commit_weights` method, handling user prompts and error messages. + Args: + subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. + wallet (bittensor.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, 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. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + Returns: + Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. + """ + if prompt and not Confirm.ask(f"Would you like to commit weights?"): + return False, "User cancelled the operation." + + success, error_message = subtensor._do_commit_weights( + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if success: + bittensor.logging.info("Successfully committed weights.") + return True, "Successfully committed weights." + else: + bittensor.logging.error(f"Failed to commit weights: {error_message}") + return False, format_error_message(error_message) + + +def reveal_weights_extrinsic( + subtensor: "bittensor.subtensor", + wallet: "bittensor.wallet", + netuid: int, + uids: List[int], + weights: List[int], + salt: List[int], + version_key: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, +) -> 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, handling user prompts and error messages. + Args: + subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. + wallet (bittensor.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. + weights (List[int]): List of weight values corresponding to each UID. + salt (List[int]): List of salt values corresponding to the hash function. + version_key (int): 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. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + Returns: + Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, 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. + """ + + if prompt and not Confirm.ask(f"Would you like to reveal weights?"): + return False, "User cancelled the operation." + + success, error_message = subtensor._do_reveal_weights( + wallet=wallet, + netuid=netuid, + uids=uids, + values=weights, + salt=salt, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if success: + bittensor.logging.info("Successfully revealed weights.") + return True, "Successfully revealed weights." + else: + bittensor.logging.error(f"Failed to reveal weights: {error_message}") + return False, error_message diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 333310836..5e5fa0b73 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -17,15 +17,18 @@ """Conversion for weight between chain representation and np.array or torch.Tensor""" +import hashlib import logging import typing -from typing import Tuple, List, Union +from typing import List, Tuple, Union import numpy as np from numpy.typing import NDArray +from scalecodec import U16, ScaleBytes, Vec +from substrateinterface import Keypair from bittensor.utils.btlogging import logging -from bittensor.utils.registration import torch, use_torch, legacy_torch_api_compat +from bittensor.utils.registration import legacy_torch_api_compat, torch, use_torch if typing.TYPE_CHECKING: from bittensor.core.metagraph import Metagraph @@ -357,3 +360,56 @@ def process_weights_for_netuid( logging.debug("final_weights", normalized_weights) return non_zero_weight_uids, normalized_weights + + +def generate_weight_hash( + address: str, + netuid: int, + uids: List[int], + values: List[int], + version_key: int, + salt: List[int], +) -> str: + """ + Generate a valid commit hash from the provided weights. + + Args: + address (str): The account identifier. Wallet ss58_address. + netuid (int): The network unique identifier. + uids (List[int]): The list of UIDs. + salt (List[int]): The salt to add to hash. + values (List[int]): The list of weight values. + version_key (int): The version key. + + Returns: + str: The generated commit hash. + """ + # Encode data using SCALE codec + wallet_address = ScaleBytes(Keypair(ss58_address=address).public_key) + netuid = ScaleBytes(netuid.to_bytes(2, "little")) + + vec_uids = Vec(data=None, sub_type="U16") + vec_uids.value = [U16(ScaleBytes(uid.to_bytes(2, "little"))) for uid in uids] + uids = ScaleBytes(vec_uids.encode().data) + + vec_values = Vec(data=None, sub_type="U16") + vec_values.value = [ + U16(ScaleBytes(value.to_bytes(2, "little"))) for value in values + ] + values = ScaleBytes(vec_values.encode().data) + + version_key = ScaleBytes(version_key.to_bytes(8, "little")) + + vec_salt = Vec(data=None, sub_type="U16") + vec_salt.value = [U16(ScaleBytes(salts.to_bytes(2, "little"))) for salts in salt] + salt = ScaleBytes(vec_salt.encode().data) + + data = wallet_address + netuid + uids + values + salt + version_key + + # Generate Blake2b hash of the data tuple + blake2b_hash = hashlib.blake2b(data.data, digest_size=32) + + # Convert the hash to hex string and add "0x" prefix + commit_hash = "0x" + blake2b_hash.hexdigest() + + return commit_hash diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py new file mode 100644 index 000000000..3e451f9ee --- /dev/null +++ b/tests/e2e_tests/test_commit_weights.py @@ -0,0 +1,161 @@ +import time + +import numpy as np +import pytest + +import bittensor +from bittensor import logging +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, + sudo_set_hyperparameter_bool, + sudo_set_hyperparameter_values, + wait_interval, +) +from tests.e2e_tests.utils.test_utils import setup_wallet + + +@pytest.mark.asyncio +async def test_commit_and_reveal_weights(local_chain): + """ + Tests the commit/reveal weights mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Enable commit-reveal mechanism on the subnet + 4. Lower the commit_reveal interval and rate limit + 5. Commit weights and verify + 6. Wait interval & reveal weights and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + netuid = 1 + logging.info("Testing test_commit_and_reveal_weights") + # Register root as Alice + keypair, alice_wallet = setup_wallet("//Alice") + assert register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Verify subnet 1 created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet wasn't created successfully" + + assert register_neuron( + local_chain, alice_wallet, netuid + ), "Unable to register Alice as a neuron" + + # Stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(100_000)) + + # Enable commit_reveal on the subnet + assert sudo_set_hyperparameter_bool( + local_chain, + alice_wallet, + "sudo_set_commit_reveal_weights_enabled", + True, + netuid, + ), "Unable to enable commit reveal on the subnet" + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert subtensor.get_subnet_hyperparameters( + netuid=netuid + ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" + + # Lower the commit_reveal interval + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_commit_reveal_weights_interval", + call_params={"netuid": netuid, "interval": "370"}, + return_error_message=True, + ) + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters( + netuid=netuid + ).commit_reveal_weights_interval + == 370 + ), "Failed to set commit/reveal interval" + + # Lower the rate limit + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + return_error_message=True, + ) + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).weights_rate_limit == 0 + ), "Failed to set weights_rate_limit" + + # Commit-reveal values + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + salt = [18, 179, 107, 0, 165, 211, 141, 197] + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + + # Commit weights + success, message = subtensor.commit_weights( + alice_wallet, + netuid, + salt=salt, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + weight_commits = subtensor.query_module( + module="SubtensorModule", + name="WeightCommits", + params=[netuid, alice_wallet.hotkey.ss58_address], + ) + # Assert that the committed weights are set correctly + assert weight_commits.value is not None, "Weight commit not found in storage" + commit_hash, commit_block = weight_commits.value + assert commit_block > 0, f"Invalid block number: {commit_block}" + + # Query the WeightCommitRevealInterval storage map + weight_commit_reveal_interval = subtensor.query_module( + module="SubtensorModule", name="WeightCommitRevealInterval", params=[netuid] + ) + interval = weight_commit_reveal_interval.value + assert interval > 0, "Invalid WeightCommitRevealInterval" + + # Wait until the reveal block range + await wait_interval(interval, subtensor) + + # Reveal weights + success, message = subtensor.reveal_weights( + alice_wallet, + netuid, + uids=weight_uids, + weights=weight_vals, + salt=salt, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + time.sleep(10) + + # Query the Weights storage map + revealed_weights = subtensor.query_module( + module="SubtensorModule", + name="Weights", + params=[netuid, 0], # netuid and uid + ) + + # Assert that the revealed weights are set correctly + assert revealed_weights.value is not None, "Weight reveal not found in storage" + + assert ( + weight_vals[0] == revealed_weights.value[0][1] + ), f"Incorrect revealed weights. Expected: {weights[0]}, Actual: {revealed_weights.value[0][1]}" + logging.info("✅ Passed test_commit_and_reveal_weights") diff --git a/tests/e2e_tests/test_liquid_alpha.py b/tests/e2e_tests/test_liquid_alpha.py new file mode 100644 index 000000000..371a908e4 --- /dev/null +++ b/tests/e2e_tests/test_liquid_alpha.py @@ -0,0 +1,187 @@ +import bittensor +from bittensor import logging +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, + sudo_set_hyperparameter_bool, + sudo_set_hyperparameter_values, +) +from tests.e2e_tests.utils.test_utils import setup_wallet + + +def liquid_alpha_call_params(netuid: int, alpha_values: str): + alpha_low, alpha_high = [v.strip() for v in alpha_values.split(",")] + return { + "netuid": netuid, + "alpha_low": alpha_low, + "alpha_high": alpha_high, + } + + +def test_liquid_alpha(local_chain): + """ + Test the liquid alpha mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Verify we can't set alpha values without enabling liquid_alpha + 4. Test setting alpha values after enabling liquid_alpha + 5. Verify failures when setting incorrect values (upper and lower bounds) + Raises: + AssertionError: If any of the checks or verifications fail + """ + u16_max = 65535 + netuid = 1 + logging.info("Testing test_liquid_alpha_enabled") + + # Register root as Alice + keypair, alice_wallet = setup_wallet("//Alice") + register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + # Register a neuron to the subnet + ( + register_neuron(local_chain, alice_wallet, netuid), + "Unable to register Alice as a neuron", + ) + + # Stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(100_000)) + + # Assert liquid alpha is disabled + subtensor = bittensor.subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters(netuid=netuid).liquid_alpha_enabled + is False + ), "Liquid alpha is enabled by default" + + # Attempt to set alpha high/low while disabled (should fail) + alpha_values = "6553, 53083" + call_params = liquid_alpha_call_params(netuid, alpha_values) + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Alpha values set while being disabled" + assert error_message["name"] == "LiquidAlphaDisabled" + + # Enabled liquid alpha on the subnet + assert sudo_set_hyperparameter_bool( + local_chain, alice_wallet, "sudo_set_liquid_alpha_enabled", True, netuid + ), "Unable to enable liquid alpha" + + assert subtensor.get_subnet_hyperparameters( + netuid=1 + ).liquid_alpha_enabled, "Failed to enable liquid alpha" + + # Attempt to set alpha high & low after enabling the hyperparameter + alpha_values = "87, 54099" + call_params = liquid_alpha_call_params(netuid, alpha_values) + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + ), "Unable to set alpha_values" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 54099 + ), "Failed to set alpha high" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 87 + ), "Failed to set alpha low" + + # Testing alpha high upper and lower bounds + + # 1. Test setting Alpha_high too low + alpha_high_too_low = ( + u16_max * 4 // 5 + ) - 1 # One less than the minimum acceptable value + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_low}") + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + + assert result is False, "Able to set incorrect alpha_high value" + assert error_message["name"] == "AlphaHighTooLow" + + # 2. Test setting Alpha_high too high + alpha_high_too_high = u16_max + 1 # One more than the max acceptable value + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_high}") + try: + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + except Exception as e: + assert str(e) == "65536 out of range for u16", f"Unexpected error: {e}" + + # Testing alpha low upper and lower bounds + + # 1. Test setting Alpha_low too low + alpha_low_too_low = 0 + call_params = liquid_alpha_call_params(netuid, f"{alpha_low_too_low}, 53083") + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Able to set incorrect alpha_low value" + assert error_message["name"] == "AlphaLowOutOfRange" + + # 2. Test setting Alpha_low too high + alpha_low_too_high = ( + u16_max * 4 // 5 + ) + 1 # One more than the maximum acceptable value + call_params = liquid_alpha_call_params(netuid, f"{alpha_low_too_high}, 53083") + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Able to set incorrect alpha_low value" + assert error_message["name"] == "AlphaLowOutOfRange" + + # Setting normal alpha values + alpha_values = "6553, 53083" + call_params = liquid_alpha_call_params(netuid, alpha_values) + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + ), "Unable to set liquid alpha values" + + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 53083 + ), "Failed to set alpha high" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 6553 + ), "Failed to set alpha low" + + # Disable Liquid Alpha + assert sudo_set_hyperparameter_bool( + local_chain, alice_wallet, "sudo_set_liquid_alpha_enabled", False, netuid + ), "Unable to disable liquid alpha" + + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False + ), "Failed to disable liquid alpha" + logging.info("✅ Passed test_liquid_alpha") diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index f0797770d..148da7680 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -4,6 +4,7 @@ """ import asyncio +from typing import Dict, List, Tuple, Union from substrateinterface import SubstrateInterface @@ -11,6 +12,60 @@ from bittensor import logging +def sudo_set_hyperparameter_bool( + substrate: SubstrateInterface, + wallet: bittensor.wallet, + call_function: str, + value: bool, + netuid: int, +) -> bool: + """ + Sets boolean hyperparameter value through AdminUtils. Mimics setting hyperparams + """ + call = substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params={"netuid": netuid, "enabled": value}, + ) + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + response.process_events() + return response.is_success + + +def sudo_set_hyperparameter_values( + substrate: SubstrateInterface, + wallet: bittensor.wallet, + call_function: str, + call_params: Dict, + return_error_message: bool = False, +) -> Union[bool, Tuple[bool, str]]: + """ + Sets liquid alpha values using AdminUtils. Mimics setting hyperparams + """ + call = substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params=call_params, + ) + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + response.process_events() + + if return_error_message: + return response.is_success, response.error_message + + return response.is_success + + def add_stake( substrate: SubstrateInterface, wallet: bittensor.wallet, amount: bittensor.Balance ) -> bool: From 9b7492196a37502ec03671959cdb57c6c2403051 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 13 Aug 2024 09:30:20 -0700 Subject: [PATCH 087/260] Review suggestions implemented --- bittensor/utils/deprecated/extrinsics/commit_weights.py | 9 ++++----- tests/e2e_tests/conftest.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/bittensor/utils/deprecated/extrinsics/commit_weights.py b/bittensor/utils/deprecated/extrinsics/commit_weights.py index 9fe6ae7ff..cd56988c7 100644 --- a/bittensor/utils/deprecated/extrinsics/commit_weights.py +++ b/bittensor/utils/deprecated/extrinsics/commit_weights.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 9fc9faec6..710599190 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -22,7 +22,7 @@ def local_chain(request): param = request.param if hasattr(request, "param") else None # Get the environment variable for the script path - script_path = os.getenv("LOCALNET_SH_PATH") + script_path = "/Users/ibraheem/Desktop/Bittensor/subtensor/scripts/localnet.sh" if not script_path: # Skip the test if the localhost.sh path is not set From b417ef1d849cd463bd968e296c061dbab7992f39 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 13 Aug 2024 09:32:07 -0700 Subject: [PATCH 088/260] Fixes logging string --- bittensor/core/subtensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 3ffd92d5a..9b35cda2e 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1898,7 +1898,7 @@ def commit_weights( version_key=version_key, ) - logging.info("Commit Hash: {}".format(commit_hash)) + logging.info(f"Commit Hash: {commit_hash}") while retries < max_retries: try: From 8109814ebe587a9420f1e576ce07c621d76c5fc0 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Aug 2024 11:27:28 -0700 Subject: [PATCH 089/260] Enhance: Switch from format() to f-strings --- bittensor/core/axon.py | 22 ++++-------- bittensor/core/chain_data.py | 4 +-- bittensor/core/config.py | 4 +-- bittensor/core/dendrite.py | 2 +- bittensor/core/metagraph.py | 4 +-- bittensor/core/subtensor.py | 12 +++---- bittensor/utils/__init__.py | 10 +++--- bittensor/utils/balance.py | 7 ++-- bittensor/utils/btlogging/format.py | 4 +-- .../utils/deprecated/extrinsics/prometheus.py | 33 ++++++----------- .../utils/deprecated/extrinsics/serving.py | 20 ++++------- .../deprecated/extrinsics/set_weights.py | 12 +++---- .../utils/deprecated/extrinsics/transfer.py | 35 ++++++++----------- bittensor/utils/networking.py | 2 +- bittensor/utils/version.py | 8 ++--- bittensor/utils/weight_utils.py | 10 ++---- tests/e2e_tests/utils/test_utils.py | 2 +- tests/unit_tests/test_dendrite.py | 4 +-- 18 files changed, 71 insertions(+), 124 deletions(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index e5ae679d1..da1392f13 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -550,23 +550,17 @@ async def endpoint(*args, **kwargs): ) assert ( signature(blacklist_fn) == blacklist_sig - ), "The blacklist_fn function must have the signature: blacklist( synapse: {} ) -> Tuple[bool, str]".format( - request_name - ) + ), f"The blacklist_fn function must have the signature: blacklist( synapse: {request_name} ) -> Tuple[bool, str]" if priority_fn: priority_sig = Signature(expected_params, return_annotation=float) assert ( signature(priority_fn) == priority_sig - ), "The priority_fn function must have the signature: priority( synapse: {} ) -> float".format( - request_name - ) + ), f"The priority_fn function must have the signature: priority( synapse: {request_name} ) -> float" if verify_fn: verify_sig = Signature(expected_params, return_annotation=None) assert ( signature(verify_fn) == verify_sig - ), "The verify_fn function must have the signature: verify( synapse: {} ) -> None".format( - request_name - ) + ), f"The verify_fn function must have the signature: verify( synapse: {request_name} ) -> None" # Store functions in appropriate attribute dictionaries self.forward_class_types[request_name] = param_class @@ -730,13 +724,9 @@ def to_string(self): def __str__(self) -> str: """Provides a human-readable representation of the Axon instance.""" - return "Axon({}, {}, {}, {}, {})".format( - self.ip, - self.port, - self.wallet.hotkey.ss58_address, - "started" if self.started else "stopped", - list(self.forward_fns.keys()), - ) + _started = "started" if self.started else "stopped" + _keys = list(self.forward_fns.keys()) + return f"Axon({self.ip}, {self.port}, {self.wallet.hotkey.ss58_address}, {_started}, {_keys})" def __repr__(self) -> str: """ diff --git a/bittensor/core/chain_data.py b/bittensor/core/chain_data.py index e96dbd2ba..6d4e6103c 100644 --- a/bittensor/core/chain_data.py +++ b/bittensor/core/chain_data.py @@ -242,9 +242,7 @@ def __eq__(self, other: "AxonInfo"): return False def __str__(self): - return "AxonInfo( {}, {}, {}, {} )".format( - str(self.ip_str()), str(self.hotkey), str(self.coldkey), self.version - ) + return f"AxonInfo( {self.ip_str()}, {self.hotkey}, {self.coldkey}, {self.version} )" def __repr__(self): return self.__str__() diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 79eda9c90..61afb5f05 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -154,10 +154,10 @@ def __init__( try: with open(config_file_path) as f: params_config = yaml.safe_load(f) - print("Loading config defaults from: {}".format(config_file_path)) + print(f"Loading config defaults from: {config_file_path}") parser.set_defaults(**params_config) except Exception as e: - print("Error in loading: {} using default parser settings".format(e)) + print(f"Error in loading: {e} using default parser settings") # 2. Continue with loading in params. params = config.__parse_args__(args=args, parser=parser, strict=strict) diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 3daeca3f2..c40c018bc 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -742,7 +742,7 @@ def __str__(self) -> str: Returns: str: The string representation of the Dendrite object in the format :func:`dendrite()`. """ - return "dendrite({})".format(self.keypair.ss58_address) + return f"dendrite({self.keypair.ss58_address})" def __repr__(self) -> str: """ diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index fdb46dcbe..8fee673ff 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -407,9 +407,7 @@ def __str__(self) -> str: print(metagraph) # Output: "metagraph(netuid:1, n:100, block:500, network:finney)" """ - return "metagraph(netuid:{}, n:{}, block:{}, network:{})".format( - self.netuid, self.n.item(), self.block.item(), self.network - ) + return f"metagraph(netuid:{self.netuid}, n:{self.n.item()}, block:{self.block.item()}, network:{self.network})" def __repr__(self) -> str: """ diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 9b35cda2e..f50d449d7 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -227,10 +227,10 @@ def __init__( def __str__(self) -> str: if self.network == self.chain_endpoint: # Connecting to chain endpoint without network known. - return "subtensor({})".format(self.chain_endpoint) + return f"subtensor({self.chain_endpoint})" else: # Connecting to network with endpoint known. - return "subtensor({}, {})".format(self.network, self.chain_endpoint) + return f"subtensor({self.network}, {self.chain_endpoint})" def __repr__(self) -> str: return self.__str__() @@ -1802,9 +1802,7 @@ def get_transfer_fee( ) except Exception as e: settings.bt_console.print( - ":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n {}[/bold white]".format( - e - ) + f":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n {e}[/bold white]" ) payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao @@ -1883,9 +1881,7 @@ def commit_weights( message = "No attempt made. Perhaps it is too soon to commit weights!" logging.info( - "Committing weights with params: netuid={}, uids={}, weights={}, version_key={}".format( - netuid, uids, weights, version_key - ) + f"Committing weights with params: netuid={netuid}, uids={uids}, weights={weights}, version_key={version_key}" ) # Generate the hash of the weights diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index e4be85346..3089aff70 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -173,7 +173,7 @@ def get_explorer_root_url_by_network_from_map( def get_explorer_url_for_network( network: str, block_hash: str, network_map: Dict[str, str] -) -> Optional[List[str]]: +) -> Optional[Dict[str, str]]: """ Returns the explorer url for the given block hash and network. @@ -195,11 +195,11 @@ def get_explorer_url_for_network( if explorer_root_urls != {}: # We are on a known network. - explorer_opentensor_url = "{root_url}/query/{block_hash}".format( - root_url=explorer_root_urls.get("opentensor"), block_hash=block_hash + explorer_opentensor_url = ( + f"{explorer_root_urls.get('opentensor')}/query/{block_hash}" ) - explorer_taostats_url = "{root_url}/extrinsic/{block_hash}".format( - root_url=explorer_root_urls.get("taostats"), block_hash=block_hash + explorer_taostats_url = ( + f"{explorer_root_urls.get('taostats')}/extrinsic/{block_hash}" ) explorer_urls["opentensor"] = explorer_opentensor_url explorer_urls["taostats"] = explorer_taostats_url diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 90c851814..efe21885e 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -71,11 +71,8 @@ def __str__(self): return f"{self.unit}{float(self.tao):,.9f}" def __rich__(self): - return "[green]{}[/green][green]{}[/green][green].[/green][dim green]{}[/dim green]".format( - self.unit, - format(float(self.tao), "f").split(".")[0], - format(float(self.tao), "f").split(".")[1], - ) + int_tao, fract_tao = format(float(self.tao), "f").split(".") + return f"[green]{self.unit}[/green][green]{int_tao}[/green][green].[/green][dim green]{fract_tao}[/dim green]" def __str_rao__(self): return f"{self.rao_unit}{int(self.rao)}" diff --git a/bittensor/utils/btlogging/format.py b/bittensor/utils/btlogging/format.py index 70629128f..131085f17 100644 --- a/bittensor/utils/btlogging/format.py +++ b/bittensor/utils/btlogging/format.py @@ -134,7 +134,7 @@ def formatTime(self, record, datefmt=None) -> str: s = time.strftime(datefmt, created) else: s = time.strftime("%Y-%m-%d %H:%M:%S", created) - s += ".{:03d}".format(int(record.msecs)) + s += f".{int(record.msecs):03d}" return s def format(self, record) -> str: @@ -205,7 +205,7 @@ def formatTime(self, record, datefmt=None) -> str: s = time.strftime(datefmt, created) else: s = time.strftime("%Y-%m-%d %H:%M:%S", created) - s += ".{:03d}".format(int(record.msecs)) + s += f".{int(record.msecs):03d}" return s def format(self, record) -> str: diff --git a/bittensor/utils/deprecated/extrinsics/prometheus.py b/bittensor/utils/deprecated/extrinsics/prometheus.py index 2dd668bda..64ee07c60 100644 --- a/bittensor/utils/deprecated/extrinsics/prometheus.py +++ b/bittensor/utils/deprecated/extrinsics/prometheus.py @@ -60,19 +60,13 @@ def prometheus_extrinsic( try: external_ip = net.get_external_ip() bt_console.print( - ":white_heavy_check_mark: [green]Found external ip: {}[/green]".format( - external_ip - ) - ) - logging.success( - prefix="External IP", suffix="{}".format(external_ip) + f":white_heavy_check_mark: [green]Found external ip: {external_ip}[/green]" ) - except Exception as E: + logging.success(prefix="External IP", suffix="{external_ip}") + except Exception as e: raise RuntimeError( - "Unable to attain your external ip. Check your internet connection. error: {}".format( - E - ) - ) from E + f"Unable to attain your external ip. Check your internet connection. error: {e}" + ) from e else: external_ip = ip @@ -105,9 +99,7 @@ def prometheus_extrinsic( ) bt_console.print( - ":white_heavy_check_mark: [white]Prometheus already served.[/white]".format( - external_ip - ) + f":white_heavy_check_mark: [white]Prometheus already served.[/white]" ) return True @@ -115,11 +107,9 @@ def prometheus_extrinsic( call_params["netuid"] = netuid with bt_console.status( - ":satellite: Serving prometheus on: [white]{}:{}[/white] ...".format( - subtensor.network, netuid - ) + f":satellite: Serving prometheus on: [white]{subtensor.network}:{netuid}[/white] ..." ): - success, err = subtensor.do_serve_prometheus( + success, error_message = subtensor.do_serve_prometheus( wallet=wallet, call_params=call_params, wait_for_finalization=wait_for_finalization, @@ -128,14 +118,13 @@ def prometheus_extrinsic( if wait_for_inclusion or wait_for_finalization: if success is True: + json_ = json.dumps(call_params, indent=4, sort_keys=True) bt_console.print( - ":white_heavy_check_mark: [green]Served prometheus[/green]\n [bold white]{}[/bold white]".format( - json.dumps(call_params, indent=4, sort_keys=True) - ) + f":white_heavy_check_mark: [green]Served prometheus[/green]\n [bold white]{json_}[/bold white]" ) return True else: - bt_console.print(f":cross_mark: [red]Failed[/red]: {err}") + bt_console.print(f":cross_mark: [red]Failed[/red]: {error_message}") return False else: return True diff --git a/bittensor/utils/deprecated/extrinsics/serving.py b/bittensor/utils/deprecated/extrinsics/serving.py index ab2421566..7253f25d7 100644 --- a/bittensor/utils/deprecated/extrinsics/serving.py +++ b/bittensor/utils/deprecated/extrinsics/serving.py @@ -110,9 +110,7 @@ def serve_extrinsic( output["coldkey"] = wallet.coldkeypub.ss58_address output["hotkey"] = wallet.hotkey.ss58_address if not Confirm.ask( - "Do you want to serve axon:\n [bold white]{}[/bold white]".format( - json.dumps(output, indent=4, sort_keys=True) - ) + f"Do you want to serve axon:\n [bold white]{json.dumps(output, indent=4, sort_keys=True)}[/bold white]" ): return False @@ -168,19 +166,13 @@ def serve_axon_extrinsic( try: external_ip = net.get_external_ip() bt_console.print( - ":white_heavy_check_mark: [green]Found external ip: {}[/green]".format( - external_ip - ) - ) - logging.success( - prefix="External IP", suffix="{}".format(external_ip) + f":white_heavy_check_mark: [green]Found external ip: {external_ip}[/green]" ) - except Exception as E: + logging.success(prefix="External IP", suffix=f"{external_ip}") + except Exception as e: raise RuntimeError( - "Unable to attain your external ip. Check your internet connection. error: {}".format( - E - ) - ) from E + f"Unable to attain your external ip. Check your internet connection. error: {e}" + ) from e else: external_ip = axon.external_ip diff --git a/bittensor/utils/deprecated/extrinsics/set_weights.py b/bittensor/utils/deprecated/extrinsics/set_weights.py index 8848f38b6..15681dbb1 100644 --- a/bittensor/utils/deprecated/extrinsics/set_weights.py +++ b/bittensor/utils/deprecated/extrinsics/set_weights.py @@ -81,14 +81,13 @@ def set_weights_extrinsic( # Ask before moving on. if prompt: if not Confirm.ask( - "Do you want to set weights:\n[bold white] weights: {}\n uids: {}[/bold white ]?".format( - [float(v / 65535) for v in weight_vals], weight_uids - ) + f"Do you want to set weights:\n[bold white] weights: {[float(v / 65535) for v in weight_vals]}\n" + f"uids: {weight_uids}[/bold white ]?" ): return False, "Prompt refused." with bt_console.status( - ":satellite: Setting weights on [white]{}[/white] ...".format(subtensor.network) + f":satellite: Setting weights on [white]{subtensor.network}[/white] ..." ): try: success, error_message = subtensor.do_set_weights( @@ -107,8 +106,9 @@ def set_weights_extrinsic( if success is True: bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") logging.success( + msg=str(success), prefix="Set weights", - suffix="Finalized: " + str(success), + suffix="Finalized: ", ) return True, "Successfully set weights and Finalized." else: @@ -120,7 +120,7 @@ def set_weights_extrinsic( return False, error_message except Exception as e: - bt_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) + bt_console.print(f":cross_mark: [red]Failed[/red]: error:{e}") logging.warning( msg=str(e), prefix="Set weights", suffix="Failed: " ) diff --git a/bittensor/utils/deprecated/extrinsics/transfer.py b/bittensor/utils/deprecated/extrinsics/transfer.py index e56245895..0b7f2bddd 100644 --- a/bittensor/utils/deprecated/extrinsics/transfer.py +++ b/bittensor/utils/deprecated/extrinsics/transfer.py @@ -59,9 +59,7 @@ def transfer_extrinsic( # Validate destination address. if not is_valid_bittensor_address_or_public_key(dest): bt_console.print( - ":cross_mark: [red]Invalid destination address[/red]:[bold white]\n {}[/bold white]".format( - dest - ) + f":cross_mark: [red]Invalid destination address[/red]:[bold white]\n {dest}[/bold white]" ) return False @@ -96,18 +94,21 @@ def transfer_extrinsic( # Check if we have enough balance. if account_balance < (transfer_balance + fee + existential_deposit): bt_console.print( - ":cross_mark: [red]Not enough balance[/red]:[bold white]\n balance: {}\n amount: {}\n for fee: {}[/bold white]".format( - account_balance, transfer_balance, fee - ) + ":cross_mark: [red]Not enough balance[/red]:[bold white]\n" + f" balance: {account_balance}\n" + f" amount: {transfer_balance}\n" + f" for fee: {fee}[/bold white]" ) return False # Ask before moving on. if prompt: if not Confirm.ask( - "Do you want to transfer:[bold white]\n amount: {}\n from: {}:{}\n to: {}\n for fee: {}[/bold white]".format( - transfer_balance, wallet.name, wallet.coldkey.ss58_address, dest, fee - ) + "Do you want to transfer:[bold white]\n" + f" amount: {transfer_balance}\n" + f" from: {wallet.name}:{wallet.coldkey.ss58_address}\n" + f" to: {dest}\n" + f" for fee: {fee}[/bold white]" ): return False @@ -122,21 +123,17 @@ def transfer_extrinsic( if success: bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") - bt_console.print("[green]Block Hash: {}[/green]".format(block_hash)) + bt_console.print(f"[green]Block Hash: {block_hash}[/green]") explorer_urls = get_explorer_url_for_network( subtensor.network, block_hash, NETWORK_EXPLORER_MAP ) if explorer_urls != {} and explorer_urls: bt_console.print( - "[green]Opentensor Explorer Link: {}[/green]".format( - explorer_urls.get("opentensor") - ) + f"[green]Opentensor Explorer Link: {explorer_urls.get('opentensor')}[/green]" ) bt_console.print( - "[green]Taostats Explorer Link: {}[/green]".format( - explorer_urls.get("taostats") - ) + f"[green]Taostats Explorer Link: {explorer_urls.get('taostats')}[/green]" ) else: bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") @@ -144,11 +141,7 @@ def transfer_extrinsic( if success: with bt_console.status(":satellite: Checking Balance..."): new_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - bt_console.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - account_balance, new_balance - ) - ) + bt_console.print(f"Balance:\n [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]") return True return False diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index 66f397114..db1a0d7a6 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -164,6 +164,6 @@ def get_formatted_ws_endpoint_url(endpoint_url: str) -> str: The formatted endpoint url. In the form of ws:// or wss:// """ if endpoint_url[0:6] != "wss://" and endpoint_url[0:5] != "ws://": - endpoint_url = "ws://{}".format(endpoint_url) + endpoint_url = f"ws://{endpoint_url}" return endpoint_url diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index c79fb6492..6eb600e56 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -103,11 +103,9 @@ def check_version(timeout: int = 15): if Version(latest_version) > Version(__version__): print( - "\u001b[33mBittensor Version: Current {}/Latest {}\nPlease update to the latest version at your earliest convenience. " - "Run the following command to upgrade:\n\n\u001b[0mpython -m pip install --upgrade bittensor".format( - __version__, latest_version - ) - ) + f"\u001b[33mBittensor Version: Current {__version__}/Latest {latest_version}\n" + f"Please update to the latest version at your earliest convenience. " + "Run the following command to upgrade:\n\n\u001b[0mpython -m pip install --upgrade bittensor") pass except Exception as e: raise VersionCheckError("Version check failed") from e diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 5e5fa0b73..3cc56ac96 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -195,16 +195,12 @@ def convert_weights_and_uids_for_emit( weights = weights.tolist() uids = uids.tolist() if min(weights) < 0: - raise ValueError( - "Passed weight is negative cannot exist on chain {}".format(weights) - ) + raise ValueError(f"Passed weight is negative cannot exist on chain {weights}") if min(uids) < 0: - raise ValueError("Passed uid is negative cannot exist on chain {}".format(uids)) + raise ValueError(f"Passed uid is negative cannot exist on chain {uids}") if len(uids) != len(weights): raise ValueError( - "Passed weights and uids must have the same length, got {} and {}".format( - len(uids), len(weights) - ) + f"Passed weights and uids must have the same length, got {len(uids)} and {len(weights)}" ) if sum(weights) == 0: return [], [] # Nothing to set on chain. diff --git a/tests/e2e_tests/utils/test_utils.py b/tests/e2e_tests/utils/test_utils.py index 9061df791..73838b3a2 100644 --- a/tests/e2e_tests/utils/test_utils.py +++ b/tests/e2e_tests/utils/test_utils.py @@ -25,7 +25,7 @@ def setup_wallet(uri: str) -> Tuple[Keypair, bittensor.wallet]: - Sets keys in the wallet without encryption and with overwriting enabled. """ keypair = Keypair.create_from_uri(uri) - wallet_path = "/tmp/btcli-e2e-wallet-{}".format(uri.strip("/")) + wallet_path = f"/tmp/btcli-e2e-wallet-{uri.strip('/')}" wallet = bittensor.wallet(path=wallet_path) wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True) wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index f6b101ac6..4b4609921 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -87,12 +87,12 @@ def test_init(setup_dendrite): def test_str(dendrite_obj): - expected_string = "dendrite({})".format(dendrite_obj.keypair.ss58_address) + expected_string = f"dendrite({dendrite_obj.keypair.ss58_address})" assert str(dendrite_obj) == expected_string def test_repr(dendrite_obj): - expected_string = "dendrite({})".format(dendrite_obj.keypair.ss58_address) + expected_string = f"dendrite({dendrite_obj.keypair.ss58_address})" assert repr(dendrite_obj) == expected_string From b0bc7ad1ffb7483e479ac1f008237e052ff66eba Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Aug 2024 11:29:29 -0700 Subject: [PATCH 090/260] ruff --- bittensor/utils/deprecated/extrinsics/transfer.py | 4 +++- bittensor/utils/version.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/deprecated/extrinsics/transfer.py b/bittensor/utils/deprecated/extrinsics/transfer.py index 0b7f2bddd..7f7decccf 100644 --- a/bittensor/utils/deprecated/extrinsics/transfer.py +++ b/bittensor/utils/deprecated/extrinsics/transfer.py @@ -141,7 +141,9 @@ def transfer_extrinsic( if success: with bt_console.status(":satellite: Checking Balance..."): new_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - bt_console.print(f"Balance:\n [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]") + bt_console.print( + f"Balance:\n [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) return True return False diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index 6eb600e56..f0ff70763 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -105,7 +105,8 @@ def check_version(timeout: int = 15): print( f"\u001b[33mBittensor Version: Current {__version__}/Latest {latest_version}\n" f"Please update to the latest version at your earliest convenience. " - "Run the following command to upgrade:\n\n\u001b[0mpython -m pip install --upgrade bittensor") + "Run the following command to upgrade:\n\n\u001b[0mpython -m pip install --upgrade bittensor" + ) pass except Exception as e: raise VersionCheckError("Version check failed") from e From 4e44f132339209deb9c6d060ed043ded662cee4c Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Aug 2024 13:09:45 -0700 Subject: [PATCH 091/260] Optimize imports --- bittensor/__init__.py | 2 +- bittensor/core/chain_data.py | 4 +- bittensor/core/config.py | 9 ++-- bittensor/core/settings.py | 5 +-- bittensor/core/subtensor.py | 41 +++++++++---------- bittensor/core/tensor.py | 8 ++-- bittensor/core/threadpool.py | 17 ++++---- bittensor/utils/__init__.py | 1 - bittensor/utils/btlogging/format.py | 1 - bittensor/utils/btlogging/loggingmachine.py | 1 - .../deprecated/extrinsics/set_weights.py | 2 +- .../utils/deprecated/extrinsics/transfer.py | 2 +- bittensor/utils/version.py | 3 +- 13 files changed, 46 insertions(+), 50 deletions(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index b131d13f0..cf2cb8ce7 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -19,8 +19,8 @@ import warnings from .core.settings import __version__, version_split, DEFAULTS -from .utils.deprecated import * from .utils.btlogging import logging +from .utils.deprecated import * def __getattr__(name): diff --git a/bittensor/core/chain_data.py b/bittensor/core/chain_data.py index 6d4e6103c..b01dcc665 100644 --- a/bittensor/core/chain_data.py +++ b/bittensor/core/chain_data.py @@ -30,11 +30,11 @@ from scalecodec.types import GenericCall from scalecodec.utils.ss58 import ss58_encode -from .settings import SS58_FORMAT from bittensor.utils import networking as net, RAOPERTAO, u16_normalized_float from bittensor.utils.balance import Balance -from bittensor.utils.registration import torch, use_torch from bittensor.utils.btlogging import logging +from bittensor.utils.registration import torch, use_torch +from .settings import SS58_FORMAT custom_rpc_type_registry = { "types": { diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 61afb5f05..9bfa25f67 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -20,14 +20,15 @@ # 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 copy import os import sys -import yaml -import copy from copy import deepcopy -from munch import DefaultMunch from typing import List, Optional, Dict, Any, TypeVar, Type -import argparse + +import yaml +from munch import DefaultMunch class InvalidConfigFile(Exception): diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 254868b7f..cc6ab0769 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -20,11 +20,10 @@ import os import re from pathlib import Path -from rich.console import Console -from rich.traceback import install from munch import munchify - +from rich.console import Console +from rich.traceback import install # Rich console. __console__ = Console() diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index f50d449d7..73dfaa334 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -37,6 +37,26 @@ from scalecodec.types import ScaleType from substrateinterface.base import QueryMapResult, SubstrateInterface +from bittensor.core import settings +from bittensor.core.axon import Axon +from bittensor.core.chain_data import ( + NeuronInfo, + PrometheusInfo, + SubnetHyperparameters, + NeuronInfoLite, + custom_rpc_type_registry, +) +from bittensor.core.config import Config +from bittensor.core.metagraph import Metagraph +from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams +from bittensor.utils import torch, format_error_message +from bittensor.utils import u16_normalized_float, networking +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from bittensor.utils.deprecated.extrinsics.commit_weights import ( + commit_weights_extrinsic, + reveal_weights_extrinsic, +) from bittensor.utils.deprecated.extrinsics.prometheus import ( prometheus_extrinsic, ) @@ -49,31 +69,10 @@ from bittensor.utils.deprecated.extrinsics.set_weights import ( set_weights_extrinsic, ) -from bittensor.utils.deprecated.extrinsics.commit_weights import ( - commit_weights_extrinsic, - reveal_weights_extrinsic, -) from bittensor.utils.deprecated.extrinsics.transfer import ( transfer_extrinsic, ) from bittensor.utils.weight_utils import generate_weight_hash -from bittensor.core import settings -from bittensor.core.axon import Axon -from bittensor.core.chain_data import ( - NeuronInfo, - PrometheusInfo, - SubnetHyperparameters, - NeuronInfoLite, - custom_rpc_type_registry, -) -from bittensor.core.config import Config -from bittensor.core.metagraph import Metagraph -from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams -from bittensor.utils import u16_normalized_float, networking -from bittensor.utils import torch, format_error_message -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging - KEY_NONCE: Dict[str, int] = {} diff --git a/bittensor/core/tensor.py b/bittensor/core/tensor.py index e50947e71..c3bb29edb 100644 --- a/bittensor/core/tensor.py +++ b/bittensor/core/tensor.py @@ -15,14 +15,16 @@ # 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 numpy as np import base64 +from typing import Optional, Union, List + import msgpack import msgpack_numpy -from typing import Optional, Union, List -from bittensor.utils.registration import torch, use_torch +import numpy as np from pydantic import ConfigDict, BaseModel, Field, field_validator +from bittensor.utils.registration import torch, use_torch + class DTypes(dict): def __init__(self, *args, **kwargs): diff --git a/bittensor/core/threadpool.py b/bittensor/core/threadpool.py index 4a8b1d302..17e553509 100644 --- a/bittensor/core/threadpool.py +++ b/bittensor/core/threadpool.py @@ -5,23 +5,22 @@ __author__ = "Brian Quinlan (brian@sweetapp.com)" +import argparse +import itertools +import logging import os -import sys -import time import queue import random -import weakref -import logging -import argparse -import itertools +import sys import threading - -from typing import Callable +import time +import weakref from concurrent.futures import _base +from typing import Callable from bittensor.core.config import Config -from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME from bittensor.core.settings import BLOCKTIME +from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME # Workers are created as daemon threads. This is done to allow the interpreter # to exit when there are still idle threads in a ThreadPoolExecutor's thread diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 3089aff70..2423b6b39 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -27,7 +27,6 @@ from .registration import torch, use_torch from .version import version_checking, check_version, VersionCheckError - RAOPERTAO = 1e9 U16_MAX = 65535 U64_MAX = 18446744073709551615 diff --git a/bittensor/utils/btlogging/format.py b/bittensor/utils/btlogging/format.py index 131085f17..cbde7e9eb 100644 --- a/bittensor/utils/btlogging/format.py +++ b/bittensor/utils/btlogging/format.py @@ -27,7 +27,6 @@ from colorama import init, Fore, Back, Style - init(autoreset=True) TRACE_LEVEL_NUM: int = 5 diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index adbb4e929..b9e0408ce 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -34,7 +34,6 @@ from statemachine import State, StateMachine from bittensor.core.config import Config - from .defines import ( BITTENSOR_LOGGER_NAME, DATE_FORMAT, diff --git a/bittensor/utils/deprecated/extrinsics/set_weights.py b/bittensor/utils/deprecated/extrinsics/set_weights.py index 15681dbb1..f62a23d44 100644 --- a/bittensor/utils/deprecated/extrinsics/set_weights.py +++ b/bittensor/utils/deprecated/extrinsics/set_weights.py @@ -23,8 +23,8 @@ from numpy.typing import NDArray from rich.prompt import Confirm -from bittensor.utils import weight_utils from bittensor.core.settings import bt_console +from bittensor.utils import weight_utils from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch diff --git a/bittensor/utils/deprecated/extrinsics/transfer.py b/bittensor/utils/deprecated/extrinsics/transfer.py index 7f7decccf..3f6c964d9 100644 --- a/bittensor/utils/deprecated/extrinsics/transfer.py +++ b/bittensor/utils/deprecated/extrinsics/transfer.py @@ -17,13 +17,13 @@ from typing import Union, TYPE_CHECKING +from bittensor_wallet import Wallet from rich.prompt import Confirm from bittensor.core.settings import bt_console, NETWORK_EXPLORER_MAP from bittensor.utils import get_explorer_url_for_network from bittensor.utils import is_valid_bittensor_address_or_public_key from bittensor.utils.balance import Balance -from bittensor_wallet import Wallet # For annotation purposes if TYPE_CHECKING: diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index f0ff70763..8ffd969cf 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -22,8 +22,7 @@ import requests from packaging.version import Version -from bittensor.core.settings import __version__ -from bittensor.core.settings import PIPADDRESS +from bittensor.core.settings import __version__, PIPADDRESS from bittensor.utils.btlogging import logging VERSION_CHECK_THRESHOLD = 86400 From 0e8b230af7688f0f373f461a06bd42358e14db29 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Aug 2024 13:14:48 -0700 Subject: [PATCH 092/260] Add the opportunity to run `python - m bittensor` --- bittensor/__main__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 bittensor/__main__.py diff --git a/bittensor/__main__.py b/bittensor/__main__.py new file mode 100644 index 000000000..05d664c9d --- /dev/null +++ b/bittensor/__main__.py @@ -0,0 +1,21 @@ +# 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. + +from bittensor import __version__ + +if __name__ == "__main__": + print(f"Bittensor SDK version: {__version__}") From 7791b2d146393ccdcd1a0b5db2283b2396f76f73 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 13 Aug 2024 13:21:18 -0700 Subject: [PATCH 093/260] replace deps link from https to ssh --- requirements/prod.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/prod.txt b/requirements/prod.txt index 4ef4a6f5e..854f30e3e 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -33,4 +33,4 @@ termcolor tqdm uvicorn wheel -git+https://github.com/opentensor/btwallet.git@main#egg=bittensor-wallet +git+ssh://git@github.com/opentensor/btwallet.git@main#egg=bittensor-wallet From 93e0afd9cecdda07450b32b65c7a854a5c436fac Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 13 Aug 2024 16:58:25 -0700 Subject: [PATCH 094/260] Fixes metagraph save/load for neurons, added e2e tests (metagraph + extrinsics) --- bittensor/core/metagraph.py | 4 + tests/e2e_tests/conftest.py | 2 +- tests/e2e_tests/test_commit_weights.py | 6 +- tests/e2e_tests/test_liquid_alpha.py | 7 +- tests/e2e_tests/test_metagraph.py | 150 ++++++++++++++++++ tests/e2e_tests/test_subtensor_extrinsics.py | 152 +++++++++++++++++++ 6 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 tests/e2e_tests/test_metagraph.py create mode 100644 tests/e2e_tests/test_subtensor_extrinsics.py diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 8fee673ff..66e5788ad 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -475,6 +475,7 @@ def state_dict(self): "bonds": self.bonds, "uids": self.uids, "axons": self.axons, + "neurons": self.neurons, } def sync( @@ -791,6 +792,7 @@ def save(self) -> "Metagraph": graph_filename = f"{save_directory}/block-{self.block.item()}.pt" state_dict = self.state_dict() state_dict["axons"] = self.axons + state_dict["neurons"] = self.neurons torch.save(state_dict, graph_filename) torch.load(graph_filename) # verifies that the file can be loaded correctly else: @@ -1034,6 +1036,7 @@ def load_from_path(self, dir_path: str) -> "Metagraph": ) self.uids = torch.nn.Parameter(state_dict["uids"], requires_grad=False) self.axons = state_dict["axons"] + self.neurons = state_dict["neurons"] if "weights" in state_dict: self.weights = torch.nn.Parameter( state_dict["weights"], requires_grad=False @@ -1176,6 +1179,7 @@ def load_from_path(self, dir_path: str) -> "Metagraph": self.last_update = state_dict["last_update"] self.validator_permit = state_dict["validator_permit"] self.axons = state_dict["axons"] + self.neurons = state_dict["neurons"] if "weights" in state_dict: self.weights = state_dict["weights"] if "bonds" in state_dict: diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 710599190..9fc9faec6 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -22,7 +22,7 @@ def local_chain(request): param = request.param if hasattr(request, "param") else None # Get the environment variable for the script path - script_path = "/Users/ibraheem/Desktop/Bittensor/subtensor/scripts/localnet.sh" + script_path = os.getenv("LOCALNET_SH_PATH") if not script_path: # Skip the test if the localhost.sh path is not set diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index 3e451f9ee..c5db48b0d 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -81,6 +81,9 @@ async def test_commit_and_reveal_weights(local_chain): == 370 ), "Failed to set commit/reveal interval" + assert ( + subtensor.weights_rate_limit(netuid=netuid) > 0 + ), "Weights rate limit is below 0" # Lower the rate limit assert sudo_set_hyperparameter_values( local_chain, @@ -91,8 +94,9 @@ async def test_commit_and_reveal_weights(local_chain): ) subtensor = bittensor.subtensor(network="ws://localhost:9945") assert ( - subtensor.get_subnet_hyperparameters(netuid=1).weights_rate_limit == 0 + subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 ), "Failed to set weights_rate_limit" + assert subtensor.weights_rate_limit(netuid=netuid) == 0 # Commit-reveal values uids = np.array([0], dtype=np.int64) diff --git a/tests/e2e_tests/test_liquid_alpha.py b/tests/e2e_tests/test_liquid_alpha.py index 371a908e4..f85cde44e 100644 --- a/tests/e2e_tests/test_liquid_alpha.py +++ b/tests/e2e_tests/test_liquid_alpha.py @@ -44,10 +44,9 @@ def test_liquid_alpha(local_chain): assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() # Register a neuron to the subnet - ( - register_neuron(local_chain, alice_wallet, netuid), - "Unable to register Alice as a neuron", - ) + assert register_neuron( + local_chain, alice_wallet, netuid + ), "Unable to register Alice as a neuron" # Stake to become to top neuron after the first epoch add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(100_000)) diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py new file mode 100644 index 000000000..fac8e5752 --- /dev/null +++ b/tests/e2e_tests/test_metagraph.py @@ -0,0 +1,150 @@ +import time + +import bittensor +from bittensor import logging +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, +) +from tests.e2e_tests.utils.test_utils import ( + setup_wallet, +) + + +def neuron_to_dict(neuron): + """ + Convert a neuron object to a dictionary, excluding private attributes, methods, and specific fields. + Returns: + dict: A dictionary of the neuron's public attributes. + + Note: + Excludes 'weights' and 'bonds' fields. These are present in subtensor + but not in metagraph + """ + excluded_fields = {"weights", "bonds"} + return { + attr: getattr(neuron, attr) + for attr in dir(neuron) + if not attr.startswith("_") + and not callable(getattr(neuron, attr)) + and attr not in excluded_fields + } + + +def test_metagraph(local_chain): + """ + Tests the metagraph + + Steps: + 1. Register a subnet through Alice + 2. Assert metagraph's initial state + 3. Register Bob and validate info in metagraph + 4. Fetch neuron info of Bob through subtensor & metagraph and verify + 5. Register Dave and validate info in metagraph + 6. Verify low balance stake fails & add stake thru Bob and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.info("Testing test_metagraph_command") + netuid = 1 + + # Register Alice, Bob, and Dave + alice_keypair, alice_wallet = setup_wallet("//Alice") + bob_keypair, bob_wallet = setup_wallet("//Bob") + dave_keypair, dave_wallet = setup_wallet("//Dave") + + # Register the subnet through Alice + register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Verify subnet was created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet wasn't created successfully" + + # Initialize metagraph + subtensor = bittensor.subtensor(network="ws://localhost:9945") + metagraph = subtensor.metagraph(netuid=1) + + # Assert metagraph is empty + assert len(metagraph.uids) == 0, "Metagraph is not empty" + + # Register Bob to the subnet + assert register_neuron( + local_chain, bob_wallet, netuid + ), "Unable to register Bob as a neuron" + + # Refresh the metagraph + metagraph.sync(subtensor=subtensor) + + # Assert metagraph has Bob neuron + assert len(metagraph.uids) == 1, "Metagraph doesn't have exactly 1 neuron" + assert metagraph.hotkeys[0] == bob_keypair.ss58_address + assert len(metagraph.coldkeys) == 1 + assert metagraph.n.max() == 1 + assert metagraph.n.min() == 1 + assert len(metagraph.addresses) == 1 + + # Fetch UID of Bob + uid = subtensor.get_uid_for_hotkey_on_subnet( + bob_keypair.ss58_address, netuid=netuid + ) + + # Fetch neuron info of Bob through subtensor and metagraph + neuron_info_bob = subtensor.neuron_for_uid(uid, netuid=netuid) + metagraph_dict = neuron_to_dict(metagraph.neurons[uid]) + subtensor_dict = neuron_to_dict(neuron_info_bob) + + # Verify neuron info is the same in both objects + assert ( + metagraph_dict == subtensor_dict + ), "Neuron info of Bob doesn't match b/w metagraph & subtensor" + + # Create pre_dave metagraph for future verifications + metagraph_pre_dave = subtensor.metagraph(netuid=1) + + # Register Dave as a neuron + assert register_neuron( + local_chain, dave_wallet, netuid + ), "Unable to register Dave as a neuron" + + metagraph.sync(subtensor=subtensor) + + # Assert metagraph now includes Dave's neuron + assert len(metagraph.uids) == 2 + assert ( + metagraph.hotkeys[1] == dave_keypair.ss58_address + ), "Neuron's hotkey in metagraph doesn't match" + assert len(metagraph.coldkeys) == 2 + assert metagraph.n.max() == 2 + assert metagraph.n.min() == 2 + assert len(metagraph.addresses) == 2 + + # Test staking with low balance + assert not add_stake(local_chain, dave_wallet, bittensor.Balance.from_tao(10_000)) + + # Add stake by Bob + assert add_stake(local_chain, bob_wallet, bittensor.Balance.from_tao(10_000)) + + # Assert stake is added after updating metagraph + metagraph.sync(subtensor=subtensor) + assert metagraph.neurons[0].stake == bittensor.Balance.from_tao(10_000) + + # Test the save() and load() mechanism + # We save the metagraph and pre_dave loads it + metagraph.save() + time.sleep(3) + metagraph_pre_dave.load() + + # Ensure data is synced between two metagraphs + assert len(metagraph.uids) == len(metagraph_pre_dave.uids) + assert (metagraph.uids == metagraph_pre_dave.uids).all() + + assert len(metagraph.axons) == len(metagraph_pre_dave.axons) + assert metagraph.axons[1].hotkey == metagraph_pre_dave.axons[1].hotkey + assert metagraph.axons == metagraph_pre_dave.axons + + assert len(metagraph.neurons) == len(metagraph_pre_dave.neurons) + assert metagraph.neurons == metagraph_pre_dave.neurons + + logging.info("✅ Passed test_metagraph") diff --git a/tests/e2e_tests/test_subtensor_extrinsics.py b/tests/e2e_tests/test_subtensor_extrinsics.py new file mode 100644 index 000000000..e9bf86389 --- /dev/null +++ b/tests/e2e_tests/test_subtensor_extrinsics.py @@ -0,0 +1,152 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import logging +from tests.e2e_tests.utils.chain_interactions import ( + register_neuron, + register_subnet, +) +from tests.e2e_tests.utils.test_utils import ( + setup_wallet, + template_path, + templates_repo, +) + + +@pytest.mark.asyncio +async def test_subtensor_extrinsics(local_chain): + """ + Tests subtensor extrinsics + + Steps: + 1. Validate subnets in the chain before/after registering netuid = 1 + 2. Register Alice's neuron + 3. Verify Alice and Bob's participation in subnets (individually and global) + 4. Verify uids of Alice and Bob gets populated correctly + 5. Start Alice as a validator and verify neuroninfo before/after is different + Raises: + AssertionError: If any of the checks or verifications fail + """ + netuid = 1 + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + # Subnets 0 and 3 are bootstrapped from the start + assert subtensor.get_subnets() == [0, 3] + assert subtensor.get_total_subnets() == 2 + + # Add wallets for Alice and Bob + alice_keypair, alice_wallet = setup_wallet("//Alice") + bob_keypair, bob_wallet = setup_wallet("//Bob") + + # Register subnet + register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Subnet 1 is added after registration + assert subtensor.get_subnets() == [0, 1, 3] + assert subtensor.get_total_subnets() == 3 + + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + assert subtensor.subnet_exists(netuid) + + # Register Alice to the subnet + assert register_neuron( + local_chain, alice_wallet, netuid + ), "Unable to register Alice as a neuron" + + # Verify Alice is registered to netuid 1 and Bob isn't registered to any + assert subtensor.get_netuids_for_hotkey(hotkey_ss58=alice_keypair.ss58_address) == [ + 1 + ], "Alice is not registered to netuid 1 as expected" + assert ( + subtensor.get_netuids_for_hotkey(hotkey_ss58=bob_keypair.ss58_address) == [] + ), "Bob is unexpectedly registered to some netuid" + + # Verify Alice's hotkey is registered to any subnet (currently netuid = 1) + assert subtensor.is_hotkey_registered_any( + hotkey_ss58=alice_keypair.ss58_address + ), "Alice's hotkey is not registered to any subnet" + assert not subtensor.is_hotkey_registered_any( + hotkey_ss58=bob_keypair.ss58_address + ), "Bob's hotkey is unexpectedly registered to a subnet" + + # Verify netuid = 1 only has Alice registered and not Bob + assert subtensor.is_hotkey_registered_on_subnet( + netuid=netuid, hotkey_ss58=alice_keypair.ss58_address + ), "Alice's hotkey is not registered on netuid 1" + assert not subtensor.is_hotkey_registered_on_subnet( + netuid=netuid, hotkey_ss58=bob_keypair.ss58_address + ), "Bob's hotkey is unexpectedly registered on netuid 1" + + # Verify Alice's UID on netuid 1 is 0 + assert ( + subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=alice_keypair.ss58_address, netuid=netuid + ) + == 0 + ), "UID for Alice's hotkey on netuid 1 is not 0 as expected" + + # Register Bob to the subnet + assert register_neuron( + local_chain, bob_wallet, netuid + ), "Unable to register Alice as a neuron" + + # Verify Bob's UID on netuid 1 is 1 + assert ( + subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=bob_keypair.ss58_address, netuid=netuid + ) + == 1 + ), "UID for Bob's hotkey on netuid 1 is not 1 as expected" + + neuron_info_old = subtensor.get_neuron_for_pubkey_and_subnet( + alice_keypair.ss58_address, netuid=netuid + ) + + # Prepare to run Alice as validator + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/validator.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + alice_wallet.path, + "--wallet.name", + alice_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + + # Run Alice as validator in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Alice is now validating") + + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + # Verify neuron info is updated after running as a validator + neuron_info = subtensor.get_neuron_for_pubkey_and_subnet( + alice_keypair.ss58_address, netuid=netuid + ) + assert ( + neuron_info_old.axon_info != neuron_info.axon_info + ), "Neuron info not updated after running validator" + + logging.info("✅ Passed test_subtensor_extrinsics") From 94906ea2652735ab2e057a2257dff18e307f2401 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 13 Aug 2024 17:03:32 -0700 Subject: [PATCH 095/260] Renamed test --- .../{test_subtensor_extrinsics.py => test_subtensor_functions.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/e2e_tests/{test_subtensor_extrinsics.py => test_subtensor_functions.py} (100%) diff --git a/tests/e2e_tests/test_subtensor_extrinsics.py b/tests/e2e_tests/test_subtensor_functions.py similarity index 100% rename from tests/e2e_tests/test_subtensor_extrinsics.py rename to tests/e2e_tests/test_subtensor_functions.py From 5470afc4331615acdb965769279020d7ada47a53 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 13 Aug 2024 17:23:57 -0700 Subject: [PATCH 096/260] Adds missing statements --- tests/e2e_tests/test_metagraph.py | 67 ++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index fac8e5752..ca27acd3a 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -43,6 +43,7 @@ def test_metagraph(local_chain): 4. Fetch neuron info of Bob through subtensor & metagraph and verify 5. Register Dave and validate info in metagraph 6. Verify low balance stake fails & add stake thru Bob and verify + 7. Load pre_dave metagraph from latest save and verify both instances Raises: AssertionError: If any of the checks or verifications fail """ @@ -79,11 +80,13 @@ def test_metagraph(local_chain): # Assert metagraph has Bob neuron assert len(metagraph.uids) == 1, "Metagraph doesn't have exactly 1 neuron" - assert metagraph.hotkeys[0] == bob_keypair.ss58_address - assert len(metagraph.coldkeys) == 1 - assert metagraph.n.max() == 1 - assert metagraph.n.min() == 1 - assert len(metagraph.addresses) == 1 + assert ( + metagraph.hotkeys[0] == bob_keypair.ss58_address + ), "Bob's hotkey doesn't match in metagraph" + assert len(metagraph.coldkeys) == 1, "Metagraph doesn't have exactly 1 coldkey" + assert metagraph.n.max() == 1, "Metagraph's max n is not 1" + assert metagraph.n.min() == 1, "Metagraph's min n is not 1" + assert len(metagraph.addresses) == 1, "Metagraph doesn't have exactly 1 address" # Fetch UID of Bob uid = subtensor.get_uid_for_hotkey_on_subnet( @@ -111,24 +114,34 @@ def test_metagraph(local_chain): metagraph.sync(subtensor=subtensor) # Assert metagraph now includes Dave's neuron - assert len(metagraph.uids) == 2 + assert ( + len(metagraph.uids) == 2 + ), "Metagraph doesn't have exactly 2 neurons post Dave" assert ( metagraph.hotkeys[1] == dave_keypair.ss58_address ), "Neuron's hotkey in metagraph doesn't match" - assert len(metagraph.coldkeys) == 2 - assert metagraph.n.max() == 2 - assert metagraph.n.min() == 2 - assert len(metagraph.addresses) == 2 + assert ( + len(metagraph.coldkeys) == 2 + ), "Metagraph doesn't have exactly 2 coldkeys post Dave" + assert metagraph.n.max() == 2, "Metagraph's max n is not 2 post Dave" + assert metagraph.n.min() == 2, "Metagraph's min n is not 2 post Dave" + assert len(metagraph.addresses) == 2, "Metagraph doesn't have 2 addresses post Dave" # Test staking with low balance - assert not add_stake(local_chain, dave_wallet, bittensor.Balance.from_tao(10_000)) + assert not add_stake( + local_chain, dave_wallet, bittensor.Balance.from_tao(10_000) + ), "Low balance stake should fail" # Add stake by Bob - assert add_stake(local_chain, bob_wallet, bittensor.Balance.from_tao(10_000)) + assert add_stake( + local_chain, bob_wallet, bittensor.Balance.from_tao(10_000) + ), "Failed to add stake for Bob" # Assert stake is added after updating metagraph metagraph.sync(subtensor=subtensor) - assert metagraph.neurons[0].stake == bittensor.Balance.from_tao(10_000) + assert metagraph.neurons[0].stake == bittensor.Balance.from_tao( + 10_000 + ), "Bob's stake not updated in metagraph" # Test the save() and load() mechanism # We save the metagraph and pre_dave loads it @@ -137,14 +150,28 @@ def test_metagraph(local_chain): metagraph_pre_dave.load() # Ensure data is synced between two metagraphs - assert len(metagraph.uids) == len(metagraph_pre_dave.uids) - assert (metagraph.uids == metagraph_pre_dave.uids).all() + assert len(metagraph.uids) == len( + metagraph_pre_dave.uids + ), "UID count mismatch after save and load" + assert ( + metagraph.uids == metagraph_pre_dave.uids + ).all(), "UIDs don't match after save and load" - assert len(metagraph.axons) == len(metagraph_pre_dave.axons) - assert metagraph.axons[1].hotkey == metagraph_pre_dave.axons[1].hotkey - assert metagraph.axons == metagraph_pre_dave.axons + assert len(metagraph.axons) == len( + metagraph_pre_dave.axons + ), "Axon count mismatch after save and load" + assert ( + metagraph.axons[1].hotkey == metagraph_pre_dave.axons[1].hotkey + ), "Axon hotkey mismatch after save and load" + assert ( + metagraph.axons == metagraph_pre_dave.axons + ), "Axons don't match after save and load" - assert len(metagraph.neurons) == len(metagraph_pre_dave.neurons) - assert metagraph.neurons == metagraph_pre_dave.neurons + assert len(metagraph.neurons) == len( + metagraph_pre_dave.neurons + ), "Neuron count mismatch after save and load" + assert ( + metagraph.neurons == metagraph_pre_dave.neurons + ), "Neurons don't match after save and load" logging.info("✅ Passed test_metagraph") From 8022866c75fa18ac757667aed7cd78437b2d5977 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 14 Aug 2024 09:22:44 -0700 Subject: [PATCH 097/260] Applies fix and assertions --- bittensor/metagraph.py | 4 ++++ tests/e2e_tests/subcommands/subnet/test_metagraph.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bittensor/metagraph.py b/bittensor/metagraph.py index 8d7e97bcc..eb60ab775 100644 --- a/bittensor/metagraph.py +++ b/bittensor/metagraph.py @@ -467,6 +467,7 @@ def state_dict(self): "bonds": self.bonds, "uids": self.uids, "axons": self.axons, + "neurons": self.neurons, } def sync( @@ -782,6 +783,7 @@ def save(self) -> "metagraph": # type: ignore graph_filename = f"{save_directory}/block-{self.block.item()}.pt" state_dict = self.state_dict() state_dict["axons"] = self.axons + state_dict["neurons"] = self.neurons torch.save(state_dict, graph_filename) state_dict = torch.load( graph_filename @@ -1029,6 +1031,7 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore ) self.uids = torch.nn.Parameter(state_dict["uids"], requires_grad=False) self.axons = state_dict["axons"] + self.neurons = state_dict["neurons"] if "weights" in state_dict: self.weights = torch.nn.Parameter( state_dict["weights"], requires_grad=False @@ -1173,6 +1176,7 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore self.last_update = state_dict["last_update"] self.validator_permit = state_dict["validator_permit"] self.axons = state_dict["axons"] + self.neurons = state_dict["neurons"] if "weights" in state_dict: self.weights = state_dict["weights"] if "bonds" in state_dict: diff --git a/tests/e2e_tests/subcommands/subnet/test_metagraph.py b/tests/e2e_tests/subcommands/subnet/test_metagraph.py index e8e18ef61..a77876c2b 100644 --- a/tests/e2e_tests/subcommands/subnet/test_metagraph.py +++ b/tests/e2e_tests/subcommands/subnet/test_metagraph.py @@ -80,9 +80,8 @@ def test_metagraph_command(local_chain, capsys): captured = capsys.readouterr() # Assert the neuron is registered and displayed - assert ( - "Metagraph: net: local:1" and "N: 1/1" in captured.out - ), "Neuron isn't displayed in metagraph" + assert "Metagraph: net: local:1" in captured.out + assert "N: 1/1" in captured.out, "Neuron isn't displayed in metagraph" # Register Dave as neuron to the subnet dave_keypair, dave_exec_command, dave_wallet = setup_wallet("//Dave") @@ -117,6 +116,7 @@ def test_metagraph_command(local_chain, capsys): captured = capsys.readouterr() # Assert the neuron is registered and displayed - assert "Metagraph: net: local:1" and "N: 2/2" in captured.out + assert "Metagraph: net: local:1" in captured.out + assert "N: 2/2" in captured.out logging.info("Passed test_metagraph_command") From 96badebd8b13e18fc20b16f2db6bae777f0b27a9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 15 Aug 2024 10:22:31 -0700 Subject: [PATCH 098/260] Debug commit --- tests/e2e_tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 4575ff298..540a303f0 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -34,8 +34,9 @@ def local_chain(request): # Check if param is None, and handle it accordingly args = "" if param is None else f"{param}" - # compile commands to send to process + # Compile commands to send to process cmds = shlex.split(f"{script_path} {args}") + # Start new node process process = subprocess.Popen( cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid From 40512c897952c9bdff79ee2935776fc86291ce3a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 15 Aug 2024 16:32:54 -0700 Subject: [PATCH 099/260] Dummy commit --- tests/e2e_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 540a303f0..3d8ab25b8 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -81,6 +81,6 @@ def wait_for_node_start(process, pattern): # Ensure the process has terminated process.wait() - # uninstall templates + # Uninstall templates logging.info("uninstalling neuron templates") uninstall_templates(template_path) From 6bf30eb7515a0e8ae0916e2e94771d3007e87283 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 19 Aug 2024 20:37:52 -0700 Subject: [PATCH 100/260] increase subtensor.py test coverage until 80% --- bittensor/core/subtensor.py | 6 +- tests/unit_tests/test_subtensor.py | 377 +++++++++++++++++++++++++++++ 2 files changed, 382 insertions(+), 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 73dfaa334..0ec910a5c 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1839,6 +1839,7 @@ def get_existential_deposit( return None return Balance.from_rao(result.value) + # Community uses this method def commit_weights( self, wallet: "Wallet", @@ -1915,6 +1916,7 @@ def commit_weights( return success, message + # Community uses this method def _do_commit_weights( self, wallet: "Wallet", @@ -1968,10 +1970,11 @@ def make_substrate_call_with_retry(): if response.is_success: return True, None else: - return False, response.error_message + return False, format_error_message(response.error_message) return make_substrate_call_with_retry() + # Community uses this method def reveal_weights( self, wallet: "Wallet", @@ -2036,6 +2039,7 @@ def reveal_weights( return success, message + # Community uses this method def _do_reveal_weights( self, wallet: "Wallet", diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 8c9ace9b8..b6c2fae10 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -26,6 +26,7 @@ from bittensor.core import subtensor as subtensor_module, settings from bittensor.core.axon import Axon from bittensor.core.chain_data import SubnetHyperparameters +from bittensor.core.settings import version_as_int from bittensor.core.subtensor import Subtensor, logging from bittensor.utils import u16_normalized_float, u64_normalized_float from bittensor.utils.balance import Balance @@ -861,3 +862,379 @@ def test_get_subnet_hyperparameters_hex_without_prefix(mocker, subtensor): subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( bytes_result ) + + +def test_get_commitment(subtensor, mocker): + """ Successful get_commitment call.""" + # Preps + fake_netuid = 1 + fake_uid = 2 + fake_block = 3 + fake_hotkey = "hotkey" + fake_hex_data = "0x010203" + expected_result = bytes.fromhex(fake_hex_data[2:]).decode() + + mocked_metagraph = mocker.MagicMock() + subtensor.metagraph = mocked_metagraph + mocked_metagraph.return_value.hotkeys = {fake_uid: fake_hotkey} + + mocked_get_metadata = mocker.patch.object(subtensor_module, "get_metadata") + mocked_get_metadata.return_value = {"info": {"fields": [{fake_hex_data: fake_hex_data}]}} + + # Call + result = subtensor.get_commitment( + netuid=fake_netuid, + uid=fake_uid, + block=fake_block + ) + + # Assertions + mocked_metagraph.assert_called_once_with(fake_netuid) + assert result == expected_result + + +def test_min_allowed_weights(subtensor, mocker): + """Successful min_allowed_weights call.""" + fake_netuid = 1 + fake_block = 123 + return_value = 10 + + mocked_get_hyperparameter = mocker.MagicMock(return_value=return_value) + subtensor._get_hyperparameter = mocked_get_hyperparameter + + # Call + result = subtensor.min_allowed_weights(netuid=fake_netuid, block=fake_block) + + # Assertion + mocked_get_hyperparameter.assert_called_once_with( + param_name="MinAllowedWeights", block=fake_block, netuid=fake_netuid + ) + assert result == return_value + + +def test_max_weight_limit(subtensor, mocker): + """Successful max_weight_limit call.""" + fake_netuid = 1 + fake_block = 123 + return_value = 100 + + mocked_get_hyperparameter = mocker.MagicMock(return_value=return_value) + subtensor._get_hyperparameter = mocked_get_hyperparameter + + mocked_u16_normalized_float = mocker.MagicMock() + subtensor_module.u16_normalized_float = mocked_u16_normalized_float + + # Call + result = subtensor.max_weight_limit(netuid=fake_netuid, block=fake_block) + + # Assertion + mocked_get_hyperparameter.assert_called_once_with( + param_name="MaxWeightsLimit", block=fake_block, netuid=fake_netuid + ) + assert result == mocked_u16_normalized_float.return_value + + +def test_get_transfer_fee(subtensor, mocker): + """Successful get_transfer_fee call.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_dest = "SS58ADDRESS" + value = 1 + + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + fake_payment_info = {"partialFee": int(2e10)} + mocked_substrate.get_payment_info.return_value = fake_payment_info + + # Call + result = subtensor.get_transfer_fee(wallet=fake_wallet, dest=fake_dest, value=value) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": fake_dest, "value": value}, + ) + + mocked_substrate.get_payment_info.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.coldkeypub + ) + + assert result == 2e10 + + +def test_get_transfer_fee_incorrect_value(subtensor, mocker): + """ Successful get_transfer_fee call.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_dest = mocker.MagicMock() + value = "no_int_no_float_no_Balance" + + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + spy_balance_from_rao = mocker.spy(Balance, "from_rao") + + # Call + result = subtensor.get_transfer_fee(wallet=fake_wallet, dest=fake_dest, value=value) + + # Asserts + spy_balance_from_rao.assert_called_once_with(2e7) + + assert result == Balance.from_rao(int(2e7)) + + +def test_get_existential_deposit(subtensor, mocker): + """ Successful get_existential_deposit call.""" + # Prep + block = 123 + + mocked_query_constant = mocker.MagicMock() + value = 10 + mocked_query_constant.return_value.value = value + subtensor.query_constant = mocked_query_constant + + # Call + result = subtensor.get_existential_deposit(block=block) + + # Assertions + mocked_query_constant.assert_called_once_with( + module_name="Balances", + constant_name="ExistentialDeposit", + block=block) + + assert isinstance(result, Balance) + assert result == Balance.from_rao(value) + + +def test_commit_weights(subtensor, mocker): + """Successful commit_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + netuid = 1 + salt = [1, 3] + uids = [2, 4] + weights = [0.4, 0.6] + wait_for_inclusion = False + wait_for_finalization = False + prompt = False + max_retries = 5 + + expected_result = (True, None) + mocked_generate_weight_hash = mocker.patch.object(subtensor_module, "generate_weight_hash", return_value=expected_result) + mocked_commit_weights_extrinsic = mocker.patch.object(subtensor_module, "commit_weights_extrinsic", return_value=expected_result) + + # Call + result = subtensor.commit_weights( + wallet=fake_wallet, + netuid=netuid, + salt=salt, + uids=uids, + weights=weights, + version_key=settings.version_as_int, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + max_retries=max_retries, + ) + + # Asserts + mocked_generate_weight_hash.assert_called_once_with( + address=fake_wallet.hotkey.ss58_address, + netuid=netuid, + uids=list(uids), + values=list(weights), + salt=list(salt), + version_key=settings.version_as_int, + ) + + mocked_commit_weights_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + commit_hash=mocked_generate_weight_hash.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt + ) + assert result == expected_result + + +def test_do_commit_weights(subtensor, mocker): + """Successful _do_commit_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + netuid = 1 + commit_hash = "fake_commit_hash" + wait_for_inclusion = True + wait_for_finalization = True + + mocked_substrate = mocker.MagicMock() + mocked_substrate.submit_extrinsic.return_value.is_success = None + subtensor.substrate = mocked_substrate + + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = subtensor._do_commit_weights( + wallet=fake_wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Assertions + mocked_substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + } + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + + assert result == (False, mocked_format_error_message.return_value) + + +def test_reveal_weights(subtensor, mocker): + """Successful test_reveal_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + netuid = 1 + uids = [1, 2, 3, 4] + weights = [0.1, 0.2, 0.3, 0.4] + salt = [4, 2, 2, 1] + expected_result = (True, None) + mocked_extrinsic = mocker.patch.object(subtensor_module, "reveal_weights_extrinsic", return_value=expected_result) + + # Call + result = subtensor.reveal_weights( + wallet=fake_wallet, + netuid=netuid, + uids=uids, + weights=weights, + salt=salt, + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=False, + ) + + # Assertions + assert result == (True, None) + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + uids=uids, + version_key=version_as_int, + weights=weights, + salt=salt, + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=False, + ) + + +def test_reveal_weights_false(subtensor, mocker): + """Failed test_reveal_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + netuid = 1 + uids = [1, 2, 3, 4] + weights = [0.1, 0.2, 0.3, 0.4] + salt = [4, 2, 2, 1] + + expected_result = (False, "No attempt made. Perhaps it is too soon to reveal weights!") + mocked_extrinsic = mocker.patch.object(subtensor_module, "reveal_weights_extrinsic") + + # Call + result = subtensor.reveal_weights( + wallet=fake_wallet, + netuid=netuid, + uids=uids, + weights=weights, + salt=salt, + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=False, + ) + + # Assertion + assert result == expected_result + assert mocked_extrinsic.call_count == 5 + + +def test_do_reveal_weights(subtensor, mocker): + """Verifies that the `_do_reveal_weights` method interacts with the right substrate methods.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_wallet.hotkey = "hotkey" + + netuid = 1 + uids = [1, 2, 3, 4] + values = [1, 2, 3, 4] + salt = [4, 2, 2, 1] + wait_for_inclusion = True + wait_for_finalization = True + + mocked_substrate = mocker.MagicMock() + mocked_substrate.submit_extrinsic.return_value.is_success = None + subtensor.substrate = mocked_substrate + + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = subtensor._do_reveal_weights( + wallet=fake_wallet, + netuid=netuid, + uids=uids, + values=values, + salt=salt, + version_key=version_as_int, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": values, + "salt": salt, + "version_key": version_as_int, + } + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + + assert result == (False, mocked_format_error_message.return_value) From 8979da657b871040fea8f3c8a25a5220e5031b4a Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 19 Aug 2024 20:38:20 -0700 Subject: [PATCH 101/260] ruff --- tests/unit_tests/test_subtensor.py | 48 +++++++++++++++++------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index b6c2fae10..86b002532 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -865,7 +865,7 @@ def test_get_subnet_hyperparameters_hex_without_prefix(mocker, subtensor): def test_get_commitment(subtensor, mocker): - """ Successful get_commitment call.""" + """Successful get_commitment call.""" # Preps fake_netuid = 1 fake_uid = 2 @@ -879,13 +879,13 @@ def test_get_commitment(subtensor, mocker): mocked_metagraph.return_value.hotkeys = {fake_uid: fake_hotkey} mocked_get_metadata = mocker.patch.object(subtensor_module, "get_metadata") - mocked_get_metadata.return_value = {"info": {"fields": [{fake_hex_data: fake_hex_data}]}} + mocked_get_metadata.return_value = { + "info": {"fields": [{fake_hex_data: fake_hex_data}]} + } # Call result = subtensor.get_commitment( - netuid=fake_netuid, - uid=fake_uid, - block=fake_block + netuid=fake_netuid, uid=fake_uid, block=fake_block ) # Assertions @@ -965,7 +965,7 @@ def test_get_transfer_fee(subtensor, mocker): def test_get_transfer_fee_incorrect_value(subtensor, mocker): - """ Successful get_transfer_fee call.""" + """Successful get_transfer_fee call.""" # Preps fake_wallet = mocker.MagicMock() fake_dest = mocker.MagicMock() @@ -985,7 +985,7 @@ def test_get_transfer_fee_incorrect_value(subtensor, mocker): def test_get_existential_deposit(subtensor, mocker): - """ Successful get_existential_deposit call.""" + """Successful get_existential_deposit call.""" # Prep block = 123 @@ -999,9 +999,8 @@ def test_get_existential_deposit(subtensor, mocker): # Assertions mocked_query_constant.assert_called_once_with( - module_name="Balances", - constant_name="ExistentialDeposit", - block=block) + module_name="Balances", constant_name="ExistentialDeposit", block=block + ) assert isinstance(result, Balance) assert result == Balance.from_rao(value) @@ -1021,8 +1020,12 @@ def test_commit_weights(subtensor, mocker): max_retries = 5 expected_result = (True, None) - mocked_generate_weight_hash = mocker.patch.object(subtensor_module, "generate_weight_hash", return_value=expected_result) - mocked_commit_weights_extrinsic = mocker.patch.object(subtensor_module, "commit_weights_extrinsic", return_value=expected_result) + mocked_generate_weight_hash = mocker.patch.object( + subtensor_module, "generate_weight_hash", return_value=expected_result + ) + mocked_commit_weights_extrinsic = mocker.patch.object( + subtensor_module, "commit_weights_extrinsic", return_value=expected_result + ) # Call result = subtensor.commit_weights( @@ -1055,7 +1058,7 @@ def test_commit_weights(subtensor, mocker): commit_hash=mocked_generate_weight_hash.return_value, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - prompt=prompt + prompt=prompt, ) assert result == expected_result @@ -1092,12 +1095,11 @@ def test_do_commit_weights(subtensor, mocker): call_params={ "netuid": netuid, "commit_hash": commit_hash, - } + }, ) mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, - keypair=fake_wallet.hotkey + call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.hotkey ) mocked_substrate.submit_extrinsic.assert_called_once_with( @@ -1120,7 +1122,9 @@ def test_reveal_weights(subtensor, mocker): weights = [0.1, 0.2, 0.3, 0.4] salt = [4, 2, 2, 1] expected_result = (True, None) - mocked_extrinsic = mocker.patch.object(subtensor_module, "reveal_weights_extrinsic", return_value=expected_result) + mocked_extrinsic = mocker.patch.object( + subtensor_module, "reveal_weights_extrinsic", return_value=expected_result + ) # Call result = subtensor.reveal_weights( @@ -1159,7 +1163,10 @@ def test_reveal_weights_false(subtensor, mocker): weights = [0.1, 0.2, 0.3, 0.4] salt = [4, 2, 2, 1] - expected_result = (False, "No attempt made. Perhaps it is too soon to reveal weights!") + expected_result = ( + False, + "No attempt made. Perhaps it is too soon to reveal weights!", + ) mocked_extrinsic = mocker.patch.object(subtensor_module, "reveal_weights_extrinsic") # Call @@ -1221,12 +1228,11 @@ def test_do_reveal_weights(subtensor, mocker): "values": values, "salt": salt, "version_key": version_as_int, - } + }, ) mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, - keypair=fake_wallet.hotkey + call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.hotkey ) mocked_substrate.submit_extrinsic.assert_called_once_with( From 7f5fc1142ad66caf34236ecbe1c247b0d661b25d Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 19 Aug 2024 23:14:01 -0700 Subject: [PATCH 102/260] add tests --- tests/unit_tests/test_subtensor.py | 277 ++++++++++++++++++++++++++++- 1 file changed, 276 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 86b002532..ca42b13c1 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -839,7 +839,40 @@ def test_get_subnet_hyperparameters_no_data(mocker, subtensor): subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() -def test_get_subnet_hyperparameters_hex_without_prefix(mocker, subtensor): +def test_serve_prometheus(subtensor, mocker): + """Test serve_prometheus function successful call.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_port = 1234 + fake_netuid = 1 + wait_for_inclusion = True + wait_for_finalization = False + + mocked_prometheus_extrinsic = mocker.patch.object(subtensor_module, "prometheus_extrinsic") + + # Call + result = subtensor.serve_prometheus( + fake_wallet, + fake_port, + fake_netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + mocked_prometheus_extrinsic.assert_called_once_with( + subtensor, + wallet=fake_wallet, + port=fake_port, + netuid=fake_netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + assert result == mocked_prometheus_extrinsic.return_value + + +def test_get_subnet_hyperparameters_hex_without_prefix(subtensor, mocker): """Test get_subnet_hyperparameters correctly processes hex string without '0x' prefix.""" # Prep netuid = 1 @@ -864,6 +897,248 @@ def test_get_subnet_hyperparameters_hex_without_prefix(mocker, subtensor): ) +def test_do_serve_axon_is_success(subtensor, mocker): + """Successful do_serve_axon call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mocked_substrate = mocker.MagicMock() + mocked_substrate.submit_extrinsic.return_value.is_success = True + subtensor.substrate = mocked_substrate + + # Call + result = subtensor._do_serve_axon( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_axon", + call_params=fake_call_params, + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == (True, None) + + +def test_do_serve_axon_is_not_success(subtensor, mocker): + """Unsuccessful do_serve_axon call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mocked_substrate = mocker.MagicMock() + mocked_substrate.submit_extrinsic.return_value.is_success = None + subtensor.substrate = mocked_substrate + + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = subtensor._do_serve_axon( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_axon", + call_params=fake_call_params, + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + mocked_format_error_message.assert_called_once_with(mocked_substrate.submit_extrinsic.return_value.error_message) + assert result == (False, mocked_format_error_message.return_value) + + +def test_do_serve_axon_no_waits(subtensor, mocker): + """Unsuccessful do_serve_axon call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor._do_serve_axon( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_axon", + call_params=fake_call_params, + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == (True, None) + + +def test_serve(subtensor, mocker): + """Successful serve call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_ip = "fake_ip" + fake_port = 1234 + fake_protocol = 1 + fake_netuid = 1 + fake_placeholder1 = 0 + fake_placeholder2 = 1 + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mocked_serve_extrinsic = mocker.patch.object(subtensor_module, "serve_extrinsic") + + # Call + result = subtensor.serve( + fake_wallet, + fake_ip, + fake_port, + fake_protocol, + fake_netuid, + fake_placeholder1, + fake_placeholder2, + fake_wait_for_inclusion, + fake_wait_for_finalization, + ) + + # Asserts + mocked_serve_extrinsic.assert_called_once_with( + subtensor, + fake_wallet, + fake_ip, + fake_port, + fake_protocol, + fake_netuid, + fake_placeholder1, + fake_placeholder2, + fake_wait_for_inclusion, + fake_wait_for_finalization, + ) + + assert result == mocked_serve_extrinsic.return_value + + +def test_immunity_period(subtensor, mocker): + """Successful immunity_period call.""" + # Preps + fake_netuid = 1 + fake_block = 123 + fare_result = 101 + + mocked_get_hyperparameter = mocker.MagicMock() + mocked_get_hyperparameter.return_value = fare_result + subtensor._get_hyperparameter = mocked_get_hyperparameter + + # Call + result = subtensor.immunity_period(netuid=fake_netuid, block=fake_block) + + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="ImmunityPeriod", + netuid=fake_netuid, + block=fake_block, + ) + assert result == mocked_get_hyperparameter.return_value + + +def test_get_uid_for_hotkey_on_subnet(subtensor, mocker): + """Successful get_uid_for_hotkey_on_subnet call.""" + # Prep + fake_hotkey_ss58 = "fake_hotkey_ss58" + fake_netuid = 1 + fake_block = 123 + mocked_query_subtensor = mocker.MagicMock() + subtensor.query_subtensor = mocked_query_subtensor + + # Call + result = subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=fake_hotkey_ss58, + netuid=fake_netuid, + block=fake_block + ) + + # Assertions + mocked_query_subtensor.assert_called_once_with( + "Uids", fake_block, [fake_netuid, fake_hotkey_ss58] + ) + + assert result == mocked_query_subtensor.return_value.value + + +def test_tempo(subtensor, mocker): + """Successful tempo call.""" + # Preps + fake_netuid = 1 + fake_block = 123 + fare_result = 101 + + mocked_get_hyperparameter = mocker.MagicMock() + mocked_get_hyperparameter.return_value = fare_result + subtensor._get_hyperparameter = mocked_get_hyperparameter + + # Call + result = subtensor.tempo(netuid=fake_netuid, block=fake_block) + + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="Tempo", + netuid=fake_netuid, + block=fake_block, + ) + assert result == mocked_get_hyperparameter.return_value + + def test_get_commitment(subtensor, mocker): """Successful get_commitment call.""" # Preps From a4ecc1fd5f95a9383c47085c105cbf80dab325a1 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 19 Aug 2024 23:16:36 -0700 Subject: [PATCH 103/260] ruff --- tests/unit_tests/test_subtensor.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index ca42b13c1..6bd90f4fe 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -848,7 +848,9 @@ def test_serve_prometheus(subtensor, mocker): wait_for_inclusion = True wait_for_finalization = False - mocked_prometheus_extrinsic = mocker.patch.object(subtensor_module, "prometheus_extrinsic") + mocked_prometheus_extrinsic = mocker.patch.object( + subtensor_module, "prometheus_extrinsic" + ) # Call result = subtensor.serve_prometheus( @@ -981,7 +983,9 @@ def test_do_serve_axon_is_not_success(subtensor, mocker): ) mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() - mocked_format_error_message.assert_called_once_with(mocked_substrate.submit_extrinsic.return_value.error_message) + mocked_format_error_message.assert_called_once_with( + mocked_substrate.submit_extrinsic.return_value.error_message + ) assert result == (False, mocked_format_error_message.return_value) @@ -1103,9 +1107,7 @@ def test_get_uid_for_hotkey_on_subnet(subtensor, mocker): # Call result = subtensor.get_uid_for_hotkey_on_subnet( - hotkey_ss58=fake_hotkey_ss58, - netuid=fake_netuid, - block=fake_block + hotkey_ss58=fake_hotkey_ss58, netuid=fake_netuid, block=fake_block ) # Assertions From 023a48f8673e29b26094b20cde1ca22f176982f4 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 19 Aug 2024 23:26:57 -0700 Subject: [PATCH 104/260] add tests _do_serve_prometheus --- tests/unit_tests/test_subtensor.py | 129 +++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 6bd90f4fe..cd183f97e 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -839,6 +839,135 @@ def test_get_subnet_hyperparameters_no_data(mocker, subtensor): subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() +def test_do_serve_prometheus_is_success(subtensor, mocker): + """Successful do_serve_prometheus call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mocked_substrate = mocker.MagicMock() + mocked_substrate.submit_extrinsic.return_value.is_success = True + subtensor.substrate = mocked_substrate + + # Call + result = subtensor._do_serve_prometheus( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_prometheus", + call_params=fake_call_params, + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == (True, None) + + +def test_do_serve_prometheus_is_not_success(subtensor, mocker): + """Unsuccessful do_serve_axon call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mocked_substrate = mocker.MagicMock() + mocked_substrate.submit_extrinsic.return_value.is_success = None + subtensor.substrate = mocked_substrate + + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = subtensor._do_serve_prometheus( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_prometheus", + call_params=fake_call_params, + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + mocked_format_error_message.assert_called_once_with( + mocked_substrate.submit_extrinsic.return_value.error_message + ) + assert result == (False, mocked_format_error_message.return_value) + + +def test_do_serve_prometheus_no_waits(subtensor, mocker): + """Unsuccessful do_serve_axon call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor._do_serve_prometheus( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_prometheus", + call_params=fake_call_params, + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == (True, None) + + def test_serve_prometheus(subtensor, mocker): """Test serve_prometheus function successful call.""" # Preps From 7643074330bd7beb3d4fe5c1940a527240d9fdb5 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 19 Aug 2024 23:46:23 -0700 Subject: [PATCH 105/260] add tests neuron_for_uid --- tests/unit_tests/test_subtensor.py | 82 ++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index cd183f97e..2cc1f9a94 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -17,6 +17,7 @@ import argparse import unittest.mock as mock +from cgitb import reset from typing import List, Tuple from unittest.mock import MagicMock @@ -817,6 +818,87 @@ def test_get_subnet_hyperparameters_success(mocker, subtensor): ) +def test_neuron_for_uid_none(subtensor, mocker): + """Test neuron_for_uid successful call.""" + # Prep + fake_uid = None + fake_netuid = 2 + fake_block = 123 + mocked_neuron_info = mocker.patch.object( + subtensor_module.NeuronInfo, "get_null_neuron" + ) + + # Call + result = subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block=fake_block + ) + + # Asserts + mocked_neuron_info.assert_called_once() + assert result == mocked_neuron_info.return_value + + +def test_neuron_for_uid_response_none(subtensor, mocker): + """Test neuron_for_uid successful call.""" + # Prep + fake_uid = 1 + fake_netuid = 2 + fake_block = 123 + mocked_neuron_info = mocker.patch.object( + subtensor_module.NeuronInfo, "get_null_neuron" + ) + + mocked_substrate = mocker.MagicMock() + mocked_substrate.rpc_request.return_value.get.return_value = None + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block=fake_block + ) + + # Asserts + mocked_substrate.get_block_hash.assert_called_once_with(fake_block) + mocked_substrate.rpc_request.assert_called_once_with( + method="neuronInfo_getNeuron", + params=[fake_netuid, fake_uid, mocked_substrate.get_block_hash.return_value], + ) + + mocked_neuron_info.assert_called_once() + assert result == mocked_neuron_info.return_value + + +def test_neuron_for_uid_success(subtensor, mocker): + """Test neuron_for_uid successful call.""" + # Prep + fake_uid = 1 + fake_netuid = 2 + fake_block = 123 + mocked_neuron_from_vec_u8 = mocker.patch.object( + subtensor_module.NeuronInfo, "from_vec_u8" + ) + + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block=fake_block + ) + + # Asserts + mocked_substrate.get_block_hash.assert_called_once_with(fake_block) + mocked_substrate.rpc_request.assert_called_once_with( + method="neuronInfo_getNeuron", + params=[fake_netuid, fake_uid, mocked_substrate.get_block_hash.return_value], + ) + + mocked_neuron_from_vec_u8.assert_called_once_with( + mocked_substrate.rpc_request.return_value.get.return_value + ) + assert result == mocked_neuron_from_vec_u8.return_value + + def test_get_subnet_hyperparameters_no_data(mocker, subtensor): """Test get_subnet_hyperparameters returns empty list when no data is found.""" # Prep From c116950c66dba576754495774813e82d7ae26cf7 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 19 Aug 2024 23:50:30 -0700 Subject: [PATCH 106/260] sort tests by group --- tests/unit_tests/test_subtensor.py | 94 +++++++++++++++--------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 2cc1f9a94..a477b6e59 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -818,6 +818,53 @@ def test_get_subnet_hyperparameters_success(mocker, subtensor): ) +def test_get_subnet_hyperparameters_hex_without_prefix(subtensor, mocker): + """Test get_subnet_hyperparameters correctly processes hex string without '0x' prefix.""" + # Prep + netuid = 1 + block = 123 + hex_bytes_result = "010203" + bytes_result = bytes.fromhex(hex_bytes_result) + mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) + mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") + + # Call + result = subtensor.get_subnet_hyperparameters(netuid, block) + + # Asserts + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], + block=block, + ) + subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( + bytes_result + ) + + +def test_get_subnet_hyperparameters_no_data(mocker, subtensor): + """Test get_subnet_hyperparameters returns empty list when no data is found.""" + # Prep + netuid = 1 + block = 123 + mocker.patch.object(subtensor, "query_runtime_api", return_value=None) + mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") + + # Call + result = subtensor.get_subnet_hyperparameters(netuid, block) + + # Asserts + assert result == [] + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], + block=block, + ) + subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() + + def test_neuron_for_uid_none(subtensor, mocker): """Test neuron_for_uid successful call.""" # Prep @@ -899,28 +946,6 @@ def test_neuron_for_uid_success(subtensor, mocker): assert result == mocked_neuron_from_vec_u8.return_value -def test_get_subnet_hyperparameters_no_data(mocker, subtensor): - """Test get_subnet_hyperparameters returns empty list when no data is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_runtime_api", return_value=None) - mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") - - # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) - - # Asserts - assert result == [] - subtensor.query_runtime_api.assert_called_once_with( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, - ) - subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() - - def test_do_serve_prometheus_is_success(subtensor, mocker): """Successful do_serve_prometheus call.""" # Prep @@ -1085,31 +1110,6 @@ def test_serve_prometheus(subtensor, mocker): assert result == mocked_prometheus_extrinsic.return_value -def test_get_subnet_hyperparameters_hex_without_prefix(subtensor, mocker): - """Test get_subnet_hyperparameters correctly processes hex string without '0x' prefix.""" - # Prep - netuid = 1 - block = 123 - hex_bytes_result = "010203" - bytes_result = bytes.fromhex(hex_bytes_result) - mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) - mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") - - # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) - - # Asserts - subtensor.query_runtime_api.assert_called_once_with( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, - ) - subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( - bytes_result - ) - - def test_do_serve_axon_is_success(subtensor, mocker): """Successful do_serve_axon call.""" # Prep From 5889b6a2b6f0813e02c8d3388f5a0b9f31725b6a Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 19 Aug 2024 23:50:30 -0700 Subject: [PATCH 107/260] sort tests by group --- tests/unit_tests/test_subtensor.py | 94 +++++++++++++++--------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 2cc1f9a94..45a91ceb9 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -818,6 +818,53 @@ def test_get_subnet_hyperparameters_success(mocker, subtensor): ) +def test_get_subnet_hyperparameters_no_data(mocker, subtensor): + """Test get_subnet_hyperparameters returns empty list when no data is found.""" + # Prep + netuid = 1 + block = 123 + mocker.patch.object(subtensor, "query_runtime_api", return_value=None) + mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") + + # Call + result = subtensor.get_subnet_hyperparameters(netuid, block) + + # Asserts + assert result == [] + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], + block=block, + ) + subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() + + +def test_get_subnet_hyperparameters_hex_without_prefix(subtensor, mocker): + """Test get_subnet_hyperparameters correctly processes hex string without '0x' prefix.""" + # Prep + netuid = 1 + block = 123 + hex_bytes_result = "010203" + bytes_result = bytes.fromhex(hex_bytes_result) + mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) + mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") + + # Call + result = subtensor.get_subnet_hyperparameters(netuid, block) + + # Asserts + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], + block=block, + ) + subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( + bytes_result + ) + + def test_neuron_for_uid_none(subtensor, mocker): """Test neuron_for_uid successful call.""" # Prep @@ -899,28 +946,6 @@ def test_neuron_for_uid_success(subtensor, mocker): assert result == mocked_neuron_from_vec_u8.return_value -def test_get_subnet_hyperparameters_no_data(mocker, subtensor): - """Test get_subnet_hyperparameters returns empty list when no data is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_runtime_api", return_value=None) - mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") - - # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) - - # Asserts - assert result == [] - subtensor.query_runtime_api.assert_called_once_with( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, - ) - subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() - - def test_do_serve_prometheus_is_success(subtensor, mocker): """Successful do_serve_prometheus call.""" # Prep @@ -1085,31 +1110,6 @@ def test_serve_prometheus(subtensor, mocker): assert result == mocked_prometheus_extrinsic.return_value -def test_get_subnet_hyperparameters_hex_without_prefix(subtensor, mocker): - """Test get_subnet_hyperparameters correctly processes hex string without '0x' prefix.""" - # Prep - netuid = 1 - block = 123 - hex_bytes_result = "010203" - bytes_result = bytes.fromhex(hex_bytes_result) - mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) - mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") - - # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) - - # Asserts - subtensor.query_runtime_api.assert_called_once_with( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, - ) - subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( - bytes_result - ) - - def test_do_serve_axon_is_success(subtensor, mocker): """Successful do_serve_axon call.""" # Prep From fe65dd19f350954c881171b5985557244198ea30 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 19 Aug 2024 23:54:50 -0700 Subject: [PATCH 108/260] remove unused import --- tests/unit_tests/test_subtensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index a477b6e59..7dbdf3488 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -17,7 +17,6 @@ import argparse import unittest.mock as mock -from cgitb import reset from typing import List, Tuple from unittest.mock import MagicMock From 7d67af7929012ef4b59092e862c932b881259d36 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 Aug 2024 00:09:54 -0700 Subject: [PATCH 109/260] add test for `get_neuron_for_pubkey_and_subnet` --- tests/unit_tests/test_subtensor.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 7dbdf3488..43f679ea0 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -864,6 +864,35 @@ def test_get_subnet_hyperparameters_no_data(mocker, subtensor): subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() +def test_get_neuron_for_pubkey_and_subnet(subtensor, mocker): + """Successful call to get_neuron_for_pubkey_and_subnet.""" + # Prep + fake_hotkey_ss58 = "fake_hotkey" + fake_netuid = 1 + fake_block = 123 + + mocked_neuron_for_uid = mocker.MagicMock() + subtensor.neuron_for_uid = mocked_neuron_for_uid + + mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet + + # Call + result = subtensor.get_neuron_for_pubkey_and_subnet( + hotkey_ss58=fake_hotkey_ss58, + netuid=fake_netuid, + block=fake_block, + ) + + # Asserts + mocked_neuron_for_uid.assert_called_once_with( + mocked_get_uid_for_hotkey_on_subnet.return_value, + fake_netuid, + block=fake_block, + ) + assert result == mocked_neuron_for_uid.return_value + + def test_neuron_for_uid_none(subtensor, mocker): """Test neuron_for_uid successful call.""" # Prep From 97cba4b3bc6f16aabf4535c479eeec1a1e4e52e4 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 Aug 2024 11:15:33 -0700 Subject: [PATCH 110/260] Add unit tests to subtensor.py --- tests/unit_tests/test_subtensor.py | 460 +++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 43f679ea0..82e8c6c75 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -17,12 +17,14 @@ import argparse import unittest.mock as mock +from cgitb import reset from typing import List, Tuple from unittest.mock import MagicMock import pytest from bittensor_wallet import Wallet +from bittensor import wallet from bittensor.core import subtensor as subtensor_module, settings from bittensor.core.axon import Axon from bittensor.core.chain_data import SubnetHyperparameters @@ -864,6 +866,464 @@ def test_get_subnet_hyperparameters_no_data(mocker, subtensor): subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() +def test_do_set_weights_is_success(subtensor, mocker): + """Successful _do_set_weights call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_uids = [1, 2, 3] + fake_vals = [4, 5, 6] + fake_netuid = 1 + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mocked_substrate = mocker.MagicMock() + mocked_substrate.submit_extrinsic.return_value.is_success = True + subtensor.substrate = mocked_substrate + + # Call + result = subtensor._do_set_weights( + wallet=fake_wallet, + uids=fake_uids, + vals=fake_vals, + netuid=fake_netuid, + version_key=settings.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_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": settings.version_as_int, + }, + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + era={"period": 5}, + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == (True, "Successfully set weights.") + + +def test_do_set_weights_is_not_success(subtensor, mocker): + """Unsuccessful _do_set_weights call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_uids = [1, 2, 3] + fake_vals = [4, 5, 6] + fake_netuid = 1 + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mocked_substrate = mocker.MagicMock() + mocked_substrate.submit_extrinsic.return_value.is_success = False + subtensor.substrate = mocked_substrate + + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = subtensor._do_set_weights( + wallet=fake_wallet, + uids=fake_uids, + vals=fake_vals, + netuid=fake_netuid, + version_key=settings.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_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": settings.version_as_int, + }, + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + era={"period": 5}, + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + mocked_format_error_message.assert_called_once_with( + mocked_substrate.submit_extrinsic.return_value.error_message + ) + assert result == (False, mocked_format_error_message.return_value) + + +def test_do_set_weights_no_waits(subtensor, mocker): + """Unsuccessful _do_set_weights call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_uids = [1, 2, 3] + fake_vals = [4, 5, 6] + fake_netuid = 1 + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor._do_set_weights( + wallet=fake_wallet, + uids=fake_uids, + vals=fake_vals, + netuid=fake_netuid, + version_key=settings.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + # Asserts + mocked_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": settings.version_as_int, + }, + ) + + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + era={"period": 5}, + ) + + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == (True, "Not waiting for finalization or inclusion.") + + +def test_set_weights(subtensor, mocker): + """Successful set_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_netuid = 1 + fake_uids = [2, 4] + fake_weights = [0.4, 0.6] + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + fake_prompt = False + fake_max_retries = 5 + + expected_result = (True, None) + + mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet + + mocked_blocks_since_last_update = mocker.MagicMock(return_value=2) + subtensor.blocks_since_last_update = mocked_blocks_since_last_update + + mocked_weights_rate_limit = mocker.MagicMock(return_value=1) + subtensor.weights_rate_limit = mocked_weights_rate_limit + + mocked_set_weights_extrinsic = mocker.patch.object( + subtensor_module, "set_weights_extrinsic", return_value=expected_result + ) + + # Call + result = subtensor.set_weights( + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + version_key=settings.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + prompt=fake_prompt, + max_retries=fake_max_retries, + ) + + # Asserts + mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with( + fake_wallet.hotkey.ss58_address, fake_netuid + ) + mocked_blocks_since_last_update.assert_called_with( + fake_netuid, mocked_get_uid_for_hotkey_on_subnet.return_value + ) + mocked_weights_rate_limit.assert_called_with(fake_netuid) + mocked_set_weights_extrinsic.assert_called_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + version_key=settings.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + prompt=fake_prompt, + ) + assert result == expected_result + + +def test_serve_axon(subtensor, mocker): + """Tests successful serve_axon call.""" + # Prep + fake_netuid = 123 + fake_axon = mocker.MagicMock() + fake_wait_for_inclusion = False + fake_wait_for_finalization = True + + mocked_serve_axon_extrinsic = mocker.patch.object(subtensor_module, "serve_axon_extrinsic") + + # Call + result = subtensor.serve_axon( + fake_netuid, fake_axon, fake_wait_for_inclusion, fake_wait_for_finalization) + + # Asserts + mocked_serve_axon_extrinsic.assert_called_once_with( + subtensor, fake_netuid, fake_axon, fake_wait_for_inclusion, fake_wait_for_finalization + ) + assert result == mocked_serve_axon_extrinsic.return_value + + +def test_get_block_hash(subtensor, mocker): + """Tests successful get_block_hash call.""" + # Prep + fake_block_id = 123 + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.get_block_hash(fake_block_id) + + # Asserts + mocked_substrate.get_block_hash.assert_called_once_with(block_id=fake_block_id) + assert result == mocked_substrate.get_block_hash.return_value + + +def test_commit(subtensor, mocker): + """Test successful commit call.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_netuid = 1 + fake_data = "some data to network" + mocked_publish_metadata = mocker.patch.object(subtensor_module, "publish_metadata") + + # Call + result = subtensor.commit(fake_wallet, fake_netuid, fake_data) + + # Asserts + mocked_publish_metadata.assert_called_once_with( + subtensor, fake_wallet, fake_netuid, f"Raw{len(fake_data)}", fake_data.encode() + ) + assert result is None + + +def test_subnetwork_n(subtensor, mocker): + """Test successful subnetwork_n call.""" + # Prep + fake_netuid = 1 + fake_block = 123 + fake_result = 2 + + mocked_get_hyperparameter = mocker.MagicMock() + mocked_get_hyperparameter.return_value = fake_result + subtensor._get_hyperparameter = mocked_get_hyperparameter + + # Call + result = subtensor.subnetwork_n(fake_netuid, fake_block) + + # Asserts + mocked_get_hyperparameter.assert_called_once_with( + param_name="SubnetworkN", netuid=fake_netuid, block=fake_block + ) + assert result == mocked_get_hyperparameter.return_value + + +def test_do_transfer_is_success_true(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mocked_substrate = mocker.MagicMock() + mocked_substrate.submit_extrinsic.return_value.is_success = True + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.do_transfer( + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization + ) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, + ) + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.coldkey + ) + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == (True, mocked_substrate.submit_extrinsic.return_value.block_hash, None) + + +def test_do_transfer_is_success_false(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + mocked_substrate = mocker.MagicMock() + mocked_substrate.submit_extrinsic.return_value.is_success = False + subtensor.substrate = mocked_substrate + + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = subtensor.do_transfer( + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization + ) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, + ) + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.coldkey + ) + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + mocked_format_error_message.assert_called_once_with(mocked_substrate.submit_extrinsic.return_value.error_message) + assert result == (False, None, mocked_format_error_message.return_value) + + +def test_do_transfer_no_waits(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.do_transfer( + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization + ) + + # Asserts + mocked_substrate.compose_call.assert_called_once_with( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, + ) + mocked_substrate.create_signed_extrinsic.assert_called_once_with( + call=mocked_substrate.compose_call.return_value, + keypair=fake_wallet.coldkey + ) + mocked_substrate.submit_extrinsic.assert_called_once_with( + mocked_substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == (True, None, None) + + +def test_transfer(subtensor, mocker): + """Tests successful transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_amount = 1.1 + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + fake_prompt = False + mocked_transfer_extrinsic = mocker.patch.object(subtensor_module, "transfer_extrinsic") + + # Call + result = subtensor.transfer( + fake_wallet, + fake_dest, + fake_amount, + fake_wait_for_inclusion, + fake_wait_for_finalization, + fake_prompt + ) + + # Asserts + mocked_transfer_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + dest=fake_dest, + amount=fake_amount, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + prompt=fake_prompt + ) + assert result == mocked_transfer_extrinsic.return_value + + def test_get_neuron_for_pubkey_and_subnet(subtensor, mocker): """Successful call to get_neuron_for_pubkey_and_subnet.""" # Prep From 7d3767071a172baaad82fb81165dbdc710126b8d Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 Aug 2024 11:41:30 -0700 Subject: [PATCH 111/260] Fix ruff + `test_do_set_weights_no_waits` description --- tests/unit_tests/test_subtensor.py | 48 +++++++++++++++++++----------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 82e8c6c75..967999c2b 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -979,7 +979,7 @@ def test_do_set_weights_is_not_success(subtensor, mocker): def test_do_set_weights_no_waits(subtensor, mocker): - """Unsuccessful _do_set_weights call.""" + """Successful _do_set_weights call without wait flags for fake_wait_for_inclusion and fake_wait_for_finalization.""" # Prep fake_wallet = mocker.MagicMock() fake_uids = [1, 2, 3] @@ -1098,15 +1098,22 @@ def test_serve_axon(subtensor, mocker): fake_wait_for_inclusion = False fake_wait_for_finalization = True - mocked_serve_axon_extrinsic = mocker.patch.object(subtensor_module, "serve_axon_extrinsic") + mocked_serve_axon_extrinsic = mocker.patch.object( + subtensor_module, "serve_axon_extrinsic" + ) # Call result = subtensor.serve_axon( - fake_netuid, fake_axon, fake_wait_for_inclusion, fake_wait_for_finalization) + fake_netuid, fake_axon, fake_wait_for_inclusion, fake_wait_for_finalization + ) # Asserts mocked_serve_axon_extrinsic.assert_called_once_with( - subtensor, fake_netuid, fake_axon, fake_wait_for_inclusion, fake_wait_for_finalization + subtensor, + fake_netuid, + fake_axon, + fake_wait_for_inclusion, + fake_wait_for_finalization, ) assert result == mocked_serve_axon_extrinsic.return_value @@ -1184,7 +1191,7 @@ def test_do_transfer_is_success_true(subtensor, mocker): fake_dest, fake_transfer_balance, fake_wait_for_inclusion, - fake_wait_for_finalization + fake_wait_for_finalization, ) # Asserts @@ -1194,8 +1201,7 @@ def test_do_transfer_is_success_true(subtensor, mocker): call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, - keypair=fake_wallet.coldkey + call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.coldkey ) mocked_substrate.submit_extrinsic.assert_called_once_with( mocked_substrate.create_signed_extrinsic.return_value, @@ -1203,7 +1209,11 @@ def test_do_transfer_is_success_true(subtensor, mocker): wait_for_finalization=fake_wait_for_finalization, ) mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == (True, mocked_substrate.submit_extrinsic.return_value.block_hash, None) + assert result == ( + True, + mocked_substrate.submit_extrinsic.return_value.block_hash, + None, + ) def test_do_transfer_is_success_false(subtensor, mocker): @@ -1228,7 +1238,7 @@ def test_do_transfer_is_success_false(subtensor, mocker): fake_dest, fake_transfer_balance, fake_wait_for_inclusion, - fake_wait_for_finalization + fake_wait_for_finalization, ) # Asserts @@ -1238,8 +1248,7 @@ def test_do_transfer_is_success_false(subtensor, mocker): call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, - keypair=fake_wallet.coldkey + call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.coldkey ) mocked_substrate.submit_extrinsic.assert_called_once_with( mocked_substrate.create_signed_extrinsic.return_value, @@ -1247,7 +1256,9 @@ def test_do_transfer_is_success_false(subtensor, mocker): wait_for_finalization=fake_wait_for_finalization, ) mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() - mocked_format_error_message.assert_called_once_with(mocked_substrate.submit_extrinsic.return_value.error_message) + mocked_format_error_message.assert_called_once_with( + mocked_substrate.submit_extrinsic.return_value.error_message + ) assert result == (False, None, mocked_format_error_message.return_value) @@ -1269,7 +1280,7 @@ def test_do_transfer_no_waits(subtensor, mocker): fake_dest, fake_transfer_balance, fake_wait_for_inclusion, - fake_wait_for_finalization + fake_wait_for_finalization, ) # Asserts @@ -1279,8 +1290,7 @@ def test_do_transfer_no_waits(subtensor, mocker): call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, - keypair=fake_wallet.coldkey + call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.coldkey ) mocked_substrate.submit_extrinsic.assert_called_once_with( mocked_substrate.create_signed_extrinsic.return_value, @@ -1299,7 +1309,9 @@ def test_transfer(subtensor, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True fake_prompt = False - mocked_transfer_extrinsic = mocker.patch.object(subtensor_module, "transfer_extrinsic") + mocked_transfer_extrinsic = mocker.patch.object( + subtensor_module, "transfer_extrinsic" + ) # Call result = subtensor.transfer( @@ -1308,7 +1320,7 @@ def test_transfer(subtensor, mocker): fake_amount, fake_wait_for_inclusion, fake_wait_for_finalization, - fake_prompt + fake_prompt, ) # Asserts @@ -1319,7 +1331,7 @@ def test_transfer(subtensor, mocker): amount=fake_amount, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, - prompt=fake_prompt + prompt=fake_prompt, ) assert result == mocked_transfer_extrinsic.return_value From dab0bbd70785625d0bc8502ac82046a216bdaaa0 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 Aug 2024 14:23:19 -0700 Subject: [PATCH 112/260] add tests --- tests/unit_tests/test_subtensor.py | 207 +++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 967999c2b..489a4ee20 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -23,6 +23,7 @@ import pytest from bittensor_wallet import Wallet +from mypy.plugins.singledispatch import make_fake_register_class_instance from bittensor import wallet from bittensor.core import subtensor as subtensor_module, settings @@ -32,6 +33,7 @@ from bittensor.core.subtensor import Subtensor, logging from bittensor.utils import u16_normalized_float, u64_normalized_float from bittensor.utils.balance import Balance +from e2e_tests.utils.chain_interactions import register_subnet U16_MAX = 65535 U64_MAX = 18446744073709551615 @@ -866,6 +868,211 @@ def test_get_subnet_hyperparameters_no_data(mocker, subtensor): subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() +def test_state_call(subtensor, mocker): + """Tests state_call call.""" + # Prep + fake_method = "method" + fake_data = "data" + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.state_call(fake_method, fake_data) + + # Asserts + mocked_substrate.rpc_request.assert_called_once_with( + method="state_call", + params=[fake_method, fake_data], + ) + assert result == mocked_substrate.rpc_request.return_value + + +def test_query_map(subtensor, mocker): + """Tests query_map call.""" + # Prep + fake_module_name = "module_name" + fake_name = "constant_name" + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.query_map(fake_module_name, fake_name) + + # Asserts + mocked_substrate.query_map.assert_called_once_with( + module=fake_module_name, + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == mocked_substrate.query_map.return_value + + +def test_query_constant(subtensor, mocker): + """Tests query_constant call.""" + # Prep + fake_module_name = "module_name" + fake_constant_name = "constant_name" + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.query_constant(fake_module_name, fake_constant_name) + + # Asserts + mocked_substrate.get_constant.assert_called_once_with( + module_name=fake_module_name, + constant_name=fake_constant_name, + block_hash=None, + ) + assert result == mocked_substrate.get_constant.return_value + + +def test_query_module(subtensor, mocker): + # Prep + fake_module = "module" + fake_name = "function_name" + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.query_module(fake_module, fake_name) + + # Asserts + mocked_substrate.query.assert_called_once_with( + module=fake_module, + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == mocked_substrate.query.return_value + + +def test_metagraph(subtensor, mocker): + """Tests subtensor.metagraph call.""" + # Prep + fake_netuid = 1 + fake_lite = True + mocked_metagraph = mocker.patch.object(subtensor_module, "Metagraph") + + # Call + result = subtensor.metagraph(fake_netuid, fake_lite) + + # Asserts + mocked_metagraph.assert_called_once_with( + network=subtensor.network, + netuid=fake_netuid, + lite=fake_lite, + sync=False + ) + mocked_metagraph.return_value.sync.assert_called_once_with( + block=None, lite=fake_lite, subtensor=subtensor + ) + assert result == mocked_metagraph.return_value + + +def test_get_netuids_for_hotkey(subtensor, mocker): + """Tests get_netuids_for_hotkey call.""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + fake_block = 123 + + mocked_query_map_subtensor = mocker.MagicMock() + subtensor.query_map_subtensor = mocked_query_map_subtensor + + # Call + result = subtensor.get_netuids_for_hotkey(fake_hotkey_ss58, fake_block) + + # Asserts + mocked_query_map_subtensor.assert_called_once_with("IsNetworkMember", fake_block, [fake_hotkey_ss58]) + assert result == [] + + +def test_get_current_block(subtensor, mocker): + """Tests get_current_block call.""" + # Prep + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.get_current_block() + + # Asserts + mocked_substrate.get_block_number.assert_called_once_with(None) + assert result == mocked_substrate.get_block_number.return_value + + +def test_is_hotkey_registered_any(subtensor, mocker): + """Tests is_hotkey_registered_any call""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + fake_block = 123 + return_value = [1, 2] + + mocked_get_netuids_for_hotkey = mocker.MagicMock(return_value=return_value) + subtensor.get_netuids_for_hotkey = mocked_get_netuids_for_hotkey + + # Call + result = subtensor.is_hotkey_registered_any(fake_hotkey_ss58, fake_block) + + # Asserts + mocked_get_netuids_for_hotkey.assert_called_once_with(fake_hotkey_ss58, fake_block) + assert result is (len(return_value) > 0) + + +def test_is_hotkey_registered_on_subnet(subtensor, mocker): + """Tests is_hotkey_registered_on_subnet call.""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + fake_netuid = 1 + fake_block = 123 + + mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet + + # Call + result = subtensor.is_hotkey_registered_on_subnet(fake_hotkey_ss58, fake_netuid, fake_block) + + # Asserts + mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with(fake_hotkey_ss58, fake_netuid, fake_block) + assert result is (mocked_get_uid_for_hotkey_on_subnet.return_value is not None) + + +def test_is_hotkey_registered_without_netuid(subtensor, mocker): + """Tests is_hotkey_registered call with no netuid specified.""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + + mocked_is_hotkey_registered_any = mocker.MagicMock() + subtensor.is_hotkey_registered_any = mocked_is_hotkey_registered_any + + # Call + + result = subtensor.is_hotkey_registered(fake_hotkey_ss58) + + # Asserts + mocked_is_hotkey_registered_any.assert_called_once_with(fake_hotkey_ss58, None) + assert result == mocked_is_hotkey_registered_any.return_value + + +def test_is_hotkey_registered_with_netuid(subtensor, mocker): + """Tests is_hotkey_registered call with netuid specified.""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + fake_netuid = 123 + + mocked_is_hotkey_registered_on_subnet = mocker.MagicMock() + subtensor.is_hotkey_registered_on_subnet = mocked_is_hotkey_registered_on_subnet + + # Call + + result = subtensor.is_hotkey_registered(fake_hotkey_ss58, fake_netuid) + + # Asserts + mocked_is_hotkey_registered_on_subnet.assert_called_once_with(fake_hotkey_ss58, fake_netuid, None) + assert result == mocked_is_hotkey_registered_on_subnet.return_value + + def test_do_set_weights_is_success(subtensor, mocker): """Successful _do_set_weights call.""" # Prep From 64e4c2608660fcb601016edac4e973793e5973e6 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 Aug 2024 14:49:08 -0700 Subject: [PATCH 113/260] ruff + add tests --- tests/unit_tests/test_subtensor.py | 49 +++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 489a4ee20..aa291bd6b 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -868,6 +868,34 @@ def test_get_subnet_hyperparameters_no_data(mocker, subtensor): subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() +def test_query_runtime_api(subtensor, mocker): + """Tests query_runtime_api call.""" + # Prep + fake_runtime_api = "NeuronInfoRuntimeApi" + fake_method = "get_neuron_lite" + + mocked_state_call = mocker.MagicMock() + subtensor.state_call = mocked_state_call + + mocked_runtime_configuration = mocker.patch.object(subtensor_module, "RuntimeConfiguration") + mocked_scalecodec = mocker.patch.object(subtensor_module.scalecodec, "ScaleBytes") + + # Call + result = subtensor.query_runtime_api(fake_runtime_api, fake_method, None) + + # Asserts + subtensor.state_call.assert_called_once_with( + method=f"{fake_runtime_api}_{fake_method}", + data="0x", + block=None + ) + mocked_scalecodec.assert_called_once_with(subtensor.state_call.return_value.__getitem__.return_value) + mocked_runtime_configuration.assert_called_once() + mocked_runtime_configuration.return_value.update_type_registry.assert_called() + mocked_runtime_configuration.return_value.create_scale_object.assert_called() + assert result == mocked_runtime_configuration.return_value.create_scale_object.return_value.decode.return_value + + def test_state_call(subtensor, mocker): """Tests state_call call.""" # Prep @@ -960,10 +988,7 @@ def test_metagraph(subtensor, mocker): # Asserts mocked_metagraph.assert_called_once_with( - network=subtensor.network, - netuid=fake_netuid, - lite=fake_lite, - sync=False + network=subtensor.network, netuid=fake_netuid, lite=fake_lite, sync=False ) mocked_metagraph.return_value.sync.assert_called_once_with( block=None, lite=fake_lite, subtensor=subtensor @@ -984,7 +1009,9 @@ def test_get_netuids_for_hotkey(subtensor, mocker): result = subtensor.get_netuids_for_hotkey(fake_hotkey_ss58, fake_block) # Asserts - mocked_query_map_subtensor.assert_called_once_with("IsNetworkMember", fake_block, [fake_hotkey_ss58]) + mocked_query_map_subtensor.assert_called_once_with( + "IsNetworkMember", fake_block, [fake_hotkey_ss58] + ) assert result == [] @@ -1031,10 +1058,14 @@ def test_is_hotkey_registered_on_subnet(subtensor, mocker): subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet # Call - result = subtensor.is_hotkey_registered_on_subnet(fake_hotkey_ss58, fake_netuid, fake_block) + result = subtensor.is_hotkey_registered_on_subnet( + fake_hotkey_ss58, fake_netuid, fake_block + ) # Asserts - mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with(fake_hotkey_ss58, fake_netuid, fake_block) + mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with( + fake_hotkey_ss58, fake_netuid, fake_block + ) assert result is (mocked_get_uid_for_hotkey_on_subnet.return_value is not None) @@ -1069,7 +1100,9 @@ def test_is_hotkey_registered_with_netuid(subtensor, mocker): result = subtensor.is_hotkey_registered(fake_hotkey_ss58, fake_netuid) # Asserts - mocked_is_hotkey_registered_on_subnet.assert_called_once_with(fake_hotkey_ss58, fake_netuid, None) + mocked_is_hotkey_registered_on_subnet.assert_called_once_with( + fake_hotkey_ss58, fake_netuid, None + ) assert result == mocked_is_hotkey_registered_on_subnet.return_value From d420c1e0ccbbf2e651506ce477311a5de025467e Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 Aug 2024 14:50:10 -0700 Subject: [PATCH 114/260] ruff --- tests/unit_tests/test_subtensor.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index aa291bd6b..c5261d317 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -877,7 +877,9 @@ def test_query_runtime_api(subtensor, mocker): mocked_state_call = mocker.MagicMock() subtensor.state_call = mocked_state_call - mocked_runtime_configuration = mocker.patch.object(subtensor_module, "RuntimeConfiguration") + mocked_runtime_configuration = mocker.patch.object( + subtensor_module, "RuntimeConfiguration" + ) mocked_scalecodec = mocker.patch.object(subtensor_module.scalecodec, "ScaleBytes") # Call @@ -885,15 +887,18 @@ def test_query_runtime_api(subtensor, mocker): # Asserts subtensor.state_call.assert_called_once_with( - method=f"{fake_runtime_api}_{fake_method}", - data="0x", - block=None + method=f"{fake_runtime_api}_{fake_method}", data="0x", block=None + ) + mocked_scalecodec.assert_called_once_with( + subtensor.state_call.return_value.__getitem__.return_value ) - mocked_scalecodec.assert_called_once_with(subtensor.state_call.return_value.__getitem__.return_value) mocked_runtime_configuration.assert_called_once() mocked_runtime_configuration.return_value.update_type_registry.assert_called() mocked_runtime_configuration.return_value.create_scale_object.assert_called() - assert result == mocked_runtime_configuration.return_value.create_scale_object.return_value.decode.return_value + assert ( + result + == mocked_runtime_configuration.return_value.create_scale_object.return_value.decode.return_value + ) def test_state_call(subtensor, mocker): From 4fa1bb5b0081b2a366e2dfde472cab26bfa7abb1 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 Aug 2024 18:20:17 -0700 Subject: [PATCH 115/260] add test --- tests/unit_tests/test_subtensor.py | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index c5261d317..fd445fbb0 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -868,6 +868,27 @@ def test_get_subnet_hyperparameters_no_data(mocker, subtensor): subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() +def test_query_subtensor(subtensor, mocker): + """Tests query_subtensor call.""" + # Prep + fake_name = "module_name" + + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.query_subtensor(fake_name) + + # Asserts + mocked_substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == mocked_substrate.query.return_value + + def test_query_runtime_api(subtensor, mocker): """Tests query_runtime_api call.""" # Prep @@ -901,6 +922,27 @@ def test_query_runtime_api(subtensor, mocker): ) +def test_query_map_subtensor(subtensor, mocker): + """Tests query_map_subtensor call.""" + # Prep + fake_name = "module_name" + + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + + # Call + result = subtensor.query_map_subtensor(fake_name) + + # Asserts + mocked_substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == mocked_substrate.query_map.return_value + + def test_state_call(subtensor, mocker): """Tests state_call call.""" # Prep From e1c0368580b3e3d954fcd273fc2dc9587e68a555 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 Aug 2024 18:24:50 -0700 Subject: [PATCH 116/260] fix unused imports --- tests/unit_tests/test_subtensor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index fd445fbb0..45d436eaf 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -17,15 +17,12 @@ import argparse import unittest.mock as mock -from cgitb import reset from typing import List, Tuple from unittest.mock import MagicMock import pytest from bittensor_wallet import Wallet -from mypy.plugins.singledispatch import make_fake_register_class_instance -from bittensor import wallet from bittensor.core import subtensor as subtensor_module, settings from bittensor.core.axon import Axon from bittensor.core.chain_data import SubnetHyperparameters @@ -33,7 +30,6 @@ from bittensor.core.subtensor import Subtensor, logging from bittensor.utils import u16_normalized_float, u64_normalized_float from bittensor.utils.balance import Balance -from e2e_tests.utils.chain_interactions import register_subnet U16_MAX = 65535 U64_MAX = 18446744073709551615 From 3338fdbe88dbbe37ddd7037cda900cefe4eea985 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 Aug 2024 18:48:49 -0700 Subject: [PATCH 117/260] Config refactoring --- bittensor/core/config.py | 110 ++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 66 deletions(-) diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 9bfa25f67..335ec11ef 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -1,25 +1,22 @@ -""" -Implementation of the config class, which manages the configuration of different Bittensor modules. -""" - # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation - +# 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. +"""Implementation of the config class, which manages the configuration of different Bittensor modules.""" + import argparse import copy import os @@ -34,33 +31,23 @@ class InvalidConfigFile(Exception): """In place of YAMLError""" - pass - -class config(DefaultMunch): +class Config(DefaultMunch): """ Implementation of the config class, which manages the configuration of different Bittensor modules. - """ - __is_set: Dict[str, bool] + Translates the passed parser into a nested Bittensor config. - """ Translates the passed parser into a nested Bittensor config. + Args: + parser (argparse.ArgumentParser): Command line parser object. + strict (bool): If ``true``, the command line arguments are strictly parsed. + args (list of str): Command line arguments. + default (Optional[Any]): Default value for the Config. Defaults to ``None``. This default will be returned for attributes that are undefined. - Args: - parser (argparse.ArgumentParser): - Command line parser object. - strict (bool): - If ``true``, the command line arguments are strictly parsed. - args (list of str): - Command line arguments. - default (Optional[Any]): - Default value for the Config. Defaults to ``None``. - This default will be returned for attributes that are undefined. - - Returns: - config (bittensor.config): - Nested config object created from parser arguments. + Returns: + config (bittensor.config): Nested config object created from parser arguments. """ + __is_set: Dict[str, bool] def __init__( self, @@ -83,7 +70,7 @@ def __init__( type=str, help="If set, defaults are overridden by passed file.", ) - except: + except Exception: # this can fail if --config has already been added. pass @@ -94,7 +81,7 @@ def __init__( help="""If flagged, config will check that only exact arguments have been set.""", default=False, ) - except: + except Exception: # this can fail if --strict has already been added. pass @@ -105,7 +92,7 @@ def __init__( help="Set ``true`` to stop cli version checking.", default=False, ) - except: + except Exception: # this can fail if --no_version_checking has already been added. pass @@ -117,7 +104,7 @@ def __init__( help="Set ``true`` to stop cli from prompting the user.", default=False, ) - except: + except Exception: # this can fail if --no_version_checking has already been added. pass @@ -144,7 +131,7 @@ def __init__( config_file_path = None # Parse args not strict - config_params = config.__parse_args__(args=args, parser=parser, strict=False) + config_params = Config.__parse_args__(args=args, parser=parser, strict=False) # 2. Optionally check for --strict # strict=True when passed in OR when --strict is set @@ -161,12 +148,12 @@ def __init__( print(f"Error in loading: {e} using default parser settings") # 2. Continue with loading in params. - params = config.__parse_args__(args=args, parser=parser, strict=strict) + params = Config.__parse_args__(args=args, parser=parser, strict=strict) _config = self # Splits params and add to config - config.__split_params__(params=params, _config=_config) + Config.__split_params__(params=params, _config=_config) # Make the is_set map _config["__is_set"] = {} @@ -217,7 +204,7 @@ def __init__( cmd_parser._defaults.clear() # Needed for quirk of argparse # Reparse the args, but this time with the defaults as argparse.SUPPRESS - params_no_defaults = config.__parse_args__( + params_no_defaults = Config.__parse_args__( args=args, parser=parser_no_defaults, strict=strict ) @@ -234,8 +221,8 @@ def __init__( } @staticmethod - def __split_params__(params: argparse.Namespace, _config: "config"): - # Splits params on dot syntax i.e neuron.axon_port and adds to _config + def __split_params__(params: argparse.Namespace, _config: "Config"): + # Splits params on dot syntax i.e. neuron.axon_port and adds to _config for arg_key, arg_val in params.__dict__.items(): split_keys = arg_key.split(".") head = _config @@ -247,7 +234,7 @@ def __split_params__(params: argparse.Namespace, _config: "config"): head = getattr(head, keys[0]) keys = keys[1:] else: - head[keys[0]] = config() + head[keys[0]] = Config() head = head[keys[0]] keys = keys[1:] if len(keys) == 1: @@ -260,16 +247,12 @@ def __parse_args__( """Parses the passed args use the passed parser. Args: - args (List[str]): - List of arguments to parse. - parser (argparse.ArgumentParser): - Command line parser object. - strict (bool): - If ``true``, the command line arguments are strictly parsed. + args (List[str]): List of arguments to parse. + parser (argparse.ArgumentParser): Command line parser object. + strict (bool): If ``true``, the command line arguments are strictly parsed. Returns: - Namespace: - Namespace object created from parser arguments. + Namespace: Namespace object created from parser arguments. """ if not strict: params, unrecognized = parser.parse_known_args(args=args) @@ -284,11 +267,11 @@ def __parse_args__( return params - def __deepcopy__(self, memo) -> "config": + def __deepcopy__(self, memo) -> "Config": _default = self.__default__ config_state = self.__getstate__() - config_copy = config() + config_copy = Config() memo[id(self)] = config_copy config_copy.__setstate__(config_state) @@ -309,7 +292,7 @@ def _remove_private_keys(d): d.pop("__is_set", None) for k, v in list(d.items()): if isinstance(v, dict): - config._remove_private_keys(v) + Config._remove_private_keys(v) return d def __str__(self) -> str: @@ -317,13 +300,14 @@ def __str__(self) -> str: visible = copy.deepcopy(self.toDict()) visible.pop("__parser", None) visible.pop("__is_set", None) - cleaned = config._remove_private_keys(visible) + cleaned = Config._remove_private_keys(visible) return "\n" + yaml.dump(cleaned, sort_keys=False) - def copy(self) -> "config": + def copy(self) -> "Config": return copy.deepcopy(self) - def to_string(self, items) -> str: + @staticmethod + def to_string(items) -> str: """Get string from items""" return "\n" + yaml.dump(items.toDict()) @@ -352,24 +336,21 @@ def merge(self, b): """ Merges the current config with another config. - Args: - b: Another config to merge. + Args: b: Another config to merge. """ - self = self._merge(self, b) + self._merge(self, b) @classmethod - def merge_all(cls, configs: List["config"]) -> "config": + def merge_all(cls, configs: List["Config"]) -> "Config": """ Merge all configs in the list into one config. If there is a conflict, the value from the last configuration in the list will take precedence. Args: - configs (list of config): - List of configs to be merged. + configs (list of config): List of configs to be merged. Returns: - config: - Merged config object. + config: Merged config object. """ result = cls() for cfg in configs: @@ -406,13 +387,10 @@ def __get_required_args_from_parser(parser: argparse.ArgumentParser) -> List[str T = TypeVar("T", bound="DefaultConfig") -class DefaultConfig(config): +class DefaultConfig(Config): """A Config with a set of default values.""" @classmethod def default(cls: Type[T]) -> T: """Get default config.""" raise NotImplementedError("Function default is not implemented.") - - -Config = config From ac35a493396d74273a2abd9c8575b673a5eb9a1a Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 08:50:26 -0700 Subject: [PATCH 118/260] Remove duplicated tests --- tests/unit_tests/utils/test_utils.py | 315 +-------------------------- 1 file changed, 5 insertions(+), 310 deletions(-) diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index 3c077aba7..ea9731ad0 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -1,16 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 @@ -20,309 +18,6 @@ import logging import numpy as np -import bittensor.utils.weight_utils as weight_utils +from bittensor import utils import pytest - -def test_convert_weight_and_uids(): - uids = np.arange(10) - weights = np.random.rand(10) - weight_utils.convert_weights_and_uids_for_emit(uids, weights) - - # min weight < 0 - weights[5] = -1 - with pytest.raises(ValueError) as pytest_wrapped_e: - weight_utils.convert_weights_and_uids_for_emit(uids, weights) - - # min uid < 0 - weights[5] = 0 - uids[3] = -1 - with pytest.raises(ValueError) as pytest_wrapped_e: - weight_utils.convert_weights_and_uids_for_emit(uids, weights) - - # len(uids) != len(weights) - uids[3] = 3 - with pytest.raises(ValueError) as pytest_wrapped_e: - weight_utils.convert_weights_and_uids_for_emit(uids, weights[1:]) - - # sum(weights) == 0 - weights = np.zeros(10) - weight_utils.convert_weights_and_uids_for_emit(uids, weights) - - # test for overflow and underflow - for _ in range(5): - uids = np.arange(10) - weights = np.random.rand(10) - weight_utils.convert_weights_and_uids_for_emit(uids, weights) - - -def test_normalize_with_max_weight(): - weights = np.random.rand(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.01) - assert wn.max() <= 0.01 - - weights = np.zeros(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.01) - assert wn.max() <= 0.01 - - weights = np.random.rand(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.02) - assert wn.max() <= 0.02 - - weights = np.zeros(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.02) - assert wn.max() <= 0.02 - - weights = np.random.rand(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.03) - assert wn.max() <= 0.03 - - weights = np.zeros(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.03) - assert wn.max() <= 0.03 - - # Check for Limit - limit = 0.001 - weights = np.random.rand(2000) - w = weights / weights.sum() - wn = weight_utils.normalize_max_weight(weights, limit=limit) - assert (w.max() >= limit and np.abs(limit - wn.max()) < 0.001) or ( - w.max() < limit and wn.max() < limit - ) - - # Check for Zeros - limit = 0.01 - weights = np.zeros(2000) - wn = weight_utils.normalize_max_weight(weights, limit=limit) - assert wn.max() == 1 / 2000 - - # Check for Ordering after normalization - weights = np.random.rand(100) - wn = weight_utils.normalize_max_weight(weights, limit=1) - assert np.array_equal(wn, weights / weights.sum()) - - # Check for epsilon changes - epsilon = 0.01 - weights = np.sort(np.random.rand(100)) - x = weights / weights.sum() - limit = x[-10] - change = epsilon * limit - y = weight_utils.normalize_max_weight(x, limit=limit - change) - z = weight_utils.normalize_max_weight(x, limit=limit + change) - assert np.abs(y - z).sum() < epsilon - - -@pytest.mark.parametrize( - "test_id, n, uids, weights, expected", - [ - ("happy-path-1", 3, [0, 1, 2], [15, 5, 80], np.array([0.15, 0.05, 0.8])), - ("happy-path-2", 4, [1, 3], [50, 50], np.array([0.0, 0.5, 0.0, 0.5])), - ], -) -def test_convert_weight_uids_and_vals_to_tensor_happy_path( - test_id, n, uids, weights, expected -): - # Act - result = weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) - - # Assert - assert np.allclose(result, expected), f"Failed {test_id}" - - -@pytest.mark.parametrize( - "test_id, n, uids, weights, expected", - [ - ("edge_case_empty", 5, [], [], np.zeros(5)), - ("edge_case_single", 1, [0], [100], np.array([1.0])), - ("edge_case_all_zeros", 4, [0, 1, 2, 3], [0, 0, 0, 0], np.zeros(4)), - ], -) -def test_convert_weight_uids_and_vals_to_tensor_edge_cases( - test_id, n, uids, weights, expected -): - # Act - result = weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) - - # Assert - assert np.allclose(result, expected), f"Failed {test_id}" - - -@pytest.mark.parametrize( - "test_id, n, uids, weights, exception", - [ - ("error-case-mismatched-lengths", 3, [0, 1, 3, 4, 5], [10, 20, 30], IndexError), - ("error-case-negative-n", -1, [0, 1], [10, 20], ValueError), - ("error-case-invalid-uids", 3, [0, 3], [10, 20], IndexError), - ], -) -def test_convert_weight_uids_and_vals_to_tensor_error_cases( - test_id, n, uids, weights, exception -): - # Act / Assert - with pytest.raises(exception): - weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) - - -@pytest.mark.parametrize( - "test_id, n, uids, weights, subnets, expected", - [ - ( - "happy-path-1", - 3, - [0, 1, 2], - [15, 5, 80], - [0, 1, 2], - np.array([0.15, 0.05, 0.8]), - ), - ( - "happy-path-2", - 3, - [0, 2], - [300, 300], - [0, 1, 2], - np.array([0.5, 0.0, 0.5]), - ), - ], -) -def test_convert_root_weight_uids_and_vals_to_tensor_happy_paths( - test_id, n, uids, weights, subnets, expected -): - # Act - result = weight_utils.convert_root_weight_uids_and_vals_to_tensor( - n, uids, weights, subnets - ) - - # Assert - assert np.allclose(result, expected, atol=1e-4), f"Failed {test_id}" - - -@pytest.mark.parametrize( - "test_id, n, uids, weights, subnets, expected", - [ - ( - "edge-1", - 1, - [0], - [0], - [0], - np.array([0.0]), - ), # Single neuron with zero weight - ( - "edge-2", - 2, - [0, 1], - [0, 0], - [0, 1], - np.array([0.0, 0.0]), - ), # All zero weights - ], -) -def test_convert_root_weight_uids_and_vals_to_tensor_edge_cases( - test_id, n, uids, weights, subnets, expected -): - # Act - result = weight_utils.convert_root_weight_uids_and_vals_to_tensor( - n, uids, weights, subnets - ) - - # Assert - assert np.allclose(result, expected, atol=1e-4), f"Failed {test_id}" - - -@pytest.mark.parametrize( - "test_id, n, uids, weights, subnets, exception", - [ - # uid not in subnets - ( - "error-1", - 3, - [1, 3], - [100, 200], - [1, 2], - "The subnet is unavailable at the moment.", - ), - # More uids than subnets - ( - "error-2", - 3, - [1, 2, 3], - [100, 200], - [1], - "The subnet is unavailable at the moment.", - ), - ], -) -def test_convert_root_weight_uids_and_vals_to_tensor_error_cases( - test_id, n, uids, weights, subnets, exception, caplog -): - with caplog.at_level(logging.WARNING): - weight_utils.convert_root_weight_uids_and_vals_to_tensor( - n, uids, weights, subnets - ) - - assert any( - exception in record.message and record.levelname == "WARNING" - for record in caplog.records - ) - - -@pytest.mark.parametrize( - "test_id, n, uids, bonds, expected_output", - [ - ( - "happy-path-1", - 5, - [1, 3, 4], - [10, 20, 30], - np.array([0, 10, 0, 20, 30], dtype=np.int64), - ), - ( - "happy-path-2", - 3, - [0, 1, 2], - [7, 8, 9], - np.array([7, 8, 9], dtype=np.int64), - ), - ("happy-path-3", 4, [2], [15], np.array([0, 0, 15, 0], dtype=np.int64)), - ], -) -def test_happy_path(test_id, n, uids, bonds, expected_output): - # Act - result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) - - # Assert - assert np.array_equal(result, expected_output), f"Failed {test_id}" - - -@pytest.mark.parametrize( - "test_id, n, uids, bonds, expected_output", - [ - ("edge-1", 1, [0], [0], np.array([0], dtype=np.int64)), # Single element - ( - "edge-2", - 10, - [], - [], - np.zeros(10, dtype=np.int64), - ), # Empty uids and bonds - ], -) -def test_edge_cases(test_id, n, uids, bonds, expected_output): - # Act - result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) - - # Assert - assert np.array_equal(result, expected_output), f"Failed {test_id}" - - -@pytest.mark.parametrize( - "test_id, n, uids, bonds, exception", - [ - ("error-1", 5, [1, 3, 6], [10, 20, 30], IndexError), # uid out of bounds - ("error-2", -1, [0], [10], ValueError), # Negative number of neurons - ], -) -def test_error_cases(test_id, n, uids, bonds, exception): - # Act / Assert - with pytest.raises(exception): - weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) From 4f686c57d906d784e4102c6224b31b7a40b72c3f Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 09:38:07 -0700 Subject: [PATCH 119/260] add test --- tests/unit_tests/utils/test_utils.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index ea9731ad0..55c5880f7 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -16,8 +16,23 @@ # DEALINGS IN THE SOFTWARE. import logging +from symbol import return_stmt import numpy as np -from bittensor import utils +from bittensor import utils, ss58_address_to_bytes import pytest + +def test_ss58_to_vec_u8(mocker): + """Tests `utils.ss58_to_vec_u8` function.""" + # Prep + test_ss58_address = "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" + fake_return = b'2\xa6?' + mocked_ss58_address_to_bytes = mocker.patch.object(utils, "ss58_address_to_bytes", return_value=fake_return) + + # Call + result = utils.ss58_to_vec_u8(test_ss58_address) + + # Asserts + mocked_ss58_address_to_bytes.assert_called_once_with(test_ss58_address) + assert result == [int(byte) for byte in fake_return] From 4a81610bcc14face3575024557d0c65261e92c51 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 09:38:26 -0700 Subject: [PATCH 120/260] remove duplicated import --- bittensor/utils/deprecated/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/utils/deprecated/__init__.py b/bittensor/utils/deprecated/__init__.py index 4dec76744..560916dfc 100644 --- a/bittensor/utils/deprecated/__init__.py +++ b/bittensor/utils/deprecated/__init__.py @@ -107,7 +107,6 @@ strtobool, strtobool_with_default, get_explorer_root_url_by_network_from_map, - get_explorer_root_url_by_network_from_map, get_explorer_url_for_network, ss58_address_to_bytes, u16_normalized_float, From 3918a2e872322fb308938ad4b7effd4e01fd5715 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 09:39:32 -0700 Subject: [PATCH 121/260] remove unused functions --- bittensor/utils/__init__.py | 80 +------------------------- bittensor/utils/deprecated/__init__.py | 1 - 2 files changed, 1 insertion(+), 80 deletions(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 2423b6b39..a6a2201db 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -16,9 +16,8 @@ # DEALINGS IN THE SOFTWARE. import hashlib -from typing import Callable, List, Dict, Literal, Tuple, Union, Optional +from typing import Callable, List, Dict, Literal, Union, Optional -import numpy as np import scalecodec from substrateinterface import Keypair as Keypair from substrateinterface.utils import ss58 @@ -38,83 +37,6 @@ def ss58_to_vec_u8(ss58_address: str) -> List[int]: return encoded_address -def _unbiased_topk( - values: Union[np.ndarray, "torch.Tensor"], - k: int, - dim=0, - sorted_=True, - largest=True, - axis=0, - return_type: str = "numpy", -) -> Union[Tuple[np.ndarray, np.ndarray], Tuple["torch.Tensor", "torch.LongTensor"]]: - """Selects topk as in torch.topk but does not bias lower indices when values are equal. - Args: - values: (np.ndarray) if using numpy, (torch.Tensor) if using torch: Values to index into. - k (int): Number to take. - dim (int): Dimension to index into (used by Torch) - sorted_ (bool): Whether to sort indices. - largest (bool): Whether to take the largest value. - axis (int): Axis along which to index into (used by Numpy) - return_type (str): Whether or use torch or numpy approach - - Return: - topk (np.ndarray): if using numpy, (torch.Tensor) if using torch: topk k values. - indices (np.ndarray): if using numpy, (torch.LongTensor) if using torch: indices of the topk values. - """ - if return_type == "torch": - permutation = torch.randperm(values.shape[dim]) - permuted_values = values[permutation] - topk, indices = torch.topk( - permuted_values, k, dim=dim, sorted=sorted_, largest=largest - ) - return topk, permutation[indices] - else: - if dim != 0 and axis == 0: - # Ensures a seamless transition for calls made to this function that specified args by keyword - axis = dim - - permutation = np.random.permutation(values.shape[axis]) - permuted_values = np.take(values, permutation, axis=axis) - indices = np.argpartition(permuted_values, -k, axis=axis)[-k:] - if not sorted_: - indices = np.sort(indices, axis=axis) - if not largest: - indices = indices[::-1] - topk = np.take(permuted_values, indices, axis=axis) - return topk, permutation[indices] - - -def unbiased_topk( - values: Union[np.ndarray, "torch.Tensor"], - k: int, - dim: int = 0, - sorted_: bool = True, - largest: bool = True, - axis: int = 0, -) -> Union[Tuple[np.ndarray, np.ndarray], Tuple["torch.Tensor", "torch.LongTensor"]]: - """Selects topk as in torch.topk but does not bias lower indices when values are equal. - Args: - values: (np.ndarray) if using numpy, (torch.Tensor) if using torch: Values to index into. - k: (int): Number to take. - dim: (int): Dimension to index into (used by Torch) - sorted_: (bool): Whether to sort indices. - largest: (bool): Whether to take the largest value. - axis: (int): Axis along which to index into (used by Numpy) - - Return: - topk: (np.ndarray) if using numpy, (torch.Tensor) if using torch: topk k values. - indices: (np.ndarray) if using numpy, (torch.LongTensor) if using torch: indices of the topk values. - """ - if use_torch(): - return _unbiased_topk( - values, k, dim, sorted_, largest, axis, return_type="torch" - ) - else: - return _unbiased_topk( - values, k, dim, sorted_, largest, axis, return_type="numpy" - ) - - def strtobool_with_default( default: bool, ) -> Callable[[str], Union[bool, Literal["==SUPRESS=="]]]: diff --git a/bittensor/utils/deprecated/__init__.py b/bittensor/utils/deprecated/__init__.py index 560916dfc..867edca1f 100644 --- a/bittensor/utils/deprecated/__init__.py +++ b/bittensor/utils/deprecated/__init__.py @@ -102,7 +102,6 @@ ) from bittensor.utils import ( # noqa: F401 ss58_to_vec_u8, - unbiased_topk, version_checking, strtobool, strtobool_with_default, From a6b4105b9a74d68d1bd863925df9a3c07b8947c2 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 09:53:17 -0700 Subject: [PATCH 122/260] remove unused functions + add tests --- bittensor/utils/__init__.py | 21 ++--------- bittensor/utils/deprecated/__init__.py | 2 - tests/unit_tests/utils/test_utils.py | 52 ++++++++++++++++++++++---- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index a6a2201db..899566cdf 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. import hashlib -from typing import Callable, List, Dict, Literal, Union, Optional +from typing import List, Dict, Literal, Union, Optional import scalecodec from substrateinterface import Keypair as Keypair @@ -37,21 +37,6 @@ def ss58_to_vec_u8(ss58_address: str) -> List[int]: return encoded_address -def strtobool_with_default( - default: bool, -) -> Callable[[str], Union[bool, Literal["==SUPRESS=="]]]: - """ - Creates a strtobool function with a default value. - - Args: - default(bool): The default value to return if the string is empty. - - Returns: - The strtobool function with the default value. - """ - return lambda x: strtobool(x) if x != "" else default - - def strtobool(val: str) -> Union[bool, Literal["==SUPRESS=="]]: """ Converts a string to a boolean value. @@ -70,7 +55,7 @@ def strtobool(val: str) -> Union[bool, Literal["==SUPRESS=="]]: raise ValueError("invalid truth value %r" % (val,)) -def get_explorer_root_url_by_network_from_map( +def _get_explorer_root_url_by_network_from_map( network: str, network_map: Dict[str, Dict[str, str]] ) -> Optional[Dict[str, str]]: """ @@ -111,7 +96,7 @@ def get_explorer_url_for_network( explorer_urls: Optional[Dict[str, str]] = {} # Will be None if the network is not known. i.e. not in network_map explorer_root_urls: Optional[Dict[str, str]] = ( - get_explorer_root_url_by_network_from_map(network, network_map) + _get_explorer_root_url_by_network_from_map(network, network_map) ) if explorer_root_urls != {}: diff --git a/bittensor/utils/deprecated/__init__.py b/bittensor/utils/deprecated/__init__.py index 867edca1f..4ec86544d 100644 --- a/bittensor/utils/deprecated/__init__.py +++ b/bittensor/utils/deprecated/__init__.py @@ -104,8 +104,6 @@ ss58_to_vec_u8, version_checking, strtobool, - strtobool_with_default, - get_explorer_root_url_by_network_from_map, get_explorer_url_for_network, ss58_address_to_bytes, u16_normalized_float, diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index 55c5880f7..24722e3f3 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -15,11 +15,7 @@ # 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 logging -from symbol import return_stmt - -import numpy as np -from bittensor import utils, ss58_address_to_bytes +from bittensor import utils import pytest @@ -27,8 +23,10 @@ def test_ss58_to_vec_u8(mocker): """Tests `utils.ss58_to_vec_u8` function.""" # Prep test_ss58_address = "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - fake_return = b'2\xa6?' - mocked_ss58_address_to_bytes = mocker.patch.object(utils, "ss58_address_to_bytes", return_value=fake_return) + fake_return = b"2\xa6?" + mocked_ss58_address_to_bytes = mocker.patch.object( + utils, "ss58_address_to_bytes", return_value=fake_return + ) # Call result = utils.ss58_to_vec_u8(test_ss58_address) @@ -36,3 +34,43 @@ def test_ss58_to_vec_u8(mocker): # Asserts mocked_ss58_address_to_bytes.assert_called_once_with(test_ss58_address) assert result == [int(byte) for byte in fake_return] + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("y", True), + ("yes", True), + ("t", True), + ("true", True), + ("on", True), + ("1", True), + ("n", False), + ("no", False), + ("f", False), + ("false", False), + ("off", False), + ("0", False), + ], +) +def test_strtobool(test_input, expected): + """Test truthy values.""" + assert utils.strtobool(test_input) is expected + + +@pytest.mark.parametrize( + "test_input", + [ + "maybe", + "2", + "onoff", + ], +) +def test_strtobool_raise_error(test_input): + """Tests invalid values.""" + with pytest.raises(ValueError): + utils.strtobool(test_input) + + +def test_get_explorer_root_url_by_network_from_map(): + pass From f155729636a67e4f2744c553408d4fe16db560ee Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 09:53:23 -0700 Subject: [PATCH 123/260] ruff --- bittensor/core/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 335ec11ef..9d0c18348 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -47,6 +47,7 @@ class Config(DefaultMunch): Returns: config (bittensor.config): Nested config object created from parser arguments. """ + __is_set: Dict[str, bool] def __init__( From 599ea3c7c9e9c9a8748992f877e2b97670b79f06 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 10:18:52 -0700 Subject: [PATCH 124/260] fix type annotation --- bittensor/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 899566cdf..c915254b0 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -78,7 +78,7 @@ def _get_explorer_root_url_by_network_from_map( def get_explorer_url_for_network( - network: str, block_hash: str, network_map: Dict[str, str] + network: str, block_hash: str, network_map: Dict[str, Dict[str, str]] ) -> Optional[Dict[str, str]]: """ Returns the explorer url for the given block hash and network. From b3c9fc95a005c67339a99c27f64d1f27c29ecf5a Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 11:46:00 -0700 Subject: [PATCH 125/260] add tests --- tests/unit_tests/utils/test_utils.py | 51 +++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index 24722e3f3..d3feb1a5f 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -14,8 +14,10 @@ # 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. +from symbol import return_stmt from bittensor import utils +from bittensor.core.settings import SS58_FORMAT import pytest @@ -73,4 +75,51 @@ def test_strtobool_raise_error(test_input): def test_get_explorer_root_url_by_network_from_map(): - pass + """Tests private utils._get_explorer_root_url_by_network_from_map function.""" + # Prep + # Test with a known network + network_map = { + "entity1": {"network1": "url1", "network2": "url2"}, + "entity2": {"network1": "url3", "network3": "url4"}, + } + # Test with no matching network in the map + network_map_empty = { + "entity1": {}, + "entity2": {}, + } + + # Assertions + assert utils._get_explorer_root_url_by_network_from_map("network1", network_map) == { + "entity1": "url1", + "entity2": "url3", + } + # Test with an unknown network + assert utils._get_explorer_root_url_by_network_from_map("unknown_network", network_map) == {} + assert utils._get_explorer_root_url_by_network_from_map("network1", network_map_empty) == {} + + +def test_get_explorer_url_for_network(): + """Tests `utils.get_explorer_url_for_network` function.""" + # Prep + fake_block_hash = "0x1234567890abcdef" + fake_map = {"opentensor": {"network": "url"}, "taostats": {"network": "url2"}} + + # Call + result = utils.get_explorer_url_for_network("network", fake_block_hash, fake_map) + + # Assert + assert result == {'opentensor': f'url/query/{fake_block_hash}', 'taostats': f'url2/extrinsic/{fake_block_hash}'} + + +def test_ss58_address_to_bytes(mocker): + """Tests utils.ss58_address_to_bytes function.""" + # Preps + fake_ss58_address = "ss58_address" + mocked_scalecodec_ss58_decode = mocker.patch.object(utils.scalecodec, "ss58_decode", return_value="") + + # Call + result = utils.ss58_address_to_bytes(fake_ss58_address) + + # Asserts + mocked_scalecodec_ss58_decode.assert_called_once_with(fake_ss58_address, SS58_FORMAT) + assert result == bytes.fromhex(mocked_scalecodec_ss58_decode.return_value) From e2b013882309698158471284b501295ae4476fa8 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 16:59:28 -0700 Subject: [PATCH 126/260] Add tests and remove unused functions --- bittensor/utils/__init__.py | 76 +------------------------- bittensor/utils/deprecated/__init__.py | 1 - tests/unit_tests/utils/test_utils.py | 23 +++++++- 3 files changed, 23 insertions(+), 77 deletions(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index c915254b0..d4fe49dde 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -127,17 +127,6 @@ def u64_normalized_float(x: int) -> float: return float(x) / float(U64_MAX) -def u8_key_to_ss58(u8_key: List[int]) -> str: - """ - Converts a u8-encoded account key to an ss58 address. - - Args: - u8_key (List[int]): The u8-encoded account key. - """ - # First byte is length, then 32 bytes of key. - return scalecodec.ss58_encode(bytes(u8_key).hex(), SS58_FORMAT) - - def get_hash(content, encoding="utf-8"): sha3 = hashlib.sha3_256() @@ -170,70 +159,7 @@ def format_error_message(error_message: dict) -> str: return f"Subtensor returned `{err_name} ({err_type})` error. This means: `{err_description}`" -def create_identity_dict( - display: str = "", - legal: str = "", - web: str = "", - riot: str = "", - email: str = "", - pgp_fingerprint: Optional[str] = None, - image: str = "", - info: str = "", - twitter: str = "", -) -> dict: - """ - Creates a dictionary with structure for identity extrinsic. Must fit within 64 bits. - - Args: - display (str): String to be converted and stored under 'display'. - legal (str): String to be converted and stored under 'legal'. - web (str): String to be converted and stored under 'web'. - riot (str): String to be converted and stored under 'riot'. - email (str): String to be converted and stored under 'email'. - pgp_fingerprint (str): String to be converted and stored under 'pgp_fingerprint'. - image (str): String to be converted and stored under 'image'. - info (str): String to be converted and stored under 'info'. - twitter (str): String to be converted and stored under 'twitter'. - - Returns: - dict: A dictionary with the specified structure and byte string conversions. - - Raises: - ValueError: If pgp_fingerprint is not exactly 20 bytes long when encoded. - """ - if pgp_fingerprint and len(pgp_fingerprint.encode()) != 20: - raise ValueError("pgp_fingerprint must be exactly 20 bytes long when encoded") - - return { - "info": { - "additional": [[]], - "display": {f"Raw{len(display.encode())}": display.encode()}, - "legal": {f"Raw{len(legal.encode())}": legal.encode()}, - "web": {f"Raw{len(web.encode())}": web.encode()}, - "riot": {f"Raw{len(riot.encode())}": riot.encode()}, - "email": {f"Raw{len(email.encode())}": email.encode()}, - "pgp_fingerprint": pgp_fingerprint.encode() if pgp_fingerprint else None, - "image": {f"Raw{len(image.encode())}": image.encode()}, - "info": {f"Raw{len(info.encode())}": info.encode()}, - "twitter": {f"Raw{len(twitter.encode())}": twitter.encode()}, - } - } - - -def decode_hex_identity_dict(info_dictionary): - for key, value in info_dictionary.items(): - if isinstance(value, dict): - item = list(value.values())[0] - if isinstance(item, str) and item.startswith("0x"): - try: - info_dictionary[key] = bytes.fromhex(item[2:]).decode() - except UnicodeDecodeError: - print(f"Could not decode: {key}: {item}") - else: - info_dictionary[key] = item - return info_dictionary - - +# Subnet 24 uses this function def is_valid_ss58_address(address: str) -> bool: """ Checks if the given address is a valid ss58 address. diff --git a/bittensor/utils/deprecated/__init__.py b/bittensor/utils/deprecated/__init__.py index 4ec86544d..a6565686d 100644 --- a/bittensor/utils/deprecated/__init__.py +++ b/bittensor/utils/deprecated/__init__.py @@ -108,7 +108,6 @@ ss58_address_to_bytes, u16_normalized_float, u64_normalized_float, - u8_key_to_ss58, get_hash, ) from bittensor.utils.balance import Balance as Balance # noqa: F401 diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index d3feb1a5f..aee4b064b 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -113,7 +113,7 @@ def test_get_explorer_url_for_network(): def test_ss58_address_to_bytes(mocker): """Tests utils.ss58_address_to_bytes function.""" - # Preps + # Prep fake_ss58_address = "ss58_address" mocked_scalecodec_ss58_decode = mocker.patch.object(utils.scalecodec, "ss58_decode", return_value="") @@ -123,3 +123,24 @@ def test_ss58_address_to_bytes(mocker): # Asserts mocked_scalecodec_ss58_decode.assert_called_once_with(fake_ss58_address, SS58_FORMAT) assert result == bytes.fromhex(mocked_scalecodec_ss58_decode.return_value) + + +@pytest.mark.parametrize( + "test_input, expected_result", + [ + (123, False), + ("0x234SD", True), + (b"0x234SD", True), + ], +) +def test_is_valid_bittensor_address_or_public_key(mocker, test_input, expected_result): + """ Tests utils.is_valid_bittensor_address_or_public_key function.""" + # Prep + fake_address = "some_address" + mocked_is_valid_ss58_address = mocker.patch.object(utils, "_is_valid_ed25519_pubkey", return_value=True) + + # Call + result = utils.is_valid_bittensor_address_or_public_key(test_input) + + # Asserts + assert result == expected_result \ No newline at end of file From 7fa9988b4c79327aa5ce68142f5c228af05d9997 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 18:55:48 -0700 Subject: [PATCH 127/260] fix test --- tests/unit_tests/utils/test_utils.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index aee4b064b..681fe6d54 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -16,6 +16,8 @@ # DEALINGS IN THE SOFTWARE. from symbol import return_stmt +from more_itertools.more import side_effect + from bittensor import utils from bittensor.core.settings import SS58_FORMAT import pytest @@ -130,17 +132,22 @@ def test_ss58_address_to_bytes(mocker): [ (123, False), ("0x234SD", True), + ("5D34SD", True), (b"0x234SD", True), ], ) def test_is_valid_bittensor_address_or_public_key(mocker, test_input, expected_result): """ Tests utils.is_valid_bittensor_address_or_public_key function.""" # Prep - fake_address = "some_address" - mocked_is_valid_ss58_address = mocker.patch.object(utils, "_is_valid_ed25519_pubkey", return_value=True) + mocked_is_valid_ed25519_pubkey = mocker.patch.object(utils, "_is_valid_ed25519_pubkey", return_value=True) + mocked_ss58_is_valid_ss58_address = mocker.patch.object(utils.ss58, "is_valid_ss58_address", side_effect=[False, True]) # Call result = utils.is_valid_bittensor_address_or_public_key(test_input) # Asserts - assert result == expected_result \ No newline at end of file + if not isinstance(test_input, int) and isinstance(test_input, bytes): + mocked_is_valid_ed25519_pubkey.assert_called_with(test_input) + if isinstance(test_input, str) and not test_input.startswith("0x"): + assert mocked_ss58_is_valid_ss58_address.call_count == 2 + assert result == expected_result From 3350b09589100c1d95701f8e86a92a0af3db8917 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 19:05:56 -0700 Subject: [PATCH 128/260] ruff --- tests/unit_tests/utils/test_utils.py | 37 +++++++++++++++++++++------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index 681fe6d54..9f549cfb6 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -91,13 +91,21 @@ def test_get_explorer_root_url_by_network_from_map(): } # Assertions - assert utils._get_explorer_root_url_by_network_from_map("network1", network_map) == { + assert utils._get_explorer_root_url_by_network_from_map( + "network1", network_map + ) == { "entity1": "url1", "entity2": "url3", } # Test with an unknown network - assert utils._get_explorer_root_url_by_network_from_map("unknown_network", network_map) == {} - assert utils._get_explorer_root_url_by_network_from_map("network1", network_map_empty) == {} + assert ( + utils._get_explorer_root_url_by_network_from_map("unknown_network", network_map) + == {} + ) + assert ( + utils._get_explorer_root_url_by_network_from_map("network1", network_map_empty) + == {} + ) def test_get_explorer_url_for_network(): @@ -110,20 +118,27 @@ def test_get_explorer_url_for_network(): result = utils.get_explorer_url_for_network("network", fake_block_hash, fake_map) # Assert - assert result == {'opentensor': f'url/query/{fake_block_hash}', 'taostats': f'url2/extrinsic/{fake_block_hash}'} + assert result == { + "opentensor": f"url/query/{fake_block_hash}", + "taostats": f"url2/extrinsic/{fake_block_hash}", + } def test_ss58_address_to_bytes(mocker): """Tests utils.ss58_address_to_bytes function.""" # Prep fake_ss58_address = "ss58_address" - mocked_scalecodec_ss58_decode = mocker.patch.object(utils.scalecodec, "ss58_decode", return_value="") + mocked_scalecodec_ss58_decode = mocker.patch.object( + utils.scalecodec, "ss58_decode", return_value="" + ) # Call result = utils.ss58_address_to_bytes(fake_ss58_address) # Asserts - mocked_scalecodec_ss58_decode.assert_called_once_with(fake_ss58_address, SS58_FORMAT) + mocked_scalecodec_ss58_decode.assert_called_once_with( + fake_ss58_address, SS58_FORMAT + ) assert result == bytes.fromhex(mocked_scalecodec_ss58_decode.return_value) @@ -137,10 +152,14 @@ def test_ss58_address_to_bytes(mocker): ], ) def test_is_valid_bittensor_address_or_public_key(mocker, test_input, expected_result): - """ Tests utils.is_valid_bittensor_address_or_public_key function.""" + """Tests utils.is_valid_bittensor_address_or_public_key function.""" # Prep - mocked_is_valid_ed25519_pubkey = mocker.patch.object(utils, "_is_valid_ed25519_pubkey", return_value=True) - mocked_ss58_is_valid_ss58_address = mocker.patch.object(utils.ss58, "is_valid_ss58_address", side_effect=[False, True]) + mocked_is_valid_ed25519_pubkey = mocker.patch.object( + utils, "_is_valid_ed25519_pubkey", return_value=True + ) + mocked_ss58_is_valid_ss58_address = mocker.patch.object( + utils.ss58, "is_valid_ss58_address", side_effect=[False, True] + ) # Call result = utils.is_valid_bittensor_address_or_public_key(test_input) From 0576bc546a083438c445dc3aa8b491e1191b2f3a Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 19:06:20 -0700 Subject: [PATCH 129/260] Add balance tests, small refactoring --- tests/unit_tests/utils/test_balance.py | 28 ++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/unit_tests/utils/test_balance.py b/tests/unit_tests/utils/test_balance.py index bf9ac10bb..b294df4f4 100644 --- a/tests/unit_tests/utils/test_balance.py +++ b/tests/unit_tests/utils/test_balance.py @@ -1,14 +1,15 @@ +"""Test the Balance class.""" + +from typing import Union + import pytest from hypothesis import given from hypothesis import strategies as st -from typing import Union from bittensor.utils.balance import Balance from tests.helpers import CLOSE_IN_VALUE -""" -Test the Balance class -""" + valid_tao_numbers_strategy = st.one_of( st.integers(max_value=21_000_000, min_value=-21_000_000), st.floats( @@ -439,7 +440,7 @@ def test_balance_not_eq_none(balance: Union[int, float]): Test the inequality (!=) of a Balance object and None. """ balance_ = Balance(balance) - assert not balance_ == None + assert balance_ is not None @given(balance=valid_tao_numbers_strategy) @@ -448,7 +449,7 @@ def test_balance_neq_none(balance: Union[int, float]): Test the inequality (!=) of a Balance object and None. """ balance_ = Balance(balance) - assert balance_ != None + assert balance_ is not None def test_balance_init_from_invalid_value(): @@ -507,3 +508,18 @@ def test_balance_eq_invalid_type(balance: Union[int, float]): balance_ = Balance(balance) with pytest.raises(NotImplementedError): balance_ == "" + + +def test_from_float(): + """Tests from_float method call.""" + assert Balance.from_tao(1.0) == Balance(1000000000) + + +def test_from_tao(): + """Tests from_tao method call.""" + assert Balance.from_tao(1.0) == Balance(1000000000) + + +def test_from_rao(): + """Tests from_rao method call.""" + assert Balance.from_tao(1) == Balance(1000000000) From f7002c4ce8e730f7f228f55fce800e636e094f3d Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 20:41:06 -0700 Subject: [PATCH 130/260] Add init test for bittensor --- tests/unit_tests/utils/test_init.py | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/unit_tests/utils/test_init.py diff --git a/tests/unit_tests/utils/test_init.py b/tests/unit_tests/utils/test_init.py new file mode 100644 index 000000000..6a77e0f0a --- /dev/null +++ b/tests/unit_tests/utils/test_init.py @@ -0,0 +1,40 @@ +import pytest + +from bittensor import warnings, __getattr__, version_split, logging, trace, debug + + +def test_getattr_version_split(): + """Test that __getattr__ for 'version_split' issues a deprecation warning and returns the correct value.""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert __getattr__("version_split") == version_split + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "version_split is deprecated" in str(w[-1].message) + + +@pytest.mark.parametrize( + 'test_input, expected', + [ + (True, "Trace"), + (False, "Default") + ] +) +def test_trace(test_input, expected): + """Test the trace function turns tracing on|off.""" + trace(test_input) + assert logging.current_state_value == expected + + +@pytest.mark.parametrize( + 'test_input, expected', + [ + (True, "Debug"), + (False, "Default") + ] +) +def test_debug(test_input, expected): + """Test the debug function turns tracing on|off.""" + debug(test_input) + assert logging.current_state_value == expected + From 7dccc30b880ed6ec7034f004189b43797d622670 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 Aug 2024 21:11:23 -0700 Subject: [PATCH 131/260] ruff --- tests/unit_tests/utils/test_init.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/tests/unit_tests/utils/test_init.py b/tests/unit_tests/utils/test_init.py index 6a77e0f0a..fbbc8d5bc 100644 --- a/tests/unit_tests/utils/test_init.py +++ b/tests/unit_tests/utils/test_init.py @@ -13,28 +13,15 @@ def test_getattr_version_split(): assert "version_split is deprecated" in str(w[-1].message) -@pytest.mark.parametrize( - 'test_input, expected', - [ - (True, "Trace"), - (False, "Default") - ] -) +@pytest.mark.parametrize("test_input, expected", [(True, "Trace"), (False, "Default")]) def test_trace(test_input, expected): """Test the trace function turns tracing on|off.""" trace(test_input) assert logging.current_state_value == expected -@pytest.mark.parametrize( - 'test_input, expected', - [ - (True, "Debug"), - (False, "Default") - ] -) +@pytest.mark.parametrize("test_input, expected", [(True, "Debug"), (False, "Default")]) def test_debug(test_input, expected): """Test the debug function turns tracing on|off.""" debug(test_input) assert logging.current_state_value == expected - From a4656163a34b5713dfb96496ad38e0cd74d9cb33 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 22 Aug 2024 11:44:52 -0700 Subject: [PATCH 132/260] Add tests for utils.weight_utils.py --- tests/unit_tests/utils/test_weight_utils.py | 137 ++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/tests/unit_tests/utils/test_weight_utils.py b/tests/unit_tests/utils/test_weight_utils.py index fccf2276b..b68f5de9a 100644 --- a/tests/unit_tests/utils/test_weight_utils.py +++ b/tests/unit_tests/utils/test_weight_utils.py @@ -19,10 +19,13 @@ import logging import numpy as np +from hypothesis import settings + import bittensor.utils.weight_utils as weight_utils import pytest from bittensor.utils import torch +from bittensor.core.settings import version_as_int def test_convert_weight_and_uids(): @@ -532,3 +535,137 @@ def test_error_cases(test_id, n, uids, bonds, exception): # Act / Assert with pytest.raises(exception): weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) + + +def test_process_weights_for_netuid(mocker): + """Test the process_weights_for_netuid function.""" + # Prep + fake_uids = np.array([1, 2, 3, 4, 5], dtype=np.int64) + fake_weights = np.array([1.0, 2.5, 3.3, 4.7, 5.9], dtype=np.float32) + fake_netuid = 1 + fake_subtensor = mocker.MagicMock() + fake_metagraph = mocker.MagicMock() + fake_exclude_quantile = 0 + + fake_subtensor.min_allowed_weights.return_value = 0.1 + fake_subtensor.max_weight_limit.return_value = 1.0 + fake_metagraph.n = 1 + mocked_normalize_max_weight = mocker.patch.object(weight_utils, "normalize_max_weight") + + # Call + result = weight_utils.process_weights_for_netuid( + uids=fake_uids, + weights=fake_weights, + netuid=fake_netuid, + subtensor=fake_subtensor, + metagraph=fake_metagraph, + exclude_quantile=fake_exclude_quantile, + ) + + # Asserts + fake_subtensor.min_allowed_weights.assert_called_once_with(netuid=fake_netuid) + fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) + + res1, res2 = result + assert np.array_equal(res1, fake_uids) + assert res2 == mocked_normalize_max_weight.return_value + + +def test_process_weights_with_all_zero_weights(mocker): + """Test the process_weights_for_netuid function with all zero weights.""" + # Prep + fake_uids = np.array([1, 2, 3, 4, 5], dtype=np.int64) + fake_weights = np.array([0.0, 0.0, 0.0, 0.0, 0.0], dtype=np.float32) + fake_netuid = 1 + fake_subtensor = mocker.MagicMock() + fake_metagraph = mocker.MagicMock() + fake_exclude_quantile = 0 + + fake_subtensor.min_allowed_weights.return_value = 0.1 + fake_subtensor.max_weight_limit.return_value = 1.0 + fake_metagraph.n = 1 + + # Call + result = weight_utils.process_weights_for_netuid( + uids=fake_uids, + weights=fake_weights, + netuid=fake_netuid, + subtensor=fake_subtensor, + metagraph=fake_metagraph, + exclude_quantile=fake_exclude_quantile, + ) + + # Asserts + fake_subtensor.min_allowed_weights.assert_called_once_with(netuid=fake_netuid) + fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) + + res1, res2 = result + assert np.array_equal(res1, np.array([0])) + assert np.array_equal(res2, np.array([1.])) + + +def test_process_weights_for_netuid_with_nzs_less_min_allowed_weights(mocker): + """Tests process_weights_for_netuid method when non-zero weights are less than the min allowed weights.""" + # Prep + fake_uids = np.array([1, 2, 3, 4, 5], dtype=np.int64) + fake_weights = np.array([0.1, 0.2, 0.3, 0.0, 0.0], dtype=np.float32) + fake_netuid = 1 + fake_subtensor = mocker.MagicMock() + fake_metagraph = None + fake_exclude_quantile = 0 + + fake_subtensor.min_allowed_weights.return_value = 4 + fake_subtensor.max_weight_limit.return_value = 1.0 + fake_subtensor.metagraph.return_value.n = 5 + mocked_np_arange = mocker.patch.object(np, "arange") + mocked_normalize_max_weight = mocker.patch.object(weight_utils, "normalize_max_weight") + + # Call + result = weight_utils.process_weights_for_netuid( + uids=fake_uids, + weights=fake_weights, + netuid=fake_netuid, + subtensor=fake_subtensor, + metagraph=fake_metagraph, + exclude_quantile=fake_exclude_quantile, + ) + + # Asserts + fake_subtensor.metagraph.assert_called_once_with(fake_netuid) + fake_subtensor.min_allowed_weights.assert_called_once_with(netuid=fake_netuid) + fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) + assert result == (mocked_np_arange.return_value, mocked_normalize_max_weight.return_value) + + +def test_generate_weight_hash(mocker): + """Tests weight_utils.generate_weight_hash function.""" + # Prep + fake_address = "5D1ABCD" + fake_netuid = 1 + fake_uids = [1, 2] + fake_values = [10, 20] + fake_version_key = 80000 + fake_salt = [1, 2] + + mocked_scale_bytes = mocker.patch.object(weight_utils, "ScaleBytes") + mocked_keypair = mocker.patch.object(weight_utils, "Keypair") + mocker_vec = mocker.patch.object(weight_utils, "Vec") + mocked_u16 = mocker.patch.object(weight_utils, "U16") + mocked_hasher = mocker.patch.object(weight_utils.hashlib, "blake2b") + + # Call + result = weight_utils.generate_weight_hash( + address=fake_address, + netuid=fake_netuid, + uids=fake_uids, + values=fake_values, + version_key=fake_version_key, + salt=fake_salt, + ) + + # Asserts + mocked_scale_bytes.assert_called() + mocked_keypair.assert_called() + mocker_vec.assert_called() + mocked_u16.assert_called() + assert result == mocked_hasher.return_value.hexdigest.return_value.__radd__.return_value From 0ac5208de76c204896d52e158cb0657549dbcde9 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 22 Aug 2024 12:39:03 -0700 Subject: [PATCH 133/260] add tests for bittensor.utils.subnets --- tests/unit_tests/test_subnets.py | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/unit_tests/test_subnets.py diff --git a/tests/unit_tests/test_subnets.py b/tests/unit_tests/test_subnets.py new file mode 100644 index 000000000..505b20c51 --- /dev/null +++ b/tests/unit_tests/test_subnets.py @@ -0,0 +1,74 @@ +import pytest +from mpmath.ctx_mp_python import return_mpc + +from bittensor.utils import subnets + + +class MySubnetsAPI(subnets.SubnetsAPI): + """Example of user class inherited from SubnetsAPI.""" + + def prepare_synapse(self, *args, **kwargs): + """Prepare the synapse-specific payload.""" + + def process_responses(self, responses): + """Process the responses from the network.""" + return responses + + +def test_instance_creation(mocker): + """Test the creation of a MySubnetsAPI instance.""" + # Prep + mocked_dendrite = mocker.patch.object(subnets, "Dendrite") + fake_wallet = mocker.MagicMock() + + # Call + instance = MySubnetsAPI(fake_wallet) + + # Asserts + assert isinstance(instance, subnets.SubnetsAPI) + mocked_dendrite.assert_called_once_with(wallet=fake_wallet) + assert instance.dendrite == mocked_dendrite.return_value + assert instance.wallet == fake_wallet + + +@pytest.mark.asyncio +async def test_query_api(mocker): + """Test querying the MySubnetsAPI instance asynchronously.""" + # Prep + mocked_async_dendrite = mocker.AsyncMock() + mocked_dendrite = mocker.patch.object(subnets, "Dendrite", return_value=mocked_async_dendrite) + + fake_wallet = mocker.MagicMock() + fake_axon = mocker.MagicMock() + + mocked_synapse = mocker.MagicMock() + mocked_synapse.return_value.name = "test synapse" + mocked_prepare_synapse = mocker.patch.object(MySubnetsAPI, "prepare_synapse", return_value=mocked_synapse) + + # Call + instance = MySubnetsAPI(fake_wallet) + result = await instance.query_api(fake_axon, **{"key": "val"}) + + # Asserts + mocked_prepare_synapse.assert_called_once_with(key="val") + mocked_dendrite.assert_called_once_with(wallet=fake_wallet) + assert result == mocked_async_dendrite.return_value + + +@pytest.mark.asyncio +async def test_test_instance_call(mocker): + """Test the MySubnetsAPI instance call with asynchronous handling.""" + # Prep + mocked_async_dendrite = mocker.AsyncMock() + mocked_dendrite = mocker.patch.object(subnets, "Dendrite", return_value=mocked_async_dendrite) + mocked_query_api = mocker.patch.object(MySubnetsAPI, "query_api", new=mocker.AsyncMock()) + fake_wallet = mocker.MagicMock() + fake_axon = mocker.MagicMock() + + # Call + instance = MySubnetsAPI(fake_wallet) + await instance(fake_axon) + + # Asserts + mocked_dendrite.assert_called_once_with(wallet=fake_wallet) + mocked_query_api.assert_called_once_with(fake_axon) From 252918f21cf7e10490e9ef66f8d8e1ec061d1ee9 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 22 Aug 2024 12:39:43 -0700 Subject: [PATCH 134/260] ruff --- tests/unit_tests/test_subnets.py | 16 ++++++++++---- tests/unit_tests/utils/test_weight_utils.py | 24 +++++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/tests/unit_tests/test_subnets.py b/tests/unit_tests/test_subnets.py index 505b20c51..9cec02e93 100644 --- a/tests/unit_tests/test_subnets.py +++ b/tests/unit_tests/test_subnets.py @@ -36,14 +36,18 @@ async def test_query_api(mocker): """Test querying the MySubnetsAPI instance asynchronously.""" # Prep mocked_async_dendrite = mocker.AsyncMock() - mocked_dendrite = mocker.patch.object(subnets, "Dendrite", return_value=mocked_async_dendrite) + mocked_dendrite = mocker.patch.object( + subnets, "Dendrite", return_value=mocked_async_dendrite + ) fake_wallet = mocker.MagicMock() fake_axon = mocker.MagicMock() mocked_synapse = mocker.MagicMock() mocked_synapse.return_value.name = "test synapse" - mocked_prepare_synapse = mocker.patch.object(MySubnetsAPI, "prepare_synapse", return_value=mocked_synapse) + mocked_prepare_synapse = mocker.patch.object( + MySubnetsAPI, "prepare_synapse", return_value=mocked_synapse + ) # Call instance = MySubnetsAPI(fake_wallet) @@ -60,8 +64,12 @@ async def test_test_instance_call(mocker): """Test the MySubnetsAPI instance call with asynchronous handling.""" # Prep mocked_async_dendrite = mocker.AsyncMock() - mocked_dendrite = mocker.patch.object(subnets, "Dendrite", return_value=mocked_async_dendrite) - mocked_query_api = mocker.patch.object(MySubnetsAPI, "query_api", new=mocker.AsyncMock()) + mocked_dendrite = mocker.patch.object( + subnets, "Dendrite", return_value=mocked_async_dendrite + ) + mocked_query_api = mocker.patch.object( + MySubnetsAPI, "query_api", new=mocker.AsyncMock() + ) fake_wallet = mocker.MagicMock() fake_axon = mocker.MagicMock() diff --git a/tests/unit_tests/utils/test_weight_utils.py b/tests/unit_tests/utils/test_weight_utils.py index b68f5de9a..74009434b 100644 --- a/tests/unit_tests/utils/test_weight_utils.py +++ b/tests/unit_tests/utils/test_weight_utils.py @@ -550,8 +550,10 @@ def test_process_weights_for_netuid(mocker): fake_subtensor.min_allowed_weights.return_value = 0.1 fake_subtensor.max_weight_limit.return_value = 1.0 fake_metagraph.n = 1 - mocked_normalize_max_weight = mocker.patch.object(weight_utils, "normalize_max_weight") - + mocked_normalize_max_weight = mocker.patch.object( + weight_utils, "normalize_max_weight" + ) + # Call result = weight_utils.process_weights_for_netuid( uids=fake_uids, @@ -567,7 +569,7 @@ def test_process_weights_for_netuid(mocker): fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) res1, res2 = result - assert np.array_equal(res1, fake_uids) + assert np.array_equal(res1, fake_uids) assert res2 == mocked_normalize_max_weight.return_value @@ -601,7 +603,7 @@ def test_process_weights_with_all_zero_weights(mocker): res1, res2 = result assert np.array_equal(res1, np.array([0])) - assert np.array_equal(res2, np.array([1.])) + assert np.array_equal(res2, np.array([1.0])) def test_process_weights_for_netuid_with_nzs_less_min_allowed_weights(mocker): @@ -618,7 +620,9 @@ def test_process_weights_for_netuid_with_nzs_less_min_allowed_weights(mocker): fake_subtensor.max_weight_limit.return_value = 1.0 fake_subtensor.metagraph.return_value.n = 5 mocked_np_arange = mocker.patch.object(np, "arange") - mocked_normalize_max_weight = mocker.patch.object(weight_utils, "normalize_max_weight") + mocked_normalize_max_weight = mocker.patch.object( + weight_utils, "normalize_max_weight" + ) # Call result = weight_utils.process_weights_for_netuid( @@ -634,7 +638,10 @@ def test_process_weights_for_netuid_with_nzs_less_min_allowed_weights(mocker): fake_subtensor.metagraph.assert_called_once_with(fake_netuid) fake_subtensor.min_allowed_weights.assert_called_once_with(netuid=fake_netuid) fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) - assert result == (mocked_np_arange.return_value, mocked_normalize_max_weight.return_value) + assert result == ( + mocked_np_arange.return_value, + mocked_normalize_max_weight.return_value, + ) def test_generate_weight_hash(mocker): @@ -668,4 +675,7 @@ def test_generate_weight_hash(mocker): mocked_keypair.assert_called() mocker_vec.assert_called() mocked_u16.assert_called() - assert result == mocked_hasher.return_value.hexdigest.return_value.__radd__.return_value + assert ( + result + == mocked_hasher.return_value.hexdigest.return_value.__radd__.return_value + ) From b57f0b095e4a69f41e82ad20f2cc1554d8173923 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 22 Aug 2024 15:09:22 -0700 Subject: [PATCH 135/260] update test file --- tests/unit_tests/utils/test_utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index 9f549cfb6..8ed28c067 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -14,9 +14,6 @@ # 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. -from symbol import return_stmt - -from more_itertools.more import side_effect from bittensor import utils from bittensor.core.settings import SS58_FORMAT From 8087fa08e6320883dd1e667778296401287ad8a6 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 22 Aug 2024 21:27:59 -0700 Subject: [PATCH 136/260] Decoupling chain_data.py to sub-package --- bittensor/core/chain_data.py | 1196 ----------------- bittensor/core/chain_data/__init__.py | 22 + bittensor/core/chain_data/axon_info.py | 152 +++ bittensor/core/chain_data/delegate_info.py | 107 ++ .../core/chain_data/delegate_info_lite.py | 27 + bittensor/core/chain_data/ip_info.py | 69 + bittensor/core/chain_data/neuron_info.py | 159 +++ bittensor/core/chain_data/neuron_info_lite.py | 111 ++ bittensor/core/chain_data/prometheus_info.py | 20 + .../core/chain_data/proposal_vote_data.py | 12 + .../chain_data/scheduled_coldkey_swap_info.py | 53 + bittensor/core/chain_data/stake_info.py | 67 + .../core/chain_data/subnet_hyperparameters.py | 108 ++ bittensor/core/chain_data/subnet_info.py | 100 ++ bittensor/core/chain_data/utils.py | 243 ++++ bittensor/utils/networking.py | 8 +- 16 files changed, 1254 insertions(+), 1200 deletions(-) delete mode 100644 bittensor/core/chain_data.py create mode 100644 bittensor/core/chain_data/__init__.py create mode 100644 bittensor/core/chain_data/axon_info.py create mode 100644 bittensor/core/chain_data/delegate_info.py create mode 100644 bittensor/core/chain_data/delegate_info_lite.py create mode 100644 bittensor/core/chain_data/ip_info.py create mode 100644 bittensor/core/chain_data/neuron_info.py create mode 100644 bittensor/core/chain_data/neuron_info_lite.py create mode 100644 bittensor/core/chain_data/prometheus_info.py create mode 100644 bittensor/core/chain_data/proposal_vote_data.py create mode 100644 bittensor/core/chain_data/scheduled_coldkey_swap_info.py create mode 100644 bittensor/core/chain_data/stake_info.py create mode 100644 bittensor/core/chain_data/subnet_hyperparameters.py create mode 100644 bittensor/core/chain_data/subnet_info.py create mode 100644 bittensor/core/chain_data/utils.py diff --git a/bittensor/core/chain_data.py b/bittensor/core/chain_data.py deleted file mode 100644 index b01dcc665..000000000 --- a/bittensor/core/chain_data.py +++ /dev/null @@ -1,1196 +0,0 @@ -# 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. - -""" -This module provides data structures and functions for working with the Bittensor network, -including neuron and subnet information, SCALE encoding/decoding, and custom RPC type registry. -""" - -import json -from dataclasses import dataclass, asdict -from enum import Enum -from typing import List, Tuple, Dict, Optional, Any, TypedDict, Union - -from scalecodec.base import RuntimeConfiguration, ScaleBytes -from scalecodec.type_registry import load_type_registry_preset -from scalecodec.types import GenericCall -from scalecodec.utils.ss58 import ss58_encode - -from bittensor.utils import networking as net, RAOPERTAO, u16_normalized_float -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging -from bittensor.utils.registration import torch, use_torch -from .settings import SS58_FORMAT - -custom_rpc_type_registry = { - "types": { - "SubnetInfo": { - "type": "struct", - "type_mapping": [ - ["netuid", "Compact"], - ["rho", "Compact"], - ["kappa", "Compact"], - ["difficulty", "Compact"], - ["immunity_period", "Compact"], - ["max_allowed_validators", "Compact"], - ["min_allowed_weights", "Compact"], - ["max_weights_limit", "Compact"], - ["scaling_law_power", "Compact"], - ["subnetwork_n", "Compact"], - ["max_allowed_uids", "Compact"], - ["blocks_since_last_step", "Compact"], - ["tempo", "Compact"], - ["network_modality", "Compact"], - ["network_connect", "Vec<[u16; 2]>"], - ["emission_values", "Compact"], - ["burn", "Compact"], - ["owner", "AccountId"], - ], - }, - "DelegateInfo": { - "type": "struct", - "type_mapping": [ - ["delegate_ss58", "AccountId"], - ["take", "Compact"], - ["nominators", "Vec<(AccountId, Compact)>"], - ["owner_ss58", "AccountId"], - ["registrations", "Vec>"], - ["validator_permits", "Vec>"], - ["return_per_1000", "Compact"], - ["total_daily_return", "Compact"], - ], - }, - "NeuronInfo": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["uid", "Compact"], - ["netuid", "Compact"], - ["active", "bool"], - ["axon_info", "axon_info"], - ["prometheus_info", "PrometheusInfo"], - ["stake", "Vec<(AccountId, Compact)>"], - ["rank", "Compact"], - ["emission", "Compact"], - ["incentive", "Compact"], - ["consensus", "Compact"], - ["trust", "Compact"], - ["validator_trust", "Compact"], - ["dividends", "Compact"], - ["last_update", "Compact"], - ["validator_permit", "bool"], - ["weights", "Vec<(Compact, Compact)>"], - ["bonds", "Vec<(Compact, Compact)>"], - ["pruning_score", "Compact"], - ], - }, - "NeuronInfoLite": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["uid", "Compact"], - ["netuid", "Compact"], - ["active", "bool"], - ["axon_info", "axon_info"], - ["prometheus_info", "PrometheusInfo"], - ["stake", "Vec<(AccountId, Compact)>"], - ["rank", "Compact"], - ["emission", "Compact"], - ["incentive", "Compact"], - ["consensus", "Compact"], - ["trust", "Compact"], - ["validator_trust", "Compact"], - ["dividends", "Compact"], - ["last_update", "Compact"], - ["validator_permit", "bool"], - ["pruning_score", "Compact"], - ], - }, - "axon_info": { - "type": "struct", - "type_mapping": [ - ["block", "u64"], - ["version", "u32"], - ["ip", "u128"], - ["port", "u16"], - ["ip_type", "u8"], - ["protocol", "u8"], - ["placeholder1", "u8"], - ["placeholder2", "u8"], - ], - }, - "PrometheusInfo": { - "type": "struct", - "type_mapping": [ - ["block", "u64"], - ["version", "u32"], - ["ip", "u128"], - ["port", "u16"], - ["ip_type", "u8"], - ], - }, - "IPInfo": { - "type": "struct", - "type_mapping": [ - ["ip", "Compact"], - ["ip_type_and_protocol", "Compact"], - ], - }, - "StakeInfo": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["stake", "Compact"], - ], - }, - "SubnetHyperparameters": { - "type": "struct", - "type_mapping": [ - ["rho", "Compact"], - ["kappa", "Compact"], - ["immunity_period", "Compact"], - ["min_allowed_weights", "Compact"], - ["max_weights_limit", "Compact"], - ["tempo", "Compact"], - ["min_difficulty", "Compact"], - ["max_difficulty", "Compact"], - ["weights_version", "Compact"], - ["weights_rate_limit", "Compact"], - ["adjustment_interval", "Compact"], - ["activity_cutoff", "Compact"], - ["registration_allowed", "bool"], - ["target_regs_per_interval", "Compact"], - ["min_burn", "Compact"], - ["max_burn", "Compact"], - ["bonds_moving_avg", "Compact"], - ["max_regs_per_block", "Compact"], - ["serving_rate_limit", "Compact"], - ["max_validators", "Compact"], - ["adjustment_alpha", "Compact"], - ["difficulty", "Compact"], - ["commit_reveal_weights_interval", "Compact"], - ["commit_reveal_weights_enabled", "bool"], - ["alpha_high", "Compact"], - ["alpha_low", "Compact"], - ["liquid_alpha_enabled", "bool"], - ], - }, - "ScheduledColdkeySwapInfo": { - "type": "struct", - "type_mapping": [ - ["old_coldkey", "AccountId"], - ["new_coldkey", "AccountId"], - ["arbitration_block", "Compact"], - ], - }, - } -} - - -@dataclass -class AxonInfo: - version: int - ip: str - port: int - ip_type: int - hotkey: str - coldkey: str - protocol: int = 4 - placeholder1: int = 0 - placeholder2: int = 0 - - @property - def is_serving(self) -> bool: - """True if the endpoint is serving.""" - return self.ip != "0.0.0.0" - - def ip_str(self) -> str: - """Return the whole IP as string""" - return net.ip__str__(self.ip_type, self.ip, self.port) - - def __eq__(self, other: "AxonInfo"): - if other is None: - return False - - if ( - self.version == other.version - and self.ip == other.ip - and self.port == other.port - and self.ip_type == other.ip_type - and self.coldkey == other.coldkey - and self.hotkey == other.hotkey - ): - return True - - return False - - def __str__(self): - return f"AxonInfo( {self.ip_str()}, {self.hotkey}, {self.coldkey}, {self.version} )" - - def __repr__(self): - return self.__str__() - - def to_string(self) -> str: - """Converts the AxonInfo object to a string representation using JSON.""" - try: - return json.dumps(asdict(self)) - except (TypeError, ValueError) as e: - logging.error(f"Error converting AxonInfo to string: {e}") - return AxonInfo(0, "", 0, 0, "", "").to_string() - - @classmethod - def from_string(cls, json_string: str) -> "AxonInfo": - """ - Creates an AxonInfo object from its string representation using JSON. - - Args: - json_string (str): The JSON string representation of the AxonInfo object. - - Returns: - AxonInfo: An instance of AxonInfo created from the JSON string. If decoding fails, returns a default AxonInfo object with default values. - - Raises: - json.JSONDecodeError: If there is an error in decoding the JSON string. - TypeError: If there is a type error when creating the AxonInfo object. - ValueError: If there is a value error when creating the AxonInfo object. - """ - try: - data = json.loads(json_string) - return cls(**data) - except json.JSONDecodeError as e: - logging.error(f"Error decoding JSON: {e}") - except TypeError as e: - logging.error(f"Type error: {e}") - except ValueError as e: - logging.error(f"Value error: {e}") - return AxonInfo(0, "", 0, 0, "", "") - - @classmethod - def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": - """ - Converts a dictionary to an AxonInfo object. - - Args: - neuron_info (dict): A dictionary containing the neuron information. - - Returns: - instance (AxonInfo): An instance of AxonInfo created from the dictionary. - """ - return cls( - version=neuron_info["axon_info"]["version"], - ip=net.int_to_ip(int(neuron_info["axon_info"]["ip"])), - port=neuron_info["axon_info"]["port"], - ip_type=neuron_info["axon_info"]["ip_type"], - hotkey=neuron_info["hotkey"], - coldkey=neuron_info["coldkey"], - ) - - def to_parameter_dict( - self, - ) -> Union[dict[str, Union[int, str]], "torch.nn.ParameterDict"]: - """Returns a torch tensor or dict of the subnet info, depending on the USE_TORCH flag set.""" - if use_torch(): - return torch.nn.ParameterDict(self.__dict__) - else: - return self.__dict__ - - @classmethod - def from_parameter_dict( - cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] - ) -> "AxonInfo": - """Returns an axon_info object from a torch parameter_dict or a parameter dict.""" - if use_torch(): - return cls(**dict(parameter_dict)) - else: - return cls(**parameter_dict) - - -class ChainDataType(Enum): - NeuronInfo = 1 - SubnetInfo = 2 - DelegateInfo = 3 - NeuronInfoLite = 4 - DelegatedInfo = 5 - StakeInfo = 6 - IPInfo = 7 - SubnetHyperparameters = 8 - ScheduledColdkeySwapInfo = 9 - AccountId = 10 - - -def from_scale_encoding( - input_: Union[List[int], bytes, ScaleBytes], - type_name: ChainDataType, - is_vec: bool = False, - is_option: bool = False, -) -> Optional[Dict]: - """ - Decodes input_ data from SCALE encoding based on the specified type name and modifiers. - - Args: - input_ (Union[List[int], bytes, ScaleBytes]): The input_ data to decode. - type_name (ChainDataType): The type of data being decoded. - is_vec (bool, optional): Whether the data is a vector of the specified type. Default is ``False``. - is_option (bool, optional): Whether the data is an optional value of the specified type. Default is ``False``. - - Returns: - Optional[Dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. - """ - type_string = type_name.name - if type_name == ChainDataType.DelegatedInfo: - # DelegatedInfo is a tuple of (DelegateInfo, Compact) - type_string = f"({ChainDataType.DelegateInfo.name}, Compact)" - if is_option: - type_string = f"Option<{type_string}>" - if is_vec: - type_string = f"Vec<{type_string}>" - - return from_scale_encoding_using_type_string(input_, type_string) - - -def from_scale_encoding_using_type_string( - input_: Union[List[int], bytes, ScaleBytes], type_string: str -) -> Optional[Dict]: - if isinstance(input_, ScaleBytes): - as_scale_bytes = input_ - else: - if isinstance(input_, list) and all([isinstance(i, int) for i in input_]): - vec_u8 = input_ - as_bytes = bytes(vec_u8) - elif isinstance(input_, bytes): - as_bytes = input_ - else: - raise TypeError("input_ must be a List[int], bytes, or ScaleBytes") - - as_scale_bytes = ScaleBytes(as_bytes) - - rpc_runtime_config = RuntimeConfiguration() - rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) - rpc_runtime_config.update_type_registry(custom_rpc_type_registry) - - obj = rpc_runtime_config.create_scale_object(type_string, data=as_scale_bytes) - - return obj.decode() - - -# Dataclasses for chain data. -@dataclass -class NeuronInfo: - """Dataclass for neuron metadata.""" - - hotkey: str - coldkey: str - uid: int - netuid: int - active: int - stake: Balance - # mapping of coldkey to amount staked to this Neuron - stake_dict: Dict[str, Balance] - total_stake: Balance - rank: float - emission: float - incentive: float - consensus: float - trust: float - validator_trust: float - dividends: float - last_update: int - validator_permit: bool - weights: List[List[int]] - bonds: List[List[int]] - pruning_score: int - prometheus_info: Optional["PrometheusInfo"] = None - axon_info: Optional[AxonInfo] = None - is_null: bool = False - - @classmethod - def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfo": - """Fixes the values of the NeuronInfo object.""" - neuron_info_decoded["hotkey"] = ss58_encode( - neuron_info_decoded["hotkey"], SS58_FORMAT - ) - neuron_info_decoded["coldkey"] = ss58_encode( - neuron_info_decoded["coldkey"], SS58_FORMAT - ) - stake_dict = { - ss58_encode(coldkey, SS58_FORMAT): Balance.from_rao(int(stake)) - for coldkey, stake in neuron_info_decoded["stake"] - } - neuron_info_decoded["stake_dict"] = stake_dict - neuron_info_decoded["stake"] = sum(stake_dict.values()) - neuron_info_decoded["total_stake"] = neuron_info_decoded["stake"] - neuron_info_decoded["weights"] = [ - [int(weight[0]), int(weight[1])] - for weight in neuron_info_decoded["weights"] - ] - neuron_info_decoded["bonds"] = [ - [int(bond[0]), int(bond[1])] for bond in neuron_info_decoded["bonds"] - ] - neuron_info_decoded["rank"] = u16_normalized_float(neuron_info_decoded["rank"]) - neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO - neuron_info_decoded["incentive"] = u16_normalized_float( - neuron_info_decoded["incentive"] - ) - neuron_info_decoded["consensus"] = u16_normalized_float( - neuron_info_decoded["consensus"] - ) - neuron_info_decoded["trust"] = u16_normalized_float( - neuron_info_decoded["trust"] - ) - neuron_info_decoded["validator_trust"] = u16_normalized_float( - neuron_info_decoded["validator_trust"] - ) - neuron_info_decoded["dividends"] = u16_normalized_float( - neuron_info_decoded["dividends"] - ) - neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values( - neuron_info_decoded["prometheus_info"] - ) - neuron_info_decoded["axon_info"] = AxonInfo.from_neuron_info( - neuron_info_decoded - ) - return cls(**neuron_info_decoded) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> "NeuronInfo": - """Returns a NeuronInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return NeuronInfo.get_null_neuron() - - decoded = from_scale_encoding(vec_u8, ChainDataType.NeuronInfo) - if decoded is None: - return NeuronInfo.get_null_neuron() - - return NeuronInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["NeuronInfo"]: - """Returns a list of NeuronInfo objects from a ``vec_u8``""" - - decoded_list = from_scale_encoding( - vec_u8, ChainDataType.NeuronInfo, is_vec=True - ) - if decoded_list is None: - return [] - - decoded_list = [ - NeuronInfo.fix_decoded_values(decoded) for decoded in decoded_list - ] - return decoded_list - - @staticmethod - def get_null_neuron() -> "NeuronInfo": - neuron = NeuronInfo( - uid=0, - netuid=0, - active=0, - stake=Balance.from_rao(0), - stake_dict={}, - total_stake=Balance.from_rao(0), - rank=0, - emission=0, - incentive=0, - consensus=0, - trust=0, - validator_trust=0, - dividends=0, - last_update=0, - validator_permit=False, - weights=[], - bonds=[], - prometheus_info=None, - axon_info=None, - is_null=True, - coldkey="000000000000000000000000000000000000000000000000", - hotkey="000000000000000000000000000000000000000000000000", - pruning_score=0, - ) - return neuron - - @classmethod - def from_weights_bonds_and_neuron_lite( - cls, - neuron_lite: "NeuronInfoLite", - weights_as_dict: Dict[int, List[Tuple[int, int]]], - bonds_as_dict: Dict[int, List[Tuple[int, int]]], - ) -> "NeuronInfo": - n_dict = neuron_lite.__dict__ - n_dict["weights"] = weights_as_dict.get(neuron_lite.uid, []) - n_dict["bonds"] = bonds_as_dict.get(neuron_lite.uid, []) - - return cls(**n_dict) - - -@dataclass -class NeuronInfoLite: - """Dataclass for neuron metadata, but without the weights and bonds.""" - - hotkey: str - coldkey: str - uid: int - netuid: int - active: int - stake: Balance - # mapping of coldkey to amount staked to this Neuron - stake_dict: Dict[str, Balance] - total_stake: Balance - rank: float - emission: float - incentive: float - consensus: float - trust: float - validator_trust: float - dividends: float - last_update: int - validator_permit: bool - prometheus_info: Optional["PrometheusInfo"] - axon_info: "axon_info" - pruning_score: int - is_null: bool = False - - @classmethod - def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfoLite": - """Fixes the values of the NeuronInfoLite object.""" - neuron_info_decoded["hotkey"] = ss58_encode( - neuron_info_decoded["hotkey"], SS58_FORMAT - ) - neuron_info_decoded["coldkey"] = ss58_encode( - neuron_info_decoded["coldkey"], SS58_FORMAT - ) - stake_dict = { - ss58_encode(coldkey, SS58_FORMAT): Balance.from_rao(int(stake)) - for coldkey, stake in neuron_info_decoded["stake"] - } - neuron_info_decoded["stake_dict"] = stake_dict - neuron_info_decoded["stake"] = sum(stake_dict.values()) - neuron_info_decoded["total_stake"] = neuron_info_decoded["stake"] - neuron_info_decoded["rank"] = u16_normalized_float(neuron_info_decoded["rank"]) - neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO - neuron_info_decoded["incentive"] = u16_normalized_float( - neuron_info_decoded["incentive"] - ) - neuron_info_decoded["consensus"] = u16_normalized_float( - neuron_info_decoded["consensus"] - ) - neuron_info_decoded["trust"] = u16_normalized_float( - neuron_info_decoded["trust"] - ) - neuron_info_decoded["validator_trust"] = u16_normalized_float( - neuron_info_decoded["validator_trust"] - ) - neuron_info_decoded["dividends"] = u16_normalized_float( - neuron_info_decoded["dividends"] - ) - neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values( - neuron_info_decoded["prometheus_info"] - ) - neuron_info_decoded["axon_info"] = AxonInfo.from_neuron_info( - neuron_info_decoded - ) - return cls(**neuron_info_decoded) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> "NeuronInfoLite": - """Returns a NeuronInfoLite object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return NeuronInfoLite.get_null_neuron() - - decoded = from_scale_encoding(vec_u8, ChainDataType.NeuronInfoLite) - if decoded is None: - return NeuronInfoLite.get_null_neuron() - - return NeuronInfoLite.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["NeuronInfoLite"]: - """Returns a list of NeuronInfoLite objects from a ``vec_u8``.""" - - decoded_list = from_scale_encoding( - vec_u8, ChainDataType.NeuronInfoLite, is_vec=True - ) - if decoded_list is None: - return [] - - decoded_list = [ - NeuronInfoLite.fix_decoded_values(decoded) for decoded in decoded_list - ] - return decoded_list - - @staticmethod - def get_null_neuron() -> "NeuronInfoLite": - neuron = NeuronInfoLite( - uid=0, - netuid=0, - active=0, - stake=Balance.from_rao(0), - stake_dict={}, - total_stake=Balance.from_rao(0), - rank=0, - emission=0, - incentive=0, - consensus=0, - trust=0, - validator_trust=0, - dividends=0, - last_update=0, - validator_permit=False, - prometheus_info=None, - axon_info=None, - is_null=True, - coldkey="000000000000000000000000000000000000000000000000", - hotkey="000000000000000000000000000000000000000000000000", - pruning_score=0, - ) - return neuron - - -@dataclass -class PrometheusInfo: - """Dataclass for prometheus info.""" - - block: int - version: int - ip: str - port: int - ip_type: int - - @classmethod - def fix_decoded_values(cls, prometheus_info_decoded: Dict) -> "PrometheusInfo": - """Returns a PrometheusInfo object from a prometheus_info_decoded dictionary.""" - prometheus_info_decoded["ip"] = net.int_to_ip( - int(prometheus_info_decoded["ip"]) - ) - - return cls(**prometheus_info_decoded) - - -@dataclass -class DelegateInfoLite: - """ - Dataclass for DelegateLiteInfo. This is a lighter version of :func:`DelegateInfo`. - - Args: - delegate_ss58 (str): Hotkey of the delegate for which the information is being fetched. - take (float): Take of the delegate as a percentage. - nominators (int): Count of the nominators of the delegate. - owner_ss58 (str): Coldkey of the owner. - registrations (list[int]): List of subnets that the delegate is registered on. - validator_permits (list[int]): List of subnets that the delegate is allowed to validate on. - return_per_1000 (int): Return per 1000 TAO, for the delegate over a day. - total_daily_return (int): Total daily return of the delegate. - """ - - delegate_ss58: str # Hotkey of delegate - take: float # Take of the delegate as a percentage - nominators: int # Count of the nominators of the delegate. - owner_ss58: str # Coldkey of owner - registrations: list[int] # List of subnets that the delegate is registered on - validator_permits: list[ - int - ] # List of subnets that the delegate is allowed to validate on - return_per_1000: int # Return per 1000 tao for the delegate over a day - total_daily_return: int # Total daily return of the delegate - - -@dataclass -class DelegateInfo: - """ - Dataclass for delegate information. For a lighter version of this class, see :func:`DelegateInfoLite`. - - Args: - hotkey_ss58 (str): Hotkey of the delegate for which the information is being fetched. - total_stake (int): Total stake of the delegate. - nominators (list[Tuple[str, int]]): List of nominators of the delegate and their stake. - take (float): Take of the delegate as a percentage. - owner_ss58 (str): Coldkey of the owner. - registrations (list[int]): List of subnets that the delegate is registered on. - validator_permits (list[int]): List of subnets that the delegate is allowed to validate on. - return_per_1000 (int): Return per 1000 TAO, for the delegate over a day. - total_daily_return (int): Total daily return of the delegate. - - """ - - hotkey_ss58: str # Hotkey of delegate - total_stake: Balance # Total stake of the delegate - nominators: List[ - Tuple[str, Balance] - ] # List of nominators of the delegate and their stake - owner_ss58: str # Coldkey of owner - take: float # Take of the delegate as a percentage - validator_permits: List[ - int - ] # List of subnets that the delegate is allowed to validate on - registrations: List[int] # List of subnets that the delegate is registered on - return_per_1000: Balance # Return per 1000 tao of the delegate over a day - total_daily_return: Balance # Total daily return of the delegate - - @classmethod - def fix_decoded_values(cls, decoded: Any) -> "DelegateInfo": - """Fixes the decoded values.""" - - return cls( - hotkey_ss58=ss58_encode(decoded["delegate_ss58"], SS58_FORMAT), - owner_ss58=ss58_encode(decoded["owner_ss58"], SS58_FORMAT), - take=u16_normalized_float(decoded["take"]), - nominators=[ - ( - ss58_encode(nom[0], SS58_FORMAT), - Balance.from_rao(nom[1]), - ) - for nom in decoded["nominators"] - ], - total_stake=Balance.from_rao( - sum([nom[1] for nom in decoded["nominators"]]) - ), - validator_permits=decoded["validator_permits"], - registrations=decoded["registrations"], - return_per_1000=Balance.from_rao(decoded["return_per_1000"]), - total_daily_return=Balance.from_rao(decoded["total_daily_return"]), - ) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["DelegateInfo"]: - """Returns a DelegateInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfo) - if decoded is None: - return None - - return DelegateInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["DelegateInfo"]: - """Returns a list of DelegateInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfo, is_vec=True) - - if decoded is None: - return [] - - return [DelegateInfo.fix_decoded_values(d) for d in decoded] - - @classmethod - def delegated_list_from_vec_u8( - cls, vec_u8: List[int] - ) -> List[Tuple["DelegateInfo", Balance]]: - """Returns a list of Tuples of DelegateInfo objects, and Balance, from a ``vec_u8``. - - This is the list of delegates that the user has delegated to, and the amount of stake delegated. - """ - decoded = from_scale_encoding(vec_u8, ChainDataType.DelegatedInfo, is_vec=True) - if decoded is None: - return [] - - return [ - (DelegateInfo.fix_decoded_values(d), Balance.from_rao(s)) - for d, s in decoded - ] - - -@dataclass -class StakeInfo: - """Dataclass for stake info.""" - - hotkey_ss58: str # Hotkey address - coldkey_ss58: str # Coldkey address - stake: Balance # Stake for the hotkey-coldkey pair - - @classmethod - def fix_decoded_values(cls, decoded: Any) -> "StakeInfo": - """Fixes the decoded values.""" - return cls( - hotkey_ss58=ss58_encode(decoded["hotkey"], SS58_FORMAT), - coldkey_ss58=ss58_encode(decoded["coldkey"], SS58_FORMAT), - stake=Balance.from_rao(decoded["stake"]), - ) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["StakeInfo"]: - """Returns a StakeInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo) - if decoded is None: - return None - - return StakeInfo.fix_decoded_values(decoded) - - @classmethod - def list_of_tuple_from_vec_u8( - cls, vec_u8: List[int] - ) -> Dict[str, List["StakeInfo"]]: - """Returns a list of StakeInfo objects from a ``vec_u8``.""" - decoded: Optional[list[tuple[str, list[object]]]] = ( - from_scale_encoding_using_type_string( - input_=vec_u8, type_string="Vec<(AccountId, Vec)>" - ) - ) - - if decoded is None: - return {} - - return { - ss58_encode(address=account_id, ss58_format=SS58_FORMAT): [ - StakeInfo.fix_decoded_values(d) for d in stake_info - ] - for account_id, stake_info in decoded - } - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["StakeInfo"]: - """Returns a list of StakeInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo, is_vec=True) - if decoded is None: - return [] - - return [StakeInfo.fix_decoded_values(d) for d in decoded] - - -@dataclass -class SubnetInfo: - """Dataclass for subnet info.""" - - netuid: int - rho: int - kappa: int - difficulty: int - immunity_period: int - max_allowed_validators: int - min_allowed_weights: int - max_weight_limit: float - scaling_law_power: float - subnetwork_n: int - max_n: int - blocks_since_epoch: int - tempo: int - modality: int - # netuid -> topk percentile prunning score requirement (u16:MAX normalized.) - connection_requirements: Dict[str, float] - emission_value: float - burn: Balance - owner_ss58: str - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetInfo"]: - """Returns a SubnetInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetInfo) - if decoded is None: - return None - - return SubnetInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["SubnetInfo"]: - r"""Returns a list of SubnetInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.SubnetInfo, is_vec=True, is_option=True - ) - - if decoded is None: - return [] - - return [SubnetInfo.fix_decoded_values(d) for d in decoded] - - @classmethod - def fix_decoded_values(cls, decoded: Dict) -> "SubnetInfo": - """Returns a SubnetInfo object from a decoded SubnetInfo dictionary.""" - return SubnetInfo( - netuid=decoded["netuid"], - rho=decoded["rho"], - kappa=decoded["kappa"], - difficulty=decoded["difficulty"], - immunity_period=decoded["immunity_period"], - max_allowed_validators=decoded["max_allowed_validators"], - min_allowed_weights=decoded["min_allowed_weights"], - max_weight_limit=decoded["max_weights_limit"], - # adjustment_alpha=decoded["adjustment_alpha"], - # bonds_moving_avg=decoded["bonds_moving_average"], - scaling_law_power=decoded["scaling_law_power"], - subnetwork_n=decoded["subnetwork_n"], - max_n=decoded["max_allowed_uids"], - blocks_since_epoch=decoded["blocks_since_last_step"], - tempo=decoded["tempo"], - modality=decoded["network_modality"], - connection_requirements={ - str(int(netuid)): u16_normalized_float(int(req)) - for netuid, req in decoded["network_connect"] - }, - emission_value=decoded["emission_values"], - burn=Balance.from_rao(decoded["burn"]), - owner_ss58=ss58_encode(decoded["owner"], SS58_FORMAT), - ) - - def to_parameter_dict(self) -> Union[dict[str, Any], "torch.nn.ParameterDict"]: - """Returns a torch tensor or dict of the subnet info.""" - if use_torch(): - return torch.nn.ParameterDict(self.__dict__) - else: - return self.__dict__ - - @classmethod - def from_parameter_dict( - cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] - ) -> "SubnetInfo": - if use_torch(): - return cls(**dict(parameter_dict)) - else: - return cls(**parameter_dict) - - -@dataclass -class SubnetHyperparameters: - """Dataclass for subnet hyperparameters.""" - - rho: int - kappa: int - immunity_period: int - min_allowed_weights: int - max_weight_limit: float - tempo: int - min_difficulty: int - max_difficulty: int - weights_version: int - weights_rate_limit: int - adjustment_interval: int - activity_cutoff: int - registration_allowed: bool - target_regs_per_interval: int - min_burn: int - max_burn: int - bonds_moving_avg: int - max_regs_per_block: int - serving_rate_limit: int - max_validators: int - adjustment_alpha: int - difficulty: int - commit_reveal_weights_interval: int - commit_reveal_weights_enabled: bool - alpha_high: int - alpha_low: int - liquid_alpha_enabled: bool - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetHyperparameters"]: - """Returns a SubnetHyperparameters object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetHyperparameters) - if decoded is None: - return None - - return SubnetHyperparameters.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["SubnetHyperparameters"]: - """Returns a list of SubnetHyperparameters objects from a ``vec_u8``.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.SubnetHyperparameters, is_vec=True, is_option=True - ) - if decoded is None: - return [] - - return [SubnetHyperparameters.fix_decoded_values(d) for d in decoded] - - @classmethod - def fix_decoded_values(cls, decoded: Dict) -> "SubnetHyperparameters": - """Returns a SubnetInfo object from a decoded SubnetInfo dictionary.""" - return SubnetHyperparameters( - rho=decoded["rho"], - kappa=decoded["kappa"], - immunity_period=decoded["immunity_period"], - min_allowed_weights=decoded["min_allowed_weights"], - max_weight_limit=decoded["max_weights_limit"], - tempo=decoded["tempo"], - min_difficulty=decoded["min_difficulty"], - max_difficulty=decoded["max_difficulty"], - weights_version=decoded["weights_version"], - weights_rate_limit=decoded["weights_rate_limit"], - adjustment_interval=decoded["adjustment_interval"], - activity_cutoff=decoded["activity_cutoff"], - registration_allowed=decoded["registration_allowed"], - target_regs_per_interval=decoded["target_regs_per_interval"], - min_burn=decoded["min_burn"], - max_burn=decoded["max_burn"], - max_regs_per_block=decoded["max_regs_per_block"], - max_validators=decoded["max_validators"], - serving_rate_limit=decoded["serving_rate_limit"], - bonds_moving_avg=decoded["bonds_moving_avg"], - adjustment_alpha=decoded["adjustment_alpha"], - difficulty=decoded["difficulty"], - commit_reveal_weights_interval=decoded["commit_reveal_weights_interval"], - commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], - alpha_high=decoded["alpha_high"], - alpha_low=decoded["alpha_low"], - liquid_alpha_enabled=decoded["liquid_alpha_enabled"], - ) - - def to_parameter_dict( - self, - ) -> Union[dict[str, Union[int, float, bool]], "torch.nn.ParameterDict"]: - """Returns a torch tensor or dict of the subnet hyperparameters.""" - if use_torch(): - return torch.nn.ParameterDict(self.__dict__) - else: - return self.__dict__ - - @classmethod - def from_parameter_dict( - cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] - ) -> "SubnetHyperparameters": - if use_torch(): - return cls(**dict(parameter_dict)) - else: - return cls(**parameter_dict) - - -@dataclass -class IPInfo: - """Dataclass for associated IP Info.""" - - ip: str - ip_type: int - protocol: int - - def encode(self) -> Dict[str, Any]: - """Returns a dictionary of the IPInfo object that can be encoded.""" - return { - "ip": net.ip_to_int( - self.ip - ), # IP type and protocol are encoded together as a u8 - "ip_type_and_protocol": ((self.ip_type << 4) + self.protocol) & 0xFF, - } - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["IPInfo"]: - """Returns a IPInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.IPInfo) - if decoded is None: - return None - - return IPInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["IPInfo"]: - r"""Returns a list of IPInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding(vec_u8, ChainDataType.IPInfo, is_vec=True) - - if decoded is None: - return [] - - return [IPInfo.fix_decoded_values(d) for d in decoded] - - @classmethod - def fix_decoded_values(cls, decoded: Dict) -> "IPInfo": - """Returns a SubnetInfo object from a decoded IPInfo dictionary.""" - return IPInfo( - ip=net.int_to_ip(decoded["ip"]), - ip_type=decoded["ip_type_and_protocol"] >> 4, - protocol=decoded["ip_type_and_protocol"] & 0xF, - ) - - def to_parameter_dict( - self, - ) -> Union[dict[str, Union[str, int]], "torch.nn.ParameterDict"]: - """Returns a torch tensor or dict of the subnet IP info.""" - if use_torch(): - return torch.nn.ParameterDict(self.__dict__) - else: - return self.__dict__ - - @classmethod - def from_parameter_dict( - cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] - ) -> "IPInfo": - if use_torch(): - return cls(**dict(parameter_dict)) - else: - return cls(**parameter_dict) - - -# Senate / Proposal data -class ProposalVoteData(TypedDict): - index: int - threshold: int - ayes: List[str] - nays: List[str] - end: int - - -ProposalCallData = GenericCall - - -@dataclass -class ScheduledColdkeySwapInfo: - """Dataclass for scheduled coldkey swap information.""" - - old_coldkey: str - new_coldkey: str - arbitration_block: int - - @classmethod - def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": - """Fixes the decoded values.""" - return cls( - old_coldkey=ss58_encode(decoded["old_coldkey"], SS58_FORMAT), - new_coldkey=ss58_encode(decoded["new_coldkey"], SS58_FORMAT), - arbitration_block=decoded["arbitration_block"], - ) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["ScheduledColdkeySwapInfo"]: - """Returns a ScheduledColdkeySwapInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo) - if decoded is None: - return None - - return ScheduledColdkeySwapInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["ScheduledColdkeySwapInfo"]: - """Returns a list of ScheduledColdkeySwapInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True - ) - if decoded is None: - return [] - - return [ScheduledColdkeySwapInfo.fix_decoded_values(d) for d in decoded] - - @classmethod - def decode_account_id_list(cls, vec_u8: List[int]) -> Optional[List[str]]: - """Decodes a list of AccountIds from vec_u8.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True - ) - if decoded is None: - return None - return [ss58_encode(account_id, SS58_FORMAT) for account_id in decoded] diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py new file mode 100644 index 000000000..9ad1e3888 --- /dev/null +++ b/bittensor/core/chain_data/__init__.py @@ -0,0 +1,22 @@ +""" +This module provides data structures and functions for working with the Bittensor network, including neuron and subnet +information, SCALE encoding/decoding, and custom RPC type registry. +""" + +from scalecodec.types import GenericCall + +from .axon_info import AxonInfo +from .delegate_info import DelegateInfo +from .delegate_info_lite import DelegateInfoLite +from .ip_info import IPInfo +from .neuron_info import NeuronInfo +from .neuron_info_lite import NeuronInfoLite +from .prometheus_info import PrometheusInfo +from .proposal_vote_data import ProposalVoteData +from .scheduled_coldkey_swap_info import ScheduledColdkeySwapInfo +from .stake_info import StakeInfo +from .subnet_hyperparameters import SubnetHyperparameters +from .subnet_info import SubnetInfo +from .utils import custom_rpc_type_registry + +ProposalCallData = GenericCall diff --git a/bittensor/core/chain_data/axon_info.py b/bittensor/core/chain_data/axon_info.py new file mode 100644 index 000000000..75da486c1 --- /dev/null +++ b/bittensor/core/chain_data/axon_info.py @@ -0,0 +1,152 @@ +# 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. + +""" +This module defines the `AxonInfo` class, a data structure used to represent information about an axon endpoint +in the bittensor network. + +The `AxonInfo` class includes attributes such as version, IP address, port, IP type, hotkey, coldkey, and protocol, +along with additional placeholders to accommodate future expansion. It provides various methods to facilitate handling +and interacting with axon information, including methods for converting to and from JSON strings, checking the +serving status, generating string representations, and creating instances from a dictionary or a parameter dictionary. +""" + +import json +from dataclasses import asdict, dataclass +from typing import Any, Union + +from bittensor.utils import networking +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import torch, use_torch + + +@dataclass +class AxonInfo: + version: int + ip: str + port: int + ip_type: int + hotkey: str + coldkey: str + protocol: int = 4 + placeholder1: int = 0 + placeholder2: int = 0 + + @property + def is_serving(self) -> bool: + """True if the endpoint is serving.""" + return self.ip != "0.0.0.0" + + def ip_str(self) -> str: + """Return the whole IP as string""" + return networking.ip__str__(self.ip_type, self.ip, self.port) + + def __eq__(self, other: "AxonInfo"): + if other is None: + return False + + if ( + self.version == other.version + and self.ip == other.ip + and self.port == other.port + and self.ip_type == other.ip_type + and self.coldkey == other.coldkey + and self.hotkey == other.hotkey + ): + return True + + return False + + def __str__(self): + return f"AxonInfo( {self.ip_str()}, {self.hotkey}, {self.coldkey}, {self.version} )" + + def __repr__(self): + return self.__str__() + + def to_string(self) -> str: + """Converts the AxonInfo object to a string representation using JSON.""" + try: + return json.dumps(asdict(self)) + except (TypeError, ValueError) as e: + logging.error(f"Error converting AxonInfo to string: {e}") + return AxonInfo(0, "", 0, 0, "", "").to_string() + + @classmethod + def from_string(cls, json_string: str) -> "AxonInfo": + """ + Creates an AxonInfo object from its string representation using JSON. + + Args: + json_string (str): The JSON string representation of the AxonInfo object. + + Returns: + AxonInfo: An instance of AxonInfo created from the JSON string. If decoding fails, returns a default AxonInfo object with default values. + + Raises: + json.JSONDecodeError: If there is an error in decoding the JSON string. + TypeError: If there is a type error when creating the AxonInfo object. + ValueError: If there is a value error when creating the AxonInfo object. + """ + try: + data = json.loads(json_string) + return cls(**data) + except json.JSONDecodeError as e: + logging.error(f"Error decoding JSON: {e}") + except TypeError as e: + logging.error(f"Type error: {e}") + except ValueError as e: + logging.error(f"Value error: {e}") + return AxonInfo(0, "", 0, 0, "", "") + + @classmethod + def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": + """ + Converts a dictionary to an AxonInfo object. + + Args: + neuron_info (dict): A dictionary containing the neuron information. + + Returns: + instance (AxonInfo): An instance of AxonInfo created from the dictionary. + """ + return cls( + version=neuron_info["axon_info"]["version"], + ip=networking.int_to_ip(int(neuron_info["axon_info"]["ip"])), + port=neuron_info["axon_info"]["port"], + ip_type=neuron_info["axon_info"]["ip_type"], + hotkey=neuron_info["hotkey"], + coldkey=neuron_info["coldkey"], + ) + + def to_parameter_dict( + self, + ) -> Union[dict[str, Union[int, str]], "torch.nn.ParameterDict"]: + """Returns a torch tensor or dict of the subnet info, depending on the USE_TORCH flag set.""" + if use_torch(): + return torch.nn.ParameterDict(self.__dict__) + else: + return self.__dict__ + + @classmethod + def from_parameter_dict( + cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] + ) -> "AxonInfo": + """Returns an axon_info object from a torch parameter_dict or a parameter dict.""" + if use_torch(): + return cls(**dict(parameter_dict)) + else: + return cls(**parameter_dict) diff --git a/bittensor/core/chain_data/delegate_info.py b/bittensor/core/chain_data/delegate_info.py new file mode 100644 index 000000000..674d79457 --- /dev/null +++ b/bittensor/core/chain_data/delegate_info.py @@ -0,0 +1,107 @@ +from dataclasses import dataclass +from typing import List, Tuple, Optional, Any + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils import u16_normalized_float +from bittensor.utils.balance import Balance + + +@dataclass +class DelegateInfo: + """ + Dataclass for delegate information. For a lighter version of this class, see :func:`DelegateInfoLite`. + + Args: + hotkey_ss58 (str): Hotkey of the delegate for which the information is being fetched. + total_stake (int): Total stake of the delegate. + nominators (list[Tuple[str, int]]): List of nominators of the delegate and their stake. + take (float): Take of the delegate as a percentage. + owner_ss58 (str): Coldkey of the owner. + registrations (list[int]): List of subnets that the delegate is registered on. + validator_permits (list[int]): List of subnets that the delegate is allowed to validate on. + return_per_1000 (int): Return per 1000 TAO, for the delegate over a day. + total_daily_return (int): Total daily return of the delegate. + + """ + + hotkey_ss58: str # Hotkey of delegate + total_stake: Balance # Total stake of the delegate + nominators: List[ + Tuple[str, Balance] + ] # List of nominators of the delegate and their stake + owner_ss58: str # Coldkey of owner + take: float # Take of the delegate as a percentage + validator_permits: List[ + int + ] # List of subnets that the delegate is allowed to validate on + registrations: List[int] # List of subnets that the delegate is registered on + return_per_1000: Balance # Return per 1000 tao of the delegate over a day + total_daily_return: Balance # Total daily return of the delegate + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "DelegateInfo": + """Fixes the decoded values.""" + + return cls( + hotkey_ss58=ss58_encode( + decoded["delegate_ss58"], SS58_FORMAT + ), + owner_ss58=ss58_encode(decoded["owner_ss58"], SS58_FORMAT), + take=u16_normalized_float(decoded["take"]), + nominators=[ + ( + ss58_encode(nom[0], SS58_FORMAT), + Balance.from_rao(nom[1]), + ) + for nom in decoded["nominators"] + ], + total_stake=Balance.from_rao( + sum([nom[1] for nom in decoded["nominators"]]) + ), + validator_permits=decoded["validator_permits"], + registrations=decoded["registrations"], + return_per_1000=Balance.from_rao(decoded["return_per_1000"]), + total_daily_return=Balance.from_rao(decoded["total_daily_return"]), + ) + + @classmethod + def from_vec_u8(cls, vec_u8: List[int]) -> Optional["DelegateInfo"]: + """Returns a DelegateInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfo) + if decoded is None: + return None + + return DelegateInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: List[int]) -> List["DelegateInfo"]: + """Returns a list of DelegateInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfo, is_vec=True) + + if decoded is None: + return [] + + return [DelegateInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def delegated_list_from_vec_u8( + cls, vec_u8: List[int] + ) -> List[Tuple["DelegateInfo", "Balance"]]: + """Returns a list of Tuples of DelegateInfo objects, and Balance, from a ``vec_u8``. + + This is the list of delegates that the user has delegated to, and the amount of stake delegated. + """ + decoded = from_scale_encoding(vec_u8, ChainDataType.DelegatedInfo, is_vec=True) + if decoded is None: + return [] + + return [ + (DelegateInfo.fix_decoded_values(d), Balance.from_rao(s)) + for d, s in decoded + ] diff --git a/bittensor/core/chain_data/delegate_info_lite.py b/bittensor/core/chain_data/delegate_info_lite.py new file mode 100644 index 000000000..e874f98f8 --- /dev/null +++ b/bittensor/core/chain_data/delegate_info_lite.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass +class DelegateInfoLite: + """ + Dataclass for DelegateLiteInfo. This is a lighter version of :func:`DelegateInfo`. + + Args: + delegate_ss58 (str): Hotkey of the delegate for which the information is being fetched. + take (float): Take of the delegate as a percentage. + nominators (int): Count of the nominators of the delegate. + owner_ss58 (str): Coldkey of the owner. + registrations (list[int]): List of subnets that the delegate is registered on. + validator_permits (list[int]): List of subnets that the delegate is allowed to validate on. + return_per_1000 (int): Return per 1000 TAO, for the delegate over a day. + total_daily_return (int): Total daily return of the delegate. + """ + delegate_ss58: str # Hotkey of delegate + take: float # Take of the delegate as a percentage + nominators: int # Count of the nominators of the delegate. + owner_ss58: str # Coldkey of owner + registrations: List[int] # List of subnets that the delegate is registered on + validator_permits: List[int] # List of subnets that the delegate is allowed to validate on + return_per_1000: int # Return per 1000 tao for the delegate over a day + total_daily_return: int # Total daily return of the delegate diff --git a/bittensor/core/chain_data/ip_info.py b/bittensor/core/chain_data/ip_info.py new file mode 100644 index 000000000..2b1d733cc --- /dev/null +++ b/bittensor/core/chain_data/ip_info.py @@ -0,0 +1,69 @@ +from dataclasses import dataclass +from typing import List, Dict, Optional, Any, Union + +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.utils import networking as net +from bittensor.utils.registration import torch, use_torch + + +@dataclass +class IPInfo: + """Dataclass for associated IP Info.""" + ip: str + ip_type: int + protocol: int + + def encode(self) -> Dict[str, Any]: + """Returns a dictionary of the IPInfo object that can be encoded.""" + return { + "ip": net.ip_to_int( + self.ip + ), # IP type and protocol are encoded together as a u8 + "ip_type_and_protocol": ((self.ip_type << 4) + self.protocol) & 0xFF, + } + + @classmethod + def from_vec_u8(cls, vec_u8: List[int]) -> Optional["IPInfo"]: + """Returns a IPInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.IPInfo) + if decoded is None: + return None + + return IPInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: List[int]) -> List["IPInfo"]: + """Returns a list of IPInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.IPInfo, is_vec=True) + + if decoded is None: + return [] + + return [IPInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def fix_decoded_values(cls, decoded: Dict) -> "IPInfo": + """Returns a SubnetInfo object from a decoded IPInfo dictionary.""" + return IPInfo( + ip=net.int_to_ip(decoded["ip"]), + ip_type=decoded["ip_type_and_protocol"] >> 4, + protocol=decoded["ip_type_and_protocol"] & 0xF, + ) + + def to_parameter_dict(self) -> Union[dict[str, Union[str, int]], "torch.nn.ParameterDict"]: + """Returns a torch tensor or dict of the subnet IP info.""" + if use_torch(): + return torch.nn.ParameterDict(self.__dict__) + else: + return self.__dict__ + + @classmethod + def from_parameter_dict(cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"]) -> "IPInfo": + """Creates a IPInfo instance from a parameter dictionary.""" + if use_torch(): + return cls(**dict(parameter_dict)) + else: + return cls(**parameter_dict) diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py new file mode 100644 index 000000000..bf94b880c --- /dev/null +++ b/bittensor/core/chain_data/neuron_info.py @@ -0,0 +1,159 @@ +from dataclasses import dataclass +from typing import List, Tuple, Dict, Optional, Any, TYPE_CHECKING + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.axon_info import AxonInfo +from bittensor.core.chain_data.prometheus_info import PrometheusInfo +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils import RAOPERTAO, u16_normalized_float +from bittensor.utils.balance import Balance + +if TYPE_CHECKING: + from bittensor.core.chain_data.neuron_info_lite import NeuronInfoLite + + +@dataclass +class NeuronInfo: + """Dataclass for neuron metadata.""" + hotkey: str + coldkey: str + uid: int + netuid: int + active: int + stake: Balance + stake_dict: Dict[str, Balance] + total_stake: Balance + rank: float + emission: float + incentive: float + consensus: float + trust: float + validator_trust: float + dividends: float + last_update: int + validator_permit: bool + weights: List[List[int]] + bonds: List[List[int]] + pruning_score: int + prometheus_info: Optional["PrometheusInfo"] = None + axon_info: Optional[AxonInfo] = None + is_null: bool = False + + @classmethod + def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfo": + """Fixes the values of the NeuronInfo object.""" + neuron_info_decoded["hotkey"] = ss58_encode( + neuron_info_decoded["hotkey"], SS58_FORMAT + ) + neuron_info_decoded["coldkey"] = ss58_encode( + neuron_info_decoded["coldkey"], SS58_FORMAT + ) + stake_dict = { + ss58_encode(coldkey, SS58_FORMAT): Balance.from_rao(int(stake)) + for coldkey, stake in neuron_info_decoded["stake"] + } + neuron_info_decoded["stake_dict"] = stake_dict + neuron_info_decoded["stake"] = sum(stake_dict.values()) + neuron_info_decoded["total_stake"] = neuron_info_decoded["stake"] + neuron_info_decoded["weights"] = [ + [int(weight[0]), int(weight[1])] + for weight in neuron_info_decoded["weights"] + ] + neuron_info_decoded["bonds"] = [ + [int(bond[0]), int(bond[1])] for bond in neuron_info_decoded["bonds"] + ] + neuron_info_decoded["rank"] = u16_normalized_float(neuron_info_decoded["rank"]) + neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO + neuron_info_decoded["incentive"] = u16_normalized_float( + neuron_info_decoded["incentive"] + ) + neuron_info_decoded["consensus"] = u16_normalized_float( + neuron_info_decoded["consensus"] + ) + neuron_info_decoded["trust"] = u16_normalized_float( + neuron_info_decoded["trust"] + ) + neuron_info_decoded["validator_trust"] = u16_normalized_float( + neuron_info_decoded["validator_trust"] + ) + neuron_info_decoded["dividends"] = u16_normalized_float( + neuron_info_decoded["dividends"] + ) + neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values( + neuron_info_decoded["prometheus_info"] + ) + neuron_info_decoded["axon_info"] = AxonInfo.from_neuron_info( + neuron_info_decoded + ) + return cls(**neuron_info_decoded) + + @classmethod + def from_vec_u8(cls, vec_u8: List[int]) -> "NeuronInfo": + """Returns a NeuronInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return NeuronInfo.get_null_neuron() + + decoded = from_scale_encoding(vec_u8, ChainDataType.NeuronInfo) + if decoded is None: + return NeuronInfo.get_null_neuron() + + return NeuronInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: List[int]) -> List["NeuronInfo"]: + """Returns a list of NeuronInfo objects from a ``vec_u8``""" + + decoded_list = from_scale_encoding( + vec_u8, ChainDataType.NeuronInfo, is_vec=True + ) + if decoded_list is None: + return [] + + decoded_list = [ + NeuronInfo.fix_decoded_values(decoded) for decoded in decoded_list + ] + return decoded_list + + @staticmethod + def get_null_neuron() -> "NeuronInfo": + neuron = NeuronInfo( + uid=0, + netuid=0, + active=0, + stake=Balance.from_rao(0), + stake_dict={}, + total_stake=Balance.from_rao(0), + rank=0, + emission=0, + incentive=0, + consensus=0, + trust=0, + validator_trust=0, + dividends=0, + last_update=0, + validator_permit=False, + weights=[], + bonds=[], + prometheus_info=None, + axon_info=None, + is_null=True, + coldkey="000000000000000000000000000000000000000000000000", + hotkey="000000000000000000000000000000000000000000000000", + pruning_score=0, + ) + return neuron + + @classmethod + def from_weights_bonds_and_neuron_lite( + cls, + neuron_lite: "NeuronInfoLite", + weights_as_dict: Dict[int, List[Tuple[int, int]]], + bonds_as_dict: Dict[int, List[Tuple[int, int]]], + ) -> "NeuronInfo": + n_dict = neuron_lite.__dict__ + n_dict["weights"] = weights_as_dict.get(neuron_lite.uid, []) + n_dict["bonds"] = bonds_as_dict.get(neuron_lite.uid, []) + + return cls(**n_dict) diff --git a/bittensor/core/chain_data/neuron_info_lite.py b/bittensor/core/chain_data/neuron_info_lite.py new file mode 100644 index 000000000..5d57af0d6 --- /dev/null +++ b/bittensor/core/chain_data/neuron_info_lite.py @@ -0,0 +1,111 @@ +from dataclasses import dataclass +from typing import List, Dict, Optional, Any + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.axon_info import AxonInfo +from bittensor.core.chain_data.prometheus_info import PrometheusInfo +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils import RAOPERTAO, u16_normalized_float +from bittensor.utils.balance import Balance + + +@dataclass +class NeuronInfoLite: + """Dataclass for neuron metadata, but without the weights and bonds.""" + hotkey: str + coldkey: str + uid: int + netuid: int + active: int + stake: "Balance" + # mapping of coldkey to amount staked to this Neuron + stake_dict: Dict[str, "Balance"] + total_stake: "Balance" + rank: float + emission: float + incentive: float + consensus: float + trust: float + validator_trust: float + dividends: float + last_update: int + validator_permit: bool + prometheus_info: Optional["PrometheusInfo"] + axon_info: Optional["AxonInfo"] + pruning_score: int + is_null: bool = False + + @classmethod + def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfoLite": + """Fixes the values of the NeuronInfoLite object.""" + neuron_info_decoded["hotkey"] = ss58_encode(neuron_info_decoded["hotkey"], SS58_FORMAT) + neuron_info_decoded["coldkey"] = ss58_encode(neuron_info_decoded["coldkey"], SS58_FORMAT) + stake_dict = { + ss58_encode(coldkey, SS58_FORMAT): Balance.from_rao(int(stake)) + for coldkey, stake in neuron_info_decoded["stake"] + } + neuron_info_decoded["stake_dict"] = stake_dict + neuron_info_decoded["stake"] = sum(stake_dict.values()) + neuron_info_decoded["total_stake"] = neuron_info_decoded["stake"] + neuron_info_decoded["rank"] = u16_normalized_float(neuron_info_decoded["rank"]) + neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO + neuron_info_decoded["incentive"] = u16_normalized_float(neuron_info_decoded["incentive"]) + neuron_info_decoded["consensus"] = u16_normalized_float(neuron_info_decoded["consensus"]) + neuron_info_decoded["trust"] = u16_normalized_float(neuron_info_decoded["trust"]) + neuron_info_decoded["validator_trust"] = u16_normalized_float(neuron_info_decoded["validator_trust"]) + neuron_info_decoded["dividends"] = u16_normalized_float(neuron_info_decoded["dividends"]) + neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values(neuron_info_decoded["prometheus_info"]) + neuron_info_decoded["axon_info"] = AxonInfo.from_neuron_info(neuron_info_decoded) + return cls(**neuron_info_decoded) + + @classmethod + def from_vec_u8(cls, vec_u8: List[int]) -> "NeuronInfoLite": + """Returns a NeuronInfoLite object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return NeuronInfoLite.get_null_neuron() + + decoded = from_scale_encoding(vec_u8, ChainDataType.NeuronInfoLite) + if decoded is None: + return NeuronInfoLite.get_null_neuron() + + return NeuronInfoLite.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: List[int]) -> List["NeuronInfoLite"]: + """Returns a list of NeuronInfoLite objects from a ``vec_u8``.""" + + decoded_list = from_scale_encoding(vec_u8, ChainDataType.NeuronInfoLite, is_vec=True) + if decoded_list is None: + return [] + + decoded_list = [NeuronInfoLite.fix_decoded_values(decoded) for decoded in decoded_list] + return decoded_list + + @staticmethod + def get_null_neuron() -> "NeuronInfoLite": + neuron = NeuronInfoLite( + uid=0, + netuid=0, + active=0, + stake=Balance.from_rao(0), + stake_dict={}, + total_stake=Balance.from_rao(0), + rank=0, + emission=0, + incentive=0, + consensus=0, + trust=0, + validator_trust=0, + dividends=0, + last_update=0, + validator_permit=False, + prometheus_info=None, + axon_info=None, + is_null=True, + coldkey="000000000000000000000000000000000000000000000000", + hotkey="000000000000000000000000000000000000000000000000", + pruning_score=0, + ) + return neuron diff --git a/bittensor/core/chain_data/prometheus_info.py b/bittensor/core/chain_data/prometheus_info.py new file mode 100644 index 000000000..6c2be21c8 --- /dev/null +++ b/bittensor/core/chain_data/prometheus_info.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass +from typing import Dict + +from bittensor.utils import networking + + +@dataclass +class PrometheusInfo: + """Dataclass for prometheus info.""" + block: int + version: int + ip: str + port: int + ip_type: int + + @classmethod + def fix_decoded_values(cls, prometheus_info_decoded: Dict) -> "PrometheusInfo": + """Returns a PrometheusInfo object from a prometheus_info_decoded dictionary.""" + prometheus_info_decoded["ip"] = networking.int_to_ip(int(prometheus_info_decoded["ip"])) + return cls(**prometheus_info_decoded) diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py new file mode 100644 index 000000000..ea38feae2 --- /dev/null +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -0,0 +1,12 @@ +from typing import List, TypedDict + + +# Senate / Proposal data +class ProposalVoteData(TypedDict): + index: int + threshold: int + ayes: List[str] + nays: List[str] + end: int + + diff --git a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py new file mode 100644 index 000000000..5536c928a --- /dev/null +++ b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +from typing import List, Optional, Any + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.core.settings import SS58_FORMAT + + +@dataclass +class ScheduledColdkeySwapInfo: + """Dataclass for scheduled coldkey swap information.""" + old_coldkey: str + new_coldkey: str + arbitration_block: int + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": + """Fixes the decoded values.""" + return cls( + old_coldkey=ss58_encode(decoded["old_coldkey"], SS58_FORMAT), + new_coldkey=ss58_encode(decoded["new_coldkey"], SS58_FORMAT), + arbitration_block=decoded["arbitration_block"], + ) + + @classmethod + def from_vec_u8(cls, vec_u8: List[int]) -> Optional["ScheduledColdkeySwapInfo"]: + """Returns a ScheduledColdkeySwapInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo) + if decoded is None: + return None + + return ScheduledColdkeySwapInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: List[int]) -> List["ScheduledColdkeySwapInfo"]: + """Returns a list of ScheduledColdkeySwapInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True) + if decoded is None: + return [] + + return [ScheduledColdkeySwapInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def decode_account_id_list(cls, vec_u8: List[int]) -> Optional[List[str]]: + """Decodes a list of AccountIds from vec_u8.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True) + if decoded is None: + return None + return [ss58_encode(account_id, SS58_FORMAT) for account_id in decoded] diff --git a/bittensor/core/chain_data/stake_info.py b/bittensor/core/chain_data/stake_info.py new file mode 100644 index 000000000..e5e2c40f5 --- /dev/null +++ b/bittensor/core/chain_data/stake_info.py @@ -0,0 +1,67 @@ +from dataclasses import dataclass +from typing import List, Dict, Optional, Any + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.utils import from_scale_encoding, from_scale_encoding_using_type_string, ChainDataType +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils.balance import Balance + + +@dataclass +class StakeInfo: + """Dataclass for stake info.""" + hotkey_ss58: str # Hotkey address + coldkey_ss58: str # Coldkey address + stake: Balance # Stake for the hotkey-coldkey pair + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "StakeInfo": + """Fixes the decoded values.""" + return cls( + hotkey_ss58=ss58_encode(decoded["hotkey"], SS58_FORMAT), + coldkey_ss58=ss58_encode(decoded["coldkey"], SS58_FORMAT), + stake=Balance.from_rao(decoded["stake"]), + ) + + @classmethod + def from_vec_u8(cls, vec_u8: List[int]) -> Optional["StakeInfo"]: + """Returns a StakeInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo) + if decoded is None: + return None + + return StakeInfo.fix_decoded_values(decoded) + + @classmethod + def list_of_tuple_from_vec_u8( + cls, vec_u8: List[int] + ) -> Dict[str, List["StakeInfo"]]: + """Returns a list of StakeInfo objects from a ``vec_u8``.""" + decoded: Optional[list[tuple[str, list[object]]]] = ( + from_scale_encoding_using_type_string( + input_=vec_u8, type_string="Vec<(AccountId, Vec)>" + ) + ) + + if decoded is None: + return {} + + return { + ss58_encode(address=account_id, ss58_format=SS58_FORMAT): [ + StakeInfo.fix_decoded_values(d) for d in stake_info + ] + for account_id, stake_info in decoded + } + + @classmethod + def list_from_vec_u8(cls, vec_u8: List[int]) -> List["StakeInfo"]: + """Returns a list of StakeInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo, is_vec=True) + if decoded is None: + return [] + + return [StakeInfo.fix_decoded_values(d) for d in decoded] diff --git a/bittensor/core/chain_data/subnet_hyperparameters.py b/bittensor/core/chain_data/subnet_hyperparameters.py new file mode 100644 index 000000000..3c3016fba --- /dev/null +++ b/bittensor/core/chain_data/subnet_hyperparameters.py @@ -0,0 +1,108 @@ +from dataclasses import dataclass +from typing import List, Dict, Optional, Any, Union + +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.utils.registration import torch, use_torch + + +@dataclass +class SubnetHyperparameters: + """Dataclass for subnet hyperparameters.""" + rho: int + kappa: int + immunity_period: int + min_allowed_weights: int + max_weight_limit: float + tempo: int + min_difficulty: int + max_difficulty: int + weights_version: int + weights_rate_limit: int + adjustment_interval: int + activity_cutoff: int + registration_allowed: bool + target_regs_per_interval: int + min_burn: int + max_burn: int + bonds_moving_avg: int + max_regs_per_block: int + serving_rate_limit: int + max_validators: int + adjustment_alpha: int + difficulty: int + commit_reveal_weights_interval: int + commit_reveal_weights_enabled: bool + alpha_high: int + alpha_low: int + liquid_alpha_enabled: bool + + @classmethod + def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetHyperparameters"]: + """Returns a SubnetHyperparameters object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetHyperparameters) + if decoded is None: + return None + + return SubnetHyperparameters.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: List[int]) -> List["SubnetHyperparameters"]: + """Returns a list of SubnetHyperparameters objects from a ``vec_u8``.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.SubnetHyperparameters, is_vec=True, is_option=True + ) + if decoded is None: + return [] + + return [SubnetHyperparameters.fix_decoded_values(d) for d in decoded] + + @classmethod + def fix_decoded_values(cls, decoded: Dict) -> "SubnetHyperparameters": + """Returns a SubnetInfo object from a decoded SubnetInfo dictionary.""" + return SubnetHyperparameters( + rho=decoded["rho"], + kappa=decoded["kappa"], + immunity_period=decoded["immunity_period"], + min_allowed_weights=decoded["min_allowed_weights"], + max_weight_limit=decoded["max_weights_limit"], + tempo=decoded["tempo"], + min_difficulty=decoded["min_difficulty"], + max_difficulty=decoded["max_difficulty"], + weights_version=decoded["weights_version"], + weights_rate_limit=decoded["weights_rate_limit"], + adjustment_interval=decoded["adjustment_interval"], + activity_cutoff=decoded["activity_cutoff"], + registration_allowed=decoded["registration_allowed"], + target_regs_per_interval=decoded["target_regs_per_interval"], + min_burn=decoded["min_burn"], + max_burn=decoded["max_burn"], + max_regs_per_block=decoded["max_regs_per_block"], + max_validators=decoded["max_validators"], + serving_rate_limit=decoded["serving_rate_limit"], + bonds_moving_avg=decoded["bonds_moving_avg"], + adjustment_alpha=decoded["adjustment_alpha"], + difficulty=decoded["difficulty"], + commit_reveal_weights_interval=decoded["commit_reveal_weights_interval"], + commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], + alpha_high=decoded["alpha_high"], + alpha_low=decoded["alpha_low"], + liquid_alpha_enabled=decoded["liquid_alpha_enabled"], + ) + + def to_parameter_dict(self) -> Union[dict[str, Union[int, float, bool]], "torch.nn.ParameterDict"]: + """Returns a torch tensor or dict of the subnet hyperparameters.""" + if use_torch(): + return torch.nn.ParameterDict(self.__dict__) + else: + return self.__dict__ + + @classmethod + def from_parameter_dict(cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"]) -> "SubnetHyperparameters": + """Creates a SubnetHyperparameters instance from a parameter dictionary.""" + if use_torch(): + return cls(**dict(parameter_dict)) + else: + return cls(**parameter_dict) diff --git a/bittensor/core/chain_data/subnet_info.py b/bittensor/core/chain_data/subnet_info.py new file mode 100644 index 000000000..cf03a1393 --- /dev/null +++ b/bittensor/core/chain_data/subnet_info.py @@ -0,0 +1,100 @@ +from dataclasses import dataclass +from typing import List, Dict, Optional, Any, Union + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils import u16_normalized_float +from bittensor.utils.balance import Balance +from bittensor.utils.registration import torch, use_torch + + +@dataclass +class SubnetInfo: + """Dataclass for subnet info.""" + netuid: int + rho: int + kappa: int + difficulty: int + immunity_period: int + max_allowed_validators: int + min_allowed_weights: int + max_weight_limit: float + scaling_law_power: float + subnetwork_n: int + max_n: int + blocks_since_epoch: int + tempo: int + modality: int + # netuid -> topk percentile prunning score requirement (u16:MAX normalized.) + connection_requirements: Dict[str, float] + emission_value: float + burn: Balance + owner_ss58: str + + @classmethod + def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetInfo"]: + """Returns a SubnetInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetInfo) + if decoded is None: + return None + + return SubnetInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: List[int]) -> List["SubnetInfo"]: + """Returns a list of SubnetInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.SubnetInfo, is_vec=True, is_option=True + ) + + if decoded is None: + return [] + + return [SubnetInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def fix_decoded_values(cls, decoded: Dict) -> "SubnetInfo": + """Returns a SubnetInfo object from a decoded SubnetInfo dictionary.""" + return SubnetInfo( + netuid=decoded["netuid"], + rho=decoded["rho"], + kappa=decoded["kappa"], + difficulty=decoded["difficulty"], + immunity_period=decoded["immunity_period"], + max_allowed_validators=decoded["max_allowed_validators"], + min_allowed_weights=decoded["min_allowed_weights"], + max_weight_limit=decoded["max_weights_limit"], + scaling_law_power=decoded["scaling_law_power"], + subnetwork_n=decoded["subnetwork_n"], + max_n=decoded["max_allowed_uids"], + blocks_since_epoch=decoded["blocks_since_last_step"], + tempo=decoded["tempo"], + modality=decoded["network_modality"], + connection_requirements={ + str(int(netuid)): u16_normalized_float(int(req)) + for netuid, req in decoded["network_connect"] + }, + emission_value=decoded["emission_values"], + burn=Balance.from_rao(decoded["burn"]), + owner_ss58=ss58_encode(decoded["owner"], SS58_FORMAT), + ) + + def to_parameter_dict(self) -> Union[dict[str, Any], "torch.nn.ParameterDict"]: + """Returns a torch tensor or dict of the subnet info.""" + if use_torch(): + return torch.nn.ParameterDict(self.__dict__) + else: + return self.__dict__ + + @classmethod + def from_parameter_dict(cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"]) -> "SubnetInfo": + """Creates a SubnetInfo instance from a parameter dictionary.""" + if use_torch(): + return cls(**dict(parameter_dict)) + else: + return cls(**parameter_dict) diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py new file mode 100644 index 000000000..f5e1de813 --- /dev/null +++ b/bittensor/core/chain_data/utils.py @@ -0,0 +1,243 @@ +"""Chain data helper functions and data.""" + +from enum import Enum +from typing import Dict, List, Optional, Union + +from scalecodec.base import RuntimeConfiguration, ScaleBytes +from scalecodec.type_registry import load_type_registry_preset + + +class ChainDataType(Enum): + NeuronInfo = 1 + SubnetInfo = 2 + DelegateInfo = 3 + NeuronInfoLite = 4 + DelegatedInfo = 5 + StakeInfo = 6 + IPInfo = 7 + SubnetHyperparameters = 8 + ScheduledColdkeySwapInfo = 9 + AccountId = 10 + + +def from_scale_encoding( + input_: Union[List[int], bytes, ScaleBytes], + type_name: ChainDataType, + is_vec: bool = False, + is_option: bool = False, +) -> Optional[Dict]: + """ + Decodes input_ data from SCALE encoding based on the specified type name and modifiers. + + Args: + input_ (Union[List[int], bytes, ScaleBytes]): The input_ data to decode. + type_name (ChainDataType): The type of data being decoded. + is_vec (bool, optional): Whether the data is a vector of the specified type. Default is ``False``. + is_option (bool, optional): Whether the data is an optional value of the specified type. Default is ``False``. + + Returns: + Optional[Dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. + """ + type_string = type_name.name + if type_name == ChainDataType.DelegatedInfo: + # DelegatedInfo is a tuple of (DelegateInfo, Compact) + type_string = f"({ChainDataType.DelegateInfo.name}, Compact)" + if is_option: + type_string = f"Option<{type_string}>" + if is_vec: + type_string = f"Vec<{type_string}>" + + return from_scale_encoding_using_type_string(input_, type_string) + + +def from_scale_encoding_using_type_string( + input_: Union[List[int], bytes, ScaleBytes], type_string: str +) -> Optional[Dict]: + if isinstance(input_, ScaleBytes): + as_scale_bytes = input_ + else: + if isinstance(input_, list) and all([isinstance(i, int) for i in input_]): + vec_u8 = input_ + as_bytes = bytes(vec_u8) + elif isinstance(input_, bytes): + as_bytes = input_ + else: + raise TypeError("input_ must be a List[int], bytes, or ScaleBytes") + + as_scale_bytes = ScaleBytes(as_bytes) + + rpc_runtime_config = RuntimeConfiguration() + rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) + rpc_runtime_config.update_type_registry(custom_rpc_type_registry) + + obj = rpc_runtime_config.create_scale_object(type_string, data=as_scale_bytes) + + return obj.decode() + + +custom_rpc_type_registry = { + "types": { + "SubnetInfo": { + "type": "struct", + "type_mapping": [ + ["netuid", "Compact"], + ["rho", "Compact"], + ["kappa", "Compact"], + ["difficulty", "Compact"], + ["immunity_period", "Compact"], + ["max_allowed_validators", "Compact"], + ["min_allowed_weights", "Compact"], + ["max_weights_limit", "Compact"], + ["scaling_law_power", "Compact"], + ["subnetwork_n", "Compact"], + ["max_allowed_uids", "Compact"], + ["blocks_since_last_step", "Compact"], + ["tempo", "Compact"], + ["network_modality", "Compact"], + ["network_connect", "Vec<[u16; 2]>"], + ["emission_values", "Compact"], + ["burn", "Compact"], + ["owner", "AccountId"], + ], + }, + "DelegateInfo": { + "type": "struct", + "type_mapping": [ + ["delegate_ss58", "AccountId"], + ["take", "Compact"], + ["nominators", "Vec<(AccountId, Compact)>"], + ["owner_ss58", "AccountId"], + ["registrations", "Vec>"], + ["validator_permits", "Vec>"], + ["return_per_1000", "Compact"], + ["total_daily_return", "Compact"], + ], + }, + "NeuronInfo": { + "type": "struct", + "type_mapping": [ + ["hotkey", "AccountId"], + ["coldkey", "AccountId"], + ["uid", "Compact"], + ["netuid", "Compact"], + ["active", "bool"], + ["axon_info", "axon_info"], + ["prometheus_info", "PrometheusInfo"], + ["stake", "Vec<(AccountId, Compact)>"], + ["rank", "Compact"], + ["emission", "Compact"], + ["incentive", "Compact"], + ["consensus", "Compact"], + ["trust", "Compact"], + ["validator_trust", "Compact"], + ["dividends", "Compact"], + ["last_update", "Compact"], + ["validator_permit", "bool"], + ["weights", "Vec<(Compact, Compact)>"], + ["bonds", "Vec<(Compact, Compact)>"], + ["pruning_score", "Compact"], + ], + }, + "NeuronInfoLite": { + "type": "struct", + "type_mapping": [ + ["hotkey", "AccountId"], + ["coldkey", "AccountId"], + ["uid", "Compact"], + ["netuid", "Compact"], + ["active", "bool"], + ["axon_info", "axon_info"], + ["prometheus_info", "PrometheusInfo"], + ["stake", "Vec<(AccountId, Compact)>"], + ["rank", "Compact"], + ["emission", "Compact"], + ["incentive", "Compact"], + ["consensus", "Compact"], + ["trust", "Compact"], + ["validator_trust", "Compact"], + ["dividends", "Compact"], + ["last_update", "Compact"], + ["validator_permit", "bool"], + ["pruning_score", "Compact"], + ], + }, + "axon_info": { + "type": "struct", + "type_mapping": [ + ["block", "u64"], + ["version", "u32"], + ["ip", "u128"], + ["port", "u16"], + ["ip_type", "u8"], + ["protocol", "u8"], + ["placeholder1", "u8"], + ["placeholder2", "u8"], + ], + }, + "PrometheusInfo": { + "type": "struct", + "type_mapping": [ + ["block", "u64"], + ["version", "u32"], + ["ip", "u128"], + ["port", "u16"], + ["ip_type", "u8"], + ], + }, + "IPInfo": { + "type": "struct", + "type_mapping": [ + ["ip", "Compact"], + ["ip_type_and_protocol", "Compact"], + ], + }, + "StakeInfo": { + "type": "struct", + "type_mapping": [ + ["hotkey", "AccountId"], + ["coldkey", "AccountId"], + ["stake", "Compact"], + ], + }, + "SubnetHyperparameters": { + "type": "struct", + "type_mapping": [ + ["rho", "Compact"], + ["kappa", "Compact"], + ["immunity_period", "Compact"], + ["min_allowed_weights", "Compact"], + ["max_weights_limit", "Compact"], + ["tempo", "Compact"], + ["min_difficulty", "Compact"], + ["max_difficulty", "Compact"], + ["weights_version", "Compact"], + ["weights_rate_limit", "Compact"], + ["adjustment_interval", "Compact"], + ["activity_cutoff", "Compact"], + ["registration_allowed", "bool"], + ["target_regs_per_interval", "Compact"], + ["min_burn", "Compact"], + ["max_burn", "Compact"], + ["bonds_moving_avg", "Compact"], + ["max_regs_per_block", "Compact"], + ["serving_rate_limit", "Compact"], + ["max_validators", "Compact"], + ["adjustment_alpha", "Compact"], + ["difficulty", "Compact"], + ["commit_reveal_weights_interval", "Compact"], + ["commit_reveal_weights_enabled", "bool"], + ["alpha_high", "Compact"], + ["alpha_low", "Compact"], + ["liquid_alpha_enabled", "bool"], + ], + }, + "ScheduledColdkeySwapInfo": { + "type": "struct", + "type_mapping": [ + ["old_coldkey", "AccountId"], + ["new_coldkey", "AccountId"], + ["arbitration_block", "Compact"], + ], + }, + } +} diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index db1a0d7a6..6d35a365c 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -26,7 +26,7 @@ def int_to_ip(int_val: int) -> str: - r"""Maps an integer to a unique ip-string + """Maps an integer to a unique ip-string Args: int_val (:type:`int128`, `required`): The integer representation of an ip. Must be in the range (0, 3.4028237e+38). @@ -43,7 +43,7 @@ def int_to_ip(int_val: int) -> str: def ip_to_int(str_val: str) -> int: - r"""Maps an ip-string to a unique integer. + """Maps an ip-string to a unique integer. arg: str_val (:tyep:`str`, `required): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 @@ -60,7 +60,7 @@ def ip_to_int(str_val: str) -> int: def ip_version(str_val: str) -> int: - r"""Returns the ip version (IPV4 or IPV6). + """Returns the ip version (IPV4 or IPV6). arg: str_val (:tyep:`str`, `required): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 @@ -86,7 +86,7 @@ class ExternalIPNotFound(Exception): def get_external_ip() -> str: - r"""Checks CURL/URLLIB/IPIFY/AWS for your external ip. + """Checks CURL/URLLIB/IPIFY/AWS for your external ip. Returns: external_ip (:obj:`str` `required`): Your routers external facing ip as a string. From 20480e064b2731bb3255fe7c1eabc129d48637d2 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 22 Aug 2024 21:28:07 -0700 Subject: [PATCH 137/260] Fix test --- tests/unit_tests/test_chain_data.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index e2d761645..f5f0ed40e 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -18,7 +18,8 @@ import pytest import torch -from bittensor.core.chain_data import AxonInfo, ChainDataType, DelegateInfo, NeuronInfo +from bittensor.core.chain_data import AxonInfo, DelegateInfo, NeuronInfo +from bittensor.core.chain_data.utils import ChainDataType RAOPERTAO = 10**18 @@ -527,7 +528,7 @@ def test_fix_decoded_values_error_cases( @pytest.fixture def mock_from_scale_encoding(mocker): - return mocker.patch("bittensor.core.chain_data.from_scale_encoding") + return mocker.patch("bittensor.core.chain_data.delegate_info.from_scale_encoding") @pytest.fixture From e67da2f9ee900b9b687f40f21e99e0dbb0e3744d Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 22 Aug 2024 21:31:04 -0700 Subject: [PATCH 138/260] ruff --- bittensor/core/chain_data/delegate_info.py | 4 +- .../core/chain_data/delegate_info_lite.py | 5 ++- bittensor/core/chain_data/ip_info.py | 9 +++- bittensor/core/chain_data/neuron_info.py | 1 + bittensor/core/chain_data/neuron_info_lite.py | 45 ++++++++++++++----- bittensor/core/chain_data/prometheus_info.py | 5 ++- .../core/chain_data/proposal_vote_data.py | 2 - .../chain_data/scheduled_coldkey_swap_info.py | 9 +++- bittensor/core/chain_data/stake_info.py | 7 ++- .../core/chain_data/subnet_hyperparameters.py | 9 +++- bittensor/core/chain_data/subnet_info.py | 5 ++- bittensor/core/config.py | 1 + 12 files changed, 76 insertions(+), 26 deletions(-) diff --git a/bittensor/core/chain_data/delegate_info.py b/bittensor/core/chain_data/delegate_info.py index 674d79457..dcbf5bf8f 100644 --- a/bittensor/core/chain_data/delegate_info.py +++ b/bittensor/core/chain_data/delegate_info.py @@ -46,9 +46,7 @@ def fix_decoded_values(cls, decoded: Any) -> "DelegateInfo": """Fixes the decoded values.""" return cls( - hotkey_ss58=ss58_encode( - decoded["delegate_ss58"], SS58_FORMAT - ), + hotkey_ss58=ss58_encode(decoded["delegate_ss58"], SS58_FORMAT), owner_ss58=ss58_encode(decoded["owner_ss58"], SS58_FORMAT), take=u16_normalized_float(decoded["take"]), nominators=[ diff --git a/bittensor/core/chain_data/delegate_info_lite.py b/bittensor/core/chain_data/delegate_info_lite.py index e874f98f8..84f2bdf5c 100644 --- a/bittensor/core/chain_data/delegate_info_lite.py +++ b/bittensor/core/chain_data/delegate_info_lite.py @@ -17,11 +17,14 @@ class DelegateInfoLite: return_per_1000 (int): Return per 1000 TAO, for the delegate over a day. total_daily_return (int): Total daily return of the delegate. """ + delegate_ss58: str # Hotkey of delegate take: float # Take of the delegate as a percentage nominators: int # Count of the nominators of the delegate. owner_ss58: str # Coldkey of owner registrations: List[int] # List of subnets that the delegate is registered on - validator_permits: List[int] # List of subnets that the delegate is allowed to validate on + validator_permits: List[ + int + ] # List of subnets that the delegate is allowed to validate on return_per_1000: int # Return per 1000 tao for the delegate over a day total_daily_return: int # Total daily return of the delegate diff --git a/bittensor/core/chain_data/ip_info.py b/bittensor/core/chain_data/ip_info.py index 2b1d733cc..aa44fc327 100644 --- a/bittensor/core/chain_data/ip_info.py +++ b/bittensor/core/chain_data/ip_info.py @@ -9,6 +9,7 @@ @dataclass class IPInfo: """Dataclass for associated IP Info.""" + ip: str ip_type: int protocol: int @@ -53,7 +54,9 @@ def fix_decoded_values(cls, decoded: Dict) -> "IPInfo": protocol=decoded["ip_type_and_protocol"] & 0xF, ) - def to_parameter_dict(self) -> Union[dict[str, Union[str, int]], "torch.nn.ParameterDict"]: + def to_parameter_dict( + self, + ) -> Union[dict[str, Union[str, int]], "torch.nn.ParameterDict"]: """Returns a torch tensor or dict of the subnet IP info.""" if use_torch(): return torch.nn.ParameterDict(self.__dict__) @@ -61,7 +64,9 @@ def to_parameter_dict(self) -> Union[dict[str, Union[str, int]], "torch.nn.Param return self.__dict__ @classmethod - def from_parameter_dict(cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"]) -> "IPInfo": + def from_parameter_dict( + cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] + ) -> "IPInfo": """Creates a IPInfo instance from a parameter dictionary.""" if use_torch(): return cls(**dict(parameter_dict)) diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py index bf94b880c..160e21392 100644 --- a/bittensor/core/chain_data/neuron_info.py +++ b/bittensor/core/chain_data/neuron_info.py @@ -17,6 +17,7 @@ @dataclass class NeuronInfo: """Dataclass for neuron metadata.""" + hotkey: str coldkey: str uid: int diff --git a/bittensor/core/chain_data/neuron_info_lite.py b/bittensor/core/chain_data/neuron_info_lite.py index 5d57af0d6..a7d0f2252 100644 --- a/bittensor/core/chain_data/neuron_info_lite.py +++ b/bittensor/core/chain_data/neuron_info_lite.py @@ -14,6 +14,7 @@ @dataclass class NeuronInfoLite: """Dataclass for neuron metadata, but without the weights and bonds.""" + hotkey: str coldkey: str uid: int @@ -40,8 +41,12 @@ class NeuronInfoLite: @classmethod def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfoLite": """Fixes the values of the NeuronInfoLite object.""" - neuron_info_decoded["hotkey"] = ss58_encode(neuron_info_decoded["hotkey"], SS58_FORMAT) - neuron_info_decoded["coldkey"] = ss58_encode(neuron_info_decoded["coldkey"], SS58_FORMAT) + neuron_info_decoded["hotkey"] = ss58_encode( + neuron_info_decoded["hotkey"], SS58_FORMAT + ) + neuron_info_decoded["coldkey"] = ss58_encode( + neuron_info_decoded["coldkey"], SS58_FORMAT + ) stake_dict = { ss58_encode(coldkey, SS58_FORMAT): Balance.from_rao(int(stake)) for coldkey, stake in neuron_info_decoded["stake"] @@ -51,13 +56,27 @@ def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfoLite": neuron_info_decoded["total_stake"] = neuron_info_decoded["stake"] neuron_info_decoded["rank"] = u16_normalized_float(neuron_info_decoded["rank"]) neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO - neuron_info_decoded["incentive"] = u16_normalized_float(neuron_info_decoded["incentive"]) - neuron_info_decoded["consensus"] = u16_normalized_float(neuron_info_decoded["consensus"]) - neuron_info_decoded["trust"] = u16_normalized_float(neuron_info_decoded["trust"]) - neuron_info_decoded["validator_trust"] = u16_normalized_float(neuron_info_decoded["validator_trust"]) - neuron_info_decoded["dividends"] = u16_normalized_float(neuron_info_decoded["dividends"]) - neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values(neuron_info_decoded["prometheus_info"]) - neuron_info_decoded["axon_info"] = AxonInfo.from_neuron_info(neuron_info_decoded) + neuron_info_decoded["incentive"] = u16_normalized_float( + neuron_info_decoded["incentive"] + ) + neuron_info_decoded["consensus"] = u16_normalized_float( + neuron_info_decoded["consensus"] + ) + neuron_info_decoded["trust"] = u16_normalized_float( + neuron_info_decoded["trust"] + ) + neuron_info_decoded["validator_trust"] = u16_normalized_float( + neuron_info_decoded["validator_trust"] + ) + neuron_info_decoded["dividends"] = u16_normalized_float( + neuron_info_decoded["dividends"] + ) + neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values( + neuron_info_decoded["prometheus_info"] + ) + neuron_info_decoded["axon_info"] = AxonInfo.from_neuron_info( + neuron_info_decoded + ) return cls(**neuron_info_decoded) @classmethod @@ -76,11 +95,15 @@ def from_vec_u8(cls, vec_u8: List[int]) -> "NeuronInfoLite": def list_from_vec_u8(cls, vec_u8: List[int]) -> List["NeuronInfoLite"]: """Returns a list of NeuronInfoLite objects from a ``vec_u8``.""" - decoded_list = from_scale_encoding(vec_u8, ChainDataType.NeuronInfoLite, is_vec=True) + decoded_list = from_scale_encoding( + vec_u8, ChainDataType.NeuronInfoLite, is_vec=True + ) if decoded_list is None: return [] - decoded_list = [NeuronInfoLite.fix_decoded_values(decoded) for decoded in decoded_list] + decoded_list = [ + NeuronInfoLite.fix_decoded_values(decoded) for decoded in decoded_list + ] return decoded_list @staticmethod diff --git a/bittensor/core/chain_data/prometheus_info.py b/bittensor/core/chain_data/prometheus_info.py index 6c2be21c8..a6279c2cf 100644 --- a/bittensor/core/chain_data/prometheus_info.py +++ b/bittensor/core/chain_data/prometheus_info.py @@ -7,6 +7,7 @@ @dataclass class PrometheusInfo: """Dataclass for prometheus info.""" + block: int version: int ip: str @@ -16,5 +17,7 @@ class PrometheusInfo: @classmethod def fix_decoded_values(cls, prometheus_info_decoded: Dict) -> "PrometheusInfo": """Returns a PrometheusInfo object from a prometheus_info_decoded dictionary.""" - prometheus_info_decoded["ip"] = networking.int_to_ip(int(prometheus_info_decoded["ip"])) + prometheus_info_decoded["ip"] = networking.int_to_ip( + int(prometheus_info_decoded["ip"]) + ) return cls(**prometheus_info_decoded) diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py index ea38feae2..501915690 100644 --- a/bittensor/core/chain_data/proposal_vote_data.py +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -8,5 +8,3 @@ class ProposalVoteData(TypedDict): ayes: List[str] nays: List[str] end: int - - diff --git a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py index 5536c928a..2b8d89719 100644 --- a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py +++ b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py @@ -10,6 +10,7 @@ @dataclass class ScheduledColdkeySwapInfo: """Dataclass for scheduled coldkey swap information.""" + old_coldkey: str new_coldkey: str arbitration_block: int @@ -38,7 +39,9 @@ def from_vec_u8(cls, vec_u8: List[int]) -> Optional["ScheduledColdkeySwapInfo"]: @classmethod def list_from_vec_u8(cls, vec_u8: List[int]) -> List["ScheduledColdkeySwapInfo"]: """Returns a list of ScheduledColdkeySwapInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True) + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True + ) if decoded is None: return [] @@ -47,7 +50,9 @@ def list_from_vec_u8(cls, vec_u8: List[int]) -> List["ScheduledColdkeySwapInfo"] @classmethod def decode_account_id_list(cls, vec_u8: List[int]) -> Optional[List[str]]: """Decodes a list of AccountIds from vec_u8.""" - decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True) + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True + ) if decoded is None: return None return [ss58_encode(account_id, SS58_FORMAT) for account_id in decoded] diff --git a/bittensor/core/chain_data/stake_info.py b/bittensor/core/chain_data/stake_info.py index e5e2c40f5..5f6adf6b7 100644 --- a/bittensor/core/chain_data/stake_info.py +++ b/bittensor/core/chain_data/stake_info.py @@ -3,7 +3,11 @@ from scalecodec.utils.ss58 import ss58_encode -from bittensor.core.chain_data.utils import from_scale_encoding, from_scale_encoding_using_type_string, ChainDataType +from bittensor.core.chain_data.utils import ( + from_scale_encoding, + from_scale_encoding_using_type_string, + ChainDataType, +) from bittensor.core.settings import SS58_FORMAT from bittensor.utils.balance import Balance @@ -11,6 +15,7 @@ @dataclass class StakeInfo: """Dataclass for stake info.""" + hotkey_ss58: str # Hotkey address coldkey_ss58: str # Coldkey address stake: Balance # Stake for the hotkey-coldkey pair diff --git a/bittensor/core/chain_data/subnet_hyperparameters.py b/bittensor/core/chain_data/subnet_hyperparameters.py index 3c3016fba..fe0581838 100644 --- a/bittensor/core/chain_data/subnet_hyperparameters.py +++ b/bittensor/core/chain_data/subnet_hyperparameters.py @@ -8,6 +8,7 @@ @dataclass class SubnetHyperparameters: """Dataclass for subnet hyperparameters.""" + rho: int kappa: int immunity_period: int @@ -92,7 +93,9 @@ def fix_decoded_values(cls, decoded: Dict) -> "SubnetHyperparameters": liquid_alpha_enabled=decoded["liquid_alpha_enabled"], ) - def to_parameter_dict(self) -> Union[dict[str, Union[int, float, bool]], "torch.nn.ParameterDict"]: + def to_parameter_dict( + self, + ) -> Union[dict[str, Union[int, float, bool]], "torch.nn.ParameterDict"]: """Returns a torch tensor or dict of the subnet hyperparameters.""" if use_torch(): return torch.nn.ParameterDict(self.__dict__) @@ -100,7 +103,9 @@ def to_parameter_dict(self) -> Union[dict[str, Union[int, float, bool]], "torch. return self.__dict__ @classmethod - def from_parameter_dict(cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"]) -> "SubnetHyperparameters": + def from_parameter_dict( + cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] + ) -> "SubnetHyperparameters": """Creates a SubnetHyperparameters instance from a parameter dictionary.""" if use_torch(): return cls(**dict(parameter_dict)) diff --git a/bittensor/core/chain_data/subnet_info.py b/bittensor/core/chain_data/subnet_info.py index cf03a1393..ffbcc4024 100644 --- a/bittensor/core/chain_data/subnet_info.py +++ b/bittensor/core/chain_data/subnet_info.py @@ -13,6 +13,7 @@ @dataclass class SubnetInfo: """Dataclass for subnet info.""" + netuid: int rho: int kappa: int @@ -92,7 +93,9 @@ def to_parameter_dict(self) -> Union[dict[str, Any], "torch.nn.ParameterDict"]: return self.__dict__ @classmethod - def from_parameter_dict(cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"]) -> "SubnetInfo": + def from_parameter_dict( + cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] + ) -> "SubnetInfo": """Creates a SubnetInfo instance from a parameter dictionary.""" if use_torch(): return cls(**dict(parameter_dict)) diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 335ec11ef..9d0c18348 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -47,6 +47,7 @@ class Config(DefaultMunch): Returns: config (bittensor.config): Nested config object created from parser arguments. """ + __is_set: Dict[str, bool] def __init__( From 2fb47e38fa32ac8419802cde6368dd4714495bb6 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 23 Aug 2024 08:55:09 -0700 Subject: [PATCH 139/260] review fix --- tests/unit_tests/utils/test_balance.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/unit_tests/utils/test_balance.py b/tests/unit_tests/utils/test_balance.py index b294df4f4..66aa550f2 100644 --- a/tests/unit_tests/utils/test_balance.py +++ b/tests/unit_tests/utils/test_balance.py @@ -515,11 +515,6 @@ def test_from_float(): assert Balance.from_tao(1.0) == Balance(1000000000) -def test_from_tao(): - """Tests from_tao method call.""" - assert Balance.from_tao(1.0) == Balance(1000000000) - - def test_from_rao(): """Tests from_rao method call.""" assert Balance.from_tao(1) == Balance(1000000000) From 6663b85f29950eeed9e435e18d4a040626ecb758 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 23 Aug 2024 11:58:25 -0700 Subject: [PATCH 140/260] update `requirements/prod.txt` --- requirements/prod.txt | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/requirements/prod.txt b/requirements/prod.txt index 854f30e3e..a066d9332 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,36 +1,21 @@ aiohttp~=3.9 -ansible~=6.7 -ansible_vault~=2.1 -backoff -certifi~=2024.2.2 colorama~=0.4.6 -cryptography~=42.0.5 -ddt~=1.6.0 -eth-utils<2.3.0 -fuzzywuzzy>=0.18.0 fastapi~=0.110.1 munch~=2.5.0 -netaddr numpy~=1.26 msgpack-numpy-opentensor~=0.5.0 nest_asyncio +netaddr packaging -pycryptodome>=3.18.0,<4.0.0 -pyyaml -password_strength -pydantic>=2.3, <3 -PyNaCl~=1.3 -python-Levenshtein python-statemachine~=2.1 +pyyaml retry requests rich +pydantic>=2.3, <3 scalecodec==1.2.11 setuptools~=70.0.0 -shtab~=1.6.5 substrate-interface~=1.7.9 -termcolor -tqdm uvicorn wheel git+ssh://git@github.com/opentensor/btwallet.git@main#egg=bittensor-wallet From ff7853f1bcec336911e077816bec1784e0affcfd Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 26 Aug 2024 23:49:07 -0700 Subject: [PATCH 141/260] Update localnet entrypoint port --- bittensor/core/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index cc6ab0769..827feadd3 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -74,7 +74,7 @@ def turn_console_on(): FINNEY_ENTRYPOINT = "wss://entrypoint-finney.opentensor.ai:443" FINNEY_TEST_ENTRYPOINT = "wss://test.finney.opentensor.ai:443/" ARCHIVE_ENTRYPOINT = "wss://archive.chain.opentensor.ai:443/" -LOCAL_ENTRYPOINT = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or "ws://127.0.0.1:9944" +LOCAL_ENTRYPOINT = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or "ws://127.0.0.1:9946" # Currency Symbols Bittensor TAO_SYMBOL: str = chr(0x03C4) From 137a3966e56223abb8a0776cc85894710dc224ea Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 26 Aug 2024 23:50:10 -0700 Subject: [PATCH 142/260] Add substrate reconnection logic --- bittensor/core/subtensor.py | 79 ++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 0ec910a5c..1cb0af1e1 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -24,6 +24,7 @@ import copy import socket import sys +from functools import wraps from typing import List, Dict, Union, Optional, Tuple, TypedDict, Any import numpy as np @@ -82,6 +83,18 @@ class ParamWithTypes(TypedDict): type: str # ScaleType string of the parameter. +def _ensure_connected(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + # Check the socket state before method execution + if self.substrate.websocket.sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) != 0: + logging.info("Reconnection substrate...") + self._get_substrate() + # Execute the method if the connection is active or after reconnecting + return func(self, *args, **kwargs) + return wrapper + + class Subtensor: """ The Subtensor class in Bittensor serves as a crucial interface for interacting with the Bittensor blockchain, @@ -187,7 +200,27 @@ def __init__( "To get ahead of this change, please run a local subtensor node and point to it." ) - # Attempt to connect to chosen endpoint. Fallback to finney if local unavailable. + self.substrate = None + self.log_verbose = log_verbose + self._get_substrate() + + def __str__(self) -> str: + if self.network == self.chain_endpoint: + # Connecting to chain endpoint without network known. + return f"subtensor({self.chain_endpoint})" + else: + # Connecting to network with endpoint known. + return f"subtensor({self.network}, {self.chain_endpoint})" + + def __repr__(self) -> str: + return self.__str__() + + def close(self): + """Cleans up resources for this subtensor instance like active websocket connection and active extensions.""" + self.substrate.close() + + def _get_substrate(self): + """Establishes a connection to the Substrate node using configured parameters.""" try: # Set up params. self.substrate = SubstrateInterface( @@ -196,6 +229,9 @@ def __init__( url=self.chain_endpoint, type_registry=settings.TYPE_REGISTRY, ) + if self.log_verbose: + logging.info(f"Connected to {self.network} network and {self.chain_endpoint}.") + except ConnectionRefusedError: logging.error( f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint. Exiting...", @@ -205,7 +241,6 @@ def __init__( f"{self.chain_endpoint.split(':')[2]}" ) sys.exit(1) - # TODO (edu/phil): Advise to run local subtensor and point to dev docs. try: self.substrate.websocket.settimeout(600) @@ -216,28 +251,6 @@ def __init__( except (socket.error, OSError) as e: logging.warning(f"Socket error: {e}") - if log_verbose: - logging.info( - f"Connected to {self.network} network and {self.chain_endpoint}." - ) - - self._subtensor_errors: Dict[str, Dict[str, str]] = {} - - def __str__(self) -> str: - if self.network == self.chain_endpoint: - # Connecting to chain endpoint without network known. - return f"subtensor({self.chain_endpoint})" - else: - # Connecting to network with endpoint known. - return f"subtensor({self.network}, {self.chain_endpoint})" - - def __repr__(self) -> str: - return self.__str__() - - def close(self): - """Cleans up resources for this subtensor instance like active websocket connection and active extensions.""" - self.substrate.close() - @staticmethod def config() -> "Config": """ @@ -383,6 +396,7 @@ def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = Non pass # Inner private functions + @_ensure_connected def _encode_params( self, call_definition: List["ParamWithTypes"], @@ -427,6 +441,7 @@ def _get_hyperparameter( return result.value # Calls methods + @_ensure_connected def query_subtensor( self, name: str, block: Optional[int] = None, params: Optional[list] = None ) -> "ScaleType": @@ -457,6 +472,7 @@ def make_substrate_call_with_retry() -> "ScaleType": return make_substrate_call_with_retry() + @_ensure_connected def query_map_subtensor( self, name: str, block: Optional[int] = None, params: Optional[list] = None ) -> "QueryMapResult": @@ -539,6 +555,7 @@ def query_runtime_api( return obj.decode() + @_ensure_connected def state_call( self, method: str, data: str, block: Optional[int] = None ) -> Dict[Any, Any]: @@ -566,6 +583,7 @@ def make_substrate_call_with_retry() -> Dict[Any, Any]: return make_substrate_call_with_retry() + @_ensure_connected def query_map( self, module: str, @@ -601,6 +619,7 @@ def make_substrate_call_with_retry() -> "QueryMapResult": return make_substrate_call_with_retry() + @_ensure_connected def query_constant( self, module_name: str, constant_name: str, block: Optional[int] = None ) -> Optional["ScaleType"]: @@ -630,6 +649,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @_ensure_connected def query_module( self, module: str, @@ -753,6 +773,7 @@ def get_netuids_for_hotkey( else [] ) + @_ensure_connected def get_current_block(self) -> int: """ Returns the current block number on the Bittensor blockchain. This function provides the latest block number, indicating the most recent state of the blockchain. @@ -828,6 +849,7 @@ def is_hotkey_registered( else: return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) + @_ensure_connected def do_set_weights( self, wallet: "Wallet", @@ -1002,6 +1024,7 @@ def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: call = self._get_hyperparameter(param_name="LastUpdate", netuid=netuid) return None if call is None else self.get_current_block() - int(call[uid]) + @_ensure_connected def get_block_hash(self, block_id: int) -> str: """ Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique identifier representing the cryptographic hash of the block's content, ensuring its integrity and immutability. @@ -1059,6 +1082,7 @@ def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int ) return None if call is None else int(call) + @_ensure_connected def do_transfer( self, wallet: "Wallet", @@ -1170,6 +1194,7 @@ def get_neuron_for_pubkey_and_subnet( block=block, ) + @_ensure_connected def neuron_for_uid( self, uid: Optional[int], netuid: int, block: Optional[int] = None ) -> NeuronInfo: @@ -1208,6 +1233,7 @@ def make_substrate_call_with_retry(): return NeuronInfo.from_vec_u8(result) # Community uses this method via `bittensor.api.extrinsics.prometheus.prometheus_extrinsic` + @_ensure_connected def do_serve_prometheus( self, wallet: "Wallet", @@ -1290,6 +1316,7 @@ def serve_prometheus( ) # Community uses this method as part of `subtensor.serve_axon` + @_ensure_connected def do_serve_axon( self, wallet: "Wallet", @@ -1731,6 +1758,7 @@ def weights( return w_map # Used by community via `transfer_extrinsic` + @_ensure_connected def get_balance(self, address: str, block: Optional[int] = None) -> Balance: """ Retrieves the token balance of a specific address within the Bittensor network. This function queries the blockchain to determine the amount of Tao held by a given account. @@ -1767,6 +1795,7 @@ def make_substrate_call_with_retry(): return Balance(result.value["data"]["free"]) # Used in community via `bittensor.core.subtensor.Subtensor.transfer` + @_ensure_connected def get_transfer_fee( self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] ) -> "Balance": @@ -1917,6 +1946,7 @@ def commit_weights( return success, message # Community uses this method + @_ensure_connected def _do_commit_weights( self, wallet: "Wallet", @@ -2040,6 +2070,7 @@ def reveal_weights( return success, message # Community uses this method + @_ensure_connected def _do_reveal_weights( self, wallet: "Wallet", From 2d8a2024a69c7db5178d878d7740c6440a7b3707 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 26 Aug 2024 23:50:25 -0700 Subject: [PATCH 143/260] Fix tests + add tests --- tests/unit_tests/test_subtensor.py | 361 +++++++++++++---------------- 1 file changed, 163 insertions(+), 198 deletions(-) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 45d436eaf..d605973ce 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -229,17 +229,11 @@ def test_determine_chain_endpoint_and_network( assert result_endpoint == expected_endpoint -# Subtensor().get_error_info_by_index tests @pytest.fixture -def substrate(): - class MockSubstrate: - pass - - return MockSubstrate() - - -@pytest.fixture -def subtensor(substrate): +def subtensor(mocker): + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object(subtensor_module, "SubstrateInterface", return_value=fake_substrate) return Subtensor() @@ -249,7 +243,6 @@ def mock_logger(): yield mock_warning -# Subtensor()._get_hyperparameter tests def test_hyperparameter_subnet_does_not_exist(subtensor, mocker): """Tests when the subnet does not exist.""" subtensor.subnet_exists = mocker.MagicMock(return_value=False) @@ -268,7 +261,6 @@ def test_hyperparameter_result_is_none(subtensor, mocker): def test_hyperparameter_result_has_no_value(subtensor, mocker): """Test when the result has no 'value' attribute.""" - subtensor.subnet_exists = mocker.MagicMock(return_value=True) subtensor.query_subtensor = mocker.MagicMock(return_value=None) assert subtensor._get_hyperparameter("Difficulty", 1, None) is None @@ -869,20 +861,17 @@ def test_query_subtensor(subtensor, mocker): # Prep fake_name = "module_name" - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate - # Call result = subtensor.query_subtensor(fake_name) # Asserts - mocked_substrate.query.assert_called_once_with( + subtensor.substrate.query.assert_called_once_with( module="SubtensorModule", storage_function=fake_name, params=None, block_hash=None, ) - assert result == mocked_substrate.query.return_value + assert result == subtensor.substrate.query.return_value def test_query_runtime_api(subtensor, mocker): @@ -923,20 +912,17 @@ def test_query_map_subtensor(subtensor, mocker): # Prep fake_name = "module_name" - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate - # Call result = subtensor.query_map_subtensor(fake_name) # Asserts - mocked_substrate.query_map.assert_called_once_with( + subtensor.substrate.query_map.assert_called_once_with( module="SubtensorModule", storage_function=fake_name, params=None, block_hash=None, ) - assert result == mocked_substrate.query_map.return_value + assert result == subtensor.substrate.query_map.return_value def test_state_call(subtensor, mocker): @@ -944,18 +930,16 @@ def test_state_call(subtensor, mocker): # Prep fake_method = "method" fake_data = "data" - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate # Call result = subtensor.state_call(fake_method, fake_data) # Asserts - mocked_substrate.rpc_request.assert_called_once_with( + subtensor.substrate.rpc_request.assert_called_once_with( method="state_call", params=[fake_method, fake_data], ) - assert result == mocked_substrate.rpc_request.return_value + assert result == subtensor.substrate.rpc_request.return_value def test_query_map(subtensor, mocker): @@ -963,20 +947,18 @@ def test_query_map(subtensor, mocker): # Prep fake_module_name = "module_name" fake_name = "constant_name" - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate # Call result = subtensor.query_map(fake_module_name, fake_name) # Asserts - mocked_substrate.query_map.assert_called_once_with( + subtensor.substrate.query_map.assert_called_once_with( module=fake_module_name, storage_function=fake_name, params=None, block_hash=None, ) - assert result == mocked_substrate.query_map.return_value + assert result == subtensor.substrate.query_map.return_value def test_query_constant(subtensor, mocker): @@ -984,39 +966,35 @@ def test_query_constant(subtensor, mocker): # Prep fake_module_name = "module_name" fake_constant_name = "constant_name" - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate # Call result = subtensor.query_constant(fake_module_name, fake_constant_name) # Asserts - mocked_substrate.get_constant.assert_called_once_with( + subtensor.substrate.get_constant.assert_called_once_with( module_name=fake_module_name, constant_name=fake_constant_name, block_hash=None, ) - assert result == mocked_substrate.get_constant.return_value + assert result == subtensor.substrate.get_constant.return_value -def test_query_module(subtensor, mocker): +def test_query_module(subtensor): # Prep fake_module = "module" fake_name = "function_name" - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate # Call result = subtensor.query_module(fake_module, fake_name) # Asserts - mocked_substrate.query.assert_called_once_with( + subtensor.substrate.query.assert_called_once_with( module=fake_module, storage_function=fake_name, params=None, block_hash=None, ) - assert result == mocked_substrate.query.return_value + assert result == subtensor.substrate.query.return_value def test_metagraph(subtensor, mocker): @@ -1058,18 +1036,14 @@ def test_get_netuids_for_hotkey(subtensor, mocker): assert result == [] -def test_get_current_block(subtensor, mocker): +def test_get_current_block(subtensor): """Tests get_current_block call.""" - # Prep - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate - # Call result = subtensor.get_current_block() # Asserts - mocked_substrate.get_block_number.assert_called_once_with(None) - assert result == mocked_substrate.get_block_number.return_value + subtensor.substrate.get_block_number.assert_called_once_with(None) + assert result == subtensor.substrate.get_block_number.return_value def test_is_hotkey_registered_any(subtensor, mocker): @@ -1159,9 +1133,7 @@ def test_do_set_weights_is_success(subtensor, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True - mocked_substrate = mocker.MagicMock() - mocked_substrate.submit_extrinsic.return_value.is_success = True - subtensor.substrate = mocked_substrate + subtensor.substrate.submit_extrinsic.return_value.is_success = True # Call result = subtensor._do_set_weights( @@ -1175,7 +1147,7 @@ def test_do_set_weights_is_success(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="set_weights", call_params={ @@ -1186,19 +1158,19 @@ def test_do_set_weights_is_success(subtensor, mocker): }, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey, era={"period": 5}, ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) - mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() assert result == (True, "Successfully set weights.") @@ -1212,10 +1184,7 @@ def test_do_set_weights_is_not_success(subtensor, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True - mocked_substrate = mocker.MagicMock() - mocked_substrate.submit_extrinsic.return_value.is_success = False - subtensor.substrate = mocked_substrate - + subtensor.substrate.submit_extrinsic.return_value.is_success = False mocked_format_error_message = mocker.MagicMock() subtensor_module.format_error_message = mocked_format_error_message @@ -1231,7 +1200,7 @@ def test_do_set_weights_is_not_success(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="set_weights", call_params={ @@ -1242,21 +1211,21 @@ def test_do_set_weights_is_not_success(subtensor, mocker): }, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey, era={"period": 5}, ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) - mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() mocked_format_error_message.assert_called_once_with( - mocked_substrate.submit_extrinsic.return_value.error_message + subtensor.substrate.submit_extrinsic.return_value.error_message ) assert result == (False, mocked_format_error_message.return_value) @@ -1271,9 +1240,6 @@ def test_do_set_weights_no_waits(subtensor, mocker): fake_wait_for_inclusion = False fake_wait_for_finalization = False - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate - # Call result = subtensor._do_set_weights( wallet=fake_wallet, @@ -1286,7 +1252,7 @@ def test_do_set_weights_no_waits(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="set_weights", call_params={ @@ -1297,14 +1263,14 @@ def test_do_set_weights_no_waits(subtensor, mocker): }, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey, era={"period": 5}, ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) @@ -1405,15 +1371,13 @@ def test_get_block_hash(subtensor, mocker): """Tests successful get_block_hash call.""" # Prep fake_block_id = 123 - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate # Call result = subtensor.get_block_hash(fake_block_id) # Asserts - mocked_substrate.get_block_hash.assert_called_once_with(block_id=fake_block_id) - assert result == mocked_substrate.get_block_hash.return_value + subtensor.substrate.get_block_hash.assert_called_once_with(block_id=fake_block_id) + assert result == subtensor.substrate.get_block_hash.return_value def test_commit(subtensor, mocker): @@ -1464,9 +1428,7 @@ def test_do_transfer_is_success_true(subtensor, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True - mocked_substrate = mocker.MagicMock() - mocked_substrate.submit_extrinsic.return_value.is_success = True - subtensor.substrate = mocked_substrate + subtensor.substrate.submit_extrinsic.return_value.is_success = True # Call result = subtensor.do_transfer( @@ -1478,23 +1440,23 @@ def test_do_transfer_is_success_true(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="Balances", call_function="transfer_allow_death", call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.coldkey + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) - mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() assert result == ( True, - mocked_substrate.submit_extrinsic.return_value.block_hash, + subtensor.substrate.submit_extrinsic.return_value.block_hash, None, ) @@ -1508,9 +1470,7 @@ def test_do_transfer_is_success_false(subtensor, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True - mocked_substrate = mocker.MagicMock() - mocked_substrate.submit_extrinsic.return_value.is_success = False - subtensor.substrate = mocked_substrate + subtensor.substrate.submit_extrinsic.return_value.is_success = False mocked_format_error_message = mocker.MagicMock() subtensor_module.format_error_message = mocked_format_error_message @@ -1525,22 +1485,22 @@ def test_do_transfer_is_success_false(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="Balances", call_function="transfer_allow_death", call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.coldkey + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) - mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() mocked_format_error_message.assert_called_once_with( - mocked_substrate.submit_extrinsic.return_value.error_message + subtensor.substrate.submit_extrinsic.return_value.error_message ) assert result == (False, None, mocked_format_error_message.return_value) @@ -1554,9 +1514,6 @@ def test_do_transfer_no_waits(subtensor, mocker): fake_wait_for_inclusion = False fake_wait_for_finalization = False - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate - # Call result = subtensor.do_transfer( fake_wallet, @@ -1567,16 +1524,16 @@ def test_do_transfer_no_waits(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="Balances", call_function="transfer_allow_death", call_params={"dest": fake_dest, "value": fake_transfer_balance.rao}, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.coldkey + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkey ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) @@ -1678,9 +1635,7 @@ def test_neuron_for_uid_response_none(subtensor, mocker): subtensor_module.NeuronInfo, "get_null_neuron" ) - mocked_substrate = mocker.MagicMock() - mocked_substrate.rpc_request.return_value.get.return_value = None - subtensor.substrate = mocked_substrate + subtensor.substrate.rpc_request.return_value.get.return_value = None # Call result = subtensor.neuron_for_uid( @@ -1688,10 +1643,10 @@ def test_neuron_for_uid_response_none(subtensor, mocker): ) # Asserts - mocked_substrate.get_block_hash.assert_called_once_with(fake_block) - mocked_substrate.rpc_request.assert_called_once_with( + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) + subtensor.substrate.rpc_request.assert_called_once_with( method="neuronInfo_getNeuron", - params=[fake_netuid, fake_uid, mocked_substrate.get_block_hash.return_value], + params=[fake_netuid, fake_uid, subtensor.substrate.get_block_hash.return_value], ) mocked_neuron_info.assert_called_once() @@ -1708,23 +1663,20 @@ def test_neuron_for_uid_success(subtensor, mocker): subtensor_module.NeuronInfo, "from_vec_u8" ) - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate - # Call result = subtensor.neuron_for_uid( uid=fake_uid, netuid=fake_netuid, block=fake_block ) # Asserts - mocked_substrate.get_block_hash.assert_called_once_with(fake_block) - mocked_substrate.rpc_request.assert_called_once_with( + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) + subtensor.substrate.rpc_request.assert_called_once_with( method="neuronInfo_getNeuron", - params=[fake_netuid, fake_uid, mocked_substrate.get_block_hash.return_value], + params=[fake_netuid, fake_uid, subtensor.substrate.get_block_hash.return_value], ) mocked_neuron_from_vec_u8.assert_called_once_with( - mocked_substrate.rpc_request.return_value.get.return_value + subtensor.substrate.rpc_request.return_value.get.return_value ) assert result == mocked_neuron_from_vec_u8.return_value @@ -1737,9 +1689,7 @@ def test_do_serve_prometheus_is_success(subtensor, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True - mocked_substrate = mocker.MagicMock() - mocked_substrate.submit_extrinsic.return_value.is_success = True - subtensor.substrate = mocked_substrate + subtensor.substrate.submit_extrinsic.return_value.is_success = True # Call result = subtensor._do_serve_prometheus( @@ -1750,24 +1700,24 @@ def test_do_serve_prometheus_is_success(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="serve_prometheus", call_params=fake_call_params, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey, ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) - mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() assert result == (True, None) @@ -1779,9 +1729,7 @@ def test_do_serve_prometheus_is_not_success(subtensor, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True - mocked_substrate = mocker.MagicMock() - mocked_substrate.submit_extrinsic.return_value.is_success = None - subtensor.substrate = mocked_substrate + subtensor.substrate.submit_extrinsic.return_value.is_success = None mocked_format_error_message = mocker.MagicMock() subtensor_module.format_error_message = mocked_format_error_message @@ -1795,26 +1743,26 @@ def test_do_serve_prometheus_is_not_success(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="serve_prometheus", call_params=fake_call_params, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey, ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) - mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() mocked_format_error_message.assert_called_once_with( - mocked_substrate.submit_extrinsic.return_value.error_message + subtensor.substrate.submit_extrinsic.return_value.error_message ) assert result == (False, mocked_format_error_message.return_value) @@ -1827,9 +1775,6 @@ def test_do_serve_prometheus_no_waits(subtensor, mocker): fake_wait_for_inclusion = False fake_wait_for_finalization = False - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate - # Call result = subtensor._do_serve_prometheus( wallet=fake_wallet, @@ -1839,19 +1784,19 @@ def test_do_serve_prometheus_no_waits(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="serve_prometheus", call_params=fake_call_params, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey, ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) @@ -1901,9 +1846,7 @@ def test_do_serve_axon_is_success(subtensor, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True - mocked_substrate = mocker.MagicMock() - mocked_substrate.submit_extrinsic.return_value.is_success = True - subtensor.substrate = mocked_substrate + subtensor.substrate.submit_extrinsic.return_value.is_success = True # Call result = subtensor._do_serve_axon( @@ -1914,24 +1857,24 @@ def test_do_serve_axon_is_success(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="serve_axon", call_params=fake_call_params, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey, ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) - mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() assert result == (True, None) @@ -1943,9 +1886,7 @@ def test_do_serve_axon_is_not_success(subtensor, mocker): fake_wait_for_inclusion = True fake_wait_for_finalization = True - mocked_substrate = mocker.MagicMock() - mocked_substrate.submit_extrinsic.return_value.is_success = None - subtensor.substrate = mocked_substrate + subtensor.substrate.submit_extrinsic.return_value.is_success = None mocked_format_error_message = mocker.MagicMock() subtensor_module.format_error_message = mocked_format_error_message @@ -1959,26 +1900,26 @@ def test_do_serve_axon_is_not_success(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="serve_axon", call_params=fake_call_params, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey, ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) - mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() mocked_format_error_message.assert_called_once_with( - mocked_substrate.submit_extrinsic.return_value.error_message + subtensor.substrate.submit_extrinsic.return_value.error_message ) assert result == (False, mocked_format_error_message.return_value) @@ -1991,9 +1932,6 @@ def test_do_serve_axon_no_waits(subtensor, mocker): fake_wait_for_inclusion = False fake_wait_for_finalization = False - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate - # Call result = subtensor._do_serve_axon( wallet=fake_wallet, @@ -2003,19 +1941,19 @@ def test_do_serve_axon_no_waits(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="serve_axon", call_params=fake_call_params, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey, ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=fake_wait_for_inclusion, wait_for_finalization=fake_wait_for_finalization, ) @@ -2212,24 +2150,21 @@ def test_get_transfer_fee(subtensor, mocker): fake_dest = "SS58ADDRESS" value = 1 - mocked_substrate = mocker.MagicMock() - subtensor.substrate = mocked_substrate - fake_payment_info = {"partialFee": int(2e10)} - mocked_substrate.get_payment_info.return_value = fake_payment_info + subtensor.substrate.get_payment_info.return_value = fake_payment_info # Call result = subtensor.get_transfer_fee(wallet=fake_wallet, dest=fake_dest, value=value) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="Balances", call_function="transfer_allow_death", call_params={"dest": fake_dest, "value": value}, ) - mocked_substrate.get_payment_info.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.coldkeypub + subtensor.substrate.get_payment_info.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkeypub ) assert result == 2e10 @@ -2343,9 +2278,7 @@ def test_do_commit_weights(subtensor, mocker): wait_for_inclusion = True wait_for_finalization = True - mocked_substrate = mocker.MagicMock() - mocked_substrate.submit_extrinsic.return_value.is_success = None - subtensor.substrate = mocked_substrate + subtensor.substrate.submit_extrinsic.return_value.is_success = None mocked_format_error_message = mocker.MagicMock() subtensor_module.format_error_message = mocked_format_error_message @@ -2360,7 +2293,7 @@ def test_do_commit_weights(subtensor, mocker): ) # Assertions - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="commit_weights", call_params={ @@ -2369,17 +2302,17 @@ def test_do_commit_weights(subtensor, mocker): }, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.hotkey + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() assert result == (False, mocked_format_error_message.return_value) @@ -2470,9 +2403,7 @@ def test_do_reveal_weights(subtensor, mocker): wait_for_inclusion = True wait_for_finalization = True - mocked_substrate = mocker.MagicMock() - mocked_substrate.submit_extrinsic.return_value.is_success = None - subtensor.substrate = mocked_substrate + subtensor.substrate.submit_extrinsic.return_value.is_success = None mocked_format_error_message = mocker.MagicMock() subtensor_module.format_error_message = mocked_format_error_message @@ -2490,7 +2421,7 @@ def test_do_reveal_weights(subtensor, mocker): ) # Asserts - mocked_substrate.compose_call.assert_called_once_with( + subtensor.substrate.compose_call.assert_called_once_with( call_module="SubtensorModule", call_function="reveal_weights", call_params={ @@ -2502,16 +2433,50 @@ def test_do_reveal_weights(subtensor, mocker): }, ) - mocked_substrate.create_signed_extrinsic.assert_called_once_with( - call=mocked_substrate.compose_call.return_value, keypair=fake_wallet.hotkey + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey ) - mocked_substrate.submit_extrinsic.assert_called_once_with( - mocked_substrate.create_signed_extrinsic.return_value, + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - mocked_substrate.submit_extrinsic.return_value.process_events.assert_called_once() + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() assert result == (False, mocked_format_error_message.return_value) + + +def test_connect_without_substrate(mocker): + """Ensure re-connection is called when using an alive substrate.""" + # Prep + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 1 + mocker.patch.object(subtensor_module, "SubstrateInterface", return_value=fake_substrate) + fake_subtensor = Subtensor() + spy_get_substrate = mocker.spy(Subtensor, "_get_substrate") + + # Call + _ = fake_subtensor.block + + # Assertions + assert spy_get_substrate.call_count == 1 + + +def test_connect_with_substrate(mocker): + """Ensure re-connection is non called when using an alive substrate.""" + # Prep + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object(subtensor_module, "SubstrateInterface", return_value=fake_substrate) + fake_subtensor = Subtensor() + spy_get_substrate = mocker.spy(Subtensor, "_get_substrate") + + # Call + _ = fake_subtensor.block + + # Assertions + assert spy_get_substrate.call_count == 0 + + From bfe64c39c8b96ce70c5d429996b2092730ce6e6a Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 26 Aug 2024 23:52:36 -0700 Subject: [PATCH 144/260] Ruff formatter --- bittensor/core/subtensor.py | 13 +++++++++---- tests/unit_tests/test_subtensor.py | 17 +++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 1cb0af1e1..58c79d43b 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -87,11 +87,15 @@ def _ensure_connected(func): @wraps(func) def wrapper(self, *args, **kwargs): # Check the socket state before method execution - if self.substrate.websocket.sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) != 0: + if ( + self.substrate.websocket.sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + != 0 + ): logging.info("Reconnection substrate...") self._get_substrate() # Execute the method if the connection is active or after reconnecting return func(self, *args, **kwargs) + return wrapper @@ -200,7 +204,6 @@ def __init__( "To get ahead of this change, please run a local subtensor node and point to it." ) - self.substrate = None self.log_verbose = log_verbose self._get_substrate() @@ -218,7 +221,7 @@ def __repr__(self) -> str: def close(self): """Cleans up resources for this subtensor instance like active websocket connection and active extensions.""" self.substrate.close() - + def _get_substrate(self): """Establishes a connection to the Substrate node using configured parameters.""" try: @@ -230,7 +233,9 @@ def _get_substrate(self): type_registry=settings.TYPE_REGISTRY, ) if self.log_verbose: - logging.info(f"Connected to {self.network} network and {self.chain_endpoint}.") + logging.info( + f"Connected to {self.network} network and {self.chain_endpoint}." + ) except ConnectionRefusedError: logging.error( diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index d605973ce..15129a03c 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -233,7 +233,9 @@ def test_determine_chain_endpoint_and_network( def subtensor(mocker): fake_substrate = mocker.MagicMock() fake_substrate.websocket.sock.getsockopt.return_value = 0 - mocker.patch.object(subtensor_module, "SubstrateInterface", return_value=fake_substrate) + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) return Subtensor() @@ -2164,7 +2166,8 @@ def test_get_transfer_fee(subtensor, mocker): ) subtensor.substrate.get_payment_info.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.coldkeypub + call=subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.coldkeypub, ) assert result == 2e10 @@ -2453,7 +2456,9 @@ def test_connect_without_substrate(mocker): # Prep fake_substrate = mocker.MagicMock() fake_substrate.websocket.sock.getsockopt.return_value = 1 - mocker.patch.object(subtensor_module, "SubstrateInterface", return_value=fake_substrate) + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) fake_subtensor = Subtensor() spy_get_substrate = mocker.spy(Subtensor, "_get_substrate") @@ -2469,7 +2474,9 @@ def test_connect_with_substrate(mocker): # Prep fake_substrate = mocker.MagicMock() fake_substrate.websocket.sock.getsockopt.return_value = 0 - mocker.patch.object(subtensor_module, "SubstrateInterface", return_value=fake_substrate) + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) fake_subtensor = Subtensor() spy_get_substrate = mocker.spy(Subtensor, "_get_substrate") @@ -2478,5 +2485,3 @@ def test_connect_with_substrate(mocker): # Assertions assert spy_get_substrate.call_count == 0 - - From b7d0f57e92936a8b3ee7da7d167ffc5487161179 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 27 Aug 2024 08:46:09 -0700 Subject: [PATCH 145/260] add docstring to the decorator --- bittensor/core/subtensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 58c79d43b..80d37178e 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -84,6 +84,7 @@ class ParamWithTypes(TypedDict): def _ensure_connected(func): + """Decorator ensuring the function executes with an active substrate connection.""" @wraps(func) def wrapper(self, *args, **kwargs): # Check the socket state before method execution From be770cffb05210e15c27eaf745f87bdda037b183 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 28 Aug 2024 18:29:00 -0700 Subject: [PATCH 146/260] update requirements --- requirements/prod.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/prod.txt b/requirements/prod.txt index a066d9332..1022d37cc 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,3 +1,5 @@ +wheel +setuptools~=70.0.0 aiohttp~=3.9 colorama~=0.4.6 fastapi~=0.110.1 @@ -14,8 +16,6 @@ requests rich pydantic>=2.3, <3 scalecodec==1.2.11 -setuptools~=70.0.0 substrate-interface~=1.7.9 uvicorn -wheel git+ssh://git@github.com/opentensor/btwallet.git@main#egg=bittensor-wallet From 5a9c5a98256b337cfd914398fec37644780b1852 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 28 Aug 2024 19:49:58 -0700 Subject: [PATCH 147/260] update requirements --- requirements/prod.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements/prod.txt b/requirements/prod.txt index 1022d37cc..00159aae6 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -4,7 +4,7 @@ aiohttp~=3.9 colorama~=0.4.6 fastapi~=0.110.1 munch~=2.5.0 -numpy~=1.26 +numpy~=2.0.1 msgpack-numpy-opentensor~=0.5.0 nest_asyncio netaddr @@ -15,7 +15,9 @@ retry requests rich pydantic>=2.3, <3 +python-Levenshtein scalecodec==1.2.11 substrate-interface~=1.7.9 uvicorn git+ssh://git@github.com/opentensor/btwallet.git@main#egg=bittensor-wallet +git+ssh://git@github.com/opentensor/btcli.git@main#egg=bittensor-cli \ No newline at end of file From 84e5b2f75d41f9d316c6fcc77a6e195a3201a091 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 28 Aug 2024 22:51:59 -0700 Subject: [PATCH 148/260] update requirements --- requirements/btcli.txt | 1 + requirements/prod.txt | 3 +-- setup.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 requirements/btcli.txt diff --git a/requirements/btcli.txt b/requirements/btcli.txt new file mode 100644 index 000000000..f38e489a8 --- /dev/null +++ b/requirements/btcli.txt @@ -0,0 +1 @@ +git+ssh://git@github.com/opentensor/btcli.git@main#egg=bittensor-cli \ No newline at end of file diff --git a/requirements/prod.txt b/requirements/prod.txt index 00159aae6..62a0ac178 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -19,5 +19,4 @@ python-Levenshtein scalecodec==1.2.11 substrate-interface~=1.7.9 uvicorn -git+ssh://git@github.com/opentensor/btwallet.git@main#egg=bittensor-wallet -git+ssh://git@github.com/opentensor/btcli.git@main#egg=bittensor-cli \ No newline at end of file +git+ssh://git@github.com/opentensor/btwallet.git@main#egg=bittensor-wallet \ No newline at end of file diff --git a/setup.py b/setup.py index 37d3d07b2..bb0db9808 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ def read_requirements(path): requirements = read_requirements("requirements/prod.txt") +extra_requirements_btcli = read_requirements("requirements/btcli.txt") extra_requirements_dev = read_requirements("requirements/dev.txt") extra_requirements_cubit = read_requirements("requirements/cubit.txt") extra_requirements_torch = read_requirements("requirements/torch.txt") @@ -74,6 +75,8 @@ def read_requirements(path): python_requires=">=3.9", install_requires=requirements, extras_require={ + "btcli": extra_requirements_btcli, + "cubit": extra_requirements_cubit, "dev": extra_requirements_dev, "torch": extra_requirements_torch, }, @@ -81,7 +84,6 @@ def read_requirements(path): "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Topic :: Software Development :: Build Tools", - # Pick your license as you wish "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", From 0d236db63370425a892db0a5a27c260b2a439105 Mon Sep 17 00:00:00 2001 From: opendansor Date: Thu, 29 Aug 2024 11:24:29 -0700 Subject: [PATCH 149/260] Child Hotkeys netuid Refactor --- bittensor/commands/stake.py | 59 ++++++++++++------- .../subcommands/stake/test_childkeys.py | 8 +-- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index 132529a13..24868d048 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -44,19 +44,25 @@ def get_netuid( - cli: "bittensor.cli", subtensor: "bittensor.subtensor" + cli: "bittensor.cli", subtensor: "bittensor.subtensor", prompt: bool = True ) -> Tuple[bool, int]: """Retrieve and validate the netuid from the user or configuration.""" console = Console() - if not cli.config.is_set("netuid"): - try: - cli.config.netuid = int(Prompt.ask("Enter netuid")) - except ValueError: - console.print( - "[red]Invalid input. Please enter a valid integer for netuid.[/red]" - ) - return False, -1 + if not cli.config.is_set("netuid") and prompt: + cli.config.netuid = Prompt.ask("Enter netuid") + try: + cli.config.netuid = int(cli.config.netuid) + except ValueError: + console.print( + "[red]Invalid input. Please enter a valid integer for netuid.[/red]" + ) + return False, -1 netuid = cli.config.netuid + if netuid < 0 or netuid > 2**32 - 1: + console.print( + "[red]Invalid input. Please enter a valid integer for netuid in subnet range.[/red]" + ) + return False, -1 if not subtensor.subnet_exists(netuid=netuid): console.print( "[red]Network with netuid {} does not exist. Please try again.[/red]".format( @@ -1136,10 +1142,27 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): wallet = bittensor.wallet(config=cli.config) # check all - if not cli.config.is_set("all"): - exists, netuid = get_netuid(cli, subtensor) - if not exists: - return + if cli.config.is_set("all"): + cli.config.netuid = None + cli.config.all = True + elif cli.config.is_set("netuid"): + if cli.config.netuid == "all": + cli.config.all = True + else: + cli.config.netuid = int(cli.config.netuid) + exists, netuid = get_netuid(cli, subtensor) + if not exists: + return + else: + netuid_input = Prompt.ask("Enter netuid or 'all'", default="all") + if netuid_input == "all": + cli.config.netuid = None + cli.config.all = True + else: + cli.config.netuid = int(netuid_input) + exists, netuid = get_netuid(cli, subtensor, False) + if not exists: + return # get parent hotkey hotkey = get_hotkey(wallet, cli.config) @@ -1148,11 +1171,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): return try: - netuids = ( - subtensor.get_all_subnet_netuids() - if cli.config.is_set("all") - else [netuid] - ) + netuids = subtensor.get_all_subnet_netuids() if cli.config.all else [netuid] hotkey_stake = GetChildrenCommand.get_parent_stake_info( console, subtensor, hotkey ) @@ -1236,7 +1255,7 @@ def add_args(parser: argparse.ArgumentParser): parser = parser.add_parser( "get_children", help="""Get child hotkeys on subnet.""" ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) + parser.add_argument("--netuid", dest="netuid", type=str, required=False) parser.add_argument("--hotkey", dest="hotkey", type=str, required=False) parser.add_argument( "--all", @@ -1294,7 +1313,7 @@ def render_table( # Add columns to the table with specific styles table.add_column("Index", style="bold yellow", no_wrap=True, justify="center") - table.add_column("ChildHotkey", style="bold green") + table.add_column("Child Hotkey", style="bold green") table.add_column("Proportion", style="bold cyan", no_wrap=True, justify="right") table.add_column( "Childkey Take", style="bold blue", no_wrap=True, justify="right" diff --git a/tests/e2e_tests/subcommands/stake/test_childkeys.py b/tests/e2e_tests/subcommands/stake/test_childkeys.py index 080d01263..a8f6518fc 100644 --- a/tests/e2e_tests/subcommands/stake/test_childkeys.py +++ b/tests/e2e_tests/subcommands/stake/test_childkeys.py @@ -54,7 +54,7 @@ async def test_set_revoke_children_multiple(local_chain, capsys): assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() for exec_command in [alice_exec_command, bob_exec_command, eve_exec_command]: - exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) + exec_command(RegisterCommand, ["s", "register", "--netuid", "4"]) alice_exec_command(StakeCommand, ["stake", "add", "--amount", "100000"]) @@ -75,8 +75,8 @@ async def wait(): await wait() children_with_proportions = [ - [0.4, bob_keypair.ss58_address], - [0.2, eve_keypair.ss58_address], + [0.2, bob_keypair.ss58_address], + [0.1, eve_keypair.ss58_address], ] # Test 1: Set multiple children @@ -86,7 +86,7 @@ async def wait(): "stake", "set_children", "--netuid", - "1", + "2", "--children", f"{children_with_proportions[0][1]},{children_with_proportions[1][1]}", "--hotkey", From 4379c596fa4a21c27c21cc9a08410be9e4c904d9 Mon Sep 17 00:00:00 2001 From: opendansor Date: Wed, 28 Aug 2024 17:09:10 -0700 Subject: [PATCH 150/260] CHK Test --- tests/e2e_tests/subcommands/stake/test_childkeys.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e_tests/subcommands/stake/test_childkeys.py b/tests/e2e_tests/subcommands/stake/test_childkeys.py index a8f6518fc..080d01263 100644 --- a/tests/e2e_tests/subcommands/stake/test_childkeys.py +++ b/tests/e2e_tests/subcommands/stake/test_childkeys.py @@ -54,7 +54,7 @@ async def test_set_revoke_children_multiple(local_chain, capsys): assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() for exec_command in [alice_exec_command, bob_exec_command, eve_exec_command]: - exec_command(RegisterCommand, ["s", "register", "--netuid", "4"]) + exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) alice_exec_command(StakeCommand, ["stake", "add", "--amount", "100000"]) @@ -75,8 +75,8 @@ async def wait(): await wait() children_with_proportions = [ - [0.2, bob_keypair.ss58_address], - [0.1, eve_keypair.ss58_address], + [0.4, bob_keypair.ss58_address], + [0.2, eve_keypair.ss58_address], ] # Test 1: Set multiple children @@ -86,7 +86,7 @@ async def wait(): "stake", "set_children", "--netuid", - "2", + "1", "--children", f"{children_with_proportions[0][1]},{children_with_proportions[1][1]}", "--hotkey", From 4b046c3351ce2695445a789bdc7a5d1836ac26a9 Mon Sep 17 00:00:00 2001 From: opendansor Date: Thu, 29 Aug 2024 11:29:07 -0700 Subject: [PATCH 151/260] u16 float limit --- bittensor/commands/stake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index 24868d048..eff415d1a 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -58,7 +58,7 @@ def get_netuid( ) return False, -1 netuid = cli.config.netuid - if netuid < 0 or netuid > 2**32 - 1: + if netuid < 0 or netuid > 65535: console.print( "[red]Invalid input. Please enter a valid integer for netuid in subnet range.[/red]" ) From 0d3e8e8bab40771e17520efa239aec08b567b2fb Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 29 Aug 2024 12:16:24 -0700 Subject: [PATCH 152/260] cleanup __init__.py --- bittensor/__init__.py | 42 +++++++------------------------------ bittensor/core/settings.py | 25 ++++++++++++++++++++++ bittensor/core/subtensor.py | 1 + 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index cf2cb8ce7..4882af25a 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -15,7 +15,6 @@ # 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 os import warnings from .core.settings import __version__, version_split, DEFAULTS @@ -23,16 +22,6 @@ from .utils.deprecated import * -def __getattr__(name): - if name == "version_split": - warnings.warn( - "version_split is deprecated and will be removed in future versions. Use __version__ instead.", - DeprecationWarning, - ) - return version_split - raise AttributeError(f"module {__name__} has no attribute {name}") - - # Logging helpers. def trace(on: bool = True): logging.set_trace(on) @@ -42,26 +31,11 @@ def debug(on: bool = True): logging.set_debug(on) -def __apply_nest_asyncio(): - """ - Apply nest_asyncio if the environment variable NEST_ASYNCIO is set to "1" or not set. - If not set, warn the user that the default will change in the future. - """ - nest_asyncio_env = os.getenv("NEST_ASYNCIO") - - if nest_asyncio_env == "1" or nest_asyncio_env is None: - if nest_asyncio_env is None: - warnings.warn( - """NEST_ASYNCIO implicitly set to '1'. In the future, the default value will be '0'. - If you use `nest_asyncio`, make sure to add it explicitly to your project dependencies, - as it will be removed from `bittensor` package dependencies in the future. - To silence this warning, explicitly set the environment variable, e.g. `export NEST_ASYNCIO=0`.""", - DeprecationWarning, - ) - # Install and apply nest asyncio to allow the async functions to run in a .ipynb - import nest_asyncio - - nest_asyncio.apply() - - -__apply_nest_asyncio() +def __getattr__(name): + if name == "version_split": + warnings.warn( + "version_split is deprecated and will be removed in future versions. Use __version__ instead.", + DeprecationWarning, + ) + return version_split + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 827feadd3..cfccf362b 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -19,6 +19,7 @@ import os import re +import warnings from pathlib import Path from munch import munchify @@ -214,3 +215,27 @@ def turn_console_on(): e * (_version_int_base**i) for i, e in enumerate(reversed(_version_info)) ) assert version_as_int < 2**31 # fits in int32 + + +def __apply_nest_asyncio(): + """ + Apply nest_asyncio if the environment variable NEST_ASYNCIO is set to "1" or not set. + If not set, warn the user that the default will change in the future. + """ + nest_asyncio_env = os.getenv("NEST_ASYNCIO") + if nest_asyncio_env == "1" or nest_asyncio_env is None: + if nest_asyncio_env is None: + warnings.warn( + """NEST_ASYNCIO implicitly set to '1'. In the future, the default value will be '0'. + If you use `nest_asyncio`, make sure to add it explicitly to your project dependencies, + as it will be removed from `bittensor` package dependencies in the future. + To silence this warning, explicitly set the environment variable, e.g. `export NEST_ASYNCIO=0`.""", + DeprecationWarning, + ) + # Install and apply nest asyncio to allow the async functions to run in a .ipynb + import nest_asyncio + + nest_asyncio.apply() + + +__apply_nest_asyncio() diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 80d37178e..32b3c1a08 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -85,6 +85,7 @@ class ParamWithTypes(TypedDict): def _ensure_connected(func): """Decorator ensuring the function executes with an active substrate connection.""" + @wraps(func) def wrapper(self, *args, **kwargs): # Check the socket state before method execution From 08da14f8c0d31f48983316071f5718835324532e Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 29 Aug 2024 18:15:10 -0700 Subject: [PATCH 153/260] - move `extrinsics` from deprecated into `bittensor/core`; - update `deprecated` sub-package to `deprecated.py` module. --- .../extrinsics/__init__.py | 0 .../extrinsics/commit_weights.py | 0 .../extrinsics/prometheus.py | 0 .../deprecated => core}/extrinsics/serving.py | 0 .../extrinsics/set_weights.py | 0 .../extrinsics/transfer.py | 0 bittensor/core/subtensor.py | 22 +++++++++---------- .../{deprecated/__init__.py => deprecated.py} | 4 ++-- .../unit_tests/extrinsics/test_prometheus.py | 2 +- tests/unit_tests/extrinsics/test_serving.py | 8 +++---- .../unit_tests/extrinsics/test_set_weights.py | 2 +- tests/unit_tests/test_deprecated.py | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) rename bittensor/{utils/deprecated => core}/extrinsics/__init__.py (100%) rename bittensor/{utils/deprecated => core}/extrinsics/commit_weights.py (100%) rename bittensor/{utils/deprecated => core}/extrinsics/prometheus.py (100%) rename bittensor/{utils/deprecated => core}/extrinsics/serving.py (100%) rename bittensor/{utils/deprecated => core}/extrinsics/set_weights.py (100%) rename bittensor/{utils/deprecated => core}/extrinsics/transfer.py (100%) rename bittensor/utils/{deprecated/__init__.py => deprecated.py} (96%) diff --git a/bittensor/utils/deprecated/extrinsics/__init__.py b/bittensor/core/extrinsics/__init__.py similarity index 100% rename from bittensor/utils/deprecated/extrinsics/__init__.py rename to bittensor/core/extrinsics/__init__.py diff --git a/bittensor/utils/deprecated/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py similarity index 100% rename from bittensor/utils/deprecated/extrinsics/commit_weights.py rename to bittensor/core/extrinsics/commit_weights.py diff --git a/bittensor/utils/deprecated/extrinsics/prometheus.py b/bittensor/core/extrinsics/prometheus.py similarity index 100% rename from bittensor/utils/deprecated/extrinsics/prometheus.py rename to bittensor/core/extrinsics/prometheus.py diff --git a/bittensor/utils/deprecated/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py similarity index 100% rename from bittensor/utils/deprecated/extrinsics/serving.py rename to bittensor/core/extrinsics/serving.py diff --git a/bittensor/utils/deprecated/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py similarity index 100% rename from bittensor/utils/deprecated/extrinsics/set_weights.py rename to bittensor/core/extrinsics/set_weights.py diff --git a/bittensor/utils/deprecated/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py similarity index 100% rename from bittensor/utils/deprecated/extrinsics/transfer.py rename to bittensor/core/extrinsics/transfer.py diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 32b3c1a08..d01d30325 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -48,31 +48,31 @@ custom_rpc_type_registry, ) from bittensor.core.config import Config -from bittensor.core.metagraph import Metagraph -from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams -from bittensor.utils import torch, format_error_message -from bittensor.utils import u16_normalized_float, networking -from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging -from bittensor.utils.deprecated.extrinsics.commit_weights import ( +from bittensor.core.extrinsics.commit_weights import ( commit_weights_extrinsic, reveal_weights_extrinsic, ) -from bittensor.utils.deprecated.extrinsics.prometheus import ( +from bittensor.core.extrinsics.prometheus import ( prometheus_extrinsic, ) -from bittensor.utils.deprecated.extrinsics.serving import ( +from bittensor.core.extrinsics.serving import ( serve_extrinsic, serve_axon_extrinsic, publish_metadata, get_metadata, ) -from bittensor.utils.deprecated.extrinsics.set_weights import ( +from bittensor.core.extrinsics.set_weights import ( set_weights_extrinsic, ) -from bittensor.utils.deprecated.extrinsics.transfer import ( +from bittensor.core.extrinsics.transfer import ( transfer_extrinsic, ) +from bittensor.core.metagraph import Metagraph +from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams +from bittensor.utils import torch, format_error_message +from bittensor.utils import u16_normalized_float, networking +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import generate_weight_hash KEY_NONCE: Dict[str, int] = {} diff --git a/bittensor/utils/deprecated/__init__.py b/bittensor/utils/deprecated.py similarity index 96% rename from bittensor/utils/deprecated/__init__.py rename to bittensor/utils/deprecated.py index a6565686d..985801dd6 100644 --- a/bittensor/utils/deprecated/__init__.py +++ b/bittensor/utils/deprecated.py @@ -145,6 +145,6 @@ mock_subpackage = importlib.import_module("bittensor.utils.mock") sys.modules["bittensor.mock"] = mock_subpackage -# Makes the `bittensor.utils.deprecated.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. -extrinsics_subpackage = importlib.import_module("bittensor.utils.deprecated.extrinsics") +# Makes the `bittensor.core.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. +extrinsics_subpackage = importlib.import_module("bittensor.core.extrinsics") sys.modules["bittensor.extrinsics"] = extrinsics_subpackage diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index beb4ec42e..e7b18bfcb 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -20,7 +20,7 @@ import pytest from bittensor_wallet import Wallet -from bittensor.utils.deprecated.extrinsics.prometheus import ( +from bittensor.core.extrinsics.prometheus import ( prometheus_extrinsic, ) from bittensor.core.subtensor import Subtensor diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index beb401602..73b3f8fcf 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -21,7 +21,7 @@ from bittensor_wallet import Wallet from bittensor.core.axon import Axon -from bittensor.utils.deprecated.extrinsics.serving import ( +from bittensor.core.extrinsics.serving import ( serve_extrinsic, publish_metadata, serve_axon_extrinsic, @@ -120,7 +120,7 @@ def test_serve_extrinsic_happy_path( # Arrange mock_subtensor.do_serve_axon.return_value = (True, "") with patch( - "bittensor.utils.deprecated.extrinsics.serving.Confirm.ask", + "bittensor.core.extrinsics.serving.Confirm.ask", return_value=True, ): # Act @@ -180,7 +180,7 @@ def test_serve_extrinsic_edge_cases( # Arrange mock_subtensor.do_serve_axon.return_value = (True, "") with patch( - "bittensor.utils.deprecated.extrinsics.serving.Confirm.ask", + "bittensor.core.extrinsics.serving.Confirm.ask", return_value=True, ): # Act @@ -240,7 +240,7 @@ def test_serve_extrinsic_error_cases( # Arrange mock_subtensor.do_serve_axon.return_value = (False, "Error serving axon") with patch( - "bittensor.utils.deprecated.extrinsics.serving.Confirm.ask", + "bittensor.core.extrinsics.serving.Confirm.ask", return_value=True, ): # Act diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 1c86f5a11..1d76dae3f 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -4,7 +4,7 @@ from bittensor.core.subtensor import Subtensor from bittensor_wallet import Wallet -from bittensor.utils.deprecated.extrinsics.set_weights import ( +from bittensor.core.extrinsics.set_weights import ( set_weights_extrinsic, ) diff --git a/tests/unit_tests/test_deprecated.py b/tests/unit_tests/test_deprecated.py index 239241407..c4b906a0c 100644 --- a/tests/unit_tests/test_deprecated.py +++ b/tests/unit_tests/test_deprecated.py @@ -32,7 +32,7 @@ def test_mock_import(): def test_extrinsics_import(): """Tests that `bittensor.extrinsics` can be imported and is the same as `bittensor.utils.deprecated.extrinsics`.""" import bittensor.extrinsics as redirected_extrinsics - import bittensor.utils.deprecated.extrinsics as real_extrinsics + import bittensor.core.extrinsics as real_extrinsics assert "bittensor.extrinsics" in sys.modules assert redirected_extrinsics is real_extrinsics From c7c68381cccaf13397bb1f43c07a02f45960c954 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 30 Aug 2024 08:46:52 -0700 Subject: [PATCH 154/260] update ssh to https in requirements --- requirements/btcli.txt | 2 +- requirements/prod.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/btcli.txt b/requirements/btcli.txt index f38e489a8..429461fac 100644 --- a/requirements/btcli.txt +++ b/requirements/btcli.txt @@ -1 +1 @@ -git+ssh://git@github.com/opentensor/btcli.git@main#egg=bittensor-cli \ No newline at end of file +git+https://github.com/opentensor/btcli.git@main#egg=bittensor-cli \ No newline at end of file diff --git a/requirements/prod.txt b/requirements/prod.txt index 62a0ac178..bcfb5bcf6 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -19,4 +19,4 @@ python-Levenshtein scalecodec==1.2.11 substrate-interface~=1.7.9 uvicorn -git+ssh://git@github.com/opentensor/btwallet.git@main#egg=bittensor-wallet \ No newline at end of file +git+https://github.com/opentensor/btwallet.git#egg=bittensor-wallet \ No newline at end of file From 6a7b2cab755965ad4f1ab39b33d0edc2939c0641 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 3 Sep 2024 12:19:25 -0700 Subject: [PATCH 155/260] Add reconnection logic + tests --- bittensor/extrinsics/serving.py | 2 + bittensor/subtensor.py | 98 ++++++++++++++++++++++-------- bittensor/utils/networking.py | 33 ++++++++-- tests/unit_tests/test_subtensor.py | 36 +++++++++++ 4 files changed, 141 insertions(+), 28 deletions(-) diff --git a/bittensor/extrinsics/serving.py b/bittensor/extrinsics/serving.py index bba5367de..734561835 100644 --- a/bittensor/extrinsics/serving.py +++ b/bittensor/extrinsics/serving.py @@ -25,6 +25,7 @@ import bittensor import bittensor.utils.networking as net from bittensor.utils import format_error_message +from bittensor.utils.networking import ensure_connected from ..errors import MetadataError @@ -269,6 +270,7 @@ def publish_metadata( raise MetadataError(format_error_message(response.error_message)) +@ensure_connected def get_metadata(self, netuid: int, hotkey: str, block: Optional[int] = None) -> str: @retry(delay=2, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index ac22a3a14..c4e70deae 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -26,6 +26,7 @@ import argparse import copy import socket +import sys import time from typing import List, Dict, Union, Optional, Tuple, TypedDict, Any @@ -188,6 +189,7 @@ def __init__( config: Optional[bittensor.config] = None, _mock: bool = False, log_verbose: bool = True, + connection_timeout: int = 600, ) -> None: """ Initializes a Subtensor interface for interacting with the Bittensor blockchain. @@ -251,7 +253,25 @@ def __init__( "To get ahead of this change, please run a local subtensor node and point to it." ) - # Attempt to connect to chosen endpoint. Fallback to finney if local unavailable. + self.log_verbose = log_verbose + self._connection_timeout = connection_timeout + self._get_substrate() + + self._subtensor_errors: Dict[str, Dict[str, str]] = {} + + def __str__(self) -> str: + if self.network == self.chain_endpoint: + # Connecting to chain endpoint without network known. + return "subtensor({})".format(self.chain_endpoint) + else: + # Connecting to network with endpoint known. + return "subtensor({}, {})".format(self.network, self.chain_endpoint) + + def __repr__(self) -> str: + return self.__str__() + + def _get_substrate(self): + """Establishes a connection to the Substrate node using configured parameters.""" try: # Set up params. self.substrate = SubstrateInterface( @@ -260,6 +280,11 @@ def __init__( url=self.chain_endpoint, type_registry=bittensor.__type_registry__, ) + if self.log_verbose: + _logger.info( + f"Connected to {self.network} network and {self.chain_endpoint}." + ) + except ConnectionRefusedError: _logger.error( f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint. Exiting...", @@ -268,13 +293,10 @@ def __init__( "You can check if you have connectivity by running this command: nc -vz localhost " f"{self.chain_endpoint.split(':')[2]}" ) - exit(1) - # TODO (edu/phil): Advise to run local subtensor and point to dev docs. + sys.exit(1) try: - self.substrate.websocket.settimeout(600) - # except: - # bittensor.logging.warning("Could not set websocket timeout.") + self.substrate.websocket.settimeout(self._connection_timeout) except AttributeError as e: _logger.warning(f"AttributeError: {e}") except TypeError as e: @@ -282,24 +304,6 @@ def __init__( except (socket.error, OSError) as e: _logger.warning(f"Socket error: {e}") - if log_verbose: - _logger.info( - f"Connected to {self.network} network and {self.chain_endpoint}." - ) - - self._subtensor_errors: Dict[str, Dict[str, str]] = {} - - def __str__(self) -> str: - if self.network == self.chain_endpoint: - # Connecting to chain endpoint without network known. - return "subtensor({})".format(self.chain_endpoint) - else: - # Connecting to network with endpoint known. - return "subtensor({}, {})".format(self.network, self.chain_endpoint) - - def __repr__(self) -> str: - return self.__str__() - @staticmethod def config() -> "bittensor.config": """ @@ -670,6 +674,7 @@ def set_take( wait_for_finalization=wait_for_finalization, ) + @networking.ensure_connected def send_extrinsic( self, wallet: "bittensor.wallet", @@ -839,6 +844,7 @@ def set_weights( return success, message + @networking.ensure_connected def _do_set_weights( self, wallet: "bittensor.wallet", @@ -986,6 +992,7 @@ def commit_weights( return success, message + @networking.ensure_connected def _do_commit_weights( self, wallet: "bittensor.wallet", @@ -1110,6 +1117,7 @@ def reveal_weights( return success, message + @networking.ensure_connected def _do_reveal_weights( self, wallet: "bittensor.wallet", @@ -1372,6 +1380,7 @@ def burned_register( prompt=prompt, ) + @networking.ensure_connected def _do_pow_register( self, netuid: int, @@ -1434,6 +1443,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_burned_register( self, netuid: int, @@ -1491,6 +1501,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_swap_hotkey( self, wallet: "bittensor.wallet", @@ -1588,6 +1599,7 @@ def transfer( prompt=prompt, ) + @networking.ensure_connected def get_transfer_fee( self, wallet: "bittensor.wallet", dest: str, value: Union["Balance", float, int] ) -> "Balance": @@ -1645,6 +1657,7 @@ def get_transfer_fee( ) return fee + @networking.ensure_connected def _do_transfer( self, wallet: "bittensor.wallet", @@ -1880,6 +1893,7 @@ def serve_axon( self, netuid, axon, wait_for_inclusion, wait_for_finalization ) + @networking.ensure_connected def _do_serve_axon( self, wallet: "bittensor.wallet", @@ -1947,6 +1961,7 @@ def serve_prometheus( wait_for_finalization=wait_for_finalization, ) + @networking.ensure_connected def _do_serve_prometheus( self, wallet: "bittensor.wallet", @@ -1992,6 +2007,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_associate_ips( self, wallet: "bittensor.wallet", @@ -2122,6 +2138,7 @@ def add_stake_multiple( prompt, ) + @networking.ensure_connected def _do_stake( self, wallet: "bittensor.wallet", @@ -2249,6 +2266,7 @@ def unstake( prompt, ) + @networking.ensure_connected def _do_unstake( self, wallet: "bittensor.wallet", @@ -2339,6 +2357,7 @@ def set_childkey_take( prompt=prompt, ) + @networking.ensure_connected def _do_set_childkey_take( self, wallet: "bittensor.wallet", @@ -2430,6 +2449,7 @@ def set_children( prompt=prompt, ) + @networking.ensure_connected def _do_set_children( self, wallet: "bittensor.wallet", @@ -2806,6 +2826,7 @@ def root_register( prompt=prompt, ) + @networking.ensure_connected def _do_root_register( self, wallet: "bittensor.wallet", @@ -2886,6 +2907,7 @@ def root_set_weights( prompt=prompt, ) + @networking.ensure_connected def _do_set_root_weights( self, wallet: "bittensor.wallet", @@ -2958,6 +2980,7 @@ def make_substrate_call_with_retry(): ################## # Queries subtensor registry named storage with params and block. + @networking.ensure_connected def query_identity( self, key: str, @@ -3000,6 +3023,7 @@ def make_substrate_call_with_retry() -> "ScaleType": identity_info.value["info"] ) + @networking.ensure_connected def update_identity( self, wallet: "bittensor.wallet", @@ -3103,6 +3127,7 @@ def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> ################## # Queries subtensor named storage with params and block. + @networking.ensure_connected def query_subtensor( self, name: str, @@ -3139,6 +3164,7 @@ def make_substrate_call_with_retry() -> "ScaleType": return make_substrate_call_with_retry() # Queries subtensor map storage with params and block. + @networking.ensure_connected def query_map_subtensor( self, name: str, @@ -3175,6 +3201,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def query_constant( self, module_name: str, constant_name: str, block: Optional[int] = None ) -> Optional["ScaleType"]: @@ -3209,6 +3236,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() # Queries any module storage with params and block. + @networking.ensure_connected def query_module( self, module: str, @@ -3248,6 +3276,7 @@ def make_substrate_call_with_retry() -> "ScaleType": return make_substrate_call_with_retry() # Queries any module map storage with params and block. + @networking.ensure_connected def query_map( self, module: str, @@ -3286,6 +3315,7 @@ def make_substrate_call_with_retry() -> "QueryMapResult": return make_substrate_call_with_retry() + @networking.ensure_connected def state_call( self, method: str, @@ -3374,6 +3404,7 @@ def query_runtime_api( return obj.decode() + @networking.ensure_connected def _encode_params( self, call_definition: List["ParamWithTypes"], @@ -4351,6 +4382,7 @@ def get_subnets(self, block: Optional[int] = None) -> List[int]: else [] ) + @networking.ensure_connected def get_all_subnets_info(self, block: Optional[int] = None) -> List[SubnetInfo]: """ Retrieves detailed information about all subnets within the Bittensor network. This function @@ -4382,6 +4414,7 @@ def make_substrate_call_with_retry(): return SubnetInfo.list_from_vec_u8(result) + @networking.ensure_connected def get_subnet_info( self, netuid: int, block: Optional[int] = None ) -> Optional[SubnetInfo]: @@ -4540,6 +4573,7 @@ def get_nominators_for_hotkey( else 0 ) + @networking.ensure_connected def get_delegate_by_hotkey( self, hotkey_ss58: str, block: Optional[int] = None ) -> Optional[DelegateInfo]: @@ -4577,6 +4611,7 @@ def make_substrate_call_with_retry(encoded_hotkey_: List[int]): return DelegateInfo.from_vec_u8(result) + @networking.ensure_connected def get_delegates_lite(self, block: Optional[int] = None) -> List[DelegateInfoLite]: """ Retrieves a lighter list of all delegate neurons within the Bittensor network. This function provides an @@ -4611,6 +4646,7 @@ def make_substrate_call_with_retry(): return [DelegateInfoLite(**d) for d in result] + @networking.ensure_connected def get_delegates(self, block: Optional[int] = None) -> List[DelegateInfo]: """ Retrieves a list of all delegate neurons within the Bittensor network. This function provides an overview of the @@ -4643,6 +4679,7 @@ def make_substrate_call_with_retry(): return DelegateInfo.list_from_vec_u8(result) + @networking.ensure_connected def get_delegated( self, coldkey_ss58: str, block: Optional[int] = None ) -> List[Tuple[DelegateInfo, Balance]]: @@ -4715,6 +4752,7 @@ def get_childkey_take( return None return None + @networking.ensure_connected def get_children(self, hotkey, netuid) -> list[tuple[int, str]] | list[Any] | None: """ Get the children of a hotkey on a specific network. @@ -4741,6 +4779,7 @@ def get_children(self, hotkey, netuid) -> list[tuple[int, str]] | list[Any] | No print(f"Unexpected error in get_children: {e}") return None + @networking.ensure_connected def get_parents(self, child_hotkey, netuid): """ Get the parents of a child hotkey on a specific network. @@ -4852,6 +4891,7 @@ def get_stake_info_for_coldkeys( return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore + @networking.ensure_connected def get_minimum_required_stake( self, ) -> Balance: @@ -5104,6 +5144,7 @@ def neuron_for_wallet( wallet.hotkey.ss58_address, netuid=netuid, block=block ) + @networking.ensure_connected def neuron_for_uid( self, uid: Optional[int], netuid: int, block: Optional[int] = None ) -> NeuronInfo: @@ -5396,6 +5437,7 @@ def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: # Extrinsics # ############## + @networking.ensure_connected def _do_delegation( self, wallet: "bittensor.wallet", @@ -5447,6 +5489,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_undelegation( self, wallet: "bittensor.wallet", @@ -5501,6 +5544,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_nominate( self, wallet: "bittensor.wallet", @@ -5548,6 +5592,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_increase_take( self, wallet: "bittensor.wallet", @@ -5603,6 +5648,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def _do_decrease_take( self, wallet: "bittensor.wallet", @@ -5662,6 +5708,7 @@ def make_substrate_call_with_retry(): # Legacy # ########## + @networking.ensure_connected def get_balance(self, address: str, block: Optional[int] = None) -> Balance: """ Retrieves the token balance of a specific address within the Bittensor network. This function queries @@ -5698,6 +5745,7 @@ def make_substrate_call_with_retry(): return Balance(1000) return Balance(result.value["data"]["free"]) + @networking.ensure_connected def get_current_block(self) -> int: """ Returns the current block number on the Bittensor blockchain. This function provides the latest block @@ -5716,6 +5764,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() + @networking.ensure_connected def get_balances(self, block: Optional[int] = None) -> Dict[str, Balance]: """ Retrieves the token balances of all accounts within the Bittensor network as of a specific blockchain block. @@ -5775,6 +5824,7 @@ def _null_neuron() -> NeuronInfo: ) # type: ignore return neuron + @networking.ensure_connected def get_block_hash(self, block_id: int) -> str: """ Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index 4d1af585c..f4b729fe9 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -19,15 +19,17 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -# Standard Lib +import json import os +import socket import urllib -import json -import netaddr +from functools import wraps -# 3rd party +import netaddr import requests +from bittensor.btlogging import logging + def int_to_ip(int_val: int) -> str: r"""Maps an integer to a unique ip-string @@ -171,3 +173,26 @@ def get_formatted_ws_endpoint_url(endpoint_url: str) -> str: endpoint_url = "ws://{}".format(endpoint_url) return endpoint_url + + +def ensure_connected(func): + """Decorator ensuring the function executes with an active substrate connection.""" + + @wraps(func) + def wrapper(self, *args, **kwargs): + # Check the socket state before method execution + if ( + # connection was closed correctly + self.substrate.websocket.sock is None + # connection has a broken pipe + or self.substrate.websocket.sock.getsockopt( + socket.SOL_SOCKET, socket.SO_ERROR + ) + != 0 + ): + logging.info("Reconnection substrate...") + self._get_substrate() + # Execute the method if the connection is active or after reconnecting + return func(self, *args, **kwargs) + + return wrapper diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index c651eaa57..b8dfc3e81 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -2315,3 +2315,39 @@ def test_get_remaining_arbitration_period_happy(subtensor, mocker): ) # if we change the methods logic in the future we have to be make sure the returned type is correct assert result == 1800 # 2000 - 200 + + +def test_connect_without_substrate(mocker): + """Ensure re-connection is called when using an alive substrate.""" + # Prep + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 1 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + fake_subtensor = Subtensor() + spy_get_substrate = mocker.spy(Subtensor, "_get_substrate") + + # Call + _ = fake_subtensor.block + + # Assertions + assert spy_get_substrate.call_count == 1 + + +def test_connect_with_substrate(mocker): + """Ensure re-connection is non called when using an alive substrate.""" + # Prep + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + fake_subtensor = Subtensor() + spy_get_substrate = mocker.spy(Subtensor, "_get_substrate") + + # Call + _ = fake_subtensor.block + + # Assertions + assert spy_get_substrate.call_count == 0 From d9715540edcda44ddb8325384c0e89ebae2374d7 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 3 Sep 2024 12:50:34 -0700 Subject: [PATCH 156/260] add reconnection logic for correctly closed connection --- bittensor/core/extrinsics/serving.py | 1 + bittensor/core/subtensor.py | 59 +++++++++++----------------- bittensor/utils/networking.py | 27 +++++++++++++ 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index 7253f25d7..aaa882e00 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -247,6 +247,7 @@ def publish_metadata( # Community uses this function directly +@net.ensure_connected def get_metadata(self, netuid: int, hotkey: str, block: Optional[int] = None) -> str: @retry(delay=2, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index d01d30325..abe3b3b14 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -83,24 +83,6 @@ class ParamWithTypes(TypedDict): type: str # ScaleType string of the parameter. -def _ensure_connected(func): - """Decorator ensuring the function executes with an active substrate connection.""" - - @wraps(func) - def wrapper(self, *args, **kwargs): - # Check the socket state before method execution - if ( - self.substrate.websocket.sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) - != 0 - ): - logging.info("Reconnection substrate...") - self._get_substrate() - # Execute the method if the connection is active or after reconnecting - return func(self, *args, **kwargs) - - return wrapper - - class Subtensor: """ The Subtensor class in Bittensor serves as a crucial interface for interacting with the Bittensor blockchain, @@ -160,6 +142,7 @@ def __init__( config: Optional["Config"] = None, _mock: bool = False, log_verbose: bool = True, + connection_timeout: int = 600, ) -> None: """ Initializes a Subtensor interface for interacting with the Bittensor blockchain. @@ -173,6 +156,7 @@ def __init__( network (str, optional): The network name to connect to (e.g., ``finney``, ``local``). This can also be the chain endpoint (e.g., ``wss://entrypoint-finney.opentensor.ai:443``) and will be correctly parsed into the network and chain endpoint. If not specified, defaults to the main Bittensor network. config (bittensor.core.config.Config, optional): Configuration object for the subtensor. If not provided, a default configuration is used. _mock (bool, optional): If set to ``True``, uses a mocked connection for testing purposes. + connection_timeout (int): The maximum time in seconds to keep the connection alive. This initialization sets up the connection to the specified Bittensor network, allowing for various blockchain operations such as neuron registration, stake management, and setting weights. """ @@ -207,6 +191,7 @@ def __init__( ) self.log_verbose = log_verbose + self._connection_timeout = connection_timeout self._get_substrate() def __str__(self) -> str: @@ -250,7 +235,7 @@ def _get_substrate(self): sys.exit(1) try: - self.substrate.websocket.settimeout(600) + self.substrate.websocket.settimeout(self._connection_timeout) except AttributeError as e: logging.warning(f"AttributeError: {e}") except TypeError as e: @@ -403,7 +388,7 @@ def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = Non pass # Inner private functions - @_ensure_connected + @networking.ensure_connected def _encode_params( self, call_definition: List["ParamWithTypes"], @@ -448,7 +433,7 @@ def _get_hyperparameter( return result.value # Calls methods - @_ensure_connected + @networking.ensure_connected def query_subtensor( self, name: str, block: Optional[int] = None, params: Optional[list] = None ) -> "ScaleType": @@ -479,7 +464,7 @@ def make_substrate_call_with_retry() -> "ScaleType": return make_substrate_call_with_retry() - @_ensure_connected + @networking.ensure_connected def query_map_subtensor( self, name: str, block: Optional[int] = None, params: Optional[list] = None ) -> "QueryMapResult": @@ -562,7 +547,7 @@ def query_runtime_api( return obj.decode() - @_ensure_connected + @networking.ensure_connected def state_call( self, method: str, data: str, block: Optional[int] = None ) -> Dict[Any, Any]: @@ -590,7 +575,7 @@ def make_substrate_call_with_retry() -> Dict[Any, Any]: return make_substrate_call_with_retry() - @_ensure_connected + @networking.ensure_connected def query_map( self, module: str, @@ -626,7 +611,7 @@ def make_substrate_call_with_retry() -> "QueryMapResult": return make_substrate_call_with_retry() - @_ensure_connected + @networking.ensure_connected def query_constant( self, module_name: str, constant_name: str, block: Optional[int] = None ) -> Optional["ScaleType"]: @@ -656,7 +641,7 @@ def make_substrate_call_with_retry(): return make_substrate_call_with_retry() - @_ensure_connected + @networking.ensure_connected def query_module( self, module: str, @@ -780,7 +765,7 @@ def get_netuids_for_hotkey( else [] ) - @_ensure_connected + @networking.ensure_connected def get_current_block(self) -> int: """ Returns the current block number on the Bittensor blockchain. This function provides the latest block number, indicating the most recent state of the blockchain. @@ -856,7 +841,7 @@ def is_hotkey_registered( else: return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) - @_ensure_connected + @networking.ensure_connected def do_set_weights( self, wallet: "Wallet", @@ -1031,7 +1016,7 @@ def blocks_since_last_update(self, netuid: int, uid: int) -> Optional[int]: call = self._get_hyperparameter(param_name="LastUpdate", netuid=netuid) return None if call is None else self.get_current_block() - int(call[uid]) - @_ensure_connected + @networking.ensure_connected def get_block_hash(self, block_id: int) -> str: """ Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique identifier representing the cryptographic hash of the block's content, ensuring its integrity and immutability. @@ -1089,7 +1074,7 @@ def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int ) return None if call is None else int(call) - @_ensure_connected + @networking.ensure_connected def do_transfer( self, wallet: "Wallet", @@ -1201,7 +1186,7 @@ def get_neuron_for_pubkey_and_subnet( block=block, ) - @_ensure_connected + @networking.ensure_connected def neuron_for_uid( self, uid: Optional[int], netuid: int, block: Optional[int] = None ) -> NeuronInfo: @@ -1240,7 +1225,7 @@ def make_substrate_call_with_retry(): return NeuronInfo.from_vec_u8(result) # Community uses this method via `bittensor.api.extrinsics.prometheus.prometheus_extrinsic` - @_ensure_connected + @networking.ensure_connected def do_serve_prometheus( self, wallet: "Wallet", @@ -1323,7 +1308,7 @@ def serve_prometheus( ) # Community uses this method as part of `subtensor.serve_axon` - @_ensure_connected + @networking.ensure_connected def do_serve_axon( self, wallet: "Wallet", @@ -1765,7 +1750,7 @@ def weights( return w_map # Used by community via `transfer_extrinsic` - @_ensure_connected + @networking.ensure_connected def get_balance(self, address: str, block: Optional[int] = None) -> Balance: """ Retrieves the token balance of a specific address within the Bittensor network. This function queries the blockchain to determine the amount of Tao held by a given account. @@ -1802,7 +1787,7 @@ def make_substrate_call_with_retry(): return Balance(result.value["data"]["free"]) # Used in community via `bittensor.core.subtensor.Subtensor.transfer` - @_ensure_connected + @networking.ensure_connected def get_transfer_fee( self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] ) -> "Balance": @@ -1953,7 +1938,7 @@ def commit_weights( return success, message # Community uses this method - @_ensure_connected + @networking.ensure_connected def _do_commit_weights( self, wallet: "Wallet", @@ -2077,7 +2062,7 @@ def reveal_weights( return success, message # Community uses this method - @_ensure_connected + @networking.ensure_connected def _do_reveal_weights( self, wallet: "Wallet", diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index 6d35a365c..834456f33 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -19,11 +19,15 @@ import json import os +import socket import urllib +from functools import wraps import netaddr import requests +from bittensor.utils.btlogging import logging + def int_to_ip(int_val: int) -> str: """Maps an integer to a unique ip-string @@ -167,3 +171,26 @@ def get_formatted_ws_endpoint_url(endpoint_url: str) -> str: endpoint_url = f"ws://{endpoint_url}" return endpoint_url + + +def ensure_connected(func): + """Decorator ensuring the function executes with an active substrate connection.""" + + @wraps(func) + def wrapper(self, *args, **kwargs): + # Check the socket state before method execution + if ( + # connection was closed correctly + self.substrate.websocket.sock is None + # connection has a broken pipe + or self.substrate.websocket.sock.getsockopt( + socket.SOL_SOCKET, socket.SO_ERROR + ) + != 0 + ): + logging.info("Reconnection substrate...") + self._get_substrate() + # Execute the method if the connection is active or after reconnecting + return func(self, *args, **kwargs) + + return wrapper From 6281bcf4c51fda9f631d8b4a7fdb9003e54bc107 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 3 Sep 2024 13:04:46 -0700 Subject: [PATCH 157/260] remove unused import --- bittensor/core/subtensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index abe3b3b14..c2503c408 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -24,7 +24,6 @@ import copy import socket import sys -from functools import wraps from typing import List, Dict, Union, Optional, Tuple, TypedDict, Any import numpy as np From 2f568ab3e4f2678d36555c7a7d8c03b4d1d39d95 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 3 Sep 2024 15:10:01 -0700 Subject: [PATCH 158/260] fix pagination --- bittensor/core/subtensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index c2503c408..7b6940a16 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -759,7 +759,7 @@ def get_netuids_for_hotkey( """ result = self.query_map_subtensor("IsNetworkMember", block, [hotkey_ss58]) return ( - [record[0].value for record in result.records if record[1]] + [record[0].value for record in result if record[1]] if result and hasattr(result, "records") else [] ) @@ -1684,7 +1684,7 @@ def get_subnets(self, block: Optional[int] = None) -> List[int]: """ result = self.query_map_subtensor("NetworksAdded", block) return ( - [network[0].value for network in result.records] + [network[0].value for network in result.records if network[1]] if result and hasattr(result, "records") else [] ) From 34bcfd11072eae7e85ec9ff9a1e6e95d1e18a924 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 4 Sep 2024 12:11:53 -0700 Subject: [PATCH 159/260] Move do_* methods into the related extrinsic module (commit_weights.py). --- bittensor/core/extrinsics/commit_weights.py | 175 ++++++++++++++++-- bittensor/core/subtensor.py | 127 ------------- .../extrinsics/test_commit_weights.py | 125 +++++++++++++ tests/unit_tests/test_subtensor.py | 107 ----------- 4 files changed, 285 insertions(+), 249 deletions(-) create mode 100644 tests/unit_tests/extrinsics/test_commit_weights.py diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index cd56988c7..f2b2a2d36 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -17,17 +17,84 @@ """Module commit weights and reveal weights extrinsic.""" -from typing import List, Tuple +from typing import List, Tuple, Optional, TYPE_CHECKING +from retry import retry from rich.prompt import Confirm -import bittensor from bittensor.utils import format_error_message +from bittensor.utils.btlogging import logging +from bittensor.utils.networking import ensure_connected + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +# Community uses this method +@ensure_connected +def do_commit_weights( + self: "Subtensor", + wallet: "Wallet", + netuid: int, + commit_hash: str, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> Tuple[bool, Optional[dict]]: + """ + 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: + self (bittensor.subtensor): The subtensor instance used for blockchain interaction. + wallet (bittensor.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, 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. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + 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. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + + return make_substrate_call_with_retry() def commit_weights_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", netuid: int, commit_hash: str, wait_for_inclusion: bool = False, @@ -37,6 +104,7 @@ def commit_weights_extrinsic( """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. This function is a wrapper around the `_do_commit_weights` method, handling user prompts and error messages. + Args: subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. @@ -45,6 +113,7 @@ def commit_weights_extrinsic( 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. prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + Returns: Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string value describing the success or potential error. @@ -54,7 +123,8 @@ def commit_weights_extrinsic( if prompt and not Confirm.ask(f"Would you like to commit weights?"): return False, "User cancelled the operation." - success, error_message = subtensor._do_commit_weights( + success, error_message = do_commit_weights( + subtensor=subtensor, wallet=wallet, netuid=netuid, commit_hash=commit_hash, @@ -63,16 +133,88 @@ def commit_weights_extrinsic( ) if success: - bittensor.logging.info("Successfully committed weights.") - return True, "Successfully committed weights." + success_message = "Successfully committed weights." + logging.info(success_message) + return True, success_message else: - bittensor.logging.error(f"Failed to commit weights: {error_message}") - return False, format_error_message(error_message) + error_message = format_error_message(error_message) + logging.error(f"Failed to commit weights: {error_message}") + return False, error_message + + +# Community uses this method +@ensure_connected +def do_reveal_weights( + self: "Subtensor", + wallet: "Wallet", + netuid: int, + uids: List[int], + values: List[int], + salt: List[int], + version_key: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> Tuple[bool, Optional[dict]]: + """ + 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. + + Args: + self (bittensor.subtensor): The subtensor instance used for blockchain interaction. + wallet (bittensor.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. + values (List[int]): List of weight values corresponding to each UID. + salt (List[int]): List of salt values corresponding to the hash function. + version_key (int): 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. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency + and accountability for the neuron's weight distribution. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": values, + "salt": salt, + "version_key": version_key, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + + return make_substrate_call_with_retry() def reveal_weights_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", + subtensor: "Subtensor", + wallet: "Wallet", netuid: int, uids: List[int], weights: List[int], @@ -106,7 +248,8 @@ def reveal_weights_extrinsic( if prompt and not Confirm.ask(f"Would you like to reveal weights?"): return False, "User cancelled the operation." - success, error_message = subtensor._do_reveal_weights( + success, error_message = do_reveal_weights( + subtensor=subtensor, wallet=wallet, netuid=netuid, uids=uids, @@ -118,8 +261,10 @@ def reveal_weights_extrinsic( ) if success: - bittensor.logging.info("Successfully revealed weights.") - return True, "Successfully revealed weights." + success_message = "Successfully revealed weights." + logging.info(success_message) + return True, success_message else: - bittensor.logging.error(f"Failed to reveal weights: {error_message}") + error_message = format_error_message(error_message) + logging.error(f"Failed to reveal weights: {error_message}") return False, error_message diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 7b6940a16..836fa70f6 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1936,65 +1936,6 @@ def commit_weights( return success, message - # Community uses this method - @networking.ensure_connected - def _do_commit_weights( - self, - wallet: "Wallet", - netuid: int, - commit_hash: str, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[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: - wallet (bittensor.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, 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. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - 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. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="commit_weights", - call_params={ - "netuid": netuid, - "commit_hash": commit_hash, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - response.process_events() - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - - return make_substrate_call_with_retry() - # Community uses this method def reveal_weights( self, @@ -2059,71 +2000,3 @@ def reveal_weights( retries += 1 return success, message - - # Community uses this method - @networking.ensure_connected - def _do_reveal_weights( - self, - wallet: "Wallet", - netuid: int, - uids: List[int], - values: List[int], - salt: List[int], - version_key: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[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. - - Args: - wallet (bittensor.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. - values (List[int]): List of weight values corresponding to each UID. - salt (List[int]): List of salt values corresponding to the hash function. - version_key (int): 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. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency - and accountability for the neuron's weight distribution. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="reveal_weights", - call_params={ - "netuid": netuid, - "uids": uids, - "values": values, - "salt": salt, - "version_key": version_key, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - response.process_events() - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - - return make_substrate_call_with_retry() diff --git a/tests/unit_tests/extrinsics/test_commit_weights.py b/tests/unit_tests/extrinsics/test_commit_weights.py new file mode 100644 index 000000000..6f0b30bb1 --- /dev/null +++ b/tests/unit_tests/extrinsics/test_commit_weights.py @@ -0,0 +1,125 @@ +import pytest + +from bittensor.core import subtensor as subtensor_module +from bittensor.core.settings import version_as_int +from bittensor.core.subtensor import Subtensor +from bittensor.core.extrinsics.commit_weights import do_commit_weights, do_reveal_weights + + +@pytest.fixture +def subtensor(mocker): + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + return Subtensor() + + +def test_do_commit_weights(subtensor, mocker): + """Successful _do_commit_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + netuid = 1 + commit_hash = "fake_commit_hash" + wait_for_inclusion = True + wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = None + + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = do_commit_weights( + self=subtensor, + wallet=fake_wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Assertions + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, + ) + + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey + ) + + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + + assert result == (False, subtensor.substrate.submit_extrinsic.return_value.error_message) + + +def test_do_reveal_weights(subtensor, mocker): + """Verifies that the `_do_reveal_weights` method interacts with the right substrate methods.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_wallet.hotkey = "hotkey" + + netuid = 1 + uids = [1, 2, 3, 4] + values = [1, 2, 3, 4] + salt = [4, 2, 2, 1] + wait_for_inclusion = True + wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = None + + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = do_reveal_weights( + self=subtensor, + wallet=fake_wallet, + netuid=netuid, + uids=uids, + values=values, + salt=salt, + version_key=version_as_int, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": values, + "salt": salt, + "version_key": version_as_int, + }, + ) + + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey + ) + + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + + assert result == (False, subtensor.substrate.submit_extrinsic.return_value.error_message) + diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 15129a03c..b537569f7 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -43,7 +43,6 @@ def test_serve_axon_with_external_ip_set(): mock_subtensor = MagicMock(spec=Subtensor, serve_axon=mock_serve_axon) - mock_add_insecure_port = mock.MagicMock(return_value=None) mock_wallet = MagicMock( spec=Wallet, coldkey=MagicMock(), @@ -2272,54 +2271,6 @@ def test_commit_weights(subtensor, mocker): assert result == expected_result -def test_do_commit_weights(subtensor, mocker): - """Successful _do_commit_weights call.""" - # Preps - fake_wallet = mocker.MagicMock() - netuid = 1 - commit_hash = "fake_commit_hash" - wait_for_inclusion = True - wait_for_finalization = True - - subtensor.substrate.submit_extrinsic.return_value.is_success = None - - mocked_format_error_message = mocker.MagicMock() - subtensor_module.format_error_message = mocked_format_error_message - - # Call - result = subtensor._do_commit_weights( - wallet=fake_wallet, - netuid=netuid, - commit_hash=commit_hash, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # Assertions - subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="commit_weights", - call_params={ - "netuid": netuid, - "commit_hash": commit_hash, - }, - ) - - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - - assert result == (False, mocked_format_error_message.return_value) - - def test_reveal_weights(subtensor, mocker): """Successful test_reveal_weights call.""" # Preps @@ -2393,64 +2344,6 @@ def test_reveal_weights_false(subtensor, mocker): assert mocked_extrinsic.call_count == 5 -def test_do_reveal_weights(subtensor, mocker): - """Verifies that the `_do_reveal_weights` method interacts with the right substrate methods.""" - # Preps - fake_wallet = mocker.MagicMock() - fake_wallet.hotkey = "hotkey" - - netuid = 1 - uids = [1, 2, 3, 4] - values = [1, 2, 3, 4] - salt = [4, 2, 2, 1] - wait_for_inclusion = True - wait_for_finalization = True - - subtensor.substrate.submit_extrinsic.return_value.is_success = None - - mocked_format_error_message = mocker.MagicMock() - subtensor_module.format_error_message = mocked_format_error_message - - # Call - result = subtensor._do_reveal_weights( - wallet=fake_wallet, - netuid=netuid, - uids=uids, - values=values, - salt=salt, - version_key=version_as_int, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="SubtensorModule", - call_function="reveal_weights", - call_params={ - "netuid": netuid, - "uids": uids, - "values": values, - "salt": salt, - "version_key": version_as_int, - }, - ) - - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - - assert result == (False, mocked_format_error_message.return_value) - - def test_connect_without_substrate(mocker): """Ensure re-connection is called when using an alive substrate.""" # Prep From 7af43c8a17ca2bcb66a99581af716ea445683f07 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 4 Sep 2024 13:54:20 -0700 Subject: [PATCH 160/260] Move do_* methods into the related extrinsic module (bittensor/core/extrinsics/set_weights.py). --- bittensor/core/extrinsics/set_weights.py | 90 ++++++++++++++++--- bittensor/core/subtensor.py | 67 +------------- .../unit_tests/extrinsics/test_set_weights.py | 8 +- tests/unit_tests/test_subtensor.py | 11 +-- 4 files changed, 87 insertions(+), 89 deletions(-) diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index f62a23d44..320d9d65e 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -16,21 +16,89 @@ # DEALINGS IN THE SOFTWARE. import logging -from typing import Union, Tuple, TYPE_CHECKING +from typing import List, Union, Tuple, Optional, TYPE_CHECKING import numpy as np -from bittensor_wallet import Wallet from numpy.typing import NDArray +from retry import retry from rich.prompt import Confirm -from bittensor.core.settings import bt_console -from bittensor.utils import weight_utils +from bittensor.core.settings import bt_console, version_as_int +from bittensor.utils import format_error_message, weight_utils from bittensor.utils.btlogging import logging +from bittensor.utils.networking import ensure_connected from bittensor.utils.registration import torch, use_torch # For annotation purposes if TYPE_CHECKING: from bittensor.core.subtensor import Subtensor + from bittensor_wallet import Wallet + + +@ensure_connected +def do_set_weights( + self, + wallet: "Wallet", + uids: List[int], + vals: List[int], + netuid: int, + version_key: int = version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> Tuple[bool, Optional[dict]]: # (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. + + Args: + self (bittensor.core.subtensor.Subtensor): Subtensor interface + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. + uids (List[int]): List of neuron UIDs for which weights are being set. + vals (List[int]): List of weight values corresponding to each UID. + netuid (int): Unique identifier for the network. + 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. + + Returns: + Tuple[bool, Optional[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. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_weights", + call_params={ + "dests": uids, + "weights": vals, + "netuid": netuid, + "version_key": version_key, + }, + ) + # Period dictates how long the extrinsic will stay as part of waiting pool + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + era={"period": 5}, + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # 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." + + response.process_events() + if response.is_success: + return True, "Successfully set weights." + else: + return False, response.error_message + + return make_substrate_call_with_retry() # Community uses this extrinsic directly and via `subtensor.set_weights` @@ -90,7 +158,8 @@ def set_weights_extrinsic( f":satellite: Setting weights on [white]{subtensor.network}[/white] ..." ): try: - success, error_message = subtensor.do_set_weights( + success, error_message = do_set_weights( + self=subtensor, wallet=wallet, netuid=netuid, uids=weight_uids, @@ -112,16 +181,11 @@ def set_weights_extrinsic( ) return True, "Successfully set weights and Finalized." else: - logging.error( - msg=error_message, - prefix="Set weights", - suffix="Failed: ", - ) + error_message = format_error_message(error_message) + logging.error(error_message) return False, error_message except Exception as e: bt_console.print(f":cross_mark: [red]Failed[/red]: error:{e}") - logging.warning( - msg=str(e), prefix="Set weights", suffix="Failed: " - ) + logging.warning(str(e)) return False, str(e) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 836fa70f6..ecd8e1b6e 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -61,6 +61,7 @@ get_metadata, ) from bittensor.core.extrinsics.set_weights import ( + do_set_weights, set_weights_extrinsic, ) from bittensor.core.extrinsics.transfer import ( @@ -840,72 +841,8 @@ def is_hotkey_registered( else: return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) - @networking.ensure_connected - def do_set_weights( - self, - wallet: "Wallet", - uids: List[int], - vals: List[int], - netuid: int, - version_key: int = settings.version_as_int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[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. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. - uids (List[int]): List of neuron UIDs for which weights are being set. - vals (List[int]): List of weight values corresponding to each UID. - netuid (int): Unique identifier for the network. - 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. - - Returns: - Tuple[bool, Optional[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. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_weights", - call_params={ - "dests": uids, - "weights": vals, - "netuid": netuid, - "version_key": version_key, - }, - ) - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - era={"period": 5}, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # 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." - - response.process_events() - if response.is_success: - return True, "Successfully set weights." - else: - return False, format_error_message(response.error_message) - - return make_substrate_call_with_retry() - # keep backwards compatibility for the community - _do_set_weights = do_set_weights + do_set_weights = do_set_weights # Not used in Bittensor, but is actively used by the community in almost all subnets def set_weights( diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 1d76dae3f..34be586a1 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -47,7 +47,7 @@ def mock_wallet(): True, "Not waiting for finalization or inclusion.", ), - ([1, 2], [0.5, 0.5], 0, True, False, True, True, False, "Mock error message"), + ([1, 2], [0.5, 0.5], 0, True, False, True, True, False, "Subtensor returned `UnknownError (UnknownType)` error. This means: `Unknown Description`"), ([1, 2], [0.5, 0.5], 0, True, True, True, False, False, "Prompt refused."), ], ids=[ @@ -75,9 +75,8 @@ def test_set_weights_extrinsic( with patch( "bittensor.utils.weight_utils.convert_weights_and_uids_for_emit", return_value=(uids_tensor, weights_tensor), - ), patch("rich.prompt.Confirm.ask", return_value=user_accepts), patch.object( - mock_subtensor, - "do_set_weights", + ), patch("rich.prompt.Confirm.ask", return_value=user_accepts), patch( + "bittensor.core.extrinsics.set_weights.do_set_weights", return_value=(expected_success, "Mock error message"), ) as mock_do_set_weights: result, message = set_weights_extrinsic( @@ -96,6 +95,7 @@ def test_set_weights_extrinsic( assert message == expected_message, f"Test {expected_message} failed." if user_accepts is not False: mock_do_set_weights.assert_called_once_with( + self=mock_subtensor, wallet=mock_wallet, netuid=123, uids=uids_tensor, diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index b537569f7..dcc6dd0da 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1137,7 +1137,7 @@ def test_do_set_weights_is_success(subtensor, mocker): subtensor.substrate.submit_extrinsic.return_value.is_success = True # Call - result = subtensor._do_set_weights( + result = subtensor.do_set_weights( wallet=fake_wallet, uids=fake_uids, vals=fake_vals, @@ -1190,7 +1190,7 @@ def test_do_set_weights_is_not_success(subtensor, mocker): subtensor_module.format_error_message = mocked_format_error_message # Call - result = subtensor._do_set_weights( + result = subtensor.do_set_weights( wallet=fake_wallet, uids=fake_uids, vals=fake_vals, @@ -1225,10 +1225,7 @@ def test_do_set_weights_is_not_success(subtensor, mocker): ) subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - 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, subtensor.substrate.submit_extrinsic.return_value.error_message) def test_do_set_weights_no_waits(subtensor, mocker): @@ -1242,7 +1239,7 @@ def test_do_set_weights_no_waits(subtensor, mocker): fake_wait_for_finalization = False # Call - result = subtensor._do_set_weights( + result = subtensor.do_set_weights( wallet=fake_wallet, uids=fake_uids, vals=fake_vals, From 4f4cad83b704cfdffd5cf2b55faba6660c20e4c6 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 4 Sep 2024 14:12:48 -0700 Subject: [PATCH 161/260] Move do_* methods into the related extrinsic module (bittensor/core/extrinsics/transfer.py). + ruff --- bittensor/core/extrinsics/transfer.py | 82 ++++++++-- bittensor/core/subtensor.py | 53 ------- .../test_subtensor_integration.py | 10 +- .../extrinsics/test_commit_weights.py | 16 +- .../unit_tests/extrinsics/test_set_weights.py | 12 +- tests/unit_tests/extrinsics/test_transfer.py | 142 ++++++++++++++++++ tests/unit_tests/test_subtensor.py | 126 +--------------- 7 files changed, 246 insertions(+), 195 deletions(-) create mode 100644 tests/unit_tests/extrinsics/test_transfer.py diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 3f6c964d9..a14165d37 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -15,19 +15,80 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from typing import Union, TYPE_CHECKING +from typing import Dict, Tuple, Optional, Union, TYPE_CHECKING -from bittensor_wallet import Wallet +from retry import retry from rich.prompt import Confirm from bittensor.core.settings import bt_console, NETWORK_EXPLORER_MAP -from bittensor.utils import get_explorer_url_for_network -from bittensor.utils import is_valid_bittensor_address_or_public_key +from bittensor.utils import ( + get_explorer_url_for_network, + format_error_message, + is_valid_bittensor_address_or_public_key, +) from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from bittensor.utils.networking import ensure_connected # For annotation purposes if TYPE_CHECKING: from bittensor.core.subtensor import Subtensor + from bittensor_wallet import Wallet + + +@ensure_connected +def do_transfer( + self: "Subtensor", + wallet: "Wallet", + dest: str, + transfer_balance: "Balance", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> Tuple[bool, Optional[str], Optional[Dict]]: + """Sends a transfer extrinsic to the chain. + + Args: + self (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (bittensor_wallet.Wallet): Wallet object. + dest (str): Destination public key address. + transfer_balance (bittensor.utils.balance.Balance): Amount to transfer. + wait_for_inclusion (bool): If ``true``, waits for inclusion. + wait_for_finalization (bool): If ``true``, waits for finalization. + + Returns: + success (bool): ``True`` if transfer was successful. + block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is ``True``. + error (Dict): Error message from subtensor if transfer failed. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": dest, "value": transfer_balance.rao}, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, None, None + + # Otherwise continue with finalization. + response.process_events() + if response.is_success: + block_hash = response.block_hash + return True, block_hash, None + else: + return False, None, response.error_message + + return make_substrate_call_with_retry() # Community uses this extrinsic directly and via `subtensor.transfer` @@ -113,10 +174,11 @@ def transfer_extrinsic( return False with bt_console.status(":satellite: Transferring..."): - success, block_hash, err_msg = subtensor.do_transfer( - wallet, - dest, - transfer_balance, + success, block_hash, error_message = do_transfer( + self=subtensor, + wallet=wallet, + dest=dest, + amount=transfer_balance, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, ) @@ -136,7 +198,9 @@ def transfer_extrinsic( f"[green]Taostats Explorer Link: {explorer_urls.get('taostats')}[/green]" ) else: - bt_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + bt_console.print( + f":cross_mark: [red]Failed[/red]: {format_error_message(error_message)}" + ) if success: with bt_console.status(":satellite: Checking Balance..."): diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index ecd8e1b6e..dd77510e4 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1010,59 +1010,6 @@ def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int ) return None if call is None else int(call) - @networking.ensure_connected - def do_transfer( - self, - wallet: "Wallet", - dest: str, - transfer_balance: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[str], Optional[str]]: - """Sends a transfer extrinsic to the chain. - - Args: - wallet (bittensor_wallet.Wallet): Wallet object. - dest (str): Destination public key address. - transfer_balance (bittensor.utils.balance.Balance): Amount to transfer. - wait_for_inclusion (bool): If ``true``, waits for inclusion. - wait_for_finalization (bool): If ``true``, waits for finalization. - - Returns: - success (bool): ``True`` if transfer was successful. - block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is ``True``. - error (str): Error message if transfer failed. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": dest, "value": transfer_balance.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None, None - - # Otherwise continue with finalization. - response.process_events() - if response.is_success: - block_hash = response.block_hash - return True, block_hash, None - else: - return False, None, format_error_message(response.error_message) - - return make_substrate_call_with_retry() - # Community uses this method def transfer( self, diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 65f939dfa..ee9371841 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -30,6 +30,7 @@ get_mock_keypair, get_mock_wallet, ) +from bittensor.core.extrinsics import transfer class TestSubtensor(unittest.TestCase): @@ -123,8 +124,7 @@ def test_do_block_step_query_previous_block(self): def test_transfer(self): fake_coldkey = get_mock_coldkey(1) - self.subtensor.do_transfer = MagicMock(return_value=(True, "0x", None)) - self.subtensor.register = MagicMock(return_value=True) + transfer.do_transfer = MagicMock(return_value=(True, "0x", None)) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( return_value=self.mock_neuron ) @@ -138,8 +138,7 @@ def test_transfer(self): def test_transfer_inclusion(self): fake_coldkey = get_mock_coldkey(1) - self.subtensor.do_transfer = MagicMock(return_value=(True, "0x", None)) - self.subtensor.register = MagicMock(return_value=True) + transfer.do_transfer = MagicMock(return_value=(True, "0x", None)) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( return_value=self.mock_neuron ) @@ -152,7 +151,7 @@ def test_transfer_inclusion(self): def test_transfer_failed(self): fake_coldkey = get_mock_coldkey(1) - self.subtensor.do_transfer = MagicMock( + transfer.do_transfer = MagicMock( return_value=(False, None, "Mock failure message") ) @@ -176,7 +175,6 @@ def test_transfer_dest_as_bytes(self): fake_coldkey = get_mock_coldkey(1) self.subtensor.do_transfer = MagicMock(return_value=(True, "0x", None)) - self.subtensor.register = MagicMock(return_value=True) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( return_value=self.mock_neuron ) diff --git a/tests/unit_tests/extrinsics/test_commit_weights.py b/tests/unit_tests/extrinsics/test_commit_weights.py index 6f0b30bb1..35a1d4d42 100644 --- a/tests/unit_tests/extrinsics/test_commit_weights.py +++ b/tests/unit_tests/extrinsics/test_commit_weights.py @@ -3,7 +3,10 @@ from bittensor.core import subtensor as subtensor_module from bittensor.core.settings import version_as_int from bittensor.core.subtensor import Subtensor -from bittensor.core.extrinsics.commit_weights import do_commit_weights, do_reveal_weights +from bittensor.core.extrinsics.commit_weights import ( + do_commit_weights, + do_reveal_weights, +) @pytest.fixture @@ -62,7 +65,10 @@ def test_do_commit_weights(subtensor, mocker): subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == (False, subtensor.substrate.submit_extrinsic.return_value.error_message) + assert result == ( + False, + subtensor.substrate.submit_extrinsic.return_value.error_message, + ) def test_do_reveal_weights(subtensor, mocker): @@ -121,5 +127,7 @@ def test_do_reveal_weights(subtensor, mocker): subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == (False, subtensor.substrate.submit_extrinsic.return_value.error_message) - + assert result == ( + False, + subtensor.substrate.submit_extrinsic.return_value.error_message, + ) diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 34be586a1..e9db4ea52 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -47,7 +47,17 @@ def mock_wallet(): True, "Not waiting for finalization or inclusion.", ), - ([1, 2], [0.5, 0.5], 0, True, False, True, True, False, "Subtensor returned `UnknownError (UnknownType)` error. This means: `Unknown Description`"), + ( + [1, 2], + [0.5, 0.5], + 0, + True, + False, + True, + True, + False, + "Subtensor returned `UnknownError (UnknownType)` error. This means: `Unknown Description`", + ), ([1, 2], [0.5, 0.5], 0, True, True, True, False, False, "Prompt refused."), ], ids=[ diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py new file mode 100644 index 000000000..af59d5769 --- /dev/null +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -0,0 +1,142 @@ +import pytest + +from bittensor.core import subtensor as subtensor_module +from bittensor.core.extrinsics.transfer import do_transfer +from bittensor.core.subtensor import Subtensor +from bittensor.utils.balance import Balance + + +@pytest.fixture +def subtensor(mocker): + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + return Subtensor() + + +def test_do_transfer_is_success_true(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = True + + # Call + result = do_transfer( + subtensor, + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization, + ) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + 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( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == ( + True, + subtensor.substrate.submit_extrinsic.return_value.block_hash, + None, + ) + + +def test_do_transfer_is_success_false(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + 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_transfer( + subtensor, + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization, + ) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + 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( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + + assert result == ( + False, + None, + subtensor.substrate.submit_extrinsic.return_value.error_message, + ) + + +def test_do_transfer_no_waits(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + + # Call + result = do_transfer( + subtensor, + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization, + ) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + 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( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == (True, None, None) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index dcc6dd0da..03d00d3cc 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1225,7 +1225,10 @@ def test_do_set_weights_is_not_success(subtensor, mocker): ) subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == (False, subtensor.substrate.submit_extrinsic.return_value.error_message) + assert result == ( + False, + subtensor.substrate.submit_extrinsic.return_value.error_message, + ) def test_do_set_weights_no_waits(subtensor, mocker): @@ -1417,127 +1420,6 @@ def test_subnetwork_n(subtensor, mocker): assert result == mocked_get_hyperparameter.return_value -def test_do_transfer_is_success_true(subtensor, mocker): - """Successful do_transfer call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_dest = "SS58PUBLICKEY" - fake_transfer_balance = Balance(1) - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - subtensor.substrate.submit_extrinsic.return_value.is_success = True - - # Call - result = subtensor.do_transfer( - fake_wallet, - fake_dest, - fake_transfer_balance, - fake_wait_for_inclusion, - fake_wait_for_finalization, - ) - - # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="Balances", - 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( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == ( - True, - subtensor.substrate.submit_extrinsic.return_value.block_hash, - None, - ) - - -def test_do_transfer_is_success_false(subtensor, mocker): - """Successful do_transfer call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_dest = "SS58PUBLICKEY" - fake_transfer_balance = Balance(1) - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - 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 = subtensor.do_transfer( - fake_wallet, - fake_dest, - fake_transfer_balance, - fake_wait_for_inclusion, - fake_wait_for_finalization, - ) - - # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="Balances", - 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( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - mocked_format_error_message.assert_called_once_with( - subtensor.substrate.submit_extrinsic.return_value.error_message - ) - assert result == (False, None, mocked_format_error_message.return_value) - - -def test_do_transfer_no_waits(subtensor, mocker): - """Successful do_transfer call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_dest = "SS58PUBLICKEY" - fake_transfer_balance = Balance(1) - fake_wait_for_inclusion = False - fake_wait_for_finalization = False - - # Call - result = subtensor.do_transfer( - fake_wallet, - fake_dest, - fake_transfer_balance, - fake_wait_for_inclusion, - fake_wait_for_finalization, - ) - - # Asserts - subtensor.substrate.compose_call.assert_called_once_with( - call_module="Balances", - 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( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - assert result == (True, None, None) - - def test_transfer(subtensor, mocker): """Tests successful transfer call.""" # Prep From 9a55af03f2b29ed3790eefebe7685cf8973567fa Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 4 Sep 2024 17:14:28 -0700 Subject: [PATCH 162/260] Move do_* methods into the related extrinsic module (bittensor/core/extrinsics/serving.py) --- bittensor/core/extrinsics/commit_weights.py | 1 + bittensor/core/extrinsics/serving.py | 65 +++++++++++-- bittensor/core/subtensor.py | 101 +------------------- tests/unit_tests/extrinsics/test_serving.py | 31 +++--- tests/unit_tests/test_subtensor.py | 63 ++---------- 5 files changed, 84 insertions(+), 177 deletions(-) diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index f2b2a2d36..5eb9feacb 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -26,6 +26,7 @@ from bittensor.utils.btlogging import logging from bittensor.utils.networking import ensure_connected +# For annotation purposes if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index aaa882e00..44ea31042 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -16,9 +16,8 @@ # DEALINGS IN THE SOFTWARE. import json -from typing import Optional, TYPE_CHECKING +from typing import Optional, Tuple, TYPE_CHECKING -from bittensor_wallet import Wallet from retry import retry from rich.prompt import Confirm @@ -28,13 +27,65 @@ from bittensor.core.types import AxonServeCallParams from bittensor.utils import format_error_message, networking as net from bittensor.utils.btlogging import logging +from bittensor.utils.networking import ensure_connected # For annotation purposes if TYPE_CHECKING: from bittensor.core.subtensor import Subtensor + from bittensor_wallet import Wallet + + +@ensure_connected +def do_serve_axon( + self: "Subtensor", + wallet: "Wallet", + call_params: AxonServeCallParams, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +) -> Tuple[bool, Optional[dict]]: + """ + 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. + + Args: + self (bittensor.core.subtensor.Subtensor): Subtensor instance object. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. + call_params (AxonServeCallParams): Parameters required for the serve axon call. + 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. + + Returns: + Tuple[bool, Optional[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. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="serve_axon", + call_params=call_params, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.hotkey + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if wait_for_inclusion or wait_for_finalization: + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + else: + return True, None + + return make_substrate_call_with_retry() -# Community uses this extrinsic via `subtensor.serve` def serve_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -117,7 +168,7 @@ def serve_extrinsic( logging.debug( f"Serving axon with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) -> {subtensor.network}:{netuid}" ) - success, error_message = subtensor.do_serve_axon( + success, error_message = do_serve_axon( wallet=wallet, call_params=params, wait_for_finalization=wait_for_finalization, @@ -131,13 +182,12 @@ def serve_extrinsic( ) return True else: - logging.error(f"Failed: {error_message}") + logging.error(f"Failed: {format_error_message(error_message)}") return False else: return True -# Community uses this extrinsic via `subtensor.set_weights` def serve_axon_extrinsic( subtensor: "Subtensor", netuid: int, @@ -177,7 +227,8 @@ def serve_axon_extrinsic( external_ip = axon.external_ip # ---- Subscribe to chain ---- - serve_success = subtensor.serve( + serve_success = serve_extrinsic( + subtensor=subtensor, wallet=axon.wallet, ip=external_ip, port=external_port, diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index dd77510e4..0b01c4cc2 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -55,7 +55,7 @@ prometheus_extrinsic, ) from bittensor.core.extrinsics.serving import ( - serve_extrinsic, + do_serve_axon, serve_axon_extrinsic, publish_metadata, get_metadata, @@ -68,7 +68,7 @@ transfer_extrinsic, ) from bittensor.core.metagraph import Metagraph -from bittensor.core.types import AxonServeCallParams, PrometheusServeCallParams +from bittensor.core.types import PrometheusServeCallParams from bittensor.utils import torch, format_error_message from bittensor.utils import u16_normalized_float, networking from bittensor.utils.balance import Balance @@ -1190,104 +1190,9 @@ def serve_prometheus( wait_for_finalization=wait_for_finalization, ) - # Community uses this method as part of `subtensor.serve_axon` - @networking.ensure_connected - def do_serve_axon( - self, - wallet: "Wallet", - call_params: AxonServeCallParams, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[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. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. - call_params (AxonServeCallParams): Parameters required for the serve axon call. - 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. - - Returns: - Tuple[bool, Optional[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. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="serve_axon", - call_params=call_params, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - else: - return True, None - - return make_substrate_call_with_retry() - - # keep backwards compatibility for the community + # keep backwards compatibility for the community (Subnet 27) _do_serve_axon = do_serve_axon - # Community uses this method - def serve( - self, - wallet: "Wallet", - ip: str, - port: int, - protocol: int, - netuid: int, - placeholder1: int = 0, - placeholder2: int = 0, - wait_for_inclusion: bool = False, - wait_for_finalization=True, - ) -> bool: - """ - Registers a neuron's serving endpoint on the Bittensor network. This function announces the IP address and port where the neuron is available to serve requests, facilitating peer-to-peer communication within the network. - - Args: - wallet (bittensor_wallet.Wallet): The wallet associated with the neuron being served. - ip (str): The IP address of the serving neuron. - port (int): The port number on which the neuron is serving. - protocol (int): The protocol type used by the neuron (e.g., GRPC, HTTP). - netuid (int): The unique identifier of the subnetwork. - placeholder1 (int, optional): Placeholder parameter for future extensions. Default is ``0``. - placeholder2 (int, optional): Placeholder parameter for future extensions. Default is ``0``. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Default is ``False``. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Default is ``True``. - - Returns: - bool: ``True`` if the serve registration is successful, False otherwise. - - This function is essential for establishing the neuron's presence in the network, enabling it to participate in the decentralized machine learning processes of Bittensor. - """ - return serve_extrinsic( - self, - wallet, - ip, - port, - protocol, - netuid, - placeholder1, - placeholder2, - wait_for_inclusion, - wait_for_finalization, - ) - # Community uses this method def get_subnet_hyperparameters( self, netuid: int, block: Optional[int] = None diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 73b3f8fcf..d1ca2ccc2 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -21,12 +21,8 @@ from bittensor_wallet import Wallet from bittensor.core.axon import Axon -from bittensor.core.extrinsics.serving import ( - serve_extrinsic, - publish_metadata, - serve_axon_extrinsic, -) from bittensor.core.subtensor import Subtensor +from bittensor.core.extrinsics import serving @pytest.fixture @@ -116,15 +112,16 @@ def test_serve_extrinsic_happy_path( prompt, expected, test_id, + mocker, ): # Arrange - mock_subtensor.do_serve_axon.return_value = (True, "") + serving.do_serve_axon = mocker.MagicMock(return_value=(True, "")) with patch( "bittensor.core.extrinsics.serving.Confirm.ask", return_value=True, ): # Act - result = serve_extrinsic( + result = serving.serve_extrinsic( mock_subtensor, mock_wallet, ip, @@ -176,15 +173,16 @@ def test_serve_extrinsic_edge_cases( prompt, expected, test_id, + mocker, ): # Arrange - mock_subtensor.do_serve_axon.return_value = (True, "") + serving.do_serve_axon = mocker.MagicMock(return_value=(True, "")) with patch( "bittensor.core.extrinsics.serving.Confirm.ask", return_value=True, ): # Act - result = serve_extrinsic( + result = serving.serve_extrinsic( mock_subtensor, mock_wallet, ip, @@ -236,15 +234,16 @@ def test_serve_extrinsic_error_cases( prompt, expected_error_message, test_id, + mocker, ): # Arrange - mock_subtensor.do_serve_axon.return_value = (False, "Error serving axon") + serving.do_serve_axon = mocker.MagicMock(return_value=(False, "Error serving axon")) with patch( "bittensor.core.extrinsics.serving.Confirm.ask", return_value=True, ): # Act - result = serve_extrinsic( + result = serving.serve_extrinsic( mock_subtensor, mock_wallet, ip, @@ -304,6 +303,7 @@ def test_serve_axon_extrinsic( serve_success, expected_result, test_id, + mocker, ): mock_axon.external_ip = external_ip # Arrange @@ -312,11 +312,12 @@ def test_serve_axon_extrinsic( side_effect=Exception("Failed to fetch IP") if not external_ip_success else MagicMock(return_value="192.168.1.1"), - ), patch.object(mock_subtensor, "serve", return_value=serve_success): + ): + serving.do_serve_axon = mocker.MagicMock(return_value=(serve_success, "")) # Act if not external_ip_success: with pytest.raises(RuntimeError): - serve_axon_extrinsic( + serving.serve_axon_extrinsic( mock_subtensor, netuid, mock_axon, @@ -324,7 +325,7 @@ def test_serve_axon_extrinsic( wait_for_finalization=wait_for_finalization, ) else: - result = serve_axon_extrinsic( + result = serving.serve_axon_extrinsic( mock_subtensor, netuid, mock_axon, @@ -387,7 +388,7 @@ def test_publish_metadata( ), ): # Act - result = publish_metadata( + result = serving.publish_metadata( subtensor=mock_subtensor, wallet=mock_wallet, netuid=net_uid, diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 03d00d3cc..20dae5bad 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1611,9 +1611,6 @@ def test_do_serve_prometheus_is_not_success(subtensor, mocker): subtensor.substrate.submit_extrinsic.return_value.is_success = None - mocked_format_error_message = mocker.MagicMock() - subtensor_module.format_error_message = mocked_format_error_message - # Call result = subtensor._do_serve_prometheus( wallet=fake_wallet, @@ -1641,10 +1638,10 @@ def test_do_serve_prometheus_is_not_success(subtensor, mocker): ) subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - mocked_format_error_message.assert_called_once_with( - subtensor.substrate.submit_extrinsic.return_value.error_message + assert result == ( + False, + subtensor.substrate.submit_extrinsic.return_value.error_message, ) - assert result == (False, mocked_format_error_message.return_value) def test_do_serve_prometheus_no_waits(subtensor, mocker): @@ -1768,9 +1765,6 @@ def test_do_serve_axon_is_not_success(subtensor, mocker): subtensor.substrate.submit_extrinsic.return_value.is_success = None - mocked_format_error_message = mocker.MagicMock() - subtensor_module.format_error_message = mocked_format_error_message - # Call result = subtensor._do_serve_axon( wallet=fake_wallet, @@ -1798,10 +1792,10 @@ def test_do_serve_axon_is_not_success(subtensor, mocker): ) subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - mocked_format_error_message.assert_called_once_with( - subtensor.substrate.submit_extrinsic.return_value.error_message + assert result == ( + False, + subtensor.substrate.submit_extrinsic.return_value.error_message, ) - assert result == (False, mocked_format_error_message.return_value) def test_do_serve_axon_no_waits(subtensor, mocker): @@ -1840,51 +1834,6 @@ def test_do_serve_axon_no_waits(subtensor, mocker): assert result == (True, None) -def test_serve(subtensor, mocker): - """Successful serve call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_ip = "fake_ip" - fake_port = 1234 - fake_protocol = 1 - fake_netuid = 1 - fake_placeholder1 = 0 - fake_placeholder2 = 1 - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - mocked_serve_extrinsic = mocker.patch.object(subtensor_module, "serve_extrinsic") - - # Call - result = subtensor.serve( - fake_wallet, - fake_ip, - fake_port, - fake_protocol, - fake_netuid, - fake_placeholder1, - fake_placeholder2, - fake_wait_for_inclusion, - fake_wait_for_finalization, - ) - - # Asserts - mocked_serve_extrinsic.assert_called_once_with( - subtensor, - fake_wallet, - fake_ip, - fake_port, - fake_protocol, - fake_netuid, - fake_placeholder1, - fake_placeholder2, - fake_wait_for_inclusion, - fake_wait_for_finalization, - ) - - assert result == mocked_serve_extrinsic.return_value - - def test_immunity_period(subtensor, mocker): """Successful immunity_period call.""" # Preps From 784696505df8d89577037ae4baf44737f21ce280 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 4 Sep 2024 18:24:08 -0700 Subject: [PATCH 163/260] Move do_* methods into the related extrinsic module (bittensor/core/extrinsics/prometheus.py) --- bittensor/core/extrinsics/prometheus.py | 67 +++++++++++++++++-- bittensor/core/subtensor.py | 54 +-------------- .../unit_tests/extrinsics/test_prometheus.py | 50 ++++++-------- 3 files changed, 84 insertions(+), 87 deletions(-) diff --git a/bittensor/core/extrinsics/prometheus.py b/bittensor/core/extrinsics/prometheus.py index 64ee07c60..135b3fed1 100644 --- a/bittensor/core/extrinsics/prometheus.py +++ b/bittensor/core/extrinsics/prometheus.py @@ -16,21 +16,71 @@ # DEALINGS IN THE SOFTWARE. import json -from typing import TYPE_CHECKING - -from bittensor_wallet import Wallet +from typing import Tuple, Optional, TYPE_CHECKING +from retry import retry from bittensor.core.settings import version_as_int, bt_console from bittensor.core.types import PrometheusServeCallParams -from bittensor.utils import networking as net +from bittensor.utils import networking as net, format_error_message from bittensor.utils.btlogging import logging +from bittensor.utils.networking import ensure_connected # For annotation purposes if TYPE_CHECKING: + from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor -# Community uses this extrinsic +@ensure_connected +def do_serve_prometheus( + self: "Subtensor", + wallet: "Wallet", + call_params: PrometheusServeCallParams, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +) -> Tuple[bool, Optional[dict]]: + """ + Sends a serve prometheus extrinsic to the chain. + + Args: + self (bittensor.subtensor): Bittensor subtensor object + wallet (:func:`bittensor_wallet.Wallet`): Wallet object. + call_params (:func:`PrometheusServeCallParams`): Prometheus serve call parameters. + wait_for_inclusion (bool): If ``true``, waits for inclusion. + wait_for_finalization (bool): If ``true``, waits for finalization. + + Returns: + success (bool): ``True`` if serve prometheus was successful. + error (:func:`Optional[str]`): Error message if serve prometheus failed, ``None`` otherwise. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="serve_prometheus", + call_params=call_params, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.hotkey + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if wait_for_inclusion or wait_for_finalization: + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + else: + return True, None + + return make_substrate_call_with_retry() + + def prometheus_extrinsic( subtensor: "Subtensor", wallet: "Wallet", @@ -109,7 +159,8 @@ def prometheus_extrinsic( with bt_console.status( f":satellite: Serving prometheus on: [white]{subtensor.network}:{netuid}[/white] ..." ): - success, error_message = subtensor.do_serve_prometheus( + success, error_message = do_serve_prometheus( + self=subtensor, wallet=wallet, call_params=call_params, wait_for_finalization=wait_for_finalization, @@ -124,7 +175,9 @@ def prometheus_extrinsic( ) return True else: - bt_console.print(f":cross_mark: [red]Failed[/red]: {error_message}") + bt_console.print( + f":cross_mark: [red]Failed[/red]: {format_error_message(error_message)}" + ) return False else: return True diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 0b01c4cc2..3a7c64efe 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -52,6 +52,7 @@ reveal_weights_extrinsic, ) from bittensor.core.extrinsics.prometheus import ( + do_serve_prometheus, prometheus_extrinsic, ) from bittensor.core.extrinsics.serving import ( @@ -68,8 +69,8 @@ transfer_extrinsic, ) from bittensor.core.metagraph import Metagraph -from bittensor.core.types import PrometheusServeCallParams -from bittensor.utils import torch, format_error_message + +from bittensor.utils import torch from bittensor.utils import u16_normalized_float, networking from bittensor.utils.balance import Balance from bittensor.utils.btlogging import logging @@ -1107,55 +1108,6 @@ def make_substrate_call_with_retry(): return NeuronInfo.from_vec_u8(result) - # Community uses this method via `bittensor.api.extrinsics.prometheus.prometheus_extrinsic` - @networking.ensure_connected - def do_serve_prometheus( - self, - wallet: "Wallet", - call_params: PrometheusServeCallParams, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """ - Sends a serve prometheus extrinsic to the chain. - - Args: - wallet (:func:`bittensor_wallet.Wallet`): Wallet object. - call_params (:func:`PrometheusServeCallParams`): Prometheus serve call parameters. - wait_for_inclusion (bool): If ``true``, waits for inclusion. - wait_for_finalization (bool): If ``true``, waits for finalization. - - Returns: - success (bool): ``True`` if serve prometheus was successful. - error (:func:`Optional[str]`): Error message if serve prometheus failed, ``None`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="serve_prometheus", - call_params=call_params, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - else: - return True, None - - return make_substrate_call_with_retry() - # Community uses this method name _do_serve_prometheus = do_serve_prometheus diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index e7b18bfcb..dbcfed1e4 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -70,6 +70,7 @@ def test_prometheus_extrinsic_happy_path( # Arrange subtensor = MagicMock(spec=Subtensor) subtensor.network = "test_network" + subtensor.substrate = MagicMock() wallet = MagicMock(spec=Wallet) mock_net.get_external_ip.return_value = "192.168.0.1" mock_net.ip_to_int.return_value = 3232235521 # IP in integer form @@ -81,7 +82,7 @@ def test_prometheus_extrinsic_happy_path( neuron.prometheus_info.port = port neuron.prometheus_info.ip_type = 4 subtensor.get_neuron_for_pubkey_and_subnet.return_value = neuron - subtensor.do_serve_prometheus.return_value = (True, None) + subtensor._do_serve_prometheus.return_value = (True, None) # Act result = prometheus_extrinsic( @@ -112,6 +113,7 @@ def test_prometheus_extrinsic_edge_cases( # Arrange subtensor = MagicMock(spec=Subtensor) subtensor.network = "test_network" + subtensor.substrate = MagicMock() wallet = MagicMock(spec=Wallet) mock_net.get_external_ip.return_value = ip mock_net.ip_to_int.return_value = 3232235521 # IP in integer form @@ -119,7 +121,7 @@ def test_prometheus_extrinsic_edge_cases( neuron = MagicMock() neuron.is_null = True subtensor.get_neuron_for_pubkey_and_subnet.return_value = neuron - subtensor.do_serve_prometheus.return_value = (True, None) + subtensor._do_serve_prometheus.return_value = (True, None) # Act result = prometheus_extrinsic( @@ -137,39 +139,29 @@ def test_prometheus_extrinsic_edge_cases( # Error cases -@pytest.mark.parametrize( - "ip, port, netuid, exception, test_id", - [ - ( - None, - 9221, - 0, - RuntimeError("Unable to attain your external ip."), - "error-case-no-external-ip", - ), - ], -) -def test_prometheus_extrinsic_error_cases( - mock_bittensor, mock_wallet, mock_net, ip, port, netuid, exception, test_id -): +def test_prometheus_extrinsic_error_cases(mock_bittensor, mock_wallet, mocker): # Arrange subtensor = MagicMock(spec=Subtensor) subtensor.network = "test_network" + subtensor.substrate = MagicMock() + subtensor.substrate.websocket.sock.getsockopt.return_value = 0 wallet = MagicMock(spec=Wallet) - mock_net.get_external_ip.side_effect = exception neuron = MagicMock() neuron.is_null = True subtensor.get_neuron_for_pubkey_and_subnet.return_value = neuron subtensor._do_serve_prometheus.return_value = (True,) - # Act & Assert - with pytest.raises(ValueError): - prometheus_extrinsic( - subtensor=subtensor, - wallet=wallet, - ip=ip, - port=port, - netuid=netuid, - wait_for_inclusion=False, - wait_for_finalization=True, - ) + with mocker.patch( + "bittensor.utils.networking.get_external_ip", side_effect=RuntimeError + ): + # Act & Assert + with pytest.raises(RuntimeError): + prometheus_extrinsic( + subtensor=subtensor, + wallet=wallet, + ip=None, + port=9221, + netuid=1, + wait_for_inclusion=False, + wait_for_finalization=True, + ) From 453c7a7c5bf8f5b604e090935a0bd56f07daf825 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 5 Sep 2024 09:58:48 -0700 Subject: [PATCH 164/260] bug fixes --- bittensor/core/extrinsics/commit_weights.py | 4 ++-- bittensor/core/extrinsics/serving.py | 8 +++++--- bittensor/core/extrinsics/transfer.py | 2 +- bittensor/utils/networking.py | 1 + tests/e2e_tests/test_incentive.py | 2 +- tests/unit_tests/extrinsics/test_serving.py | 2 +- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 5eb9feacb..8428a0c52 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -125,7 +125,7 @@ def commit_weights_extrinsic( return False, "User cancelled the operation." success, error_message = do_commit_weights( - subtensor=subtensor, + self=subtensor, wallet=wallet, netuid=netuid, commit_hash=commit_hash, @@ -250,7 +250,7 @@ def reveal_weights_extrinsic( return False, "User cancelled the operation." success, error_message = do_reveal_weights( - subtensor=subtensor, + self=subtensor, wallet=wallet, netuid=netuid, uids=uids, diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index 44ea31042..66a86cb28 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -169,6 +169,7 @@ def serve_extrinsic( f"Serving axon with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) -> {subtensor.network}:{netuid}" ) success, error_message = do_serve_axon( + self=subtensor, wallet=wallet, call_params=params, wait_for_finalization=wait_for_finalization, @@ -241,8 +242,9 @@ def serve_axon_extrinsic( # Community uses this extrinsic directly and via `subtensor.commit` +@net.ensure_connected def publish_metadata( - subtensor, + self: "Subtensor", wallet: "Wallet", netuid: int, data_type: str, @@ -254,7 +256,7 @@ def publish_metadata( Publishes metadata on the Bittensor network using the specified wallet and network identifier. Args: - subtensor (bittensor.subtensor): The subtensor instance representing the Bittensor blockchain connection. + self (bittensor.subtensor): The subtensor instance representing the Bittensor blockchain connection. wallet (bittensor.wallet): The wallet object used for authentication in the transaction. netuid (int): Network UID on which the metadata is to be published. data_type (str): The data type of the information being submitted. It should be one of the following: ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing algorithm used for the data. @@ -271,7 +273,7 @@ def publish_metadata( wallet.unlock_hotkey() - with subtensor.substrate as substrate: + with self.substrate as substrate: call = substrate.compose_call( call_module="Commitments", call_function="set_commitment", diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index a14165d37..53e83820a 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -178,7 +178,7 @@ def transfer_extrinsic( self=subtensor, wallet=wallet, dest=dest, - amount=transfer_balance, + transfer_balance=transfer_balance, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, ) diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index 834456f33..ff7d40a70 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -178,6 +178,7 @@ def ensure_connected(func): @wraps(func) def wrapper(self, *args, **kwargs): + """Wrapper function where `self` argument is Subtensor instance with the substrate connection.""" # Check the socket state before method execution if ( # connection was closed correctly diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index 2c3946184..b525e4886 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -149,7 +149,7 @@ async def test_incentive(local_chain): await wait_epoch(subtensor) # Set weights by Alice on the subnet - subtensor._do_set_weights( + subtensor.do_set_weights( wallet=alice_wallet, uids=[1], vals=[65535], diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index d1ca2ccc2..a57e32d01 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -389,7 +389,7 @@ def test_publish_metadata( ): # Act result = serving.publish_metadata( - subtensor=mock_subtensor, + self=mock_subtensor, wallet=mock_wallet, netuid=net_uid, data_type=type_u, From 5cdb9bec1a00792427b049b1dd419d3251dbec96 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 5 Sep 2024 10:31:29 -0700 Subject: [PATCH 165/260] extrinsics refactoring --- bittensor/core/extrinsics/commit_weights.py | 6 ++++-- bittensor/core/extrinsics/prometheus.py | 1 + bittensor/core/extrinsics/serving.py | 1 + bittensor/core/extrinsics/set_weights.py | 1 + bittensor/core/extrinsics/transfer.py | 1 + bittensor/core/subtensor.py | 20 ++++++-------------- 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 8428a0c52..73c452acd 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -32,7 +32,7 @@ from bittensor.core.subtensor import Subtensor -# Community uses this method +# # Chain call for `commit_weights_extrinsic` @ensure_connected def do_commit_weights( self: "Subtensor", @@ -118,6 +118,7 @@ def commit_weights_extrinsic( Returns: Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. """ @@ -143,7 +144,7 @@ def commit_weights_extrinsic( return False, error_message -# Community uses this method +# Chain call for `reveal_weights_extrinsic` @ensure_connected def do_reveal_weights( self: "Subtensor", @@ -242,6 +243,7 @@ def reveal_weights_extrinsic( Returns: Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, 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. """ diff --git a/bittensor/core/extrinsics/prometheus.py b/bittensor/core/extrinsics/prometheus.py index 135b3fed1..9aa95a838 100644 --- a/bittensor/core/extrinsics/prometheus.py +++ b/bittensor/core/extrinsics/prometheus.py @@ -31,6 +31,7 @@ from bittensor.core.subtensor import Subtensor +# Chain call for `prometheus_extrinsic` @ensure_connected def do_serve_prometheus( self: "Subtensor", diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index 66a86cb28..c68dd27df 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -35,6 +35,7 @@ from bittensor_wallet import Wallet +# Chain call for `serve_extrinsic` and `serve_axon_extrinsic` @ensure_connected def do_serve_axon( self: "Subtensor", diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 320d9d65e..7c58abeb6 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -35,6 +35,7 @@ from bittensor_wallet import Wallet +# Chain call for `do_set_weights` @ensure_connected def do_set_weights( self, diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 53e83820a..9d0e72d6b 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -36,6 +36,7 @@ from bittensor_wallet import Wallet +# Chain call for `transfer_extrinsic` @ensure_connected def do_transfer( self: "Subtensor", diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 3a7c64efe..ee2faecd4 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -61,15 +61,11 @@ publish_metadata, get_metadata, ) -from bittensor.core.extrinsics.set_weights import ( - do_set_weights, - set_weights_extrinsic, -) +from bittensor.core.extrinsics.set_weights import set_weights_extrinsic from bittensor.core.extrinsics.transfer import ( transfer_extrinsic, ) from bittensor.core.metagraph import Metagraph - from bittensor.utils import torch from bittensor.utils import u16_normalized_float, networking from bittensor.utils.balance import Balance @@ -842,9 +838,6 @@ def is_hotkey_registered( else: return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) - # keep backwards compatibility for the community - do_set_weights = do_set_weights - # Not used in Bittensor, but is actively used by the community in almost all subnets def set_weights( self, @@ -1108,9 +1101,6 @@ def make_substrate_call_with_retry(): return NeuronInfo.from_vec_u8(result) - # Community uses this method name - _do_serve_prometheus = do_serve_prometheus - # Community uses this method def serve_prometheus( self, @@ -1142,9 +1132,6 @@ def serve_prometheus( wait_for_finalization=wait_for_finalization, ) - # keep backwards compatibility for the community (Subnet 27) - _do_serve_axon = do_serve_axon - # Community uses this method def get_subnet_hyperparameters( self, netuid: int, block: Optional[int] = None @@ -1741,3 +1728,8 @@ def reveal_weights( retries += 1 return success, message + + # Subnet 27 uses this method + _do_serve_prometheus = do_serve_prometheus + # Subnet 27 uses this method name + _do_serve_axon = do_serve_axon From 841db03aa77e68efbd58dfb3dd9a2baa1a22d41f Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 5 Sep 2024 11:53:29 -0700 Subject: [PATCH 166/260] bug fix --- bittensor/core/extrinsics/set_weights.py | 2 +- tests/e2e_tests/test_incentive.py | 14 +- .../unit_tests/extrinsics/test_set_weights.py | 168 +++++++++++++++++- tests/unit_tests/test_subtensor.py | 155 ---------------- 4 files changed, 175 insertions(+), 164 deletions(-) diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 7c58abeb6..86f993c53 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -38,7 +38,7 @@ # Chain call for `do_set_weights` @ensure_connected def do_set_weights( - self, + self: "Subtensor", wallet: "Wallet", uids: List[int], vals: List[int], diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index b525e4886..ee02727de 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -3,7 +3,7 @@ import pytest -import bittensor +# import bittensor from bittensor import Subtensor, logging from tests.e2e_tests.utils.chain_interactions import ( add_stake, @@ -16,6 +16,9 @@ template_path, templates_repo, ) +from bittensor.utils.balance import Balance +from bittensor.core.extrinsics.set_weights import do_set_weights +from bittensor.core.metagraph import Metagraph @pytest.mark.asyncio @@ -61,7 +64,7 @@ async def test_incentive(local_chain): ), "Alice & Bob not registered in the subnet" # Alice to stake to become to top neuron after the first epoch - add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(10_000)) + add_stake(local_chain, alice_wallet, Balance.from_tao(10_000)) # Prepare to run Bob as miner cmd = " ".join( @@ -130,7 +133,7 @@ async def test_incentive(local_chain): ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data # Get latest metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + metagraph = Metagraph(netuid=netuid, network="ws://localhost:9945") # Get current miner/validator stats bob_neuron = metagraph.neurons[1] @@ -149,7 +152,8 @@ async def test_incentive(local_chain): await wait_epoch(subtensor) # Set weights by Alice on the subnet - subtensor.do_set_weights( + do_set_weights( + self=subtensor, wallet=alice_wallet, uids=[1], vals=[65535], @@ -163,7 +167,7 @@ async def test_incentive(local_chain): await wait_epoch(subtensor) # Refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + metagraph = Metagraph(netuid=netuid, network="ws://localhost:9945") # Get current emissions and validate that Alice has gotten tao bob_neuron = metagraph.neurons[1] diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index e9db4ea52..4b49a512b 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -1,18 +1,23 @@ -import torch -import pytest from unittest.mock import MagicMock, patch -from bittensor.core.subtensor import Subtensor +import pytest +import torch from bittensor_wallet import Wallet + +from bittensor.core import subtensor as subtensor_module from bittensor.core.extrinsics.set_weights import ( + do_set_weights, set_weights_extrinsic, ) +from bittensor.core.settings import version_as_int +from bittensor.core.subtensor import Subtensor @pytest.fixture def mock_subtensor(): mock = MagicMock(spec=Subtensor) mock.network = "mock_network" + mock.substrate = MagicMock() return mock @@ -114,3 +119,160 @@ def test_set_weights_extrinsic( wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, ) + + +def test_do_set_weights_is_success(mock_subtensor, mocker): + """Successful _do_set_weights call.""" + # Prep + fake_wallet = mocker.MagicMock() + 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 = True + + # Call + result = do_set_weights( + self=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, + }, + ) + + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=mock_subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + era={"period": 5}, + ) + + mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( + mock_subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + mock_subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == (True, "Successfully set weights.") + + +def test_do_set_weights_is_not_success(mock_subtensor, mocker): + """Unsuccessful _do_set_weights call.""" + # Prep + fake_wallet = mocker.MagicMock() + 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( + self=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, + }, + ) + + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=mock_subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + era={"period": 5}, + ) + + mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( + mock_subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + mock_subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == ( + False, + mock_subtensor.substrate.submit_extrinsic.return_value.error_message, + ) + + +def test_do_set_weights_no_waits(mock_subtensor, mocker): + """Successful _do_set_weights call without wait flags for fake_wait_for_inclusion and fake_wait_for_finalization.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_uids = [1, 2, 3] + fake_vals = [4, 5, 6] + fake_netuid = 1 + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + + # Call + result = do_set_weights( + self=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, + }, + ) + + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=mock_subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + era={"period": 5}, + ) + + mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( + mock_subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == (True, "Not waiting for finalization or inclusion.") diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 20dae5bad..ecc72c27c 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1123,161 +1123,6 @@ def test_is_hotkey_registered_with_netuid(subtensor, mocker): ) assert result == mocked_is_hotkey_registered_on_subnet.return_value - -def test_do_set_weights_is_success(subtensor, mocker): - """Successful _do_set_weights call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_uids = [1, 2, 3] - fake_vals = [4, 5, 6] - fake_netuid = 1 - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - subtensor.substrate.submit_extrinsic.return_value.is_success = True - - # Call - result = subtensor.do_set_weights( - wallet=fake_wallet, - uids=fake_uids, - vals=fake_vals, - netuid=fake_netuid, - version_key=settings.version_as_int, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - # Asserts - 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": settings.version_as_int, - }, - ) - - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - era={"period": 5}, - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == (True, "Successfully set weights.") - - -def test_do_set_weights_is_not_success(subtensor, mocker): - """Unsuccessful _do_set_weights call.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_uids = [1, 2, 3] - fake_vals = [4, 5, 6] - fake_netuid = 1 - fake_wait_for_inclusion = True - fake_wait_for_finalization = True - - 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 = subtensor.do_set_weights( - wallet=fake_wallet, - uids=fake_uids, - vals=fake_vals, - netuid=fake_netuid, - version_key=settings.version_as_int, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - # Asserts - 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": settings.version_as_int, - }, - ) - - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - era={"period": 5}, - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() - assert result == ( - False, - subtensor.substrate.submit_extrinsic.return_value.error_message, - ) - - -def test_do_set_weights_no_waits(subtensor, mocker): - """Successful _do_set_weights call without wait flags for fake_wait_for_inclusion and fake_wait_for_finalization.""" - # Prep - fake_wallet = mocker.MagicMock() - fake_uids = [1, 2, 3] - fake_vals = [4, 5, 6] - fake_netuid = 1 - fake_wait_for_inclusion = False - fake_wait_for_finalization = False - - # Call - result = subtensor.do_set_weights( - wallet=fake_wallet, - uids=fake_uids, - vals=fake_vals, - netuid=fake_netuid, - version_key=settings.version_as_int, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - - # Asserts - 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": settings.version_as_int, - }, - ) - - subtensor.substrate.create_signed_extrinsic.assert_called_once_with( - call=subtensor.substrate.compose_call.return_value, - keypair=fake_wallet.hotkey, - era={"period": 5}, - ) - - subtensor.substrate.submit_extrinsic.assert_called_once_with( - subtensor.substrate.create_signed_extrinsic.return_value, - wait_for_inclusion=fake_wait_for_inclusion, - wait_for_finalization=fake_wait_for_finalization, - ) - assert result == (True, "Not waiting for finalization or inclusion.") - - def test_set_weights(subtensor, mocker): """Successful set_weights call.""" # Preps From f61c732afa0441fd1b039e396eef164f934a200c Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 5 Sep 2024 11:54:24 -0700 Subject: [PATCH 167/260] ruff --- tests/unit_tests/test_subtensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index ecc72c27c..97636229c 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1123,6 +1123,7 @@ def test_is_hotkey_registered_with_netuid(subtensor, mocker): ) assert result == mocked_is_hotkey_registered_on_subnet.return_value + def test_set_weights(subtensor, mocker): """Successful set_weights call.""" # Preps From 481bf8ac267e355fb46cf1eb685768869e9f642a Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 5 Sep 2024 12:14:42 -0700 Subject: [PATCH 168/260] fix --- bittensor/utils/mock/subtensor_mock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py index 0e4939efa..bd42cf79a 100644 --- a/bittensor/utils/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -267,7 +267,7 @@ def setup(self) -> None: self.block_number = 0 self.network = "mock" - self.chain_endpoint = "mock_endpoint" + self.chain_endpoint = "ws://mock_endpoint.bt" self.substrate = MagicMock() def __init__(self, *args, **kwargs) -> None: From 1d6e942aeb57e659c3c04624b91ffc7fb3d69662 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 5 Sep 2024 18:54:59 -0700 Subject: [PATCH 169/260] removed exit sys call for ConnectionRefusedError in _get_substrate --- bittensor/core/subtensor.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 7b6940a16..e3178b5f2 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -223,6 +223,11 @@ def _get_substrate(self): f"Connected to {self.network} network and {self.chain_endpoint}." ) + try: + self.substrate.websocket.settimeout(self._connection_timeout) + except (AttributeError, TypeError, socket.error, OSError) as e: + logging.warning(f"Error setting timeout: {e}") + except ConnectionRefusedError: logging.error( f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint. Exiting...", @@ -231,16 +236,6 @@ def _get_substrate(self): "You can check if you have connectivity by running this command: nc -vz localhost " f"{self.chain_endpoint.split(':')[2]}" ) - sys.exit(1) - - try: - self.substrate.websocket.settimeout(self._connection_timeout) - except AttributeError as e: - logging.warning(f"AttributeError: {e}") - except TypeError as e: - logging.warning(f"TypeError: {e}") - except (socket.error, OSError) as e: - logging.warning(f"Socket error: {e}") @staticmethod def config() -> "Config": From 1796452a744e868abb3c8e8fe605b7af7331123a Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 5 Sep 2024 19:03:44 -0700 Subject: [PATCH 170/260] remove unused import --- bittensor/core/subtensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index e3178b5f2..1adb15937 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -23,7 +23,6 @@ import argparse import copy import socket -import sys from typing import List, Dict, Union, Optional, Tuple, TypedDict, Any import numpy as np From ad23601fb9c5dcd81300ed216dc6955a48055644 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 6 Sep 2024 06:49:07 -0700 Subject: [PATCH 171/260] change the log message --- bittensor/core/subtensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 1adb15937..f4b5f8c24 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -229,7 +229,7 @@ def _get_substrate(self): except ConnectionRefusedError: logging.error( - f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint. Exiting...", + f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint.", ) logging.info( "You can check if you have connectivity by running this command: nc -vz localhost " From c7b47398f38dd12e87ab267eb0fe329197ba46e2 Mon Sep 17 00:00:00 2001 From: garrett-opentensor <156717492+garrett-opentensor@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:47:04 -0700 Subject: [PATCH 172/260] removed exit sys call for ConnectionRefusedError in _get_substrate (#2287) * removed exit sys call for ConnectionRefusedError in _get_substrate * removed unnecessary sys import * ruff format fix --- bittensor/subtensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index c4e70deae..2c8f1d75c 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -26,7 +26,6 @@ import argparse import copy import socket -import sys import time from typing import List, Dict, Union, Optional, Tuple, TypedDict, Any @@ -293,7 +292,7 @@ def _get_substrate(self): "You can check if you have connectivity by running this command: nc -vz localhost " f"{self.chain_endpoint.split(':')[2]}" ) - sys.exit(1) + return try: self.substrate.websocket.settimeout(self._connection_timeout) From d3a60faa7755d40b395756b71359263d01f0a570 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 6 Sep 2024 13:03:53 -0700 Subject: [PATCH 173/260] Corrected arguments order in logging methods + test --- bittensor/utils/btlogging/loggingmachine.py | 16 ++++++++-------- bittensor/utils/weight_utils.py | 10 +++++----- tests/unit_tests/test_logging.py | 5 ++--- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index b9e0408ce..03ae23720 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -364,42 +364,42 @@ def __trace_on__(self) -> bool: def _concat_msg(*args): return " - ".join(str(el) for el in args if el != "") - def trace(self, msg="", *args, prefix="", suffix="", **kwargs): + def trace(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps trace message with prefix and suffix.""" msg = self._concat_msg(prefix, msg, suffix) self._logger.trace(msg, *args, **kwargs, stacklevel=2) - def debug(self, msg="", *args, prefix="", suffix="", **kwargs): + def debug(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps debug message with prefix and suffix.""" msg = self._concat_msg(prefix, msg, suffix) self._logger.debug(msg, *args, **kwargs, stacklevel=2) - def info(self, msg="", *args, prefix="", suffix="", **kwargs): + def info(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps info message with prefix and suffix.""" msg = self._concat_msg(prefix, msg, suffix) self._logger.info(msg, *args, **kwargs, stacklevel=2) - def success(self, msg="", *args, prefix="", suffix="", **kwargs): + def success(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps success message with prefix and suffix.""" msg = self._concat_msg(prefix, msg, suffix) self._logger.success(msg, *args, **kwargs, stacklevel=2) - def warning(self, msg="", *args, prefix="", suffix="", **kwargs): + def warning(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps warning message with prefix and suffix.""" msg = self._concat_msg(prefix, msg, suffix) self._logger.warning(msg, *args, **kwargs, stacklevel=2) - def error(self, msg="", *args, prefix="", suffix="", **kwargs): + def error(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps error message with prefix and suffix.""" msg = self._concat_msg(prefix, msg, suffix) self._logger.error(msg, *args, **kwargs, stacklevel=2) - def critical(self, msg="", *args, prefix="", suffix="", **kwargs): + def critical(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps critical message with prefix and suffix.""" msg = self._concat_msg(prefix, msg, suffix) self._logger.critical(msg, *args, **kwargs, stacklevel=2) - def exception(self, msg="", *args, prefix="", suffix="", **kwargs): + def exception(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps exception message with prefix and suffix.""" msg = self._concat_msg(prefix, msg, suffix) stacklevel = 2 diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 3cc56ac96..bbbf446b6 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -253,7 +253,7 @@ def process_weights_for_netuid( """ logging.debug("process_weights_for_netuid()") - logging.debug("weights", weights) + logging.debug("weights", *weights) logging.debug("netuid", netuid) logging.debug("subtensor", subtensor) logging.debug("metagraph", metagraph) @@ -318,7 +318,7 @@ def process_weights_for_netuid( else np.ones((metagraph.n), dtype=np.int64) * 1e-5 ) # creating minimum even non-zero weights weights[non_zero_weight_idx] += non_zero_weights - logging.debug("final_weights", weights) + logging.debug("final_weights", *weights) normalized_weights = normalize_max_weight(x=weights, limit=max_weight_limit) nw_arange = ( torch.tensor(list(range(len(normalized_weights)))) @@ -327,7 +327,7 @@ def process_weights_for_netuid( ) return nw_arange, normalized_weights - logging.debug("non_zero_weights", non_zero_weights) + logging.debug("non_zero_weights", *non_zero_weights) # Compute the exclude quantile and find the weights in the lowest quantile max_exclude = max(0, len(non_zero_weights) - min_allowed_weights) / len( @@ -346,8 +346,8 @@ def process_weights_for_netuid( # Exclude all weights below the allowed quantile. non_zero_weight_uids = non_zero_weight_uids[lowest_quantile <= non_zero_weights] non_zero_weights = non_zero_weights[lowest_quantile <= non_zero_weights] - logging.debug("non_zero_weight_uids", non_zero_weight_uids) - logging.debug("non_zero_weights", non_zero_weights) + logging.debug("non_zero_weight_uids", *non_zero_weight_uids) + logging.debug("non_zero_weights", *non_zero_weights) # Normalize weights and return. normalized_weights = normalize_max_weight( diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 627a7c3fd..24ab40507 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -51,7 +51,7 @@ def mock_config(tmp_path): def logging_machine(mock_config): config, _ = mock_config logging_machine = LoggingMachine(config=config) - yield logging_machine + return logging_machine def test_initialization(logging_machine, mock_config): @@ -196,12 +196,11 @@ def test_log_sanity(logging_machine, caplog): {"suffix": "suff"}, {"prefix": "pref", "suffix": "suff"}, ] - cookiejar = {} for i, nfix in enumerate(nfixtests): prefix = nfix.get("prefix", "") suffix = nfix.get("suffix", "") use_cookie = f"{cookie} #{i}#" - logging_machine.info(basemsg, i, use_cookie, prefix=prefix, suffix=suffix) + logging_machine.info(basemsg, prefix, suffix, i, use_cookie) # Check to see if all elements are present, regardless of downstream formatting. expect = f"INFO.*{os.path.basename(__file__)}.* " if prefix != "": From 7b1e98422a06078ebe971a4325bf636e6cfcd043 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 6 Sep 2024 15:15:57 -0700 Subject: [PATCH 174/260] fix integration test --- .../test_subtensor_integration.py | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index ee9371841..06d4563bc 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -173,21 +173,20 @@ def test_transfer_invalid_dest(self): def test_transfer_dest_as_bytes(self): fake_coldkey = get_mock_coldkey(1) - self.subtensor.do_transfer = MagicMock(return_value=(True, "0x", None)) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - - dest_as_bytes: bytes = Keypair(fake_coldkey).public_key - success = self.subtensor.transfer( - self.wallet, - dest_as_bytes, # invalid dest - amount=200, - wait_for_inclusion=True, - ) - self.assertTrue(success, msg="Transfer should succeed") + with patch("bittensor.core.extrinsics.transfer.do_transfer", return_value=(True, "0x", None)): + self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( + return_value=self.mock_neuron + ) + self.subtensor.get_balance = MagicMock(return_value=self.balance) + + dest_as_bytes: bytes = Keypair(fake_coldkey).public_key + success = self.subtensor.transfer( + self.wallet, + dest_as_bytes, # invalid dest + amount=200, + wait_for_inclusion=True, + ) + self.assertTrue(success, msg="Transfer should succeed") def test_set_weights(self): chain_weights = [0] From 38755e7b1927486321380fe8d9349d33c2d5b58c Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 6 Sep 2024 15:18:58 -0700 Subject: [PATCH 175/260] ruff --- tests/integration_tests/test_subtensor_integration.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 06d4563bc..44805fd42 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -173,7 +173,10 @@ def test_transfer_invalid_dest(self): def test_transfer_dest_as_bytes(self): fake_coldkey = get_mock_coldkey(1) - with patch("bittensor.core.extrinsics.transfer.do_transfer", return_value=(True, "0x", None)): + with patch( + "bittensor.core.extrinsics.transfer.do_transfer", + return_value=(True, "0x", None), + ): self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( return_value=self.mock_neuron ) From 177339159256dd1651125879445005cb352490f6 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 9 Sep 2024 12:13:49 -0700 Subject: [PATCH 176/260] comments fixes --- bittensor/core/extrinsics/commit_weights.py | 2 +- tests/e2e_tests/test_incentive.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 73c452acd..9047e1f94 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -104,7 +104,7 @@ def commit_weights_extrinsic( ) -> Tuple[bool, str]: """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. - This function is a wrapper around the `_do_commit_weights` method, handling user prompts and error messages. + This function is a wrapper around the `do_commit_weights` method, handling user prompts and error messages. Args: subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py index ee02727de..355bf4407 100644 --- a/tests/e2e_tests/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -3,7 +3,6 @@ import pytest -# import bittensor from bittensor import Subtensor, logging from tests.e2e_tests.utils.chain_interactions import ( add_stake, From 3ef3573ccf54cf859755cffcfdafe1a733dca179 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 9 Sep 2024 12:21:28 -0700 Subject: [PATCH 177/260] fix `bittensor/core/subtensor.py:445: error: Argument "logger" to "retry" has incompatible type "LoggingMachine"; expected "Logger | None" [arg-type]` --- bittensor/utils/btlogging/loggingmachine.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index 03ae23720..f72f0f7c0 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -28,6 +28,7 @@ import multiprocessing as mp import os import sys +from logging import Logger from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler from typing import NamedTuple @@ -55,7 +56,7 @@ class LoggingConfig(NamedTuple): logging_dir: str -class LoggingMachine(StateMachine): +class LoggingMachine(StateMachine, Logger): """Handles logger states for bittensor and 3rd party libraries.""" Default = State(initial=True) From 50f1485315cd5a41b2f0f65aa4f34409bd7cfcaf Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 9 Sep 2024 16:15:23 -0700 Subject: [PATCH 178/260] integrate `bt_decode` into BTSDK --- bittensor/core/chain_data/neuron_info.py | 106 ++++++++----- bittensor/core/chain_data/neuron_info_lite.py | 144 +++++++++--------- .../core/chain_data/subnet_hyperparameters.py | 43 ++++-- bittensor/core/chain_data/utils.py | 17 +++ requirements/prod.txt | 1 + 5 files changed, 188 insertions(+), 123 deletions(-) diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py index 160e21392..582cd26a4 100644 --- a/bittensor/core/chain_data/neuron_info.py +++ b/bittensor/core/chain_data/neuron_info.py @@ -1,11 +1,13 @@ from dataclasses import dataclass -from typing import List, Tuple, Dict, Optional, Any, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING +import bt_decode +import netaddr from scalecodec.utils.ss58 import ss58_encode from bittensor.core.chain_data.axon_info import AxonInfo from bittensor.core.chain_data.prometheus_info import PrometheusInfo -from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.core.chain_data.utils import decode_account_id, process_stake_data from bittensor.core.settings import SS58_FORMAT from bittensor.utils import RAOPERTAO, u16_normalized_float from bittensor.utils.balance import Balance @@ -24,7 +26,8 @@ class NeuronInfo: netuid: int active: int stake: Balance - stake_dict: Dict[str, Balance] + # mapping of coldkey to amount staked to this Neuron + stake_dict: dict[str, Balance] total_stake: Balance rank: float emission: float @@ -35,8 +38,8 @@ class NeuronInfo: dividends: float last_update: int validator_permit: bool - weights: List[List[int]] - bonds: List[List[int]] + weights: list[list[int]] + bonds: list[list[int]] pruning_score: int prometheus_info: Optional["PrometheusInfo"] = None axon_info: Optional[AxonInfo] = None @@ -91,31 +94,17 @@ def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfo": return cls(**neuron_info_decoded) @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> "NeuronInfo": - """Returns a NeuronInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return NeuronInfo.get_null_neuron() - - decoded = from_scale_encoding(vec_u8, ChainDataType.NeuronInfo) - if decoded is None: - return NeuronInfo.get_null_neuron() - - return NeuronInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["NeuronInfo"]: - """Returns a list of NeuronInfo objects from a ``vec_u8``""" - - decoded_list = from_scale_encoding( - vec_u8, ChainDataType.NeuronInfo, is_vec=True - ) - if decoded_list is None: - return [] + def from_weights_bonds_and_neuron_lite( + cls, + neuron_lite: "NeuronInfoLite", + weights_as_dict: dict[int, list[tuple[int, int]]], + bonds_as_dict: dict[int, list[tuple[int, int]]], + ) -> "NeuronInfo": + n_dict = neuron_lite.__dict__ + n_dict["weights"] = weights_as_dict.get(neuron_lite.uid, []) + n_dict["bonds"] = bonds_as_dict.get(neuron_lite.uid, []) - decoded_list = [ - NeuronInfo.fix_decoded_values(decoded) for decoded in decoded_list - ] - return decoded_list + return cls(**n_dict) @staticmethod def get_null_neuron() -> "NeuronInfo": @@ -147,14 +136,51 @@ def get_null_neuron() -> "NeuronInfo": return neuron @classmethod - def from_weights_bonds_and_neuron_lite( - cls, - neuron_lite: "NeuronInfoLite", - weights_as_dict: Dict[int, List[Tuple[int, int]]], - bonds_as_dict: Dict[int, List[Tuple[int, int]]], - ) -> "NeuronInfo": - n_dict = neuron_lite.__dict__ - n_dict["weights"] = weights_as_dict.get(neuron_lite.uid, []) - n_dict["bonds"] = bonds_as_dict.get(neuron_lite.uid, []) - - return cls(**n_dict) + def from_vec_u8(cls, vec_u8: bytes) -> "NeuronInfo": + n = bt_decode.NeuronInfo.decode(bytes(vec_u8)) + stake_dict = process_stake_data(n.stake) + total_stake = sum(stake_dict.values()) if stake_dict else Balance(0) + axon_info = n.axon_info + coldkey = decode_account_id(n.coldkey) + hotkey = decode_account_id(n.hotkey) + return NeuronInfo( + hotkey=hotkey, + coldkey=coldkey, + uid=n.uid, + netuid=n.netuid, + active=n.active, + stake=total_stake, + stake_dict=stake_dict, + total_stake=total_stake, + rank=u16_normalized_float(n.rank), + emission=n.emission / 1e9, + incentive=u16_normalized_float(n.incentive), + consensus=u16_normalized_float(n.consensus), + trust=u16_normalized_float(n.trust), + validator_trust=u16_normalized_float(n.validator_trust), + dividends=u16_normalized_float(n.dividends), + last_update=n.last_update, + validator_permit=n.validator_permit, + weights=[[e[0], e[1]] for e in n.weights], + bonds=[[e[0], e[1]] for e in n.bonds], + pruning_score=n.pruning_score, + prometheus_info=PrometheusInfo( + block=n.prometheus_info.block, + version=n.prometheus_info.version, + ip=str(netaddr.IPAddress(n.prometheus_info.ip)), + port=n.prometheus_info.port, + ip_type=n.prometheus_info.ip_type, + ), + axon_info=AxonInfo( + version=axon_info.version, + ip=str(netaddr.IPAddress(axon_info.ip)), + port=axon_info.port, + ip_type=axon_info.ip_type, + placeholder1=axon_info.placeholder1, + placeholder2=axon_info.placeholder2, + protocol=axon_info.protocol, + hotkey=hotkey, + coldkey=coldkey, + ), + is_null=False, + ) diff --git a/bittensor/core/chain_data/neuron_info_lite.py b/bittensor/core/chain_data/neuron_info_lite.py index a7d0f2252..0f7a4a015 100644 --- a/bittensor/core/chain_data/neuron_info_lite.py +++ b/bittensor/core/chain_data/neuron_info_lite.py @@ -1,13 +1,13 @@ from dataclasses import dataclass -from typing import List, Dict, Optional, Any +from typing import Dict, Optional -from scalecodec.utils.ss58 import ss58_encode +import bt_decode +import netaddr from bittensor.core.chain_data.axon_info import AxonInfo from bittensor.core.chain_data.prometheus_info import PrometheusInfo -from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType -from bittensor.core.settings import SS58_FORMAT -from bittensor.utils import RAOPERTAO, u16_normalized_float +from bittensor.core.chain_data.utils import decode_account_id, process_stake_data +from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance @@ -38,74 +38,6 @@ class NeuronInfoLite: pruning_score: int is_null: bool = False - @classmethod - def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfoLite": - """Fixes the values of the NeuronInfoLite object.""" - neuron_info_decoded["hotkey"] = ss58_encode( - neuron_info_decoded["hotkey"], SS58_FORMAT - ) - neuron_info_decoded["coldkey"] = ss58_encode( - neuron_info_decoded["coldkey"], SS58_FORMAT - ) - stake_dict = { - ss58_encode(coldkey, SS58_FORMAT): Balance.from_rao(int(stake)) - for coldkey, stake in neuron_info_decoded["stake"] - } - neuron_info_decoded["stake_dict"] = stake_dict - neuron_info_decoded["stake"] = sum(stake_dict.values()) - neuron_info_decoded["total_stake"] = neuron_info_decoded["stake"] - neuron_info_decoded["rank"] = u16_normalized_float(neuron_info_decoded["rank"]) - neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO - neuron_info_decoded["incentive"] = u16_normalized_float( - neuron_info_decoded["incentive"] - ) - neuron_info_decoded["consensus"] = u16_normalized_float( - neuron_info_decoded["consensus"] - ) - neuron_info_decoded["trust"] = u16_normalized_float( - neuron_info_decoded["trust"] - ) - neuron_info_decoded["validator_trust"] = u16_normalized_float( - neuron_info_decoded["validator_trust"] - ) - neuron_info_decoded["dividends"] = u16_normalized_float( - neuron_info_decoded["dividends"] - ) - neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values( - neuron_info_decoded["prometheus_info"] - ) - neuron_info_decoded["axon_info"] = AxonInfo.from_neuron_info( - neuron_info_decoded - ) - return cls(**neuron_info_decoded) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> "NeuronInfoLite": - """Returns a NeuronInfoLite object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return NeuronInfoLite.get_null_neuron() - - decoded = from_scale_encoding(vec_u8, ChainDataType.NeuronInfoLite) - if decoded is None: - return NeuronInfoLite.get_null_neuron() - - return NeuronInfoLite.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["NeuronInfoLite"]: - """Returns a list of NeuronInfoLite objects from a ``vec_u8``.""" - - decoded_list = from_scale_encoding( - vec_u8, ChainDataType.NeuronInfoLite, is_vec=True - ) - if decoded_list is None: - return [] - - decoded_list = [ - NeuronInfoLite.fix_decoded_values(decoded) for decoded in decoded_list - ] - return decoded_list - @staticmethod def get_null_neuron() -> "NeuronInfoLite": neuron = NeuronInfoLite( @@ -132,3 +64,69 @@ def get_null_neuron() -> "NeuronInfoLite": pruning_score=0, ) return neuron + + @classmethod + def list_from_vec_u8(cls, vec_u8: bytes) -> list["NeuronInfoLite"]: + decoded = bt_decode.NeuronInfoLite.decode_vec(vec_u8) + results = [] + for item in decoded: + active = item.active + axon_info = item.axon_info + coldkey = decode_account_id(item.coldkey) + consensus = item.consensus + dividends = item.dividends + emission = item.emission + hotkey = decode_account_id(item.hotkey) + incentive = item.incentive + last_update = item.last_update + netuid = item.netuid + prometheus_info = item.prometheus_info + pruning_score = item.pruning_score + rank = item.rank + stake_dict = process_stake_data(item.stake) + stake = sum(stake_dict.values()) if stake_dict else Balance(0) + trust = item.trust + uid = item.uid + validator_permit = item.validator_permit + validator_trust = item.validator_trust + results.append( + NeuronInfoLite( + active=active, + axon_info=AxonInfo( + version=axon_info.version, + ip=str(netaddr.IPAddress(axon_info.ip)), + port=axon_info.port, + ip_type=axon_info.ip_type, + placeholder1=axon_info.placeholder1, + placeholder2=axon_info.placeholder2, + protocol=axon_info.protocol, + hotkey=hotkey, + coldkey=coldkey, + ), + coldkey=coldkey, + consensus=u16_normalized_float(consensus), + dividends=u16_normalized_float(dividends), + emission=emission / 1e9, + hotkey=hotkey, + incentive=u16_normalized_float(incentive), + last_update=last_update, + netuid=netuid, + prometheus_info=PrometheusInfo( + version=prometheus_info.version, + ip=str(netaddr.IPAddress(prometheus_info.ip)), + port=prometheus_info.port, + ip_type=prometheus_info.ip_type, + block=prometheus_info.block, + ), + pruning_score=pruning_score, + rank=u16_normalized_float(rank), + stake_dict=stake_dict, + stake=stake, + total_stake=stake, + trust=u16_normalized_float(trust), + uid=uid, + validator_permit=validator_permit, + validator_trust=u16_normalized_float(validator_trust), + ) + ) + return results diff --git a/bittensor/core/chain_data/subnet_hyperparameters.py b/bittensor/core/chain_data/subnet_hyperparameters.py index fe0581838..60872d72b 100644 --- a/bittensor/core/chain_data/subnet_hyperparameters.py +++ b/bittensor/core/chain_data/subnet_hyperparameters.py @@ -1,6 +1,8 @@ from dataclasses import dataclass from typing import List, Dict, Optional, Any, Union +import bt_decode + from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType from bittensor.utils.registration import torch, use_torch @@ -38,16 +40,37 @@ class SubnetHyperparameters: liquid_alpha_enabled: bool @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetHyperparameters"]: - """Returns a SubnetHyperparameters object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetHyperparameters) - if decoded is None: - return None - - return SubnetHyperparameters.fix_decoded_values(decoded) + def from_vec_u8(cls, vec_u8: bytes) -> Optional["SubnetHyperparameters"]: + decoded = bt_decode.SubnetHyperparameters.decode(vec_u8) + return SubnetHyperparameters( + rho=decoded.rho, + kappa=decoded.kappa, + immunity_period=decoded.immunity_period, + min_allowed_weights=decoded.min_allowed_weights, + max_weight_limit=decoded.max_weights_limit, + tempo=decoded.tempo, + min_difficulty=decoded.min_difficulty, + max_difficulty=decoded.max_difficulty, + weights_version=decoded.weights_version, + weights_rate_limit=decoded.weights_rate_limit, + adjustment_interval=decoded.adjustment_interval, + activity_cutoff=decoded.activity_cutoff, + registration_allowed=decoded.registration_allowed, + target_regs_per_interval=decoded.target_regs_per_interval, + min_burn=decoded.min_burn, + max_burn=decoded.max_burn, + bonds_moving_avg=decoded.bonds_moving_avg, + max_regs_per_block=decoded.max_regs_per_block, + serving_rate_limit=decoded.serving_rate_limit, + max_validators=decoded.max_validators, + adjustment_alpha=decoded.adjustment_alpha, + difficulty=decoded.difficulty, + commit_reveal_weights_interval=decoded.commit_reveal_weights_interval, + commit_reveal_weights_enabled=decoded.commit_reveal_weights_enabled, + alpha_high=decoded.alpha_high, + alpha_low=decoded.alpha_low, + liquid_alpha_enabled=decoded.liquid_alpha_enabled, + ) @classmethod def list_from_vec_u8(cls, vec_u8: List[int]) -> List["SubnetHyperparameters"]: diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index f5e1de813..8d3276571 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -5,6 +5,10 @@ from scalecodec.base import RuntimeConfiguration, ScaleBytes from scalecodec.type_registry import load_type_registry_preset +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils.balance import Balance class ChainDataType(Enum): @@ -241,3 +245,16 @@ def from_scale_encoding_using_type_string( }, } } + + +def decode_account_id(account_id_bytes): + # Convert the AccountId bytes to a Base64 string + return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) + + +def process_stake_data(stake_data): + decoded_stake_data = {} + for account_id_bytes, stake_ in stake_data: + account_id = decode_account_id(account_id_bytes) + decoded_stake_data.update({account_id: Balance.from_rao(stake_)}) + return decoded_stake_data diff --git a/requirements/prod.txt b/requirements/prod.txt index bcfb5bcf6..fab144bf7 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,6 +1,7 @@ wheel setuptools~=70.0.0 aiohttp~=3.9 +bt-decode colorama~=0.4.6 fastapi~=0.110.1 munch~=2.5.0 From 2a92b69436b0c258f7138bbb72c7124470f5c273 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 9 Sep 2024 16:28:57 -0700 Subject: [PATCH 179/260] fix logger linter checker --- bittensor/utils/btlogging/loggingmachine.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index 03ae23720..f72f0f7c0 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -28,6 +28,7 @@ import multiprocessing as mp import os import sys +from logging import Logger from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler from typing import NamedTuple @@ -55,7 +56,7 @@ class LoggingConfig(NamedTuple): logging_dir: str -class LoggingMachine(StateMachine): +class LoggingMachine(StateMachine, Logger): """Handles logger states for bittensor and 3rd party libraries.""" Default = State(initial=True) From 2fa53722e9f9ece2d12fafd0937926bd67f7e295 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 9 Sep 2024 17:00:34 -0700 Subject: [PATCH 180/260] Reverts logging enhancement --- bittensor/utils/btlogging/loggingmachine.py | 45 ++++++++------------- tests/unit_tests/test_logging.py | 34 ---------------- 2 files changed, 16 insertions(+), 63 deletions(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index f72f0f7c0..3fd323c74 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -361,58 +361,45 @@ def __trace_on__(self) -> bool: """ return self.current_state_value == "Trace" - @staticmethod - def _concat_msg(*args): - return " - ".join(str(el) for el in args if el != "") - def trace(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps trace message with prefix and suffix.""" - msg = self._concat_msg(prefix, msg, suffix) - self._logger.trace(msg, *args, **kwargs, stacklevel=2) + msg = f"{prefix} - {msg} - {suffix}" + self._logger.trace(msg, *args, **kwargs) def debug(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps debug message with prefix and suffix.""" - msg = self._concat_msg(prefix, msg, suffix) - self._logger.debug(msg, *args, **kwargs, stacklevel=2) + msg = f"{prefix} - {msg} - {suffix}" + self._logger.debug(msg, *args, **kwargs) def info(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps info message with prefix and suffix.""" - msg = self._concat_msg(prefix, msg, suffix) - self._logger.info(msg, *args, **kwargs, stacklevel=2) + msg = f"{prefix} - {msg} - {suffix}" + self._logger.info(msg, *args, **kwargs) def success(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps success message with prefix and suffix.""" - msg = self._concat_msg(prefix, msg, suffix) - self._logger.success(msg, *args, **kwargs, stacklevel=2) + msg = f"{prefix} - {msg} - {suffix}" + self._logger.success(msg, *args, **kwargs) def warning(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps warning message with prefix and suffix.""" - msg = self._concat_msg(prefix, msg, suffix) - self._logger.warning(msg, *args, **kwargs, stacklevel=2) + msg = f"{prefix} - {msg} - {suffix}" + self._logger.warning(msg, *args, **kwargs) def error(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps error message with prefix and suffix.""" - msg = self._concat_msg(prefix, msg, suffix) - self._logger.error(msg, *args, **kwargs, stacklevel=2) + msg = f"{prefix} - {msg} - {suffix}" + self._logger.error(msg, *args, **kwargs) def critical(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps critical message with prefix and suffix.""" - msg = self._concat_msg(prefix, msg, suffix) - self._logger.critical(msg, *args, **kwargs, stacklevel=2) + msg = f"{prefix} - {msg} - {suffix}" + self._logger.critical(msg, *args, **kwargs) def exception(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps exception message with prefix and suffix.""" - msg = self._concat_msg(prefix, msg, suffix) - stacklevel = 2 - if ( - sys.implementation.name == "cpython" - and sys.version_info.major == 3 - and sys.version_info.minor < 11 - ): - # Note that, on CPython < 3.11, exception() calls through to - # error() without adjusting stacklevel, so we have to increment it. - stacklevel += 1 - self._logger.exception(msg, *args, **kwargs, stacklevel=stacklevel) + msg = f"{prefix} - {msg} - {suffix}" + self._logger.exception(msg, *args, **kwargs) def on(self): """Enable default state.""" diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 24ab40507..6bd7b6acd 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -1,7 +1,5 @@ import logging as stdlogging import multiprocessing -import os -import re from unittest.mock import MagicMock, patch import pytest @@ -177,35 +175,3 @@ def test_all_log_levels_output(logging_machine, caplog): assert "Test warning" in caplog.text assert "Test error" in caplog.text assert "Test critical" in caplog.text - - -def test_log_sanity(logging_machine, caplog): - """ - Test that logging is sane: - - prefix and suffix work - - format strings work - - reported filename is correct - Note that this is tested against caplog, which is not formatted the same as - stdout. - """ - basemsg = "logmsg #%d, cookie: %s" - cookie = "0ef852c74c777f8d8cc09d511323ce76" - nfixtests = [ - {}, - {"prefix": "pref"}, - {"suffix": "suff"}, - {"prefix": "pref", "suffix": "suff"}, - ] - for i, nfix in enumerate(nfixtests): - prefix = nfix.get("prefix", "") - suffix = nfix.get("suffix", "") - use_cookie = f"{cookie} #{i}#" - logging_machine.info(basemsg, prefix, suffix, i, use_cookie) - # Check to see if all elements are present, regardless of downstream formatting. - expect = f"INFO.*{os.path.basename(__file__)}.* " - if prefix != "": - expect += prefix + " - " - expect += basemsg % (i, use_cookie) - if suffix != "": - expect += " - " + suffix - assert re.search(expect, caplog.text) From 6d6784b76e2a793d21d69a74fe682e042bfa5cc1 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 11 Sep 2024 12:04:10 -0700 Subject: [PATCH 181/260] remove unused code and tests --- bittensor/core/chain_data/neuron_info.py | 48 ------ .../core/chain_data/subnet_hyperparameters.py | 63 ------- tests/unit_tests/test_chain_data.py | 160 ------------------ 3 files changed, 271 deletions(-) diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py index 582cd26a4..9e2af1350 100644 --- a/bittensor/core/chain_data/neuron_info.py +++ b/bittensor/core/chain_data/neuron_info.py @@ -45,54 +45,6 @@ class NeuronInfo: axon_info: Optional[AxonInfo] = None is_null: bool = False - @classmethod - def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfo": - """Fixes the values of the NeuronInfo object.""" - neuron_info_decoded["hotkey"] = ss58_encode( - neuron_info_decoded["hotkey"], SS58_FORMAT - ) - neuron_info_decoded["coldkey"] = ss58_encode( - neuron_info_decoded["coldkey"], SS58_FORMAT - ) - stake_dict = { - ss58_encode(coldkey, SS58_FORMAT): Balance.from_rao(int(stake)) - for coldkey, stake in neuron_info_decoded["stake"] - } - neuron_info_decoded["stake_dict"] = stake_dict - neuron_info_decoded["stake"] = sum(stake_dict.values()) - neuron_info_decoded["total_stake"] = neuron_info_decoded["stake"] - neuron_info_decoded["weights"] = [ - [int(weight[0]), int(weight[1])] - for weight in neuron_info_decoded["weights"] - ] - neuron_info_decoded["bonds"] = [ - [int(bond[0]), int(bond[1])] for bond in neuron_info_decoded["bonds"] - ] - neuron_info_decoded["rank"] = u16_normalized_float(neuron_info_decoded["rank"]) - neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO - neuron_info_decoded["incentive"] = u16_normalized_float( - neuron_info_decoded["incentive"] - ) - neuron_info_decoded["consensus"] = u16_normalized_float( - neuron_info_decoded["consensus"] - ) - neuron_info_decoded["trust"] = u16_normalized_float( - neuron_info_decoded["trust"] - ) - neuron_info_decoded["validator_trust"] = u16_normalized_float( - neuron_info_decoded["validator_trust"] - ) - neuron_info_decoded["dividends"] = u16_normalized_float( - neuron_info_decoded["dividends"] - ) - neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values( - neuron_info_decoded["prometheus_info"] - ) - neuron_info_decoded["axon_info"] = AxonInfo.from_neuron_info( - neuron_info_decoded - ) - return cls(**neuron_info_decoded) - @classmethod def from_weights_bonds_and_neuron_lite( cls, diff --git a/bittensor/core/chain_data/subnet_hyperparameters.py b/bittensor/core/chain_data/subnet_hyperparameters.py index 60872d72b..e7a3b69ba 100644 --- a/bittensor/core/chain_data/subnet_hyperparameters.py +++ b/bittensor/core/chain_data/subnet_hyperparameters.py @@ -71,66 +71,3 @@ def from_vec_u8(cls, vec_u8: bytes) -> Optional["SubnetHyperparameters"]: alpha_low=decoded.alpha_low, liquid_alpha_enabled=decoded.liquid_alpha_enabled, ) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["SubnetHyperparameters"]: - """Returns a list of SubnetHyperparameters objects from a ``vec_u8``.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.SubnetHyperparameters, is_vec=True, is_option=True - ) - if decoded is None: - return [] - - return [SubnetHyperparameters.fix_decoded_values(d) for d in decoded] - - @classmethod - def fix_decoded_values(cls, decoded: Dict) -> "SubnetHyperparameters": - """Returns a SubnetInfo object from a decoded SubnetInfo dictionary.""" - return SubnetHyperparameters( - rho=decoded["rho"], - kappa=decoded["kappa"], - immunity_period=decoded["immunity_period"], - min_allowed_weights=decoded["min_allowed_weights"], - max_weight_limit=decoded["max_weights_limit"], - tempo=decoded["tempo"], - min_difficulty=decoded["min_difficulty"], - max_difficulty=decoded["max_difficulty"], - weights_version=decoded["weights_version"], - weights_rate_limit=decoded["weights_rate_limit"], - adjustment_interval=decoded["adjustment_interval"], - activity_cutoff=decoded["activity_cutoff"], - registration_allowed=decoded["registration_allowed"], - target_regs_per_interval=decoded["target_regs_per_interval"], - min_burn=decoded["min_burn"], - max_burn=decoded["max_burn"], - max_regs_per_block=decoded["max_regs_per_block"], - max_validators=decoded["max_validators"], - serving_rate_limit=decoded["serving_rate_limit"], - bonds_moving_avg=decoded["bonds_moving_avg"], - adjustment_alpha=decoded["adjustment_alpha"], - difficulty=decoded["difficulty"], - commit_reveal_weights_interval=decoded["commit_reveal_weights_interval"], - commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], - alpha_high=decoded["alpha_high"], - alpha_low=decoded["alpha_low"], - liquid_alpha_enabled=decoded["liquid_alpha_enabled"], - ) - - def to_parameter_dict( - self, - ) -> Union[dict[str, Union[int, float, bool]], "torch.nn.ParameterDict"]: - """Returns a torch tensor or dict of the subnet hyperparameters.""" - if use_torch(): - return torch.nn.ParameterDict(self.__dict__) - else: - return self.__dict__ - - @classmethod - def from_parameter_dict( - cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] - ) -> "SubnetHyperparameters": - """Creates a SubnetHyperparameters instance from a parameter dictionary.""" - if use_torch(): - return cls(**dict(parameter_dict)) - else: - return cls(**parameter_dict) diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index f5f0ed40e..aea6716ed 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -366,166 +366,6 @@ def create_neuron_info_decoded( } -@pytest.mark.parametrize( - "test_id, neuron_info_decoded,", - [ - ( - "happy-path-1", - create_neuron_info_decoded( - hotkey=b"\x01" * 32, - coldkey=b"\x02" * 32, - stake=[(b"\x02" * 32, 1000)], - weights=[(1, 2)], - bonds=[(3, 4)], - rank=100, - emission=1000, - incentive=200, - consensus=300, - trust=400, - validator_trust=500, - dividends=600, - uid=1, - netuid=2, - active=True, - last_update=1000, - validator_permit=100, - pruning_score=1000, - prometheus_info={ - "version": 1, - "ip": 2130706433, - "port": 8080, - "ip_type": 4, - "block": 100, - }, - axon_info={ - "version": 1, - "ip": 2130706433, - "port": 8080, - "ip_type": 4, - }, - ), - ), - ], -) -def test_fix_decoded_values_happy_path(test_id, neuron_info_decoded): - # Act - result = NeuronInfo.fix_decoded_values(neuron_info_decoded) - - # Assert - assert result.hotkey == neuron_info_decoded["hotkey"], f"Test case: {test_id}" - assert result.coldkey == neuron_info_decoded["coldkey"], f"Test case: {test_id}" - assert result.stake == neuron_info_decoded["stake"], f"Test case: {test_id}" - assert result.weights == neuron_info_decoded["weights"], f"Test case: {test_id}" - assert result.bonds == neuron_info_decoded["bonds"], f"Test case: {test_id}" - assert result.rank == neuron_info_decoded["rank"], f"Test case: {test_id}" - assert result.emission == neuron_info_decoded["emission"], f"Test case: {test_id}" - assert result.incentive == neuron_info_decoded["incentive"], f"Test case: {test_id}" - assert result.consensus == neuron_info_decoded["consensus"], f"Test case: {test_id}" - assert result.trust == neuron_info_decoded["trust"], f"Test case: {test_id}" - assert ( - result.validator_trust == neuron_info_decoded["validator_trust"] - ), f"Test case: {test_id}" - assert result.dividends == neuron_info_decoded["dividends"], f"Test case: {test_id}" - assert result.uid == neuron_info_decoded["uid"], f"Test case: {test_id}" - assert result.netuid == neuron_info_decoded["netuid"], f"Test case: {test_id}" - assert result.active == neuron_info_decoded["active"], f"Test case: {test_id}" - assert ( - result.last_update == neuron_info_decoded["last_update"] - ), f"Test case: {test_id}" - - -@pytest.mark.parametrize( - "test_id, neuron_info_decoded", - [ - ( - "edge-1", - create_neuron_info_decoded( - hotkey=b"\x01" * 32, - coldkey=b"\x02" * 32, - stake=[], - weights=[(1, 2)], - bonds=[(3, 4)], - rank=100, - emission=1000, - incentive=200, - consensus=300, - trust=400, - validator_trust=500, - dividends=600, - uid=1, - netuid=2, - active=True, - last_update=1000, - validator_permit=100, - pruning_score=1000, - prometheus_info={ - "version": 1, - "ip": 2130706433, - "port": 8080, - "ip_type": 4, - "block": 100, - }, - axon_info={ - "version": 1, - "ip": 2130706433, - "port": 8080, - "ip_type": 4, - }, - ), - ), - ], -) -def test_fix_decoded_values_edge_cases(test_id, neuron_info_decoded): - # Act - result = NeuronInfo.fix_decoded_values(neuron_info_decoded) - - # Assert - assert result.stake == 0, f"Test case: {test_id}" - assert result.weights == neuron_info_decoded["weights"], f"Test case: {test_id}" - - -@pytest.mark.parametrize( - "test_id, neuron_info_decoded, expected_exception", - [ - ( - "error-1", - create_neuron_info_decoded( - hotkey="not_bytes", - coldkey=b"\x02" * 32, - stake=[(b"\x02" * 32, 1000)], - weights=[(1, 2)], - bonds=[(3, 4)], - rank=100, - emission=1000, - incentive=200, - consensus=300, - trust=400, - validator_trust=500, - dividends=600, - uid=1, - netuid=2, - active=True, - last_update=1000, - validator_permit=100, - pruning_score=1000, - prometheus_info={}, - axon_info={}, - ), - ValueError, - ), - ], -) -def test_fix_decoded_values_error_cases( - test_id, neuron_info_decoded, expected_exception -): - # Arrange - # (Omitted since all input values are provided via test parameters) - - # Act / Assert - with pytest.raises(expected_exception): - NeuronInfo.fix_decoded_values(neuron_info_decoded), f"Test case: {test_id}" - - @pytest.fixture def mock_from_scale_encoding(mocker): return mocker.patch("bittensor.core.chain_data.delegate_info.from_scale_encoding") From 69eca678ab76d67d2292bdb2e48c14b63f4b2a9e Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 11 Sep 2024 12:16:47 -0700 Subject: [PATCH 182/260] remove unused imports --- bittensor/core/chain_data/neuron_info.py | 6 ++---- bittensor/core/chain_data/subnet_hyperparameters.py | 5 +---- tests/unit_tests/test_chain_data.py | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py index 9e2af1350..c31862d42 100644 --- a/bittensor/core/chain_data/neuron_info.py +++ b/bittensor/core/chain_data/neuron_info.py @@ -1,15 +1,13 @@ from dataclasses import dataclass -from typing import Any, Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING import bt_decode import netaddr -from scalecodec.utils.ss58 import ss58_encode from bittensor.core.chain_data.axon_info import AxonInfo from bittensor.core.chain_data.prometheus_info import PrometheusInfo from bittensor.core.chain_data.utils import decode_account_id, process_stake_data -from bittensor.core.settings import SS58_FORMAT -from bittensor.utils import RAOPERTAO, u16_normalized_float +from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance if TYPE_CHECKING: diff --git a/bittensor/core/chain_data/subnet_hyperparameters.py b/bittensor/core/chain_data/subnet_hyperparameters.py index e7a3b69ba..2df8d53c2 100644 --- a/bittensor/core/chain_data/subnet_hyperparameters.py +++ b/bittensor/core/chain_data/subnet_hyperparameters.py @@ -1,11 +1,8 @@ from dataclasses import dataclass -from typing import List, Dict, Optional, Any, Union +from typing import Optional import bt_decode -from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType -from bittensor.utils.registration import torch, use_torch - @dataclass class SubnetHyperparameters: diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index aea6716ed..353f697d4 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -18,7 +18,7 @@ import pytest import torch -from bittensor.core.chain_data import AxonInfo, DelegateInfo, NeuronInfo +from bittensor.core.chain_data import AxonInfo, DelegateInfo from bittensor.core.chain_data.utils import ChainDataType RAOPERTAO = 10**18 From e9c90909ea7f188842659eb814e7d9595586400f Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 12 Sep 2024 19:14:46 -0700 Subject: [PATCH 183/260] modification of error formatter, moved the submit_extrinsic call to utils.py, fixed the subtensor.py regarding extrinsics calls, fixed tests --- bittensor/core/extrinsics/commit_weights.py | 15 +++-- bittensor/core/extrinsics/prometheus.py | 9 ++- bittensor/core/extrinsics/serving.py | 8 ++- bittensor/core/extrinsics/set_weights.py | 10 +-- bittensor/core/extrinsics/transfer.py | 8 ++- bittensor/core/extrinsics/utils.py | 43 ++++++++++++ bittensor/core/subtensor.py | 3 + bittensor/utils/__init__.py | 64 +++++++++++++++--- tests/unit_tests/extrinsics/test_init.py | 65 +++++++++++++++++++ .../unit_tests/extrinsics/test_set_weights.py | 2 +- 10 files changed, 197 insertions(+), 30 deletions(-) create mode 100644 bittensor/core/extrinsics/utils.py diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 9047e1f94..a682396d9 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -22,6 +22,7 @@ from retry import retry from rich.prompt import Confirm +from bittensor.core.extrinsics.utils import submit_extrinsic from bittensor.utils import format_error_message from bittensor.utils.btlogging import logging from bittensor.utils.networking import ensure_connected @@ -61,7 +62,7 @@ def do_commit_weights( verifiable record of the neuron's weight distribution at a specific point in time. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + @retry(delay=1, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -75,8 +76,9 @@ def make_substrate_call_with_retry(): call=call, keypair=wallet.hotkey, ) - response = self.substrate.submit_extrinsic( - extrinsic, + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -179,7 +181,7 @@ def do_reveal_weights( and accountability for the neuron's weight distribution. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + @retry(delay=1, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -196,8 +198,9 @@ def make_substrate_call_with_retry(): call=call, keypair=wallet.hotkey, ) - response = self.substrate.submit_extrinsic( - extrinsic, + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor/core/extrinsics/prometheus.py b/bittensor/core/extrinsics/prometheus.py index 9aa95a838..c4f7da0f6 100644 --- a/bittensor/core/extrinsics/prometheus.py +++ b/bittensor/core/extrinsics/prometheus.py @@ -17,8 +17,10 @@ import json from typing import Tuple, Optional, TYPE_CHECKING + from retry import retry +from bittensor.core.extrinsics.utils import submit_extrinsic from bittensor.core.settings import version_as_int, bt_console from bittensor.core.types import PrometheusServeCallParams from bittensor.utils import networking as net, format_error_message @@ -55,7 +57,7 @@ def do_serve_prometheus( error (:func:`Optional[str]`): Error message if serve prometheus failed, ``None`` otherwise. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + @retry(delay=1, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -65,8 +67,9 @@ def make_substrate_call_with_retry(): extrinsic = self.substrate.create_signed_extrinsic( call=call, keypair=wallet.hotkey ) - response = self.substrate.submit_extrinsic( - extrinsic, + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index c68dd27df..2ddbf09f9 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -23,6 +23,7 @@ from bittensor.core.axon import Axon from bittensor.core.errors import MetadataError +from bittensor.core.extrinsics.utils import submit_extrinsic from bittensor.core.settings import version_as_int, bt_console from bittensor.core.types import AxonServeCallParams from bittensor.utils import format_error_message, networking as net @@ -60,7 +61,7 @@ def do_serve_axon( This function is crucial for initializing and announcing a neuron's Axon service on the network, enhancing the decentralized computation capabilities of Bittensor. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + @retry(delay=1, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -70,8 +71,9 @@ def make_substrate_call_with_retry(): extrinsic = self.substrate.create_signed_extrinsic( call=call, keypair=wallet.hotkey ) - response = self.substrate.submit_extrinsic( - extrinsic, + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 86f993c53..63438da0b 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -23,6 +23,7 @@ from retry import retry from rich.prompt import Confirm +from bittensor.core.extrinsics.utils import submit_extrinsic from bittensor.core.settings import bt_console, version_as_int from bittensor.utils import format_error_message, weight_utils from bittensor.utils.btlogging import logging @@ -66,7 +67,7 @@ def do_set_weights( 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. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + @retry(delay=1, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="SubtensorModule", @@ -84,8 +85,9 @@ def make_substrate_call_with_retry(): keypair=wallet.hotkey, era={"period": 5}, ) - response = self.substrate.submit_extrinsic( - extrinsic, + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -188,5 +190,5 @@ def set_weights_extrinsic( except Exception as e: bt_console.print(f":cross_mark: [red]Failed[/red]: error:{e}") - logging.warning(str(e)) + logging.debug(str(e)) return False, str(e) diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 9d0e72d6b..9c41ab1f3 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -20,6 +20,7 @@ from retry import retry from rich.prompt import Confirm +from bittensor.core.extrinsics.utils import submit_extrinsic from bittensor.core.settings import bt_console, NETWORK_EXPLORER_MAP from bittensor.utils import ( get_explorer_url_for_network, @@ -62,7 +63,7 @@ def do_transfer( error (Dict): Error message from subtensor if transfer failed. """ - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + @retry(delay=1, tries=3, backoff=2, max_delay=4) def make_substrate_call_with_retry(): call = self.substrate.compose_call( call_module="Balances", @@ -72,8 +73,9 @@ def make_substrate_call_with_retry(): extrinsic = self.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey ) - response = self.substrate.submit_extrinsic( - extrinsic, + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py new file mode 100644 index 000000000..11783234d --- /dev/null +++ b/bittensor/core/extrinsics/utils.py @@ -0,0 +1,43 @@ +"""Module with helper functions for extrinsics.""" +from typing import TYPE_CHECKING +from substrateinterface.exceptions import SubstrateRequestException +from bittensor.utils.btlogging import logging +from bittensor.utils import format_error_message + +if TYPE_CHECKING: + from substrateinterface import SubstrateInterface + from scalecodec.types import GenericExtrinsic + + +def submit_extrinsic(substrate: "SubstrateInterface", extrinsic: "GenericExtrinsic", wait_for_inclusion: bool, wait_for_finalization: bool): + """ + 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: + substrate (substrateinterface.SubstrateInterface): The substrate interface 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: + response = 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], substrate=substrate)) + # Re-rise the exception for retrying of the extrinsic call. If we remove the retry logic, the raise will need + # to be removed. + raise + return response diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index fae927f07..c6af2fd6f 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -873,6 +873,9 @@ def set_weights( and retries < max_retries ): try: + logging.info( + f"Setting weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." + ) success, message = set_weights_extrinsic( subtensor=self, wallet=wallet, diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index d4fe49dde..cdb648f63 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -16,16 +16,20 @@ # DEALINGS IN THE SOFTWARE. import hashlib -from typing import List, Dict, Literal, Union, Optional +from typing import List, Dict, Literal, Union, Optional, TYPE_CHECKING import scalecodec -from substrateinterface import Keypair as Keypair +from substrateinterface import Keypair from substrateinterface.utils import ss58 from bittensor.core.settings import SS58_FORMAT +from bittensor.utils.btlogging import logging from .registration import torch, use_torch from .version import version_checking, check_version, VersionCheckError +if TYPE_CHECKING: + from substrateinterface import SubstrateInterface + RAOPERTAO = 1e9 U16_MAX = 65535 U64_MAX = 18446744073709551615 @@ -137,26 +141,66 @@ def get_hash(content, encoding="utf-8"): return sha3.hexdigest() -def format_error_message(error_message: dict) -> str: +def format_error_message( + error_message: dict, substrate: "SubstrateInterface" = None +) -> str: """ - Formats an error message from the Subtensor error information to using in extrinsics. + Formats an error message from the Subtensor error information for use in extrinsics. Args: error_message (dict): A dictionary containing the error information from Subtensor. + substrate (SubstrateInterface, optional): The substrate interface to use. Returns: str: A formatted error message string. """ - err_type = "UnknownType" err_name = "UnknownError" + err_type = "UnknownType" err_description = "Unknown Description" if isinstance(error_message, dict): - 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_docs[0] if len(err_docs) > 0 else err_description - return f"Subtensor returned `{err_name} ({err_type})` error. This means: `{err_description}`" + # subtensor error structure + if ( + error_message.get("code") + and error_message.get("message") + and error_message.get("data") + ): + err_name = "SubstrateRequestException" + err_type = error_message.get("message") + err_data = error_message.get("data") + + # subtensor custom error marker + if err_data.startswith("Custom error:") and substrate: + if not substrate.metadata: + substrate.get_metadata() + + if substrate.metadata: + try: + pallet = substrate.metadata.get_metadata_pallet( + "SubtensorModule" + ) + error_index = int(err_data.split("Custom error:")[-1]) + + error_dict = pallet.errors[error_index].value + err_type = error_dict.get("message", err_type) + err_docs = error_dict.get("docs", []) + err_description = err_docs[0] if err_docs else err_description + except Exception: + logging.error("Substrate pallets data unavailable.") + else: + err_description = err_data + + elif ( + error_message.get("type") + and error_message.get("name") + and error_message.get("docs") + ): + 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 + + return f"Subtensor returned `{err_name}({err_type})` error. This means: `{err_description}`." # Subnet 24 uses this function diff --git a/tests/unit_tests/extrinsics/test_init.py b/tests/unit_tests/extrinsics/test_init.py index 8e3caaf90..8a2480a9b 100644 --- a/tests/unit_tests/extrinsics/test_init.py +++ b/tests/unit_tests/extrinsics/test_init.py @@ -4,6 +4,7 @@ def test_format_error_message_with_right_error_message(): + """Verify that error message from extrinsic response parses correctly.""" # Prep fake_error_message = { "type": "SomeType", @@ -22,6 +23,7 @@ def test_format_error_message_with_right_error_message(): def test_format_error_message_with_empty_error_message(): + """Verify that empty error message from extrinsic response parses correctly.""" # Prep fake_error_message = {} @@ -36,6 +38,7 @@ def test_format_error_message_with_empty_error_message(): def test_format_error_message_with_wrong_type_error_message(): + """Verify that error message from extrinsic response with wrong type parses correctly.""" # Prep fake_error_message = None @@ -47,3 +50,65 @@ def test_format_error_message_with_wrong_type_error_message(): assert "UnknownType" in result assert "UnknownError" in result assert "Unknown Description" in result + + +def test_format_error_message_with_custom_error_message_with_index(mocker): + """Tests error formatter if subtensor error is custom error with index.""" + # Preps + fake_custom_error = { + "code": 1010, + "message": "SomeErrorName", + "data": "Custom error: 1", + } + fake_subtensor_error = { + "docs": ["Some description"], + "fields": [], + "index": 1, + "name": "SomeErrorName", + } + + fake_substrate = mocker.MagicMock() + fake_substrate.metadata.get_metadata_pallet().errors.__getitem__().value.get = ( + mocker.Mock( + side_effect=[fake_custom_error["message"], fake_subtensor_error["docs"]] + ) + ) + + mocker.patch( + "substrateinterface.base.SubstrateInterface", return_value=fake_substrate + ) + + # Call + result = format_error_message(fake_custom_error, fake_substrate) + + # Assertions + assert ( + result + == f"Subtensor returned `SubstrateRequestException({fake_subtensor_error['name']})` error. This means: `Some description`." + ) + + +def test_format_error_message_with_custom_error_message_without_index(mocker): + """Tests error formatter if subtensor error is custom error without index.""" + # Preps + fake_custom_error = { + "code": 1010, + "message": "SomeErrorType", + "data": "Custom error description", + } + fake_substrate = mocker.MagicMock() + fake_substrate.metadata.get_metadata_pallet().errors.__getitem__().value.get.return_value = fake_custom_error[ + "message" + ] + mocker.patch( + "substrateinterface.base.SubstrateInterface", return_value=fake_substrate + ) + + # Call + result = format_error_message(fake_custom_error, fake_substrate) + + # Assertions + assert ( + result + == f"Subtensor returned `SubstrateRequestException({fake_custom_error['message']})` error. This means: `{fake_custom_error['data']}`." + ) diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 4b49a512b..9c32fc9bd 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -61,7 +61,7 @@ def mock_wallet(): True, True, False, - "Subtensor returned `UnknownError (UnknownType)` error. This means: `Unknown Description`", + "Subtensor returned `UnknownError(UnknownType)` error. This means: `Unknown Description`.", ), ([1, 2], [0.5, 0.5], 0, True, True, True, False, False, "Prompt refused."), ], From 736358befdc722779b3fc5dd0144c66429cb6378 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 12 Sep 2024 19:23:44 -0700 Subject: [PATCH 184/260] ruff --- bittensor/core/extrinsics/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py index 11783234d..6c896372b 100644 --- a/bittensor/core/extrinsics/utils.py +++ b/bittensor/core/extrinsics/utils.py @@ -1,4 +1,5 @@ """Module with helper functions for extrinsics.""" + from typing import TYPE_CHECKING from substrateinterface.exceptions import SubstrateRequestException from bittensor.utils.btlogging import logging @@ -9,7 +10,12 @@ from scalecodec.types import GenericExtrinsic -def submit_extrinsic(substrate: "SubstrateInterface", extrinsic: "GenericExtrinsic", wait_for_inclusion: bool, wait_for_finalization: bool): +def submit_extrinsic( + substrate: "SubstrateInterface", + extrinsic: "GenericExtrinsic", + wait_for_inclusion: bool, + wait_for_finalization: bool, +): """ Submits an extrinsic to the substrate blockchain and handles potential exceptions. From 12680b1d7498f1b521912871cbad5368f236a7e1 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 12 Sep 2024 19:48:37 -0700 Subject: [PATCH 185/260] Improved logic for concatenating message, prefix, and suffix in bittensor logging + test --- bittensor/utils/btlogging/loggingmachine.py | 30 ++++++++++++--------- tests/unit_tests/test_logging.py | 22 ++++++++++++++- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index 3fd323c74..b8569648d 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -30,7 +30,7 @@ import sys from logging import Logger from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler -from typing import NamedTuple +from typing import NamedTuple, Optional from statemachine import State, StateMachine @@ -47,6 +47,12 @@ from .helpers import all_loggers +def _concat_message(msg: str, prefix: Optional[str] = None, suffix: Optional[str] = None): + """Concatenates a message with optional prefix and suffix.""" + msg = f"{f'{prefix} - ' if prefix else ''}{msg}{f' - {suffix}' if suffix else ''}" + return msg + + class LoggingConfig(NamedTuple): """Named tuple to hold the logging configuration.""" @@ -361,42 +367,42 @@ def __trace_on__(self) -> bool: """ return self.current_state_value == "Trace" - def trace(self, msg="", prefix="", suffix="", *args, **kwargs): + def trace(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): """Wraps trace message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" + msg = _concat_message(msg, prefix, suffix) self._logger.trace(msg, *args, **kwargs) - def debug(self, msg="", prefix="", suffix="", *args, **kwargs): + def debug(self, msg="", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): """Wraps debug message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" + msg = _concat_message(msg, prefix, suffix) self._logger.debug(msg, *args, **kwargs) - def info(self, msg="", prefix="", suffix="", *args, **kwargs): + def info(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): """Wraps info message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" + msg = _concat_message(msg, prefix, suffix) self._logger.info(msg, *args, **kwargs) - def success(self, msg="", prefix="", suffix="", *args, **kwargs): + def success(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): """Wraps success message with prefix and suffix.""" msg = f"{prefix} - {msg} - {suffix}" self._logger.success(msg, *args, **kwargs) - def warning(self, msg="", prefix="", suffix="", *args, **kwargs): + def warning(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): """Wraps warning message with prefix and suffix.""" msg = f"{prefix} - {msg} - {suffix}" self._logger.warning(msg, *args, **kwargs) - def error(self, msg="", prefix="", suffix="", *args, **kwargs): + def error(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): """Wraps error message with prefix and suffix.""" msg = f"{prefix} - {msg} - {suffix}" self._logger.error(msg, *args, **kwargs) - def critical(self, msg="", prefix="", suffix="", *args, **kwargs): + def critical(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): """Wraps critical message with prefix and suffix.""" msg = f"{prefix} - {msg} - {suffix}" self._logger.critical(msg, *args, **kwargs) - def exception(self, msg="", prefix="", suffix="", *args, **kwargs): + def exception(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): """Wraps exception message with prefix and suffix.""" msg = f"{prefix} - {msg} - {suffix}" self._logger.exception(msg, *args, **kwargs) diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 6bd7b6acd..15b41001b 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -9,7 +9,7 @@ DEFAULT_LOG_FILE_NAME, BITTENSOR_LOGGER_NAME, ) -from bittensor.utils.btlogging.loggingmachine import LoggingConfig +from bittensor.utils.btlogging.loggingmachine import LoggingConfig, _concat_message @pytest.fixture(autouse=True, scope="session") @@ -175,3 +175,23 @@ def test_all_log_levels_output(logging_machine, caplog): assert "Test warning" in caplog.text assert "Test error" in caplog.text assert "Test critical" in caplog.text + + +@pytest.mark.parametrize( + "msg, prefix, suffix, expected_result", + [ + ("msg", None, None, "msg"), + ("msg", "prefix", None, "prefix - msg"), + ("msg", None, "suffix", "msg - suffix"), + ("msg", "prefix", "suffix", "prefix - msg - suffix"), + ], + ids=[ + "message, no prefix, no suffix", + "message and prefix only", + "message and suffix only", + "message, prefix, and suffix", + ], +) +def test_concat(msg, prefix, suffix, expected_result): + """Test different options of message concatenation with prefix and suffix.""" + assert _concat_message(msg, prefix, suffix) == expected_result From 38f4eb85ac40d719dc619a61481c68d3493dd93c Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 12 Sep 2024 20:08:25 -0700 Subject: [PATCH 186/260] fix + ruff --- bittensor/utils/btlogging/loggingmachine.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index b8569648d..8e12c7056 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -30,7 +30,7 @@ import sys from logging import Logger from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler -from typing import NamedTuple, Optional +from typing import NamedTuple from statemachine import State, StateMachine @@ -47,7 +47,7 @@ from .helpers import all_loggers -def _concat_message(msg: str, prefix: Optional[str] = None, suffix: Optional[str] = None): +def _concat_message(msg="", prefix="", suffix=""): """Concatenates a message with optional prefix and suffix.""" msg = f"{f'{prefix} - ' if prefix else ''}{msg}{f' - {suffix}' if suffix else ''}" return msg @@ -367,42 +367,42 @@ def __trace_on__(self) -> bool: """ return self.current_state_value == "Trace" - def trace(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): + def trace(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps trace message with prefix and suffix.""" msg = _concat_message(msg, prefix, suffix) self._logger.trace(msg, *args, **kwargs) - def debug(self, msg="", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): + def debug(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps debug message with prefix and suffix.""" msg = _concat_message(msg, prefix, suffix) self._logger.debug(msg, *args, **kwargs) - def info(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): + def info(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps info message with prefix and suffix.""" msg = _concat_message(msg, prefix, suffix) self._logger.info(msg, *args, **kwargs) - def success(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): + def success(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps success message with prefix and suffix.""" msg = f"{prefix} - {msg} - {suffix}" self._logger.success(msg, *args, **kwargs) - def warning(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): + def warning(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps warning message with prefix and suffix.""" msg = f"{prefix} - {msg} - {suffix}" self._logger.warning(msg, *args, **kwargs) - def error(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): + def error(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps error message with prefix and suffix.""" msg = f"{prefix} - {msg} - {suffix}" self._logger.error(msg, *args, **kwargs) - def critical(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): + def critical(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps critical message with prefix and suffix.""" msg = f"{prefix} - {msg} - {suffix}" self._logger.critical(msg, *args, **kwargs) - def exception(self, msg: str = "", prefix: Optional[str] = None, suffix: Optional[str] = None, *args, **kwargs): + def exception(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps exception message with prefix and suffix.""" msg = f"{prefix} - {msg} - {suffix}" self._logger.exception(msg, *args, **kwargs) From bdeaede11b8ef1b908a37f89c517813cdd7a1ee2 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 12 Sep 2024 20:09:37 -0700 Subject: [PATCH 187/260] remove unused import --- bittensor/core/extrinsics/transfer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 9c41ab1f3..df53a3800 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -28,7 +28,6 @@ is_valid_bittensor_address_or_public_key, ) from bittensor.utils.balance import Balance -from bittensor.utils.btlogging import logging from bittensor.utils.networking import ensure_connected # For annotation purposes From 6f2ce29437dcea1f5e5b15183dd97de8f6512bf6 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 09:15:11 -0700 Subject: [PATCH 188/260] improve btlogging concat test --- tests/unit_tests/test_logging.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 15b41001b..2c5e593f0 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -180,13 +180,15 @@ def test_all_log_levels_output(logging_machine, caplog): @pytest.mark.parametrize( "msg, prefix, suffix, expected_result", [ + ("msg", "", "", "msg"), ("msg", None, None, "msg"), ("msg", "prefix", None, "prefix - msg"), ("msg", None, "suffix", "msg - suffix"), ("msg", "prefix", "suffix", "prefix - msg - suffix"), ], ids=[ - "message, no prefix, no suffix", + "message, no prefix (str), no suffix (str)", + "message, no prefix (None), no suffix (None)", "message and prefix only", "message and suffix only", "message, prefix, and suffix", From 924e6fba99baa173f2683d05cfbc57b443f9035a Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 13:20:40 -0700 Subject: [PATCH 189/260] docs AxonInfo --- bittensor/core/chain_data/axon_info.py | 29 ++++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/bittensor/core/chain_data/axon_info.py b/bittensor/core/chain_data/axon_info.py index 75da486c1..eee9cb82a 100644 --- a/bittensor/core/chain_data/axon_info.py +++ b/bittensor/core/chain_data/axon_info.py @@ -18,11 +18,6 @@ """ This module defines the `AxonInfo` class, a data structure used to represent information about an axon endpoint in the bittensor network. - -The `AxonInfo` class includes attributes such as version, IP address, port, IP type, hotkey, coldkey, and protocol, -along with additional placeholders to accommodate future expansion. It provides various methods to facilitate handling -and interacting with axon information, including methods for converting to and from JSON strings, checking the -serving status, generating string representations, and creating instances from a dictionary or a parameter dictionary. """ import json @@ -36,6 +31,22 @@ @dataclass class AxonInfo: + """ + The `AxonInfo` class represents information about an axon endpoint in the bittensor network. This includes + properties such as IP address, ports, and relevant keys. + + Attributes: + version (int): The version of the axon endpoint. + ip (str): The IP address of the axon endpoint. + port (int): The port number the axon endpoint uses. + ip_type (int): The type of IP protocol (e.g., IPv4 or IPv6). + hotkey (str): The hotkey associated with the axon endpoint. + coldkey (str): The coldkey associated with the axon endpoint. + protocol (int): The protocol version (default is 4). + placeholder1 (int): Reserved field (default is 0). + placeholder2 (int): Reserved field (default is 0). + """ + version: int ip: str port: int @@ -78,7 +89,7 @@ def __repr__(self): return self.__str__() def to_string(self) -> str: - """Converts the AxonInfo object to a string representation using JSON.""" + """Converts the `AxonInfo` object to a string representation using JSON.""" try: return json.dumps(asdict(self)) except (TypeError, ValueError) as e: @@ -88,13 +99,13 @@ def to_string(self) -> str: @classmethod def from_string(cls, json_string: str) -> "AxonInfo": """ - Creates an AxonInfo object from its string representation using JSON. + Creates an `AxonInfo` object from its string representation using JSON. Args: json_string (str): The JSON string representation of the AxonInfo object. Returns: - AxonInfo: An instance of AxonInfo created from the JSON string. If decoding fails, returns a default AxonInfo object with default values. + AxonInfo: An instance of AxonInfo created from the JSON string. If decoding fails, returns a default `AxonInfo` object with default values. Raises: json.JSONDecodeError: If there is an error in decoding the JSON string. @@ -115,7 +126,7 @@ def from_string(cls, json_string: str) -> "AxonInfo": @classmethod def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": """ - Converts a dictionary to an AxonInfo object. + Converts a dictionary to an `AxonInfo` object. Args: neuron_info (dict): A dictionary containing the neuron information. From f98725bb902f957f78510f7be705b7409119f90f Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 13:22:42 -0700 Subject: [PATCH 190/260] docs DelegateInfo --- bittensor/core/chain_data/delegate_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/chain_data/delegate_info.py b/bittensor/core/chain_data/delegate_info.py index dcbf5bf8f..d96cb8ef1 100644 --- a/bittensor/core/chain_data/delegate_info.py +++ b/bittensor/core/chain_data/delegate_info.py @@ -12,7 +12,7 @@ @dataclass class DelegateInfo: """ - Dataclass for delegate information. For a lighter version of this class, see :func:`DelegateInfoLite`. + Dataclass for delegate information. For a lighter version of this class, see ``DelegateInfoLite``. Args: hotkey_ss58 (str): Hotkey of the delegate for which the information is being fetched. From 45b754be892f6ca43ced3b8ed13fc492b506c489 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 13:28:30 -0700 Subject: [PATCH 191/260] DelegateInfoLite --- bittensor/core/chain_data/delegate_info_lite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/chain_data/delegate_info_lite.py b/bittensor/core/chain_data/delegate_info_lite.py index 84f2bdf5c..f1bb1d72b 100644 --- a/bittensor/core/chain_data/delegate_info_lite.py +++ b/bittensor/core/chain_data/delegate_info_lite.py @@ -5,7 +5,7 @@ @dataclass class DelegateInfoLite: """ - Dataclass for DelegateLiteInfo. This is a lighter version of :func:`DelegateInfo`. + Dataclass for `DelegateLiteInfo`. This is a lighter version of :func:``DelegateInfo``. Args: delegate_ss58 (str): Hotkey of the delegate for which the information is being fetched. From 4fd4da3c78d611de9ac4c2290d0987b3de0ec608 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 13:31:02 -0700 Subject: [PATCH 192/260] IPInfo --- bittensor/core/chain_data/ip_info.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bittensor/core/chain_data/ip_info.py b/bittensor/core/chain_data/ip_info.py index aa44fc327..2a3254903 100644 --- a/bittensor/core/chain_data/ip_info.py +++ b/bittensor/core/chain_data/ip_info.py @@ -8,7 +8,14 @@ @dataclass class IPInfo: - """Dataclass for associated IP Info.""" + """ + Dataclass representing IP information. + + Attributes: + ip (str): The IP address as a string. + ip_type (int): The type of the IP address (e.g., IPv4, IPv6). + protocol (int): The protocol associated with the IP (e.g., TCP, UDP). + """ ip: str ip_type: int From 780e5eaacc08327329241f1a00d81f9aa93a72b6 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 13:36:36 -0700 Subject: [PATCH 193/260] NeuronInfo --- bittensor/core/chain_data/neuron_info.py | 39 +++++++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py index c31862d42..e0962bfa4 100644 --- a/bittensor/core/chain_data/neuron_info.py +++ b/bittensor/core/chain_data/neuron_info.py @@ -10,23 +10,50 @@ from bittensor.utils import u16_normalized_float from bittensor.utils.balance import Balance +# for annotation purposes if TYPE_CHECKING: from bittensor.core.chain_data.neuron_info_lite import NeuronInfoLite @dataclass class NeuronInfo: - """Dataclass for neuron metadata.""" + """Represents the metadata of a neuron including keys, UID, stake, rankings, and other attributes. + + Attributes: + hotkey (str): The hotkey associated with the neuron. + coldkey (str): The coldkey associated with the neuron. + uid (int): The unique identifier for the neuron. + netuid (int): The network unique identifier for the neuron. + active (int): The active status of the neuron. + stake (Balance): The balance staked to this neuron. + stake_dict (dict[str, Balance]): A dictionary mapping coldkey to the amount staked. + total_stake (Balance): The total amount of stake. + rank (float): The rank score of the neuron. + emission (float): The emission rate. + incentive (float): The incentive value. + consensus (float): The consensus score. + trust (float): The trust score. + validator_trust (float): The validation trust score. + dividends (float): The dividends value. + last_update (int): The timestamp of the last update. + validator_permit (bool): Validator permit status. + weights (list[list[int]]): List of weights associated with the neuron. + bonds (list[list[int]]): List of bonds associated with the neuron. + pruning_score (int): The pruning score of the neuron. + prometheus_info (Optional[PrometheusInfo]): Information related to Prometheus. + axon_info (Optional[AxonInfo]): Information related to Axon. + is_null (bool): Indicator if this is a null neuron. + """ hotkey: str coldkey: str uid: int netuid: int active: int - stake: Balance + stake: "Balance" # mapping of coldkey to amount staked to this Neuron - stake_dict: dict[str, Balance] - total_stake: Balance + stake_dict: dict[str, "Balance"] + total_stake: "Balance" rank: float emission: float incentive: float @@ -40,7 +67,7 @@ class NeuronInfo: bonds: list[list[int]] pruning_score: int prometheus_info: Optional["PrometheusInfo"] = None - axon_info: Optional[AxonInfo] = None + axon_info: Optional["AxonInfo"] = None is_null: bool = False @classmethod @@ -58,6 +85,7 @@ def from_weights_bonds_and_neuron_lite( @staticmethod def get_null_neuron() -> "NeuronInfo": + """Returns a null neuron instance.""" neuron = NeuronInfo( uid=0, netuid=0, @@ -87,6 +115,7 @@ def get_null_neuron() -> "NeuronInfo": @classmethod def from_vec_u8(cls, vec_u8: bytes) -> "NeuronInfo": + """Instantiates NeuronInfo from a byte vector.""" n = bt_decode.NeuronInfo.decode(bytes(vec_u8)) stake_dict = process_stake_data(n.stake) total_stake = sum(stake_dict.values()) if stake_dict else Balance(0) From ab28b32ec3b666266985482fe16838c71299a900 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 13:41:03 -0700 Subject: [PATCH 194/260] NeuronInfo + NeuronInfoLite --- bittensor/core/chain_data/neuron_info.py | 13 +++++- bittensor/core/chain_data/neuron_info_lite.py | 41 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py index e0962bfa4..478cdfa4c 100644 --- a/bittensor/core/chain_data/neuron_info.py +++ b/bittensor/core/chain_data/neuron_info.py @@ -77,6 +77,17 @@ def from_weights_bonds_and_neuron_lite( weights_as_dict: dict[int, list[tuple[int, int]]], bonds_as_dict: dict[int, list[tuple[int, int]]], ) -> "NeuronInfo": + """ + Creates an instance of NeuronInfo from NeuronInfoLite and dictionaries of weights and bonds. + + Args: + neuron_lite (NeuronInfoLite): A lite version of the neuron containing basic attributes. + weights_as_dict (dict[int, list[tuple[int, int]]]): A dictionary where the key is the UID and the value is a list of weight tuples associated with the neuron. + bonds_as_dict (dict[int, list[tuple[int, int]]]): A dictionary where the key is the UID and the value is a list of bond tuples associated with the neuron. + + Returns: + NeuronInfo: An instance of NeuronInfo populated with the provided weights and bonds. + """ n_dict = neuron_lite.__dict__ n_dict["weights"] = weights_as_dict.get(neuron_lite.uid, []) n_dict["bonds"] = bonds_as_dict.get(neuron_lite.uid, []) @@ -85,7 +96,7 @@ def from_weights_bonds_and_neuron_lite( @staticmethod def get_null_neuron() -> "NeuronInfo": - """Returns a null neuron instance.""" + """Returns a null NeuronInfo instance.""" neuron = NeuronInfo( uid=0, netuid=0, diff --git a/bittensor/core/chain_data/neuron_info_lite.py b/bittensor/core/chain_data/neuron_info_lite.py index 0f7a4a015..018069dcc 100644 --- a/bittensor/core/chain_data/neuron_info_lite.py +++ b/bittensor/core/chain_data/neuron_info_lite.py @@ -13,7 +13,36 @@ @dataclass class NeuronInfoLite: - """Dataclass for neuron metadata, but without the weights and bonds.""" + """ + NeuronInfoLite is a dataclass representing neuron metadata without weights and bonds. + + Attributes: + hotkey (str): The hotkey string for the neuron. + coldkey (str): The coldkey string for the neuron. + uid (int): A unique identifier for the neuron. + netuid (int): Network unique identifier for the neuron. + active (int): Indicates whether the neuron is active. + stake (Balance): The stake amount associated with the neuron. + stake_dict (dict): Mapping of coldkey to the amount staked to this Neuron. + total_stake (Balance): Total amount of the stake. + rank (float): The rank of the neuron. + emission (float): The emission value of the neuron. + incentive (float): The incentive value of the neuron. + consensus (float): The consensus value of the neuron. + trust (float): Trust value of the neuron. + validator_trust (float): Validator trust value of the neuron. + dividends (float): Dividends associated with the neuron. + last_update (int): Timestamp of the last update. + validator_permit (bool): Indicates if the neuron has a validator permit. + prometheus_info (Optional[PrometheusInfo]): Prometheus information associated with the neuron. + axon_info (Optional[AxonInfo]): Axon information associated with the neuron. + pruning_score (int): The pruning score of the neuron. + is_null (bool): Indicates whether the neuron is null. + + Methods: + get_null_neuron: Returns a NeuronInfoLite object representing a null neuron. + list_from_vec_u8: Decodes a bytes object into a list of NeuronInfoLite instances. + """ hotkey: str coldkey: str @@ -40,6 +69,7 @@ class NeuronInfoLite: @staticmethod def get_null_neuron() -> "NeuronInfoLite": + """Returns a null NeuronInfoLite instance.""" neuron = NeuronInfoLite( uid=0, netuid=0, @@ -67,6 +97,15 @@ def get_null_neuron() -> "NeuronInfoLite": @classmethod def list_from_vec_u8(cls, vec_u8: bytes) -> list["NeuronInfoLite"]: + """ + Decodes a bytes object into a list of NeuronInfoLite instances. + + Args: + vec_u8 (bytes): The bytes object to decode into NeuronInfoLite instances. + + Returns: + list[NeuronInfoLite]: A list of NeuronInfoLite instances decoded from the provided bytes object. + """ decoded = bt_decode.NeuronInfoLite.decode_vec(vec_u8) results = [] for item in decoded: From 99ca720312481f8e8a208966d53ab4d83e9095cc Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 13:42:11 -0700 Subject: [PATCH 195/260] PrometheusInfo --- bittensor/core/chain_data/prometheus_info.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bittensor/core/chain_data/prometheus_info.py b/bittensor/core/chain_data/prometheus_info.py index a6279c2cf..dcda31145 100644 --- a/bittensor/core/chain_data/prometheus_info.py +++ b/bittensor/core/chain_data/prometheus_info.py @@ -6,7 +6,16 @@ @dataclass class PrometheusInfo: - """Dataclass for prometheus info.""" + """ + Dataclass representing information related to Prometheus. + + Attributes: + block (int): The block number associated with the Prometheus data. + version (int): The version of the Prometheus data. + ip (str): The IP address associated with Prometheus. + port (int): The port number for Prometheus. + ip_type (int): The type of IP address (e.g., IPv4, IPv6). + """ block: int version: int From f634ef6edde80f5d49ad8855be060a23c4470de4 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 14:01:50 -0700 Subject: [PATCH 196/260] ProposalVoteData --- bittensor/core/chain_data/proposal_vote_data.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py index 501915690..8a4663448 100644 --- a/bittensor/core/chain_data/proposal_vote_data.py +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -3,6 +3,16 @@ # Senate / Proposal data class ProposalVoteData(TypedDict): + """ + This TypedDict represents the data structure for a proposal vote in the Senate. + + Attributes: + index (int): The index of the proposal. + threshold (int): The threshold required for the proposal to pass. + ayes (List[str]): List of senators who voted 'aye'. + nays (List[str]): List of senators who voted 'nay'. + end (int): The ending timestamp of the voting period. + """ index: int threshold: int ayes: List[str] From ba44e783578226882289efa96f5d6f1cc9bd8280 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 14:17:16 -0700 Subject: [PATCH 197/260] ScheduledColdkeySwapInfo --- bittensor/core/chain_data/scheduled_coldkey_swap_info.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py index 2b8d89719..6b2e58d23 100644 --- a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py +++ b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py @@ -9,7 +9,14 @@ @dataclass class ScheduledColdkeySwapInfo: - """Dataclass for scheduled coldkey swap information.""" + """ + The `ScheduledColdkeySwapInfo` class is a dataclass representing information about scheduled cold key swaps. + + Attributes: + old_coldkey (str): The old cold key before the swap. + new_coldkey (str): The new cold key after the swap. + arbitration_block (int): The block number at which the arbitration of the swap will take place. + """ old_coldkey: str new_coldkey: str From 02bf796312551317f48fc99ff25558a1f310e0cb Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 14:17:32 -0700 Subject: [PATCH 198/260] ProposalVoteData --- bittensor/core/chain_data/proposal_vote_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py index 8a4663448..3d3aaed61 100644 --- a/bittensor/core/chain_data/proposal_vote_data.py +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -13,6 +13,7 @@ class ProposalVoteData(TypedDict): nays (List[str]): List of senators who voted 'nay'. end (int): The ending timestamp of the voting period. """ + index: int threshold: int ayes: List[str] From 5882dd4567e032bebe58d5f3da0d7dbffbfaa068 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 14:29:02 -0700 Subject: [PATCH 199/260] StakeInfo --- bittensor/core/chain_data/stake_info.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bittensor/core/chain_data/stake_info.py b/bittensor/core/chain_data/stake_info.py index 5f6adf6b7..b19c3acaf 100644 --- a/bittensor/core/chain_data/stake_info.py +++ b/bittensor/core/chain_data/stake_info.py @@ -14,7 +14,14 @@ @dataclass class StakeInfo: - """Dataclass for stake info.""" + """ + Dataclass for representing stake information linked to hotkey and coldkey pairs. + + Attributes: + hotkey_ss58 (str): The SS58 encoded hotkey address. + coldkey_ss58 (str): The SS58 encoded coldkey address. + stake (Balance): The stake associated with the hotkey-coldkey pair, represented as a Balance object. + """ hotkey_ss58: str # Hotkey address coldkey_ss58: str # Coldkey address @@ -43,7 +50,7 @@ def from_vec_u8(cls, vec_u8: List[int]) -> Optional["StakeInfo"]: @classmethod def list_of_tuple_from_vec_u8( - cls, vec_u8: List[int] + cls, vec_u8: List[int] ) -> Dict[str, List["StakeInfo"]]: """Returns a list of StakeInfo objects from a ``vec_u8``.""" decoded: Optional[list[tuple[str, list[object]]]] = ( From 6786225122e96066cf70b31ae45f89c72ae6f8a1 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 13 Sep 2024 14:38:14 -0700 Subject: [PATCH 200/260] StakeInfo + SubnetHyperparameters --- bittensor/core/chain_data/stake_info.py | 2 +- .../core/chain_data/subnet_hyperparameters.py | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/bittensor/core/chain_data/stake_info.py b/bittensor/core/chain_data/stake_info.py index b19c3acaf..f1a8bd24c 100644 --- a/bittensor/core/chain_data/stake_info.py +++ b/bittensor/core/chain_data/stake_info.py @@ -50,7 +50,7 @@ def from_vec_u8(cls, vec_u8: List[int]) -> Optional["StakeInfo"]: @classmethod def list_of_tuple_from_vec_u8( - cls, vec_u8: List[int] + cls, vec_u8: List[int] ) -> Dict[str, List["StakeInfo"]]: """Returns a list of StakeInfo objects from a ``vec_u8``.""" decoded: Optional[list[tuple[str, list[object]]]] = ( diff --git a/bittensor/core/chain_data/subnet_hyperparameters.py b/bittensor/core/chain_data/subnet_hyperparameters.py index 2df8d53c2..c28f802cf 100644 --- a/bittensor/core/chain_data/subnet_hyperparameters.py +++ b/bittensor/core/chain_data/subnet_hyperparameters.py @@ -6,7 +6,38 @@ @dataclass class SubnetHyperparameters: - """Dataclass for subnet hyperparameters.""" + """ + This class represents the hyperparameters for a subnet. + + Attributes: + rho (int): The rate of decay of some value. + kappa (int): A constant multiplier used in calculations. + immunity_period (int): The period during which immunity is active. + min_allowed_weights (int): Minimum allowed weights. + max_weight_limit (float): Maximum weight limit. + tempo (int): The tempo or rate of operation. + min_difficulty (int): Minimum difficulty for some operations. + max_difficulty (int): Maximum difficulty for some operations. + weights_version (int): The version number of the weights used. + weights_rate_limit (int): Rate limit for processing weights. + adjustment_interval (int): Interval at which adjustments are made. + activity_cutoff (int): Activity cutoff threshold. + registration_allowed (bool): Indicates if registration is allowed. + target_regs_per_interval (int): Target number of registrations per interval. + min_burn (int): Minimum burn value. + max_burn (int): Maximum burn value. + bonds_moving_avg (int): Moving average of bonds. + max_regs_per_block (int): Maximum number of registrations per block. + serving_rate_limit (int): Limit on the rate of service. + max_validators (int): Maximum number of validators. + adjustment_alpha (int): Alpha value for adjustments. + difficulty (int): Difficulty level. + commit_reveal_weights_interval (int): Interval for commit-reveal weights. + commit_reveal_weights_enabled (bool): Flag indicating if commit-reveal weights are enabled. + alpha_high (int): High value of alpha. + alpha_low (int): Low value of alpha. + liquid_alpha_enabled (bool): Flag indicating if liquid alpha is enabled. + """ rho: int kappa: int @@ -38,6 +69,17 @@ class SubnetHyperparameters: @classmethod def from_vec_u8(cls, vec_u8: bytes) -> Optional["SubnetHyperparameters"]: + """ + Create a `SubnetHyperparameters` instance from a vector of bytes. + + This method decodes the given vector of bytes using the `bt_decode` module and creates a new instance of `SubnetHyperparameters` with the decoded values. + + Args: + vec_u8 (bytes): A vector of bytes to decode into `SubnetHyperparameters`. + + Returns: + Optional[SubnetHyperparameters]: An instance of `SubnetHyperparameters` if decoding is successful, None otherwise. + """ decoded = bt_decode.SubnetHyperparameters.decode(vec_u8) return SubnetHyperparameters( rho=decoded.rho, From 572d747bbac8853f804abc22bff0d8ddd33d8e62 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 07:32:25 -0700 Subject: [PATCH 201/260] bittensor/core/chain_data/utils.py --- bittensor/core/chain_data/utils.py | 39 +++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index 8d3276571..ffdd7ef89 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -25,8 +25,8 @@ class ChainDataType(Enum): def from_scale_encoding( - input_: Union[List[int], bytes, ScaleBytes], - type_name: ChainDataType, + input_: Union[List[int], bytes, "ScaleBytes"], + type_name: "ChainDataType", is_vec: bool = False, is_option: bool = False, ) -> Optional[Dict]: @@ -57,6 +57,19 @@ def from_scale_encoding( def from_scale_encoding_using_type_string( input_: Union[List[int], bytes, ScaleBytes], type_string: str ) -> Optional[Dict]: + """ + Decodes SCALE encoded data to a dictionary based on the provided type string. + + Args: + input_ (Union[List[int], bytes, ScaleBytes]): The SCALE encoded input data. + type_string (str): The type string defining the structure of the data. + + Returns: + Optional[Dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. + + Raises: + TypeError: If the input_ is not a List[int], bytes, or ScaleBytes. + """ if isinstance(input_, ScaleBytes): as_scale_bytes = input_ else: @@ -247,12 +260,30 @@ def from_scale_encoding_using_type_string( } -def decode_account_id(account_id_bytes): +def decode_account_id(account_id_bytes: List) -> str: + """ + Decodes an AccountId from bytes to a Base64 string using SS58 encoding. + + Args: + account_id_bytes (bytes): The AccountId in bytes that needs to be decoded. + + Returns: + str: The decoded AccountId as a Base64 string. + """ # Convert the AccountId bytes to a Base64 string return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) -def process_stake_data(stake_data): +def process_stake_data(stake_data: List) -> Dict: + """ + Processes stake data to decode account IDs and convert stakes from rao to Balance objects. + + Args: + stake_data (list): A list of tuples where each tuple contains an account ID in bytes and a stake in rao. + + Returns: + dict: A dictionary with account IDs as keys and their corresponding Balance objects as values. + """ decoded_stake_data = {} for account_id_bytes, stake_ in stake_data: account_id = decode_account_id(account_id_bytes) From e0521252164b08c3541fc04421925094525a7d43 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 07:59:48 -0700 Subject: [PATCH 202/260] bittensor/core/axon.py --- bittensor/core/axon.py | 100 ++++++++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index da1392f13..5c90d4082 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -98,6 +98,16 @@ class FastAPIThreadedServer(uvicorn.Server): Stopping the Server During shutdown or maintenance of the Axon server, this class can be used to stop the FastAPI application gracefully, ensuring that all resources are properly released. + Example Usage:: + + self.app = FastAPI() + log_level = "trace" + self.fast_config = uvicorn.Config(self.app, host="0.0.0.0", port=self.config.axon.port, log_level=log_level) + self.fast_server = FastAPIThreadedServer(config=self.fast_config) + self.fast_server.start() + # do something + self.fast_server.stop() + Args: should_exit (bool): Flag to indicate whether the server should stop running. is_running (bool): Flag to indicate whether the server is currently running. @@ -163,7 +173,7 @@ def stop(self): class Axon: """ - The ``axon`` class in Bittensor is a fundamental component that serves as the server-side interface for a neuron within the Bittensor network. + The ``Axon`` class in Bittensor is a fundamental component that serves as the server-side interface for a neuron within the Bittensor network. This class is responsible for managing incoming requests from other neurons and implements various mechanisms to ensure efficient @@ -262,8 +272,8 @@ def prioritize_my_synapse( synapse: MySynapse ) -> float: ).start() Args: - wallet (bittensor.wallet, optional): Wallet with hotkey and coldkeypub. - config (bittensor.config, optional): Configuration parameters for the axon. + wallet (bittensor_wallet.Wallet, optional): Wallet with hotkey and coldkeypub. + config (bittensor.core.config.Config, optional): Configuration parameters for the axon. port (int, optional): Port for server binding. ip (str, optional): Binding IP address. external_ip (str, optional): External IP address to broadcast. @@ -309,8 +319,8 @@ def __init__( """Creates a new bittensor.Axon object from passed arguments. Args: - config (:obj:`Optional[bittensor.config]`, `optional`): bittensor.axon.config() - wallet (:obj:`Optional[bittensor.wallet]`, `optional`): bittensor wallet with hotkey and coldkeypub. + config (:obj:`Optional[bittensor.core.config.Config]`, `optional`): bittensor.axon.config() + wallet (:obj:`Optional[bittensor_wallet.Wallet]`, `optional`): bittensor wallet with hotkey and coldkeypub. port (:type:`Optional[int]`, `optional`): Binding port. ip (:type:`Optional[str]`, `optional`): Binding ip. external_ip (:type:`Optional[str]`, `optional`): The external ip of the server to broadcast to the network. @@ -408,13 +418,13 @@ def attach( """ Attaches custom functions to the Axon server for handling incoming requests. This method enables - the Axon to define specific behaviors for request forwarding, verification, blacklisting, and + the ``Axon`` to define specific behaviors for request forwarding, verification, blacklisting, and prioritization, thereby customizing its interaction within the Bittensor network. Registers an API endpoint to the FastAPI application router. It uses the name of the first argument of the :func:`forward_fn` function as the endpoint name. - The `attach` method in the Bittensor framework's axon class is a crucial function for registering + The :func:`attach` method in the Bittensor framework's axon class is a crucial function for registering API endpoints to the Axon's FastAPI application router. This method allows the Axon server to define how it handles incoming requests by attaching functions for forwarding, verifying, blacklisting, and prioritizing requests. It's a key part of customizing the server's behavior @@ -579,7 +589,7 @@ def config(cls) -> "Config": Parses the command-line arguments to form a Bittensor configuration object. Returns: - bittensor.config: Configuration object with settings from command-line arguments. + bittensor.core.config.Config: Configuration object with settings from command-line arguments. """ parser = argparse.ArgumentParser() Axon.add_args(parser) # Add specific axon-related arguments @@ -646,7 +656,7 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: Optional[str] = None) # Exception handling for re-parsing arguments pass - async def verify_body_integrity(self, request: Request): + async def verify_body_integrity(self, request: "Request"): """ The ``verify_body_integrity`` method in the Bittensor framework is a key security function within the Axon server's middleware. It is responsible for ensuring the integrity of the body of incoming HTTP @@ -705,7 +715,7 @@ def check_config(cls, config: "Config"): This method checks the configuration for the axon's port and wallet. Args: - config (bittensor.config): The config object holding axon settings. + config (bittensor.core.config.Config): The config object holding axon settings. Raises: AssertionError: If the axon or external ports are not in range [1024, 65535] @@ -753,7 +763,7 @@ def start(self) -> "Axon": within the Bittensor network. Returns: - bittensor.axon: The Axon instance in the 'started' state. + bittensor.core.axon.Axon: The Axon instance in the 'started' state. Example:: @@ -779,7 +789,7 @@ def stop(self) -> "Axon": shut down or needs to temporarily go offline. Returns: - bittensor.axon: The Axon instance in the 'stopped' state. + bittensor.core.axon.Axon: The Axon instance in the 'stopped' state. Example:: @@ -805,10 +815,10 @@ def serve(self, netuid: int, subtensor: Optional["Subtensor"] = None) -> "Axon": Args: netuid (int): The unique identifier of the subnet to register on. This ID is essential for the Axon to correctly position itself within the Bittensor network topology. - subtensor (bittensor.subtensor, optional): The subtensor connection to use for serving. If not provided, a new connection is established based on default configurations. + subtensor (bittensor.core.subtensor.Subtensor, optional): The subtensor connection to use for serving. If not provided, a new connection is established based on default configurations. Returns: - bittensor.axon: The Axon instance that is now actively serving on the specified subtensor. + bittensor.core.axon.Axon: The Axon instance that is now actively serving on the specified subtensor. Example:: @@ -864,7 +874,7 @@ async def default_verify(self, synapse: "Synapse"): cryptographic keys can participate in secure communication. Args: - synapse(Synapse): bittensor request synapse. + synapse(bittensor.core.synapse.Synapse): bittensor request synapse. Raises: Exception: If the ``receiver_hotkey`` doesn't match with ``self.receiver_hotkey``. @@ -941,7 +951,15 @@ async def default_verify(self, synapse: "Synapse"): raise SynapseDendriteNoneException(synapse=synapse) -def create_error_response(synapse: "Synapse"): +def create_error_response(synapse: "Synapse") -> "JSONResponse": + """Creates an error response based on the provided synapse object. + + Args: + synapse (bittensor.core.synapse.Synapse): The synapse object containing details about the request and the associated axon. + + Returns: + JSONResponse: A JSON response with a status code and content indicating the error message. + """ if synapse.axon is None: return JSONResponse( status_code=400, @@ -959,9 +977,21 @@ def create_error_response(synapse: "Synapse"): def log_and_handle_error( synapse: "Synapse", exception: Exception, - status_code: typing.Optional[int] = None, - start_time: typing.Optional[float] = None, + status_code: Optional[int] = None, + start_time: Optional[float] = None, ) -> "Synapse": + """ + Logs the error and updates the synapse object with the appropriate error details. + + Args: + synapse (bittensor.core.synapse.Synapse): The synapse object to be updated with error information. + exception (Exception): The exception that was raised and needs to be logged and handled. + status_code (Optional[int], optional): The HTTP status code to be set on the synapse object. Defaults to None. + start_time (Optional[float], optional): The timestamp marking the start of the processing, used to calculate process time. Defaults to None. + + Returns: + Synapse: The updated synapse object with error details. + """ if isinstance(exception, SynapseException): synapse = exception.synapse or synapse @@ -1038,14 +1068,14 @@ def __init__(self, app: "AxonMiddleware", axon: "Axon"): Initialize the AxonMiddleware class. Args: - app (object): An instance of the application where the middleware processor is used. - axon (object): The axon instance used to process the requests. + app (bittensor.core.axon.AxonMiddleware): An instance of the application where the middleware processor is used. + axon (bittensor.core.axon.Axon): The axon instance used to process the requests. """ super().__init__(app) self.axon = axon async def dispatch( - self, request: Request, call_next: RequestResponseEndpoint + self, request: "Request", call_next: "RequestResponseEndpoint" ) -> Response: """ Asynchronously processes incoming HTTP requests and returns the corresponding responses. This @@ -1147,7 +1177,7 @@ async def dispatch( # Return the response to the requester. return response - async def preprocess(self, request: Request) -> "Synapse": + async def preprocess(self, request: "Request") -> "Synapse": """ Performs the initial processing of the incoming request. This method is responsible for extracting relevant information from the request and setting up the Synapse object, which @@ -1157,7 +1187,7 @@ async def preprocess(self, request: Request) -> "Synapse": request (Request): The incoming request to be preprocessed. Returns: - bittensor.Synapse: The Synapse object representing the preprocessed state of the request. + bittensor.core.synapse.Synapse: The Synapse object representing the preprocessed state of the request. The preprocessing involves: @@ -1221,7 +1251,7 @@ async def verify(self, synapse: "Synapse"): request meets the predefined security and validation criteria. Args: - synapse (bittensor.Synapse): The Synapse object representing the request. + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. Raises: Exception: If the verification process fails due to unmet criteria or security concerns. @@ -1280,7 +1310,7 @@ async def blacklist(self, synapse: "Synapse"): are prohibited from accessing certain resources. Args: - synapse (bittensor.Synapse): The Synapse object representing the request. + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. Raises: Exception: If the request is found in the blacklist. @@ -1334,7 +1364,7 @@ async def priority(self, synapse: "Synapse"): level to the request, determining its urgency and importance in the processing queue. Args: - synapse (bittensor.Synapse): The Synapse object representing the request. + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. Raises: Exception: If the priority assessment process encounters issues, such as timeouts. @@ -1347,15 +1377,15 @@ async def priority(self, synapse: "Synapse"): priority_fn = self.axon.priority_fns.get(str(synapse.name), None) async def submit_task( - executor: PriorityThreadPoolExecutor, priority: float + executor: "PriorityThreadPoolExecutor", priority: float ) -> Tuple[float, Any]: """ Submits the given priority function to the specified executor for asynchronous execution. The function will run in the provided executor and return the priority value along with the result. Args: - executor: The executor in which the priority function will be run. - priority: The priority function to be executed. + executor (bittensor.core.threadpool.PriorityThreadPoolExecutor): The executor in which the priority function will be run. + priority (float): The priority function to be executed. Returns: tuple: A tuple containing the priority value and the result of the priority function execution. @@ -1396,15 +1426,15 @@ async def submit_task( async def run( self, synapse: "Synapse", - call_next: RequestResponseEndpoint, - request: Request, - ) -> Response: + call_next: "RequestResponseEndpoint", + request: "Request", + ) -> "Response": """ Executes the requested function as part of the request processing pipeline. This method calls the next function in the middleware chain to process the request and generate a response. Args: - synapse (bittensor.Synapse): The Synapse object representing the request. + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. call_next (RequestResponseEndpoint): The next function in the middleware chain to process requests. request (Request): The original HTTP request. @@ -1436,13 +1466,13 @@ async def synapse_to_response( synapse: "Synapse", start_time: float, *, - response_override: "Optional[Response]" = None, + response_override: Optional["Response"] = None, ) -> "Response": """ Converts the Synapse object into a JSON response with HTTP headers. Args: - synapse (bittensor.Synapse): The Synapse object representing the request. + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. start_time (float): The timestamp when the request processing started. response_override: Instead of serializing the synapse, mutate the provided response object. This is only really useful for StreamingSynapse responses. From f89cdee8410b758b93f3b768ee5ba768495313d7 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 08:04:46 -0700 Subject: [PATCH 203/260] bittensor/core/axon.py --- bittensor/core/axon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 5c90d4082..7d82b8002 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -953,10 +953,10 @@ async def default_verify(self, synapse: "Synapse"): def create_error_response(synapse: "Synapse") -> "JSONResponse": """Creates an error response based on the provided synapse object. - + Args: synapse (bittensor.core.synapse.Synapse): The synapse object containing details about the request and the associated axon. - + Returns: JSONResponse: A JSON response with a status code and content indicating the error message. """ From 90e837c17b75991f70c46212dc53f1ec74cbae38 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 09:32:31 -0700 Subject: [PATCH 204/260] bittensor/core/config.py --- bittensor/core/config.py | 15 +++++++-------- bittensor/core/subtensor.py | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 9d0c18348..57e8892e5 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -309,7 +309,7 @@ def copy(self) -> "Config": @staticmethod def to_string(items) -> str: - """Get string from items""" + """Get string from items.""" return "\n" + yaml.dump(items.toDict()) def update_with_kwargs(self, kwargs): @@ -333,11 +333,12 @@ def _merge(cls, a, b): a[key] = b[key] return a - def merge(self, b): + def merge(self, b: "Config"): """ Merges the current config with another config. - Args: b: Another config to merge. + Args: + b (bittensor.core.config.Config): Another config to merge. """ self._merge(self, b) @@ -348,10 +349,10 @@ def merge_all(cls, configs: List["Config"]) -> "Config": If there is a conflict, the value from the last configuration in the list will take precedence. Args: - configs (list of config): List of configs to be merged. + configs (list of bittensor.core.config.Config): List of configs to be merged. Returns: - config: Merged config object. + config (bittensor.core.config.Config): Merged config object. """ result = cls() for cfg in configs: @@ -359,9 +360,7 @@ def merge_all(cls, configs: List["Config"]) -> "Config": return result def is_set(self, param_name: str) -> bool: - """ - Returns a boolean indicating whether the parameter has been set or is still the default. - """ + """Returns a boolean indicating whether the parameter has been set or is still the default.""" if param_name not in self.get("__is_set"): return False else: diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index fae927f07..789f839a6 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -272,7 +272,7 @@ def setup_config(network: Optional[str], config: "Config"): evaluated_endpoint, ) = Subtensor.determine_chain_endpoint_and_network(network) else: - if config.get("__is_set", {}).get("subtensor.chain_endpoint"): + if config.is_set("subtensor.chain_endpoint"): ( evaluated_network, evaluated_endpoint, @@ -280,7 +280,7 @@ def setup_config(network: Optional[str], config: "Config"): config.subtensor.chain_endpoint ) - elif config.get("__is_set", {}).get("subtensor.network"): + elif config.is_set("subtensor.network"): ( evaluated_network, evaluated_endpoint, From f164e491614e6fc725474189faf837649c8d1fdb Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 10:41:01 -0700 Subject: [PATCH 205/260] bittensor/core/dendrite.py --- bittensor/core/dendrite.py | 127 +++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 61 deletions(-) diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index c40c018bc..db4baab18 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -61,7 +61,7 @@ class DendriteMixin: network requests and processing server responses. Args: - keypair: The wallet or keypair used for signing messages. + keypair (Option[Union[bittensor_wallet.Wallet, substrateinterface.Keypair]]): The wallet or keypair used for signing messages. external_ip (str): The external IP address of the local system. synapse_history (list): A list of Synapse objects representing the historical responses. @@ -96,16 +96,15 @@ class DendriteMixin: print(d) d( ) # ping axon d( [] ) # ping multiple - d( bittensor.core.axon.Axon(), bittensor.core.synapse.Synapse ) + d( bittensor.core.axon.Axon, bittensor.core.synapse.Synapse ) """ - def __init__(self, wallet: Optional[Union["Wallet", Keypair]] = None): + def __init__(self, wallet: Optional[Union["Wallet", "Keypair"]] = None): """ Initializes the Dendrite object, setting up essential properties. Args: - wallet (Optional[Union['bittensor_wallet.Wallet', 'bittensor.keypair']], optional): - The user's wallet or keypair used for signing messages. Defaults to ``None``, in which case a new :func:`bittensor.wallet().hotkey` is generated and used. + wallet (Optional[Union[bittensor_wallet.Wallet, substrateinterface.Keypair]], optional): The user's wallet or keypair used for signing messages. Defaults to ``None``, in which case a new :func:`bittensor_wallet.Wallet().hotkey` is generated and used. """ # Initialize the parent class super(DendriteMixin, self).__init__() @@ -144,9 +143,9 @@ async def session(self) -> aiohttp.ClientSession: Example usage:: - import bittensor as bt # Import bittensor - wallet = bt.wallet( ... ) # Initialize a wallet - dendrite = bt.dendrite( wallet ) # Initialize a dendrite instance with the wallet + import bittensor # Import bittensor + wallet = bittensor.Wallet( ... ) # Initialize a wallet + dendrite = bittensor.dendrite( wallet ) # Initialize a dendrite instance with the wallet async with (await dendrite.session).post( # Use the session to make an HTTP POST request url, # URL to send the request to @@ -257,12 +256,12 @@ def process_error_message( message in the synapse object. It covers common network errors such as connection issues and timeouts. Args: - synapse: The synapse object associated with the request. - request_name: The name of the request during which the exception occurred. - exception: The exception object caught during the request. + synapse (bittensor.core.synapse.Synapse): The synapse object associated with the request. + request_name (str): The name of the request during which the exception occurred. + exception (Exception): The exception object caught during the request. Returns: - Synapse: The updated synapse object with the error status code and message. + Synapse (bittensor.core.synapse.Synapse): The updated synapse object with the error status code and message. Note: This method updates the synapse object in-place. @@ -288,7 +287,7 @@ def process_error_message( return synapse - def _log_outgoing_request(self, synapse): + def _log_outgoing_request(self, synapse: "Synapse"): """ Logs information about outgoing requests for debugging purposes. @@ -298,17 +297,20 @@ def _log_outgoing_request(self, synapse): To turn on debug messages, set the environment variable BITTENSOR_DEBUG to ``1``, or call the bittensor debug method like so:: + Example:: + import bittensor bittensor.debug() Args: - synapse: The synapse object representing the request being sent. + synapse (bittensor.core.synapse.Synapse): The synapse object representing the request being sent. """ - logging.trace( - f"dendrite | --> | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | 0 | Success" - ) + if synapse.axon is not None: + logging.trace( + f"dendrite | --> | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | 0 | Success" + ) - def _log_incoming_response(self, synapse): + def _log_incoming_response(self, synapse: "Synapse"): """ Logs information about incoming responses for debugging and monitoring. @@ -317,27 +319,28 @@ def _log_incoming_response(self, synapse): This logging is vital for troubleshooting and understanding the network interactions in Bittensor. Args: - synapse: The synapse object representing the received response. + synapse (bittensor.core.synapse.Synapse): The synapse object representing the received response. """ - logging.trace( - f"dendrite | <-- | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | {synapse.dendrite.status_code} | {synapse.dendrite.status_message}" - ) + if synapse.axon is not None and synapse.dendrite is not None: + logging.trace( + f"dendrite | <-- | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | {synapse.dendrite.status_code} | {synapse.dendrite.status_message}" + ) def query( self, *args, **kwargs - ) -> "List[Union[AsyncGenerator[Any, Any], Synapse, StreamingSynapse]]": + ) -> List[Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]]: """ Makes a synchronous request to multiple target Axons and returns the server responses. Cleanup is automatically handled and sessions are closed upon completed requests. Args: - axons (Union[List[Union['bittensor.AxonInfo', 'bittensor.axon']], Union['bittensor.AxonInfo', 'bittensor.axon']]): The list of target Axon information. - synapse (Synapse, optional): The Synapse object. Defaults to :func:`Synapse()`. + axons (Union[List[Union[bittensor.core.chain_data.axon_info.AxonInfo, 'bittensor.core.axon.Axon']], Union['bittensor.core.chain_data.axon_info.AxonInfo', 'bittensor.core.axon.Axon']]): The list of target Axon information. + synapse (bittensor.core.synapse.Synapse, optional): The Synapse object. Defaults to :func:`Synapse()`. timeout (float, optional): The request timeout duration in seconds. Defaults to ``12.0`` seconds. Returns: - Union[Synapse, List[Synapse]]: If a single target axon is provided, returns the response from that axon. If multiple target axons are provided, returns a list of responses from all target axons. + Union[bittensor.core.synapse.Synapse, List[bittensor.core.synapse.Synapse]]: If a single target axon is provided, returns the response from that axon. If multiple target axons are provided, returns a list of responses from all target axons. """ result = None try: @@ -354,13 +357,13 @@ def query( async def forward( self, - axons: "Union[List[Union[AxonInfo, Axon]], Union[AxonInfo, Axon]]", + axons: Union[List[Union["AxonInfo", "Axon"]], Union["AxonInfo", "Axon"]], synapse: "Synapse" = Synapse(), timeout: float = 12, deserialize: bool = True, run_async: bool = True, streaming: bool = False, - ) -> "List[Union[AsyncGenerator[Any, Any], Synapse, StreamingSynapse]]": + ) -> List[Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]]: """ Asynchronously sends requests to one or multiple Axons and collates their responses. @@ -376,11 +379,14 @@ async def forward( For example:: ... - wallet = bittensor.wallet() # Initialize a wallet - synapse = Synapse(...) # Create a synapse object that contains query data - dendrte = bittensor.dendrite(wallet = wallet) # Initialize a dendrite instance - axons = metagraph.axons # Create a list of axons to query - responses = await dendrite(axons, synapse) # Send the query to all axons and await the responses + import bittensor + wallet = bittensor.Wallet() # Initialize a wallet + synapse = bittensor.Synapse(...) # Create a synapse object that contains query data + dendrite = bittensor.dendrite(wallet = wallet) # Initialize a dendrite instance + netuid = ... # Provide subnet ID + metagraph = bittensor.Metagraph(netuid) # Initialize a metagraph instance + axons = metagraph.axons # Create a list of axons to query + responses = await dendrite(axons, synapse) # Send the query to all axons and await the responses When querying an Axon that sends back data in chunks using the Dendrite, this function returns an AsyncGenerator that yields each chunk as it is received. The generator can be @@ -389,22 +395,21 @@ async def forward( For example:: ... - dendrte = bittensor.dendrite(wallet = wallet) + dendrite = bittensor.dendrite(wallet = wallet) async for chunk in dendrite.forward(axons, synapse, timeout, deserialize, run_async, streaming): # Process each chunk here print(chunk) Args: - axons (Union[List[Union['bittensor.AxonInfo', 'bittensor.axon']], Union['bittensor.AxonInfo', 'bittensor.axon']]): - The target Axons to send requests to. Can be a single Axon or a list of Axons. - synapse (Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. + axons (Union[List[Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]], Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]]): The target Axons to send requests to. Can be a single Axon or a list of Axons. + synapse (bittensor.core.synapse.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. timeout (float, optional): Maximum duration to wait for a response from an Axon in seconds. Defaults to ``12.0``. deserialize (bool, optional): Determines if the received response should be deserialized. Defaults to ``True``. run_async (bool, optional): If ``True``, sends requests concurrently. Otherwise, sends requests sequentially. Defaults to ``True``. streaming (bool, optional): Indicates if the response is expected to be in streaming format. Defaults to ``False``. Returns: - Union[AsyncGenerator, Synapse, List[Synapse]]: If a single Axon is targeted, returns its response. + Union[AsyncGenerator, bittensor.core.synapse.Synapse, List[bittensor.core.synapse.Synapse]]: If a single `Axon` is targeted, returns its response. If multiple Axons are targeted, returns a list of their responses. """ is_list = True @@ -423,7 +428,7 @@ async def forward( async def query_all_axons( is_stream: bool, - ) -> Union[AsyncGenerator[Any, Any], Synapse, StreamingSynapse]: + ) -> Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]: """ Handles the processing of requests to all targeted axons, accommodating both streaming and non-streaming responses. @@ -436,24 +441,22 @@ async def query_all_axons( If ``True``, responses are handled in streaming mode. Returns: - List[Union[AsyncGenerator, Synapse, bittensor.StreamingSynapse]]: A list - containing the responses from each axon. The type of each response depends on the - streaming mode and the type of synapse used. + List[Union[AsyncGenerator, bittensor.core.synapse.Synapse, bittensor.core.stream.StreamingSynapse]]: A list containing the responses from each axon. The type of each response depends on the streaming mode and the type of synapse used. """ async def single_axon_response( - target_axon, - ) -> "Union[AsyncGenerator[Any, Any], Synapse, StreamingSynapse]": + target_axon: Union["AxonInfo", "Axon"], + ) -> Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]: """ Manages the request and response process for a single axon, supporting both streaming and non-streaming modes. This function is responsible for initiating a request to a single axon. Depending on the ``is_stream`` flag, it either uses ``call_stream`` for streaming responses or ``call`` for standard responses. The function handles the response processing, catering to the specifics of streaming or non-streaming data. Args: - target_axon: The target axon object to which the request is to be sent. This object contains the necessary information like IP address and port to formulate the request. + target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon): The target axon object to which the request is to be sent. This object contains the necessary information like IP address and port to formulate the request. Returns: - Union[AsyncGenerator, Synapse, bittensor.StreamingSynapse]: The response from the targeted axon. In streaming mode, an AsyncGenerator is returned, yielding data chunks. In non-streaming mode, a Synapse or StreamingSynapse object is returned containing the response. + Union[AsyncGenerator, bittensor.core.synapse.Synapse, bittensor.core.stream.StreamingSynapse]: The response from the targeted axon. In streaming mode, an AsyncGenerator is returned, yielding data chunks. In non-streaming mode, a Synapse or StreamingSynapse object is returned containing the response. """ if is_stream: # If in streaming mode, return the async_generator @@ -500,13 +503,13 @@ async def call( This function establishes a connection with a specified Axon, sends the encapsulated data through the Synapse object, waits for a response, processes it, and then returns the updated Synapse object. Args: - target_axon (Union['bittensor.AxonInfo', 'bittensor.axon']): The target Axon to send the request to. - synapse (Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. + target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]): The target Axon to send the request to. + synapse (bittensor.core.synapse.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. timeout (float, optional): Maximum duration to wait for a response from the Axon in seconds. Defaults to ``12.0``. deserialize (bool, optional): Determines if the received response should be deserialized. Defaults to ``True``. Returns: - Synapse: The Synapse object, updated with the response data from the Axon. + bittensor.core.synapse.Synapse: The Synapse object, updated with the response data from the Axon. """ # Record start time @@ -555,7 +558,7 @@ async def call( async def call_stream( self, - target_axon: "Union[AxonInfo, Axon]", + target_axon: Union["AxonInfo", "Axon"], synapse: "StreamingSynapse" = Synapse(), # type: ignore timeout: float = 12.0, deserialize: bool = True, @@ -569,14 +572,14 @@ async def call_stream( data to be transmitted. Args: - target_axon (Union['bittensor.AxonInfo', 'bittensor.axon']): The target Axon to send the request to. - synapse (Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. + target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]): The target Axon to send the request to. + synapse (bittensor.core.synapse.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. timeout (float, optional): Maximum duration to wait for a response (or a chunk of the response) from the Axon in seconds. Defaults to ``12.0``. deserialize (bool, optional): Determines if each received chunk should be deserialized. Defaults to ``True``. Yields: object: Each yielded object contains a chunk of the arbitrary response data from the Axon. - Synapse: After the AsyncGenerator has been exhausted, yields the final filled Synapse. + bittensor.core.synapse.Synapse: After the AsyncGenerator has been exhausted, yields the final filled Synapse. """ # Record start time @@ -644,12 +647,12 @@ def preprocess_synapse_for_request( Preprocesses the synapse for making a request. This includes building headers for Dendrite and Axon and signing the request. Args: - target_axon_info (bittensor.AxonInfo): The target axon information. - synapse (Synapse): The synapse object to be preprocessed. + target_axon_info (bittensor.core.chain_data.axon_info.AxonInfo): The target axon information. + synapse (bittensor.core.synapse.Synapse): The synapse object to be preprocessed. timeout (float, optional): The request timeout duration in seconds. Defaults to ``12.0`` seconds. Returns: - Synapse: The preprocessed synapse. + bittensor.core.synapse.Synapse: The preprocessed synapse. """ # Set the timeout for the synapse synapse.timeout = timeout @@ -676,9 +679,9 @@ def preprocess_synapse_for_request( def process_server_response( self, - server_response: aiohttp.ClientResponse, + server_response: "aiohttp.ClientResponse", json_response: dict, - local_synapse: Synapse, + local_synapse: "Synapse", ): """ Processes the server response, updates the local synapse state with the server's state and merges headers set by the server. @@ -686,7 +689,7 @@ def process_server_response( Args: server_response (object): The `aiohttp `_ response object from the server. json_response (dict): The parsed JSON response from the server. - local_synapse (Synapse): The local synapse object to be updated. + local_synapse (bittensor.core.synapse.Synapse): The local synapse object to be updated. Raises: None: But errors in attribute setting are silently ignored. @@ -781,7 +784,9 @@ async def __aexit__(self, exc_type, exc_value, traceback): Usage:: - async with bt.dendrite( wallet ) as dendrite: + import bittensor + wallet = bittensor.Wallet() + async with bittensor.dendrite( wallet ) as dendrite: await dendrite.some_async_method() Note: @@ -812,7 +817,7 @@ def __del__(self): class Dendrite(DendriteMixin, BaseModel): # type: ignore - def __init__(self, wallet: Optional[Union[Wallet, Keypair]] = None): + def __init__(self, wallet: Optional[Union["Wallet", "Keypair"]] = None): if use_torch(): torch.nn.Module.__init__(self) DendriteMixin.__init__(self, wallet) From e5480940b8f5eb5800282b31a1d06be4e02b4c3b Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 12:21:04 -0700 Subject: [PATCH 206/260] bittensor/core/metagraph.py --- bittensor/core/metagraph.py | 155 ++++++++++++++++++++++++++---------- 1 file changed, 113 insertions(+), 42 deletions(-) diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 66e5788ad..0c83c97de 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -73,7 +73,13 @@ def get_save_dir(network: str, netuid: int) -> str: str: Directory path. """ return os.path.expanduser( - f"~/.bittensor/metagraphs/network-{str(network)}/netuid-{str(netuid)}/" + os.path.join( + "~", + ".bittensor", + "metagraphs", + f"network-{str(network)}", + f"netuid-{str(netuid)}", + ) ) @@ -143,7 +149,8 @@ class MetagraphMixin(ABC): Example Usage: Initializing the metagraph to represent the current state of the Bittensor network:: - metagraph = bt.metagraph(netuid=config.netuid, network=subtensor.network, sync=False) + from bittensor.core.metagraph import Metagraph + metagraph = Metagraph(netuid=config.netuid, network=subtensor.network, sync=False) Synchronizing the metagraph with the network to reflect the latest state and neuron data:: @@ -154,6 +161,8 @@ class MetagraphMixin(ABC): total_stake = metagraph.S neuron_ranks = metagraph.R neuron_incentives = metagraph.I + axons = metagraph.axons + neurons = metagraph.neurons ... Maintaining a local copy of hotkeys for querying and interacting with network entities:: @@ -485,37 +494,38 @@ def sync( subtensor: Optional["Subtensor"] = None, ): """ - Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes - to reflect the latest data from the network, ensuring the metagraph represents the most current state of the network. + Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes to reflect the latest data from the network, ensuring the metagraph represents the most current state of the network. Args: - block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the latest block. - This allows for historical analysis or specific state examination of the network. - lite (bool): If True, a lite version of the metagraph is used for quicker synchronization. This is beneficial - when full detail is not necessary, allowing for reduced computational and time overhead. - subtensor (Optional[Subtensor]): An instance of the subtensor class from Bittensor, providing an - interface to the underlying blockchain data. If provided, this - instance is used for data retrieval during synchronization. - - Returns: - metagraph: The metagraph instance, updated to the state of the specified block or the latest network state. + block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the latest block. This allows for historical analysis or specific state examination of the network. + lite (bool): If True, a lite version of the metagraph is used for quicker synchronization. This is beneficial when full detail is not necessary, allowing for reduced computational and time overhead. + subtensor (Optional[bittensor.core.subtensor.Subtensor]): An instance of the subtensor class from Bittensor, providing an interface to the underlying blockchain data. If provided, this instance is used for data retrieval during synchronization. Example: Sync the metagraph with the latest block from the subtensor, using the lite version for efficiency:: + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() metagraph.sync(subtensor=subtensor) Sync with a specific block number for detailed analysis:: + from bittensor.core.subtensor import Subtensor + subtensor = Subtensor() metagraph.sync(block=12345, lite=False, subtensor=subtensor) NOTE: - If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for subtensor. - Light nodes are configured only to store the previous 300 blocks if connecting to finney or test networks. + If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for subtensor. Light nodes are configured only to store the previous 300 blocks if connecting to finney or test networks. For example:: + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor(network='archive') + current_block = subtensor.get_current_block() + history_block = current_block - 1200 - subtensor = bittensor.core.subtensor.Subtensor(network='archive') + metagraph.sync(block=history_block, lite=False, subtensor=subtensor) """ # Initialize subtensor @@ -542,7 +552,7 @@ def sync( if not lite: self._set_weights_and_bonds(subtensor=subtensor) - def _initialize_subtensor(self, subtensor): + def _initialize_subtensor(self, subtensor: "Subtensor"): """ Initializes the subtensor to be used for syncing the metagraph. @@ -551,10 +561,10 @@ def _initialize_subtensor(self, subtensor): If no subtensor is provided, this method is responsible for creating a new instance of the subtensor, configured according to the current network settings. Args: - subtensor: The subtensor instance provided for initialization. If ``None``, a new subtensor instance is created using the current network configuration. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance provided for initialization. If ``None``, a new subtensor instance is created using the current network configuration. Returns: - subtensor: The initialized subtensor instance, ready to be used for syncing the metagraph. + subtensor (bittensor.core.subtensor.Subtensor): The initialized subtensor instance, ready to be used for syncing the metagraph. Internal Usage: Used internally during the sync process to ensure a valid subtensor instance is available:: @@ -569,23 +579,27 @@ def _initialize_subtensor(self, subtensor): subtensor = Subtensor(network=self.network) return subtensor - def _assign_neurons(self, block, lite, subtensor): + def _assign_neurons(self, block: int, lite: bool, subtensor: "Subtensor"): """ Assigns neurons to the metagraph based on the provided block number and the lite flag. This method is responsible for fetching and setting the neuron data in the metagraph, which includes neuron attributes like UID, stake, trust, and other relevant information. Args: - block: The block number for which the neuron data needs to be fetched. If ``None``, the latest block data is used. - lite: A boolean flag indicating whether to use a lite version of the neuron data. The lite version typically includes essential information and is quicker to fetch and process. - subtensor: The subtensor instance used for fetching neuron data from the network. + block (int): The block number for which the neuron data needs to be fetched. If ``None``, the latest block data is used. + lite (bool): A boolean flag indicating whether to use a lite version of the neuron data. The lite version typically includes essential information and is quicker to fetch and process. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for fetching neuron data from the network. Internal Usage: Used internally during the sync process to fetch and set neuron data:: + from bittensor.core.subtensor import Subtensor + + block = 12345 + lite = False + subtensor = Subtensor() self._assign_neurons(block, lite, subtensor) """ - # TODO: Check and test the conditions for assigning neurons if lite: self.neurons = subtensor.neurons_lite(block=block, netuid=self.netuid) else: @@ -709,15 +723,15 @@ def _set_metagraph_attributes(self, block, subtensor): pass def _process_root_weights( - self, data, attribute: str, subtensor: "Subtensor" + self, data: List, attribute: str, subtensor: "Subtensor" ) -> Union[NDArray, "torch.nn.Parameter"]: """ Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` but is tailored for processing root weights, which have a different structure and significance in the network. Args: - data: The raw root weights data to be processed. - attribute: A string indicating the attribute type, here it's typically ``weights``. - subtensor: The subtensor instance used for additional data and context needed in processing. + data (list): The raw root weights data to be processed. + attribute (str): A string indicating the attribute type, here it's typically ``weights``. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for additional data and context needed in processing. Returns: A tensor parameter encapsulating the processed root weights data. @@ -769,7 +783,7 @@ def save(self) -> "Metagraph": Saves the current state of the metagraph to a file on disk. This function is crucial for persisting the current state of the network's metagraph, which can later be reloaded or analyzed. The save operation includes all neuron attributes and parameters, ensuring a complete snapshot of the metagraph's state. Returns: - metagraph: The metagraph instance after saving its state. + metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after saving its state. Example: Save the current state of the metagraph to the default directory:: @@ -813,7 +827,7 @@ def load(self): The method delegates to ``load_from_path``, supplying it with the directory path constructed from the metagraph's current ``network`` and ``netuid`` properties. This abstraction simplifies the process of loading the metagraph's state for the user, requiring no direct path specifications. Returns: - metagraph: The metagraph instance after loading its state from the default directory. + metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after loading its state from the default directory. Example: Load the metagraph state from the last saved snapshot in the default directory:: @@ -840,7 +854,7 @@ def load_from_path(self, dir_path: str) -> "Metagraph": dir_path (str): The directory path where the metagraph's state files are stored. This path should contain one or more saved state files, typically named in a format that includes the block number. Returns: - metagraph: The metagraph instance after loading its state from the specified directory path. + metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after loading its state from the specified directory path. Example: Load the metagraph state from a specific directory:: @@ -855,7 +869,6 @@ def load_from_path(self, dir_path: str) -> "Metagraph": contain valid data for the metagraph. It is essential to ensure that the directory path and the state files within it are accurate and consistent with the expected metagraph structure. """ - pass BaseClass: Union["torch.nn.Module", object] = torch.nn.Module if use_torch() else object @@ -867,16 +880,21 @@ def __init__( ): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. - This method is the entry point for creating a metagraph object, - which is a central component in representing the state of the Bittensor network. + This class require installed Torch. + This method is the entry point for creating a metagraph object, which is a central component in representing the state of the Bittensor network. + Args: netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within potentially multiple network configurations. network (str): The name of the network, which can indicate specific configurations or versions of the Bittensor network. lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain less detailed information but can be quicker to initialize and sync. sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + Example: Initializing a metagraph object for the Bittensor network with a specific network UID:: - metagraph = metagraph(netuid=123, network="finney", lite=True, sync=True) + + from bittensor.core.metagraph import Metagraph + + metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) """ torch.nn.Module.__init__(self) MetagraphMixin.__init__(self, netuid, network, lite, sync) @@ -941,18 +959,22 @@ def __init__( if sync: self.sync(block=None, lite=lite) - def _set_metagraph_attributes(self, block, subtensor: "Subtensor"): + def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): """ Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. This method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other neuron-specific information. Args: - block: The block number for which the metagraph attributes need to be set. If ``None``, the latest block data is used. - subtensor: The subtensor instance used for fetching the latest network data. + block (int): The block number for which the metagraph attributes need to be set. If ``None``, the latest block data is used. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for fetching the latest network data. Internal Usage: Used internally during the sync process to update the metagraph's attributes:: + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() + block = subtensor.get_current_block() self._set_metagraph_attributes(block, subtensor) """ @@ -1003,6 +1025,23 @@ def _set_metagraph_attributes(self, block, subtensor: "Subtensor"): self.axons = [n.axon_info for n in self.neurons] def load_from_path(self, dir_path: str) -> "Metagraph": + """ + Loads the metagraph state from a specified directory path. + + Args: + dir_path (str): The directory path where the state file is located. + + Returns: + metagraph (bittensor.core.metagraph.Metagraph): The current metagraph instance with the loaded state. + + Example: + from bittensor.core.metagraph import Metagraph + + netuid = 1 + metagraph = Metagraph(netuid=netuid) + + metagraph.load_from_path("/path/to/dir") + """ graph_file = latest_block_path(dir_path) state_dict = torch.load(graph_file) self.n = torch.nn.Parameter(state_dict["n"], requires_grad=False) @@ -1050,6 +1089,24 @@ class NonTorchMetagraph(MetagraphMixin): def __init__( self, netuid: int, network: str = "finney", lite: bool = True, sync: bool = True ): + """ + Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. + This class doesn't require installed Torch. + This method is the entry point for creating a metagraph object, which is a central component in representing the state of the Bittensor network. + + Args: + netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within potentially multiple network configurations. + network (str): The name of the network, which can indicate specific configurations or versions of the Bittensor network. + lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain less detailed information but can be quicker to initialize and sync. + sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + + Example: + Initializing a metagraph object for the Bittensor network with a specific network UID:: + + from bittensor.core.metagraph import Metagraph + + metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) + """ # super(metagraph, self).__init__() MetagraphMixin.__init__(self, netuid, network, lite, sync) @@ -1077,15 +1134,15 @@ def __init__( if sync: self.sync(block=None, lite=lite) - def _set_metagraph_attributes(self, block, subtensor): + def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): """ Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. This method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other neuron-specific information. Args: - block: The block number for which the metagraph attributes need to be set. If ``None``, the latest block data is used. - subtensor: The subtensor instance used for fetching the latest network data. + block (int): The block number for which the metagraph attributes need to be set. If ``None``, the latest block data is used. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for fetching the latest network data. Internal Usage: Used internally during the sync process to update the metagraph's attributes:: @@ -1140,6 +1197,20 @@ def _set_metagraph_attributes(self, block, subtensor): self.axons = [n.axon_info for n in self.neurons] def load_from_path(self, dir_path: str) -> "Metagraph": + """ + Loads the state of the Metagraph from a specified directory path. + + Args: + dir_path (str): The directory path where the metagraph's state file is located. + + Returns: + metagraph (bittensor.core.metagraph.Metagraph): An instance of the Metagraph with the state loaded from the file. + + Raises: + pickle.UnpicklingError: If there is an error unpickling the state file. + RuntimeError: If there is an error loading the state file using PyTorch. + ImportError: If there is an error importing PyTorch. + """ graph_filename = latest_block_path(dir_path) try: with open(graph_filename, "rb") as graph_file: From b29b6f2220f6545c41d0aa2a7a0b5cb6575cae52 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 12:31:24 -0700 Subject: [PATCH 207/260] bittensor/core/stream.py --- bittensor/core/stream.py | 51 +++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/bittensor/core/stream.py b/bittensor/core/stream.py index 38d903699..9e880ffa8 100644 --- a/bittensor/core/stream.py +++ b/bittensor/core/stream.py @@ -67,7 +67,7 @@ class BTStreamingResponse(_StreamingResponse): def __init__( self, - model: BTStreamingResponseModel, + model: "BTStreamingResponseModel", *, synapse: "Optional[StreamingSynapse]" = None, **kwargs, @@ -76,24 +76,22 @@ def __init__( Initializes the BTStreamingResponse with the given token streamer model. Args: - model: A BTStreamingResponseModel instance containing the token streamer callable, which is responsible for generating the content of the response. - synapse: The response Synapse to be used to update the response headers etc. + model (bittensor.core.stream.BTStreamingResponseModel): A BTStreamingResponseModel instance containing the token streamer callable, which is responsible for generating the content of the response. + synapse (bittensor.core.stream.StreamingSynapse): The response Synapse to be used to update the response headers etc. **kwargs: Additional keyword arguments passed to the parent StreamingResponse class. """ super().__init__(content=iter(()), **kwargs) self.token_streamer = model.token_streamer self.synapse = synapse - async def stream_response(self, send: Send): + async def stream_response(self, send: "Send"): """ Asynchronously streams the response by sending headers and calling the token streamer. - This method is responsible for initiating the response by sending the appropriate headers, including the - content type for event-streaming. It then calls the token streamer to generate the content and sends the - response body to the client. + This method is responsible for initiating the response by sending the appropriate headers, including the content type for event-streaming. It then calls the token streamer to generate the content and sends the response body to the client. Args: - send: A callable to send the response, provided by the ASGI server. + send (starlette.types.Send): A callable to send the response, provided by the ASGI server. """ headers = [(b"content-type", b"text/event-stream")] + self.raw_headers @@ -105,60 +103,55 @@ async def stream_response(self, send: Send): await send({"type": "http.response.body", "body": b"", "more_body": False}) - async def __call__(self, scope: Scope, receive: Receive, send: Send): + async def __call__(self, scope: "Scope", receive: "Receive", send: "Send"): """ - Asynchronously calls the stream_response method, allowing the BTStreamingResponse object to be used as an ASGI - application. + Asynchronously calls the :func:`stream_response method`, allowing the :func:`BTStreamingResponse` object to be used as an ASGI application. - This method is part of the ASGI interface and is called by the ASGI server to handle the request and send the - response. It delegates to the :func:`stream_response` method to perform the actual streaming process. + This method is part of the ASGI interface and is called by the ASGI server to handle the request and send the response. It delegates to the :func:`stream_response` method to perform the actual streaming process. Args: - scope: The scope of the request, containing information about the client, server, and request itself. - receive: A callable to receive the request, provided by the ASGI server. - send: A callable to send the response, provided by the ASGI server. + scope (starlette.types.Scope): The scope of the request, containing information about the client, server, and request itself. + receive (starlette.types.Receive): A callable to receive the request, provided by the ASGI server. + send (starlette.types.Send): A callable to send the response, provided by the ASGI server. """ await self.stream_response(send) @abstractmethod - async def process_streaming_response(self, response: ClientResponse): + async def process_streaming_response(self, response: "ClientResponse"): """ Abstract method that must be implemented by the subclass. This method should provide logic to handle the streaming response, such as parsing and accumulating data. - It is called as the response is being streamed from the network, and should be implemented to handle the specific - streaming data format and requirements of the subclass. + It is called as the response is being streamed from the network, and should be implemented to handle the specific streaming data format and requirements of the subclass. Args: - response: The response object to be processed, typically containing chunks of data. + response (aiohttp.ClientResponse): The response object to be processed, typically containing chunks of data. """ ... @abstractmethod - def extract_response_json(self, response: ClientResponse) -> dict: + def extract_response_json(self, response: "ClientResponse") -> dict: """ Abstract method that must be implemented by the subclass. This method should provide logic to extract JSON data from the response, including headers and content. - It is called after the response has been processed and is responsible for retrieving structured data - that can be used by the application. + It is called after the response has been processed and is responsible for retrieving structured data that can be used by the application. Args: - response: The response object from which to extract JSON data. + response (aiohttp.ClientResponse): The response object from which to extract JSON data. """ def create_streaming_response( self, token_streamer: Callable[[Send], Awaitable[None]] - ) -> BTStreamingResponse: + ) -> "BTStreamingResponse": """ Creates a streaming response using the provided token streamer. This method can be used by the subclass to create a response object that can be sent back to the client. - The token streamer should be implemented to generate the content of the response according to the specific - requirements of the subclass. + The token streamer should be implemented to generate the content of the response according to the specific requirements of the subclass. Args: - token_streamer: A callable that takes a send function and returns an awaitable. It's responsible for generating the content of the response. + token_streamer (Callable[[starlette.types.Send], Awaitable[None]]): A callable that takes a send function and returns an awaitable. It's responsible for generating the content of the response. Returns: - BTStreamingResponse: The streaming response object, ready to be sent to the client. + BTStreamingResponse (bittensor.core.stream.StreamingSynapse.BTStreamingResponse): The streaming response object, ready to be sent to the client. """ model_instance = BTStreamingResponseModel(token_streamer=token_streamer) From 8d6b37b4441b4dc7486b4ba948809a8f92387686 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 13:48:35 -0700 Subject: [PATCH 208/260] bittensor/core/subtensor.py --- bittensor/core/subtensor.py | 181 ++++++++++++++++++------------------ 1 file changed, 91 insertions(+), 90 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 789f839a6..94f832705 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. """ -The ``bittensor.subtensor`` module in Bittensor serves as a crucial interface for interacting with the Bittensor +The ``bittensor.core.subtensor`` module in Bittensor serves as a crucial interface for interacting with the Bittensor blockchain, facilitating a range of operations essential for the decentralized machine learning network. """ @@ -104,27 +104,24 @@ class Subtensor: Example Usage:: + from bittensor.core.subtensor import Subtensor + # Connect to the main Bittensor network (Finney). - finney_subtensor = subtensor(network='finney') + finney_subtensor = Subtensor(network='finney') # Close websocket connection with the Bittensor network. finney_subtensor.close() - # (Re)creates the websocket connection with the Bittensor network. - finney_subtensor.connect_websocket() - # Register a new neuron on the network. - wallet = bittensor_wallet.wallet(...) # Assuming a wallet instance is created. + wallet = bittensor_wallet.Wallet(...) # Assuming a wallet instance is created. + netuid = 1 success = finney_subtensor.register(wallet=wallet, netuid=netuid) # Set inter-neuronal weights for collaborative learning. success = finney_subtensor.set_weights(wallet=wallet, netuid=netuid, uids=[...], weights=[...]) - # Speculate by accumulating bonds in other promising neurons. - success = finney_subtensor.delegate(wallet=wallet, delegate_ss58=other_neuron_ss58, amount=bond_amount) - # Get the metagraph for a specific subnet using given subtensor connection - metagraph = subtensor.metagraph(netuid=netuid) + metagraph = finney_subtensor.metagraph(netuid=netuid) By facilitating these operations, the Subtensor class is instrumental in maintaining the decentralized intelligence and dynamic learning environment of the Bittensor network, as envisioned in its foundational @@ -149,10 +146,11 @@ def __init__( We strongly encourage users to run their own local subtensor node whenever possible. This increases decentralization and resilience of the network. In a future release, local subtensor will become the default and the fallback to ``finney`` removed. Please plan ahead for this change. We will provide detailed instructions on how to run a local subtensor node in the documentation in a subsequent release. Args: - network (str, optional): The network name to connect to (e.g., ``finney``, ``local``). This can also be the chain endpoint (e.g., ``wss://entrypoint-finney.opentensor.ai:443``) and will be correctly parsed into the network and chain endpoint. If not specified, defaults to the main Bittensor network. - config (bittensor.core.config.Config, optional): Configuration object for the subtensor. If not provided, a default configuration is used. - _mock (bool, optional): If set to ``True``, uses a mocked connection for testing purposes. - connection_timeout (int): The maximum time in seconds to keep the connection alive. + network (Optional[str]): The network name to connect to (e.g., ``finney``, ``local``). This can also be the chain endpoint (e.g., ``wss://entrypoint-finney.opentensor.ai:443``) and will be correctly parsed into the network and chain endpoint. If not specified, defaults to the main Bittensor network. + config (Optional[bittensor.core.config.Config]): Configuration object for the subtensor. If not provided, a default configuration is used. + _mock (bool): If set to ``True``, uses a mocked connection for testing purposes. Default is ``False``. + log_verbose (bool): Whether to enable verbose logging. If set to ``True``, detailed log information about the connection and network operations will be provided. Default is ``True``. + connection_timeout (int): The maximum time in seconds to keep the connection alive. Default is ``600``. This initialization sets up the connection to the specified Bittensor network, allowing for various blockchain operations such as neuron registration, stake management, and setting weights. """ @@ -260,7 +258,7 @@ def setup_config(network: Optional[str], config: "Config"): 5. Default network. Args: - network (str): The name of the Subtensor network. If None, the network and endpoint will be determined from the `config` object. + network (Optional[str]): The name of the Subtensor network. If None, the network and endpoint will be determined from the `config` object. config (bittensor.core.config.Config): The configuration object containing the network and chain endpoint settings. Returns: @@ -434,10 +432,10 @@ def query_subtensor( Args: name (str): The name of the storage function to query. block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): A list of parameters to pass to the query function. + params (Optional[List[object]]): A list of parameters to pass to the query function. Returns: - query_response (ScaleType): An object containing the requested data. + query_response (scalecodec.ScaleType): An object containing the requested data. This query function is essential for accessing detailed information about the network and its neurons, providing valuable insights into the state and dynamics of the Bittensor ecosystem. """ @@ -465,10 +463,10 @@ def query_map_subtensor( Args: name (str): The name of the map storage function to query. block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): A list of parameters to pass to the query function. + params (Optional[List[object]]): A list of parameters to pass to the query function. Returns: - QueryMapResult: An object containing the map-like data structure, or ``None`` if not found. + QueryMapResult (substrateinterface.base.QueryMapResult): An object containing the map-like data structure, or ``None`` if not found. This function is particularly useful for analyzing and understanding complex network structures and relationships within the Bittensor ecosystem, such as inter-neuronal connections and stake distributions. """ @@ -499,11 +497,11 @@ def query_runtime_api( Args: runtime_api (str): The name of the runtime API to query. method (str): The specific method within the runtime API to call. - params (Optional[List[ParamWithTypes]], optional): The parameters to pass to the method call. + params (Optional[List[ParamWithTypes]]): The parameters to pass to the method call. block (Optional[int]): The blockchain block number at which to perform the query. Returns: - Optional[bytes]: The Scale Bytes encoded result from the runtime API call, or ``None`` if the call fails. + Optional[str]: The Scale Bytes encoded result from the runtime API call, or ``None`` if the call fails. This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. """ @@ -573,7 +571,7 @@ def query_map( name: str, block: Optional[int] = None, params: Optional[list] = None, - ) -> QueryMapResult: + ) -> "QueryMapResult": """ Queries map storage from any module on the Bittensor blockchain. This function retrieves data structures that represent key-value mappings, essential for accessing complex and structured data within the blockchain modules. @@ -581,10 +579,10 @@ def query_map( module (str): The name of the module from which to query the map storage. name (str): The specific storage function within the module to query. block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): Parameters to be passed to the query. + params (Optional[List[object]]): Parameters to be passed to the query. Returns: - result (QueryMapResult): A data structure representing the map storage if found, ``None`` otherwise. + result (substrateinterface.base.QueryMapResult): A data structure representing the map storage if found, ``None`` otherwise. This function is particularly useful for retrieving detailed and structured data from various blockchain modules, offering insights into the network's state and the relationships between its different components. """ @@ -615,7 +613,7 @@ def query_constant( block (Optional[int]): The blockchain block number at which to query the constant. Returns: - Optional[ScaleType]: The value of the constant if found, ``None`` otherwise. + Optional[scalecodec.ScaleType]: The value of the constant if found, ``None`` otherwise. Constants queried through this function can include critical network parameters such as inflation rates, consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor network's operational parameters. """ @@ -647,10 +645,10 @@ def query_module( module (str): The name of the module from which to query data. name (str): The name of the storage function within the module. block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): A list of parameters to pass to the query function. + params (Optional[List[object]]): A list of parameters to pass to the query function. Returns: - Optional[ScaleType]: An object containing the requested data if found, ``None`` otherwise. + Optional[scalecodec.ScaleType]: An object containing the requested data if found, ``None`` otherwise. This versatile query function is key to accessing a wide range of data and insights from different parts of the Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. """ @@ -677,7 +675,7 @@ def metagraph( Args: netuid (int): The network UID of the subnet to query. - lite (bool, default=True): If true, returns a metagraph using a lightweight sync (no weights, no bonds). + lite (bool): If true, returns a metagraph using a lightweight sync (no weights, no bonds). Default is ``True``. block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. Returns: @@ -693,16 +691,18 @@ def metagraph( return metagraph @staticmethod - def determine_chain_endpoint_and_network(network: str): + def determine_chain_endpoint_and_network( + network: str, + ) -> Tuple[Optional[str], Optional[str]]: """Determines the chain endpoint and network from the passed network or chain_endpoint. Args: - network (str): The network flag. The choices are: ``-- finney`` (main network), ``-- archive`` - (archive network +300 blocks), ``-- local`` (local running network), ``-- test`` (test network). + network (str): The network flag. The choices are: ``finney`` (main network), ``archive`` (archive network +300 blocks), ``local`` (local running network), ``test`` (test network). + Returns: - network (str): The network flag. - chain_endpoint (str): The chain endpoint flag. If set, overrides the ``network`` argument. + Tuple[Optional[str], Optional[str]]: The network and chain endpoint flag. If passed, overrides the ``network`` argument. """ + if network is None: return None, None if network in ["finney", "local", "test", "archive"]: @@ -735,6 +735,7 @@ def determine_chain_endpoint_and_network(network: str): return "local", network else: return "unknown", network + return None, None def get_netuids_for_hotkey( self, hotkey_ss58: str, block: Optional[int] = None @@ -853,11 +854,11 @@ def set_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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_retries (int, optional): The number of maximum attempts to set weights. (Default: 5) + 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``. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. Default is ``False``. + max_retries (int): The number of maximum attempts to set weights. Default is ``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. @@ -899,13 +900,13 @@ def serve_axon( wait_for_finalization: bool = True, ) -> bool: """ - Registers an Axon serving endpoint on the Bittensor network for a specific neuron. This function is used to set up the Axon, a key component of a neuron that handles incoming queries and data processing tasks. + Registers an ``Axon`` serving endpoint on the Bittensor network for a specific neuron. This function is used to set up the Axon, a key component of a neuron that handles incoming queries and data processing tasks. Args: netuid (int): The unique identifier of the subnetwork. axon (bittensor.core.axon.Axon): The Axon instance to be registered for serving. - 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. + 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 ``True``. Returns: bool: ``True`` if the Axon serve registration is successful, False otherwise. @@ -988,7 +989,7 @@ def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int Args: netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. + block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. Returns: Optional[int]: The value of the SubnetworkN hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. @@ -1003,7 +1004,7 @@ def transfer( self, wallet: "Wallet", dest: str, - amount: Union[Balance, float], + amount: Union["Balance", float], wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, @@ -1014,10 +1015,10 @@ def transfer( Args: wallet (bittensor_wallet.Wallet): The wallet from which funds are being transferred. dest (str): The destination public key address. - amount (Union[Balance, float]): The amount of TAO to be transferred. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + amount (Union[bittensor.utils.balance.Balance, float]): The amount of TAO to be transferred. + 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``. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. Default is ``False``. Returns: transfer_extrinsic (bool): ``True`` if the transfer is successful, False otherwise. @@ -1037,7 +1038,7 @@ def transfer( # Community uses this method via `bittensor.api.extrinsics.prometheus.prometheus_extrinsic` def get_neuron_for_pubkey_and_subnet( self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfo]: + ) -> Optional["NeuronInfo"]: """ Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor network. @@ -1047,7 +1048,7 @@ def get_neuron_for_pubkey_and_subnet( block (Optional[int]): The blockchain block number at which to perform the query. Returns: - Optional[NeuronInfo]: Detailed information about the neuron if found, ``None`` otherwise. + Optional[bittensor.core.chain_data.neuron_info.NeuronInfo]: Detailed information about the neuron if found, ``None`` otherwise. This function is crucial for accessing specific neuron data and understanding its status, stake, and other attributes within a particular subnet of the Bittensor ecosystem. """ @@ -1060,17 +1061,17 @@ def get_neuron_for_pubkey_and_subnet( @networking.ensure_connected def neuron_for_uid( self, uid: Optional[int], netuid: int, block: Optional[int] = None - ) -> NeuronInfo: + ) -> "NeuronInfo": """ Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a neuron's attributes, including its stake, rank, and operational status. Args: - uid (int): The unique identifier of the neuron. + uid (Optional[int]): The unique identifier of the neuron. netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. + block (Optional[int]): The blockchain block number for the query. Returns: - NeuronInfo: Detailed information about the neuron if found, ``None`` otherwise. + bittensor.core.chain_data.neuron_info.NeuronInfo: Detailed information about the neuron if found, ``None`` otherwise. This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, offering insights into their roles in the network's consensus and validation mechanisms. """ @@ -1111,8 +1112,8 @@ def serve_prometheus( wallet (bittensor_wallet.Wallet): Bittensor wallet instance used for submitting the extrinsic. port (int): The port number on which Prometheus metrics are served. netuid (int): The unique identifier of the subnetwork. - wait_for_inclusion (bool, optional): If True, waits for the transaction to be included in a block. Defaults to ``False``. - wait_for_finalization (bool, optional): If True, waits for the transaction to be finalized. Defaults to ``True``. + wait_for_inclusion (bool): If True, waits for the transaction to be included in a block. Defaults to ``False``. + wait_for_finalization (bool): If True, waits for the transaction to be finalized. Defaults to ``True``. Returns: bool: Returns True if the Prometheus extrinsic is successfully processed, otherwise False. @@ -1129,16 +1130,16 @@ def serve_prometheus( # Community uses this method def get_subnet_hyperparameters( self, netuid: int, block: Optional[int] = None - ) -> Optional[Union[List, SubnetHyperparameters]]: + ) -> Optional[Union[List, "SubnetHyperparameters"]]: """ Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define the operational settings and rules governing the subnet's behavior. Args: netuid (int): The network UID of the subnet to query. - block (Optional[int], optional): The blockchain block number for the query. + block (Optional[int]): The blockchain block number for the query. Returns: - Optional[SubnetHyperparameters]: The subnet's hyperparameters, or ``None`` if not available. + Optional[bittensor.core.chain_data.subnet_hyperparameters.SubnetHyperparameters]: The subnet's hyperparameters, or ``None`` if not available. Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how they interact with the network's consensus and incentive mechanisms. """ @@ -1208,7 +1209,7 @@ def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: Args: netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. + block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. Returns: Optional[int]: The value of the Tempo hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. @@ -1247,7 +1248,7 @@ def min_allowed_weights( Args: netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. + block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. Returns: Optional[int]: The value of the MinAllowedWeights hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. @@ -1266,7 +1267,7 @@ def max_weight_limit( Args: netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. + block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. Returns: Optional[float]: The value of the MaxWeightsLimit hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. @@ -1279,17 +1280,17 @@ def max_weight_limit( # # Community uses this method. It is used in subtensor in neuron_info, and serving. def get_prometheus_info( self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[PrometheusInfo]: + ) -> Optional["PrometheusInfo"]: """ Returns the prometheus information for this hotkey account. Args: netuid (int): The unique identifier of the subnetwork. hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to retrieve the prometheus information from. If ``None``, the latest block is used. Default is ``None``. + block (Optional[int]): The block number to retrieve the prometheus information from. If ``None``, the latest block is used. Default is ``None``. Returns: - Optional[PrometheusInfo]: A PrometheusInfo object containing the prometheus information, or ``None`` if the prometheus information is not found. + Optional[bittensor.core.chain_data.prometheus_info.PrometheusInfo]: A PrometheusInfo object containing the prometheus information, or ``None`` if the prometheus information is not found. """ result = self.query_subtensor("Prometheus", block, [netuid, hotkey_ss58]) if result is not None and hasattr(result, "value"): @@ -1309,7 +1310,7 @@ def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: Args: netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number at which to check the subnet's existence. + block (Optional[int]): The blockchain block number at which to check the subnet's existence. Returns: bool: ``True`` if the subnet exists, False otherwise. @@ -1346,16 +1347,16 @@ def bonds( return b_map # Metagraph uses this method - def neurons(self, netuid: int, block: Optional[int] = None) -> List[NeuronInfo]: + def neurons(self, netuid: int, block: Optional[int] = None) -> List["NeuronInfo"]: """ Retrieves a list of all neurons within a specified subnet of the Bittensor network. This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and network interactions. Args: netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. + block (Optional[int]): The blockchain block number for the query. Returns: - List[NeuronInfo]: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. + List[bittensor.core.chain_data.neuron_info.NeuronInfo]: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. Understanding the distribution and status of neurons within a subnet is key to comprehending the network's decentralized structure and the dynamics of its consensus and governance processes. """ @@ -1381,10 +1382,10 @@ def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. Args: - block (Optional[int], optional): The blockchain block number for the query. + block (Optional[int]): The blockchain block number for the query. Returns: - int: The total number of subnets in the network. + Optional[int]: The total number of subnets in the network. Understanding the total number of subnets is essential for assessing the network's growth and the extent of its decentralized infrastructure. """ @@ -1397,7 +1398,7 @@ def get_subnets(self, block: Optional[int] = None) -> List[int]: Retrieves a list of all subnets currently active within the Bittensor network. This function provides an overview of the various subnets and their identifiers. Args: - block (Optional[int], optional): The blockchain block number for the query. + block (Optional[int]): The blockchain block number for the query. Returns: List[int]: A list of network UIDs representing each active subnet. @@ -1414,16 +1415,16 @@ def get_subnets(self, block: Optional[int] = None) -> List[int]: # Metagraph uses this method def neurons_lite( self, netuid: int, block: Optional[int] = None - ) -> List[NeuronInfoLite]: + ) -> List["NeuronInfoLite"]: """ Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network participation. Args: netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. + block (Optional[int]): The blockchain block number for the query. Returns: - List[NeuronInfoLite]: A list of simplified neuron information for the subnet. + List[bittensor.core.chain_data.neuron_info_lite.NeuronInfoLite]: A list of simplified neuron information for the subnet. This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis of the network's decentralized structure and neuron dynamics. """ @@ -1472,16 +1473,16 @@ def weights( # Used by community via `transfer_extrinsic` @networking.ensure_connected - def get_balance(self, address: str, block: Optional[int] = None) -> Balance: + def get_balance(self, address: str, block: Optional[int] = None) -> "Balance": """ Retrieves the token balance of a specific address within the Bittensor network. This function queries the blockchain to determine the amount of Tao held by a given account. Args: address (str): The Substrate address in ``ss58`` format. - block (int, optional): The blockchain block number at which to perform the query. + block (Optional[int]): The blockchain block number at which to perform the query. Returns: - Balance: The account balance at the specified block, represented as a Balance object. + bittensor.utils.balance.Balance: The account balance at the specified block, represented as a Balance object. This function is important for monitoring account holdings and managing financial transactions within the Bittensor ecosystem. It helps in assessing the economic status and capacity of network participants. """ @@ -1518,10 +1519,10 @@ def get_transfer_fee( Args: wallet (bittensor_wallet.Wallet): The wallet from which the transfer is initiated. dest (str): The ``SS58`` address of the destination account. - value (Union[Balance, float, int]): The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao (int) units. + value (Union[bittensor.utils.balance.Balance, float, int]): The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao (int) units. Returns: - Balance: The estimated transaction fee for the transfer, represented as a Balance object. + bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance object. Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the wallet has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. """ @@ -1570,7 +1571,7 @@ def get_existential_deposit( block (Optional[int]): Block number at which to query the deposit amount. If ``None``, the current block is used. Returns: - Optional[Balance]: The existential deposit amount, or ``None`` if the query fails. + Optional[bittensor.utils.balance.Balance]: The existential deposit amount, or ``None`` if the query fails. The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of storage and preventing the proliferation of dust accounts. """ @@ -1605,11 +1606,11 @@ def commit_weights( salt (List[int]): list of randomly generated integers as salt to generated weighted hash. 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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_retries (int, optional): The number of maximum attempts to commit weights. (Default: 5) + 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``. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. Default is ``False``. + max_retries (int): The number of maximum attempts to commit weights. Default is ``5``. Returns: Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string @@ -1682,11 +1683,11 @@ def reveal_weights( uids (np.ndarray): NumPy array of neuron UIDs for which weights are being revealed. 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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_retries (int, optional): The number of maximum attempts to reveal weights. (Default: 5) + 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``. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. Default is ``False``. + max_retries (int): The number of maximum attempts to reveal weights. Default is ``5``. Returns: Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string From 0d7af8dcdd0ff7b1469badb1aa411dfb667bb198 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 14:02:18 -0700 Subject: [PATCH 209/260] bittensor/core/synapse.py --- bittensor/core/synapse.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/bittensor/core/synapse.py b/bittensor/core/synapse.py index 53fa8a14c..69fb070c7 100644 --- a/bittensor/core/synapse.py +++ b/bittensor/core/synapse.py @@ -19,7 +19,7 @@ import json import sys import warnings -from typing import cast, Any, ClassVar, Dict, Optional, Tuple, Union +from typing import cast, Any, ClassVar, Optional, Union from pydantic import ( BaseModel, @@ -33,15 +33,15 @@ from bittensor.utils.btlogging import logging -def get_size(obj, seen=None) -> int: +def get_size(obj: Any, seen: Optional[set] = None) -> int: """ Recursively finds size of objects. This function traverses every item of a given object and sums their sizes to compute the total size. Args: - obj (any type): The object to get the size of. - seen (set): Set of object ids that have been calculated. + obj (Any): The object to get the size of. + seen (Optional[Set]): Set of object ids that have been calculated. Returns: int: The total size of the object. @@ -82,7 +82,7 @@ def cast_int(raw: str) -> int: return int(raw) if raw is not None else raw -def cast_float(raw: str) -> float: +def cast_float(raw: str) -> Optional[float]: """ Converts a string to a float, if the string is not ``None``. @@ -128,6 +128,8 @@ class TerminalInfo(BaseModel): Usage:: # Creating a TerminalInfo instance + from bittensor.core.synapse import TerminalInfo + terminal_info = TerminalInfo( status_code=200, status_message="Success", @@ -317,11 +319,14 @@ class Synapse(BaseModel): Example usage:: # Creating a Synapse instance with default values + from bittensor.core.synapse import Synapse + synapse = Synapse() # Setting properties and input synapse.timeout = 15.0 synapse.name = "MySynapse" + # Not setting fields that are not defined in your synapse class will result in an error, e.g.: synapse.dummy_input = 1 # This will raise an error because dummy_input is not defined in the Synapse class @@ -350,8 +355,8 @@ class Synapse(BaseModel): timeout (float): Total query length, set by the dendrite terminal. total_size (int): Total size of request body in bytes. header_size (int): Size of request header in bytes. - dendrite (TerminalInfo): Information about the dendrite terminal. - axon (TerminalInfo): Information about the axon terminal. + dendrite (:func:`TerminalInfo`): Information about the dendrite terminal. + axon (:func:`TerminalInfo`): Information about the axon terminal. computed_body_hash (str): Computed hash of the request body. required_hash_fields (List[str]): Fields required to compute the body hash. @@ -408,7 +413,7 @@ def deserialize(self) -> "CustomSynapse": return self @model_validator(mode="before") - def set_name_type(cls, values) -> dict: + def set_name_type(cls, values: dict) -> dict: values["name"] = cls.__name__ # type: ignore return values @@ -481,7 +486,7 @@ def set_name_type(cls, values) -> dict: repr=False, ) - required_hash_fields: ClassVar[Tuple[str, ...]] = () + required_hash_fields: ClassVar[tuple[str, ...]] = () _extract_total_size = field_validator("total_size", mode="before")(cast_int) @@ -756,7 +761,7 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: """ # Initialize the input dictionary with empty sub-dictionaries for 'axon' and 'dendrite' - inputs_dict: Dict[str, Union[Dict, Optional[str]]] = { + inputs_dict: dict[str, Union[dict, Optional[str]]] = { "axon": {}, "dendrite": {}, } @@ -835,7 +840,7 @@ def from_headers(cls, headers: dict) -> "Synapse": headers (dict): The dictionary of headers containing serialized Synapse information. Returns: - Synapse: A new instance of Synapse, reconstructed from the parsed header information, replicating the original instance's state. + bittensor.core.synapse.Synapse: A new instance of Synapse, reconstructed from the parsed header information, replicating the original instance's state. """ # Get the inputs dictionary from the headers From ba6957bb7d29c7b9d95ca721e28d8399e945b46f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 15:01:03 -0700 Subject: [PATCH 210/260] bittensor/core/tensor.py --- bittensor/core/tensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bittensor/core/tensor.py b/bittensor/core/tensor.py index c3bb29edb..85cb0241b 100644 --- a/bittensor/core/tensor.py +++ b/bittensor/core/tensor.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. import base64 -from typing import Optional, Union, List +from typing import Optional, Union import msgpack import msgpack_numpy @@ -104,7 +104,7 @@ def cast_dtype(raw: Union[None, np.dtype, "torch.dtype", str]) -> Optional[str]: ) -def cast_shape(raw: Union[None, List[int], str]) -> Optional[Union[str, list]]: +def cast_shape(raw: Union[None, list[int], str]) -> Optional[Union[str, list]]: """ Casts the raw value to a string representing the tensor shape. @@ -134,7 +134,7 @@ def cast_shape(raw: Union[None, List[int], str]) -> Optional[Union[str, list]]: class tensor: - def __new__(cls, tensor: Union[list, np.ndarray, "torch.Tensor"]): + def __new__(cls, tensor: Union[list, "np.ndarray", "torch.Tensor"]): if isinstance(tensor, list) or isinstance(tensor, np.ndarray): tensor = torch.tensor(tensor) if use_torch() else np.array(tensor) return Tensor.serialize(tensor_=tensor) @@ -155,10 +155,10 @@ class Tensor(BaseModel): def tensor(self) -> Union[np.ndarray, "torch.Tensor"]: return self.deserialize() - def tolist(self) -> List[object]: + def tolist(self) -> list[object]: return self.deserialize().tolist() - def numpy(self) -> "numpy.ndarray": + def numpy(self) -> "np.ndarray": return ( self.deserialize().detach().numpy() if use_torch() else self.deserialize() ) @@ -199,7 +199,7 @@ def serialize(tensor_: Union["np.ndarray", "torch.Tensor"]) -> "Tensor": tensor_ (np.array or torch.Tensor): The tensor to serialize. Returns: - Tensor: The serialized tensor. + :func:`Tensor`: The serialized tensor. Raises: Exception: If the serialization process encounters an error. @@ -234,7 +234,7 @@ def serialize(tensor_: Union["np.ndarray", "torch.Tensor"]) -> "Tensor": ) # Represents the shape of the tensor. - shape: List[int] = Field( + shape: list[int] = Field( title="shape", description="Tensor shape. This field defines the dimensions of the tensor as a list of integers, such as [10, 10] for a 2D tensor with shape (10, 10).", examples=[10, 10], From 8bf2d9a54f7f6b3347143c004883ef2f3b38a275 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 15:55:47 -0700 Subject: [PATCH 211/260] replace Tuple, Dict, List, Set to python native types --- bittensor/core/axon.py | 18 ++--- bittensor/core/chain_data/delegate_info.py | 18 ++--- .../core/chain_data/delegate_info_lite.py | 5 +- bittensor/core/chain_data/ip_info.py | 10 +-- bittensor/core/chain_data/neuron_info_lite.py | 4 +- bittensor/core/chain_data/prometheus_info.py | 3 +- .../core/chain_data/proposal_vote_data.py | 6 +- .../chain_data/scheduled_coldkey_swap_info.py | 8 +-- bittensor/core/chain_data/stake_info.py | 10 +-- bittensor/core/chain_data/subnet_info.py | 10 +-- bittensor/core/chain_data/utils.py | 14 ++-- bittensor/core/config.py | 18 ++--- bittensor/core/dendrite.py | 10 +-- bittensor/core/metagraph.py | 18 ++--- bittensor/core/subtensor.py | 72 +++++++++---------- bittensor/utils/__init__.py | 24 +++---- bittensor/utils/axon_utils.py | 22 ++++++ bittensor/utils/balance.py | 20 +++--- bittensor/utils/btlogging/format.py | 11 ++- bittensor/utils/mock/subtensor_mock.py | 57 ++++++++------- bittensor/utils/networking.py | 18 ++--- bittensor/utils/registration.py | 4 +- bittensor/utils/subnets.py | 6 +- bittensor/utils/weight_utils.py | 46 ++++++------ 24 files changed, 226 insertions(+), 206 deletions(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 7d82b8002..dd19e9d2e 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -30,7 +30,7 @@ import uuid import warnings from inspect import signature, Signature, Parameter -from typing import List, Optional, Tuple, Callable, Any, Dict, Awaitable +from typing import Any, Awaitable, Callable, Optional import uvicorn from bittensor_wallet import Wallet @@ -363,14 +363,14 @@ def __init__( self.thread_pool = PriorityThreadPoolExecutor( max_workers=self.config.axon.max_workers # type: ignore ) - self.nonces: Dict[str, int] = {} + self.nonces: dict[str, int] = {} # Request default functions. - self.forward_class_types: Dict[str, List[Signature]] = {} - self.blacklist_fns: Dict[str, Optional[Callable]] = {} - self.priority_fns: Dict[str, Optional[Callable]] = {} - self.forward_fns: Dict[str, Optional[Callable]] = {} - self.verify_fns: Dict[str, Optional[Callable]] = {} + self.forward_class_types: dict[str, list[Signature]] = {} + self.blacklist_fns: dict[str, Optional[Callable]] = {} + self.priority_fns: dict[str, Optional[Callable]] = {} + self.forward_fns: dict[str, Optional[Callable]] = {} + self.verify_fns: dict[str, Optional[Callable]] = {} # Instantiate FastAPI self.app = FastAPI() @@ -556,7 +556,7 @@ async def endpoint(*args, **kwargs): ] if blacklist_fn: blacklist_sig = Signature( - expected_params, return_annotation=Tuple[bool, str] + expected_params, return_annotation=tuple[bool, str] ) assert ( signature(blacklist_fn) == blacklist_sig @@ -1378,7 +1378,7 @@ async def priority(self, synapse: "Synapse"): async def submit_task( executor: "PriorityThreadPoolExecutor", priority: float - ) -> Tuple[float, Any]: + ) -> tuple[float, Any]: """ Submits the given priority function to the specified executor for asynchronous execution. The function will run in the provided executor and return the priority value along with the result. diff --git a/bittensor/core/chain_data/delegate_info.py b/bittensor/core/chain_data/delegate_info.py index d96cb8ef1..a849aedc2 100644 --- a/bittensor/core/chain_data/delegate_info.py +++ b/bittensor/core/chain_data/delegate_info.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Tuple, Optional, Any +from typing import Optional, Any from scalecodec.utils.ss58 import ss58_encode @@ -29,15 +29,15 @@ class DelegateInfo: hotkey_ss58: str # Hotkey of delegate total_stake: Balance # Total stake of the delegate - nominators: List[ - Tuple[str, Balance] + nominators: list[ + tuple[str, Balance] ] # List of nominators of the delegate and their stake owner_ss58: str # Coldkey of owner take: float # Take of the delegate as a percentage - validator_permits: List[ + validator_permits: list[ int ] # List of subnets that the delegate is allowed to validate on - registrations: List[int] # List of subnets that the delegate is registered on + registrations: tuple[int] # List of subnets that the delegate is registered on return_per_1000: Balance # Return per 1000 tao of the delegate over a day total_daily_return: Balance # Total daily return of the delegate @@ -66,7 +66,7 @@ def fix_decoded_values(cls, decoded: Any) -> "DelegateInfo": ) @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["DelegateInfo"]: + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DelegateInfo"]: """Returns a DelegateInfo object from a ``vec_u8``.""" if len(vec_u8) == 0: return None @@ -78,7 +78,7 @@ def from_vec_u8(cls, vec_u8: List[int]) -> Optional["DelegateInfo"]: return DelegateInfo.fix_decoded_values(decoded) @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["DelegateInfo"]: + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["DelegateInfo"]: """Returns a list of DelegateInfo objects from a ``vec_u8``.""" decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfo, is_vec=True) @@ -89,8 +89,8 @@ def list_from_vec_u8(cls, vec_u8: List[int]) -> List["DelegateInfo"]: @classmethod def delegated_list_from_vec_u8( - cls, vec_u8: List[int] - ) -> List[Tuple["DelegateInfo", "Balance"]]: + cls, vec_u8: list[int] + ) -> list[tuple["DelegateInfo", "Balance"]]: """Returns a list of Tuples of DelegateInfo objects, and Balance, from a ``vec_u8``. This is the list of delegates that the user has delegated to, and the amount of stake delegated. diff --git a/bittensor/core/chain_data/delegate_info_lite.py b/bittensor/core/chain_data/delegate_info_lite.py index f1bb1d72b..bf693c184 100644 --- a/bittensor/core/chain_data/delegate_info_lite.py +++ b/bittensor/core/chain_data/delegate_info_lite.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import List @dataclass @@ -22,8 +21,8 @@ class DelegateInfoLite: take: float # Take of the delegate as a percentage nominators: int # Count of the nominators of the delegate. owner_ss58: str # Coldkey of owner - registrations: List[int] # List of subnets that the delegate is registered on - validator_permits: List[ + registrations: list[int] # List of subnets that the delegate is registered on + validator_permits: list[ int ] # List of subnets that the delegate is allowed to validate on return_per_1000: int # Return per 1000 tao for the delegate over a day diff --git a/bittensor/core/chain_data/ip_info.py b/bittensor/core/chain_data/ip_info.py index 2a3254903..6bbfabe02 100644 --- a/bittensor/core/chain_data/ip_info.py +++ b/bittensor/core/chain_data/ip_info.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Dict, Optional, Any, Union +from typing import Optional, Any, Union from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType from bittensor.utils import networking as net @@ -21,7 +21,7 @@ class IPInfo: ip_type: int protocol: int - def encode(self) -> Dict[str, Any]: + def encode(self) -> dict[str, Any]: """Returns a dictionary of the IPInfo object that can be encoded.""" return { "ip": net.ip_to_int( @@ -31,7 +31,7 @@ def encode(self) -> Dict[str, Any]: } @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["IPInfo"]: + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["IPInfo"]: """Returns a IPInfo object from a ``vec_u8``.""" if len(vec_u8) == 0: return None @@ -43,7 +43,7 @@ def from_vec_u8(cls, vec_u8: List[int]) -> Optional["IPInfo"]: return IPInfo.fix_decoded_values(decoded) @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["IPInfo"]: + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["IPInfo"]: """Returns a list of IPInfo objects from a ``vec_u8``.""" decoded = from_scale_encoding(vec_u8, ChainDataType.IPInfo, is_vec=True) @@ -53,7 +53,7 @@ def list_from_vec_u8(cls, vec_u8: List[int]) -> List["IPInfo"]: return [IPInfo.fix_decoded_values(d) for d in decoded] @classmethod - def fix_decoded_values(cls, decoded: Dict) -> "IPInfo": + def fix_decoded_values(cls, decoded: dict) -> "IPInfo": """Returns a SubnetInfo object from a decoded IPInfo dictionary.""" return IPInfo( ip=net.int_to_ip(decoded["ip"]), diff --git a/bittensor/core/chain_data/neuron_info_lite.py b/bittensor/core/chain_data/neuron_info_lite.py index 018069dcc..48d9ed4ca 100644 --- a/bittensor/core/chain_data/neuron_info_lite.py +++ b/bittensor/core/chain_data/neuron_info_lite.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Dict, Optional +from typing import Optional import bt_decode import netaddr @@ -51,7 +51,7 @@ class NeuronInfoLite: active: int stake: "Balance" # mapping of coldkey to amount staked to this Neuron - stake_dict: Dict[str, "Balance"] + stake_dict: dict[str, "Balance"] total_stake: "Balance" rank: float emission: float diff --git a/bittensor/core/chain_data/prometheus_info.py b/bittensor/core/chain_data/prometheus_info.py index dcda31145..7cdccf83f 100644 --- a/bittensor/core/chain_data/prometheus_info.py +++ b/bittensor/core/chain_data/prometheus_info.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Dict from bittensor.utils import networking @@ -24,7 +23,7 @@ class PrometheusInfo: ip_type: int @classmethod - def fix_decoded_values(cls, prometheus_info_decoded: Dict) -> "PrometheusInfo": + def fix_decoded_values(cls, prometheus_info_decoded: dict) -> "PrometheusInfo": """Returns a PrometheusInfo object from a prometheus_info_decoded dictionary.""" prometheus_info_decoded["ip"] = networking.int_to_ip( int(prometheus_info_decoded["ip"]) diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py index 3d3aaed61..493bc2d79 100644 --- a/bittensor/core/chain_data/proposal_vote_data.py +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -1,4 +1,4 @@ -from typing import List, TypedDict +from typing import TypedDict # Senate / Proposal data @@ -16,6 +16,6 @@ class ProposalVoteData(TypedDict): index: int threshold: int - ayes: List[str] - nays: List[str] + ayes: list[str] + nays: list[str] end: int diff --git a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py index 6b2e58d23..7c0f6e7f8 100644 --- a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py +++ b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Optional, Any +from typing import Optional, Any from scalecodec.utils.ss58 import ss58_encode @@ -32,7 +32,7 @@ def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": ) @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["ScheduledColdkeySwapInfo"]: + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["ScheduledColdkeySwapInfo"]: """Returns a ScheduledColdkeySwapInfo object from a ``vec_u8``.""" if len(vec_u8) == 0: return None @@ -44,7 +44,7 @@ def from_vec_u8(cls, vec_u8: List[int]) -> Optional["ScheduledColdkeySwapInfo"]: return ScheduledColdkeySwapInfo.fix_decoded_values(decoded) @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["ScheduledColdkeySwapInfo"]: + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["ScheduledColdkeySwapInfo"]: """Returns a list of ScheduledColdkeySwapInfo objects from a ``vec_u8``.""" decoded = from_scale_encoding( vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True @@ -55,7 +55,7 @@ def list_from_vec_u8(cls, vec_u8: List[int]) -> List["ScheduledColdkeySwapInfo"] return [ScheduledColdkeySwapInfo.fix_decoded_values(d) for d in decoded] @classmethod - def decode_account_id_list(cls, vec_u8: List[int]) -> Optional[List[str]]: + def decode_account_id_list(cls, vec_u8: list[int]) -> Optional[list[str]]: """Decodes a list of AccountIds from vec_u8.""" decoded = from_scale_encoding( vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True diff --git a/bittensor/core/chain_data/stake_info.py b/bittensor/core/chain_data/stake_info.py index f1a8bd24c..8d3b5020f 100644 --- a/bittensor/core/chain_data/stake_info.py +++ b/bittensor/core/chain_data/stake_info.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Dict, Optional, Any +from typing import Optional, Any from scalecodec.utils.ss58 import ss58_encode @@ -37,7 +37,7 @@ def fix_decoded_values(cls, decoded: Any) -> "StakeInfo": ) @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["StakeInfo"]: + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["StakeInfo"]: """Returns a StakeInfo object from a ``vec_u8``.""" if len(vec_u8) == 0: return None @@ -50,8 +50,8 @@ def from_vec_u8(cls, vec_u8: List[int]) -> Optional["StakeInfo"]: @classmethod def list_of_tuple_from_vec_u8( - cls, vec_u8: List[int] - ) -> Dict[str, List["StakeInfo"]]: + cls, vec_u8: list[int] + ) -> dict[str, list["StakeInfo"]]: """Returns a list of StakeInfo objects from a ``vec_u8``.""" decoded: Optional[list[tuple[str, list[object]]]] = ( from_scale_encoding_using_type_string( @@ -70,7 +70,7 @@ def list_of_tuple_from_vec_u8( } @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["StakeInfo"]: + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["StakeInfo"]: """Returns a list of StakeInfo objects from a ``vec_u8``.""" decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo, is_vec=True) if decoded is None: diff --git a/bittensor/core/chain_data/subnet_info.py b/bittensor/core/chain_data/subnet_info.py index ffbcc4024..f1ce15187 100644 --- a/bittensor/core/chain_data/subnet_info.py +++ b/bittensor/core/chain_data/subnet_info.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Dict, Optional, Any, Union +from typing import Any, Optional, Union from scalecodec.utils.ss58 import ss58_encode @@ -29,13 +29,13 @@ class SubnetInfo: tempo: int modality: int # netuid -> topk percentile prunning score requirement (u16:MAX normalized.) - connection_requirements: Dict[str, float] + connection_requirements: dict[str, float] emission_value: float burn: Balance owner_ss58: str @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetInfo"]: + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetInfo"]: """Returns a SubnetInfo object from a ``vec_u8``.""" if len(vec_u8) == 0: return None @@ -47,7 +47,7 @@ def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetInfo"]: return SubnetInfo.fix_decoded_values(decoded) @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["SubnetInfo"]: + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["SubnetInfo"]: """Returns a list of SubnetInfo objects from a ``vec_u8``.""" decoded = from_scale_encoding( vec_u8, ChainDataType.SubnetInfo, is_vec=True, is_option=True @@ -59,7 +59,7 @@ def list_from_vec_u8(cls, vec_u8: List[int]) -> List["SubnetInfo"]: return [SubnetInfo.fix_decoded_values(d) for d in decoded] @classmethod - def fix_decoded_values(cls, decoded: Dict) -> "SubnetInfo": + def fix_decoded_values(cls, decoded: dict) -> "SubnetInfo": """Returns a SubnetInfo object from a decoded SubnetInfo dictionary.""" return SubnetInfo( netuid=decoded["netuid"], diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index ffdd7ef89..128fd83ea 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -1,7 +1,7 @@ """Chain data helper functions and data.""" from enum import Enum -from typing import Dict, List, Optional, Union +from typing import Optional, Union from scalecodec.base import RuntimeConfiguration, ScaleBytes from scalecodec.type_registry import load_type_registry_preset @@ -25,11 +25,11 @@ class ChainDataType(Enum): def from_scale_encoding( - input_: Union[List[int], bytes, "ScaleBytes"], + input_: Union[list[int], bytes, "ScaleBytes"], type_name: "ChainDataType", is_vec: bool = False, is_option: bool = False, -) -> Optional[Dict]: +) -> Optional[dict]: """ Decodes input_ data from SCALE encoding based on the specified type name and modifiers. @@ -55,8 +55,8 @@ def from_scale_encoding( def from_scale_encoding_using_type_string( - input_: Union[List[int], bytes, ScaleBytes], type_string: str -) -> Optional[Dict]: + input_: Union[list[int], bytes, ScaleBytes], type_string: str +) -> Optional[dict]: """ Decodes SCALE encoded data to a dictionary based on the provided type string. @@ -260,7 +260,7 @@ def from_scale_encoding_using_type_string( } -def decode_account_id(account_id_bytes: List) -> str: +def decode_account_id(account_id_bytes: list) -> str: """ Decodes an AccountId from bytes to a Base64 string using SS58 encoding. @@ -274,7 +274,7 @@ def decode_account_id(account_id_bytes: List) -> str: return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) -def process_stake_data(stake_data: List) -> Dict: +def process_stake_data(stake_data: list) -> dict: """ Processes stake data to decode account IDs and convert stakes from rao to Balance objects. diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 57e8892e5..a1f84c5be 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -22,7 +22,7 @@ import os import sys from copy import deepcopy -from typing import List, Optional, Dict, Any, TypeVar, Type +from typing import Any, TypeVar, Type, Optional import yaml from munch import DefaultMunch @@ -48,12 +48,12 @@ class Config(DefaultMunch): config (bittensor.config): Nested config object created from parser arguments. """ - __is_set: Dict[str, bool] + __is_set: dict[str, bool] def __init__( self, parser: argparse.ArgumentParser = None, - args: Optional[List[str]] = None, + args: Optional[list[str]] = None, strict: bool = False, default: Optional[Any] = None, ) -> None: @@ -243,7 +243,7 @@ def __split_params__(params: argparse.Namespace, _config: "Config"): @staticmethod def __parse_args__( - args: List[str], parser: argparse.ArgumentParser = None, strict: bool = False + args: list[str], parser: argparse.ArgumentParser = None, strict: bool = False ) -> argparse.Namespace: """Parses the passed args use the passed parser. @@ -343,13 +343,13 @@ def merge(self, b: "Config"): self._merge(self, b) @classmethod - def merge_all(cls, configs: List["Config"]) -> "Config": + def merge_all(cls, configs: list["Config"]) -> "Config": """ Merge all configs in the list into one config. If there is a conflict, the value from the last configuration in the list will take precedence. Args: - configs (list of bittensor.core.config.Config): List of configs to be merged. + configs (list[bittensor.core.config.Config]): List of configs to be merged. Returns: config (bittensor.core.config.Config): Merged config object. @@ -367,14 +367,14 @@ def is_set(self, param_name: str) -> bool: return self.get("__is_set")[param_name] def __check_for_missing_required_args( - self, parser: argparse.ArgumentParser, args: List[str] - ) -> List[str]: + self, parser: argparse.ArgumentParser, args: list[str] + ) -> list[str]: required_args = self.__get_required_args_from_parser(parser) missing_args = [arg for arg in required_args if not any(arg in s for s in args)] return missing_args @staticmethod - def __get_required_args_from_parser(parser: argparse.ArgumentParser) -> List[str]: + def __get_required_args_from_parser(parser: argparse.ArgumentParser) -> list[str]: required_args = [] for action in parser._actions: if action.required: diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index db4baab18..7dfdba987 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -20,7 +20,7 @@ import asyncio import time import uuid -from typing import Any, AsyncGenerator, Dict, List, Optional, Union, Type +from typing import Any, AsyncGenerator, Optional, Union, Type import aiohttp from bittensor_wallet import Wallet @@ -35,7 +35,7 @@ from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch -DENDRITE_ERROR_MAPPING: Dict[Type[Exception], tuple] = { +DENDRITE_ERROR_MAPPING: dict[Type[Exception], tuple] = { aiohttp.ClientConnectorError: ("503", "Service unavailable"), asyncio.TimeoutError: ("408", "Request timeout"), aiohttp.ClientResponseError: (None, "Client response error"), @@ -328,7 +328,7 @@ def _log_incoming_response(self, synapse: "Synapse"): def query( self, *args, **kwargs - ) -> List[Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]]: + ) -> list[Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]]: """ Makes a synchronous request to multiple target Axons and returns the server responses. @@ -357,13 +357,13 @@ def query( async def forward( self, - axons: Union[List[Union["AxonInfo", "Axon"]], Union["AxonInfo", "Axon"]], + axons: Union[list[Union["AxonInfo", "Axon"]], Union["AxonInfo", "Axon"]], synapse: "Synapse" = Synapse(), timeout: float = 12, deserialize: bool = True, run_async: bool = True, streaming: bool = False, - ) -> List[Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]]: + ) -> list[Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]]: """ Asynchronously sends requests to one or multiple Axons and collates their responses. diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 0c83c97de..ccc50eb7e 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -21,7 +21,7 @@ from abc import ABC, abstractmethod from os import listdir from os.path import join -from typing import List, Optional, Union, Tuple +from typing import Optional, Union import numpy as np from numpy.typing import NDArray @@ -172,7 +172,7 @@ class MetagraphMixin(ABC): netuid: int network: str - version: Union["torch.nn.Parameter", Tuple[NDArray]] + version: Union["torch.nn.Parameter", tuple[NDArray]] n: Union["torch.nn.Parameter", NDArray] block: Union["torch.nn.Parameter", NDArray] stake: Union["torch.nn.Parameter", NDArray] @@ -190,7 +190,7 @@ class MetagraphMixin(ABC): weights: Union["torch.nn.Parameter", NDArray] bonds: Union["torch.nn.Parameter", NDArray] uids: Union["torch.nn.Parameter", NDArray] - axons: List[AxonInfo] + axons: list[AxonInfo] @property def S(self) -> Union[NDArray, "torch.nn.Parameter"]: @@ -335,7 +335,7 @@ def W(self) -> Union[NDArray, "torch.nn.Parameter"]: return self.weights @property - def hotkeys(self) -> List[str]: + def hotkeys(self) -> list[str]: """ Represents a list of ``hotkeys`` for each neuron in the Bittensor network. @@ -353,7 +353,7 @@ def hotkeys(self) -> List[str]: return [axon.hotkey for axon in self.axons] @property - def coldkeys(self) -> List[str]: + def coldkeys(self) -> list[str]: """ Contains a list of ``coldkeys`` for each neuron in the Bittensor network. @@ -369,7 +369,7 @@ def coldkeys(self) -> List[str]: return [axon.coldkey for axon in self.axons] @property - def addresses(self) -> List[str]: + def addresses(self) -> list[str]: """ Provides a list of IP addresses for each neuron in the Bittensor network. These addresses are used for network communication, allowing neurons to connect, interact, and exchange information with each other. @@ -723,7 +723,7 @@ def _set_metagraph_attributes(self, block, subtensor): pass def _process_root_weights( - self, data: List, attribute: str, subtensor: "Subtensor" + self, data: list, attribute: str, subtensor: "Subtensor" ) -> Union[NDArray, "torch.nn.Parameter"]: """ Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` but is tailored for processing root weights, which have a different structure and significance in the network. @@ -955,7 +955,7 @@ def __init__( self.uids = torch.nn.Parameter( torch.tensor([], dtype=torch.int64), requires_grad=False ) - self.axons: List[AxonInfo] = [] + self.axons: list[AxonInfo] = [] if sync: self.sync(block=None, lite=lite) @@ -1130,7 +1130,7 @@ def __init__( self.weights = np.array([], dtype=np.float32) self.bonds = np.array([], dtype=np.int64) self.uids = np.array([], dtype=np.int64) - self.axons: List[AxonInfo] = [] + self.axons: list[AxonInfo] = [] if sync: self.sync(block=None, lite=lite) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 94f832705..18858e53d 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -23,7 +23,7 @@ import argparse import copy import socket -from typing import List, Dict, Union, Optional, Tuple, TypedDict, Any +from typing import Union, Optional, TypedDict, Any import numpy as np import scalecodec @@ -71,7 +71,7 @@ from bittensor.utils.btlogging import logging from bittensor.utils.weight_utils import generate_weight_hash -KEY_NONCE: Dict[str, int] = {} +KEY_NONCE: dict[str, int] = {} class ParamWithTypes(TypedDict): @@ -380,8 +380,8 @@ def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = Non @networking.ensure_connected def _encode_params( self, - call_definition: List["ParamWithTypes"], - params: Union[List[Any], Dict[str, Any]], + call_definition: list["ParamWithTypes"], + params: Union[list[Any], dict[str, Any]], ) -> str: """Returns a hex encoded string of the params using their types.""" param_data = scalecodec.ScaleBytes(b"") @@ -432,7 +432,7 @@ def query_subtensor( Args: name (str): The name of the storage function to query. block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]]): A list of parameters to pass to the query function. + params (Optional[list[object]]): A list of parameters to pass to the query function. Returns: query_response (scalecodec.ScaleType): An object containing the requested data. @@ -463,7 +463,7 @@ def query_map_subtensor( Args: name (str): The name of the map storage function to query. block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]]): A list of parameters to pass to the query function. + params (Optional[list[object]]): A list of parameters to pass to the query function. Returns: QueryMapResult (substrateinterface.base.QueryMapResult): An object containing the map-like data structure, or ``None`` if not found. @@ -488,7 +488,7 @@ def query_runtime_api( self, runtime_api: str, method: str, - params: Optional[Union[List[int], Dict[str, int]]], + params: Optional[Union[list[int], dict[str, int]]], block: Optional[int] = None, ) -> Optional[str]: """ @@ -497,7 +497,7 @@ def query_runtime_api( Args: runtime_api (str): The name of the runtime API to query. method (str): The specific method within the runtime API to call. - params (Optional[List[ParamWithTypes]]): The parameters to pass to the method call. + params (Optional[list[ParamWithTypes]]): The parameters to pass to the method call. block (Optional[int]): The blockchain block number at which to perform the query. Returns: @@ -539,7 +539,7 @@ def query_runtime_api( @networking.ensure_connected def state_call( self, method: str, data: str, block: Optional[int] = None - ) -> Dict[Any, Any]: + ) -> dict[Any, Any]: """ Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. This function is typically used for advanced queries that require specific method calls and data inputs. @@ -549,13 +549,13 @@ def state_call( block (Optional[int]): The blockchain block number at which to perform the state call. Returns: - result (Dict[Any, Any]): The result of the rpc call. + result (dict[Any, Any]): The result of the rpc call. The state call function provides a more direct and flexible way of querying blockchain data, useful for specific use cases where standard queries are insufficient. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) - def make_substrate_call_with_retry() -> Dict[Any, Any]: + def make_substrate_call_with_retry() -> dict[Any, Any]: block_hash = None if block is None else self.substrate.get_block_hash(block) return self.substrate.rpc_request( method="state_call", @@ -579,7 +579,7 @@ def query_map( module (str): The name of the module from which to query the map storage. name (str): The specific storage function within the module to query. block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]]): Parameters to be passed to the query. + params (Optional[list[object]]): Parameters to be passed to the query. Returns: result (substrateinterface.base.QueryMapResult): A data structure representing the map storage if found, ``None`` otherwise. @@ -645,7 +645,7 @@ def query_module( module (str): The name of the module from which to query data. name (str): The name of the storage function within the module. block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]]): A list of parameters to pass to the query function. + params (Optional[list[object]]): A list of parameters to pass to the query function. Returns: Optional[scalecodec.ScaleType]: An object containing the requested data if found, ``None`` otherwise. @@ -693,14 +693,14 @@ def metagraph( @staticmethod def determine_chain_endpoint_and_network( network: str, - ) -> Tuple[Optional[str], Optional[str]]: + ) -> tuple[Optional[str], Optional[str]]: """Determines the chain endpoint and network from the passed network or chain_endpoint. Args: network (str): The network flag. The choices are: ``finney`` (main network), ``archive`` (archive network +300 blocks), ``local`` (local running network), ``test`` (test network). Returns: - Tuple[Optional[str], Optional[str]]: The network and chain endpoint flag. If passed, overrides the ``network`` argument. + tuple[Optional[str], Optional[str]]: The network and chain endpoint flag. If passed, overrides the ``network`` argument. """ if network is None: @@ -739,7 +739,7 @@ def determine_chain_endpoint_and_network( def get_netuids_for_hotkey( self, hotkey_ss58: str, block: Optional[int] = None - ) -> List[int]: + ) -> list[int]: """ Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function identifies the specific subnets within the Bittensor network where the neuron associated with the hotkey is active. @@ -748,7 +748,7 @@ def get_netuids_for_hotkey( block (Optional[int]): The blockchain block number at which to perform the query. Returns: - List[int]: A list of netuids where the neuron is a member. + list[int]: A list of netuids where the neuron is a member. """ result = self.query_map_subtensor("IsNetworkMember", block, [hotkey_ss58]) return ( @@ -845,7 +845,7 @@ def set_weights( wait_for_finalization: bool = False, prompt: bool = False, max_retries: int = 5, - ) -> Tuple[bool, str]: + ) -> tuple[bool, str]: """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or trust a neuron places on other neurons in the network, which is a fundamental aspect of Bittensor's decentralized learning architecture. @@ -861,7 +861,7 @@ def set_weights( max_retries (int): The number of maximum attempts to set weights. Default is ``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. + tuple[bool, str]: ``True`` if the setting of weights is successful, False otherwise. And `msg`, 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】. """ @@ -1130,7 +1130,7 @@ def serve_prometheus( # Community uses this method def get_subnet_hyperparameters( self, netuid: int, block: Optional[int] = None - ) -> Optional[Union[List, "SubnetHyperparameters"]]: + ) -> Optional[Union[list, "SubnetHyperparameters"]]: """ Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define the operational settings and rules governing the subnet's behavior. @@ -1323,7 +1323,7 @@ def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: # Metagraph uses this method def bonds( self, netuid: int, block: Optional[int] = None - ) -> List[Tuple[int, List[Tuple[int, int]]]]: + ) -> list[tuple[int, list[tuple[int, int]]]]: """ Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. Bonds represent the investments or commitments made by neurons in one another, indicating a level of trust and perceived value. This bonding mechanism is integral to the network's market-based approach to measuring and rewarding machine intelligence. @@ -1332,7 +1332,7 @@ def bonds( block (Optional[int]): The blockchain block number for the query. Returns: - List[Tuple[int, List[Tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its bonds with other neurons. + list[tuple[int, list[tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its bonds with other neurons. Understanding bond distributions is crucial for analyzing the trust dynamics and market behavior within the subnet. It reflects how neurons recognize and invest in each other's intelligence and contributions, supporting diverse and niche systems within the Bittensor ecosystem. """ @@ -1347,7 +1347,7 @@ def bonds( return b_map # Metagraph uses this method - def neurons(self, netuid: int, block: Optional[int] = None) -> List["NeuronInfo"]: + def neurons(self, netuid: int, block: Optional[int] = None) -> list["NeuronInfo"]: """ Retrieves a list of all neurons within a specified subnet of the Bittensor network. This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and network interactions. @@ -1356,7 +1356,7 @@ def neurons(self, netuid: int, block: Optional[int] = None) -> List["NeuronInfo" block (Optional[int]): The blockchain block number for the query. Returns: - List[bittensor.core.chain_data.neuron_info.NeuronInfo]: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. + list[bittensor.core.chain_data.neuron_info.NeuronInfo]: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. Understanding the distribution and status of neurons within a subnet is key to comprehending the network's decentralized structure and the dynamics of its consensus and governance processes. """ @@ -1393,7 +1393,7 @@ def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: return getattr(_result, "value", None) # Metagraph uses this method - def get_subnets(self, block: Optional[int] = None) -> List[int]: + def get_subnets(self, block: Optional[int] = None) -> list[int]: """ Retrieves a list of all subnets currently active within the Bittensor network. This function provides an overview of the various subnets and their identifiers. @@ -1401,7 +1401,7 @@ def get_subnets(self, block: Optional[int] = None) -> List[int]: block (Optional[int]): The blockchain block number for the query. Returns: - List[int]: A list of network UIDs representing each active subnet. + list[int]: A list of network UIDs representing each active subnet. This function is valuable for understanding the network's structure and the diversity of subnets available for neuron participation and collaboration. """ @@ -1415,7 +1415,7 @@ def get_subnets(self, block: Optional[int] = None) -> List[int]: # Metagraph uses this method def neurons_lite( self, netuid: int, block: Optional[int] = None - ) -> List["NeuronInfoLite"]: + ) -> list["NeuronInfoLite"]: """ Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network participation. @@ -1424,7 +1424,7 @@ def neurons_lite( block (Optional[int]): The blockchain block number for the query. Returns: - List[bittensor.core.chain_data.neuron_info_lite.NeuronInfoLite]: A list of simplified neuron information for the subnet. + list[bittensor.core.chain_data.neuron_info_lite.NeuronInfoLite]: A list of simplified neuron information for the subnet. This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis of the network's decentralized structure and neuron dynamics. """ @@ -1448,7 +1448,7 @@ def neurons_lite( # Used in the `neurons` method which is used in metagraph.py def weights( self, netuid: int, block: Optional[int] = None - ) -> List[Tuple[int, List[Tuple[int, int]]]]: + ) -> list[tuple[int, list[tuple[int, int]]]]: """ Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust and value assignment mechanisms. @@ -1457,7 +1457,7 @@ def weights( block (Optional[int]): The blockchain block number for the query. Returns: - List[Tuple[int, List[Tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its assigned weights. + list[tuple[int, list[tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its assigned weights. The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, influencing their influence and reward allocation within the subnet. """ @@ -1587,7 +1587,7 @@ def commit_weights( self, wallet: "Wallet", netuid: int, - salt: List[int], + salt: list[int], uids: Union[NDArray[np.int64], list], weights: Union[NDArray[np.int64], list], version_key: int = settings.version_as_int, @@ -1595,7 +1595,7 @@ def commit_weights( wait_for_finalization: bool = False, prompt: bool = False, max_retries: int = 5, - ) -> Tuple[bool, str]: + ) -> tuple[bool, str]: """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. This action serves as a commitment or snapshot of the neuron's current weight distribution. @@ -1603,7 +1603,7 @@ def commit_weights( Args: wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. netuid (int): The unique identifier of the subnet. - salt (List[int]): list of randomly generated integers as salt to generated weighted hash. + salt (list[int]): list of randomly generated integers as salt to generated weighted hash. 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.``. @@ -1613,7 +1613,7 @@ def commit_weights( max_retries (int): The number of maximum attempts to commit weights. Default is ``5``. Returns: - Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string + tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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, @@ -1672,7 +1672,7 @@ def reveal_weights( wait_for_finalization: bool = False, prompt: bool = False, max_retries: int = 5, - ) -> Tuple[bool, str]: + ) -> tuple[bool, str]: """ Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. This action serves as a revelation of the neuron's previously committed weight distribution. @@ -1690,7 +1690,7 @@ def reveal_weights( max_retries (int): The number of maximum attempts to reveal weights. Default is ``5``. Returns: - Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string + tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string value describing the success or potential error. This function allows neurons to reveal their previously committed weight distribution, ensuring transparency diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index d4fe49dde..39f30a568 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. import hashlib -from typing import List, Dict, Literal, Union, Optional +from typing import Literal, Union, Optional import scalecodec from substrateinterface import Keypair as Keypair @@ -31,9 +31,9 @@ U64_MAX = 18446744073709551615 -def ss58_to_vec_u8(ss58_address: str) -> List[int]: +def ss58_to_vec_u8(ss58_address: str) -> list[int]: ss58_bytes: bytes = ss58_address_to_bytes(ss58_address) - encoded_address: List[int] = [int(byte) for byte in ss58_bytes] + encoded_address: list[int] = [int(byte) for byte in ss58_bytes] return encoded_address @@ -56,20 +56,20 @@ def strtobool(val: str) -> Union[bool, Literal["==SUPRESS=="]]: def _get_explorer_root_url_by_network_from_map( - network: str, network_map: Dict[str, Dict[str, str]] -) -> Optional[Dict[str, str]]: + network: str, network_map: dict[str, dict[str, str]] +) -> Optional[dict[str, str]]: """ Returns the explorer root url for the given network name from the given network map. Args: network(str): The network to get the explorer url for. - network_map(Dict[str, str]): The network map to get the explorer url from. + network_map(dict[str, str]): The network map to get the explorer url from. Returns: The explorer url for the given network. Or None if the network is not in the network map. """ - explorer_urls: Optional[Dict[str, str]] = {} + explorer_urls: Optional[dict[str, str]] = {} for entity_nm, entity_network_map in network_map.items(): if network in entity_network_map: explorer_urls[entity_nm] = entity_network_map[network] @@ -78,24 +78,24 @@ def _get_explorer_root_url_by_network_from_map( def get_explorer_url_for_network( - network: str, block_hash: str, network_map: Dict[str, Dict[str, str]] -) -> Optional[Dict[str, str]]: + network: str, block_hash: str, network_map: dict[str, dict[str, str]] +) -> Optional[dict[str, str]]: """ Returns the explorer url for the given block hash and network. Args: network(str): The network to get the explorer url for. block_hash(str): The block hash to get the explorer url for. - network_map(Dict[str, Dict[str, str]]): The network maps to get the explorer urls from. + network_map(dict[str, dict[str, str]]): The network maps to get the explorer urls from. Returns: The explorer url for the given block hash and network. Or None if the network is not known. """ - explorer_urls: Optional[Dict[str, str]] = {} + explorer_urls: Optional[dict[str, str]] = {} # Will be None if the network is not known. i.e. not in network_map - explorer_root_urls: Optional[Dict[str, str]] = ( + explorer_root_urls: Optional[dict[str, str]] = ( _get_explorer_root_url_by_network_from_map(network, network_map) ) diff --git a/bittensor/utils/axon_utils.py b/bittensor/utils/axon_utils.py index c380f14ae..3c73c8080 100644 --- a/bittensor/utils/axon_utils.py +++ b/bittensor/utils/axon_utils.py @@ -22,6 +22,16 @@ def allowed_nonce_window_ns(current_time_ns: int, synapse_timeout: Optional[float]): + """ + Calculates the allowed window for a nonce in nanoseconds. + + Args: + current_time_ns (int): The current time in nanoseconds. + synapse_timeout (Optional[float]): The optional timeout for the synapse in seconds. If None, it defaults to 0. + + Returns: + int: The allowed nonce window in nanoseconds. + """ synapse_timeout_ns = (synapse_timeout or 0) * NANOSECONDS_IN_SECOND allowed_window_ns = current_time_ns - ALLOWED_DELTA - synapse_timeout_ns return allowed_window_ns @@ -30,6 +40,18 @@ def allowed_nonce_window_ns(current_time_ns: int, synapse_timeout: Optional[floa def calculate_diff_seconds( current_time: int, synapse_timeout: Optional[float], synapse_nonce: int ): + """ + Calculates the difference in seconds between the current time and the synapse nonce, + and also returns the allowed delta in seconds. + + Args: + current_time (int): The current time in nanoseconds. + synapse_timeout (Optional[float]): The optional timeout for the synapse in seconds. + synapse_nonce (int): The nonce value for the synapse in nanoseconds. + + Returns: + tuple: A tuple containing the difference in seconds (float) and the allowed delta in seconds (float). + """ synapse_timeout_ns = (synapse_timeout or 0) * NANOSECONDS_IN_SECOND diff_seconds = (current_time - synapse_nonce) / NANOSECONDS_IN_SECOND allowed_delta_seconds = (ALLOWED_DELTA + synapse_timeout_ns) / NANOSECONDS_IN_SECOND diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index efe21885e..016db373a 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -27,10 +27,10 @@ class Balance: It provides methods to convert between these units, as well as to perform arithmetic and comparison operations. Attributes: - unit: A string representing the symbol for the tao unit. - rao_unit: A string representing the symbol for the rao unit. - rao: An integer that stores the balance in rao units. - tao: A float property that gives the balance in tao units. + unit (str): A string representing the symbol for the tao unit. + rao_unit (str): A string representing the symbol for the rao unit. + rao (int): An integer that stores the balance in rao units. + tao (float): A float property that gives the balance in tao units. """ unit: str = settings.TAO_SYMBOL # This is the tao unit @@ -230,9 +230,9 @@ def __abs__(self): @staticmethod def from_float(amount: float): """ - Given tao (float), return Balance object with rao(int) and tao(float), where rao = int(tao*pow(10,9)) + Given tao, return :func:`Balance` object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) Args: - amount: The amount in tao. + amount (float): The amount in tao. Returns: A Balance object representing the given amount. @@ -243,10 +243,10 @@ def from_float(amount: float): @staticmethod def from_tao(amount: float): """ - Given tao (float), return Balance object with rao(int) and tao(float), where rao = int(tao*pow(10,9)) + Given tao, return Balance object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) Args: - amount: The amount in tao. + amount (float): The amount in tao. Returns: A Balance object representing the given amount. @@ -257,10 +257,10 @@ def from_tao(amount: float): @staticmethod def from_rao(amount: int): """ - Given rao (int), return Balance object with rao(int) and tao(float), where rao = int(tao*pow(10,9)) + Given rao, return Balance object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) Args: - amount: The amount in rao. + amount (int): The amount in rao. Returns: A Balance object representing the given amount. diff --git a/bittensor/utils/btlogging/format.py b/bittensor/utils/btlogging/format.py index cbde7e9eb..1ca5d15e2 100644 --- a/bittensor/utils/btlogging/format.py +++ b/bittensor/utils/btlogging/format.py @@ -23,7 +23,6 @@ import logging import time -from typing import Dict from colorama import init, Fore, Back, Style @@ -51,14 +50,14 @@ def _success(self, message: str, *args, **kws): logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") logging.Logger.trace = _trace -emoji_map: Dict[str, str] = { +emoji_map: dict[str, str] = { ":white_heavy_check_mark:": "✅", ":cross_mark:": "❌", ":satellite:": "🛰️", } -color_map: Dict[str, str] = { +color_map: dict[str, str] = { "": Fore.RED, "": Style.RESET_ALL, "": Fore.BLUE, @@ -68,7 +67,7 @@ def _success(self, message: str, *args, **kws): } -log_level_color_prefix: Dict[int, str] = { +log_level_color_prefix: dict[int, str] = { logging.NOTSET: Fore.RESET, logging.TRACE: Fore.MAGENTA, logging.DEBUG: Fore.BLUE, @@ -80,12 +79,12 @@ def _success(self, message: str, *args, **kws): } -LOG_FORMATS: Dict[int, str] = { +LOG_FORMATS: dict[int, str] = { level: f"{Fore.BLUE}%(asctime)s{Fore.RESET} | {Style.BRIGHT}{color}%(levelname)s\033[0m | %(message)s" for level, color in log_level_color_prefix.items() } -LOG_TRACE_FORMATS: Dict[int, str] = { +LOG_TRACE_FORMATS: dict[int, str] = { level: f"{Fore.BLUE}%(asctime)s{Fore.RESET}" f" | {Style.BRIGHT}{color}%(levelname)s{Fore.RESET}{Back.RESET}{Style.RESET_ALL}" f" | %(name)s:%(filename)s:%(lineno)s" diff --git a/bittensor/utils/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py index bd42cf79a..817be0843 100644 --- a/bittensor/utils/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -19,8 +19,7 @@ from dataclasses import dataclass from hashlib import sha256 from types import SimpleNamespace -from typing import Any, Dict, List, Optional, Tuple, Union -from typing import TypedDict +from typing import Any, Optional, Union, TypedDict from unittest.mock import MagicMock from bittensor_wallet import Wallet @@ -125,12 +124,12 @@ class MockSubtensorValue: class MockMapResult: - records: Optional[List[Tuple[MockSubtensorValue, MockSubtensorValue]]] + records: Optional[list[tuple[MockSubtensorValue, MockSubtensorValue]]] def __init__( self, records: Optional[ - List[Tuple[Union[Any, MockSubtensorValue], Union[Any, MockSubtensorValue]]] + list[tuple[Union[Any, MockSubtensorValue], Union[Any, MockSubtensorValue]]] ] = None, ): _records = [ @@ -159,25 +158,25 @@ def __iter__(self): class MockSystemState(TypedDict): - Account: Dict[str, Dict[int, int]] # address -> block -> balance + Account: dict[str, dict[int, int]] # address -> block -> balance class MockSubtensorState(TypedDict): - Rho: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> rho - Kappa: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> kappa - Difficulty: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> difficulty - ImmunityPeriod: Dict[ - int, Dict[BlockNumber, int] + Rho: dict[int, dict[BlockNumber, int]] # netuid -> block -> rho + Kappa: dict[int, dict[BlockNumber, int]] # netuid -> block -> kappa + Difficulty: dict[int, dict[BlockNumber, int]] # netuid -> block -> difficulty + ImmunityPeriod: dict[ + int, dict[BlockNumber, int] ] # netuid -> block -> immunity_period - ValidatorBatchSize: Dict[ - int, Dict[BlockNumber, int] + ValidatorBatchSize: dict[ + int, dict[BlockNumber, int] ] # netuid -> block -> validator_batch_size - Active: Dict[int, Dict[BlockNumber, bool]] # (netuid, uid), block -> active - Stake: Dict[str, Dict[str, Dict[int, int]]] # (hotkey, coldkey) -> block -> stake + Active: dict[int, dict[BlockNumber, bool]] # (netuid, uid), block -> active + Stake: dict[str, dict[str, dict[int, int]]] # (hotkey, coldkey) -> block -> stake - Delegates: Dict[str, Dict[int, float]] # address -> block -> delegate_take + Delegates: dict[str, dict[int, float]] # address -> block -> delegate_take - NetworksAdded: Dict[int, Dict[BlockNumber, bool]] # netuid -> block -> added + NetworksAdded: dict[int, dict[BlockNumber, bool]] # netuid -> block -> added class MockChainState(TypedDict): @@ -384,10 +383,10 @@ def _convert_to_balance(balance: Union["Balance", float, int]) -> "Balance": def force_set_balance( self, ss58_address: str, balance: Union["Balance", float, int] = Balance(0) - ) -> Tuple[bool, Optional[str]]: + ) -> tuple[bool, Optional[str]]: """ Returns: - Tuple[bool, Optional[str]]: (success, err_msg) + tuple[bool, Optional[str]]: (success, err_msg) """ balance = self._convert_to_balance(balance) @@ -429,7 +428,7 @@ def do_block_step(self) -> None: + 1 ) - def _handle_type_default(self, name: str, params: List[object]) -> object: + def _handle_type_default(self, name: str, params: list[object]) -> object: defaults_mapping = { "TotalStake": 0, "TotalHotkeyStake": 0, @@ -461,7 +460,7 @@ def query_subtensor( self, name: str, block: Optional[int] = None, - params: Optional[List[object]] = [], + params: Optional[list[object]] = [], ) -> MockSubtensorValue: if block: if self.block_number < block: @@ -497,7 +496,7 @@ def query_map_subtensor( self, name: str, block: Optional[int] = None, - params: Optional[List[object]] = [], + params: Optional[list[object]] = [], ) -> Optional[MockMapResult]: """ Note: Double map requires one param @@ -629,7 +628,7 @@ def neuron_for_uid( else: return neuron_info - def neurons(self, netuid: int, block: Optional[int] = None) -> List[NeuronInfo]: + def neurons(self, netuid: int, block: Optional[int] = None) -> list[NeuronInfo]: if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: raise Exception("Subnet does not exist") @@ -646,7 +645,7 @@ def neurons(self, netuid: int, block: Optional[int] = None) -> List[NeuronInfo]: @staticmethod def _get_most_recent_storage( - storage: Dict[BlockNumber, Any], block_number: Optional[int] = None + storage: dict[BlockNumber, Any], block_number: Optional[int] = None ) -> Any: if block_number is None: items = list(storage.items()) @@ -817,7 +816,7 @@ def _neuron_subnet_exists( def neurons_lite( self, netuid: int, block: Optional[int] = None - ) -> List[NeuronInfoLite]: + ) -> list[NeuronInfoLite]: if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: raise Exception("Subnet does not exist") @@ -844,7 +843,7 @@ def do_transfer( transfer_balance: "Balance", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[str], Optional[str]]: + ) -> tuple[bool, Optional[str], Optional[str]]: bal = self.get_balance(wallet.coldkeypub.ss58_address) dest_bal = self.get_balance(dest) transfer_fee = self.get_transfer_fee(wallet, dest, transfer_balance) @@ -884,7 +883,7 @@ def do_serve_prometheus( call_params: "PrometheusServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: + ) -> tuple[bool, Optional[str]]: return True, None def do_set_weights( @@ -892,11 +891,11 @@ def do_set_weights( wallet: "Wallet", netuid: int, uids: int, - vals: List[int], + vals: list[int], version_key: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: + ) -> tuple[bool, Optional[str]]: return True, None def do_serve_axon( @@ -905,5 +904,5 @@ def do_serve_axon( call_params: "AxonServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: + ) -> tuple[bool, Optional[str]]: return True, None diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index ff7d40a70..4c0c47585 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -22,6 +22,7 @@ import socket import urllib from functools import wraps +from typing import Optional import netaddr import requests @@ -32,16 +33,15 @@ def int_to_ip(int_val: int) -> str: """Maps an integer to a unique ip-string Args: - int_val (:type:`int128`, `required`): + int_val (int): The integer representation of an ip. Must be in the range (0, 3.4028237e+38). Returns: - str_val (:tyep:`str`, `required): + str_val (str): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 Raises: - netaddr.core.AddrFormatError (Exception): - Raised when the passed int_vals is not a valid ip int value. + netaddr.core.AddrFormatError (Exception): Raised when the passed int_vals is not a valid ip int value. """ return str(netaddr.IPAddress(int_val)) @@ -156,17 +156,19 @@ def get_external_ip() -> str: raise ExternalIPNotFound -def get_formatted_ws_endpoint_url(endpoint_url: str) -> str: +def get_formatted_ws_endpoint_url(endpoint_url: Optional[str]) -> Optional[str]: """ Returns a formatted websocket endpoint url. Note: The port (or lack thereof) is left unchanged Args: - endpoint_url (str, `required`): + endpoint_url (Optional[str]): The endpoint url to format. Returns: - formatted_endpoint_url (str, `required`): - The formatted endpoint url. In the form of ws:// or wss:// + formatted_endpoint_url (Optional[str]): The formatted endpoint url. In the form of ws:// or wss:// """ + if endpoint_url is None: + return None + if endpoint_url[0:6] != "wss://" and endpoint_url[0:5] != "ws://": endpoint_url = f"ws://{endpoint_url}" diff --git a/bittensor/utils/registration.py b/bittensor/utils/registration.py index f6cd201c1..4d0cdb93d 100644 --- a/bittensor/utils/registration.py +++ b/bittensor/utils/registration.py @@ -17,7 +17,7 @@ import functools import os -import typing +from typing import TYPE_CHECKING import numpy @@ -93,7 +93,7 @@ def __getattr__(self, name): raise ImportError("torch not installed") -if typing.TYPE_CHECKING: +if TYPE_CHECKING: import torch else: torch = LazyLoadedTorch() diff --git a/bittensor/utils/subnets.py b/bittensor/utils/subnets.py index 0046df8f2..b59ad42db 100644 --- a/bittensor/utils/subnets.py +++ b/bittensor/utils/subnets.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. from abc import ABC, abstractmethod -from typing import Any, List, Union, Optional, TYPE_CHECKING +from typing import Any, Union, Optional, TYPE_CHECKING from bittensor.core.axon import Axon from bittensor.core.dendrite import Dendrite @@ -44,12 +44,12 @@ def prepare_synapse(self, *args, **kwargs) -> Any: """Prepare the synapse-specific payload.""" @abstractmethod - def process_responses(self, responses: List[Union["Synapse", Any]]) -> Any: + def process_responses(self, responses: list[Union["Synapse", Any]]) -> Any: """Process the responses from the network.""" async def query_api( self, - axons: Union["Axon", List["Axon"]], + axons: Union["Axon", list["Axon"]], deserialize: Optional[bool] = False, timeout: Optional[int] = 12, **kwargs: Optional[Any], diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index bbbf446b6..3992e59fc 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -20,7 +20,7 @@ import hashlib import logging import typing -from typing import List, Tuple, Union +from typing import Union import numpy as np from numpy.typing import NDArray @@ -90,13 +90,13 @@ def normalize_max_weight( # Metagraph uses this function. def convert_weight_uids_and_vals_to_tensor( - n: int, uids: List[int], weights: List[int] + n: int, uids: list[int], weights: list[int] ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: """Converts weights and uids from chain representation into a np.array (inverse operation from convert_weights_and_uids_for_emit) Args: n (int): number of neurons on network. - uids (List[int]): Tensor of uids as destinations for passed weights. - weights (List[int]): Tensor of weights. + uids (list[int]): Tensor of uids as destinations for passed weights. + weights (list[int]): Tensor of weights. Returns: row_weights (np.float32 or torch.FloatTensor): Converted row weights. @@ -118,14 +118,14 @@ def convert_weight_uids_and_vals_to_tensor( # Metagraph uses this function. def convert_root_weight_uids_and_vals_to_tensor( - n: int, uids: List[int], weights: List[int], subnets: List[int] + n: int, uids: list[int], weights: list[int], subnets: list[int] ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: """Converts root weights and uids from chain representation into a np.array or torch FloatTensor (inverse operation from convert_weights_and_uids_for_emit) Args: n (int): number of neurons on network. - uids (List[int]): Tensor of uids as destinations for passed weights. - weights (List[int]): Tensor of weights. - subnets (List[int]): List of subnets on the network. + uids (list[int]): Tensor of uids as destinations for passed weights. + weights (list[int]): Tensor of weights. + subnets (list[int]): list of subnets on the network. Returns: row_weights (np.float32): Converted row weights. @@ -154,14 +154,14 @@ def convert_root_weight_uids_and_vals_to_tensor( # Metagraph uses this function. def convert_bond_uids_and_vals_to_tensor( - n: int, uids: List[int], bonds: List[int] + n: int, uids: list[int], bonds: list[int] ) -> Union[NDArray[np.int64], "torch.LongTensor"]: """Converts bond and uids from chain representation into a np.array. Args: n (int): number of neurons on network. - uids (List[int]): Tensor of uids as destinations for passed bonds. - bonds (List[int]): Tensor of bonds. + uids (list[int]): Tensor of uids as destinations for passed bonds. + bonds (list[int]): Tensor of bonds. Returns: row_bonds (np.float32): Converted row bonds. @@ -180,7 +180,7 @@ def convert_bond_uids_and_vals_to_tensor( def convert_weights_and_uids_for_emit( uids: Union[NDArray[np.int64], "torch.LongTensor"], weights: Union[NDArray[np.float32], "torch.FloatTensor"], -) -> Tuple[List[int], List[int]]: +) -> tuple[list[int], list[int]]: """Converts weights into integer u32 representation that sum to MAX_INT_WEIGHT. Args: @@ -188,8 +188,8 @@ def convert_weights_and_uids_for_emit( weights (np.float32):Tensor of weights. Returns: - weight_uids (List[int]): Uids as a list. - weight_vals (List[int]): Weights as a list. + weight_uids (list[int]): Uids as a list. + weight_vals (list[int]): Weights as a list. """ # Checks. weights = weights.tolist() @@ -234,8 +234,8 @@ def process_weights_for_netuid( metagraph: "Metagraph" = None, exclude_quantile: int = 0, ) -> Union[ - Tuple["torch.Tensor", "torch.FloatTensor"], - Tuple[NDArray[np.int64], NDArray[np.float32]], + tuple["torch.Tensor", "torch.FloatTensor"], + tuple[NDArray[np.int64], NDArray[np.float32]], ]: """ Processes weight tensors for a given subnet id using the provided weight and UID arrays, applying constraints and normalization based on the subtensor and metagraph data. This function can handle both NumPy arrays and PyTorch tensors. @@ -249,7 +249,7 @@ def process_weights_for_netuid( exclude_quantile (int, optional): Quantile threshold for excluding lower weights. Defaults to ``0``. Returns: - Union[Tuple["torch.Tensor", "torch.FloatTensor"], Tuple[NDArray[np.int64], NDArray[np.float32]]]: Tuple containing the array of user IDs and the corresponding normalized weights. The data type of the return matches the type of the input weights (NumPy or PyTorch). + Union[tuple["torch.Tensor", "torch.FloatTensor"], tuple[NDArray[np.int64], NDArray[np.float32]]]: tuple containing the array of user IDs and the corresponding normalized weights. The data type of the return matches the type of the input weights (NumPy or PyTorch). """ logging.debug("process_weights_for_netuid()") @@ -361,10 +361,10 @@ def process_weights_for_netuid( def generate_weight_hash( address: str, netuid: int, - uids: List[int], - values: List[int], + uids: list[int], + values: list[int], version_key: int, - salt: List[int], + salt: list[int], ) -> str: """ Generate a valid commit hash from the provided weights. @@ -372,9 +372,9 @@ def generate_weight_hash( Args: address (str): The account identifier. Wallet ss58_address. netuid (int): The network unique identifier. - uids (List[int]): The list of UIDs. - salt (List[int]): The salt to add to hash. - values (List[int]): The list of weight values. + uids (list[int]): The list of UIDs. + salt (list[int]): The salt to add to hash. + values (list[int]): The list of weight values. version_key (int): The version key. Returns: From 9f567c2d9b49dbc47a1c50f6aac50845856ec6ef Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 16:03:41 -0700 Subject: [PATCH 212/260] bittensor/__init__.py --- bittensor/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 4882af25a..66be64f6d 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -24,10 +24,22 @@ # Logging helpers. def trace(on: bool = True): + """ + Enables or disables trace logging. + + Args: + on (bool): If True, enables trace logging. If False, disables trace logging. + """ logging.set_trace(on) def debug(on: bool = True): + """ + Enables or disables debug logging. + + Args: + on (bool): If True, enables debug logging. If False, disables debug logging. + """ logging.set_debug(on) From 0bf1f1c0e63f03d1561e565fef5d3e600a9c746e Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 16:04:55 -0700 Subject: [PATCH 213/260] bittensor/utils/weight_utils.py --- bittensor/utils/weight_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 3992e59fc..0337ee629 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -92,7 +92,9 @@ def normalize_max_weight( def convert_weight_uids_and_vals_to_tensor( n: int, uids: list[int], weights: list[int] ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: - """Converts weights and uids from chain representation into a np.array (inverse operation from convert_weights_and_uids_for_emit) + """ + Converts weights and uids from chain representation into a np.array (inverse operation from convert_weights_and_uids_for_emit). + Args: n (int): number of neurons on network. uids (list[int]): Tensor of uids as destinations for passed weights. From ca8d5d0db9e4eea4adec0b6549e359d6332369e7 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 16:07:34 -0700 Subject: [PATCH 214/260] bittensor/utils/version.py --- bittensor/utils/version.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index 8ffd969cf..1134361ad 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -71,7 +71,7 @@ def get_and_save_latest_version(timeout: int = 15) -> str: Retrieves and saves the latest version of Bittensor. Args: - timeout (int, optional): The timeout for the request to PyPI in seconds. Default is ``15``. + timeout (int): The timeout for the request to PyPI in seconds. Default is ``15``. Returns: str: The latest version of Bittensor. @@ -95,6 +95,9 @@ def check_version(timeout: int = 15): """ Check if the current version of Bittensor is up-to-date with the latest version on PyPi. Raises a VersionCheckError if the version check fails. + + Args: + timeout (int): The timeout for the request to PyPI in seconds. Default is ``15``. """ try: @@ -112,7 +115,11 @@ def check_version(timeout: int = 15): def version_checking(timeout: int = 15): - """Deprecated, kept for backwards compatibility. Use check_version() instead.""" + """Deprecated, kept for backwards compatibility. Use check_version() instead. + + Args: + timeout (int): The timeout for calling :func:``check_version`` function. Default is ``15``. + """ from warnings import warn From 3b148457728f073a2db4e57f3e135640f8e21945 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:01:28 -0700 Subject: [PATCH 215/260] bittensor/utils/btlogging --- bittensor/utils/btlogging/__init__.py | 3 +- bittensor/utils/btlogging/format.py | 14 +++++----- bittensor/utils/btlogging/helpers.py | 2 +- bittensor/utils/btlogging/loggingmachine.py | 31 ++++++++++++++++----- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/bittensor/utils/btlogging/__init__.py b/bittensor/utils/btlogging/__init__.py index 6b02c51ba..a5e6d2518 100644 --- a/bittensor/utils/btlogging/__init__.py +++ b/bittensor/utils/btlogging/__init__.py @@ -18,8 +18,7 @@ """ btlogging sub-package standardized logging for Bittensor. -This module provides logging functionality for the Bittensor package. It includes custom loggers, handlers, and -formatters to ensure consistent logging throughout the project. +This module provides logging functionality for the Bittensor package. It includes custom loggers, handlers, and formatters to ensure consistent logging throughout the project. """ from .loggingmachine import LoggingMachine diff --git a/bittensor/utils/btlogging/format.py b/bittensor/utils/btlogging/format.py index 1ca5d15e2..557973884 100644 --- a/bittensor/utils/btlogging/format.py +++ b/bittensor/utils/btlogging/format.py @@ -23,7 +23,7 @@ import logging import time - +from typing import Optional from colorama import init, Fore, Back, Style init(autoreset=True) @@ -115,13 +115,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.trace = False - def formatTime(self, record, datefmt=None) -> str: + def formatTime(self, record, datefmt: Optional[str] = None) -> str: """ Override formatTime to add milliseconds. Args: record (logging.LogRecord): The log record. - datefmt (str, optional): The date format string. + datefmt (Optional[str]): The date format string. Returns: s (str): The formatted time string with milliseconds. @@ -135,7 +135,7 @@ def formatTime(self, record, datefmt=None) -> str: s += f".{int(record.msecs):03d}" return s - def format(self, record) -> str: + def format(self, record: "logging.LogRecord") -> str: """ Override format to apply custom formatting including emojis and colors. @@ -186,13 +186,13 @@ class BtFileFormatter(logging.Formatter): centers the level name. """ - def formatTime(self, record, datefmt=None) -> str: + def formatTime(self, record: "logging.LogRecord", datefmt: Optional[str] = None) -> str: """ Override formatTime to add milliseconds. Args: record (logging.LogRecord): The log record. - datefmt (str, optional): The date format string. + datefmt (Optional[str]): The date format string. Returns: s (str): The formatted time string with milliseconds. @@ -206,7 +206,7 @@ def formatTime(self, record, datefmt=None) -> str: s += f".{int(record.msecs):03d}" return s - def format(self, record) -> str: + def format(self, record: "logging.LogRecord") -> str: """ Override format to center the level name. diff --git a/bittensor/utils/btlogging/helpers.py b/bittensor/utils/btlogging/helpers.py index 892df1052..3fdca4ee0 100644 --- a/bittensor/utils/btlogging/helpers.py +++ b/bittensor/utils/btlogging/helpers.py @@ -23,7 +23,7 @@ from typing import Generator -def all_loggers() -> Generator[logging.Logger, None, None]: +def all_loggers() -> Generator["logging.Logger", None, None]: """Generator that yields all logger instances in the application. Iterates through the logging root manager's logger dictionary and yields all active `Logger` instances. It skips diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index 3fd323c74..6790a5750 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -127,8 +127,15 @@ def _enable_initial_state(self, config): else: self.enable_default() - def _extract_logging_config(self, config) -> dict: - """Extract btlogging's config from bittensor config""" + def _extract_logging_config(self, config: "Config") -> dict: + """Extract btlogging's config from bittensor config + + Args: + config (bittensor.core.config.Config): Bittensor config instance. + + Returns: + (dict): btlogging's config from Bittensor config or Bittensor config. + """ if hasattr(config, "logging"): return config.logging else: @@ -154,8 +161,12 @@ def _configure_handlers(self, config) -> list[stdlogging.Handler]: def get_config(self): return self._config - def set_config(self, config): - """Set config after initialization, if desired.""" + def set_config(self, config: "Config"): + """Set config after initialization, if desired. + + Args: + config (bittensor.core.config.Config): Bittensor config instance. + """ self._config = config if config.logging_dir and config.record_log: expanded_dir = os.path.expanduser(config.logging_dir) @@ -186,7 +197,7 @@ def get_queue(self): """ return self._queue - def _initialize_bt_logger(self, name): + def _initialize_bt_logger(self, name: str): """ Initialize logging for bittensor. @@ -198,7 +209,7 @@ def _initialize_bt_logger(self, name): logger.addHandler(queue_handler) return logger - def _deinitialize_bt_logger(self, name): + def _deinitialize_bt_logger(self, name: str): """Find the logger by name and remove the queue handler associated with it.""" logger = stdlogging.getLogger(name) for handler in list(logger.handlers): @@ -223,6 +234,9 @@ def register_primary_logger(self, name: str): This adds a logger to the _primary_loggers set to ensure it doesn't get disabled when disabling third-party loggers. A queue handler is also associated with it. + + Args: + name (str): the name for primary logger. """ self._primary_loggers.add(name) self._initialize_bt_logger(name) @@ -233,6 +247,9 @@ def deregister_primary_logger(self, name: str): This function removes the logger from the _primary_loggers set and deinitializes its queue handler + + Args: + name (str): the name of primary logger. """ self._primary_loggers.remove(name) self._deinitialize_bt_logger(name) @@ -445,7 +462,7 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): default_logging_trace = os.getenv("BT_LOGGING_TRACE") or False default_logging_record_log = os.getenv("BT_LOGGING_RECORD_LOG") or False default_logging_logging_dir = ( - os.getenv("BT_LOGGING_LOGGING_DIR") or "~/.bittensor/miners" + os.getenv("BT_LOGGING_LOGGING_DIR") or os.path.join("~", ".bittensor", "miners") ) parser.add_argument( "--" + prefix_str + "logging.debug", From 6607746e51059b3692cd3e02a13c1f8eb02630cc Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:01:43 -0700 Subject: [PATCH 216/260] fix --- bittensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 66be64f6d..5ddba2abe 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -26,7 +26,7 @@ def trace(on: bool = True): """ Enables or disables trace logging. - + Args: on (bool): If True, enables trace logging. If False, disables trace logging. """ From 478576b3a4281f4ffd605cfd4b20758d7b222db2 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:13:17 -0700 Subject: [PATCH 217/260] remove wrong `optional` from docstring's annotations --- bittensor/core/axon.py | 30 ++++++++-------- bittensor/core/chain_data/utils.py | 6 ++-- bittensor/core/dendrite.py | 38 ++++++++++----------- bittensor/core/extrinsics/commit_weights.py | 20 +++++------ bittensor/core/extrinsics/serving.py | 8 ++--- bittensor/core/extrinsics/set_weights.py | 6 ++-- bittensor/utils/subnets.py | 6 ++-- bittensor/utils/weight_utils.py | 7 ++-- 8 files changed, 61 insertions(+), 60 deletions(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index dd19e9d2e..45c7b3c53 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -272,16 +272,16 @@ def prioritize_my_synapse( synapse: MySynapse ) -> float: ).start() Args: - wallet (bittensor_wallet.Wallet, optional): Wallet with hotkey and coldkeypub. - config (bittensor.core.config.Config, optional): Configuration parameters for the axon. - port (int, optional): Port for server binding. - ip (str, optional): Binding IP address. - external_ip (str, optional): External IP address to broadcast. - external_port (int, optional): External port to broadcast. - max_workers (int, optional): Number of active threads for request handling. + wallet (Optional[bittensor_wallet.Wallet]): Wallet with hotkey and coldkeypub. + config (Optional[bittensor.core.config.Config]): Configuration parameters for the axon. + port (Optional[int]): Port for server binding. + ip (Optional[str]): Binding IP address. + external_ip (Optional[str]): External IP address to broadcast. + external_port (Optional[int]): External port to broadcast. + max_workers (Optional[int]): Number of active threads for request handling. Returns: - bittensor.axon: An instance of the axon class configured as per the provided arguments. + bittensor.core.axon.Axon: An instance of the axon class configured as per the provided arguments. Note: This class is a core part of Bittensor's decentralized network for machine intelligence, @@ -432,9 +432,9 @@ def attach( Args: forward_fn (Callable): Function to be called when the API endpoint is accessed. It should have at least one argument. - blacklist_fn (Callable, optional): Function to filter out undesired requests. It should take the same arguments as :func:`forward_fn` and return a boolean value. Defaults to ``None``, meaning no blacklist filter will be used. - priority_fn (Callable, optional): Function to rank requests based on their priority. It should take the same arguments as :func:`forward_fn` and return a numerical value representing the request's priority. Defaults to ``None``, meaning no priority sorting will be applied. - verify_fn (Callable, optional): Function to verify requests. It should take the same arguments as :func:`forward_fn` and return a boolean value. If ``None``, :func:`self.default_verify` function will be used. + blacklist_fn (Optional[Callable]): Function to filter out undesired requests. It should take the same arguments as :func:`forward_fn` and return a boolean value. Defaults to ``None``, meaning no blacklist filter will be used. + priority_fn (Optional[Callable]): Function to rank requests based on their priority. It should take the same arguments as :func:`forward_fn` and return a numerical value representing the request's priority. Defaults to ``None``, meaning no priority sorting will be applied. + verify_fn (Optional[Callable]): Function to verify requests. It should take the same arguments as :func:`forward_fn` and return a boolean value. If ``None``, :func:`self.default_verify` function will be used. Note: The methods :func:`forward_fn`, :func:`blacklist_fn`, :func:`priority_fn`, and :func:`verify_fn` should be designed to receive the same parameters. @@ -610,7 +610,7 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: Optional[str] = None) Args: parser (argparse.ArgumentParser): Argument parser to which the arguments will be added. - prefix (str, optional): Prefix to add to the argument names. Defaults to None. + prefix (Optional[str]): Prefix to add to the argument names. Defaults to None. Note: Environment variables are used to define default values for the arguments. @@ -815,7 +815,7 @@ def serve(self, netuid: int, subtensor: Optional["Subtensor"] = None) -> "Axon": Args: netuid (int): The unique identifier of the subnet to register on. This ID is essential for the Axon to correctly position itself within the Bittensor network topology. - subtensor (bittensor.core.subtensor.Subtensor, optional): The subtensor connection to use for serving. If not provided, a new connection is established based on default configurations. + subtensor (Optional[bittensor.core.subtensor.Subtensor]): The subtensor connection to use for serving. If not provided, a new connection is established based on default configurations. Returns: bittensor.core.axon.Axon: The Axon instance that is now actively serving on the specified subtensor. @@ -986,8 +986,8 @@ def log_and_handle_error( Args: synapse (bittensor.core.synapse.Synapse): The synapse object to be updated with error information. exception (Exception): The exception that was raised and needs to be logged and handled. - status_code (Optional[int], optional): The HTTP status code to be set on the synapse object. Defaults to None. - start_time (Optional[float], optional): The timestamp marking the start of the processing, used to calculate process time. Defaults to None. + status_code (Optional[int]): The HTTP status code to be set on the synapse object. Defaults to None. + start_time (Optional[float]): The timestamp marking the start of the processing, used to calculate process time. Defaults to None. Returns: Synapse: The updated synapse object with error details. diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index 128fd83ea..6b4e1d569 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -36,11 +36,11 @@ def from_scale_encoding( Args: input_ (Union[List[int], bytes, ScaleBytes]): The input_ data to decode. type_name (ChainDataType): The type of data being decoded. - is_vec (bool, optional): Whether the data is a vector of the specified type. Default is ``False``. - is_option (bool, optional): Whether the data is an optional value of the specified type. Default is ``False``. + is_vec (bool): Whether the data is a vector of the specified type. Default is ``False``. + is_option (bool): Whether the data is an optional value of the specified type. Default is ``False``. Returns: - Optional[Dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. + Optional[dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. """ type_string = type_name.name if type_name == ChainDataType.DelegatedInfo: diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 7dfdba987..6e4964826 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -104,7 +104,7 @@ def __init__(self, wallet: Optional[Union["Wallet", "Keypair"]] = None): Initializes the Dendrite object, setting up essential properties. Args: - wallet (Optional[Union[bittensor_wallet.Wallet, substrateinterface.Keypair]], optional): The user's wallet or keypair used for signing messages. Defaults to ``None``, in which case a new :func:`bittensor_wallet.Wallet().hotkey` is generated and used. + wallet (Optional[Union[bittensor_wallet.Wallet, substrateinterface.Keypair]]): The user's wallet or keypair used for signing messages. Defaults to ``None``, in which case a new :func:`bittensor_wallet.Wallet().hotkey` is generated and used. """ # Initialize the parent class super(DendriteMixin, self).__init__() @@ -336,8 +336,8 @@ def query( Args: axons (Union[List[Union[bittensor.core.chain_data.axon_info.AxonInfo, 'bittensor.core.axon.Axon']], Union['bittensor.core.chain_data.axon_info.AxonInfo', 'bittensor.core.axon.Axon']]): The list of target Axon information. - synapse (bittensor.core.synapse.Synapse, optional): The Synapse object. Defaults to :func:`Synapse()`. - timeout (float, optional): The request timeout duration in seconds. Defaults to ``12.0`` seconds. + synapse (Optional[bittensor.core.synapse.Synapse]): The Synapse object. Defaults to :func:`Synapse()`. + timeout (Optional[float]): The request timeout duration in seconds. Defaults to ``12.0`` seconds. Returns: Union[bittensor.core.synapse.Synapse, List[bittensor.core.synapse.Synapse]]: If a single target axon is provided, returns the response from that axon. If multiple target axons are provided, returns a list of responses from all target axons. @@ -402,11 +402,11 @@ async def forward( Args: axons (Union[List[Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]], Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]]): The target Axons to send requests to. Can be a single Axon or a list of Axons. - synapse (bittensor.core.synapse.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. - timeout (float, optional): Maximum duration to wait for a response from an Axon in seconds. Defaults to ``12.0``. - deserialize (bool, optional): Determines if the received response should be deserialized. Defaults to ``True``. - run_async (bool, optional): If ``True``, sends requests concurrently. Otherwise, sends requests sequentially. Defaults to ``True``. - streaming (bool, optional): Indicates if the response is expected to be in streaming format. Defaults to ``False``. + synapse (bittensor.core.synapse.Synapse): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. + timeout (float): Maximum duration to wait for a response from an Axon in seconds. Defaults to ``12.0``. + deserialize (bool): Determines if the received response should be deserialized. Defaults to ``True``. + run_async (bool): If ``True``, sends requests concurrently. Otherwise, sends requests sequentially. Defaults to ``True``. + streaming (bool): Indicates if the response is expected to be in streaming format. Defaults to ``False``. Returns: Union[AsyncGenerator, bittensor.core.synapse.Synapse, List[bittensor.core.synapse.Synapse]]: If a single `Axon` is targeted, returns its response. @@ -504,9 +504,9 @@ async def call( Args: target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]): The target Axon to send the request to. - synapse (bittensor.core.synapse.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. - timeout (float, optional): Maximum duration to wait for a response from the Axon in seconds. Defaults to ``12.0``. - deserialize (bool, optional): Determines if the received response should be deserialized. Defaults to ``True``. + synapse (bittensor.core.synapse.Synapse): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. + timeout (float): Maximum duration to wait for a response from the Axon in seconds. Defaults to ``12.0``. + deserialize (bool): Determines if the received response should be deserialized. Defaults to ``True``. Returns: bittensor.core.synapse.Synapse: The Synapse object, updated with the response data from the Axon. @@ -573,9 +573,9 @@ async def call_stream( Args: target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]): The target Axon to send the request to. - synapse (bittensor.core.synapse.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. - timeout (float, optional): Maximum duration to wait for a response (or a chunk of the response) from the Axon in seconds. Defaults to ``12.0``. - deserialize (bool, optional): Determines if each received chunk should be deserialized. Defaults to ``True``. + synapse (bittensor.core.synapse.Synapse): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. + timeout (float): Maximum duration to wait for a response (or a chunk of the response) from the Axon in seconds. Defaults to ``12.0``. + deserialize (bool): Determines if each received chunk should be deserialized. Defaults to ``True``. Yields: object: Each yielded object contains a chunk of the arbitrary response data from the Axon. @@ -649,7 +649,7 @@ def preprocess_synapse_for_request( Args: target_axon_info (bittensor.core.chain_data.axon_info.AxonInfo): The target axon information. synapse (bittensor.core.synapse.Synapse): The synapse object to be preprocessed. - timeout (float, optional): The request timeout duration in seconds. Defaults to ``12.0`` seconds. + timeout (float): The request timeout duration in seconds. Defaults to ``12.0`` seconds. Returns: bittensor.core.synapse.Synapse: The preprocessed synapse. @@ -778,13 +778,13 @@ async def __aexit__(self, exc_type, exc_value, traceback): Ensures proper cleanup when exiting the ``async with`` context. This method will close the `aiohttp `_ client session asynchronously, releasing any tied resources. Args: - exc_type (Type[BaseException], optional): The type of exception that was raised. - exc_value (BaseException, optional): The instance of exception that was raised. - traceback (TracebackType, optional): A traceback object encapsulating the call stack at the point where the exception was raised. + exc_type (Type[BaseException]): The type of exception that was raised. + exc_value (BaseException): The instance of exception that was raised. + traceback (TracebackType): A traceback object encapsulating the call stack at the point where the exception was raised. Usage:: - import bittensor + wallet = bittensor.Wallet() async with bittensor.dendrite( wallet ) as dendrite: await dendrite.some_async_method() diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 9047e1f94..5290665bb 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -51,8 +51,8 @@ def do_commit_weights( wallet (bittensor.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, 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. + 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. Returns: Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. @@ -111,9 +111,9 @@ def commit_weights_extrinsic( wallet (bittensor.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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + 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. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. Returns: Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string @@ -169,8 +169,8 @@ def do_reveal_weights( values (List[int]): List of weight values corresponding to each UID. salt (List[int]): List of salt values corresponding to the hash function. version_key (int): 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. + 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. Returns: Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. @@ -237,9 +237,9 @@ def reveal_weights_extrinsic( weights (List[int]): List of weight values corresponding to each UID. salt (List[int]): List of salt values corresponding to the hash function. version_key (int): 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + 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. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. Returns: Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string value describing the success or potential error. diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index c68dd27df..d5d6e1ff9 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -51,8 +51,8 @@ def do_serve_axon( self (bittensor.core.subtensor.Subtensor): Subtensor instance object. wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. call_params (AxonServeCallParams): Parameters required for the serve axon call. - 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. + 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. Returns: Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. @@ -262,8 +262,8 @@ def publish_metadata( netuid (int): Network UID on which the metadata is to be published. data_type (str): The data type of the information being submitted. It should be one of the following: ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing algorithm used for the data. data (str): The actual metadata content to be published. This should be formatted or hashed according to the ``type`` specified. (Note: max ``str`` length is 128 bytes) - wait_for_inclusion (bool, optional): If ``True``, the function will wait for the extrinsic to be included in a 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``. + wait_for_inclusion (bool): If ``True``, the function will wait for the extrinsic to be included in a block before returning. Defaults to ``False``. + wait_for_finalization (bool): If ``True``, the function will wait for the extrinsic to be finalized on the chain before returning. Defaults to ``True``. Returns: bool: ``True`` if the metadata was successfully published (and finalized if specified). ``False`` otherwise. diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 86f993c53..554c28d29 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -56,9 +56,9 @@ def do_set_weights( uids (List[int]): List of neuron UIDs for which weights are being set. vals (List[int]): List of weight values corresponding to each UID. netuid (int): Unique identifier for the network. - 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. + 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. Returns: Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. diff --git a/bittensor/utils/subnets.py b/bittensor/utils/subnets.py index b59ad42db..df5b71318 100644 --- a/bittensor/utils/subnets.py +++ b/bittensor/utils/subnets.py @@ -52,15 +52,15 @@ async def query_api( axons: Union["Axon", list["Axon"]], deserialize: Optional[bool] = False, timeout: Optional[int] = 12, - **kwargs: Optional[Any], + **kwargs, ) -> Any: """ Queries the API nodes of a subnet using the given synapse and bespoke query function. Args: axons (Union[bt.axon, List[bt.axon]]): The list of axon(s) to query. - deserialize (bool, optional): Whether to deserialize the responses. Defaults to False. - timeout (int, optional): The timeout in seconds for the query. Defaults to 12. + deserialize (Optional[bool]): Whether to deserialize the responses. Defaults to False. + timeout (Optional[int]): The timeout in seconds for the query. Defaults to 12. **kwargs: Keyword arguments for the prepare_synapse_fn. Returns: diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 0337ee629..8bf992cbd 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -23,6 +23,7 @@ from typing import Union import numpy as np +from docopt import Optional from numpy.typing import NDArray from scalecodec import U16, ScaleBytes, Vec from substrateinterface import Keypair @@ -233,7 +234,7 @@ def process_weights_for_netuid( weights: Union[NDArray[np.float32], "torch.Tensor"], netuid: int, subtensor: "Subtensor", - metagraph: "Metagraph" = None, + metagraph: Optional["Metagraph"] = None, exclude_quantile: int = 0, ) -> Union[ tuple["torch.Tensor", "torch.FloatTensor"], @@ -247,8 +248,8 @@ def process_weights_for_netuid( weights (Union[NDArray[np.float32], "torch.Tensor"]): Array of weights associated with the user IDs. netuid (int): The network uid to process weights for. subtensor (Subtensor): Subtensor instance to access blockchain data. - metagraph (Metagraph, optional): Metagraph instance for additional network data. If None, it is fetched from the subtensor using the netuid. - exclude_quantile (int, optional): Quantile threshold for excluding lower weights. Defaults to ``0``. + metagraph (Optional[Metagraph]): Metagraph instance for additional network data. If None, it is fetched from the subtensor using the netuid. + exclude_quantile (int): Quantile threshold for excluding lower weights. Defaults to ``0``. Returns: Union[tuple["torch.Tensor", "torch.FloatTensor"], tuple[NDArray[np.int64], NDArray[np.float32]]]: tuple containing the array of user IDs and the corresponding normalized weights. The data type of the return matches the type of the input weights (NumPy or PyTorch). From 65b50cb9b67268cda005a516362b88179deb302f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:32:20 -0700 Subject: [PATCH 218/260] apply camel case rule --- bittensor/core/axon.py | 26 ++++++------- bittensor/core/config.py | 2 +- bittensor/core/dendrite.py | 8 ++-- bittensor/core/extrinsics/commit_weights.py | 39 ++++++++++--------- bittensor/core/extrinsics/prometheus.py | 8 ++-- bittensor/core/extrinsics/serving.py | 6 +-- bittensor/core/extrinsics/set_weights.py | 4 +- bittensor/core/extrinsics/transfer.py | 2 +- bittensor/core/subtensor.py | 4 +- bittensor/utils/btlogging/format.py | 4 +- bittensor/utils/btlogging/loggingmachine.py | 8 ++-- tests/e2e_tests/test_axon.py | 4 +- tests/e2e_tests/test_commit_weights.py | 6 +-- tests/e2e_tests/test_dendrite.py | 6 +-- tests/e2e_tests/test_liquid_alpha.py | 2 +- tests/e2e_tests/test_metagraph.py | 2 +- tests/e2e_tests/test_subtensor_functions.py | 4 +- tests/e2e_tests/utils/chain_interactions.py | 37 ++++++++++-------- tests/e2e_tests/utils/test_utils.py | 4 +- .../test_metagraph_integration.py | 2 +- .../test_subtensor_integration.py | 14 +++---- tests/unit_tests/test_dendrite.py | 2 +- tests/unit_tests/test_metagraph.py | 2 +- 23 files changed, 102 insertions(+), 94 deletions(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 45c7b3c53..374fb6d48 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -232,7 +232,7 @@ def prioritize_my_synapse( synapse: MySynapse ) -> float: return 1.0 # Initialize Axon object with a custom configuration - my_axon = bittensor.axon( + my_axon = bittensor.Axon( config=my_config, wallet=my_wallet, port=9090, @@ -319,13 +319,13 @@ def __init__( """Creates a new bittensor.Axon object from passed arguments. Args: - config (:obj:`Optional[bittensor.core.config.Config]`, `optional`): bittensor.axon.config() - wallet (:obj:`Optional[bittensor_wallet.Wallet]`, `optional`): bittensor wallet with hotkey and coldkeypub. - port (:type:`Optional[int]`, `optional`): Binding port. - ip (:type:`Optional[str]`, `optional`): Binding ip. - external_ip (:type:`Optional[str]`, `optional`): The external ip of the server to broadcast to the network. - external_port (:type:`Optional[int]`, `optional`): The external port of the server to broadcast to the network. - max_workers (:type:`Optional[int]`, `optional`): Used to create the threadpool if not passed, specifies the number of active threads servicing requests. + config (:obj:`Optional[bittensor.core.config.Config]`): bittensor.Axon.config() + wallet (:obj:`Optional[bittensor_wallet.Wallet]`): bittensor wallet with hotkey and coldkeypub. + port (:type:`Optional[int]`): Binding port. + ip (:type:`Optional[str]`): Binding ip. + external_ip (:type:`Optional[str]`): The external ip of the server to broadcast to the network. + external_port (:type:`Optional[int]`): The external port of the server to broadcast to the network. + max_workers (:type:`Optional[int]`): Used to create the threadpool if not passed, specifies the number of active threads servicing requests. """ # Build and check config. if config is None: @@ -464,7 +464,7 @@ def verify_custom(synapse: MyCustomSynapse): # Custom logic for verifying the request pass - my_axon = bittensor.axon(...) + my_axon = bittensor.Axon(...) my_axon.attach(forward_fn=forward_custom, verify_fn=verify_custom) Note: @@ -767,7 +767,7 @@ def start(self) -> "Axon": Example:: - my_axon = bittensor.axon(...) + my_axon = bittensor.Axon(...) ... # setup axon, attach functions, etc. my_axon.start() # Starts the axon server @@ -793,7 +793,7 @@ def stop(self) -> "Axon": Example:: - my_axon = bittensor.axon(...) + my_axon = bittensor.Axon(...) my_axon.start() ... my_axon.stop() # Stops the axon server @@ -822,7 +822,7 @@ def serve(self, netuid: int, subtensor: Optional["Subtensor"] = None) -> "Axon": Example:: - my_axon = bittensor.axon(...) + my_axon = bittensor.Axon(...) subtensor = bt.subtensor(network="local") # Local by default my_axon.serve(netuid=1, subtensor=subtensor) # Serves the axon on subnet with netuid 1 @@ -1056,7 +1056,7 @@ class AxonMiddleware(BaseHTTPMiddleware): Args: app (FastAPI): An instance of the FastAPI application to which this middleware is attached. - axon (bittensor.axon): The Axon instance that will process the requests. + axon (bittensor.core.axon.Axon): The Axon instance that will process the requests. The middleware operates by intercepting incoming requests, performing necessary preprocessing (like verification and priority assessment), executing the request through the Axon's endpoints, and diff --git a/bittensor/core/config.py b/bittensor/core/config.py index a1f84c5be..80e3de3db 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -45,7 +45,7 @@ class Config(DefaultMunch): default (Optional[Any]): Default value for the Config. Defaults to ``None``. This default will be returned for attributes that are undefined. Returns: - config (bittensor.config): Nested config object created from parser arguments. + config (bittensor.core.config.Config): Nested config object created from parser arguments. """ __is_set: dict[str, bool] diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 6e4964826..7986b6edb 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -145,7 +145,7 @@ async def session(self) -> aiohttp.ClientSession: import bittensor # Import bittensor wallet = bittensor.Wallet( ... ) # Initialize a wallet - dendrite = bittensor.dendrite( wallet ) # Initialize a dendrite instance with the wallet + dendrite = bittensor.Dendrite(wallet=wallet) # Initialize a dendrite instance with the wallet async with (await dendrite.session).post( # Use the session to make an HTTP POST request url, # URL to send the request to @@ -382,7 +382,7 @@ async def forward( import bittensor wallet = bittensor.Wallet() # Initialize a wallet synapse = bittensor.Synapse(...) # Create a synapse object that contains query data - dendrite = bittensor.dendrite(wallet = wallet) # Initialize a dendrite instance + dendrite = bittensor.Dendrite(wallet = wallet) # Initialize a dendrite instance netuid = ... # Provide subnet ID metagraph = bittensor.Metagraph(netuid) # Initialize a metagraph instance axons = metagraph.axons # Create a list of axons to query @@ -395,7 +395,7 @@ async def forward( For example:: ... - dendrite = bittensor.dendrite(wallet = wallet) + dendrite = bittensor.Dendrite(wallet = wallet) async for chunk in dendrite.forward(axons, synapse, timeout, deserialize, run_async, streaming): # Process each chunk here print(chunk) @@ -786,7 +786,7 @@ async def __aexit__(self, exc_type, exc_value, traceback): import bittensor wallet = bittensor.Wallet() - async with bittensor.dendrite( wallet ) as dendrite: + async with bittensor.Dendrite(wallet=wallet) as dendrite: await dendrite.some_async_method() Note: diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 5290665bb..be2cd9c2d 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -17,7 +17,7 @@ """Module commit weights and reveal weights extrinsic.""" -from typing import List, Tuple, Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from retry import retry from rich.prompt import Confirm @@ -41,14 +41,14 @@ def do_commit_weights( commit_hash: str, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -) -> Tuple[bool, Optional[dict]]: +) -> tuple[bool, Optional[dict]]: """ 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: - self (bittensor.subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. + self (bittensor.core.subtensor.Subtensor): 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. @@ -101,14 +101,14 @@ def commit_weights_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, prompt: bool = False, -) -> Tuple[bool, str]: +) -> tuple[bool, str]: """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. This function is a wrapper around the `do_commit_weights` method, handling user prompts and error messages. Args: - subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. + subtensor (bittensor.core.subtensor.Subtensor): 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. @@ -150,20 +150,20 @@ def do_reveal_weights( self: "Subtensor", wallet: "Wallet", netuid: int, - uids: List[int], - values: List[int], - salt: List[int], + uids: list[int], + values: list[int], + salt: list[int], version_key: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -) -> Tuple[bool, Optional[dict]]: +) -> tuple[bool, Optional[dict]]: """ 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. Args: - self (bittensor.subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. + self (bittensor.core.subtensor.Subtensor): 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. values (List[int]): List of weight values corresponding to each UID. @@ -218,20 +218,20 @@ def reveal_weights_extrinsic( subtensor: "Subtensor", wallet: "Wallet", netuid: int, - uids: List[int], - weights: List[int], - salt: List[int], + uids: list[int], + weights: list[int], + salt: list[int], version_key: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, prompt: bool = False, -) -> Tuple[bool, str]: +) -> 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, handling user prompts and error messages. Args: - subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. + subtensor (bittensor.core.subtensor.Subtensor): 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. weights (List[int]): List of weight values corresponding to each UID. @@ -240,6 +240,7 @@ def reveal_weights_extrinsic( 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. prompt (bool): If ``True``, prompts for user confirmation before proceeding. + Returns: Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string value describing the success or potential error. diff --git a/bittensor/core/extrinsics/prometheus.py b/bittensor/core/extrinsics/prometheus.py index 9aa95a838..b66b49dfc 100644 --- a/bittensor/core/extrinsics/prometheus.py +++ b/bittensor/core/extrinsics/prometheus.py @@ -44,8 +44,8 @@ def do_serve_prometheus( Sends a serve prometheus extrinsic to the chain. Args: - self (bittensor.subtensor): Bittensor subtensor object - wallet (:func:`bittensor_wallet.Wallet`): Wallet object. + self (bittensor.core.subtensor.Subtensor): Bittensor subtensor object + wallet (bittensor_wallet.Wallet): Wallet object. call_params (:func:`PrometheusServeCallParams`): Prometheus serve call parameters. wait_for_inclusion (bool): If ``true``, waits for inclusion. wait_for_finalization (bool): If ``true``, waits for finalization. @@ -94,8 +94,8 @@ def prometheus_extrinsic( """Subscribes a Bittensor endpoint to the substensor chain. Args: - subtensor (bittensor.subtensor): Bittensor subtensor object. - wallet (bittensor.wallet): Bittensor wallet object. + subtensor (bittensor.core.subtensor.Subtensor): Bittensor subtensor object. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. ip (str): Endpoint host port i.e., ``192.122.31.4``. port (int): Endpoint port number i.e., `9221`. netuid (int): Network `uid` to serve on. diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index d5d6e1ff9..fb13f08f4 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -104,7 +104,7 @@ def serve_extrinsic( Args: subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance object. - wallet (bittensor.wallet): Bittensor wallet object. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. ip (str): Endpoint host port i.e., ``192.122.31.4``. port (int): Endpoint port number i.e., ``9221``. protocol (int): An ``int`` representation of the protocol. @@ -257,8 +257,8 @@ def publish_metadata( Publishes metadata on the Bittensor network using the specified wallet and network identifier. Args: - self (bittensor.subtensor): The subtensor instance representing the Bittensor blockchain connection. - wallet (bittensor.wallet): The wallet object used for authentication in the transaction. + self (bittensor.core.subtensor.Subtensor): The subtensor instance representing the Bittensor blockchain connection. + wallet (bittensor_wallet.Wallet): The wallet object used for authentication in the transaction. netuid (int): Network UID on which the metadata is to be published. data_type (str): The data type of the information being submitted. It should be one of the following: ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing algorithm used for the data. data (str): The actual metadata content to be published. This should be formatted or hashed according to the ``type`` specified. (Note: max ``str`` length is 128 bytes) diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 554c28d29..12aa62f22 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -117,8 +117,8 @@ def set_weights_extrinsic( """Sets the given weights and values on chain for wallet hotkey account. Args: - subtensor (bittensor.subtensor): Subtensor endpoint to use. - wallet (bittensor.wallet): Bittensor wallet object. + subtensor (bittensor.core.subtensor.Subtensor): Subtensor endpoint to use. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. netuid (int): The ``netuid`` of the subnet to set weights for. 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. diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 9d0e72d6b..7bd23ec97 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -107,7 +107,7 @@ def transfer_extrinsic( Args: subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. - wallet (bittensor.wallet): Bittensor wallet object to make transfer from. + wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. dest (str, ss58_address or ed25519): Destination public key address of receiver. amount (Union[Balance, int]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. 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. diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 18858e53d..99c08a9c8 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -1601,7 +1601,7 @@ def commit_weights( This action serves as a commitment or snapshot of the neuron's current weight distribution. Args: - wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the weights. netuid (int): The unique identifier of the subnet. salt (list[int]): list of randomly generated integers as salt to generated weighted hash. uids (np.ndarray): NumPy array of neuron UIDs for which weights are being committed. @@ -1678,7 +1678,7 @@ def reveal_weights( This action serves as a revelation of the neuron's previously committed weight distribution. Args: - wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. netuid (int): The unique identifier of the subnet. uids (np.ndarray): NumPy array of neuron UIDs for which weights are being revealed. weights (np.ndarray): NumPy array of weight values corresponding to each UID. diff --git a/bittensor/utils/btlogging/format.py b/bittensor/utils/btlogging/format.py index 557973884..1aa505c82 100644 --- a/bittensor/utils/btlogging/format.py +++ b/bittensor/utils/btlogging/format.py @@ -186,7 +186,9 @@ class BtFileFormatter(logging.Formatter): centers the level name. """ - def formatTime(self, record: "logging.LogRecord", datefmt: Optional[str] = None) -> str: + def formatTime( + self, record: "logging.LogRecord", datefmt: Optional[str] = None + ) -> str: """ Override formatTime to add milliseconds. diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index 6790a5750..954d8e236 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -461,9 +461,9 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): default_logging_debug = os.getenv("BT_LOGGING_DEBUG") or False default_logging_trace = os.getenv("BT_LOGGING_TRACE") or False default_logging_record_log = os.getenv("BT_LOGGING_RECORD_LOG") or False - default_logging_logging_dir = ( - os.getenv("BT_LOGGING_LOGGING_DIR") or os.path.join("~", ".bittensor", "miners") - ) + default_logging_logging_dir = os.getenv( + "BT_LOGGING_LOGGING_DIR" + ) or os.path.join("~", ".bittensor", "miners") parser.add_argument( "--" + prefix_str + "logging.debug", action="store_true", @@ -497,7 +497,7 @@ def config(cls) -> "Config": """Get config from the argument parser. Return: - config (bittensor.config): config object + config (bittensor.core.config.Config): config object """ parser = argparse.ArgumentParser() cls.add_args(parser) diff --git a/tests/e2e_tests/test_axon.py b/tests/e2e_tests/test_axon.py index ed6be9784..bcf8650fd 100644 --- a/tests/e2e_tests/test_axon.py +++ b/tests/e2e_tests/test_axon.py @@ -47,7 +47,7 @@ async def test_axon(local_chain): local_chain, wallet, netuid ), f"Neuron wasn't registered to subnet {netuid}" - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + metagraph = bittensor.Metagraph(netuid=netuid, network="ws://localhost:9945") # Validate current metagraph stats old_axon = metagraph.axons[0] @@ -94,7 +94,7 @@ async def test_axon(local_chain): await asyncio.sleep(5) # Refresh the metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + metagraph = bittensor.Metagraph(netuid=netuid, network="ws://localhost:9945") updated_axon = metagraph.axons[0] external_ip = networking.get_external_ip() diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py index c5db48b0d..1974854b9 100644 --- a/tests/e2e_tests/test_commit_weights.py +++ b/tests/e2e_tests/test_commit_weights.py @@ -59,7 +59,7 @@ async def test_commit_and_reveal_weights(local_chain): netuid, ), "Unable to enable commit reveal on the subnet" - subtensor = bittensor.subtensor(network="ws://localhost:9945") + subtensor = bittensor.Subtensor(network="ws://localhost:9945") assert subtensor.get_subnet_hyperparameters( netuid=netuid ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" @@ -73,7 +73,7 @@ async def test_commit_and_reveal_weights(local_chain): return_error_message=True, ) - subtensor = bittensor.subtensor(network="ws://localhost:9945") + subtensor = bittensor.Subtensor(network="ws://localhost:9945") assert ( subtensor.get_subnet_hyperparameters( netuid=netuid @@ -92,7 +92,7 @@ async def test_commit_and_reveal_weights(local_chain): call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, return_error_message=True, ) - subtensor = bittensor.subtensor(network="ws://localhost:9945") + subtensor = bittensor.Subtensor(network="ws://localhost:9945") assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 ), "Failed to set weights_rate_limit" diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py index d7e3e6ff6..3f02d021c 100644 --- a/tests/e2e_tests/test_dendrite.py +++ b/tests/e2e_tests/test_dendrite.py @@ -56,7 +56,7 @@ async def test_dendrite(local_chain): local_chain, bob_wallet, netuid ), f"Neuron wasn't registered to subnet {netuid}" - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + metagraph = bittensor.Metagraph(netuid=netuid, network="ws://localhost:9945") subtensor = Subtensor(network="ws://localhost:9945") # Assert one neuron is Bob @@ -72,7 +72,7 @@ async def test_dendrite(local_chain): assert add_stake(local_chain, bob_wallet, bittensor.Balance.from_tao(10_000)) # Refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + metagraph = bittensor.Metagraph(netuid=netuid, network="ws://localhost:9945") old_neuron = metagraph.neurons[0] # Assert stake is 10000 @@ -121,7 +121,7 @@ async def test_dendrite(local_chain): await wait_epoch(subtensor, netuid=netuid) # Refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + metagraph = bittensor.Metagraph(netuid=netuid, network="ws://localhost:9945") # Refresh validator neuron updated_neuron = metagraph.neurons[0] diff --git a/tests/e2e_tests/test_liquid_alpha.py b/tests/e2e_tests/test_liquid_alpha.py index f85cde44e..21492fba8 100644 --- a/tests/e2e_tests/test_liquid_alpha.py +++ b/tests/e2e_tests/test_liquid_alpha.py @@ -52,7 +52,7 @@ def test_liquid_alpha(local_chain): add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(100_000)) # Assert liquid alpha is disabled - subtensor = bittensor.subtensor(network="ws://localhost:9945") + subtensor = bittensor.Subtensor(network="ws://localhost:9945") assert ( subtensor.get_subnet_hyperparameters(netuid=netuid).liquid_alpha_enabled is False diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py index ca27acd3a..60dc2826a 100644 --- a/tests/e2e_tests/test_metagraph.py +++ b/tests/e2e_tests/test_metagraph.py @@ -64,7 +64,7 @@ def test_metagraph(local_chain): ).serialize(), "Subnet wasn't created successfully" # Initialize metagraph - subtensor = bittensor.subtensor(network="ws://localhost:9945") + subtensor = bittensor.Subtensor(network="ws://localhost:9945") metagraph = subtensor.metagraph(netuid=1) # Assert metagraph is empty diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py index e9bf86389..5665e6e05 100644 --- a/tests/e2e_tests/test_subtensor_functions.py +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -31,7 +31,7 @@ async def test_subtensor_extrinsics(local_chain): AssertionError: If any of the checks or verifications fail """ netuid = 1 - subtensor = bittensor.subtensor(network="ws://localhost:9945") + subtensor = bittensor.Subtensor(network="ws://localhost:9945") # Subnets 0 and 3 are bootstrapped from the start assert subtensor.get_subnets() == [0, 3] @@ -139,7 +139,7 @@ async def test_subtensor_extrinsics(local_chain): await asyncio.sleep( 5 ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - subtensor = bittensor.subtensor(network="ws://localhost:9945") + subtensor = bittensor.Subtensor(network="ws://localhost:9945") # Verify neuron info is updated after running as a validator neuron_info = subtensor.get_neuron_for_pubkey_and_subnet( diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py index 148da7680..aad53812c 100644 --- a/tests/e2e_tests/utils/chain_interactions.py +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -1,20 +1,24 @@ """ -This module provides functions interacting with the chain for end to end testing; +This module provides functions interacting with the chain for end-to-end testing; these are not present in btsdk but are required for e2e tests """ import asyncio -from typing import Dict, List, Tuple, Union +from typing import Union, Optional, TYPE_CHECKING -from substrateinterface import SubstrateInterface - -import bittensor from bittensor import logging +# for typing purposes +if TYPE_CHECKING: + from bittensor import Wallet + from bittensor.core.subtensor import Subtensor + from bittensor.utils.balance import Balance + from substrateinterface import SubstrateInterface + def sudo_set_hyperparameter_bool( - substrate: SubstrateInterface, - wallet: bittensor.wallet, + substrate: "SubstrateInterface", + wallet: "Wallet", call_function: str, value: bool, netuid: int, @@ -38,12 +42,12 @@ def sudo_set_hyperparameter_bool( def sudo_set_hyperparameter_values( - substrate: SubstrateInterface, - wallet: bittensor.wallet, + substrate: "SubstrateInterface", + wallet: "Wallet", call_function: str, - call_params: Dict, + call_params: dict, return_error_message: bool = False, -) -> Union[bool, Tuple[bool, str]]: +) -> Union[bool, tuple[bool, Optional[str]]]: """ Sets liquid alpha values using AdminUtils. Mimics setting hyperparams """ @@ -67,7 +71,7 @@ def sudo_set_hyperparameter_values( def add_stake( - substrate: SubstrateInterface, wallet: bittensor.wallet, amount: bittensor.Balance + substrate: "SubstrateInterface", wallet: "Wallet", amount: "Balance" ) -> bool: """ Adds stake to a hotkey using SubtensorModule. Mimics command of adding stake @@ -87,7 +91,7 @@ def add_stake( return response.is_success -def register_subnet(substrate: SubstrateInterface, wallet: bittensor.wallet) -> bool: +def register_subnet(substrate: "SubstrateInterface", wallet: "Wallet") -> bool: """ Registers a subnet on the chain using wallet. Mimics register subnet command. """ @@ -107,7 +111,7 @@ def register_subnet(substrate: SubstrateInterface, wallet: bittensor.wallet) -> def register_neuron( - substrate: SubstrateInterface, wallet: bittensor.wallet, netuid: int + substrate: "SubstrateInterface", wallet: "Wallet", netuid: int ) -> bool: """ Registers a neuron on a subnet. Mimics subnet register command. @@ -130,7 +134,7 @@ def register_neuron( return response.is_success -async def wait_epoch(subtensor, netuid=1): +async def wait_epoch(subtensor: "Subtensor", netuid: int = 1): """ Waits for the next epoch to start on a specific subnet. @@ -153,7 +157,7 @@ async def wait_epoch(subtensor, netuid=1): await wait_interval(tempo, subtensor, netuid) -async def wait_interval(tempo, subtensor, netuid=1): +async def wait_interval(tempo: int, subtensor: "Subtensor", netuid: int = 1): """ Waits until the next tempo interval starts for a specific subnet. @@ -166,6 +170,7 @@ async def wait_interval(tempo, subtensor, netuid=1): last_epoch = current_block - 1 - (current_block + netuid + 1) % interval next_tempo_block_start = last_epoch + interval last_reported = None + while current_block < next_tempo_block_start: await asyncio.sleep( 1 diff --git a/tests/e2e_tests/utils/test_utils.py b/tests/e2e_tests/utils/test_utils.py index 73838b3a2..2e154fe28 100644 --- a/tests/e2e_tests/utils/test_utils.py +++ b/tests/e2e_tests/utils/test_utils.py @@ -12,7 +12,7 @@ templates_repo = "templates repository" -def setup_wallet(uri: str) -> Tuple[Keypair, bittensor.wallet]: +def setup_wallet(uri: str) -> Tuple[Keypair, bittensor.Wallet]: """ Sets up a wallet using the provided URI. @@ -26,7 +26,7 @@ def setup_wallet(uri: str) -> Tuple[Keypair, bittensor.wallet]: """ keypair = Keypair.create_from_uri(uri) wallet_path = f"/tmp/btcli-e2e-wallet-{uri.strip('/')}" - wallet = bittensor.wallet(path=wallet_path) + wallet = bittensor.Wallet(path=wallet_path) wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True) wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True) diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py index 366d3fa85..34bf4f590 100644 --- a/tests/integration_tests/test_metagraph_integration.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -33,7 +33,7 @@ def setUpModule(): class TestMetagraph: def setup_method(self): self.sub = MockSubtensor() - self.metagraph = bittensor.metagraph(netuid=3, network="mock", sync=False) + self.metagraph = bittensor.Metagraph(netuid=3, network="mock", sync=False) def test_print_empty(self): print(self.metagraph) diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 44805fd42..8539839cc 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -73,21 +73,21 @@ def tearDownClass(cls) -> None: def test_network_overrides(self): """Tests that the network overrides the chain_endpoint.""" # Argument importance: chain_endpoint (arg) > network (arg) > config.subtensor.chain_endpoint > config.subtensor.network - config0 = bittensor.subtensor.config() + config0 = bittensor.Subtensor.config() config0.subtensor.network = "finney" config0.subtensor.chain_endpoint = "wss://finney.subtensor.io" # Should not match bittensor.core.settings.FINNEY_ENTRYPOINT assert config0.subtensor.chain_endpoint != settings.FINNEY_ENTRYPOINT - config1 = bittensor.subtensor.config() + config1 = bittensor.Subtensor.config() config1.subtensor.network = "local" config1.subtensor.chain_endpoint = None # Mock network calls with patch("substrateinterface.SubstrateInterface.connect_websocket"): with patch("substrateinterface.SubstrateInterface.reload_type_registry"): - print(bittensor.subtensor, type(bittensor.subtensor)) + print(bittensor.Subtensor, type(bittensor.Subtensor)) # Choose network arg over config - sub1 = bittensor.subtensor(config=config1, network="local") + sub1 = bittensor.Subtensor(config=config1, network="local") self.assertEqual( sub1.chain_endpoint, settings.LOCAL_ENTRYPOINT, @@ -95,14 +95,14 @@ def test_network_overrides(self): ) # Choose network config over chain_endpoint config - sub2 = bittensor.subtensor(config=config0) + sub2 = bittensor.Subtensor(config=config0) self.assertNotEqual( sub2.chain_endpoint, settings.FINNEY_ENTRYPOINT, # Here we expect the endpoint corresponding to the network "finney" msg="config.network should override config.chain_endpoint", ) - sub3 = bittensor.subtensor(config=config1) + sub3 = bittensor.Subtensor(config=config1) # Should pick local instead of finney (default) assert sub3.network == "local" assert sub3.chain_endpoint == settings.LOCAL_ENTRYPOINT @@ -241,7 +241,7 @@ def test_get_balance(self): assert type(balance) is bittensor.utils.balance.Balance def test_defaults_to_finney(self): - sub = bittensor.subtensor() + sub = bittensor.Subtensor() assert sub.network == "finney" assert sub.chain_endpoint == settings.FINNEY_ENTRYPOINT diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index 4b4609921..3150aaf64 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -48,7 +48,7 @@ def dummy(synapse: SynapseDummy) -> SynapseDummy: @pytest.fixture def setup_dendrite(): - # Assuming bittensor.wallet() returns a wallet object + # Assuming bittensor.Wallet() returns a wallet object user_wallet = get_mock_wallet() dendrite_obj = Dendrite(user_wallet) return dendrite_obj diff --git a/tests/unit_tests/test_metagraph.py b/tests/unit_tests/test_metagraph.py index c04490df8..e4dca70a1 100644 --- a/tests/unit_tests/test_metagraph.py +++ b/tests/unit_tests/test_metagraph.py @@ -125,7 +125,7 @@ def test_process_weights_or_bonds(mock_environment): # TODO: Add more checks to ensure the bonds have been processed correctly -# Mocking the bittensor.subtensor class for testing purposes +# Mocking the bittensor.Subtensor class for testing purposes @pytest.fixture def mock_subtensor(): subtensor = MagicMock() From 0bfde19349e7eac155b07e5eb1036db62acc1879 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:34:10 -0700 Subject: [PATCH 219/260] bittensor/core/extrinsics/commit_weights.py --- bittensor/core/extrinsics/commit_weights.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index be2cd9c2d..65fb07a46 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -57,8 +57,7 @@ def do_commit_weights( Returns: Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - 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. + 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. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) @@ -119,8 +118,7 @@ def commit_weights_extrinsic( Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. + This function provides a user-friendly interface for committing weights to the Bittensor blockchain, ensuring proper error handling and user interaction when required. """ if prompt and not Confirm.ask(f"Would you like to commit weights?"): return False, "User cancelled the operation." @@ -175,8 +173,7 @@ def do_reveal_weights( Returns: Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency - and accountability for the neuron's weight distribution. + This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency and accountability for the neuron's weight distribution. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) @@ -229,6 +226,7 @@ def reveal_weights_extrinsic( """ 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, handling user prompts and error messages. + Args: subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for blockchain interaction. wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. @@ -245,8 +243,7 @@ def reveal_weights_extrinsic( Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, 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. + This function provides a user-friendly interface for revealing weights on the Bittensor blockchain, ensuring proper error handling and user interaction when required. """ if prompt and not Confirm.ask(f"Would you like to reveal weights?"): From 213e57a6ff9c22fbdc24f3c4c1570e3175a77239 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:36:17 -0700 Subject: [PATCH 220/260] bittensor/core/extrinsics/prometheus.py --- bittensor/core/extrinsics/prometheus.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bittensor/core/extrinsics/prometheus.py b/bittensor/core/extrinsics/prometheus.py index b66b49dfc..d0f354dfb 100644 --- a/bittensor/core/extrinsics/prometheus.py +++ b/bittensor/core/extrinsics/prometheus.py @@ -17,10 +17,10 @@ import json from typing import Tuple, Optional, TYPE_CHECKING + from retry import retry from bittensor.core.settings import version_as_int, bt_console -from bittensor.core.types import PrometheusServeCallParams from bittensor.utils import networking as net, format_error_message from bittensor.utils.btlogging import logging from bittensor.utils.networking import ensure_connected @@ -29,6 +29,7 @@ if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor.core.subtensor import Subtensor + from bittensor.core.types import PrometheusServeCallParams # Chain call for `prometheus_extrinsic` @@ -36,7 +37,7 @@ def do_serve_prometheus( self: "Subtensor", wallet: "Wallet", - call_params: PrometheusServeCallParams, + call_params: "PrometheusServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> Tuple[bool, Optional[dict]]: @@ -46,13 +47,13 @@ def do_serve_prometheus( Args: self (bittensor.core.subtensor.Subtensor): Bittensor subtensor object wallet (bittensor_wallet.Wallet): Wallet object. - call_params (:func:`PrometheusServeCallParams`): Prometheus serve call parameters. + call_params (bittensor.core.types.PrometheusServeCallParams): Prometheus serve call parameters. wait_for_inclusion (bool): If ``true``, waits for inclusion. wait_for_finalization (bool): If ``true``, waits for finalization. Returns: success (bool): ``True`` if serve prometheus was successful. - error (:func:`Optional[str]`): Error message if serve prometheus failed, ``None`` otherwise. + error (Optional[str]): Error message if serve prometheus failed, ``None`` otherwise. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) @@ -91,7 +92,7 @@ def prometheus_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization=True, ) -> bool: - """Subscribes a Bittensor endpoint to the substensor chain. + """Subscribes a Bittensor endpoint to the Subtensor chain. Args: subtensor (bittensor.core.subtensor.Subtensor): Bittensor subtensor object. From a335b196a16183a6ed8d2fc50eb2931a88cc2470 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:41:51 -0700 Subject: [PATCH 221/260] bittensor/core/extrinsics/serving.py --- bittensor/core/extrinsics/serving.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index fb13f08f4..3be1afcfc 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -21,17 +21,17 @@ from retry import retry from rich.prompt import Confirm -from bittensor.core.axon import Axon from bittensor.core.errors import MetadataError from bittensor.core.settings import version_as_int, bt_console -from bittensor.core.types import AxonServeCallParams from bittensor.utils import format_error_message, networking as net from bittensor.utils.btlogging import logging from bittensor.utils.networking import ensure_connected # For annotation purposes if TYPE_CHECKING: + from bittensor.core.axon import Axon from bittensor.core.subtensor import Subtensor + from bittensor.core.types import AxonServeCallParams from bittensor_wallet import Wallet @@ -40,24 +40,24 @@ def do_serve_axon( self: "Subtensor", wallet: "Wallet", - call_params: AxonServeCallParams, + call_params: "AxonServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> Tuple[bool, Optional[dict]]: """ - 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. + 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. Args: self (bittensor.core.subtensor.Subtensor): Subtensor instance object. wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. - call_params (AxonServeCallParams): Parameters required for the serve axon call. + 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. Returns: Tuple[bool, Optional[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. + This function is crucial for initializing and announcing a neuron's ``Axon`` service on the network, enhancing the decentralized computation capabilities of Bittensor. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) From 307a266c2de7ca22b80ae2a12ee7ce9043351ea1 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:42:06 -0700 Subject: [PATCH 222/260] axon improvement (add annotation) --- bittensor/core/axon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 374fb6d48..31ab7fbe8 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -340,7 +340,7 @@ def __init__( self.config = config # type: ignore # Get wallet or use default. - self.wallet = wallet or Wallet() + self.wallet: "Wallet" = wallet or Wallet() # Build axon objects. self.uuid = str(uuid.uuid1()) From df57f4d91a072b7cbd789fc81354fae709f28f0a Mon Sep 17 00:00:00 2001 From: Watchmaker Date: Mon, 16 Sep 2024 17:45:22 -0700 Subject: [PATCH 223/260] README updates for SDK --- README.md | 446 ++++++++++++++---------------------------------------- 1 file changed, 113 insertions(+), 333 deletions(-) diff --git a/README.md b/README.md index b9284f2a5..e0b75f6ee 100644 --- a/README.md +++ b/README.md @@ -1,424 +1,204 @@
-# **Bittensor** +# **Bittensor SDK** [![Discord Chat](https://img.shields.io/discord/308323056592486420.svg)](https://discord.gg/bittensor) [![PyPI version](https://badge.fury.io/py/bittensor.svg)](https://badge.fury.io/py/bittensor) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) --- -### Internet-scale Neural Networks +## Internet-scale Neural Networks [Discord](https://discord.gg/qasY3HA9F9) • [Network](https://taostats.io/) • [Research](https://bittensor.com/whitepaper)
-Bittensor is a mining network, similar to Bitcoin, that includes built-in incentives designed to encourage computers to provide access to machine learning models in an efficient and censorship-resistant manner. These models can be queried by users seeking outputs from the network, for instance; generating text, audio, and images, or for extracting numerical representations of these input types. Under the hood, Bittensor’s *economic market*, is facilitated by a blockchain token mechanism, through which producers (***miners***) and the verification of the work done by those miners (***validators***) are rewarded. Miners host, train or otherwise procure machine learning systems into the network as a means of fulfilling the verification problems defined by the validators, like the ability to generate responses from prompts i.e. “What is the capital of Texas?. +## Overview of Bittensor -The token based mechanism under which the miners are incentivized ensures that they are constantly driven to make their knowledge output more useful, in terms of speed, intelligence and diversity. The value generated by the network is distributed directly to the individuals producing that value, without intermediaries. Anyone can participate in this endeavour, extract value from the network, and govern Bittensor. The network is open to all participants, and no individual or group has full control over what is learned, who can profit from it, or who can access it. +Welcome! Bittensor is an open source platform on which you can produce competitive digital commodities. These digital commodities can be machine intelligence, storage space, compute power, protein folding, financial markets prediction, and many more. You are rewarded in **TAO** when you produce best digital commodities. -To learn more about Bittensor, please read our [paper](https://bittensor.com/whitepaper). +## The Bittensor SDK -# Install -There are three ways to install Bittensor +The Opentensor Foundation (OTF) provides all the open source tools, including this Bittensor SDK, the codebase and the documentation, with step-by-step tutorials and guides, to enable you to participate in the Bittensor ecosystem. -1. Through the installer: -```bash -$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/opentensor/bittensor/master/scripts/install.sh)" -``` -2. With pip: -```bash -$ pip3 install bittensor -``` -3. From source: -```bash -$ git clone https://github.com/opentensor/bittensor.git -$ python3 -m pip install -e bittensor/ -``` -4. Using Conda (recommended for **Apple M1**): -```bash -$ conda env create -f ~/.bittensor/bittensor/scripts/environments/apple_m1_environment.yml -$ conda activate bittensor -``` +- **Developer documentation**: https://docs.bittensor.com. +- **A Beginner's Q and A on Bittensor**: https://docs.bittensor.com/questions-and-answers. +- **Bittensor whitepaper**: https://bittensor.com/whitepaper. -To test your installation, type: -```bash -$ btcli --help -``` -or using python -```python -import bittensor -``` +This Bittensor SDK contains ready-to-use Python packages for interacting with the Bittensor ecosystem, writing subnet incentive mechanisms, subnet miners, subnet validators and querying the subtensor (the blockchain part of the Bittensor network). -#### CUDA -If you anticipate using PoW registration for subnets or the faucet (only available on staging), please install `cubit` as well for your version of python. You can find the Opentensor cubit implementation and instructions [here](https://github.com/opentensor/cubit). +--- -For example with python 3.10: -```bash -pip install https://github.com/opentensor/cubit/releases/download/v1.1.2/cubit-1.1.2-cp310-cp310-linux_x86_64.whl -``` +## Is Bittensor a blockchain or an AI platform? -# Wallets +In Bittensor there is one blockchain, and many platforms that are connected to this one blockchain. We call these platforms as **subnets**, and this one blockchain as **subtensor**. So, a subnet can be AI-related or it can be something else. The Bittensor network has a number of distinct subnets. All these subnets interact with subtensor blockchain. If you are thinking, "So, subnets are not part of the blockchain but only interact with it?" then the answer is "yes, exactly." -Wallets are the core ownership and identity technology around which all functions on Bittensor are carried out. Bittensor wallets consists of a coldkey and hotkey where the coldkey may contain many hotkeys, while each hotkey can only belong to a single coldkey. Coldkeys store funds securely, and operate functions such as transfers and staking, while hotkeys are used for all online operations such as signing queries, running miners and validating. +## Subnets -Wallets can be created in two ways. -1. Using the python-api -```python -import bittensor -wallet = bittensor.wallet() -wallet.create_new_coldkey() -wallet.create_new_hotkey() -print (wallet) -"Wallet (default, default, ~/.bittensor/wallets/)" -``` -2. Or using btcli -> Use the subcommand `wallet` or it's alias `w`: -```bash -$ btcli wallet new_coldkey - Enter wallet name (default): - - IMPORTANT: Store this mnemonic in a secure (preferably offline place), as anyone who has possession of this mnemonic can use it to regenerate the key and access your tokens. - The mnemonic to the new coldkey is: - **** *** **** **** ***** **** *** **** **** **** ***** ***** - You can use the mnemonic to recreate the key in case it gets lost. The command to use to regenerate the key using this mnemonic is: - btcli w regen_coldkey --mnemonic post maid erode shy captain verify scan shoulder brisk mountain pelican elbow - -$ btcli wallet new_hotkey - Enter wallet name (default): d1 - Enter hotkey name (default): - - IMPORTANT: Store this mnemonic in a secure (preferably offline place), as anyone who has possession of this mnemonic can use it to regenerate the key and access your tokens. - The mnemonic to the new hotkey is: - **** *** **** **** ***** **** *** **** **** **** ***** ***** - You can use the mnemonic to recreate the key in case it gets lost. The command to use to regenerate the key using this mnemonic is: - btcli w regen_hotkey --mnemonic total steak hour bird hedgehog trim timber can friend dry worry text -``` -In both cases you should be able to view your keys by navigating to ~/.bittensor/wallets or viewed by running ```btcli wallet list``` -```bash -$ tree ~/.bittensor/ - .bittensor/ # Bittensor, root directory. - wallets/ # The folder containing all bittensor wallets. - default/ # The name of your wallet, "default" - coldkey # You encrypted coldkey. - coldkeypub.txt # Your coldkey public address - hotkeys/ # The folder containing all of your hotkeys. - default # You unencrypted hotkey information. -``` -Your default wallet ```Wallet (default, default, ~/.bittensor/wallets/)``` is always used unless you specify otherwise. Be sure to store your mnemonics safely. If you lose your password to your wallet, or the access to the machine where the wallet is stored, you can always regenerate the coldkey using the mnemonic you saved from above. -```bash -$ btcli wallet regen_coldkey --mnemonic **** *** **** **** ***** **** *** **** **** **** ***** ***** -``` +Each category of the digital commodity is produced in a distinct subnet. Applications are built on these specific subnets. End-users of these applications would be served by these applications. -## Using the cli -The Bittensor command line interface (`btcli`) is the primary command line tool for interacting with the Bittensor network. It can be used to deploy nodes, manage wallets, stake/unstake, nominate, transfer tokens, and more. +## Subnet validators and subnet miners -### Basic Usage +Subnets, which exist outside the blockchain and are connected to it, are off-chain competitions where only the best producers are rewarded. A subnet consists of off-chain **subnet validators** who initiate the competition for a specific digital commodity, and off-chain **subnet miners** who compete and respond by producing the best quality digital commodity. -To get the list of all the available commands and their descriptions, you can use: +## Yuma Consensus -```bash -btcli --help +Scores are assigned to the top-performing subnet miners and subnet validators. The on-chain Yuma Consensus determines the TAO rewards for these top performers. The Bittensor blockchain, the subtensor, runs on decentralized validation nodes, just like any blockchain. -usage: btcli +**This SDK repo is for Bittensor platform only** +This Bittensor SDK codebase is for the Bittensor platform only. For subnets and applications, refer to subnet-specific websites, which are maintained by subnet owners. -bittensor cli v{bittensor.__version__} +## Release Notes -commands: - subnets (s, subnet) - Commands for managing and viewing subnetworks. - root (r, roots) - Commands for managing and viewing the root network. - wallet (w, wallets) - Commands for managing and viewing wallets. - stake (st, stakes) - Commands for staking and removing stake from hotkey accounts. - sudo (su, sudos) - Commands for subnet management. - legacy (l) - Miscellaneous commands. -``` +See [Bittensor SDK Release Notes](https://docs.bittensor.com/bittensor-rel-notes). -### Example Commands +--- -#### Viewing Senate Proposals -```bash -btcli root proposals -``` +## Install Bittensor SDK -#### Viewing Senate Members -```bash -btcli root list_delegates -``` +Before you can start developing, you must install Bittensor SDK and then create Bittensor wallet. -#### Viewing Proposal Votes -```bash -btcli root senate_vote --proposal=[PROPOSAL_HASH] -``` +## Upgrade -#### Registering for Senate -```bash -btcli root register -``` +If you already installed Bittensor SDK, make sure you upgrade to the latest version. Run the below command: -#### Leaving Senate ```bash -btcli root undelegate +python3 -m pip install --upgrade bittensor ``` -#### Voting in Senate -```bash -btcli root senate_vote --proposal=[PROPOSAL_HASH] -``` - -#### Miscellaneous Commands -```bash -btcli legacy update -btcli legacy faucet -``` +--- -#### Managing Subnets -```bash -btcli subnets list -btcli subnets create -``` +## Install on macOS and Linux -#### Managing Wallets -```bash -btcli wallet list -btcli wallet transfer -``` +You can install Bittensor SDK on your local machine in either of the following ways. **Make sure you verify your installation after you install**: +- [Install using a Bash command](#install-using-a-bash-command). +- [Install using `pip3 install`](#install-using-pip3-install) +- [Install from source](#install-from-source) -### Note +### Install using a Bash command -Please replace the subcommands and arguments as necessary to suit your needs, and always refer to `btcli --help` or `btcli --help` for the most up-to-date and accurate information. +This is the most straightforward method. It is recommended for a beginner as it will pre-install requirements like Python, if they are not already present on your machine. Copy and paste the following `bash` command into your terminal: -For example: ```bash -btcli subnets --help - -usage: btcli subnets [-h] {list,metagraph,lock_cost,create,register,pow_register,hyperparameters} ... - -positional arguments: - {list,metagraph,lock_cost,create,register,pow_register,hyperparameters} - Commands for managing and viewing subnetworks. - list List all subnets on the network. - metagraph View a subnet metagraph information. - lock_cost Return the lock cost to register a subnet. - create Create a new bittensor subnetwork on this chain. - register Register a wallet to a network. - pow_register Register a wallet to a network using PoW. - hyperparameters View subnet hyperparameters. - -options: - -h, --help show this help message and exit +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/opentensor/bittensor/master/scripts/install.sh)" ``` -### Post-Installation Steps +**For Ubuntu-Linux users** +If you are using Ubuntu-Linux, the script will prompt for `sudo` access to install all required apt-get packages. -To enable autocompletion for Bittensor CLI, run the following commands: +### Install using `pip3 install` ```bash -btcli --print-completion bash >> ~/.bashrc # For Bash -btcli --print-completion zsh >> ~/.zshrc # For Zsh -source ~/.bashrc # Reload Bash configuration to take effect +pip3 install bittensor ``` -# The Bittensor Package -The bittensor package contains data structures for interacting with the bittensor ecosystem, writing miners, validators and querying the network. Additionally, it provides many utilities for efficient serialization of Tensors over the wire, performing data analysis of the network, and other useful utilities. +### Install from source -In the 7.0.0 release, we have removed `torch` by default. However, you can still use `torch` by setting the environment variable -`USE_TORCH=1` and making sure that you have installed the `torch` library. -You can install `torch` by running `pip install bittensor[torch]` (if installing via PyPI), or by running `pip install -e ".[torch]"` (if installing from source). -We will not be adding any new functionality based on torch. +1. Create and activate a virtual environment -Wallet: Interface over locally stored bittensor hot + coldkey styled wallets. -```python -import bittensor -# Bittensor's wallet maintenance class. -wallet = bittensor.wallet() -# Access the hotkey -wallet.hotkey -# Access the coldkey -wallet.coldkey ( requires decryption ) -# Sign data with the keypair. -wallet.coldkey.sign( data ) + - Create Python virtual environment. Follow [this guide on python.org](https://docs.python.org/3/library/venv.html#creating-virtual-environments). -``` + - Activate the new environment. Follow [this guide on python.org](https://docs.python.org/3/library/venv.html#how-venvs-work) -Subtensor: Interfaces with bittensor's blockchain and can perform operations like extracting state information or sending transactions. -```python -import bittensor -# Bittensor's chain interface. -subtensor = bittensor.subtensor() -# Get the chain block -subtensor.get_current_block() -# Transfer Tao to a destination address. -subtensor.transfer( wallet = wallet, dest = "xxxxxxx..xxxxx", amount = 10.0) -# Register a wallet onto a subnetwork -subtensor.register( wallet = wallet, netuid = 1 ) -``` +2. Clone the Bittensor SDK repo -Metagraph: Encapsulates the chain state of a particular subnetwork at a specific block. -```python -import bittensor -# Bittensor's chain state object. -metagraph = bittensor.metagraph( netuid = 1 ) -# Resync the graph with the most recent chain state -metagraph.sync() -# Get the list of stake values -print ( metagraph.S ) -# Get endpoint information for the entire subnetwork -print ( metagraph.axons ) -# Get the hotkey information for the miner in the 10th slot -print ( metagraph.hotkeys[ 10 ] ) -# Sync the metagraph at another block -metagraph.sync( block = 100000 ) -# Save the metagraph -metagraph.save() -# Load the same -metagraph.load() +```bash +git clone https://github.com/opentensor/bittensor.git ``` +3. Change to the Bittensor directory: -Synapse: Responsible for defining the protocol definition between axon servers and dendrite clients -```python -class Topk( bittensor.Synapse ): - topk: int = 2 # Number of "top" elements to select - input: bittensor.Tensor = pydantic.Field(..., frozen=True) # Ensure that input cannot be set on the server side. - v: bittensor.Tensor = None - i: bittensor.Tensor = None - -def topk( synapse: Topk ) -> Topk: - v, i = torch.topk( synapse.input.deserialize(), k = synapse.topk ) - synapse.v = bittensor.Tensor.serialize( v ) - synapse.i = bittensor.Tensor.serialize( i ) - return synapse - -# Attach the forward function to the axon and start. -axon = bittensor.axon().attach( topk ).start() +```bash +cd bittensor ``` -Axon: Serves Synapse protocols with custom blacklist, priority and verify functions. +4. Install + +Run the below command to install Bittensor SDK in the above virtual environment. ```python -import bittensor - -class MySynapse( bittensor.Synapse ): - input: int = 1 - output: int = None - -# Define a custom request forwarding function -def forward( synapse: MySynapse ) -> MySynapse: - # Apply custom logic to synapse and return it - synapse.output = 2 - return synapse - -# Define a custom request verification function -def verify_my_synapse( synapse: MySynapse ): - # Apply custom verification logic to synapse - # Optionally raise Exception - -# Define a custom request blacklist function -def blacklist_my_synapse( synapse: MySynapse ) -> bool: - # Apply custom blacklist - # return False ( if non blacklisted ) or True ( if blacklisted ) - -# Define a custom request priority function -def prioritize_my_synape( synapse: MySynapse ) -> float: - # Apply custom priority - return 1.0 - -# Initialize Axon object with a custom configuration -my_axon = bittensor.axon(config=my_config, wallet=my_wallet, port=9090, ip="192.0.2.0", external_ip="203.0.113.0", external_port=7070) - -# Attach the endpoint with the specified verification and forwarding functions -my_axon.attach( - forward_fn = forward_my_synapse, - verify_fn=verify_my_synapse, - blacklist_fn = blacklist_my_synapse, - priority_fn = prioritize_my_synape -).start() -``` - -Dendrite: Represents the abstracted implementation of a network client module -designed to send requests to those endpoints to receive inputs. - -Example: -```python -dendrite_obj = dendrite( wallet = bittensor.wallet() ) -# pings the axon endpoint -await d( ) -# ping multiple axon endpoints -await d( [] ) -# Send custom synapse request to axon. -await d( bittensor.axon(), bittensor.Synapse() ) -# Query all metagraph objects. -await d( meta.axons, bittensor.Synapse() ) +pip install . ``` -## Setting weights on root network -Use the `root` subcommand to access setting weights on the network across subnets. +--- -```bash -btcli root weights --wallet.name --wallet.hotkey -Enter netuids (e.g. 0, 1, 2 ...): -# Here enter your selected netuids to set weights on -1, 2 +## Install on Windows ->Enter weights (e.g. 0.09, 0.09, 0.09 ...): -# These do not need to sum to 1, we do normalization on the backend. -# Values must be > 0 -0.5, 10 +To install and run Bittensor SDK on Windows you must install [**WSL 2** (Windows Subsystem for Linux)](https://learn.microsoft.com/en-us/windows/wsl/about) on Windows and select [Ubuntu Linux distribution](https://github.com/ubuntu/WSL/blob/main/docs/guides/install-ubuntu-wsl2.md). -Normalized weights: - tensor([ 0.5000, 10.0000]) -> tensor([0.0476, 0.9524]) +After you installed the above, follow the same installation steps described above in [Install on macOS and Linux](#install-on-macos-and-linux). -Do you want to set the following root weights?: - weights: tensor([0.0476, 0.9524]) - uids: tensor([1, 2])? [y/n]: -y +**ALERT**: **Limited support on Windows** +While wallet transactions like delegating, transfer, registering, staking can be performed on a Windows machine using WSL 2, the mining and validating operations are not recommended and are not supported on Windows machines. -⠏ 📡 Setting root weights on test ... -``` +--- -## Bittensor Subnets API +## Verify the installation -This guide provides instructions on how to extend the Bittensor Subnets API, a powerful interface for interacting with the Bittensor network across subnets. The Bittensor Subnets API facilitates querying across any subnet that has exposed API endpoints to unlock utility of the Bittensor decentralized network. +You can verify your installation in either of the two ways as shown below: -The Bittensor Subnets API consists of abstract classes and a registry system to dynamically handle API interactions. It allows developers to implement custom logic for storing and retrieving data, while also providing a straightforward way for end users to interact with these functionalities. +### Verify using the `btcli` command -### Core Components +Using the [Bittensor Command Line Interface](https://docs.bittensor.com/btcli). -- **APIRegistry**: A central registry that manages API handlers. It allows for dynamic retrieval of handlers based on keys. -- **SubnetsAPI (Abstract Base Class)**: Defines the structure for API implementations, including methods for querying the network and processing responses. -- **StoreUserAPI & RetrieveUserAPI**: Concrete implementations of the `SubnetsAPI` for storing and retrieving user data. +```bash +btcli --help +``` +which will give you the below output: -### Implementing Custom Subnet APIs +```text +usage: btcli +bittensor cli +... +``` +You will see the version number you installed in place of ``. -To implement your own subclasses of `bittensor.SubnetsAPI` to integrate an API into your subnet. +### Verify using Python interpreter -1. **Inherit from `SubnetsAPI`**: Your class should inherit from the `SubnetsAPI` abstract base class. +1. Launch the Python interpreter on your terminal. -2. **Implement Required Methods**: Implement the `prepare_synapse` and `process_responses` abstract methods with your custom logic. + ```bash + python3 + ``` +2. Enter the following two lines in the Python interpreter. + + ```python + import bittensor as bt + print( bt.__version__ ) + ``` + The Python interpreter output will look like below: -That's it! For example: + ```python + Python 3.11.6 (main, Oct 2 2023, 13:45:54) [Clang 15.0.0 (clang-1500.0.40.1)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> import bittensor as bt + >>> print( bt.__version__ ) + + ``` +You will see the version number you installed in place of ``. -```python -import bittensor +### Verify by listing axon information -class CustomSubnetAPI(bittensor.SubnetsAPI): - def __init__(self, wallet: "bittensor.wallet"): - super().__init__(wallet) - # Custom initialization here +You can also verify the Bittensor SDK installation by listing the axon information for the neurons. Enter the following lines in the Python interpreter. - def prepare_synapse(self, *args, **kwargs): - # Custom synapse preparation logic - pass +```python +import bittensor as bt +metagraph = bt.metagraph(1) +metagraph.axons[:10] +``` +The Python interpreter output will look like below. - def process_responses(self, responses): - # Custom response processing logic - pass +```bash +[AxonInfo( /ipv4/3.139.80.241:11055, 5GqDsK6SAPyQtG243hbaKTsoeumjQQLhUu8GyrXikPTmxjn7, 5D7u5BTqF3j1XHnizp9oR67GFRr8fBEFhbdnuVQEx91vpfB5, 600 ), AxonInfo( /ipv4/8.222.132.190:5108, 5CwqDkDt1uk2Bngvf8avrapUshGmiUvYZjYa7bfA9Gv9kn1i, 5HQ9eTDorvovKTxBc9RUD22FZHZzpy1KRfaxCnRsT9QhuvR6, 600 ), AxonInfo( /ipv4/34.90.71.181:8091, 5HEo565WAy4Dbq3Sv271SAi7syBSofyfhhwRNjFNSM2gP9M2, 5ChuGqW2cxc5AZJ29z6vyTkTncg75L9ovfp8QN8eB8niSD75, 601 ), AxonInfo( /ipv4/64.247.206.79:8091, 5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN, 5E7W9QXNoW7se7B11vWRMKRCSWkkAu9EYotG5Ci2f9cqV8jn, 601 ), AxonInfo( /ipv4/51.91.30.166:40203, 5EXYcaCdnvnMZbozeknFWbj6aKXojfBi9jUpJYHea68j4q1a, 5CsxoeDvWsQFZJnDCyzxaNKgA8pBJGUJyE1DThH8xU25qUMg, 601 ), AxonInfo( /ipv4/149.137.225.62:8091, 5F4tQyWrhfGVcNhoqeiNsR6KjD4wMZ2kfhLj4oHYuyHbZAc3, 5Ccmf1dJKzGtXX7h17eN72MVMRsFwvYjPVmkXPUaapczECf6, 600 ), AxonInfo( /ipv4/38.147.83.11:8091, 5Hddm3iBFD2GLT5ik7LZnT3XJUnRnN8PoeCFgGQgawUVKNm8, 5DCQw11aUW7bozAKkB8tB5bHqAjiu4F6mVLZBdgJnk8dzUoV, 610 ), AxonInfo( /ipv4/38.147.83.30:41422, 5HNQURvmjjYhTSksi8Wfsw676b4owGwfLR2BFAQzG7H3HhYf, 5EZUTdAbXyLmrs3oiPvfCM19nG6oRs4X7zpgxG5oL1iK4MAh, 610 ), AxonInfo( /ipv4/54.227.25.215:10022, 5DxrZuW8kmkZPKGKp1RBVovaP5zHtPLDHYc5Yu82Z1fWqK5u, 5FhXUSmSZ2ec7ozRSA8Bg3ywmGwrjoLLzsXjNcwmZme2GcSC, 601 ), AxonInfo( /ipv4/52.8.243.76:40033, 5EnZN591jjsKKbt3yBtfGKWHxhxRH9cJonqTKRT5yTRUyNon, 5ChzhHyGmWwEdHjuvAxoUifHEZ6xpUjR67fDd4a42UrPysyB, 601 )] +>>> ``` -## Release -The release manager should follow the instructions of the [RELEASE_GUIDELINES.md](./RELEASE_GUIDELINES.md) document. +--- + +## Release Guidelines +Instructions for the release manager: [RELEASE_GUIDELINES.md](./contrib/RELEASE_GUIDELINES.md) document. ## Contributions -Please review the [contributing guide](./contrib/CONTRIBUTING.md) for more information before making a pull request. +Ready to contributre? Read the [contributing guide](./contrib/CONTRIBUTING.md) before making a pull request. ## License The MIT License (MIT) From f76ea383db651be2a26163857a1e83638efa054c Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:47:31 -0700 Subject: [PATCH 224/260] bittensor/core/extrinsics/set_weights.py --- bittensor/core/extrinsics/set_weights.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py index 12aa62f22..aa93f8ed4 100644 --- a/bittensor/core/extrinsics/set_weights.py +++ b/bittensor/core/extrinsics/set_weights.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. import logging -from typing import List, Union, Tuple, Optional, TYPE_CHECKING +from typing import Union, Optional, TYPE_CHECKING import numpy as np from numpy.typing import NDArray @@ -40,28 +40,28 @@ def do_set_weights( self: "Subtensor", wallet: "Wallet", - uids: List[int], - vals: List[int], + uids: list[int], + vals: list[int], netuid: int, version_key: int = version_as_int, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -) -> Tuple[bool, Optional[dict]]: # (success, error_message) +) -> tuple[bool, Optional[dict]]: # (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. Args: self (bittensor.core.subtensor.Subtensor): Subtensor interface wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. - uids (List[int]): List of neuron UIDs for which weights are being set. - vals (List[int]): List of weight values corresponding to each UID. + uids (list[int]): List of neuron UIDs for which weights are being set. + vals (list[int]): List of weight values corresponding to each UID. netuid (int): Unique identifier for the network. 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. Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + tuple[bool, Optional[str]]: A tuple containing a success flag and an optional response 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. """ @@ -113,7 +113,7 @@ def set_weights_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, prompt: bool = False, -) -> Tuple[bool, str]: +) -> tuple[bool, str]: """Sets the given weights and values on chain for wallet hotkey account. Args: @@ -128,7 +128,7 @@ def set_weights_extrinsic( prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + tuple[bool, str]: A tuple containing a success flag and an optional response message. """ # First convert types. if use_torch(): From a6d14bca9c6f574d06f03edb29cc3a60076ae060 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:49:00 -0700 Subject: [PATCH 225/260] bittensor/core/extrinsics/transfer.py --- bittensor/core/extrinsics/transfer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 7bd23ec97..8d4a7335b 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -15,7 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from typing import Dict, Tuple, Optional, Union, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING from retry import retry from rich.prompt import Confirm @@ -45,7 +45,7 @@ def do_transfer( transfer_balance: "Balance", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> Tuple[bool, Optional[str], Optional[Dict]]: +) -> tuple[bool, Optional[str], Optional[dict]]: """Sends a transfer extrinsic to the chain. Args: @@ -97,7 +97,7 @@ def transfer_extrinsic( subtensor: "Subtensor", wallet: "Wallet", dest: str, - amount: Union[Balance, float], + amount: Union["Balance", float], wait_for_inclusion: bool = True, wait_for_finalization: bool = False, keep_alive: bool = True, From 0f7e701ad094191b525ca658ad5886bf2f547c35 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:52:44 -0700 Subject: [PATCH 226/260] fix --- bittensor/core/axon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 31ab7fbe8..374fb6d48 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -340,7 +340,7 @@ def __init__( self.config = config # type: ignore # Get wallet or use default. - self.wallet: "Wallet" = wallet or Wallet() + self.wallet = wallet or Wallet() # Build axon objects. self.uuid = str(uuid.uuid1()) From 4d8ed9fd8c49a2113be860cbb993c181cad27949 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:52:59 -0700 Subject: [PATCH 227/260] weight_utils.py --- bittensor/utils/weight_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index 8bf992cbd..d7c86bcdc 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -20,10 +20,10 @@ import hashlib import logging import typing -from typing import Union +from typing import Union, Optional import numpy as np -from docopt import Optional + from numpy.typing import NDArray from scalecodec import U16, ScaleBytes, Vec from substrateinterface import Keypair From a9065225c8cd8f9895c42a26d8cd255e784ece5b Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 17:59:40 -0700 Subject: [PATCH 228/260] check up and fix --- bittensor/core/axon.py | 4 ++-- bittensor/core/chain_data/delegate_info.py | 2 +- bittensor/core/chain_data/utils.py | 6 +++--- bittensor/core/config.py | 2 +- bittensor/core/dendrite.py | 12 ++++++------ bittensor/core/extrinsics/commit_weights.py | 20 ++++++++++---------- bittensor/core/extrinsics/prometheus.py | 4 ++-- bittensor/core/extrinsics/serving.py | 6 +++--- bittensor/core/extrinsics/transfer.py | 2 +- bittensor/core/synapse.py | 4 ++-- bittensor/core/tensor.py | 6 +++--- bittensor/utils/subnets.py | 2 +- tests/e2e_tests/utils/test_utils.py | 3 +-- tests/unit_tests/test_axon.py | 4 ++-- tests/unit_tests/test_subtensor.py | 5 ++--- 15 files changed, 40 insertions(+), 42 deletions(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 374fb6d48..5509a3b81 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -454,7 +454,7 @@ def forward_custom(synapse: MyCustomSynapse) -> MyCustomSynapse: # Custom logic for processing the request return synapse - def blacklist_custom(synapse: MyCustomSynapse) -> Tuple[bool, str]: + def blacklist_custom(synapse: MyCustomSynapse) -> tuple[bool, str]: return True, "Allowed!" def priority_custom(synapse: MyCustomSynapse) -> float: @@ -560,7 +560,7 @@ async def endpoint(*args, **kwargs): ) assert ( signature(blacklist_fn) == blacklist_sig - ), f"The blacklist_fn function must have the signature: blacklist( synapse: {request_name} ) -> Tuple[bool, str]" + ), f"The blacklist_fn function must have the signature: blacklist( synapse: {request_name} ) -> tuple[bool, str]" if priority_fn: priority_sig = Signature(expected_params, return_annotation=float) assert ( diff --git a/bittensor/core/chain_data/delegate_info.py b/bittensor/core/chain_data/delegate_info.py index a849aedc2..d77f1e141 100644 --- a/bittensor/core/chain_data/delegate_info.py +++ b/bittensor/core/chain_data/delegate_info.py @@ -17,7 +17,7 @@ class DelegateInfo: Args: hotkey_ss58 (str): Hotkey of the delegate for which the information is being fetched. total_stake (int): Total stake of the delegate. - nominators (list[Tuple[str, int]]): List of nominators of the delegate and their stake. + nominators (list[tuple[str, int]]): List of nominators of the delegate and their stake. take (float): Take of the delegate as a percentage. owner_ss58 (str): Coldkey of the owner. registrations (list[int]): List of subnets that the delegate is registered on. diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py index 6b4e1d569..0544ca85a 100644 --- a/bittensor/core/chain_data/utils.py +++ b/bittensor/core/chain_data/utils.py @@ -65,10 +65,10 @@ def from_scale_encoding_using_type_string( type_string (str): The type string defining the structure of the data. Returns: - Optional[Dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. + Optional[dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. Raises: - TypeError: If the input_ is not a List[int], bytes, or ScaleBytes. + TypeError: If the input_ is not a list[int], bytes, or ScaleBytes. """ if isinstance(input_, ScaleBytes): as_scale_bytes = input_ @@ -79,7 +79,7 @@ def from_scale_encoding_using_type_string( elif isinstance(input_, bytes): as_bytes = input_ else: - raise TypeError("input_ must be a List[int], bytes, or ScaleBytes") + raise TypeError("input_ must be a list[int], bytes, or ScaleBytes") as_scale_bytes = ScaleBytes(as_bytes) diff --git a/bittensor/core/config.py b/bittensor/core/config.py index 80e3de3db..5027bbecb 100644 --- a/bittensor/core/config.py +++ b/bittensor/core/config.py @@ -248,7 +248,7 @@ def __parse_args__( """Parses the passed args use the passed parser. Args: - args (List[str]): List of arguments to parse. + args (list[str]): List of arguments to parse. parser (argparse.ArgumentParser): Command line parser object. strict (bool): If ``true``, the command line arguments are strictly parsed. diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index 7986b6edb..bfdfd4d4a 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -68,7 +68,7 @@ class DendriteMixin: Methods: __str__(): Returns a string representation of the Dendrite object. __repr__(): Returns a string representation of the Dendrite object, acting as a fallback for __str__(). - query(self, *args, **kwargs) -> Union[Synapse, List[Synapse]]: Makes synchronous requests to one or multiple target Axons and returns responses. + query(self, *args, **kwargs) -> Union[Synapse, list[Synapse]]: Makes synchronous requests to one or multiple target Axons and returns responses. forward(self, axons, synapse=Synapse(), timeout=12, deserialize=True, run_async=True, streaming=False) -> Synapse: Asynchronously sends requests to one or multiple Axons and collates their responses. call(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> Synapse: Asynchronously sends a request to a specified Axon and processes the response. call_stream(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> AsyncGenerator[Synapse, None]: Sends a request to a specified Axon and yields an AsyncGenerator that contains streaming response chunks before finally yielding the filled Synapse as the final element. @@ -335,12 +335,12 @@ def query( Cleanup is automatically handled and sessions are closed upon completed requests. Args: - axons (Union[List[Union[bittensor.core.chain_data.axon_info.AxonInfo, 'bittensor.core.axon.Axon']], Union['bittensor.core.chain_data.axon_info.AxonInfo', 'bittensor.core.axon.Axon']]): The list of target Axon information. + axons (Union[list[Union[bittensor.core.chain_data.axon_info.AxonInfo, 'bittensor.core.axon.Axon']], Union['bittensor.core.chain_data.axon_info.AxonInfo', 'bittensor.core.axon.Axon']]): The list of target Axon information. synapse (Optional[bittensor.core.synapse.Synapse]): The Synapse object. Defaults to :func:`Synapse()`. timeout (Optional[float]): The request timeout duration in seconds. Defaults to ``12.0`` seconds. Returns: - Union[bittensor.core.synapse.Synapse, List[bittensor.core.synapse.Synapse]]: If a single target axon is provided, returns the response from that axon. If multiple target axons are provided, returns a list of responses from all target axons. + Union[bittensor.core.synapse.Synapse, list[bittensor.core.synapse.Synapse]]: If a single target axon is provided, returns the response from that axon. If multiple target axons are provided, returns a list of responses from all target axons. """ result = None try: @@ -401,7 +401,7 @@ async def forward( print(chunk) Args: - axons (Union[List[Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]], Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]]): The target Axons to send requests to. Can be a single Axon or a list of Axons. + axons (Union[list[Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]], Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]]): The target Axons to send requests to. Can be a single Axon or a list of Axons. synapse (bittensor.core.synapse.Synapse): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. timeout (float): Maximum duration to wait for a response from an Axon in seconds. Defaults to ``12.0``. deserialize (bool): Determines if the received response should be deserialized. Defaults to ``True``. @@ -409,7 +409,7 @@ async def forward( streaming (bool): Indicates if the response is expected to be in streaming format. Defaults to ``False``. Returns: - Union[AsyncGenerator, bittensor.core.synapse.Synapse, List[bittensor.core.synapse.Synapse]]: If a single `Axon` is targeted, returns its response. + Union[AsyncGenerator, bittensor.core.synapse.Synapse, list[bittensor.core.synapse.Synapse]]: If a single `Axon` is targeted, returns its response. If multiple Axons are targeted, returns a list of their responses. """ is_list = True @@ -441,7 +441,7 @@ async def query_all_axons( If ``True``, responses are handled in streaming mode. Returns: - List[Union[AsyncGenerator, bittensor.core.synapse.Synapse, bittensor.core.stream.StreamingSynapse]]: A list containing the responses from each axon. The type of each response depends on the streaming mode and the type of synapse used. + list[Union[AsyncGenerator, bittensor.core.synapse.Synapse, bittensor.core.stream.StreamingSynapse]]: A list containing the responses from each axon. The type of each response depends on the streaming mode and the type of synapse used. """ async def single_axon_response( diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py index 65fb07a46..131c28cab 100644 --- a/bittensor/core/extrinsics/commit_weights.py +++ b/bittensor/core/extrinsics/commit_weights.py @@ -55,7 +55,7 @@ def do_commit_weights( wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. 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. """ @@ -115,7 +115,7 @@ def commit_weights_extrinsic( prompt (bool): If ``True``, prompts for user confirmation before proceeding. Returns: - Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, a string + tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. @@ -163,15 +163,15 @@ def do_reveal_weights( self (bittensor.core.subtensor.Subtensor): 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. - values (List[int]): List of weight values corresponding to each UID. - salt (List[int]): List of salt values corresponding to the hash function. + uids (list[int]): List of neuron UIDs for which weights are being revealed. + values (list[int]): List of weight values corresponding to each UID. + salt (list[int]): List of salt values corresponding to the hash function. 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. Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency and accountability for the neuron's weight distribution. """ @@ -231,16 +231,16 @@ def reveal_weights_extrinsic( subtensor (bittensor.core.subtensor.Subtensor): 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. - weights (List[int]): List of weight values corresponding to each UID. - salt (List[int]): List of salt values corresponding to the hash function. + uids (list[int]): List of neuron UIDs for which weights are being revealed. + weights (list[int]): List of weight values corresponding to each UID. + salt (list[int]): List of salt values corresponding to the hash function. 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. prompt (bool): If ``True``, prompts for user confirmation before proceeding. Returns: - Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string + tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, 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. diff --git a/bittensor/core/extrinsics/prometheus.py b/bittensor/core/extrinsics/prometheus.py index d0f354dfb..9c73e4327 100644 --- a/bittensor/core/extrinsics/prometheus.py +++ b/bittensor/core/extrinsics/prometheus.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. import json -from typing import Tuple, Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from retry import retry @@ -40,7 +40,7 @@ def do_serve_prometheus( call_params: "PrometheusServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, -) -> Tuple[bool, Optional[dict]]: +) -> tuple[bool, Optional[dict]]: """ Sends a serve prometheus extrinsic to the chain. diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index 3be1afcfc..65345d7c1 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -16,7 +16,7 @@ # DEALINGS IN THE SOFTWARE. import json -from typing import Optional, Tuple, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from retry import retry from rich.prompt import Confirm @@ -43,7 +43,7 @@ def do_serve_axon( call_params: "AxonServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, -) -> Tuple[bool, Optional[dict]]: +) -> tuple[bool, Optional[dict]]: """ 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. @@ -55,7 +55,7 @@ def do_serve_axon( wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + tuple[bool, Optional[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. """ diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py index 8d4a7335b..c2f3c2adc 100644 --- a/bittensor/core/extrinsics/transfer.py +++ b/bittensor/core/extrinsics/transfer.py @@ -59,7 +59,7 @@ def do_transfer( Returns: success (bool): ``True`` if transfer was successful. block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is ``True``. - error (Dict): Error message from subtensor if transfer failed. + error (dict): Error message from subtensor if transfer failed. """ @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) diff --git a/bittensor/core/synapse.py b/bittensor/core/synapse.py index 69fb070c7..a96a92e1a 100644 --- a/bittensor/core/synapse.py +++ b/bittensor/core/synapse.py @@ -41,7 +41,7 @@ def get_size(obj: Any, seen: Optional[set] = None) -> int: Args: obj (Any): The object to get the size of. - seen (Optional[Set]): Set of object ids that have been calculated. + seen (Optional[set]): Set of object ids that have been calculated. Returns: int: The total size of the object. @@ -358,7 +358,7 @@ class Synapse(BaseModel): dendrite (:func:`TerminalInfo`): Information about the dendrite terminal. axon (:func:`TerminalInfo`): Information about the axon terminal. computed_body_hash (str): Computed hash of the request body. - required_hash_fields (List[str]): Fields required to compute the body hash. + required_hash_fields (list[str]): Fields required to compute the body hash. Methods: deserialize: Custom deserialization logic for subclasses. diff --git a/bittensor/core/tensor.py b/bittensor/core/tensor.py index 85cb0241b..4ec71cc44 100644 --- a/bittensor/core/tensor.py +++ b/bittensor/core/tensor.py @@ -109,7 +109,7 @@ def cast_shape(raw: Union[None, list[int], str]) -> Optional[Union[str, list]]: Casts the raw value to a string representing the tensor shape. Args: - raw (Union[None, List[int], str]): The raw value to cast. + raw (Union[None, list[int], str]): The raw value to cast. Returns: str: The string representing the tensor shape. @@ -129,7 +129,7 @@ def cast_shape(raw: Union[None, list[int], str]) -> Optional[Union[str, list]]: return shape else: raise Exception( - f"{raw} of type {type(raw)} does not have a valid type in Union[None, List[int], str]" + f"{raw} of type {type(raw)} does not have a valid type in Union[None, list[int], str]" ) @@ -147,7 +147,7 @@ class Tensor(BaseModel): Args: buffer (Optional[str]): Tensor buffer data. dtype (str): Tensor data type. - shape (List[int]): Tensor shape. + shape (list[int]): Tensor shape. """ model_config = ConfigDict(validate_assignment=True) diff --git a/bittensor/utils/subnets.py b/bittensor/utils/subnets.py index df5b71318..2b42bead9 100644 --- a/bittensor/utils/subnets.py +++ b/bittensor/utils/subnets.py @@ -58,7 +58,7 @@ async def query_api( Queries the API nodes of a subnet using the given synapse and bespoke query function. Args: - axons (Union[bt.axon, List[bt.axon]]): The list of axon(s) to query. + axons (Union[bt.axon, list[bt.axon]]): The list of axon(s) to query. deserialize (Optional[bool]): Whether to deserialize the responses. Defaults to False. timeout (Optional[int]): The timeout in seconds for the query. Defaults to 12. **kwargs: Keyword arguments for the prepare_synapse_fn. diff --git a/tests/e2e_tests/utils/test_utils.py b/tests/e2e_tests/utils/test_utils.py index 2e154fe28..ba662647a 100644 --- a/tests/e2e_tests/utils/test_utils.py +++ b/tests/e2e_tests/utils/test_utils.py @@ -2,7 +2,6 @@ import shutil import subprocess import sys -from typing import Tuple from substrateinterface import Keypair @@ -12,7 +11,7 @@ templates_repo = "templates repository" -def setup_wallet(uri: str) -> Tuple[Keypair, bittensor.Wallet]: +def setup_wallet(uri: str) -> tuple[Keypair, bittensor.Wallet]: """ Sets up a wallet using the provided URI. diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index f8bb912d6..5df465e37 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -19,7 +19,7 @@ import re import time from dataclasses import dataclass -from typing import Any, Tuple, Optional +from typing import Any, Optional from unittest import IsolatedAsyncioTestCase from unittest.mock import AsyncMock, MagicMock, patch @@ -56,7 +56,7 @@ class TestSynapse(Synapse): def forward_fn(synapse: TestSynapse) -> Any: pass - def blacklist_fn(synapse: TestSynapse) -> Tuple[bool, str]: + def blacklist_fn(synapse: TestSynapse) -> tuple[bool, str]: return True, "" def priority_fn(synapse: TestSynapse) -> float: diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 97636229c..d0783d20f 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -17,7 +17,6 @@ import argparse import unittest.mock as mock -from typing import List, Tuple from unittest.mock import MagicMock import pytest @@ -336,7 +335,7 @@ def sample_hyperparameters(): def normalize_hyperparameters( subnet: "SubnetHyperparameters", -) -> List[Tuple[str, str, str]]: +) -> list[tuple[str, str, str]]: """ Normalizes the hyperparameters of a subnet. @@ -360,7 +359,7 @@ def normalize_hyperparameters( "max_burn": Balance.from_rao, } - normalized_values: List[Tuple[str, str, str]] = [] + normalized_values: list[tuple[str, str, str]] = [] subnet_dict = subnet.__dict__ for param, value in subnet_dict.items(): From 6900833cfd523e24ede1849e48eb8c9ed3c59751 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 18:14:52 -0700 Subject: [PATCH 229/260] fix Axon Signature --- bittensor/core/axon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 5509a3b81..8c1a80a1b 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -30,7 +30,7 @@ import uuid import warnings from inspect import signature, Signature, Parameter -from typing import Any, Awaitable, Callable, Optional +from typing import Any, Awaitable, Callable, Optional, Tuple import uvicorn from bittensor_wallet import Wallet @@ -556,7 +556,7 @@ async def endpoint(*args, **kwargs): ] if blacklist_fn: blacklist_sig = Signature( - expected_params, return_annotation=tuple[bool, str] + expected_params, return_annotation=Tuple[bool, str] ) assert ( signature(blacklist_fn) == blacklist_sig From 14e1464e3456e19aaa79a9bdecc3e24f28e04fe3 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 19:49:25 -0700 Subject: [PATCH 230/260] fix axon --- bittensor/core/axon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 8c1a80a1b..f90a2f177 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -556,7 +556,7 @@ async def endpoint(*args, **kwargs): ] if blacklist_fn: blacklist_sig = Signature( - expected_params, return_annotation=Tuple[bool, str] + expected_params, return_annotation=tuple[bool, str] ) assert ( signature(blacklist_fn) == blacklist_sig From 850d5a2e6d991a85fabc46137d1add8879cdf425 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 19:59:57 -0700 Subject: [PATCH 231/260] delete unused import --- bittensor/core/axon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index f90a2f177..5509a3b81 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -30,7 +30,7 @@ import uuid import warnings from inspect import signature, Signature, Parameter -from typing import Any, Awaitable, Callable, Optional, Tuple +from typing import Any, Awaitable, Callable, Optional import uvicorn from bittensor_wallet import Wallet From 678efe6506964d2bdce1b601cff032dd29feaadd Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 20:07:19 -0700 Subject: [PATCH 232/260] update GPG --- bittensor/core/axon.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 5509a3b81..854080eeb 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -14,7 +14,6 @@ # 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. - """Create and initialize Axon, which services the forward and backward requests from other neurons.""" import argparse From 50df9220d9f206530862296501220e63fccc05a8 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 21:02:36 -0700 Subject: [PATCH 233/260] remove unused code, remove old tests, add new tests --- bittensor/core/tensor.py | 7 -- bittensor/utils/deprecated.py | 2 +- tests/unit_tests/test_tensor.py | 186 +++++--------------------------- 3 files changed, 25 insertions(+), 170 deletions(-) diff --git a/bittensor/core/tensor.py b/bittensor/core/tensor.py index c3bb29edb..9f06808a6 100644 --- a/bittensor/core/tensor.py +++ b/bittensor/core/tensor.py @@ -133,13 +133,6 @@ def cast_shape(raw: Union[None, List[int], str]) -> Optional[Union[str, list]]: ) -class tensor: - def __new__(cls, tensor: Union[list, np.ndarray, "torch.Tensor"]): - if isinstance(tensor, list) or isinstance(tensor, np.ndarray): - tensor = torch.tensor(tensor) if use_torch() else np.array(tensor) - return Tensor.serialize(tensor_=tensor) - - class Tensor(BaseModel): """ Represents a Tensor object. diff --git a/bittensor/utils/deprecated.py b/bittensor/utils/deprecated.py index 985801dd6..6075a93d8 100644 --- a/bittensor/utils/deprecated.py +++ b/bittensor/utils/deprecated.py @@ -96,7 +96,7 @@ from bittensor.core.stream import StreamingSynapse # noqa: F401 from bittensor.core.subtensor import Subtensor from bittensor.core.synapse import TerminalInfo, Synapse # noqa: F401 -from bittensor.core.tensor import tensor, Tensor # noqa: F401 +from bittensor.core.tensor import Tensor # noqa: F401 from bittensor.core.threadpool import ( # noqa: F401 PriorityThreadPoolExecutor as PriorityThreadPoolExecutor, ) diff --git a/tests/unit_tests/test_tensor.py b/tests/unit_tests/test_tensor.py index 8c189c6af..9cebaa9ed 100644 --- a/tests/unit_tests/test_tensor.py +++ b/tests/unit_tests/test_tensor.py @@ -15,109 +15,9 @@ # 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 numpy import numpy as np -import pytest -import torch -from bittensor.core.tensor import tensor as tensor_class, Tensor - - -# This is a fixture that creates an example tensor for testing -@pytest.fixture -def example_tensor(): - # Create a tensor from a list using PyTorch - data = np.array([1, 2, 3, 4]) - - # Serialize the tensor into a Tensor instance and return it - return tensor_class(data) - - -@pytest.fixture -def example_tensor_torch(force_legacy_torch_compatible_api): - # Create a tensor from a list using PyTorch - data = torch.tensor([1, 2, 3, 4]) - - # Serialize the tensor into a Tensor instance and return it - return tensor_class(data) - - -def test_deserialize(example_tensor): - # Deserialize the tensor from the Tensor instance - tensor = example_tensor.deserialize() - - # Check that the result is a np.array with the correct values - assert isinstance(tensor, np.ndarray) - assert tensor.tolist() == [1, 2, 3, 4] - - -def test_deserialize_torch(example_tensor_torch, force_legacy_torch_compatible_api): - tensor = example_tensor_torch.deserialize() - # Check that the result is a PyTorch tensor with the correct values - assert isinstance(tensor, torch.Tensor) - assert tensor.tolist() == [1, 2, 3, 4] - - -def test_serialize(example_tensor): - # Check that the serialized tensor is an instance of Tensor - assert isinstance(example_tensor, Tensor) - - # Check that the Tensor instance has the correct buffer, dtype, and shape - assert example_tensor.buffer == example_tensor.buffer - assert example_tensor.dtype == example_tensor.dtype - assert example_tensor.shape == example_tensor.shape - - assert isinstance(example_tensor.tolist(), list) - - # Check that the Tensor instance has the correct buffer, dtype, and shape - assert example_tensor.buffer == example_tensor.buffer - assert example_tensor.dtype == example_tensor.dtype - assert example_tensor.shape == example_tensor.shape - - assert isinstance(example_tensor.numpy(), numpy.ndarray) - - # Check that the Tensor instance has the correct buffer, dtype, and shape - assert example_tensor.buffer == example_tensor.buffer - assert example_tensor.dtype == example_tensor.dtype - assert example_tensor.shape == example_tensor.shape - - assert isinstance(example_tensor.tensor(), np.ndarray) - - # Check that the Tensor instance has the correct buffer, dtype, and shape - assert example_tensor.buffer == example_tensor.buffer - assert example_tensor.dtype == example_tensor.dtype - assert example_tensor.shape == example_tensor.shape - - -def test_serialize_torch(example_tensor_torch, force_legacy_torch_compatible_api): - # Check that the serialized tensor is an instance of Tensor - assert isinstance(example_tensor_torch, Tensor) - - # Check that the Tensor instance has the correct buffer, dtype, and shape - assert example_tensor_torch.buffer == example_tensor_torch.buffer - assert example_tensor_torch.dtype == example_tensor_torch.dtype - assert example_tensor_torch.shape == example_tensor_torch.shape - - assert isinstance(example_tensor_torch.tolist(), list) - - # Check that the Tensor instance has the correct buffer, dtype, and shape - assert example_tensor_torch.buffer == example_tensor_torch.buffer - assert example_tensor_torch.dtype == example_tensor_torch.dtype - assert example_tensor_torch.shape == example_tensor_torch.shape - - assert isinstance(example_tensor_torch.numpy(), numpy.ndarray) - - # Check that the Tensor instance has the correct buffer, dtype, and shape - assert example_tensor_torch.buffer == example_tensor_torch.buffer - assert example_tensor_torch.dtype == example_tensor_torch.dtype - assert example_tensor_torch.shape == example_tensor_torch.shape - - assert isinstance(example_tensor_torch.tensor(), torch.Tensor) - - # Check that the Tensor instance has the correct buffer, dtype, and shape - assert example_tensor_torch.buffer == example_tensor_torch.buffer - assert example_tensor_torch.dtype == example_tensor_torch.dtype - assert example_tensor_torch.shape == example_tensor_torch.shape +from bittensor.core.tensor import Tensor def test_buffer_field(): @@ -174,72 +74,34 @@ def test_shape_field_torch(force_legacy_torch_compatible_api): assert tensor.shape == [3, 3] -def test_serialize_all_types(): - tensor_class(np.array([1], dtype=np.float16)) - tensor_class(np.array([1], dtype=np.float32)) - tensor_class(np.array([1], dtype=np.float64)) - tensor_class(np.array([1], dtype=np.uint8)) - tensor_class(np.array([1], dtype=np.int32)) - tensor_class(np.array([1], dtype=np.int64)) - tensor_class(np.array([1], dtype=bool)) - - -def test_serialize_all_types_torch(force_legacy_torch_compatible_api): - tensor_class(torch.tensor([1], dtype=torch.float16)) - tensor_class(torch.tensor([1], dtype=torch.float32)) - tensor_class(torch.tensor([1], dtype=torch.float64)) - tensor_class(torch.tensor([1], dtype=torch.uint8)) - tensor_class(torch.tensor([1], dtype=torch.int32)) - tensor_class(torch.tensor([1], dtype=torch.int64)) - tensor_class(torch.tensor([1], dtype=torch.bool)) - - -def test_serialize_all_types_equality(): - rng = np.random.default_rng() - - tensor = rng.standard_normal((100,), dtype=np.float32) - assert np.all(tensor_class(tensor).tensor() == tensor) +def test_serialize(): + """Test the deserialization of the Tensor instance.""" + # Preps + tensor_ = np.ndarray([3, 3], dtype=np.float32) - tensor = rng.standard_normal((100,), dtype=np.float64) - assert np.all(tensor_class(tensor).tensor() == tensor) + # Call + tensor = Tensor.serialize(tensor_) - tensor = np.random.randint(255, 256, (1000,), dtype=np.uint8) - assert np.all(tensor_class(tensor).tensor() == tensor) - - tensor = np.random.randint(2_147_483_646, 2_147_483_647, (1000,), dtype=np.int32) - assert np.all(tensor_class(tensor).tensor() == tensor) - - tensor = np.random.randint( - 9_223_372_036_854_775_806, 9_223_372_036_854_775_807, (1000,), dtype=np.int64 + # Asserts + assert ( + tensor.buffer + == "hcQCbmTDxAR0eXBlozxmNMQEa2luZMQAxAVzaGFwZZIDA8QEZGF0YcQkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ) - assert np.all(tensor_class(tensor).tensor() == tensor) - - tensor = rng.standard_normal((100,), dtype=np.float32) < 0.5 - assert np.all(tensor_class(tensor).tensor() == tensor) - - -def test_serialize_all_types_equality_torch(force_legacy_torch_compatible_api): - torchtensor = torch.randn([100], dtype=torch.float16) - assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) - - torchtensor = torch.randn([100], dtype=torch.float32) - assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) - - torchtensor = torch.randn([100], dtype=torch.float64) - assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) + assert tensor.dtype == "float32" + assert tensor.shape == [3, 3] - torchtensor = torch.randint(255, 256, (1000,), dtype=torch.uint8) - assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) - torchtensor = torch.randint( - 2_147_483_646, 2_147_483_647, (1000,), dtype=torch.int32 +def test_deserialize(): + """Test the deserialization of the Tensor instance.""" + # Preps + tensor = Tensor( + buffer="hcQCbmTDxAR0eXBlozxmNMQEa2luZMQAxAVzaGFwZZIDA8QEZGF0YcQkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + dtype="float32", + shape=[3, 3], ) - assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) - torchtensor = torch.randint( - 9_223_372_036_854_775_806, 9_223_372_036_854_775_807, (1000,), dtype=torch.int64 - ) - assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) + # Call + result = tensor.deserialize() - torchtensor = torch.randn([100], dtype=torch.float32) < 0.5 - assert torch.all(tensor_class(torchtensor).tensor() == torchtensor) + # Asserts + assert np.array_equal(result, np.zeros((3, 3))) From 9d06d352f37a16ecc9101c6a2c793589e8a5a190 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 21:04:31 -0700 Subject: [PATCH 234/260] fix --- bittensor/core/tensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/tensor.py b/bittensor/core/tensor.py index 9f06808a6..f1a72d886 100644 --- a/bittensor/core/tensor.py +++ b/bittensor/core/tensor.py @@ -151,7 +151,7 @@ def tensor(self) -> Union[np.ndarray, "torch.Tensor"]: def tolist(self) -> List[object]: return self.deserialize().tolist() - def numpy(self) -> "numpy.ndarray": + def numpy(self) -> "np.ndarray": return ( self.deserialize().detach().numpy() if use_torch() else self.deserialize() ) From 4e09490f5476badb91761ef5f9f44a0bef533c92 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 16 Sep 2024 21:12:33 -0700 Subject: [PATCH 235/260] temp commented line --- tests/unit_tests/test_tensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/test_tensor.py b/tests/unit_tests/test_tensor.py index 9cebaa9ed..18efea380 100644 --- a/tests/unit_tests/test_tensor.py +++ b/tests/unit_tests/test_tensor.py @@ -83,10 +83,11 @@ def test_serialize(): tensor = Tensor.serialize(tensor_) # Asserts - assert ( - tensor.buffer - == "hcQCbmTDxAR0eXBlozxmNMQEa2luZMQAxAVzaGFwZZIDA8QEZGF0YcQkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - ) + # TODO: figure it how to check shape + # assert ( + # tensor.buffer + # == "hcQCbmTDxAR0eXBlozxmNMQEa2luZMQAxAVzaGFwZZIDA8QEZGF0YcQkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + # ) assert tensor.dtype == "float32" assert tensor.shape == [3, 3] From 826ac12437130003fb5ece03111027aec400c2fc Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Sep 2024 08:29:49 -0700 Subject: [PATCH 236/260] fix review comments --- bittensor/core/metagraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index ccc50eb7e..b3830ac55 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -880,7 +880,7 @@ def __init__( ): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. - This class require installed Torch. + This class requires Torch to be installed. This method is the entry point for creating a metagraph object, which is a central component in representing the state of the Bittensor network. Args: From 10dee4a8ce08ec9a8e128086d27d96696b5c3d47 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Sep 2024 09:07:22 -0700 Subject: [PATCH 237/260] fix review comments --- tests/unit_tests/test_tensor.py | 187 +++++++++++++++++++++++++++----- 1 file changed, 162 insertions(+), 25 deletions(-) diff --git a/tests/unit_tests/test_tensor.py b/tests/unit_tests/test_tensor.py index 18efea380..8bf7bf06a 100644 --- a/tests/unit_tests/test_tensor.py +++ b/tests/unit_tests/test_tensor.py @@ -15,11 +15,111 @@ # 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 numpy import numpy as np +import pytest +import torch from bittensor.core.tensor import Tensor +# This is a fixture that creates an example tensor for testing +@pytest.fixture +def example_tensor(): + # Create a tensor from a list using PyTorch + data = np.array([1, 2, 3, 4]) + + # Serialize the tensor into a Tensor instance and return it + return Tensor.serialize(data) + + +@pytest.fixture +def example_tensor_torch(force_legacy_torch_compatible_api): + # Create a tensor from a list using PyTorch + data = torch.tensor([1, 2, 3, 4]) + + # Serialize the tensor into a Tensor instance and return it + return Tensor.serialize(data) + + +def test_deserialize(example_tensor): + # Deserialize the tensor from the Tensor instance + tensor = example_tensor.deserialize() + + # Check that the result is a np.array with the correct values + assert isinstance(tensor, np.ndarray) + assert tensor.tolist() == [1, 2, 3, 4] + + +def test_deserialize_torch(example_tensor_torch, force_legacy_torch_compatible_api): + tensor = example_tensor_torch.deserialize() + # Check that the result is a PyTorch tensor with the correct values + assert isinstance(tensor, torch.Tensor) + assert tensor.tolist() == [1, 2, 3, 4] + + +def test_serialize(example_tensor): + # Check that the serialized tensor is an instance of Tensor + assert isinstance(example_tensor, Tensor) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor.buffer == example_tensor.buffer + assert example_tensor.dtype == example_tensor.dtype + assert example_tensor.shape == example_tensor.shape + + assert isinstance(example_tensor.tolist(), list) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor.buffer == example_tensor.buffer + assert example_tensor.dtype == example_tensor.dtype + assert example_tensor.shape == example_tensor.shape + + assert isinstance(example_tensor.numpy(), numpy.ndarray) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor.buffer == example_tensor.buffer + assert example_tensor.dtype == example_tensor.dtype + assert example_tensor.shape == example_tensor.shape + + assert isinstance(example_tensor.tensor(), np.ndarray) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor.buffer == example_tensor.buffer + assert example_tensor.dtype == example_tensor.dtype + assert example_tensor.shape == example_tensor.shape + + +def test_serialize_torch(example_tensor_torch, force_legacy_torch_compatible_api): + # Check that the serialized tensor is an instance of Tensor + assert isinstance(example_tensor_torch, Tensor) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor_torch.buffer == example_tensor_torch.buffer + assert example_tensor_torch.dtype == example_tensor_torch.dtype + assert example_tensor_torch.shape == example_tensor_torch.shape + + assert isinstance(example_tensor_torch.tolist(), list) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor_torch.buffer == example_tensor_torch.buffer + assert example_tensor_torch.dtype == example_tensor_torch.dtype + assert example_tensor_torch.shape == example_tensor_torch.shape + + assert isinstance(example_tensor_torch.numpy(), numpy.ndarray) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor_torch.buffer == example_tensor_torch.buffer + assert example_tensor_torch.dtype == example_tensor_torch.dtype + assert example_tensor_torch.shape == example_tensor_torch.shape + + assert isinstance(example_tensor_torch.tensor(), torch.Tensor) + + # Check that the Tensor instance has the correct buffer, dtype, and shape + assert example_tensor_torch.buffer == example_tensor_torch.buffer + assert example_tensor_torch.dtype == example_tensor_torch.dtype + assert example_tensor_torch.shape == example_tensor_torch.shape + + def test_buffer_field(): # Create a Tensor instance with a specified buffer, dtype, and shape tensor = Tensor( @@ -74,35 +174,72 @@ def test_shape_field_torch(force_legacy_torch_compatible_api): assert tensor.shape == [3, 3] -def test_serialize(): - """Test the deserialization of the Tensor instance.""" - # Preps - tensor_ = np.ndarray([3, 3], dtype=np.float32) +def test_serialize_all_types(): + Tensor.serialize(np.array([1], dtype=np.float16)) + Tensor.serialize(np.array([1], dtype=np.float32)) + Tensor.serialize(np.array([1], dtype=np.float64)) + Tensor.serialize(np.array([1], dtype=np.uint8)) + Tensor.serialize(np.array([1], dtype=np.int32)) + Tensor.serialize(np.array([1], dtype=np.int64)) + Tensor.serialize(np.array([1], dtype=bool)) - # Call - tensor = Tensor.serialize(tensor_) - # Asserts - # TODO: figure it how to check shape - # assert ( - # tensor.buffer - # == "hcQCbmTDxAR0eXBlozxmNMQEa2luZMQAxAVzaGFwZZIDA8QEZGF0YcQkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - # ) - assert tensor.dtype == "float32" - assert tensor.shape == [3, 3] +def test_serialize_all_types_torch(force_legacy_torch_compatible_api): + Tensor.serialize(torch.tensor([1], dtype=torch.float16)) + Tensor.serialize(torch.tensor([1], dtype=torch.float32)) + Tensor.serialize(torch.tensor([1], dtype=torch.float64)) + Tensor.serialize(torch.tensor([1], dtype=torch.uint8)) + Tensor.serialize(torch.tensor([1], dtype=torch.int32)) + Tensor.serialize(torch.tensor([1], dtype=torch.int64)) + Tensor.serialize(torch.tensor([1], dtype=torch.bool)) -def test_deserialize(): - """Test the deserialization of the Tensor instance.""" - # Preps - tensor = Tensor( - buffer="hcQCbmTDxAR0eXBlozxmNMQEa2luZMQAxAVzaGFwZZIDA8QEZGF0YcQkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - dtype="float32", - shape=[3, 3], +def test_serialize_all_types_equality(): + rng = np.random.default_rng() + + tensor = rng.standard_normal((100,), dtype=np.float32) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + tensor = rng.standard_normal((100,), dtype=np.float64) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + tensor = np.random.randint(255, 256, (1000,), dtype=np.uint8) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + tensor = np.random.randint(2_147_483_646, 2_147_483_647, (1000,), dtype=np.int32) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + tensor = np.random.randint( + 9_223_372_036_854_775_806, 9_223_372_036_854_775_807, (1000,), dtype=np.int64 ) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + tensor = rng.standard_normal((100,), dtype=np.float32) < 0.5 + assert np.all(Tensor.serialize(tensor).tensor() == tensor) + + +def test_serialize_all_types_equality_torch(force_legacy_torch_compatible_api): + torchtensor = torch.randn([100], dtype=torch.float16) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) + + torchtensor = torch.randn([100], dtype=torch.float32) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) - # Call - result = tensor.deserialize() + torchtensor = torch.randn([100], dtype=torch.float64) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) + + torchtensor = torch.randint(255, 256, (1000,), dtype=torch.uint8) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) + + torchtensor = torch.randint( + 2_147_483_646, 2_147_483_647, (1000,), dtype=torch.int32 + ) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) + + torchtensor = torch.randint( + 9_223_372_036_854_775_806, 9_223_372_036_854_775_807, (1000,), dtype=torch.int64 + ) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) - # Asserts - assert np.array_equal(result, np.zeros((3, 3))) + torchtensor = torch.randn([100], dtype=torch.float32) < 0.5 + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) From 377e913b9f278cdf5337274ff5207901c623c4bb Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Sep 2024 09:42:08 -0700 Subject: [PATCH 238/260] fix --- bittensor/subtensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index 11b325d50..416d264b4 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -20,6 +20,7 @@ The ``bittensor.subtensor`` module in Bittensor serves as a crucial interface for interacting with the Bittensor blockchain, facilitating a range of operations essential for the decentralized machine learning network. """ +from __future__ import annotations import argparse import copy From 55a2a2519d90241059d45883eda0684cf0110a72 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Sep 2024 09:57:39 -0700 Subject: [PATCH 239/260] ruff --- bittensor/subtensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index 416d264b4..c477a0b77 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -20,6 +20,7 @@ The ``bittensor.subtensor`` module in Bittensor serves as a crucial interface for interacting with the Bittensor blockchain, facilitating a range of operations essential for the decentralized machine learning network. """ + from __future__ import annotations import argparse From 23c0489b43084b503a4d733d1f58decee7cddd7d Mon Sep 17 00:00:00 2001 From: Watchmaker Date: Tue, 17 Sep 2024 14:32:25 -0700 Subject: [PATCH 240/260] Adding TOC and working in review comments. --- README.md | 49 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e0b75f6ee..368f7c9a3 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,31 @@ +- [Overview of Bittensor](#overview-of-bittensor) +- [The Bittensor SDK](#the-bittensor-sdk) +- [Is Bittensor a blockchain or an AI platform?](#is-bittensor-a-blockchain-or-an-ai-platform) +- [Subnets](#subnets) +- [Subnet validators and subnet miners](#subnet-validators-and-subnet-miners) +- [Yuma Consensus](#yuma-consensus) +- [Release Notes](#release-notes) +- [Install Bittensor SDK](#install-bittensor-sdk) +- [Upgrade](#upgrade) +- [Install on macOS and Linux](#install-on-macos-and-linux) + - [Install using a Bash command](#install-using-a-bash-command) + - [Install using `pip3 install`](#install-using-pip3-install) + - [Install from source](#install-from-source) +- [Install on Windows](#install-on-windows) +- [Verify the installation](#verify-the-installation) + - [Verify using `btsdk` version](#verify-using-btsdk-version) + - [Verify using Python interpreter](#verify-using-python-interpreter) + - [Verify by listing axon information](#verify-by-listing-axon-information) +- [Release Guidelines](#release-guidelines) +- [Contributions](#contributions) +- [License](#license) +- [Acknowledgments](#acknowledgments) + +--- + ## Overview of Bittensor Welcome! Bittensor is an open source platform on which you can produce competitive digital commodities. These digital commodities can be machine intelligence, storage space, compute power, protein folding, financial markets prediction, and many more. You are rewarded in **TAO** when you produce best digital commodities. @@ -89,7 +114,9 @@ If you are using Ubuntu-Linux, the script will prompt for `sudo` access to insta ### Install using `pip3 install` ```bash -pip3 install bittensor +python3 -m venv myvenv +source myvenv/bin/activate +pip install bittensor ``` ### Install from source @@ -134,23 +161,15 @@ While wallet transactions like delegating, transfer, registering, staking can be ## Verify the installation -You can verify your installation in either of the two ways as shown below: +You can verify your installation in either of the below ways: -### Verify using the `btcli` command - -Using the [Bittensor Command Line Interface](https://docs.bittensor.com/btcli). +### Verify using `btsdk` version ```bash -btcli --help +python3 -m bittensor ``` -which will give you the below output: -```text -usage: btcli -bittensor cli -... -``` -You will see the version number you installed in place of ``. +The above command will show you the version of the `btsdk` you just installed. ### Verify using Python interpreter @@ -198,11 +217,11 @@ The Python interpreter output will look like below. Instructions for the release manager: [RELEASE_GUIDELINES.md](./contrib/RELEASE_GUIDELINES.md) document. ## Contributions -Ready to contributre? Read the [contributing guide](./contrib/CONTRIBUTING.md) before making a pull request. +Ready to contribute? Read the [contributing guide](./contrib/CONTRIBUTING.md) before making a pull request. ## License The MIT License (MIT) -Copyright © 2021 Yuma Rao +Copyright © 2024 The 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: From 98b261059fe855557dfad8ad835bcf84389198df Mon Sep 17 00:00:00 2001 From: Watchmaker Date: Tue, 17 Sep 2024 16:17:33 -0700 Subject: [PATCH 241/260] Adding installation options --- README.md | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 368f7c9a3..f3de0a6af 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,6 @@ - [Install using a Bash command](#install-using-a-bash-command) - [Install using `pip3 install`](#install-using-pip3-install) - [Install from source](#install-from-source) -- [Install on Windows](#install-on-windows) -- [Verify the installation](#verify-the-installation) - - [Verify using `btsdk` version](#verify-using-btsdk-version) - [Verify using Python interpreter](#verify-using-python-interpreter) - [Verify by listing axon information](#verify-by-listing-axon-information) - [Release Guidelines](#release-guidelines) @@ -114,8 +111,8 @@ If you are using Ubuntu-Linux, the script will prompt for `sudo` access to insta ### Install using `pip3 install` ```bash -python3 -m venv myvenv -source myvenv/bin/activate +python3 -m venv bt_venv +source bt_venv/bin/activate pip install bittensor ``` @@ -132,19 +129,35 @@ pip install bittensor ```bash git clone https://github.com/opentensor/bittensor.git ``` -3. Change to the Bittensor directory: -```bash -cd bittensor -``` +3. Install -4. Install +You can install using any of the below options: -Run the below command to install Bittensor SDK in the above virtual environment. +- **Install only SDK**: Run the below command to install Bittensor SDK in the above virtual environment. + + ```python + pip install bittensor + ``` + +- **Install SDK with `btcli`**: Install Bittensor SDK with `btcli`. The `btcli` will be installed as an independent tool and its Python package is `bittensor-cli`. + + ```python + pip install bittensor[btcli] + ``` + +- **Install SDK with `torch`**: Install Bittensor SDK with [`torch`](https://pytorch.org/docs/stable/torch.html). + + ```python + pip install bittensor[torch] + ``` + +- **Install SDK with `cubit`**: Install Bittensor SDK with [`cubit`](https://pytorch.org/docs/stable/torch.html). + + ```python + pip install bittensor[cubit] + ``` -```python -pip install . -``` --- From 257b9a15b52c0e72d3870120c4abfd032e5e6d6c Mon Sep 17 00:00:00 2001 From: Watchmaker Date: Tue, 17 Sep 2024 19:43:43 -0700 Subject: [PATCH 242/260] Formatting adjustments for html display --- bittensor/core/metagraph.py | 51 ++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index b3830ac55..df070f3f1 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -59,11 +59,32 @@ "validator_permit", "uids", ] +"""List of keys for the metagraph state dictionary used in NDArray serialization. + +This list defines the set of keys expected in the metagraph's state dictionary when serializing and deserializing NumPy ndarray objects. Each key corresponds to a specific attribute or metric associated with the nodes in the metagraph. + +- **version** (`str`): The version identifier of the metagraph state. +- **n** (`int`): The total number of nodes in the metagraph. +- **block** (`int`): The current block number in the blockchain or ledger. +- **stake** (`ndarray`): An array representing the stake of each node. +- **total_stake** (`float`): The sum of all individual stakes in the metagraph. +- **ranks** (`ndarray`): An array of rank scores assigned to each node. +- **trust** (`ndarray`): An array of trust scores for the nodes. +- **consensus** (`ndarray`): An array indicating consensus levels among nodes. +- **validator_trust** (`ndarray`): Trust scores specific to validator nodes. +- **incentive** (`ndarray`): Incentive values allocated to nodes. +- **emission** (`float`): The rate of emission for new tokens or units. +- **dividends** (`ndarray`): Dividend amounts distributed to nodes. +- **active** (`ndarray`): Boolean array indicating active (`True`) or inactive (`False`) nodes. +- **last_update** (`int`): Timestamp of the last state update. +- **validator_permit** (`ndarray`): Boolean array indicating nodes permitted to validate. +- **uids** (`ndarray`): Unique identifiers for each node in the metagraph. +""" def get_save_dir(network: str, netuid: int) -> str: """ - Return directory path from ``network`` and ``netuid``. + Returns a directory path given ``network`` and ``netuid`` inputs. Args: network (str): Network name. @@ -85,7 +106,7 @@ def get_save_dir(network: str, netuid: int) -> str: def latest_block_path(dir_path: str) -> str: """ - Get the latest block path from the directory. + Get the latest block path from the provided directory path. Args: dir_path (str): Directory path. @@ -393,14 +414,18 @@ def __init__( Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. This method is the entry point for creating a metagraph object, which is a central component in representing the state of the Bittensor network. + Args: netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within potentially multiple network configurations. network (str): The name of the network, which can indicate specific configurations or versions of the Bittensor network. lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain less detailed information but can be quicker to initialize and sync. sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + Example: Initializing a metagraph object for the Bittensor network with a specific network UID:: + metagraph = metagraph(netuid=123, network="finney", lite=True, sync=True) + """ def __str__(self) -> str: @@ -414,7 +439,7 @@ def __str__(self) -> str: Example: When printing the metagraph object or using it in a string context, this method is automatically invoked:: - print(metagraph) # Output: "metagraph(netuid:1, n:100, block:500, network:finney)" + print(metagraph) # Output: "metagraph(netuid:1, n:100, block:500, network:finney)" """ return f"metagraph(netuid:{self.netuid}, n:{self.n.item()}, block:{self.block.item()}, network:{self.network})" @@ -510,6 +535,7 @@ def sync( metagraph.sync(subtensor=subtensor) Sync with a specific block number for detailed analysis:: + from bittensor.core.subtensor import Subtensor subtensor = Subtensor() @@ -519,6 +545,7 @@ def sync( If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for subtensor. Light nodes are configured only to store the previous 300 blocks if connecting to finney or test networks. For example:: + from bittensor.core.subtensor import Subtensor subtensor = Subtensor(network='archive') @@ -739,7 +766,7 @@ def _process_root_weights( Internal Usage: Used internally to process and set root weights for the metagraph:: - self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) + self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) """ data_array = [] n_subnets = subtensor.get_total_subnets() or 0 @@ -872,6 +899,9 @@ def load_from_path(self, dir_path: str) -> "Metagraph": BaseClass: Union["torch.nn.Module", object] = torch.nn.Module if use_torch() else object +""" +Base class that extends :class:`torch.nn.Module` if PyTorch is used; otherwise, it defaults to object. +""" class TorchMetaGraph(MetagraphMixin, BaseClass): @@ -971,6 +1001,7 @@ def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): Internal Usage: Used internally during the sync process to update the metagraph's attributes:: + from bittensor.core.subtensor import Subtensor subtensor = Subtensor() @@ -1034,14 +1065,17 @@ def load_from_path(self, dir_path: str) -> "Metagraph": Returns: metagraph (bittensor.core.metagraph.Metagraph): The current metagraph instance with the loaded state. - Example: + Example:: + from bittensor.core.metagraph import Metagraph netuid = 1 metagraph = Metagraph(netuid=netuid) metagraph.load_from_path("/path/to/dir") + """ + graph_file = latest_block_path(dir_path) state_dict = torch.load(graph_file) self.n = torch.nn.Parameter(state_dict["n"], requires_grad=False) @@ -1204,7 +1238,7 @@ def load_from_path(self, dir_path: str) -> "Metagraph": dir_path (str): The directory path where the metagraph's state file is located. Returns: - metagraph (bittensor.core.metagraph.Metagraph): An instance of the Metagraph with the state loaded from the file. + metagraph (:func:`bittensor.core.metagraph.Metagraph`): An instance of the Metagraph with the state loaded from the file. Raises: pickle.UnpicklingError: If there is an error unpickling the state file. @@ -1259,3 +1293,8 @@ def load_from_path(self, dir_path: str) -> "Metagraph": Metagraph = TorchMetaGraph if use_torch() else NonTorchMetagraph +"""Metagraph class that uses :class:`TorchMetaGraph` if PyTorch is available; otherwise, it falls back to :class:`NonTorchMetagraph`. + +- **With PyTorch**: When `use_torch()` returns `True`, `Metagraph` is set to :class:`TorchMetaGraph`, which utilizes PyTorch functionalities. +- **Without PyTorch**: When `use_torch()` returns `False`, `Metagraph` is set to :class:`NonTorchMetagraph`, which does not rely on PyTorch. +""" \ No newline at end of file From 45c0ef9c8eb35cd157dbdb5c13fa6937d1070e5a Mon Sep 17 00:00:00 2001 From: Watchmaker Date: Tue, 17 Sep 2024 19:59:08 -0700 Subject: [PATCH 243/260] Ruff formatted --- bittensor/core/metagraph.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index df070f3f1..15a86ad70 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -414,13 +414,13 @@ def __init__( Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. This method is the entry point for creating a metagraph object, which is a central component in representing the state of the Bittensor network. - + Args: netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within potentially multiple network configurations. network (str): The name of the network, which can indicate specific configurations or versions of the Bittensor network. lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain less detailed information but can be quicker to initialize and sync. sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. - + Example: Initializing a metagraph object for the Bittensor network with a specific network UID:: @@ -1073,7 +1073,7 @@ def load_from_path(self, dir_path: str) -> "Metagraph": metagraph = Metagraph(netuid=netuid) metagraph.load_from_path("/path/to/dir") - + """ graph_file = latest_block_path(dir_path) @@ -1297,4 +1297,4 @@ def load_from_path(self, dir_path: str) -> "Metagraph": - **With PyTorch**: When `use_torch()` returns `True`, `Metagraph` is set to :class:`TorchMetaGraph`, which utilizes PyTorch functionalities. - **Without PyTorch**: When `use_torch()` returns `False`, `Metagraph` is set to :class:`NonTorchMetagraph`, which does not rely on PyTorch. -""" \ No newline at end of file +""" From e4afdeca051bc2347522a09e866d31a4e928f920 Mon Sep 17 00:00:00 2001 From: Watchmaker Date: Tue, 17 Sep 2024 21:23:40 -0700 Subject: [PATCH 244/260] Example reformatted --- bittensor/core/metagraph.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 15a86ad70..2e17915e8 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -167,16 +167,16 @@ class MetagraphMixin(ABC): The metagraph plays a pivotal role in Bittensor's decentralized AI operations, influencing everything from data propagation to reward distribution. It embodies the principles of decentralized governance and collaborative intelligence, ensuring that the network remains adaptive, secure, and efficient. - Example Usage: - Initializing the metagraph to represent the current state of the Bittensor network:: + Example: + Initializing the metagraph to represent the current state of the Bittensor network:: from bittensor.core.metagraph import Metagraph metagraph = Metagraph(netuid=config.netuid, network=subtensor.network, sync=False) - + Synchronizing the metagraph with the network to reflect the latest state and neuron data:: metagraph.sync(subtensor=subtensor) - + Accessing metagraph properties to inform network interactions and decisions:: total_stake = metagraph.S @@ -184,7 +184,6 @@ class MetagraphMixin(ABC): neuron_incentives = metagraph.I axons = metagraph.axons neurons = metagraph.neurons - ... Maintaining a local copy of hotkeys for querying and interacting with network entities:: From a052ef5fedacd770fb997a72c1697d5294bef05c Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Sep 2024 21:48:33 -0700 Subject: [PATCH 245/260] update metagraph --- bittensor/core/metagraph.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor/core/metagraph.py b/bittensor/core/metagraph.py index 2e17915e8..208eaa6b9 100644 --- a/bittensor/core/metagraph.py +++ b/bittensor/core/metagraph.py @@ -168,15 +168,15 @@ class MetagraphMixin(ABC): and collaborative intelligence, ensuring that the network remains adaptive, secure, and efficient. Example: - Initializing the metagraph to represent the current state of the Bittensor network:: + Initializing the metagraph to represent the current state of the Bittensor network:: from bittensor.core.metagraph import Metagraph metagraph = Metagraph(netuid=config.netuid, network=subtensor.network, sync=False) - + Synchronizing the metagraph with the network to reflect the latest state and neuron data:: metagraph.sync(subtensor=subtensor) - + Accessing metagraph properties to inform network interactions and decisions:: total_stake = metagraph.S From 44f73589a5a7f4ca7f198b470636f3596fd311a1 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Sep 2024 21:58:08 -0700 Subject: [PATCH 246/260] update dendrite.py --- bittensor/core/dendrite.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/bittensor/core/dendrite.py b/bittensor/core/dendrite.py index bfdfd4d4a..0b9bc5381 100644 --- a/bittensor/core/dendrite.py +++ b/bittensor/core/dendrite.py @@ -143,17 +143,17 @@ async def session(self) -> aiohttp.ClientSession: Example usage:: - import bittensor # Import bittensor - wallet = bittensor.Wallet( ... ) # Initialize a wallet + import bittensor # Import bittensor + wallet = bittensor.Wallet( ... ) # Initialize a wallet dendrite = bittensor.Dendrite(wallet=wallet) # Initialize a dendrite instance with the wallet - async with (await dendrite.session).post( # Use the session to make an HTTP POST request - url, # URL to send the request to - headers={...}, # Headers dict to be sent with the request - json={...}, # JSON body data to be sent with the request - timeout=10, # Timeout duration in seconds + async with (await dendrite.session).post( # Use the session to make an HTTP POST request + url, # URL to send the request to + headers={...}, # Headers dict to be sent with the request + json={...}, # JSON body data to be sent with the request + timeout=10, # Timeout duration in seconds ) as response: - json_response = await response.json() # Extract the JSON response from the server + json_response = await response.json() # Extract the JSON response from the server """ if self._session is None: @@ -189,16 +189,17 @@ async def aclose_session(self): releases resources associated with the session, such as open connections and internal buffers, which is essential for resource management in asynchronous applications. - Usage: - When finished with dendrite in an asynchronous context - await :func:`dendrite_instance.aclose_session()`. - - Example:: + Example: + Usage:: + When finished with dendrite in an asynchronous context + await :func:`dendrite_instance.aclose_session()`. - async with dendrite_instance: - # Operations using dendrite - pass - # The session will be closed automatically after the above block + Example: + Usage:: + async with dendrite_instance: + # Operations using dendrite + pass + # The session will be closed automatically after the above block """ if self._session: await self._session.close() From 6dc4814a7c87be944f5600da0b915fde618e42c3 Mon Sep 17 00:00:00 2001 From: Roman <167799377+roman-opentensor@users.noreply.github.com> Date: Wed, 18 Sep 2024 08:48:44 -0700 Subject: [PATCH 247/260] Update README.md Co-authored-by: garrett-opentensor <156717492+garrett-opentensor@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3de0a6af..cc5878dd7 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ This Bittensor SDK contains ready-to-use Python packages for interacting with th ## Is Bittensor a blockchain or an AI platform? -In Bittensor there is one blockchain, and many platforms that are connected to this one blockchain. We call these platforms as **subnets**, and this one blockchain as **subtensor**. So, a subnet can be AI-related or it can be something else. The Bittensor network has a number of distinct subnets. All these subnets interact with subtensor blockchain. If you are thinking, "So, subnets are not part of the blockchain but only interact with it?" then the answer is "yes, exactly." +In Bittensor there is one blockchain, and many platforms that are connected to this one blockchain. We call these platforms as **subnets**, and this one blockchain **subtensor**. So, a subnet can be AI-related or it can be something else. The Bittensor network has a number of distinct subnets. All these subnets interact with subtensor blockchain. If you are thinking, "So, subnets are not part of the blockchain but only interact with it?" then the answer is "yes, exactly." ## Subnets From 83b14269ee4e714c7d70c12273b9ec9927814b34 Mon Sep 17 00:00:00 2001 From: Roman <167799377+roman-opentensor@users.noreply.github.com> Date: Wed, 18 Sep 2024 08:54:34 -0700 Subject: [PATCH 248/260] Update README.md Co-authored-by: garrett-opentensor <156717492+garrett-opentensor@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc5878dd7..7a72822d0 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Subnets, which exist outside the blockchain and are connected to it, are off-cha Scores are assigned to the top-performing subnet miners and subnet validators. The on-chain Yuma Consensus determines the TAO rewards for these top performers. The Bittensor blockchain, the subtensor, runs on decentralized validation nodes, just like any blockchain. **This SDK repo is for Bittensor platform only** -This Bittensor SDK codebase is for the Bittensor platform only. For subnets and applications, refer to subnet-specific websites, which are maintained by subnet owners. +This Bittensor SDK codebase is for the Bittensor platform only, designed to help developers create subnets and build tools on Bittensor. For subnets and applications, refer to subnet-specific websites, which are maintained by subnet owners. ## Release Notes From 305b149828571dba075e1b954c8367711ef00e22 Mon Sep 17 00:00:00 2001 From: Roman <167799377+roman-opentensor@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:02:56 -0700 Subject: [PATCH 249/260] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7a72822d0..08f060aaf 100644 --- a/README.md +++ b/README.md @@ -213,8 +213,8 @@ You will see the version number you installed in place of ``. You can also verify the Bittensor SDK installation by listing the axon information for the neurons. Enter the following lines in the Python interpreter. ```python -import bittensor as bt -metagraph = bt.metagraph(1) +import bittensor +metagraph = bittensor.Metagraph(1) metagraph.axons[:10] ``` The Python interpreter output will look like below. From bacde39622eb829a54ac201fe08925446d5bba39 Mon Sep 17 00:00:00 2001 From: Roman <167799377+roman-opentensor@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:21:14 -0700 Subject: [PATCH 250/260] Update axon.py --- bittensor/core/axon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index 854080eeb..cd32ba421 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -66,6 +66,7 @@ if typing.TYPE_CHECKING: from bittensor.core.subtensor import Subtensor +# Latest version with old style nonce structure (this in not a current SDK version) V_7_2_0 = 7002000 From 2047fac58e29ca241c9eade6cf7caa3e4fe14170 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 25 Sep 2024 09:48:53 -0700 Subject: [PATCH 251/260] fix tests/unit_tests/test_axon.py --- bittensor/core/axon.py | 4 ++-- tests/unit_tests/test_axon.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor/core/axon.py b/bittensor/core/axon.py index cd32ba421..63b79d49c 100644 --- a/bittensor/core/axon.py +++ b/bittensor/core/axon.py @@ -29,7 +29,7 @@ import uuid import warnings from inspect import signature, Signature, Parameter -from typing import Any, Awaitable, Callable, Optional +from typing import Any, Awaitable, Callable, Optional, Tuple import uvicorn from bittensor_wallet import Wallet @@ -556,7 +556,7 @@ async def endpoint(*args, **kwargs): ] if blacklist_fn: blacklist_sig = Signature( - expected_params, return_annotation=tuple[bool, str] + expected_params, return_annotation=Tuple[bool, str] ) assert ( signature(blacklist_fn) == blacklist_sig diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index 5df465e37..689d21737 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -19,7 +19,7 @@ import re import time from dataclasses import dataclass -from typing import Any, Optional +from typing import Any, Optional, Tuple from unittest import IsolatedAsyncioTestCase from unittest.mock import AsyncMock, MagicMock, patch @@ -56,7 +56,7 @@ class TestSynapse(Synapse): def forward_fn(synapse: TestSynapse) -> Any: pass - def blacklist_fn(synapse: TestSynapse) -> tuple[bool, str]: + def blacklist_fn(synapse: TestSynapse) -> Tuple[bool, str]: return True, "" def priority_fn(synapse: TestSynapse) -> float: From 454c80d3a2d90c538077a45afde01e2807d6b5c7 Mon Sep 17 00:00:00 2001 From: Watchmaker Date: Wed, 25 Sep 2024 10:39:18 -0700 Subject: [PATCH 252/260] Update README.md --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 08f060aaf..541b8ec6b 100644 --- a/README.md +++ b/README.md @@ -152,13 +152,11 @@ You can install using any of the below options: pip install bittensor[torch] ``` -- **Install SDK with `cubit`**: Install Bittensor SDK with [`cubit`](https://pytorch.org/docs/stable/torch.html). - - ```python - pip install bittensor[cubit] - ``` - +- **Install SDK with `cubit`**: Install Bittensor SDK with [`cubit`](https://github.com/opentensor/cubit). + 1. Install `cubit` first. See the [Install](https://github.com/opentensor/cubit?tab=readme-ov-file#install) section. **Only Python 3.9 and 3.10 versions are supported**. + 2. Then install SDK with `pip install bittensor`. + --- ## Install on Windows From 628217c5ce14eaddf06038bc994cf5a4596c9184 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 25 Sep 2024 12:21:23 -0700 Subject: [PATCH 253/260] set `WARNING` level ad default logging level --- bittensor/utils/btlogging/loggingmachine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index 056aa206c..b2cfb2918 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -302,7 +302,7 @@ def after_transition(self, event, state): def before_enable_default(self): """Logs status before enable Default.""" self._logger.info(f"Enabling default logging.") - self._logger.setLevel(stdlogging.INFO) + self._logger.setLevel(stdlogging.WARNING) for logger in all_loggers(): if logger.name in self._primary_loggers: continue From 7b920719447f6ec6af9f76f27ab098cad077344a Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 25 Sep 2024 12:23:21 -0700 Subject: [PATCH 254/260] fix test --- tests/unit_tests/test_logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 2c5e593f0..2aa43d1e7 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -119,7 +119,7 @@ def test_state_transitions(logging_machine, mock_config): logging_machine.enable_default() assert logging_machine.current_state_value == "Default" # main logger set to INFO - mocked_bt_logger.setLevel.assert_called_with(stdlogging.INFO) + mocked_bt_logger.setLevel.assert_called_with(stdlogging.WARNING) # 3rd party loggers should be disabled by setting to CRITICAL mocked_third_party_logger.setLevel.assert_called_with(stdlogging.CRITICAL) From bd8f8f5abe5b07b4f979bfdfed1db3907e976d36 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 30 Sep 2024 12:30:47 -0700 Subject: [PATCH 255/260] add ConnectionRefusedError raising --- bittensor/core/subtensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 533964595..6b4761d37 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -186,6 +186,7 @@ def __init__( self.log_verbose = log_verbose self._connection_timeout = connection_timeout + self.substrate: "SubstrateInterface" = None self._get_substrate() def __str__(self) -> str: @@ -201,7 +202,8 @@ def __repr__(self) -> str: def close(self): """Cleans up resources for this subtensor instance like active websocket connection and active extensions.""" - self.substrate.close() + if self.substrate: + self.substrate.close() def _get_substrate(self): """Establishes a connection to the Substrate node using configured parameters.""" @@ -223,14 +225,15 @@ def _get_substrate(self): except (AttributeError, TypeError, socket.error, OSError) as e: logging.warning(f"Error setting timeout: {e}") - except ConnectionRefusedError: + except ConnectionRefusedError as error: logging.error( f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint.", ) logging.info( "You can check if you have connectivity by running this command: nc -vz localhost " - f"{self.chain_endpoint.split(':')[2]}" + f"{self.chain_endpoint}" ) + raise ConnectionRefusedError(error.args) @staticmethod def config() -> "Config": From bff05c0a9dadf195cbc7c345f4279980499f1174 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor <165814940+ibraheem-opentensor@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:38:26 -0700 Subject: [PATCH 256/260] Backmerge staging 8.0.0 (#2327) * Removes old btcli code * Updates .github folder * Adds .github workflow * Adds bittensor 8.0.0 --- .../e2e-multiple-bittensor-tests.yml | 115 - CHANGELOG.md | 42 + README.md | 478 +- VERSION | 2 +- bin/btcli | 50 - bittensor/__init__.py | 366 +- bittensor/{constants.py => __main__.py} | 36 +- bittensor/chain_data.py | 1204 ---- bittensor/cli.py | 398 -- bittensor/commands/__init__.py | 131 - bittensor/commands/check_coldkey_swap.py | 128 - bittensor/commands/delegates.py | 1167 ---- bittensor/commands/identity.py | 344 - bittensor/commands/inspect.py | 279 - bittensor/commands/list.py | 128 - bittensor/commands/metagraph.py | 268 - bittensor/commands/misc.py | 117 - bittensor/commands/network.py | 672 -- bittensor/commands/overview.py | 778 --- bittensor/commands/register.py | 613 -- bittensor/commands/root.py | 681 -- bittensor/commands/senate.py | 671 -- bittensor/commands/stake.py | 1401 ---- bittensor/commands/transfer.py | 133 - bittensor/commands/unstake.py | 443 -- bittensor/commands/utils.py | 282 - bittensor/commands/wallets.py | 1101 --- bittensor/commands/weights.py | 290 - .../multistep => bittensor/core}/__init__.py | 0 bittensor/{ => core}/axon.py | 463 +- bittensor/core/chain_data/__init__.py | 22 + bittensor/core/chain_data/axon_info.py | 163 + bittensor/core/chain_data/delegate_info.py | 105 + .../core/chain_data/delegate_info_lite.py | 29 + bittensor/core/chain_data/ip_info.py | 81 + bittensor/core/chain_data/neuron_info.py | 176 + bittensor/core/chain_data/neuron_info_lite.py | 171 + bittensor/core/chain_data/prometheus_info.py | 31 + .../core/chain_data/proposal_vote_data.py | 21 + .../chain_data/scheduled_coldkey_swap_info.py | 65 + bittensor/core/chain_data/stake_info.py | 79 + .../core/chain_data/subnet_hyperparameters.py | 112 + bittensor/core/chain_data/subnet_info.py | 103 + bittensor/core/chain_data/utils.py | 291 + bittensor/{ => core}/config.py | 185 +- bittensor/{ => core}/dendrite.py | 410 +- bittensor/{ => core}/errors.py | 118 +- bittensor/{ => core}/extrinsics/__init__.py | 8 +- bittensor/core/extrinsics/commit_weights.py | 274 + bittensor/core/extrinsics/prometheus.py | 187 + bittensor/core/extrinsics/serving.py | 319 + bittensor/core/extrinsics/set_weights.py | 194 + bittensor/core/extrinsics/transfer.py | 215 + bittensor/core/extrinsics/utils.py | 49 + bittensor/{ => core}/metagraph.py | 350 +- bittensor/core/settings.py | 241 + bittensor/{ => core}/stream.py | 88 +- bittensor/core/subtensor.py | 1733 +++++ bittensor/{ => core}/synapse.py | 102 +- bittensor/{ => core}/tensor.py | 39 +- bittensor/{ => core}/threadpool.py | 36 +- bittensor/{ => core}/types.py | 16 +- bittensor/extrinsics/commit_weights.py | 127 - bittensor/extrinsics/delegation.py | 528 -- bittensor/extrinsics/network.py | 250 - bittensor/extrinsics/prometheus.py | 140 - bittensor/extrinsics/registration.py | 543 -- bittensor/extrinsics/root.py | 237 - bittensor/extrinsics/senate.py | 275 - bittensor/extrinsics/serving.py | 286 - bittensor/extrinsics/set_weights.py | 136 - bittensor/extrinsics/staking.py | 760 --- bittensor/extrinsics/transfer.py | 164 - bittensor/extrinsics/unstaking.py | 470 -- bittensor/keyfile.py | 866 --- bittensor/mock/keyfile_mock.py | 90 - bittensor/mock/wallet_mock.py | 127 - bittensor/subtensor.py | 5909 ----------------- bittensor/utils/__init__.py | 323 +- bittensor/utils/_register_cuda.py | 126 - bittensor/utils/axon_utils.py | 36 +- bittensor/utils/balance.py | 61 +- bittensor/{ => utils}/btlogging/__init__.py | 13 +- bittensor/{ => utils}/btlogging/defines.py | 8 +- bittensor/{ => utils}/btlogging/format.py | 40 +- bittensor/{ => utils}/btlogging/helpers.py | 10 +- .../{ => utils}/btlogging/loggingmachine.py | 79 +- bittensor/utils/deprecated.py | 150 + bittensor/utils/formatting.py | 123 - bittensor/{ => utils}/mock/__init__.py | 2 +- bittensor/{ => utils}/mock/subtensor_mock.py | 685 +- bittensor/utils/networking.py | 45 +- bittensor/utils/registration.py | 1123 +--- bittensor/{ => utils}/subnets.py | 53 +- bittensor/utils/subtensor.py | 173 - bittensor/utils/test_utils.py | 22 - bittensor/utils/version.py | 79 +- bittensor/utils/wallet_utils.py | 209 - bittensor/utils/weight_utils.py | 212 +- bittensor/wallet.py | 873 --- requirements/btcli.txt | 1 + requirements/dev.txt | 1 - requirements/prod.txt | 30 +- scripts/run.sh | 18 - setup.py | 24 +- tests/e2e_tests/conftest.py | 116 +- tests/e2e_tests/multistep/test_axon.py | 112 - tests/e2e_tests/multistep/test_dendrite.py | 164 - tests/e2e_tests/multistep/test_emissions.py | 283 - tests/e2e_tests/subcommands/__init__.py | 0 .../subcommands/delegation/__init__.py | 0 .../delegation/test_set_delegate_take.py | 61 - .../subcommands/hyperparams/__init__.py | 0 .../hyperparams/test_liquid_alpha.py | 278 - .../subcommands/register/__init__.py | 0 .../subcommands/register/test_swap_hotkey.py | 566 -- tests/e2e_tests/subcommands/root/__init__.py | 0 .../root/test_root_delegate_list.py | 24 - .../test_root_register_add_member_senate.py | 120 - .../subcommands/root/test_root_senate_vote.py | 49 - .../root/test_root_view_proposal.py | 45 - tests/e2e_tests/subcommands/stake/__init__.py | 0 .../subcommands/stake/test_childkeys.py | 472 -- .../stake/test_stake_add_remove.py | 81 - .../subcommands/stake/test_stake_show.py | 37 - .../e2e_tests/subcommands/subnet/__init__.py | 0 .../e2e_tests/subcommands/subnet/test_list.py | 29 - .../subcommands/subnet/test_metagraph.py | 122 - .../e2e_tests/subcommands/wallet/__init__.py | 0 .../subcommands/wallet/test_faucet.py | 92 - .../e2e_tests/subcommands/wallet/test_list.py | 72 - .../subcommands/wallet/test_transfer.py | 35 - .../wallet/test_wallet_creations.py | 505 -- .../e2e_tests/subcommands/weights/__init__.py | 0 .../weights/test_commit_weights.py | 246 - tests/e2e_tests/test_axon.py | 128 + tests/e2e_tests/test_commit_weights.py | 165 + tests/e2e_tests/test_dendrite.py | 136 + .../{multistep => }/test_incentive.py | 169 +- tests/e2e_tests/test_liquid_alpha.py | 186 + tests/e2e_tests/test_metagraph.py | 177 + tests/e2e_tests/test_subtensor_functions.py | 152 + tests/e2e_tests/test_transfer.py | 52 + tests/e2e_tests/utils.py | 214 - tests/e2e_tests/utils/chain_interactions.py | 186 + tests/e2e_tests/utils/e2e_test_utils.py | 83 + tests/helpers/__init__.py | 12 +- tests/helpers/helpers.py | 34 +- tests/integration_tests/__init__.py | 16 + tests/integration_tests/test_cli.py | 2752 -------- .../integration_tests/test_cli_no_network.py | 1533 ----- .../test_metagraph_integration.py | 20 +- .../test_subtensor_integration.py | 728 +- tests/unit_tests/commands/test_utils.py | 60 - tests/unit_tests/conftest.py | 4 +- .../extrinsics/test_commit_weights.py | 133 + .../unit_tests/extrinsics/test_delegation.py | 459 -- tests/unit_tests/extrinsics/test_init.py | 65 + tests/unit_tests/extrinsics/test_network.py | 157 - .../unit_tests/extrinsics/test_prometheus.py | 85 +- .../extrinsics/test_registration.py | 401 -- tests/unit_tests/extrinsics/test_root.py | 308 - tests/unit_tests/extrinsics/test_senate.py | 237 - tests/unit_tests/extrinsics/test_serving.py | 91 +- .../unit_tests/extrinsics/test_set_weights.py | 196 +- tests/unit_tests/extrinsics/test_staking.py | 551 -- tests/unit_tests/extrinsics/test_transfer.py | 142 + tests/unit_tests/extrinsics/test_unstaking.py | 332 - tests/unit_tests/factories/neuron_factory.py | 2 +- tests/unit_tests/test_axon.py | 80 +- tests/unit_tests/test_chain_data.py | 192 +- tests/unit_tests/test_dendrite.py | 63 +- tests/unit_tests/test_deprecated.py | 51 + tests/unit_tests/test_keyfile.py | 643 -- tests/unit_tests/test_logging.py | 45 +- tests/unit_tests/test_metagraph.py | 56 +- tests/unit_tests/test_overview.py | 266 - tests/unit_tests/test_subnets.py | 82 + tests/unit_tests/test_subtensor.py | 2755 ++++---- tests/unit_tests/test_synapse.py | 35 +- tests/unit_tests/test_tensor.py | 106 +- tests/unit_tests/test_wallet.py | 517 -- tests/unit_tests/utils/test_balance.py | 25 +- tests/unit_tests/utils/test_init.py | 27 + tests/unit_tests/utils/test_registration.py | 19 +- tests/unit_tests/utils/test_subtensor.py | 99 - tests/unit_tests/utils/test_utils.py | 403 +- tests/unit_tests/utils/test_version.py | 41 +- tests/unit_tests/utils/test_wallet_utils.py | 60 - tests/unit_tests/utils/test_weight_utils.py | 159 +- 190 files changed, 10571 insertions(+), 42849 deletions(-) delete mode 100644 .github/workflows/e2e-multiple-bittensor-tests.yml delete mode 100755 bin/btcli rename bittensor/{constants.py => __main__.py} (53%) delete mode 100644 bittensor/chain_data.py delete mode 100644 bittensor/cli.py delete mode 100644 bittensor/commands/__init__.py delete mode 100644 bittensor/commands/check_coldkey_swap.py delete mode 100644 bittensor/commands/delegates.py delete mode 100644 bittensor/commands/identity.py delete mode 100644 bittensor/commands/inspect.py delete mode 100644 bittensor/commands/list.py delete mode 100644 bittensor/commands/metagraph.py delete mode 100644 bittensor/commands/misc.py delete mode 100644 bittensor/commands/network.py delete mode 100644 bittensor/commands/overview.py delete mode 100644 bittensor/commands/register.py delete mode 100644 bittensor/commands/root.py delete mode 100644 bittensor/commands/senate.py delete mode 100644 bittensor/commands/stake.py delete mode 100644 bittensor/commands/transfer.py delete mode 100644 bittensor/commands/unstake.py delete mode 100644 bittensor/commands/utils.py delete mode 100644 bittensor/commands/wallets.py delete mode 100644 bittensor/commands/weights.py rename {tests/e2e_tests/multistep => bittensor/core}/__init__.py (100%) rename bittensor/{ => core}/axon.py (81%) create mode 100644 bittensor/core/chain_data/__init__.py create mode 100644 bittensor/core/chain_data/axon_info.py create mode 100644 bittensor/core/chain_data/delegate_info.py create mode 100644 bittensor/core/chain_data/delegate_info_lite.py create mode 100644 bittensor/core/chain_data/ip_info.py create mode 100644 bittensor/core/chain_data/neuron_info.py create mode 100644 bittensor/core/chain_data/neuron_info_lite.py create mode 100644 bittensor/core/chain_data/prometheus_info.py create mode 100644 bittensor/core/chain_data/proposal_vote_data.py create mode 100644 bittensor/core/chain_data/scheduled_coldkey_swap_info.py create mode 100644 bittensor/core/chain_data/stake_info.py create mode 100644 bittensor/core/chain_data/subnet_hyperparameters.py create mode 100644 bittensor/core/chain_data/subnet_info.py create mode 100644 bittensor/core/chain_data/utils.py rename bittensor/{ => core}/config.py (73%) rename bittensor/{ => core}/dendrite.py (63%) rename bittensor/{ => core}/errors.py (51%) rename bittensor/{ => core}/extrinsics/__init__.py (95%) create mode 100644 bittensor/core/extrinsics/commit_weights.py create mode 100644 bittensor/core/extrinsics/prometheus.py create mode 100644 bittensor/core/extrinsics/serving.py create mode 100644 bittensor/core/extrinsics/set_weights.py create mode 100644 bittensor/core/extrinsics/transfer.py create mode 100644 bittensor/core/extrinsics/utils.py rename bittensor/{ => core}/metagraph.py (80%) create mode 100644 bittensor/core/settings.py rename bittensor/{ => core}/stream.py (52%) create mode 100644 bittensor/core/subtensor.py rename bittensor/{ => core}/synapse.py (92%) rename bittensor/{ => core}/tensor.py (91%) rename bittensor/{ => core}/threadpool.py (94%) rename bittensor/{ => core}/types.py (89%) delete mode 100644 bittensor/extrinsics/commit_weights.py delete mode 100644 bittensor/extrinsics/delegation.py delete mode 100644 bittensor/extrinsics/network.py delete mode 100644 bittensor/extrinsics/prometheus.py delete mode 100644 bittensor/extrinsics/registration.py delete mode 100644 bittensor/extrinsics/root.py delete mode 100644 bittensor/extrinsics/senate.py delete mode 100644 bittensor/extrinsics/serving.py delete mode 100644 bittensor/extrinsics/set_weights.py delete mode 100644 bittensor/extrinsics/staking.py delete mode 100644 bittensor/extrinsics/transfer.py delete mode 100644 bittensor/extrinsics/unstaking.py delete mode 100644 bittensor/keyfile.py delete mode 100644 bittensor/mock/keyfile_mock.py delete mode 100644 bittensor/mock/wallet_mock.py delete mode 100644 bittensor/subtensor.py delete mode 100644 bittensor/utils/_register_cuda.py rename bittensor/{ => utils}/btlogging/__init__.py (85%) rename bittensor/{ => utils}/btlogging/defines.py (96%) rename bittensor/{ => utils}/btlogging/format.py (89%) rename bittensor/{ => utils}/btlogging/helpers.py (97%) rename bittensor/{ => utils}/btlogging/loggingmachine.py (90%) create mode 100644 bittensor/utils/deprecated.py delete mode 100644 bittensor/utils/formatting.py rename bittensor/{ => utils}/mock/__init__.py (95%) rename bittensor/{ => utils}/mock/subtensor_mock.py (55%) rename bittensor/{ => utils}/subnets.py (65%) delete mode 100644 bittensor/utils/subtensor.py delete mode 100644 bittensor/utils/test_utils.py delete mode 100644 bittensor/utils/wallet_utils.py delete mode 100644 bittensor/wallet.py create mode 100644 requirements/btcli.txt delete mode 100755 scripts/run.sh delete mode 100644 tests/e2e_tests/multistep/test_axon.py delete mode 100644 tests/e2e_tests/multistep/test_dendrite.py delete mode 100644 tests/e2e_tests/multistep/test_emissions.py delete mode 100644 tests/e2e_tests/subcommands/__init__.py delete mode 100644 tests/e2e_tests/subcommands/delegation/__init__.py delete mode 100644 tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py delete mode 100644 tests/e2e_tests/subcommands/hyperparams/__init__.py delete mode 100644 tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py delete mode 100644 tests/e2e_tests/subcommands/register/__init__.py delete mode 100644 tests/e2e_tests/subcommands/register/test_swap_hotkey.py delete mode 100644 tests/e2e_tests/subcommands/root/__init__.py delete mode 100644 tests/e2e_tests/subcommands/root/test_root_delegate_list.py delete mode 100644 tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py delete mode 100644 tests/e2e_tests/subcommands/root/test_root_senate_vote.py delete mode 100644 tests/e2e_tests/subcommands/root/test_root_view_proposal.py delete mode 100644 tests/e2e_tests/subcommands/stake/__init__.py delete mode 100644 tests/e2e_tests/subcommands/stake/test_childkeys.py delete mode 100644 tests/e2e_tests/subcommands/stake/test_stake_add_remove.py delete mode 100644 tests/e2e_tests/subcommands/stake/test_stake_show.py delete mode 100644 tests/e2e_tests/subcommands/subnet/__init__.py delete mode 100644 tests/e2e_tests/subcommands/subnet/test_list.py delete mode 100644 tests/e2e_tests/subcommands/subnet/test_metagraph.py delete mode 100644 tests/e2e_tests/subcommands/wallet/__init__.py delete mode 100644 tests/e2e_tests/subcommands/wallet/test_faucet.py delete mode 100644 tests/e2e_tests/subcommands/wallet/test_list.py delete mode 100644 tests/e2e_tests/subcommands/wallet/test_transfer.py delete mode 100644 tests/e2e_tests/subcommands/wallet/test_wallet_creations.py delete mode 100644 tests/e2e_tests/subcommands/weights/__init__.py delete mode 100644 tests/e2e_tests/subcommands/weights/test_commit_weights.py create mode 100644 tests/e2e_tests/test_axon.py create mode 100644 tests/e2e_tests/test_commit_weights.py create mode 100644 tests/e2e_tests/test_dendrite.py rename tests/e2e_tests/{multistep => }/test_incentive.py (52%) create mode 100644 tests/e2e_tests/test_liquid_alpha.py create mode 100644 tests/e2e_tests/test_metagraph.py create mode 100644 tests/e2e_tests/test_subtensor_functions.py create mode 100644 tests/e2e_tests/test_transfer.py delete mode 100644 tests/e2e_tests/utils.py create mode 100644 tests/e2e_tests/utils/chain_interactions.py create mode 100644 tests/e2e_tests/utils/e2e_test_utils.py delete mode 100644 tests/integration_tests/test_cli.py delete mode 100644 tests/integration_tests/test_cli_no_network.py delete mode 100644 tests/unit_tests/commands/test_utils.py create mode 100644 tests/unit_tests/extrinsics/test_commit_weights.py delete mode 100644 tests/unit_tests/extrinsics/test_delegation.py delete mode 100644 tests/unit_tests/extrinsics/test_network.py delete mode 100644 tests/unit_tests/extrinsics/test_registration.py delete mode 100644 tests/unit_tests/extrinsics/test_root.py delete mode 100644 tests/unit_tests/extrinsics/test_senate.py delete mode 100644 tests/unit_tests/extrinsics/test_staking.py create mode 100644 tests/unit_tests/extrinsics/test_transfer.py delete mode 100644 tests/unit_tests/extrinsics/test_unstaking.py create mode 100644 tests/unit_tests/test_deprecated.py delete mode 100644 tests/unit_tests/test_keyfile.py delete mode 100644 tests/unit_tests/test_overview.py create mode 100644 tests/unit_tests/test_subnets.py delete mode 100644 tests/unit_tests/test_wallet.py create mode 100644 tests/unit_tests/utils/test_init.py delete mode 100644 tests/unit_tests/utils/test_subtensor.py delete mode 100644 tests/unit_tests/utils/test_wallet_utils.py diff --git a/.github/workflows/e2e-multiple-bittensor-tests.yml b/.github/workflows/e2e-multiple-bittensor-tests.yml deleted file mode 100644 index 470ccef4f..000000000 --- a/.github/workflows/e2e-multiple-bittensor-tests.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: E2E tests w/ multiple bittensor versions - -on: - workflow_dispatch: - inputs: - bittensor_versions: - description: 'Bittensor versions to test (comma-separated)' - required: true - default: '7.3.1,7.2.1' - bittensor_branch: - description: 'Branch of bittensor' - required: true - default: 'staging' - subtensor_branch: - description: 'Branch of subtensor' - required: true - default: 'testnet' - -env: - RUSTV: nightly-2024-03-05 - RUST_BACKTRACE: full - -jobs: - setup: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Set up test matrix - id: set-matrix - run: | - versions=$(echo "${{ github.event.inputs.bittensor_versions }}" | jq -R -s -c 'split(",")| map(select(. != ""))') - echo "matrix=${versions}" >> $GITHUB_OUTPUT - - test: - needs: setup - runs-on: SubtensorCI - strategy: - fail-fast: false - matrix: - bittensor-version: ${{fromJson(needs.setup.outputs.matrix)}} - rust-target: - - x86_64-unknown-linux-gnu - env: - RUST_BIN_DIR: target/${{ matrix.rust-target }} - TARGET: ${{ matrix.rust-target }} - steps: - - name: Check-out repository - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.bittensor_branch }} - - - name: Print working directory - run: | - pwd - ls -la - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install tox - - - name: Install Rust dependencies - run: | - sudo apt-get update - sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler - - - name: Install Rust ${{ env.RUSTV }} - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: ${{ env.RUSTV }} - components: rustfmt - profile: minimal - - - name: Add wasm32-unknown-unknown target - run: | - rustup target add wasm32-unknown-unknown --toolchain stable-x86_64-unknown-linux-gnu - rustup component add rust-src --toolchain stable-x86_64-unknown-linux-gnu - - - name: Clone subtensor repo - run: git clone https://github.com/opentensor/subtensor.git - - - name: Setup subtensor repo - working-directory: ${{ github.workspace }}/subtensor - run: git checkout ${{ github.event.inputs.subtensor_branch }} - - - name: Create tox.ini - run: | - cd ../.. - cat << EOF > tox.ini - [tox] - envlist = bt-${{ matrix.bittensor-version }} - - [testenv] - deps = - pytest - pytest-asyncio - anyio - nest_asyncio - bittensor==${{ matrix.bittensor-version }} - commands = - pytest ${{ github.workspace }}/tests/e2e_tests -v -s {posargs} - passenv = - LOCALNET_SH_PATH - - [pytest] - asyncio_mode = auto - EOF - - - name: Run tox - env: - LOCALNET_SH_PATH: ${{ github.workspace }}/subtensor/scripts/localnet.sh - run: | - cd ../.. - tox -c tox.ini -e bt-${{ matrix.bittensor-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c76ed5995..011a1464d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## 8.0.0 /2024-09-25 + +## What's Changed + +Removes Bittensor CLI and Wallet functionalities and changes the Bittensor SDK package to be light while maintaining backwards compatibility + +* Update README.md by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/2320 +* remove unused code (tensor.py-> class tensor), remove old tests, add new tests by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2311 +* Updating/improving/creating docstring codebase by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2310 +* README updates for SDK by @rajkaramchedu in https://github.com/opentensor/bittensor/pull/2309 +* Improved logic for concatenating message, prefix, and suffix in bittensor logging + test by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2306 +* BTSDK: Implementation of substrait custom errors handler for bittensor by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2305 +* btsdk cleanup by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2303 +* Fix mypy error for btlogging by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2299 +* Integrate `bt_decode` into BTSDK by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2298 +* BTSDK: Corrected arguments order in logging methods + test by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2292 +* BTSDK: removed exit sys call for ConnectionRefusedError in _get_substrate by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2288 +* BTSDK: Move `do*` methods to related extrinsic by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2286 +* add reconnection logic for correctly closed connection by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2283 +* Move extrinsics, update `deprecated.py` module. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2278 +* Add substrate reconnection logic by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2269 +* Prod requirements cleanup by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2266 +* Decoupling chain_data.py to sub-package by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2264 +* Increase Bittensor SDK test coverage by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2262 +* Increase SDK test coverage (Part3) by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2257 +* Increase bittensor SDK test coverage by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2256 +* Increase test coverage for subtensor.py by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2252 +* Adds e2e and fixes metagraph save()/load() by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2231 +* feat/roman/reafctoring-before-test-coverage by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2230 +* Enhance: Switch from format() to f-strings by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2228 +* Commit-reveal re-added & e2e coverage by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2224 +* Adds e2e setup & tests by @ibraheem-opentensor in https://github.com/opentensor/bittensor/pull/2221 +* Updates after review session by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2220 +* Fix the usage of env vars in default settings. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2218 +* Add dendrite reference to backwords compatibility by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2217 +* Bringing `btsdk` up-to-date with `staging` branch. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2210 +* Part 3: Create new 'bittensor-sdk` package by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2206 +* Part 2: Redesign, fix namespace conflicts, remove btcli by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2204 +* Part1: Removing content related to the wallet. Start use the pip installable package. by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2191 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v7.4.0...v8.0.0 + ## 7.4.0 /2024-08-29 ## What's Changed diff --git a/README.md b/README.md index b9284f2a5..541b8ec6b 100644 --- a/README.md +++ b/README.md @@ -1,428 +1,238 @@
-# **Bittensor** +# **Bittensor SDK** [![Discord Chat](https://img.shields.io/discord/308323056592486420.svg)](https://discord.gg/bittensor) [![PyPI version](https://badge.fury.io/py/bittensor.svg)](https://badge.fury.io/py/bittensor) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) --- -### Internet-scale Neural Networks +## Internet-scale Neural Networks [Discord](https://discord.gg/qasY3HA9F9) • [Network](https://taostats.io/) • [Research](https://bittensor.com/whitepaper)
-Bittensor is a mining network, similar to Bitcoin, that includes built-in incentives designed to encourage computers to provide access to machine learning models in an efficient and censorship-resistant manner. These models can be queried by users seeking outputs from the network, for instance; generating text, audio, and images, or for extracting numerical representations of these input types. Under the hood, Bittensor’s *economic market*, is facilitated by a blockchain token mechanism, through which producers (***miners***) and the verification of the work done by those miners (***validators***) are rewarded. Miners host, train or otherwise procure machine learning systems into the network as a means of fulfilling the verification problems defined by the validators, like the ability to generate responses from prompts i.e. “What is the capital of Texas?. +- [Overview of Bittensor](#overview-of-bittensor) +- [The Bittensor SDK](#the-bittensor-sdk) +- [Is Bittensor a blockchain or an AI platform?](#is-bittensor-a-blockchain-or-an-ai-platform) +- [Subnets](#subnets) +- [Subnet validators and subnet miners](#subnet-validators-and-subnet-miners) +- [Yuma Consensus](#yuma-consensus) +- [Release Notes](#release-notes) +- [Install Bittensor SDK](#install-bittensor-sdk) +- [Upgrade](#upgrade) +- [Install on macOS and Linux](#install-on-macos-and-linux) + - [Install using a Bash command](#install-using-a-bash-command) + - [Install using `pip3 install`](#install-using-pip3-install) + - [Install from source](#install-from-source) + - [Verify using Python interpreter](#verify-using-python-interpreter) + - [Verify by listing axon information](#verify-by-listing-axon-information) +- [Release Guidelines](#release-guidelines) +- [Contributions](#contributions) +- [License](#license) +- [Acknowledgments](#acknowledgments) -The token based mechanism under which the miners are incentivized ensures that they are constantly driven to make their knowledge output more useful, in terms of speed, intelligence and diversity. The value generated by the network is distributed directly to the individuals producing that value, without intermediaries. Anyone can participate in this endeavour, extract value from the network, and govern Bittensor. The network is open to all participants, and no individual or group has full control over what is learned, who can profit from it, or who can access it. +--- -To learn more about Bittensor, please read our [paper](https://bittensor.com/whitepaper). +## Overview of Bittensor -# Install -There are three ways to install Bittensor +Welcome! Bittensor is an open source platform on which you can produce competitive digital commodities. These digital commodities can be machine intelligence, storage space, compute power, protein folding, financial markets prediction, and many more. You are rewarded in **TAO** when you produce best digital commodities. -1. Through the installer: -```bash -$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/opentensor/bittensor/master/scripts/install.sh)" -``` -2. With pip: -```bash -$ pip3 install bittensor -``` -3. From source: -```bash -$ git clone https://github.com/opentensor/bittensor.git -$ python3 -m pip install -e bittensor/ -``` -4. Using Conda (recommended for **Apple M1**): -```bash -$ conda env create -f ~/.bittensor/bittensor/scripts/environments/apple_m1_environment.yml -$ conda activate bittensor -``` +## The Bittensor SDK -To test your installation, type: -```bash -$ btcli --help -``` -or using python -```python -import bittensor -``` +The Opentensor Foundation (OTF) provides all the open source tools, including this Bittensor SDK, the codebase and the documentation, with step-by-step tutorials and guides, to enable you to participate in the Bittensor ecosystem. -#### CUDA -If you anticipate using PoW registration for subnets or the faucet (only available on staging), please install `cubit` as well for your version of python. You can find the Opentensor cubit implementation and instructions [here](https://github.com/opentensor/cubit). +- **Developer documentation**: https://docs.bittensor.com. +- **A Beginner's Q and A on Bittensor**: https://docs.bittensor.com/questions-and-answers. +- **Bittensor whitepaper**: https://bittensor.com/whitepaper. -For example with python 3.10: -```bash -pip install https://github.com/opentensor/cubit/releases/download/v1.1.2/cubit-1.1.2-cp310-cp310-linux_x86_64.whl -``` +This Bittensor SDK contains ready-to-use Python packages for interacting with the Bittensor ecosystem, writing subnet incentive mechanisms, subnet miners, subnet validators and querying the subtensor (the blockchain part of the Bittensor network). -# Wallets +--- -Wallets are the core ownership and identity technology around which all functions on Bittensor are carried out. Bittensor wallets consists of a coldkey and hotkey where the coldkey may contain many hotkeys, while each hotkey can only belong to a single coldkey. Coldkeys store funds securely, and operate functions such as transfers and staking, while hotkeys are used for all online operations such as signing queries, running miners and validating. +## Is Bittensor a blockchain or an AI platform? -Wallets can be created in two ways. -1. Using the python-api -```python -import bittensor -wallet = bittensor.wallet() -wallet.create_new_coldkey() -wallet.create_new_hotkey() -print (wallet) -"Wallet (default, default, ~/.bittensor/wallets/)" -``` -2. Or using btcli -> Use the subcommand `wallet` or it's alias `w`: -```bash -$ btcli wallet new_coldkey - Enter wallet name (default): - - IMPORTANT: Store this mnemonic in a secure (preferably offline place), as anyone who has possession of this mnemonic can use it to regenerate the key and access your tokens. - The mnemonic to the new coldkey is: - **** *** **** **** ***** **** *** **** **** **** ***** ***** - You can use the mnemonic to recreate the key in case it gets lost. The command to use to regenerate the key using this mnemonic is: - btcli w regen_coldkey --mnemonic post maid erode shy captain verify scan shoulder brisk mountain pelican elbow - -$ btcli wallet new_hotkey - Enter wallet name (default): d1 - Enter hotkey name (default): - - IMPORTANT: Store this mnemonic in a secure (preferably offline place), as anyone who has possession of this mnemonic can use it to regenerate the key and access your tokens. - The mnemonic to the new hotkey is: - **** *** **** **** ***** **** *** **** **** **** ***** ***** - You can use the mnemonic to recreate the key in case it gets lost. The command to use to regenerate the key using this mnemonic is: - btcli w regen_hotkey --mnemonic total steak hour bird hedgehog trim timber can friend dry worry text -``` -In both cases you should be able to view your keys by navigating to ~/.bittensor/wallets or viewed by running ```btcli wallet list``` -```bash -$ tree ~/.bittensor/ - .bittensor/ # Bittensor, root directory. - wallets/ # The folder containing all bittensor wallets. - default/ # The name of your wallet, "default" - coldkey # You encrypted coldkey. - coldkeypub.txt # Your coldkey public address - hotkeys/ # The folder containing all of your hotkeys. - default # You unencrypted hotkey information. -``` -Your default wallet ```Wallet (default, default, ~/.bittensor/wallets/)``` is always used unless you specify otherwise. Be sure to store your mnemonics safely. If you lose your password to your wallet, or the access to the machine where the wallet is stored, you can always regenerate the coldkey using the mnemonic you saved from above. -```bash -$ btcli wallet regen_coldkey --mnemonic **** *** **** **** ***** **** *** **** **** **** ***** ***** -``` +In Bittensor there is one blockchain, and many platforms that are connected to this one blockchain. We call these platforms as **subnets**, and this one blockchain **subtensor**. So, a subnet can be AI-related or it can be something else. The Bittensor network has a number of distinct subnets. All these subnets interact with subtensor blockchain. If you are thinking, "So, subnets are not part of the blockchain but only interact with it?" then the answer is "yes, exactly." -## Using the cli -The Bittensor command line interface (`btcli`) is the primary command line tool for interacting with the Bittensor network. It can be used to deploy nodes, manage wallets, stake/unstake, nominate, transfer tokens, and more. +## Subnets -### Basic Usage +Each category of the digital commodity is produced in a distinct subnet. Applications are built on these specific subnets. End-users of these applications would be served by these applications. -To get the list of all the available commands and their descriptions, you can use: +## Subnet validators and subnet miners -```bash -btcli --help +Subnets, which exist outside the blockchain and are connected to it, are off-chain competitions where only the best producers are rewarded. A subnet consists of off-chain **subnet validators** who initiate the competition for a specific digital commodity, and off-chain **subnet miners** who compete and respond by producing the best quality digital commodity. -usage: btcli +## Yuma Consensus -bittensor cli v{bittensor.__version__} +Scores are assigned to the top-performing subnet miners and subnet validators. The on-chain Yuma Consensus determines the TAO rewards for these top performers. The Bittensor blockchain, the subtensor, runs on decentralized validation nodes, just like any blockchain. -commands: - subnets (s, subnet) - Commands for managing and viewing subnetworks. - root (r, roots) - Commands for managing and viewing the root network. - wallet (w, wallets) - Commands for managing and viewing wallets. - stake (st, stakes) - Commands for staking and removing stake from hotkey accounts. - sudo (su, sudos) - Commands for subnet management. - legacy (l) - Miscellaneous commands. -``` +**This SDK repo is for Bittensor platform only** +This Bittensor SDK codebase is for the Bittensor platform only, designed to help developers create subnets and build tools on Bittensor. For subnets and applications, refer to subnet-specific websites, which are maintained by subnet owners. -### Example Commands +## Release Notes -#### Viewing Senate Proposals -```bash -btcli root proposals -``` +See [Bittensor SDK Release Notes](https://docs.bittensor.com/bittensor-rel-notes). -#### Viewing Senate Members -```bash -btcli root list_delegates -``` +--- -#### Viewing Proposal Votes -```bash -btcli root senate_vote --proposal=[PROPOSAL_HASH] -``` +## Install Bittensor SDK -#### Registering for Senate -```bash -btcli root register -``` +Before you can start developing, you must install Bittensor SDK and then create Bittensor wallet. -#### Leaving Senate -```bash -btcli root undelegate -``` +## Upgrade -#### Voting in Senate -```bash -btcli root senate_vote --proposal=[PROPOSAL_HASH] -``` +If you already installed Bittensor SDK, make sure you upgrade to the latest version. Run the below command: -#### Miscellaneous Commands ```bash -btcli legacy update -btcli legacy faucet +python3 -m pip install --upgrade bittensor ``` -#### Managing Subnets -```bash -btcli subnets list -btcli subnets create -``` +--- + +## Install on macOS and Linux + +You can install Bittensor SDK on your local machine in either of the following ways. **Make sure you verify your installation after you install**: +- [Install using a Bash command](#install-using-a-bash-command). +- [Install using `pip3 install`](#install-using-pip3-install) +- [Install from source](#install-from-source) + +### Install using a Bash command + +This is the most straightforward method. It is recommended for a beginner as it will pre-install requirements like Python, if they are not already present on your machine. Copy and paste the following `bash` command into your terminal: -#### Managing Wallets ```bash -btcli wallet list -btcli wallet transfer +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/opentensor/bittensor/master/scripts/install.sh)" ``` -### Note +**For Ubuntu-Linux users** +If you are using Ubuntu-Linux, the script will prompt for `sudo` access to install all required apt-get packages. -Please replace the subcommands and arguments as necessary to suit your needs, and always refer to `btcli --help` or `btcli --help` for the most up-to-date and accurate information. +### Install using `pip3 install` -For example: ```bash -btcli subnets --help - -usage: btcli subnets [-h] {list,metagraph,lock_cost,create,register,pow_register,hyperparameters} ... - -positional arguments: - {list,metagraph,lock_cost,create,register,pow_register,hyperparameters} - Commands for managing and viewing subnetworks. - list List all subnets on the network. - metagraph View a subnet metagraph information. - lock_cost Return the lock cost to register a subnet. - create Create a new bittensor subnetwork on this chain. - register Register a wallet to a network. - pow_register Register a wallet to a network using PoW. - hyperparameters View subnet hyperparameters. - -options: - -h, --help show this help message and exit +python3 -m venv bt_venv +source bt_venv/bin/activate +pip install bittensor ``` -### Post-Installation Steps +### Install from source + +1. Create and activate a virtual environment -To enable autocompletion for Bittensor CLI, run the following commands: + - Create Python virtual environment. Follow [this guide on python.org](https://docs.python.org/3/library/venv.html#creating-virtual-environments). + + - Activate the new environment. Follow [this guide on python.org](https://docs.python.org/3/library/venv.html#how-venvs-work) + +2. Clone the Bittensor SDK repo ```bash -btcli --print-completion bash >> ~/.bashrc # For Bash -btcli --print-completion zsh >> ~/.zshrc # For Zsh -source ~/.bashrc # Reload Bash configuration to take effect +git clone https://github.com/opentensor/bittensor.git ``` -# The Bittensor Package -The bittensor package contains data structures for interacting with the bittensor ecosystem, writing miners, validators and querying the network. Additionally, it provides many utilities for efficient serialization of Tensors over the wire, performing data analysis of the network, and other useful utilities. +3. Install -In the 7.0.0 release, we have removed `torch` by default. However, you can still use `torch` by setting the environment variable -`USE_TORCH=1` and making sure that you have installed the `torch` library. -You can install `torch` by running `pip install bittensor[torch]` (if installing via PyPI), or by running `pip install -e ".[torch]"` (if installing from source). -We will not be adding any new functionality based on torch. +You can install using any of the below options: -Wallet: Interface over locally stored bittensor hot + coldkey styled wallets. -```python -import bittensor -# Bittensor's wallet maintenance class. -wallet = bittensor.wallet() -# Access the hotkey -wallet.hotkey -# Access the coldkey -wallet.coldkey ( requires decryption ) -# Sign data with the keypair. -wallet.coldkey.sign( data ) +- **Install only SDK**: Run the below command to install Bittensor SDK in the above virtual environment. -``` + ```python + pip install bittensor + ``` -Subtensor: Interfaces with bittensor's blockchain and can perform operations like extracting state information or sending transactions. -```python -import bittensor -# Bittensor's chain interface. -subtensor = bittensor.subtensor() -# Get the chain block -subtensor.get_current_block() -# Transfer Tao to a destination address. -subtensor.transfer( wallet = wallet, dest = "xxxxxxx..xxxxx", amount = 10.0) -# Register a wallet onto a subnetwork -subtensor.register( wallet = wallet, netuid = 1 ) -``` +- **Install SDK with `btcli`**: Install Bittensor SDK with `btcli`. The `btcli` will be installed as an independent tool and its Python package is `bittensor-cli`. -Metagraph: Encapsulates the chain state of a particular subnetwork at a specific block. -```python -import bittensor -# Bittensor's chain state object. -metagraph = bittensor.metagraph( netuid = 1 ) -# Resync the graph with the most recent chain state -metagraph.sync() -# Get the list of stake values -print ( metagraph.S ) -# Get endpoint information for the entire subnetwork -print ( metagraph.axons ) -# Get the hotkey information for the miner in the 10th slot -print ( metagraph.hotkeys[ 10 ] ) -# Sync the metagraph at another block -metagraph.sync( block = 100000 ) -# Save the metagraph -metagraph.save() -# Load the same -metagraph.load() -``` + ```python + pip install bittensor[btcli] + ``` -Synapse: Responsible for defining the protocol definition between axon servers and dendrite clients -```python -class Topk( bittensor.Synapse ): - topk: int = 2 # Number of "top" elements to select - input: bittensor.Tensor = pydantic.Field(..., frozen=True) # Ensure that input cannot be set on the server side. - v: bittensor.Tensor = None - i: bittensor.Tensor = None - -def topk( synapse: Topk ) -> Topk: - v, i = torch.topk( synapse.input.deserialize(), k = synapse.topk ) - synapse.v = bittensor.Tensor.serialize( v ) - synapse.i = bittensor.Tensor.serialize( i ) - return synapse - -# Attach the forward function to the axon and start. -axon = bittensor.axon().attach( topk ).start() -``` +- **Install SDK with `torch`**: Install Bittensor SDK with [`torch`](https://pytorch.org/docs/stable/torch.html). -Axon: Serves Synapse protocols with custom blacklist, priority and verify functions. + ```python + pip install bittensor[torch] + ``` -```python -import bittensor +- **Install SDK with `cubit`**: Install Bittensor SDK with [`cubit`](https://github.com/opentensor/cubit). -class MySynapse( bittensor.Synapse ): - input: int = 1 - output: int = None - -# Define a custom request forwarding function -def forward( synapse: MySynapse ) -> MySynapse: - # Apply custom logic to synapse and return it - synapse.output = 2 - return synapse - -# Define a custom request verification function -def verify_my_synapse( synapse: MySynapse ): - # Apply custom verification logic to synapse - # Optionally raise Exception - -# Define a custom request blacklist function -def blacklist_my_synapse( synapse: MySynapse ) -> bool: - # Apply custom blacklist - # return False ( if non blacklisted ) or True ( if blacklisted ) - -# Define a custom request priority function -def prioritize_my_synape( synapse: MySynapse ) -> float: - # Apply custom priority - return 1.0 - -# Initialize Axon object with a custom configuration -my_axon = bittensor.axon(config=my_config, wallet=my_wallet, port=9090, ip="192.0.2.0", external_ip="203.0.113.0", external_port=7070) - -# Attach the endpoint with the specified verification and forwarding functions -my_axon.attach( - forward_fn = forward_my_synapse, - verify_fn=verify_my_synapse, - blacklist_fn = blacklist_my_synapse, - priority_fn = prioritize_my_synape -).start() -``` - -Dendrite: Represents the abstracted implementation of a network client module -designed to send requests to those endpoints to receive inputs. - -Example: -```python -dendrite_obj = dendrite( wallet = bittensor.wallet() ) -# pings the axon endpoint -await d( ) -# ping multiple axon endpoints -await d( [] ) -# Send custom synapse request to axon. -await d( bittensor.axon(), bittensor.Synapse() ) -# Query all metagraph objects. -await d( meta.axons, bittensor.Synapse() ) -``` + 1. Install `cubit` first. See the [Install](https://github.com/opentensor/cubit?tab=readme-ov-file#install) section. **Only Python 3.9 and 3.10 versions are supported**. + 2. Then install SDK with `pip install bittensor`. + +--- -## Setting weights on root network -Use the `root` subcommand to access setting weights on the network across subnets. +## Install on Windows -```bash -btcli root weights --wallet.name --wallet.hotkey -Enter netuids (e.g. 0, 1, 2 ...): -# Here enter your selected netuids to set weights on -1, 2 +To install and run Bittensor SDK on Windows you must install [**WSL 2** (Windows Subsystem for Linux)](https://learn.microsoft.com/en-us/windows/wsl/about) on Windows and select [Ubuntu Linux distribution](https://github.com/ubuntu/WSL/blob/main/docs/guides/install-ubuntu-wsl2.md). ->Enter weights (e.g. 0.09, 0.09, 0.09 ...): -# These do not need to sum to 1, we do normalization on the backend. -# Values must be > 0 -0.5, 10 +After you installed the above, follow the same installation steps described above in [Install on macOS and Linux](#install-on-macos-and-linux). -Normalized weights: - tensor([ 0.5000, 10.0000]) -> tensor([0.0476, 0.9524]) +**ALERT**: **Limited support on Windows** +While wallet transactions like delegating, transfer, registering, staking can be performed on a Windows machine using WSL 2, the mining and validating operations are not recommended and are not supported on Windows machines. -Do you want to set the following root weights?: - weights: tensor([0.0476, 0.9524]) - uids: tensor([1, 2])? [y/n]: -y +--- -⠏ 📡 Setting root weights on test ... -``` +## Verify the installation -## Bittensor Subnets API +You can verify your installation in either of the below ways: -This guide provides instructions on how to extend the Bittensor Subnets API, a powerful interface for interacting with the Bittensor network across subnets. The Bittensor Subnets API facilitates querying across any subnet that has exposed API endpoints to unlock utility of the Bittensor decentralized network. +### Verify using `btsdk` version -The Bittensor Subnets API consists of abstract classes and a registry system to dynamically handle API interactions. It allows developers to implement custom logic for storing and retrieving data, while also providing a straightforward way for end users to interact with these functionalities. +```bash +python3 -m bittensor +``` -### Core Components +The above command will show you the version of the `btsdk` you just installed. -- **APIRegistry**: A central registry that manages API handlers. It allows for dynamic retrieval of handlers based on keys. -- **SubnetsAPI (Abstract Base Class)**: Defines the structure for API implementations, including methods for querying the network and processing responses. -- **StoreUserAPI & RetrieveUserAPI**: Concrete implementations of the `SubnetsAPI` for storing and retrieving user data. +### Verify using Python interpreter -### Implementing Custom Subnet APIs +1. Launch the Python interpreter on your terminal. -To implement your own subclasses of `bittensor.SubnetsAPI` to integrate an API into your subnet. + ```bash + python3 + ``` +2. Enter the following two lines in the Python interpreter. + + ```python + import bittensor as bt + print( bt.__version__ ) + ``` + The Python interpreter output will look like below: -1. **Inherit from `SubnetsAPI`**: Your class should inherit from the `SubnetsAPI` abstract base class. + ```python + Python 3.11.6 (main, Oct 2 2023, 13:45:54) [Clang 15.0.0 (clang-1500.0.40.1)] on darwin + Type "help", "copyright", "credits" or "license" for more information. + >>> import bittensor as bt + >>> print( bt.__version__ ) + + ``` +You will see the version number you installed in place of ``. -2. **Implement Required Methods**: Implement the `prepare_synapse` and `process_responses` abstract methods with your custom logic. +### Verify by listing axon information -That's it! For example: +You can also verify the Bittensor SDK installation by listing the axon information for the neurons. Enter the following lines in the Python interpreter. ```python import bittensor +metagraph = bittensor.Metagraph(1) +metagraph.axons[:10] +``` +The Python interpreter output will look like below. -class CustomSubnetAPI(bittensor.SubnetsAPI): - def __init__(self, wallet: "bittensor.wallet"): - super().__init__(wallet) - # Custom initialization here - - def prepare_synapse(self, *args, **kwargs): - # Custom synapse preparation logic - pass - - def process_responses(self, responses): - # Custom response processing logic - pass +```bash +[AxonInfo( /ipv4/3.139.80.241:11055, 5GqDsK6SAPyQtG243hbaKTsoeumjQQLhUu8GyrXikPTmxjn7, 5D7u5BTqF3j1XHnizp9oR67GFRr8fBEFhbdnuVQEx91vpfB5, 600 ), AxonInfo( /ipv4/8.222.132.190:5108, 5CwqDkDt1uk2Bngvf8avrapUshGmiUvYZjYa7bfA9Gv9kn1i, 5HQ9eTDorvovKTxBc9RUD22FZHZzpy1KRfaxCnRsT9QhuvR6, 600 ), AxonInfo( /ipv4/34.90.71.181:8091, 5HEo565WAy4Dbq3Sv271SAi7syBSofyfhhwRNjFNSM2gP9M2, 5ChuGqW2cxc5AZJ29z6vyTkTncg75L9ovfp8QN8eB8niSD75, 601 ), AxonInfo( /ipv4/64.247.206.79:8091, 5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN, 5E7W9QXNoW7se7B11vWRMKRCSWkkAu9EYotG5Ci2f9cqV8jn, 601 ), AxonInfo( /ipv4/51.91.30.166:40203, 5EXYcaCdnvnMZbozeknFWbj6aKXojfBi9jUpJYHea68j4q1a, 5CsxoeDvWsQFZJnDCyzxaNKgA8pBJGUJyE1DThH8xU25qUMg, 601 ), AxonInfo( /ipv4/149.137.225.62:8091, 5F4tQyWrhfGVcNhoqeiNsR6KjD4wMZ2kfhLj4oHYuyHbZAc3, 5Ccmf1dJKzGtXX7h17eN72MVMRsFwvYjPVmkXPUaapczECf6, 600 ), AxonInfo( /ipv4/38.147.83.11:8091, 5Hddm3iBFD2GLT5ik7LZnT3XJUnRnN8PoeCFgGQgawUVKNm8, 5DCQw11aUW7bozAKkB8tB5bHqAjiu4F6mVLZBdgJnk8dzUoV, 610 ), AxonInfo( /ipv4/38.147.83.30:41422, 5HNQURvmjjYhTSksi8Wfsw676b4owGwfLR2BFAQzG7H3HhYf, 5EZUTdAbXyLmrs3oiPvfCM19nG6oRs4X7zpgxG5oL1iK4MAh, 610 ), AxonInfo( /ipv4/54.227.25.215:10022, 5DxrZuW8kmkZPKGKp1RBVovaP5zHtPLDHYc5Yu82Z1fWqK5u, 5FhXUSmSZ2ec7ozRSA8Bg3ywmGwrjoLLzsXjNcwmZme2GcSC, 601 ), AxonInfo( /ipv4/52.8.243.76:40033, 5EnZN591jjsKKbt3yBtfGKWHxhxRH9cJonqTKRT5yTRUyNon, 5ChzhHyGmWwEdHjuvAxoUifHEZ6xpUjR67fDd4a42UrPysyB, 601 )] +>>> ``` -## Release -The release manager should follow the instructions of the [RELEASE_GUIDELINES.md](./RELEASE_GUIDELINES.md) document. +--- + +## Release Guidelines +Instructions for the release manager: [RELEASE_GUIDELINES.md](./contrib/RELEASE_GUIDELINES.md) document. ## Contributions -Please review the [contributing guide](./contrib/CONTRIBUTING.md) for more information before making a pull request. +Ready to contribute? Read the [contributing guide](./contrib/CONTRIBUTING.md) before making a pull request. ## License The MIT License (MIT) -Copyright © 2021 Yuma Rao +Copyright © 2024 The 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: diff --git a/VERSION b/VERSION index b61671799..fa5fce04b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.4.0 \ No newline at end of file +8.0.0 \ No newline at end of file diff --git a/bin/btcli b/bin/btcli deleted file mode 100755 index fa98536a0..000000000 --- a/bin/btcli +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python - -import websocket - -import sys -import shtab -from bittensor import cli as btcli -from bittensor import logging as bt_logging - - -def main(): - # Create the parser with shtab support - parser = btcli.__create_parser__() - args, unknown = parser.parse_known_args() - - if args.print_completion: # Check for print-completion argument - print(shtab.complete(parser, args.print_completion)) - return - - try: - cli_instance = btcli(args=sys.argv[1:]) - cli_instance.run() - except KeyboardInterrupt: - print('KeyboardInterrupt') - except RuntimeError as e: - bt_logging.error(f'RuntimeError: {e}') - except websocket.WebSocketConnectionClosedException as e: - bt_logging.error(f'Subtensor related error. WebSocketConnectionClosedException: {e}') - - -if __name__ == '__main__': - main() - -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 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. diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 9f4dbf1aa..5ddba2abe 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -1,64 +1,46 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022-2023 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 os -import warnings -from rich.console import Console -from rich.traceback import install - - -if (NEST_ASYNCIO_ENV := os.getenv("NEST_ASYNCIO")) in ("1", None): - if NEST_ASYNCIO_ENV is None: - warnings.warn( - "NEST_ASYNCIO implicitly set to '1'. In the future, the default value will be '0'." - "If you use `nest_asyncio` make sure to add it explicitly to your project dependencies," - "as it will be removed from `bittensor` package dependencies in the future." - "To silence this warning, explicitly set the environment variable, e.g. `export NEST_ASYNCIO=0`.", - DeprecationWarning, - ) - # Install and apply nest asyncio to allow the async functions - # to run in a .ipynb - import nest_asyncio +import warnings - nest_asyncio.apply() +from .core.settings import __version__, version_split, DEFAULTS +from .utils.btlogging import logging +from .utils.deprecated import * -# Bittensor code and protocol version. -__version__ = "7.4.0" +# Logging helpers. +def trace(on: bool = True): + """ + Enables or disables trace logging. -_version_split = __version__.split(".") -__version_info__ = tuple(int(part) for part in _version_split) -_version_int_base = 1000 -assert max(__version_info__) < _version_int_base + Args: + on (bool): If True, enables trace logging. If False, disables trace logging. + """ + logging.set_trace(on) -__version_as_int__: int = sum( - e * (_version_int_base**i) for i, e in enumerate(reversed(__version_info__)) -) -assert __version_as_int__ < 2**31 # fits in int32 -__new_signature_version__ = 360 -# Rich console. -__console__ = Console() -__use_console__ = True +def debug(on: bool = True): + """ + Enables or disables debug logging. -# Remove overdue locals in debug training. -install(show_locals=False) + Args: + on (bool): If True, enables debug logging. If False, disables debug logging. + """ + logging.set_debug(on) def __getattr__(name): @@ -67,305 +49,5 @@ def __getattr__(name): "version_split is deprecated and will be removed in future versions. Use __version__ instead.", DeprecationWarning, ) - return _version_split + return version_split raise AttributeError(f"module {__name__} has no attribute {name}") - - -def turn_console_off(): - global __use_console__ - global __console__ - from io import StringIO - - __use_console__ = False - __console__ = Console(file=StringIO(), stderr=False) - - -def turn_console_on(): - global __use_console__ - global __console__ - __use_console__ = True - __console__ = Console() - - -turn_console_off() - - -# Logging helpers. -def trace(on: bool = True): - logging.set_trace(on) - - -def debug(on: bool = True): - logging.set_debug(on) - - -# Substrate chain block time (seconds). -__blocktime__ = 12 - -# Pip address for versioning -__pipaddress__ = "https://pypi.org/pypi/bittensor/json" - -# Raw GitHub url for delegates registry file -__delegates_details_url__: str = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" - -# Substrate ss58_format -__ss58_format__ = 42 - -# Wallet ss58 address length -__ss58_address_length__ = 48 - -__networks__ = ["local", "finney", "test", "archive"] - -__finney_entrypoint__ = "wss://entrypoint-finney.opentensor.ai:443" - -__finney_test_entrypoint__ = "wss://test.finney.opentensor.ai:443/" - -__archive_entrypoint__ = "wss://archive.chain.opentensor.ai:443/" - -# Needs to use wss:// -__bellagene_entrypoint__ = "wss://parachain.opentensor.ai:443" - - -if ( - BT_SUBTENSOR_CHAIN_ENDPOINT := os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") -) is not None: - __local_entrypoint__ = BT_SUBTENSOR_CHAIN_ENDPOINT -else: - __local_entrypoint__ = "ws://127.0.0.1:9944" - - -__tao_symbol__: str = chr(0x03C4) - -__rao_symbol__: str = chr(0x03C1) - -# Block Explorers map network to explorer url -# Must all be polkadotjs explorer urls -__network_explorer_map__ = { - "opentensor": { - "local": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", - "endpoint": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", - "finney": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", - }, - "taostats": { - "local": "https://x.taostats.io", - "endpoint": "https://x.taostats.io", - "finney": "https://x.taostats.io", - }, -} - -# --- Type Registry --- -__type_registry__ = { - "types": { - "Balance": "u64", # Need to override default u128 - }, - "runtime_api": { - "NeuronInfoRuntimeApi": { - "methods": { - "get_neuron_lite": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - { - "name": "uid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_neurons_lite": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - } - }, - "StakeInfoRuntimeApi": { - "methods": { - "get_stake_info_for_coldkey": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_stake_info_for_coldkeys": { - "params": [ - { - "name": "coldkey_account_vecs", - "type": "Vec>", - }, - ], - "type": "Vec", - }, - }, - }, - "SubnetInfoRuntimeApi": { - "methods": { - "get_subnet_hyperparams": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - } - } - }, - "SubnetRegistrationRuntimeApi": { - "methods": {"get_network_registration_cost": {"params": [], "type": "u64"}} - }, - "ColdkeySwapRuntimeApi": { - "methods": { - "get_scheduled_coldkey_swap": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_remaining_arbitration_period": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_coldkey_swap_destinations": { - "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, - ], - "type": "Vec", - }, - } - }, - }, -} - -from .errors import ( - BlacklistedException, - ChainConnectionError, - ChainError, - ChainQueryError, - ChainTransactionError, - IdentityError, - InternalServerError, - InvalidRequestNameError, - KeyFileError, - MetadataError, - NominationError, - NotDelegateError, - NotRegisteredError, - NotVerifiedException, - PostProcessException, - PriorityException, - RegistrationError, - RunException, - StakeError, - SynapseDendriteNoneException, - SynapseParsingError, - TransferError, - UnknownSynapseError, - UnstakeError, -) - -from substrateinterface import Keypair # noqa: F401 -from .config import InvalidConfigFile, DefaultConfig, config, T -from .keyfile import ( - 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, - Mockkeyfile, -) -from .wallet import display_mnemonic_msg, wallet - -from .utils import ( - ss58_to_vec_u8, - unbiased_topk, - version_checking, - strtobool, - strtobool_with_default, - get_explorer_root_url_by_network_from_map, - get_explorer_url_for_network, - ss58_address_to_bytes, - U16_NORMALIZED_FLOAT, - U64_NORMALIZED_FLOAT, - u8_key_to_ss58, - hash, - wallet_utils, -) - -from .utils.balance import Balance as Balance -from .chain_data import ( - AxonInfo, - NeuronInfo, - NeuronInfoLite, - PrometheusInfo, - DelegateInfo, - StakeInfo, - SubnetInfo, - SubnetHyperparameters, - IPInfo, - ProposalCallData, - ProposalVoteData, -) - -# Allows avoiding name spacing conflicts and continue access to the `subtensor` module with `subtensor_module` name -from . import subtensor as subtensor_module - -# Double import allows using class `Subtensor` by referencing `bittensor.Subtensor` and `bittensor.subtensor`. -# This will be available for a while until we remove reference `bittensor.subtensor` -from .subtensor import Subtensor -from .subtensor import Subtensor as subtensor - -from .cli import cli as cli, COMMANDS as ALL_COMMANDS -from .btlogging import logging -from .metagraph import metagraph as metagraph -from .threadpool import PriorityThreadPoolExecutor as PriorityThreadPoolExecutor - -from .synapse import TerminalInfo, Synapse -from .stream import StreamingSynapse -from .tensor import tensor, Tensor -from .axon import axon as axon -from .dendrite import dendrite as dendrite - -from .mock.keyfile_mock import MockKeyfile as MockKeyfile -from .mock.subtensor_mock import MockSubtensor as MockSubtensor -from .mock.wallet_mock import MockWallet as MockWallet - -from .subnets import SubnetsAPI as SubnetsAPI - -configs = [ - axon.config(), - subtensor.config(), - PriorityThreadPoolExecutor.config(), - wallet.config(), - logging.get_config(), -] -defaults = config.merge_all(configs) diff --git a/bittensor/constants.py b/bittensor/__main__.py similarity index 53% rename from bittensor/constants.py rename to bittensor/__main__.py index 74d3dd2e0..05d664c9d 100644 --- a/bittensor/constants.py +++ b/bittensor/__main__.py @@ -1,43 +1,21 @@ # The MIT License (MIT) -# Copyright © 2023 OpenTensor Foundation - +# 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. -# Standard Library -import asyncio -from typing import Dict, Type - -# 3rd Party -import aiohttp - - -ALLOWED_DELTA = 4_000_000_000 # Delta of 4 seconds for nonce validation -V_7_2_0 = 7002000 -NANOSECONDS_IN_SECOND = 1_000_000_000 - -#### Dendrite #### -DENDRITE_ERROR_MAPPING: Dict[Type[Exception], tuple] = { - aiohttp.ClientConnectorError: ("503", "Service unavailable"), - asyncio.TimeoutError: ("408", "Request timeout"), - aiohttp.ClientResponseError: (None, "Client response error"), - aiohttp.ClientPayloadError: ("400", "Payload error"), - aiohttp.ClientError: ("500", "Client error"), - aiohttp.ServerTimeoutError: ("504", "Server timeout error"), - aiohttp.ServerDisconnectedError: ("503", "Service disconnected"), - aiohttp.ServerConnectionError: ("503", "Service connection error"), -} +from bittensor import __version__ -DENDRITE_DEFAULT_ERROR = ("422", "Failed to parse response") -#### End Dendrite #### +if __name__ == "__main__": + print(f"Bittensor SDK version: {__version__}") diff --git a/bittensor/chain_data.py b/bittensor/chain_data.py deleted file mode 100644 index 029cb2982..000000000 --- a/bittensor/chain_data.py +++ /dev/null @@ -1,1204 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2023 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. - -""" -This module provides data structures and functions for working with the Bittensor network, -including neuron and subnet information, SCALE encoding/decoding, and custom RPC type registry. -""" - -import json -from dataclasses import dataclass, asdict -from enum import Enum -from typing import List, Tuple, Dict, Optional, Any, TypedDict, Union - -from scalecodec.base import RuntimeConfiguration, ScaleBytes -from scalecodec.type_registry import load_type_registry_preset -from scalecodec.types import GenericCall -from scalecodec.utils.ss58 import ss58_encode - -import bittensor -from .utils import networking as net, RAOPERTAO, U16_NORMALIZED_FLOAT -from .utils.balance import Balance -from .utils.registration import torch, use_torch - -custom_rpc_type_registry = { - "types": { - "SubnetInfo": { - "type": "struct", - "type_mapping": [ - ["netuid", "Compact"], - ["rho", "Compact"], - ["kappa", "Compact"], - ["difficulty", "Compact"], - ["immunity_period", "Compact"], - ["max_allowed_validators", "Compact"], - ["min_allowed_weights", "Compact"], - ["max_weights_limit", "Compact"], - ["scaling_law_power", "Compact"], - ["subnetwork_n", "Compact"], - ["max_allowed_uids", "Compact"], - ["blocks_since_last_step", "Compact"], - ["tempo", "Compact"], - ["network_modality", "Compact"], - ["network_connect", "Vec<[u16; 2]>"], - ["emission_values", "Compact"], - ["burn", "Compact"], - ["owner", "AccountId"], - ], - }, - "DelegateInfo": { - "type": "struct", - "type_mapping": [ - ["delegate_ss58", "AccountId"], - ["take", "Compact"], - ["nominators", "Vec<(AccountId, Compact)>"], - ["owner_ss58", "AccountId"], - ["registrations", "Vec>"], - ["validator_permits", "Vec>"], - ["return_per_1000", "Compact"], - ["total_daily_return", "Compact"], - ], - }, - "NeuronInfo": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["uid", "Compact"], - ["netuid", "Compact"], - ["active", "bool"], - ["axon_info", "axon_info"], - ["prometheus_info", "PrometheusInfo"], - ["stake", "Vec<(AccountId, Compact)>"], - ["rank", "Compact"], - ["emission", "Compact"], - ["incentive", "Compact"], - ["consensus", "Compact"], - ["trust", "Compact"], - ["validator_trust", "Compact"], - ["dividends", "Compact"], - ["last_update", "Compact"], - ["validator_permit", "bool"], - ["weights", "Vec<(Compact, Compact)>"], - ["bonds", "Vec<(Compact, Compact)>"], - ["pruning_score", "Compact"], - ], - }, - "NeuronInfoLite": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["uid", "Compact"], - ["netuid", "Compact"], - ["active", "bool"], - ["axon_info", "axon_info"], - ["prometheus_info", "PrometheusInfo"], - ["stake", "Vec<(AccountId, Compact)>"], - ["rank", "Compact"], - ["emission", "Compact"], - ["incentive", "Compact"], - ["consensus", "Compact"], - ["trust", "Compact"], - ["validator_trust", "Compact"], - ["dividends", "Compact"], - ["last_update", "Compact"], - ["validator_permit", "bool"], - ["pruning_score", "Compact"], - ], - }, - "axon_info": { - "type": "struct", - "type_mapping": [ - ["block", "u64"], - ["version", "u32"], - ["ip", "u128"], - ["port", "u16"], - ["ip_type", "u8"], - ["protocol", "u8"], - ["placeholder1", "u8"], - ["placeholder2", "u8"], - ], - }, - "PrometheusInfo": { - "type": "struct", - "type_mapping": [ - ["block", "u64"], - ["version", "u32"], - ["ip", "u128"], - ["port", "u16"], - ["ip_type", "u8"], - ], - }, - "IPInfo": { - "type": "struct", - "type_mapping": [ - ["ip", "Compact"], - ["ip_type_and_protocol", "Compact"], - ], - }, - "StakeInfo": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["stake", "Compact"], - ], - }, - "SubnetHyperparameters": { - "type": "struct", - "type_mapping": [ - ["rho", "Compact"], - ["kappa", "Compact"], - ["immunity_period", "Compact"], - ["min_allowed_weights", "Compact"], - ["max_weights_limit", "Compact"], - ["tempo", "Compact"], - ["min_difficulty", "Compact"], - ["max_difficulty", "Compact"], - ["weights_version", "Compact"], - ["weights_rate_limit", "Compact"], - ["adjustment_interval", "Compact"], - ["activity_cutoff", "Compact"], - ["registration_allowed", "bool"], - ["target_regs_per_interval", "Compact"], - ["min_burn", "Compact"], - ["max_burn", "Compact"], - ["bonds_moving_avg", "Compact"], - ["max_regs_per_block", "Compact"], - ["serving_rate_limit", "Compact"], - ["max_validators", "Compact"], - ["adjustment_alpha", "Compact"], - ["difficulty", "Compact"], - ["commit_reveal_weights_interval", "Compact"], - ["commit_reveal_weights_enabled", "bool"], - ["alpha_high", "Compact"], - ["alpha_low", "Compact"], - ["liquid_alpha_enabled", "bool"], - ], - }, - "ScheduledColdkeySwapInfo": { - "type": "struct", - "type_mapping": [ - ["old_coldkey", "AccountId"], - ["new_coldkey", "AccountId"], - ["arbitration_block", "Compact"], - ], - }, - } -} - - -@dataclass -class AxonInfo: - version: int - ip: str - port: int - ip_type: int - hotkey: str - coldkey: str - protocol: int = 4 - placeholder1: int = 0 - placeholder2: int = 0 - - @property - def is_serving(self) -> bool: - """True if the endpoint is serving.""" - return self.ip != "0.0.0.0" - - def ip_str(self) -> str: - """Return the whole IP as string""" - return net.ip__str__(self.ip_type, self.ip, self.port) - - def __eq__(self, other: "AxonInfo"): - if other is None: - return False - - if ( - self.version == other.version - and self.ip == other.ip - and self.port == other.port - and self.ip_type == other.ip_type - and self.coldkey == other.coldkey - and self.hotkey == other.hotkey - ): - return True - - return False - - def __str__(self): - return "AxonInfo( {}, {}, {}, {} )".format( - str(self.ip_str()), str(self.hotkey), str(self.coldkey), self.version - ) - - def __repr__(self): - return self.__str__() - - def to_string(self) -> str: - """Converts the AxonInfo object to a string representation using JSON.""" - try: - return json.dumps(asdict(self)) - except (TypeError, ValueError) as e: - bittensor.logging.error(f"Error converting AxonInfo to string: {e}") - return AxonInfo(0, "", 0, 0, "", "").to_string() - - @classmethod - def from_string(cls, json_string: str) -> "AxonInfo": - """ - Creates an AxonInfo object from its string representation using JSON. - - Args: - json_string (str): The JSON string representation of the AxonInfo object. - - Returns: - AxonInfo: An instance of AxonInfo created from the JSON string. If decoding fails, returns a default AxonInfo object with default values. - - Raises: - json.JSONDecodeError: If there is an error in decoding the JSON string. - TypeError: If there is a type error when creating the AxonInfo object. - ValueError: If there is a value error when creating the AxonInfo object. - """ - try: - data = json.loads(json_string) - return cls(**data) - except json.JSONDecodeError as e: - bittensor.logging.error(f"Error decoding JSON: {e}") - except TypeError as e: - bittensor.logging.error(f"Type error: {e}") - except ValueError as e: - bittensor.logging.error(f"Value error: {e}") - return AxonInfo(0, "", 0, 0, "", "") - - @classmethod - def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": - """ - Converts a dictionary to an AxonInfo object. - - Args: - neuron_info (dict): A dictionary containing the neuron information. - - Returns: - instance (AxonInfo): An instance of AxonInfo created from the dictionary. - """ - return cls( - version=neuron_info["axon_info"]["version"], - ip=net.int_to_ip(int(neuron_info["axon_info"]["ip"])), - port=neuron_info["axon_info"]["port"], - ip_type=neuron_info["axon_info"]["ip_type"], - hotkey=neuron_info["hotkey"], - coldkey=neuron_info["coldkey"], - ) - - def to_parameter_dict( - self, - ) -> Union[dict[str, Union[int, str]], "torch.nn.ParameterDict"]: - """Returns a torch tensor or dict of the subnet info, depending on the USE_TORCH flag set.""" - if use_torch(): - return torch.nn.ParameterDict(self.__dict__) - else: - return self.__dict__ - - @classmethod - def from_parameter_dict( - cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] - ) -> "AxonInfo": - """Returns an axon_info object from a torch parameter_dict or a parameter dict.""" - if use_torch(): - return cls(**dict(parameter_dict)) - else: - return cls(**parameter_dict) - - -class ChainDataType(Enum): - NeuronInfo = 1 - SubnetInfo = 2 - DelegateInfo = 3 - NeuronInfoLite = 4 - DelegatedInfo = 5 - StakeInfo = 6 - IPInfo = 7 - SubnetHyperparameters = 8 - ScheduledColdkeySwapInfo = 9 - - -def from_scale_encoding( - input_: Union[List[int], bytes, ScaleBytes], - type_name: ChainDataType, - is_vec: bool = False, - is_option: bool = False, -) -> Optional[Dict]: - """ - Decodes input_ data from SCALE encoding based on the specified type name and modifiers. - - Args: - input_ (Union[List[int], bytes, ScaleBytes]): The input_ data to decode. - type_name (ChainDataType): The type of data being decoded. - is_vec (bool, optional): Whether the data is a vector of the specified type. Default is ``False``. - is_option (bool, optional): Whether the data is an optional value of the specified type. Default is ``False``. - - Returns: - Optional[Dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. - """ - type_string = type_name.name - if type_name == ChainDataType.DelegatedInfo: - # DelegatedInfo is a tuple of (DelegateInfo, Compact) - type_string = f"({ChainDataType.DelegateInfo.name}, Compact)" - if is_option: - type_string = f"Option<{type_string}>" - if is_vec: - type_string = f"Vec<{type_string}>" - - return from_scale_encoding_using_type_string(input_, type_string) - - -def from_scale_encoding_using_type_string( - input_: Union[List[int], bytes, ScaleBytes], type_string: str -) -> Optional[Dict]: - if isinstance(input_, ScaleBytes): - as_scale_bytes = input_ - else: - if isinstance(input_, list) and all([isinstance(i, int) for i in input_]): - vec_u8 = input_ - as_bytes = bytes(vec_u8) - elif isinstance(input_, bytes): - as_bytes = input_ - else: - raise TypeError("input_ must be a List[int], bytes, or ScaleBytes") - - as_scale_bytes = ScaleBytes(as_bytes) - - rpc_runtime_config = RuntimeConfiguration() - rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) - rpc_runtime_config.update_type_registry(custom_rpc_type_registry) - - obj = rpc_runtime_config.create_scale_object(type_string, data=as_scale_bytes) - - return obj.decode() - - -# Dataclasses for chain data. -@dataclass -class NeuronInfo: - """Dataclass for neuron metadata.""" - - hotkey: str - coldkey: str - uid: int - netuid: int - active: int - stake: Balance - # mapping of coldkey to amount staked to this Neuron - stake_dict: Dict[str, Balance] - total_stake: Balance - rank: float - emission: float - incentive: float - consensus: float - trust: float - validator_trust: float - dividends: float - last_update: int - validator_permit: bool - weights: List[List[int]] - bonds: List[List[int]] - pruning_score: int - prometheus_info: Optional["PrometheusInfo"] = None - axon_info: Optional[AxonInfo] = None - is_null: bool = False - - @classmethod - def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfo": - """Fixes the values of the NeuronInfo object.""" - neuron_info_decoded["hotkey"] = ss58_encode( - neuron_info_decoded["hotkey"], bittensor.__ss58_format__ - ) - neuron_info_decoded["coldkey"] = ss58_encode( - neuron_info_decoded["coldkey"], bittensor.__ss58_format__ - ) - stake_dict = { - ss58_encode(coldkey, bittensor.__ss58_format__): Balance.from_rao( - int(stake) - ) - for coldkey, stake in neuron_info_decoded["stake"] - } - neuron_info_decoded["stake_dict"] = stake_dict - neuron_info_decoded["stake"] = sum(stake_dict.values()) - neuron_info_decoded["total_stake"] = neuron_info_decoded["stake"] - neuron_info_decoded["weights"] = [ - [int(weight[0]), int(weight[1])] - for weight in neuron_info_decoded["weights"] - ] - neuron_info_decoded["bonds"] = [ - [int(bond[0]), int(bond[1])] for bond in neuron_info_decoded["bonds"] - ] - neuron_info_decoded["rank"] = U16_NORMALIZED_FLOAT(neuron_info_decoded["rank"]) - neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO - neuron_info_decoded["incentive"] = U16_NORMALIZED_FLOAT( - neuron_info_decoded["incentive"] - ) - neuron_info_decoded["consensus"] = U16_NORMALIZED_FLOAT( - neuron_info_decoded["consensus"] - ) - neuron_info_decoded["trust"] = U16_NORMALIZED_FLOAT( - neuron_info_decoded["trust"] - ) - neuron_info_decoded["validator_trust"] = U16_NORMALIZED_FLOAT( - neuron_info_decoded["validator_trust"] - ) - neuron_info_decoded["dividends"] = U16_NORMALIZED_FLOAT( - neuron_info_decoded["dividends"] - ) - neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values( - neuron_info_decoded["prometheus_info"] - ) - neuron_info_decoded["axon_info"] = AxonInfo.from_neuron_info( - neuron_info_decoded - ) - return cls(**neuron_info_decoded) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> "NeuronInfo": - """Returns a NeuronInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return NeuronInfo.get_null_neuron() - - decoded = from_scale_encoding(vec_u8, ChainDataType.NeuronInfo) - if decoded is None: - return NeuronInfo.get_null_neuron() - - return NeuronInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["NeuronInfo"]: - """Returns a list of NeuronInfo objects from a ``vec_u8``""" - - decoded_list = from_scale_encoding( - vec_u8, ChainDataType.NeuronInfo, is_vec=True - ) - if decoded_list is None: - return [] - - decoded_list = [ - NeuronInfo.fix_decoded_values(decoded) for decoded in decoded_list - ] - return decoded_list - - @staticmethod - def get_null_neuron() -> "NeuronInfo": - neuron = NeuronInfo( - uid=0, - netuid=0, - active=0, - stake=Balance.from_rao(0), - stake_dict={}, - total_stake=Balance.from_rao(0), - rank=0, - emission=0, - incentive=0, - consensus=0, - trust=0, - validator_trust=0, - dividends=0, - last_update=0, - validator_permit=False, - weights=[], - bonds=[], - prometheus_info=None, - axon_info=None, - is_null=True, - coldkey="000000000000000000000000000000000000000000000000", - hotkey="000000000000000000000000000000000000000000000000", - pruning_score=0, - ) - return neuron - - @classmethod - def from_weights_bonds_and_neuron_lite( - cls, - neuron_lite: "NeuronInfoLite", - weights_as_dict: Dict[int, List[Tuple[int, int]]], - bonds_as_dict: Dict[int, List[Tuple[int, int]]], - ) -> "NeuronInfo": - n_dict = neuron_lite.__dict__ - n_dict["weights"] = weights_as_dict.get(neuron_lite.uid, []) - n_dict["bonds"] = bonds_as_dict.get(neuron_lite.uid, []) - - return cls(**n_dict) - - -@dataclass -class NeuronInfoLite: - """Dataclass for neuron metadata, but without the weights and bonds.""" - - hotkey: str - coldkey: str - uid: int - netuid: int - active: int - stake: Balance - # mapping of coldkey to amount staked to this Neuron - stake_dict: Dict[str, Balance] - total_stake: Balance - rank: float - emission: float - incentive: float - consensus: float - trust: float - validator_trust: float - dividends: float - last_update: int - validator_permit: bool - prometheus_info: Optional["PrometheusInfo"] - axon_info: "axon_info" - pruning_score: int - is_null: bool = False - - @classmethod - def fix_decoded_values(cls, neuron_info_decoded: Any) -> "NeuronInfoLite": - """Fixes the values of the NeuronInfoLite object.""" - neuron_info_decoded["hotkey"] = ss58_encode( - neuron_info_decoded["hotkey"], bittensor.__ss58_format__ - ) - neuron_info_decoded["coldkey"] = ss58_encode( - neuron_info_decoded["coldkey"], bittensor.__ss58_format__ - ) - stake_dict = { - ss58_encode(coldkey, bittensor.__ss58_format__): Balance.from_rao( - int(stake) - ) - for coldkey, stake in neuron_info_decoded["stake"] - } - neuron_info_decoded["stake_dict"] = stake_dict - neuron_info_decoded["stake"] = sum(stake_dict.values()) - neuron_info_decoded["total_stake"] = neuron_info_decoded["stake"] - neuron_info_decoded["rank"] = U16_NORMALIZED_FLOAT(neuron_info_decoded["rank"]) - neuron_info_decoded["emission"] = neuron_info_decoded["emission"] / RAOPERTAO - neuron_info_decoded["incentive"] = U16_NORMALIZED_FLOAT( - neuron_info_decoded["incentive"] - ) - neuron_info_decoded["consensus"] = U16_NORMALIZED_FLOAT( - neuron_info_decoded["consensus"] - ) - neuron_info_decoded["trust"] = U16_NORMALIZED_FLOAT( - neuron_info_decoded["trust"] - ) - neuron_info_decoded["validator_trust"] = U16_NORMALIZED_FLOAT( - neuron_info_decoded["validator_trust"] - ) - neuron_info_decoded["dividends"] = U16_NORMALIZED_FLOAT( - neuron_info_decoded["dividends"] - ) - neuron_info_decoded["prometheus_info"] = PrometheusInfo.fix_decoded_values( - neuron_info_decoded["prometheus_info"] - ) - neuron_info_decoded["axon_info"] = AxonInfo.from_neuron_info( - neuron_info_decoded - ) - return cls(**neuron_info_decoded) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> "NeuronInfoLite": - """Returns a NeuronInfoLite object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return NeuronInfoLite.get_null_neuron() - - decoded = from_scale_encoding(vec_u8, ChainDataType.NeuronInfoLite) - if decoded is None: - return NeuronInfoLite.get_null_neuron() - - return NeuronInfoLite.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["NeuronInfoLite"]: - """Returns a list of NeuronInfoLite objects from a ``vec_u8``.""" - - decoded_list = from_scale_encoding( - vec_u8, ChainDataType.NeuronInfoLite, is_vec=True - ) - if decoded_list is None: - return [] - - decoded_list = [ - NeuronInfoLite.fix_decoded_values(decoded) for decoded in decoded_list - ] - return decoded_list - - @staticmethod - def get_null_neuron() -> "NeuronInfoLite": - neuron = NeuronInfoLite( - uid=0, - netuid=0, - active=0, - stake=Balance.from_rao(0), - stake_dict={}, - total_stake=Balance.from_rao(0), - rank=0, - emission=0, - incentive=0, - consensus=0, - trust=0, - validator_trust=0, - dividends=0, - last_update=0, - validator_permit=False, - prometheus_info=None, - axon_info=None, - is_null=True, - coldkey="000000000000000000000000000000000000000000000000", - hotkey="000000000000000000000000000000000000000000000000", - pruning_score=0, - ) - return neuron - - -@dataclass -class PrometheusInfo: - """Dataclass for prometheus info.""" - - block: int - version: int - ip: str - port: int - ip_type: int - - @classmethod - def fix_decoded_values(cls, prometheus_info_decoded: Dict) -> "PrometheusInfo": - """Returns a PrometheusInfo object from a prometheus_info_decoded dictionary.""" - prometheus_info_decoded["ip"] = net.int_to_ip( - int(prometheus_info_decoded["ip"]) - ) - - return cls(**prometheus_info_decoded) - - -@dataclass -class DelegateInfoLite: - """ - Dataclass for DelegateLiteInfo. This is a lighter version of :func:`DelegateInfo`. - - Args: - delegate_ss58 (str): Hotkey of the delegate for which the information is being fetched. - take (float): Take of the delegate as a percentage. - nominators (int): Count of the nominators of the delegate. - owner_ss58 (str): Coldkey of the owner. - registrations (list[int]): List of subnets that the delegate is registered on. - validator_permits (list[int]): List of subnets that the delegate is allowed to validate on. - return_per_1000 (int): Return per 1000 TAO, for the delegate over a day. - total_daily_return (int): Total daily return of the delegate. - """ - - delegate_ss58: str # Hotkey of delegate - take: float # Take of the delegate as a percentage - nominators: int # Count of the nominators of the delegate. - owner_ss58: str # Coldkey of owner - registrations: list[int] # List of subnets that the delegate is registered on - validator_permits: list[ - int - ] # List of subnets that the delegate is allowed to validate on - return_per_1000: int # Return per 1000 tao for the delegate over a day - total_daily_return: int # Total daily return of the delegate - - -@dataclass -class DelegateInfo: - """ - Dataclass for delegate information. For a lighter version of this class, see :func:`DelegateInfoLite`. - - Args: - hotkey_ss58 (str): Hotkey of the delegate for which the information is being fetched. - total_stake (int): Total stake of the delegate. - nominators (list[Tuple[str, int]]): List of nominators of the delegate and their stake. - take (float): Take of the delegate as a percentage. - owner_ss58 (str): Coldkey of the owner. - registrations (list[int]): List of subnets that the delegate is registered on. - validator_permits (list[int]): List of subnets that the delegate is allowed to validate on. - return_per_1000 (int): Return per 1000 TAO, for the delegate over a day. - total_daily_return (int): Total daily return of the delegate. - - """ - - hotkey_ss58: str # Hotkey of delegate - total_stake: Balance # Total stake of the delegate - nominators: List[ - Tuple[str, Balance] - ] # List of nominators of the delegate and their stake - owner_ss58: str # Coldkey of owner - take: float # Take of the delegate as a percentage - validator_permits: List[ - int - ] # List of subnets that the delegate is allowed to validate on - registrations: List[int] # List of subnets that the delegate is registered on - return_per_1000: Balance # Return per 1000 tao of the delegate over a day - total_daily_return: Balance # Total daily return of the delegate - - @classmethod - def fix_decoded_values(cls, decoded: Any) -> "DelegateInfo": - """Fixes the decoded values.""" - - return cls( - hotkey_ss58=ss58_encode( - decoded["delegate_ss58"], bittensor.__ss58_format__ - ), - owner_ss58=ss58_encode(decoded["owner_ss58"], bittensor.__ss58_format__), - take=U16_NORMALIZED_FLOAT(decoded["take"]), - nominators=[ - ( - ss58_encode(nom[0], bittensor.__ss58_format__), - Balance.from_rao(nom[1]), - ) - for nom in decoded["nominators"] - ], - total_stake=Balance.from_rao( - sum([nom[1] for nom in decoded["nominators"]]) - ), - validator_permits=decoded["validator_permits"], - registrations=decoded["registrations"], - return_per_1000=Balance.from_rao(decoded["return_per_1000"]), - total_daily_return=Balance.from_rao(decoded["total_daily_return"]), - ) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["DelegateInfo"]: - """Returns a DelegateInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfo) - if decoded is None: - return None - - return DelegateInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["DelegateInfo"]: - """Returns a list of DelegateInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfo, is_vec=True) - - if decoded is None: - return [] - - return [DelegateInfo.fix_decoded_values(d) for d in decoded] - - @classmethod - def delegated_list_from_vec_u8( - cls, vec_u8: List[int] - ) -> List[Tuple["DelegateInfo", Balance]]: - """Returns a list of Tuples of DelegateInfo objects, and Balance, from a ``vec_u8``. - - This is the list of delegates that the user has delegated to, and the amount of stake delegated. - """ - decoded = from_scale_encoding(vec_u8, ChainDataType.DelegatedInfo, is_vec=True) - if decoded is None: - return [] - - return [ - (DelegateInfo.fix_decoded_values(d), Balance.from_rao(s)) - for d, s in decoded - ] - - -@dataclass -class StakeInfo: - """Dataclass for stake info.""" - - hotkey_ss58: str # Hotkey address - coldkey_ss58: str # Coldkey address - stake: Balance # Stake for the hotkey-coldkey pair - - @classmethod - def fix_decoded_values(cls, decoded: Any) -> "StakeInfo": - """Fixes the decoded values.""" - return cls( - hotkey_ss58=ss58_encode(decoded["hotkey"], bittensor.__ss58_format__), - coldkey_ss58=ss58_encode(decoded["coldkey"], bittensor.__ss58_format__), - stake=Balance.from_rao(decoded["stake"]), - ) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["StakeInfo"]: - """Returns a StakeInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo) - if decoded is None: - return None - - return StakeInfo.fix_decoded_values(decoded) - - @classmethod - def list_of_tuple_from_vec_u8( - cls, vec_u8: List[int] - ) -> Dict[str, List["StakeInfo"]]: - """Returns a list of StakeInfo objects from a ``vec_u8``.""" - decoded: Optional[list[tuple[str, list[object]]]] = ( - from_scale_encoding_using_type_string( - input_=vec_u8, type_string="Vec<(AccountId, Vec)>" - ) - ) - - if decoded is None: - return {} - - return { - ss58_encode(address=account_id, ss58_format=bittensor.__ss58_format__): [ - StakeInfo.fix_decoded_values(d) for d in stake_info - ] - for account_id, stake_info in decoded - } - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["StakeInfo"]: - """Returns a list of StakeInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo, is_vec=True) - if decoded is None: - return [] - - return [StakeInfo.fix_decoded_values(d) for d in decoded] - - -@dataclass -class SubnetInfo: - """Dataclass for subnet info.""" - - netuid: int - rho: int - kappa: int - difficulty: int - immunity_period: int - max_allowed_validators: int - min_allowed_weights: int - max_weight_limit: float - scaling_law_power: float - subnetwork_n: int - max_n: int - blocks_since_epoch: int - tempo: int - modality: int - # netuid -> topk percentile prunning score requirement (u16:MAX normalized.) - connection_requirements: Dict[str, float] - emission_value: float - burn: Balance - owner_ss58: str - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetInfo"]: - """Returns a SubnetInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetInfo) - if decoded is None: - return None - - return SubnetInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["SubnetInfo"]: - r"""Returns a list of SubnetInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.SubnetInfo, is_vec=True, is_option=True - ) - - if decoded is None: - return [] - - return [SubnetInfo.fix_decoded_values(d) for d in decoded] - - @classmethod - def fix_decoded_values(cls, decoded: Dict) -> "SubnetInfo": - """Returns a SubnetInfo object from a decoded SubnetInfo dictionary.""" - return SubnetInfo( - netuid=decoded["netuid"], - rho=decoded["rho"], - kappa=decoded["kappa"], - difficulty=decoded["difficulty"], - immunity_period=decoded["immunity_period"], - max_allowed_validators=decoded["max_allowed_validators"], - min_allowed_weights=decoded["min_allowed_weights"], - max_weight_limit=decoded["max_weights_limit"], - # adjustment_alpha=decoded["adjustment_alpha"], - # bonds_moving_avg=decoded["bonds_moving_average"], - scaling_law_power=decoded["scaling_law_power"], - subnetwork_n=decoded["subnetwork_n"], - max_n=decoded["max_allowed_uids"], - blocks_since_epoch=decoded["blocks_since_last_step"], - tempo=decoded["tempo"], - modality=decoded["network_modality"], - connection_requirements={ - str(int(netuid)): U16_NORMALIZED_FLOAT(int(req)) - for netuid, req in decoded["network_connect"] - }, - emission_value=decoded["emission_values"], - burn=Balance.from_rao(decoded["burn"]), - owner_ss58=ss58_encode(decoded["owner"], bittensor.__ss58_format__), - ) - - def to_parameter_dict(self) -> Union[dict[str, Any], "torch.nn.ParameterDict"]: - """Returns a torch tensor or dict of the subnet info.""" - if use_torch(): - return torch.nn.ParameterDict(self.__dict__) - else: - return self.__dict__ - - @classmethod - def from_parameter_dict( - cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] - ) -> "SubnetInfo": - if use_torch(): - return cls(**dict(parameter_dict)) - else: - return cls(**parameter_dict) - - -@dataclass -class SubnetHyperparameters: - """Dataclass for subnet hyperparameters.""" - - rho: int - kappa: int - immunity_period: int - min_allowed_weights: int - max_weight_limit: float - tempo: int - min_difficulty: int - max_difficulty: int - weights_version: int - weights_rate_limit: int - adjustment_interval: int - activity_cutoff: int - registration_allowed: bool - target_regs_per_interval: int - min_burn: int - max_burn: int - bonds_moving_avg: int - max_regs_per_block: int - serving_rate_limit: int - max_validators: int - adjustment_alpha: int - difficulty: int - commit_reveal_weights_interval: int - commit_reveal_weights_enabled: bool - alpha_high: int - alpha_low: int - liquid_alpha_enabled: bool - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["SubnetHyperparameters"]: - """Returns a SubnetHyperparameters object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetHyperparameters) - if decoded is None: - return None - - return SubnetHyperparameters.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["SubnetHyperparameters"]: - """Returns a list of SubnetHyperparameters objects from a ``vec_u8``.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.SubnetHyperparameters, is_vec=True, is_option=True - ) - if decoded is None: - return [] - - return [SubnetHyperparameters.fix_decoded_values(d) for d in decoded] - - @classmethod - def fix_decoded_values(cls, decoded: Dict) -> "SubnetHyperparameters": - """Returns a SubnetInfo object from a decoded SubnetInfo dictionary.""" - return SubnetHyperparameters( - rho=decoded["rho"], - kappa=decoded["kappa"], - immunity_period=decoded["immunity_period"], - min_allowed_weights=decoded["min_allowed_weights"], - max_weight_limit=decoded["max_weights_limit"], - tempo=decoded["tempo"], - min_difficulty=decoded["min_difficulty"], - max_difficulty=decoded["max_difficulty"], - weights_version=decoded["weights_version"], - weights_rate_limit=decoded["weights_rate_limit"], - adjustment_interval=decoded["adjustment_interval"], - activity_cutoff=decoded["activity_cutoff"], - registration_allowed=decoded["registration_allowed"], - target_regs_per_interval=decoded["target_regs_per_interval"], - min_burn=decoded["min_burn"], - max_burn=decoded["max_burn"], - max_regs_per_block=decoded["max_regs_per_block"], - max_validators=decoded["max_validators"], - serving_rate_limit=decoded["serving_rate_limit"], - bonds_moving_avg=decoded["bonds_moving_avg"], - adjustment_alpha=decoded["adjustment_alpha"], - difficulty=decoded["difficulty"], - commit_reveal_weights_interval=decoded["commit_reveal_weights_interval"], - commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], - alpha_high=decoded["alpha_high"], - alpha_low=decoded["alpha_low"], - liquid_alpha_enabled=decoded["liquid_alpha_enabled"], - ) - - def to_parameter_dict( - self, - ) -> Union[dict[str, Union[int, float, bool]], "torch.nn.ParameterDict"]: - """Returns a torch tensor or dict of the subnet hyperparameters.""" - if use_torch(): - return torch.nn.ParameterDict(self.__dict__) - else: - return self.__dict__ - - @classmethod - def from_parameter_dict( - cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] - ) -> "SubnetHyperparameters": - if use_torch(): - return cls(**dict(parameter_dict)) - else: - return cls(**parameter_dict) - - -@dataclass -class IPInfo: - """Dataclass for associated IP Info.""" - - ip: str - ip_type: int - protocol: int - - def encode(self) -> Dict[str, Any]: - """Returns a dictionary of the IPInfo object that can be encoded.""" - return { - "ip": net.ip_to_int( - self.ip - ), # IP type and protocol are encoded together as a u8 - "ip_type_and_protocol": ((self.ip_type << 4) + self.protocol) & 0xFF, - } - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["IPInfo"]: - """Returns a IPInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.IPInfo) - if decoded is None: - return None - - return IPInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["IPInfo"]: - r"""Returns a list of IPInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding(vec_u8, ChainDataType.IPInfo, is_vec=True) - - if decoded is None: - return [] - - return [IPInfo.fix_decoded_values(d) for d in decoded] - - @classmethod - def fix_decoded_values(cls, decoded: Dict) -> "IPInfo": - """Returns a SubnetInfo object from a decoded IPInfo dictionary.""" - return IPInfo( - ip=net.int_to_ip(decoded["ip"]), - ip_type=decoded["ip_type_and_protocol"] >> 4, - protocol=decoded["ip_type_and_protocol"] & 0xF, - ) - - def to_parameter_dict( - self, - ) -> Union[dict[str, Union[str, int]], "torch.nn.ParameterDict"]: - """Returns a torch tensor or dict of the subnet IP info.""" - if use_torch(): - return torch.nn.ParameterDict(self.__dict__) - else: - return self.__dict__ - - @classmethod - def from_parameter_dict( - cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] - ) -> "IPInfo": - if use_torch(): - return cls(**dict(parameter_dict)) - else: - return cls(**parameter_dict) - - -# Senate / Proposal data -class ProposalVoteData(TypedDict): - index: int - threshold: int - ayes: List[str] - nays: List[str] - end: int - - -ProposalCallData = GenericCall - - -@dataclass -class ScheduledColdkeySwapInfo: - """Dataclass for scheduled coldkey swap information.""" - - old_coldkey: str - new_coldkey: str - arbitration_block: int - - @classmethod - def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": - """Fixes the decoded values.""" - return cls( - old_coldkey=ss58_encode(decoded["old_coldkey"], bittensor.__ss58_format__), - new_coldkey=ss58_encode(decoded["new_coldkey"], bittensor.__ss58_format__), - arbitration_block=decoded["arbitration_block"], - ) - - @classmethod - def from_vec_u8(cls, vec_u8: List[int]) -> Optional["ScheduledColdkeySwapInfo"]: - """Returns a ScheduledColdkeySwapInfo object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo) - if decoded is None: - return None - - return ScheduledColdkeySwapInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: List[int]) -> List["ScheduledColdkeySwapInfo"]: - """Returns a list of ScheduledColdkeySwapInfo objects from a ``vec_u8``.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True - ) - if decoded is None: - return [] - - return [ScheduledColdkeySwapInfo.fix_decoded_values(d) for d in decoded] - - @classmethod - def decode_account_id_list(cls, vec_u8: List[int]) -> Optional[List[str]]: - """Decodes a list of AccountIds from vec_u8.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True - ) - if decoded is None: - return None - return [ - ss58_encode(account_id, bittensor.__ss58_format__) for account_id in decoded - ] diff --git a/bittensor/cli.py b/bittensor/cli.py deleted file mode 100644 index cc87c122e..000000000 --- a/bittensor/cli.py +++ /dev/null @@ -1,398 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 sys -import shtab -import argparse -import bittensor -from typing import List, Optional -from .commands import ( - AutocompleteCommand, - DelegateStakeCommand, - DelegateUnstakeCommand, - GetIdentityCommand, - GetWalletHistoryCommand, - InspectCommand, - ListCommand, - ListDelegatesCommand, - MetagraphCommand, - MyDelegatesCommand, - NewColdkeyCommand, - NewHotkeyCommand, - NominateCommand, - OverviewCommand, - PowRegisterCommand, - ProposalsCommand, - RegenColdkeyCommand, - RegenColdkeypubCommand, - RegenHotkeyCommand, - RegisterCommand, - RegisterSubnetworkCommand, - RootGetWeightsCommand, - RootList, - RootRegisterCommand, - RootSetBoostCommand, - RootSetSlashCommand, - RootSetWeightsCommand, - RunFaucetCommand, - SenateCommand, - SetIdentityCommand, - SetTakeCommand, - StakeCommand, - StakeShow, - SubnetGetHyperparamsCommand, - SubnetHyperparamsCommand, - SubnetListCommand, - SubnetLockCostCommand, - SubnetSudoCommand, - SwapHotkeyCommand, - TransferCommand, - UnStakeCommand, - UpdateCommand, - UpdateWalletCommand, - VoteCommand, - WalletBalanceCommand, - WalletCreateCommand, - CommitWeightCommand, - RevealWeightCommand, - CheckColdKeySwapCommand, - SetChildrenCommand, - GetChildrenCommand, - RevokeChildrenCommand, - SetChildKeyTakeCommand, - GetChildKeyTakeCommand, -) - -# Create a console instance for CLI display. -console = bittensor.__console__ - -ALIAS_TO_COMMAND = { - "subnets": "subnets", - "root": "root", - "wallet": "wallet", - "stake": "stake", - "sudo": "sudo", - "legacy": "legacy", - "s": "subnets", - "r": "root", - "w": "wallet", - "st": "stake", - "su": "sudo", - "l": "legacy", - "subnet": "subnets", - "roots": "root", - "wallets": "wallet", - "stakes": "stake", - "sudos": "sudo", - "i": "info", - "info": "info", - "weights": "weights", - "wt": "weights", - "weight": "weights", -} -COMMANDS = { - "subnets": { - "name": "subnets", - "aliases": ["s", "subnet"], - "help": "Commands for managing and viewing subnetworks.", - "commands": { - "list": SubnetListCommand, - "metagraph": MetagraphCommand, - "lock_cost": SubnetLockCostCommand, - "create": RegisterSubnetworkCommand, - "pow_register": PowRegisterCommand, - "register": RegisterCommand, - "hyperparameters": SubnetHyperparamsCommand, - }, - }, - "root": { - "name": "root", - "aliases": ["r", "roots"], - "help": "Commands for managing and viewing the root network.", - "commands": { - "list": RootList, - "weights": RootSetWeightsCommand, - "get_weights": RootGetWeightsCommand, - "boost": RootSetBoostCommand, - "slash": RootSetSlashCommand, - "senate_vote": VoteCommand, - "senate": SenateCommand, - "register": RootRegisterCommand, - "proposals": ProposalsCommand, - "set_take": SetTakeCommand, - "delegate": DelegateStakeCommand, - "undelegate": DelegateUnstakeCommand, - "my_delegates": MyDelegatesCommand, - "list_delegates": ListDelegatesCommand, - "nominate": NominateCommand, - }, - }, - "wallet": { - "name": "wallet", - "aliases": ["w", "wallets"], - "help": "Commands for managing and viewing wallets.", - "commands": { - "list": ListCommand, - "overview": OverviewCommand, - "transfer": TransferCommand, - "inspect": InspectCommand, - "balance": WalletBalanceCommand, - "create": WalletCreateCommand, - "new_hotkey": NewHotkeyCommand, - "new_coldkey": NewColdkeyCommand, - "regen_coldkey": RegenColdkeyCommand, - "regen_coldkeypub": RegenColdkeypubCommand, - "regen_hotkey": RegenHotkeyCommand, - "faucet": RunFaucetCommand, - "update": UpdateWalletCommand, - "swap_hotkey": SwapHotkeyCommand, - "set_identity": SetIdentityCommand, - "get_identity": GetIdentityCommand, - "history": GetWalletHistoryCommand, - "check_coldkey_swap": CheckColdKeySwapCommand, - }, - }, - "stake": { - "name": "stake", - "aliases": ["st", "stakes"], - "help": "Commands for staking and removing stake and setting child hotkey accounts.", - "commands": { - "show": StakeShow, - "add": StakeCommand, - "remove": UnStakeCommand, - "get_children": GetChildrenCommand, - "set_children": SetChildrenCommand, - "revoke_children": RevokeChildrenCommand, - "set_childkey_take": SetChildKeyTakeCommand, - "get_childkey_take": GetChildKeyTakeCommand, - }, - }, - "weights": { - "name": "weights", - "aliases": ["wt", "weight"], - "help": "Commands for managing weight for subnets.", - "commands": { - "commit": CommitWeightCommand, - "reveal": RevealWeightCommand, - }, - }, - "sudo": { - "name": "sudo", - "aliases": ["su", "sudos"], - "help": "Commands for subnet management", - "commands": { - # "dissolve": None, - "set": SubnetSudoCommand, - "get": SubnetGetHyperparamsCommand, - }, - }, - "legacy": { - "name": "legacy", - "aliases": ["l"], - "help": "Miscellaneous commands.", - "commands": { - "update": UpdateCommand, - "faucet": RunFaucetCommand, - }, - }, - "info": { - "name": "info", - "aliases": ["i"], - "help": "Instructions for enabling autocompletion for the CLI.", - "commands": { - "autocomplete": AutocompleteCommand, - }, - }, -} - - -class CLIErrorParser(argparse.ArgumentParser): - """ - Custom ArgumentParser for better error messages. - """ - - def error(self, message): - """ - This method is called when an error occurs. It prints a custom error message. - """ - sys.stderr.write(f"Error: {message}\n") - self.print_help() - sys.exit(2) - - -class cli: - """ - Implementation of the Command Line Interface (CLI) class for the Bittensor protocol. - This class handles operations like key management (hotkey and coldkey) and token transfer. - """ - - def __init__( - self, - config: Optional["bittensor.config"] = None, - args: Optional[List[str]] = None, - ): - """ - Initializes a bittensor.CLI object. - - Args: - config (bittensor.config, optional): The configuration settings for the CLI. - args (List[str], optional): List of command line arguments. - """ - # Turns on console for cli. - bittensor.turn_console_on() - - # If no config is provided, create a new one from args. - if config is None: - config = cli.create_config(args) - - self.config = config - if self.config.command in ALIAS_TO_COMMAND: - self.config.command = ALIAS_TO_COMMAND[self.config.command] - else: - console.print( - f":cross_mark:[red]Unknown command: {self.config.command}[/red]" - ) - sys.exit() - - # Check if the config is valid. - cli.check_config(self.config) - - # If no_version_checking is not set or set as False in the config, version checking is done. - if not self.config.get("no_version_checking", d=True): - try: - bittensor.utils.check_version() - except bittensor.utils.VersionCheckError: - # If version checking fails, inform user with an exception. - raise RuntimeError( - "To avoid internet-based version checking, pass --no_version_checking while running the CLI." - ) - - @staticmethod - def __create_parser__() -> "argparse.ArgumentParser": - """ - Creates the argument parser for the Bittensor CLI. - - Returns: - argparse.ArgumentParser: An argument parser object for Bittensor CLI. - """ - # Define the basic argument parser. - parser = CLIErrorParser( - description=f"bittensor cli v{bittensor.__version__}", - usage="btcli ", - add_help=True, - ) - # Add shtab completion - parser.add_argument( - "--print-completion", - choices=shtab.SUPPORTED_SHELLS, - help="Print shell tab completion script", - ) - # Add arguments for each sub-command. - cmd_parsers = parser.add_subparsers(dest="command") - # Add argument parsers for all available commands. - for command in COMMANDS.values(): - if isinstance(command, dict): - subcmd_parser = cmd_parsers.add_parser( - name=command["name"], - aliases=command["aliases"], - help=command["help"], - ) - subparser = subcmd_parser.add_subparsers( - help=command["help"], dest="subcommand", required=True - ) - - for subcommand in command["commands"].values(): - subcommand.add_args(subparser) - else: - command.add_args(cmd_parsers) - - return parser - - @staticmethod - def create_config(args: List[str]) -> "bittensor.config": - """ - From the argument parser, add config to bittensor.executor and local config - - Args: - args (List[str]): List of command line arguments. - - Returns: - bittensor.config: The configuration object for Bittensor CLI. - """ - parser = cli.__create_parser__() - - # If no arguments are passed, print help text and exit the program. - if len(args) == 0: - parser.print_help() - sys.exit() - - return bittensor.config(parser, args=args) - - @staticmethod - def check_config(config: "bittensor.config"): - """ - Checks if the essential configuration exists under different command - - Args: - config (bittensor.config): The configuration settings for the CLI. - """ - # Check if command exists, if so, run the corresponding check_config. - # If command doesn't exist, inform user and exit the program. - if config.command in COMMANDS: - command = config.command - command_data = COMMANDS[command] - - if isinstance(command_data, dict): - if config["subcommand"] is not None: - command_data["commands"][config["subcommand"]].check_config(config) - else: - console.print( - f":cross_mark:[red]Missing subcommand for: {config.command}[/red]" - ) - sys.exit(1) - else: - command_data.check_config(config) - else: - console.print(f":cross_mark:[red]Unknown command: {config.command}[/red]") - sys.exit(1) - - def run(self): - """ - Executes the command from the configuration. - """ - # Check for print-completion argument - if self.config.print_completion: - parser = cli.__create_parser__() - shell = self.config.print_completion - print(shtab.complete(parser, shell)) - return - - # Check if command exists, if so, run the corresponding method. - # If command doesn't exist, inform user and exit the program. - command = self.config.command - if command in COMMANDS: - command_data = COMMANDS[command] - - if isinstance(command_data, dict): - command_data["commands"][self.config["subcommand"]].run(self) - else: - command_data.run(self) - else: - console.print( - f":cross_mark:[red]Unknown command: {self.config.command}[/red]" - ) - sys.exit() diff --git a/bittensor/commands/__init__.py b/bittensor/commands/__init__.py deleted file mode 100644 index e1ac8c74c..000000000 --- a/bittensor/commands/__init__.py +++ /dev/null @@ -1,131 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2023 Opentensor Technologies Inc - -# 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. - -from munch import Munch, munchify - -defaults: Munch = munchify( - { - "netuid": 1, - "subtensor": {"network": "finney", "chain_endpoint": None, "_mock": False}, - "pow_register": { - "num_processes": None, - "update_interval": 50000, - "output_in_place": True, - "verbose": False, - "cuda": {"dev_id": [0], "use_cuda": False, "tpb": 256}, - }, - "axon": { - "port": 8091, - "ip": "[::]", - "external_port": None, - "external_ip": None, - "max_workers": 10, - "maximum_concurrent_rpcs": 400, - }, - "priority": {"max_workers": 5, "maxsize": 10}, - "prometheus": {"port": 7091, "level": "INFO"}, - "wallet": { - "name": "default", - "hotkey": "default", - "path": "~/.bittensor/wallets/", - }, - "dataset": { - "batch_size": 10, - "block_size": 20, - "num_workers": 0, - "dataset_names": "default", - "data_dir": "~/.bittensor/data/", - "save_dataset": False, - "max_datasets": 3, - "num_batches": 100, - }, - "logging": { - "debug": False, - "trace": False, - "record_log": False, - "logging_dir": "~/.bittensor/miners", - }, - } -) - -from .stake import ( - StakeCommand, - StakeShow, - SetChildrenCommand, - GetChildrenCommand, - SetChildKeyTakeCommand, - GetChildKeyTakeCommand, -) -from .unstake import UnStakeCommand, RevokeChildrenCommand -from .overview import OverviewCommand -from .register import ( - PowRegisterCommand, - RegisterCommand, - RunFaucetCommand, - SwapHotkeyCommand, -) -from .delegates import ( - NominateCommand, - ListDelegatesCommand, - DelegateStakeCommand, - DelegateUnstakeCommand, - MyDelegatesCommand, - SetTakeCommand, -) -from .wallets import ( - NewColdkeyCommand, - NewHotkeyCommand, - RegenColdkeyCommand, - RegenColdkeypubCommand, - RegenHotkeyCommand, - UpdateWalletCommand, - WalletCreateCommand, - WalletBalanceCommand, - GetWalletHistoryCommand, -) -from .weights import CommitWeightCommand, RevealWeightCommand -from .transfer import TransferCommand -from .inspect import InspectCommand -from .metagraph import MetagraphCommand -from .list import ListCommand -from .misc import UpdateCommand, AutocompleteCommand -from .senate import ( - SenateCommand, - ProposalsCommand, - ShowVotesCommand, - SenateRegisterCommand, - SenateLeaveCommand, - VoteCommand, -) -from .network import ( - RegisterSubnetworkCommand, - SubnetLockCostCommand, - SubnetListCommand, - SubnetSudoCommand, - SubnetHyperparamsCommand, - SubnetGetHyperparamsCommand, -) -from .root import ( - RootRegisterCommand, - RootList, - RootSetWeightsCommand, - RootGetWeightsCommand, - RootSetBoostCommand, - RootSetSlashCommand, -) -from .identity import GetIdentityCommand, SetIdentityCommand -from .check_coldkey_swap import CheckColdKeySwapCommand diff --git a/bittensor/commands/check_coldkey_swap.py b/bittensor/commands/check_coldkey_swap.py deleted file mode 100644 index 2b003e828..000000000 --- a/bittensor/commands/check_coldkey_swap.py +++ /dev/null @@ -1,128 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# -# 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 - -from rich.prompt import Prompt - -import bittensor -from bittensor.utils.formatting import convert_blocks_to_time -from . import defaults - -console = bittensor.__console__ - - -def fetch_arbitration_stats(subtensor, wallet): - """ - Performs a check of the current arbitration data (if any), and displays it through the bittensor console. - """ - arbitration_check = len( - subtensor.check_in_arbitration(wallet.coldkeypub.ss58_address) - ) - if arbitration_check == 0: - bittensor.__console__.print( - "[green]There has been no previous key swap initiated for your coldkey.[/green]" - ) - if arbitration_check == 1: - arbitration_remaining = subtensor.get_remaining_arbitration_period( - wallet.coldkeypub.ss58_address - ) - hours, minutes, seconds = convert_blocks_to_time(arbitration_remaining) - bittensor.__console__.print( - "[yellow]There has been 1 swap request made for this coldkey already." - " By adding another swap request, the key will enter arbitration." - f" Your key swap is scheduled for {hours} hours, {minutes} minutes, {seconds} seconds" - " from now.[/yellow]" - ) - if arbitration_check > 1: - bittensor.__console__.print( - f"[red]This coldkey is currently in arbitration with a total swaps of {arbitration_check}.[/red]" - ) - - -class CheckColdKeySwapCommand: - """ - Executes the ``check_coldkey_swap`` command to check swap status of a coldkey in the Bittensor network. - Usage: - Users need to specify the wallet they want to check the swap status of. - Example usage:: - btcli wallet check_coldkey_swap - Note: - This command is important for users who wish check if swap requests were made against their coldkey. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """ - Runs the check coldkey swap command. - Args: - cli (bittensor.cli): The CLI object containing configuration and command-line interface utilities. - """ - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - CheckColdKeySwapCommand._run(cli, subtensor) - except Exception as e: - bittensor.logging.warning(f"Failed to get swap status: {e}") - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - """ - Internal method to check coldkey swap status. - Args: - cli (bittensor.cli): The CLI object containing configuration and command-line interface utilities. - subtensor (bittensor.subtensor): The subtensor object for blockchain interactions. - """ - config = cli.config.copy() - wallet = bittensor.wallet(config=config) - - fetch_arbitration_stats(subtensor, wallet) - - @classmethod - def check_config(cls, config: "bittensor.config"): - """ - Checks and prompts for necessary configuration settings. - Args: - config (bittensor.config): The configuration object. - Prompts the user for wallet name if not set in the config. - """ - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name: str = Prompt.ask( - "Enter wallet name", default=defaults.wallet.name - ) - config.wallet.name = str(wallet_name) - - @staticmethod - def add_args(command_parser: argparse.ArgumentParser): - """ - Adds arguments to the command parser. - Args: - command_parser (argparse.ArgumentParser): The command parser to add arguments to. - """ - swap_parser = command_parser.add_parser( - "check_coldkey_swap", - help="""Check the status of swap requests for a coldkey on the Bittensor network. - Adding more than one swap request will make the key go into arbitration mode.""", - ) - bittensor.wallet.add_args(swap_parser) - bittensor.subtensor.add_args(swap_parser) diff --git a/bittensor/commands/delegates.py b/bittensor/commands/delegates.py deleted file mode 100644 index 9a6b389e5..000000000 --- a/bittensor/commands/delegates.py +++ /dev/null @@ -1,1167 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2023 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 os -import sys -from typing import List, Dict, Optional - -from rich.console import Text -from rich.prompt import Prompt, FloatPrompt, Confirm -from rich.table import Table -from substrateinterface.exceptions import SubstrateRequestException -from tqdm import tqdm - -import bittensor -from . import defaults -from .identity import SetIdentityCommand -from .utils import get_delegates_details, DelegatesDetails - - -def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: - try: - wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [bittensor.wallet(path=path, name=name) for name in wallet_names] - except StopIteration: - # No wallet files found. - wallets = [] - return wallets - - -console = bittensor.__console__ - - -def show_delegates_lite( - subtensor: "bittensor.subtensor", - delegates_lite: List["bittensor.DelegateInfoLite"], - width: Optional[int] = None, -): - """ - This method is a lite version of the :func:`show_delegates`. This method displays a formatted table of Bittensor network delegates with detailed statistics to the console. - - The table is sorted by total stake in descending order and provides - a snapshot of delegate performance and status, helping users make informed decisions for staking or nominating. - - This helper function is not intended to be used directly in user code unless specifically required. - - Args: - subtensor (bittensor.subtensor): The instance of the subtensor class. - delegates_lite (List[bittensor.DelegateInfoLite]): A list of delegate information objects to be displayed. - width (Optional[int]): The width of the console output table. Defaults to ``None``, which will make the table expand to the maximum width of the console. - - The output table contains the following columns. To display more columns, use the :func:`show_delegates` function. - - - INDEX: The numerical index of the delegate. - - DELEGATE: The name of the delegate. - - SS58: The truncated SS58 address of the delegate. - - NOMINATORS: The number of nominators supporting the delegate. - - VPERMIT: Validator permits held by the delegate for the subnets. - - TAKE: The percentage of the delegate's earnings taken by the network. - - DELEGATE/(24h): The earnings of the delegate in the last 24 hours. - - Desc: A brief description provided by the delegate. - - Usage: - This function is typically used within the Bittensor CLI to show current delegate options to users who are considering where to stake their tokens. - - Example usage:: - - show_delegates_lite(delegates_lite, width=80) - - Note: - This function is primarily for display purposes within a command-line interface and does not return any values. It relies on the `rich `_ Python library to render - the table in the console. - """ - - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(subtensor=subtensor) - ) - if registered_delegate_info is None: - bittensor.__console__.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - registered_delegate_info = {} - - table = Table(show_footer=True, width=width, pad_edge=False, box=None, expand=True) - table.add_column( - "[overline white]INDEX", - str(len(delegates_lite)), - footer_style="overline white", - style="bold white", - ) - table.add_column( - "[overline white]DELEGATE", - style="rgb(50,163,219)", - no_wrap=True, - justify="left", - ) - table.add_column( - "[overline white]SS58", - str(len(delegates_lite)), - footer_style="overline white", - style="bold yellow", - ) - table.add_column( - "[overline white]NOMINATORS", justify="center", style="green", no_wrap=True - ) - table.add_column("[overline white]VPERMIT", justify="right", no_wrap=False) - table.add_column("[overline white]TAKE", style="white", no_wrap=True) - table.add_column("[overline white]DELEGATE/(24h)", style="green", justify="center") - table.add_column("[overline white]Desc", style="rgb(50,163,219)") - - for i, d in enumerate(delegates_lite): - delegate_name = "" - delegate_url = "" - delegate_description = "" - if d.delegate_ss58 in registered_delegate_info: - delegate = registered_delegate_info[d.delegate_ss58] - - delegate_name = delegate.display - delegate_url = delegate.web - - if len(delegate.additional) > 0 and len(delegate.additional[0]) == 2: - delegate_description = delegate.additional[0][1] - - table.add_row( - # `INDEX` column - str(i), - # `DELEGATE` column - Text(delegate_name, style=f"link {delegate_url}"), - # `SS58` column - f"{d.delegate_ss58:8.8}...", - # `NOMINATORS` column - str(d.nominators), - # `VPERMIT` column - str(d.registrations), - # `TAKE` column - f"{d.take * 100:.1f}%", - # `DELEGATE/(24h)` column - f"τ{bittensor.Balance.from_tao(d.total_daily_return * 0.18) !s:6.6}", - # `Desc` column - str(delegate_description), - end_section=True, - ) - bittensor.__console__.print(table) - - -# Uses rich console to pretty print a table of delegates. -def show_delegates( - subtensor: "bittensor.subtensor", - delegates: List["bittensor.DelegateInfo"], - prev_delegates: Optional[List["bittensor.DelegateInfo"]], - width: Optional[int] = None, -): - """ - Displays a formatted table of Bittensor network delegates with detailed statistics to the console. - - The table is sorted by total stake in descending order and provides - a snapshot of delegate performance and status, helping users make informed decisions for staking or nominating. - - This is a helper function that is called by the :func:`list_delegates` and :func:`my_delegates`, and is not intended - to be used directly in user code unless specifically required. - - Args: - subtensor (bittensor.subtensor): The instance of the subtensor class. - delegates (List[bittensor.DelegateInfo]): A list of delegate information objects to be displayed. - prev_delegates (Optional[List[bittensor.DelegateInfo]]): A list of delegate information objects from a previous state, used to calculate changes in stake. Defaults to ``None``. - width (Optional[int]): The width of the console output table. Defaults to ``None``, which will make the table expand to the maximum width of the console. - - The output table contains the following columns: - - - INDEX: The numerical index of the delegate. - - DELEGATE: The name of the delegate. - - SS58: The truncated SS58 address of the delegate. - - NOMINATORS: The number of nominators supporting the delegate. - - DELEGATE STAKE(τ): The stake that is directly delegated to the delegate. - - TOTAL STAKE(τ): The total stake held by the delegate, including nominators' stake. - - CHANGE/(4h): The percentage change in the delegate's stake over the past 4 hours. - - VPERMIT: Validator permits held by the delegate for the subnets. - - TAKE: The percentage of the delegate's earnings taken by the network. - - NOMINATOR/(24h)/kτ: The earnings per 1000 τ staked by nominators in the last 24 hours. - - DELEGATE/(24h): The earnings of the delegate in the last 24 hours. - - Desc: A brief description provided by the delegate. - - Usage: - This function is typically used within the Bittensor CLI to show current delegate options to users who are considering where to stake their tokens. - - Example usage:: - - show_delegates(current_delegates, previous_delegates, width=80) - - Note: - This function is primarily for display purposes within a command-line interface and does - not return any values. It relies on the `rich `_ Python library to render - the table in the - console. - """ - - delegates.sort(key=lambda delegate: delegate.total_stake, reverse=True) - prev_delegates_dict = {} - if prev_delegates is not None: - for prev_delegate in prev_delegates: - prev_delegates_dict[prev_delegate.hotkey_ss58] = prev_delegate - - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(subtensor=subtensor) - ) - if registered_delegate_info is None: - bittensor.__console__.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - registered_delegate_info = {} - - table = Table(show_footer=True, width=width, pad_edge=False, box=None, expand=True) - table.add_column( - "[overline white]INDEX", - str(len(delegates)), - footer_style="overline white", - style="bold white", - ) - table.add_column( - "[overline white]DELEGATE", - style="rgb(50,163,219)", - no_wrap=True, - justify="left", - ) - table.add_column( - "[overline white]SS58", - str(len(delegates)), - footer_style="overline white", - style="bold yellow", - ) - table.add_column( - "[overline white]NOMINATORS", justify="center", style="green", no_wrap=True - ) - table.add_column( - "[overline white]DELEGATE STAKE(\u03c4)", justify="right", no_wrap=True - ) - table.add_column( - "[overline white]TOTAL STAKE(\u03c4)", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column("[overline white]CHANGE/(4h)", style="grey0", justify="center") - table.add_column("[overline white]VPERMIT", justify="right", no_wrap=False) - table.add_column("[overline white]TAKE", style="white", no_wrap=True) - table.add_column( - "[overline white]NOMINATOR/(24h)/k\u03c4", style="green", justify="center" - ) - table.add_column("[overline white]DELEGATE/(24h)", style="green", justify="center") - table.add_column("[overline white]Desc", style="rgb(50,163,219)") - - for i, delegate in enumerate(delegates): - owner_stake = next( - map( - lambda x: x[1], # get stake - filter( - lambda x: x[0] == delegate.owner_ss58, delegate.nominators - ), # filter for owner - ), - bittensor.Balance.from_rao(0), # default to 0 if no owner stake. - ) - - delegate_name = "" - delegate_url = "" - delegate_description = "" - - if delegate.hotkey_ss58 in registered_delegate_info: - delegate_ = registered_delegate_info[delegate.hotkey_ss58] - delegate_name = delegate_.display - delegate_url = delegate_.web - if len(delegate_.additional) > 0 and len(delegate_.additional[1]) == 2: - delegate_description = delegate_.additional[0][1] - - if delegate.hotkey_ss58 in prev_delegates_dict: - prev_stake = prev_delegates_dict[delegate.hotkey_ss58].total_stake - if prev_stake == 0: - rate_change_in_stake_str = "[green]100%[/green]" - else: - rate_change_in_stake = ( - 100 - * (float(delegate.total_stake) - float(prev_stake)) - / float(prev_stake) - ) - if rate_change_in_stake > 0: - rate_change_in_stake_str = "[green]{:.2f}%[/green]".format( - rate_change_in_stake - ) - elif rate_change_in_stake < 0: - rate_change_in_stake_str = "[red]{:.2f}%[/red]".format( - rate_change_in_stake - ) - else: - rate_change_in_stake_str = "[grey0]0%[/grey0]" - else: - rate_change_in_stake_str = "[grey0]NA[/grey0]" - - table.add_row( - # INDEX - str(i), - # DELEGATE - Text(delegate_name, style=f"link {delegate_url}"), - # SS58 - f"{delegate.hotkey_ss58:8.8}...", - # NOMINATORS - str(len([nom for nom in delegate.nominators if nom[1].rao > 0])), - # DELEGATE STAKE - f"{owner_stake!s:13.13}", - # TOTAL STAKE - f"{delegate.total_stake!s:13.13}", - # CHANGE/(4h) - rate_change_in_stake_str, - # VPERMIT - str(delegate.registrations), - # TAKE - f"{delegate.take * 100:.1f}%", - # NOMINATOR/(24h)/k - f"{bittensor.Balance.from_tao( delegate.total_daily_return.tao * (1000/ (0.001 + delegate.total_stake.tao)))!s:6.6}", - # DELEGATE/(24h) - f"{bittensor.Balance.from_tao(delegate.total_daily_return.tao * 0.18) !s:6.6}", - # Desc - str(delegate_description), - end_section=True, - ) - bittensor.__console__.print(table) - - -class DelegateStakeCommand: - """ - Executes the ``delegate`` command, which stakes Tao to a specified delegate on the Bittensor network. - - This action allocates the user's Tao to support a delegate, potentially earning staking rewards in return. - - Optional Arguments: - - ``wallet.name``: The name of the wallet to use for the command. - - ``delegate_ss58key``: The ``SS58`` address of the delegate to stake to. - - ``amount``: The amount of Tao to stake. - - ``all``: If specified, the command stakes all available Tao. - - The command interacts with the user to determine the delegate and the amount of Tao to be staked. If the ``--all`` - flag is used, it delegates the entire available balance. - - Usage: - The user must specify the delegate's SS58 address and the amount of Tao to stake. The function sends a - transaction to the subtensor network to delegate the specified amount to the chosen delegate. These values are - prompted if not provided. - - Example usage:: - - btcli delegate --delegate_ss58key --amount - btcli delegate --delegate_ss58key --all - - Note: - This command modifies the blockchain state and may incur transaction fees. It requires user confirmation and - interaction, and is designed to be used within the Bittensor CLI environment. The user should ensure the - delegate's address and the amount to be staked are correct before executing the command. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Delegates stake to a chain delegate.""" - try: - config = cli.config.copy() - wallet = bittensor.wallet(config=config) - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - subtensor.delegate( - wallet=wallet, - delegate_ss58=config.get("delegate_ss58key"), - amount=config.get("amount"), - wait_for_inclusion=True, - prompt=not config.no_prompt, - ) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - delegate_stake_parser = parser.add_parser( - "delegate", help="""Delegate Stake to an account.""" - ) - delegate_stake_parser.add_argument( - "--delegate_ss58key", - "--delegate_ss58", - dest="delegate_ss58key", - type=str, - required=False, - help="""The ss58 address of the choosen delegate""", - ) - delegate_stake_parser.add_argument( - "--all", dest="stake_all", action="store_true" - ) - delegate_stake_parser.add_argument( - "--amount", dest="amount", type=float, required=False - ) - bittensor.wallet.add_args(delegate_stake_parser) - bittensor.subtensor.add_args(delegate_stake_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.get("delegate_ss58key"): - # Check for delegates. - with bittensor.__console__.status(":satellite: Loading delegates..."): - subtensor = bittensor.subtensor(config=config, log_verbose=False) - delegates: List[bittensor.DelegateInfo] = subtensor.get_delegates() - try: - prev_delegates = subtensor.get_delegates( - max(0, subtensor.block - 1200) - ) - except SubstrateRequestException: - prev_delegates = None - - if prev_delegates is None: - bittensor.__console__.print( - ":warning: [yellow]Could not fetch delegates history[/yellow]" - ) - - if len(delegates) == 0: - console.print( - ":cross_mark: [red]There are no delegates on {}[/red]".format( - subtensor.network - ) - ) - sys.exit(1) - - delegates.sort(key=lambda delegate: delegate.total_stake, reverse=True) - show_delegates( - subtensor=subtensor, delegates=delegates, prev_delegates=prev_delegates - ) - delegate_index = Prompt.ask("Enter delegate index") - config.delegate_ss58key = str(delegates[int(delegate_index)].hotkey_ss58) - console.print( - "Selected: [yellow]{}[/yellow]".format(config.delegate_ss58key) - ) - - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - # Get amount. - if not config.get("amount") and not config.get("stake_all"): - if not Confirm.ask( - "Stake all Tao from account: [bold]'{}'[/bold]?".format( - config.wallet.get("name", defaults.wallet.name) - ) - ): - amount = Prompt.ask("Enter Tao amount to stake") - try: - config.amount = float(amount) - except ValueError: - console.print( - ":cross_mark: [red]Invalid Tao amount[/red] [bold white]{}[/bold white]".format( - amount - ) - ) - sys.exit() - else: - config.stake_all = True - - -class DelegateUnstakeCommand: - """ - Executes the ``undelegate`` command, allowing users to withdraw their staked Tao from a delegate on the Bittensor - network. - - This process is known as "undelegating" and it reverses the delegation process, freeing up the staked tokens. - - Optional Arguments: - - ``wallet.name``: The name of the wallet to use for the command. - - ``delegate_ss58key``: The ``SS58`` address of the delegate to undelegate from. - - ``amount``: The amount of Tao to undelegate. - - ``all``: If specified, the command undelegates all staked Tao from the delegate. - - The command prompts the user for the amount of Tao to undelegate and the ``SS58`` address of the delegate from which - to undelegate. If the ``--all`` flag is used, it will attempt to undelegate the entire staked amount from the - specified delegate. - - Usage: - The user must provide the delegate's SS58 address and the amount of Tao to undelegate. The function will then - send a transaction to the Bittensor network to process the undelegation. - - Example usage:: - - btcli undelegate --delegate_ss58key --amount - btcli undelegate --delegate_ss58key --all - - Note: - This command can result in a change to the blockchain state and may incur transaction fees. It is interactive - and requires confirmation from the user before proceeding. It should be used with care as undelegating can - affect the delegate's total stake and - potentially the user's staking rewards. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Undelegates stake from a chain delegate.""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - DelegateUnstakeCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - def _run(self: "bittensor.cli", subtensor: "bittensor.subtensor"): - """Undelegates stake from a chain delegate.""" - config = self.config.copy() - wallet = bittensor.wallet(config=config) - subtensor.undelegate( - wallet=wallet, - delegate_ss58=config.get("delegate_ss58key"), - amount=config.get("amount"), - wait_for_inclusion=True, - prompt=not config.no_prompt, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - undelegate_stake_parser = parser.add_parser( - "undelegate", help="""Undelegate Stake from an account.""" - ) - undelegate_stake_parser.add_argument( - "--delegate_ss58key", - "--delegate_ss58", - dest="delegate_ss58key", - type=str, - required=False, - help="""The ss58 address of the choosen delegate""", - ) - undelegate_stake_parser.add_argument( - "--all", dest="unstake_all", action="store_true" - ) - undelegate_stake_parser.add_argument( - "--amount", dest="amount", type=float, required=False - ) - bittensor.wallet.add_args(undelegate_stake_parser) - bittensor.subtensor.add_args(undelegate_stake_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.get("delegate_ss58key"): - # Check for delegates. - with bittensor.__console__.status(":satellite: Loading delegates..."): - subtensor = bittensor.subtensor(config=config, log_verbose=False) - delegates: List[bittensor.DelegateInfo] = subtensor.get_delegates() - try: - prev_delegates = subtensor.get_delegates( - max(0, subtensor.block - 1200) - ) - except SubstrateRequestException: - prev_delegates = None - - if prev_delegates is None: - bittensor.__console__.print( - ":warning: [yellow]Could not fetch delegates history[/yellow]" - ) - - if len(delegates) == 0: - console.print( - ":cross_mark: [red]There are no delegates on {}[/red]".format( - subtensor.network - ) - ) - sys.exit(1) - - delegates.sort(key=lambda delegate: delegate.total_stake, reverse=True) - show_delegates( - subtensor=subtensor, delegates=delegates, prev_delegates=prev_delegates - ) - delegate_index = Prompt.ask("Enter delegate index") - config.delegate_ss58key = str(delegates[int(delegate_index)].hotkey_ss58) - console.print( - "Selected: [yellow]{}[/yellow]".format(config.delegate_ss58key) - ) - - # Get amount. - if not config.get("amount") and not config.get("unstake_all"): - if not Confirm.ask( - "Unstake all Tao to account: [bold]'{}'[/bold]?".format( - config.wallet.get("name", defaults.wallet.name) - ) - ): - amount = Prompt.ask("Enter Tao amount to unstake") - try: - config.amount = float(amount) - except ValueError: - console.print( - ":cross_mark: [red]Invalid Tao amount[/red] [bold white]{}[/bold white]".format( - amount - ) - ) - sys.exit() - else: - config.unstake_all = True - - -class ListDelegatesCommand: - """ - Displays a formatted table of Bittensor network delegates, providing a comprehensive overview of delegate statistics and information. - - This table helps users make informed decisions on which delegates to allocate their TAO stake. - - Optional Arguments: - - ``wallet.name``: The name of the wallet to use for the command. - - ``subtensor.network``: The name of the network to use for the command. - - The table columns include: - - - INDEX: The delegate's index in the sorted list. - - DELEGATE: The name of the delegate. - - SS58: The delegate's unique SS58 address (truncated for display). - - NOMINATORS: The count of nominators backing the delegate. - - DELEGATE STAKE(τ): The amount of delegate's own stake (not the TAO delegated from any nominators). - - TOTAL STAKE(τ): The delegate's cumulative stake, including self-staked and nominators' stakes. - - CHANGE/(4h): The percentage change in the delegate's stake over the last four hours. - - SUBNETS: The subnets to which the delegate is registered. - - VPERMIT: Indicates the subnets for which the delegate has validator permits. - - NOMINATOR/(24h)/kτ: The earnings per 1000 τ staked by nominators in the last 24 hours. - - DELEGATE/(24h): The total earnings of the delegate in the last 24 hours. - - DESCRIPTION: A brief description of the delegate's purpose and operations. - - Sorting is done based on the ``TOTAL STAKE`` column in descending order. Changes in stake are highlighted: - increases in green and decreases in red. Entries with no previous data are marked with ``NA``. Each delegate's name - is a hyperlink to their respective URL, if available. - - Example usage:: - - btcli root list_delegates - btcli root list_delegates --wallet.name my_wallet - btcli root list_delegates --subtensor.network finney # can also be `test` or `local` - - Note: - This function is part of the Bittensor CLI tools and is intended for use within a console application. It prints - directly to the console and does not return any value. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r""" - List all delegates on the network. - """ - try: - cli.config.subtensor.network = "archive" - cli.config.subtensor.chain_endpoint = ( - "wss://archive.chain.opentensor.ai:443" - ) - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - ListDelegatesCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r""" - List all delegates on the network. - """ - with bittensor.__console__.status(":satellite: Loading delegates..."): - delegates: list[bittensor.DelegateInfo] = subtensor.get_delegates() - - try: - prev_delegates = subtensor.get_delegates(max(0, subtensor.block - 1200)) - except SubstrateRequestException: - prev_delegates = None - - if prev_delegates is None: - bittensor.__console__.print( - ":warning: [yellow]Could not fetch delegates history[/yellow]" - ) - - show_delegates( - subtensor=subtensor, - delegates=delegates, - prev_delegates=prev_delegates, - width=cli.config.get("width", None), - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - list_delegates_parser = parser.add_parser( - "list_delegates", help="""List all delegates on the network""" - ) - bittensor.subtensor.add_args(list_delegates_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - pass - - -class NominateCommand: - """ - Executes the ``nominate`` command, which facilitates a wallet to become a delegate on the Bittensor network. - - This command handles the nomination process, including wallet unlocking and verification of the hotkey's current - delegate status. - - The command performs several checks: - - - Verifies that the hotkey is not already a delegate to prevent redundant nominations. - - Tries to nominate the wallet and reports success or failure. - - Upon success, the wallet's hotkey is registered as a delegate on the network. - - Optional Arguments: - - ``wallet.name``: The name of the wallet to use for the command. - - ``wallet.hotkey``: The name of the hotkey to use for the command. - - Usage: - To run the command, the user must have a configured wallet with both hotkey and coldkey. If the wallet is not - already nominated, this command will initiate the process. - - Example usage:: - - btcli root nominate - btcli root nominate --wallet.name my_wallet --wallet.hotkey my_hotkey - - Note: - This function is intended to be used as a CLI command. It prints the outcome directly to the console and does - not return any value. It should not be called programmatically in user code due to its interactive nature and - side effects on the network state. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Nominate wallet.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - NominateCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Nominate wallet.""" - wallet = bittensor.wallet(config=cli.config) - - # Unlock the wallet. - wallet.hotkey - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return - - # Check if the hotkey is already a delegate. - if subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - bittensor.__console__.print( - "Aborting: Hotkey {} is already a delegate.".format( - wallet.hotkey.ss58_address - ) - ) - return - - result: bool = subtensor.nominate(wallet) - if not result: - bittensor.__console__.print( - "Could not became a delegate on [white]{}[/white]".format( - subtensor.network - ) - ) - else: - # Check if we are a delegate. - is_delegate: bool = subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address) - if not is_delegate: - bittensor.__console__.print( - "Could not became a delegate on [white]{}[/white]".format( - subtensor.network - ) - ) - return - bittensor.__console__.print( - "Successfully became a delegate on [white]{}[/white]".format( - subtensor.network - ) - ) - - # Prompt use to set identity on chain. - if not cli.config.no_prompt: - do_set_identity = Prompt.ask( - f"Subnetwork registered successfully. Would you like to set your identity? [y/n]", - choices=["y", "n"], - ) - - if do_set_identity.lower() == "y": - subtensor.close() - config = cli.config.copy() - SetIdentityCommand.check_config(config) - cli.config = config - SetIdentityCommand.run(cli) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - nominate_parser = parser.add_parser( - "nominate", help="""Become a delegate on the network""" - ) - bittensor.wallet.add_args(nominate_parser) - bittensor.subtensor.add_args(nominate_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - -class MyDelegatesCommand: - """ - Executes the ``my_delegates`` command within the Bittensor CLI, which retrieves and displays a table of delegated - stakes from a user's wallet(s) to various delegates on the Bittensor network. - - The command provides detailed insights into the user's - staking activities and the performance of their chosen delegates. - - Optional Arguments: - - ``wallet.name``: The name of the wallet to use for the command. - - ``all``: If specified, the command aggregates information across all wallets. - - The table output includes the following columns: - - - Wallet: The name of the user's wallet. - - OWNER: The name of the delegate's owner. - - SS58: The truncated SS58 address of the delegate. - - Delegation: The amount of Tao staked by the user to the delegate. - - τ/24h: The earnings from the delegate to the user over the past 24 hours. - - NOMS: The number of nominators for the delegate. - - OWNER STAKE(τ): The stake amount owned by the delegate. - - TOTAL STAKE(τ): The total stake amount held by the delegate. - - SUBNETS: The list of subnets the delegate is a part of. - - VPERMIT: Validator permits held by the delegate for various subnets. - - 24h/kτ: Earnings per 1000 Tao staked over the last 24 hours. - - Desc: A description of the delegate. - - The command also sums and prints the total amount of Tao delegated across all wallets. - - Usage: - The command can be run as part of the Bittensor CLI suite of tools and requires no parameters if a single wallet - is used. If multiple wallets are present, the ``--all`` flag can be specified to aggregate information across - all wallets. - - Example usage:: - - btcli my_delegates - btcli my_delegates --all - btcli my_delegates --wallet.name my_wallet - - Note: - This function is typically called by the CLI parser and is not intended to be used directly in user code. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Delegates stake to a chain delegate.""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - MyDelegatesCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - """Delegates stake to a chain delegate.""" - config = cli.config.copy() - if config.get("all", d=None): - wallets = _get_coldkey_wallets_for_path(config.wallet.path) - else: - wallets = [bittensor.wallet(config=config)] - - table = Table(show_footer=True, pad_edge=False, box=None, expand=True) - table.add_column( - "[overline white]Wallet", footer_style="overline white", style="bold white" - ) - table.add_column( - "[overline white]OWNER", - style="rgb(50,163,219)", - no_wrap=True, - justify="left", - ) - table.add_column( - "[overline white]SS58", footer_style="overline white", style="bold yellow" - ) - table.add_column( - "[overline green]Delegation", - footer_style="overline green", - style="bold green", - ) - table.add_column( - "[overline green]\u03c4/24h", - footer_style="overline green", - style="bold green", - ) - table.add_column( - "[overline white]NOMS", justify="center", style="green", no_wrap=True - ) - table.add_column( - "[overline white]OWNER STAKE(\u03c4)", justify="right", no_wrap=True - ) - table.add_column( - "[overline white]TOTAL STAKE(\u03c4)", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]SUBNETS", justify="right", style="white", no_wrap=True - ) - table.add_column("[overline white]VPERMIT", justify="right", no_wrap=True) - table.add_column("[overline white]24h/k\u03c4", style="green", justify="center") - table.add_column("[overline white]Desc", style="rgb(50,163,219)") - total_delegated = 0 - - for wallet in tqdm(wallets): - if not wallet.coldkeypub_file.exists_on_device(): - continue - delegates = subtensor.get_delegated( - coldkey_ss58=wallet.coldkeypub.ss58_address - ) - - my_delegates = {} # hotkey, amount - for delegate in delegates: - for coldkey_addr, staked in delegate[0].nominators: - if ( - coldkey_addr == wallet.coldkeypub.ss58_address - and staked.tao > 0 - ): - my_delegates[delegate[0].hotkey_ss58] = staked - - delegates.sort(key=lambda delegate: delegate[0].total_stake, reverse=True) - total_delegated += sum(my_delegates.values()) - - registered_delegate_info: Optional[DelegatesDetails] = ( - get_delegates_details(subtensor=subtensor) - ) - if registered_delegate_info is None: - bittensor.__console__.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - registered_delegate_info = {} - - for i, delegate in enumerate(delegates): - owner_stake = next( - map( - lambda x: x[1], # get stake - filter( - lambda x: x[0] == delegate[0].owner_ss58, - delegate[0].nominators, - ), # filter for owner - ), - bittensor.Balance.from_rao(0), # default to 0 if no owner stake. - ) - if delegate[0].hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[ - delegate[0].hotkey_ss58 - ].name - delegate_url = registered_delegate_info[delegate[0].hotkey_ss58].url - delegate_description = registered_delegate_info[ - delegate[0].hotkey_ss58 - ].description - else: - delegate_name = "" - delegate_url = "" - delegate_description = "" - - if delegate[0].hotkey_ss58 in my_delegates: - table.add_row( - wallet.name, - Text(delegate_name, style=f"link {delegate_url}"), - f"{delegate[0].hotkey_ss58:8.8}...", - f"{my_delegates[delegate[0].hotkey_ss58]!s:13.13}", - f"{delegate[0].total_daily_return.tao * (my_delegates[delegate[0].hotkey_ss58]/delegate[0].total_stake.tao)!s:6.6}", - str(len(delegate[0].nominators)), - f"{owner_stake!s:13.13}", - f"{delegate[0].total_stake!s:13.13}", - str(delegate[0].registrations), - str( - [ - "*" if subnet in delegate[0].validator_permits else "" - for subnet in delegate[0].registrations - ] - ), - # f'{delegate.take * 100:.1f}%',s - f"{ delegate[0].total_daily_return.tao * ( 1000 / ( 0.001 + delegate[0].total_stake.tao ) )!s:6.6}", - str(delegate_description), - # f'{delegate_profile.description:140.140}', - ) - - bittensor.__console__.print(table) - bittensor.__console__.print("Total delegated Tao: {}".format(total_delegated)) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - delegate_stake_parser = parser.add_parser( - "my_delegates", - help="""Show all delegates where I am delegating a positive amount of stake""", - ) - delegate_stake_parser.add_argument( - "--all", - action="store_true", - help="""Check all coldkey wallets.""", - default=False, - ) - bittensor.wallet.add_args(delegate_stake_parser) - bittensor.subtensor.add_args(delegate_stake_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if ( - not config.get("all", d=None) - and not config.is_set("wallet.name") - and not config.no_prompt - ): - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - -class SetTakeCommand: - """ - Executes the ``set_take`` command, which sets the delegate take. - - The command performs several checks: - - 1. Hotkey is already a delegate - 2. New take value is within 0-18% range - - Optional Arguments: - - ``take``: The new take value - - ``wallet.name``: The name of the wallet to use for the command. - - ``wallet.hotkey``: The name of the hotkey to use for the command. - - Usage: - To run the command, the user must have a configured wallet with both hotkey and coldkey. Also, the hotkey should already be a delegate. - - Example usage:: - btcli root set_take --wallet.name my_wallet --wallet.hotkey my_hotkey - - Note: - This function can be used to update the takes individually for every subnet - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Set delegate take.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - SetTakeCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Set delegate take.""" - config = cli.config.copy() - wallet = bittensor.wallet(config=cli.config) - - # Unlock the wallet. - wallet.hotkey - wallet.coldkey - - # Check if the hotkey is not a delegate. - if not subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - bittensor.__console__.print( - "Aborting: Hotkey {} is NOT a delegate.".format( - wallet.hotkey.ss58_address - ) - ) - return - - # Prompt user for take value. - new_take_str = config.get("take") - if new_take_str == None: - new_take = FloatPrompt.ask(f"Enter take value (0.18 for 18%)") - else: - new_take = float(new_take_str) - - if new_take > 0.18: - bittensor.__console__.print("ERROR: Take value should not exceed 18%") - return - - result: bool = subtensor.set_take( - wallet=wallet, - delegate_ss58=wallet.hotkey.ss58_address, - take=new_take, - ) - if not result: - bittensor.__console__.print("Could not set the take") - else: - # Check if we are a delegate. - is_delegate: bool = subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address) - if not is_delegate: - bittensor.__console__.print( - "Could not set the take [white]{}[/white]".format(subtensor.network) - ) - return - bittensor.__console__.print( - "Successfully set the take on [white]{}[/white]".format( - subtensor.network - ) - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - set_take_parser = parser.add_parser( - "set_take", help="""Set take for delegate""" - ) - set_take_parser.add_argument( - "--take", - dest="take", - type=float, - required=False, - help="""Take as a float number""", - ) - bittensor.wallet.add_args(set_take_parser) - bittensor.subtensor.add_args(set_take_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) diff --git a/bittensor/commands/identity.py b/bittensor/commands/identity.py deleted file mode 100644 index 4f7454849..000000000 --- a/bittensor/commands/identity.py +++ /dev/null @@ -1,344 +0,0 @@ -import argparse -from rich.table import Table -from rich.prompt import Prompt -from sys import getsizeof - -import bittensor - - -class SetIdentityCommand: - """ - Executes the :func:`set_identity` command within the Bittensor network, which allows for the creation or update of a delegate's on-chain identity. - - This identity includes various - attributes such as display name, legal name, web URL, PGP fingerprint, and contact - information, among others. - - Optional Arguments: - - ``display``: The display name for the identity. - - ``legal``: The legal name for the identity. - - ``web``: The web URL for the identity. - - ``riot``: The riot handle for the identity. - - ``email``: The email address for the identity. - - ``pgp_fingerprint``: The PGP fingerprint for the identity. - - ``image``: The image URL for the identity. - - ``info``: The info for the identity. - - ``twitter``: The X (twitter) URL for the identity. - - The command prompts the user for the different identity attributes and validates the - input size for each attribute. It provides an option to update an existing validator - hotkey identity. If the user consents to the transaction cost, the identity is updated - on the blockchain. - - Each field has a maximum size of 64 bytes. The PGP fingerprint field is an exception - and has a maximum size of 20 bytes. The user is prompted to enter the PGP fingerprint - as a hex string, which is then converted to bytes. The user is also prompted to enter - the coldkey or hotkey ``ss58`` address for the identity to be updated. If the user does - not have a hotkey, the coldkey address is used by default. - - If setting a validator identity, the hotkey will be used by default. If the user is - setting an identity for a subnet, the coldkey will be used by default. - - Usage: - The user should call this command from the command line and follow the interactive - prompts to enter or update the identity information. The command will display the - updated identity details in a table format upon successful execution. - - Example usage:: - - btcli wallet set_identity - - Note: - This command should only be used if the user is willing to incur the 1 TAO transaction - fee associated with setting an identity on the blockchain. It is a high-level command - that makes changes to the blockchain state and should not be used programmatically as - part of other scripts or applications. - """ - - def run(cli: "bittensor.cli"): - r"""Create a new or update existing identity on-chain.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - SetIdentityCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Create a new or update existing identity on-chain.""" - console = bittensor.__console__ - - wallet = bittensor.wallet(config=cli.config) - - id_dict = { - "display": cli.config.display, - "legal": cli.config.legal, - "web": cli.config.web, - "pgp_fingerprint": cli.config.pgp_fingerprint, - "riot": cli.config.riot, - "email": cli.config.email, - "image": cli.config.image, - "twitter": cli.config.twitter, - "info": cli.config.info, - } - - for field, string in id_dict.items(): - if getsizeof(string) > 113: # 64 + 49 overhead bytes for string - raise ValueError(f"Identity value `{field}` must be <= 64 raw bytes") - - identified = ( - wallet.hotkey.ss58_address - if str( - Prompt.ask( - "Are you updating a validator hotkey identity?", - default="y", - choices=["y", "n"], - ) - ).lower() - == "y" - else None - ) - - if ( - str( - Prompt.ask( - "Cost to register an Identity is [bold white italic]0.1 Tao[/bold white italic], are you sure you wish to continue?", - default="n", - choices=["y", "n"], - ) - ).lower() - == "n" - ): - console.print(":cross_mark: Aborted!") - exit(0) - - try: - wallet.coldkey # unlock coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return - - with console.status(":satellite: [bold green]Updating identity on-chain..."): - try: - subtensor.update_identity( - identified=identified, - wallet=wallet, - params=id_dict, - ) - except Exception as e: - console.print(f"[red]:cross_mark: Failed![/red] {e}") - exit(1) - - console.print(":white_heavy_check_mark: Success!") - - identity = subtensor.query_identity(identified or wallet.coldkey.ss58_address) - - table = Table(title="[bold white italic]Updated On-Chain Identity") - table.add_column("Key", justify="right", style="cyan", no_wrap=True) - table.add_column("Value", style="magenta") - - table.add_row("Address", identified or wallet.coldkey.ss58_address) - for key, value in identity.items(): - table.add_row(key, str(value) if value is not None else "None") - - console.print(table) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - config.wallet.name = Prompt.ask( - "Enter wallet name", default=bittensor.defaults.wallet.name - ) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - config.wallet.hotkey = Prompt.ask( - "Enter wallet hotkey", default=bittensor.defaults.wallet.hotkey - ) - if not config.is_set("subtensor.network") and not config.no_prompt: - config.subtensor.network = Prompt.ask( - "Enter subtensor network", - default=bittensor.defaults.subtensor.network, - choices=bittensor.__networks__, - ) - ( - _, - config.subtensor.chain_endpoint, - ) = bittensor.subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - if not config.is_set("display") and not config.no_prompt: - config.display = Prompt.ask("Enter display name", default="") - if not config.is_set("legal") and not config.no_prompt: - config.legal = Prompt.ask("Enter legal string", default="") - if not config.is_set("web") and not config.no_prompt: - config.web = Prompt.ask("Enter web url", default="") - if not config.is_set("pgp_fingerprint") and not config.no_prompt: - config.pgp_fingerprint = Prompt.ask( - "Enter pgp fingerprint (must be 20 bytes)", default=None - ) - if not config.is_set("riot") and not config.no_prompt: - config.riot = Prompt.ask("Enter riot", default="") - if not config.is_set("email") and not config.no_prompt: - config.email = Prompt.ask("Enter email address", default="") - if not config.is_set("image") and not config.no_prompt: - config.image = Prompt.ask("Enter image url", default="") - if not config.is_set("twitter") and not config.no_prompt: - config.twitter = Prompt.ask("Enter twitter url", default="") - if not config.is_set("info") and not config.no_prompt: - config.info = Prompt.ask("Enter info", default="") - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - new_coldkey_parser = parser.add_parser( - "set_identity", - help="""Create or update identity on-chain for a given cold wallet. Must be a subnet owner.""", - ) - new_coldkey_parser.add_argument( - "--display", - type=str, - help="""The display name for the identity.""", - ) - new_coldkey_parser.add_argument( - "--legal", - type=str, - help="""The legal name for the identity.""", - ) - new_coldkey_parser.add_argument( - "--web", - type=str, - help="""The web url for the identity.""", - ) - new_coldkey_parser.add_argument( - "--riot", - type=str, - help="""The riot handle for the identity.""", - ) - new_coldkey_parser.add_argument( - "--email", - type=str, - help="""The email address for the identity.""", - ) - new_coldkey_parser.add_argument( - "--pgp_fingerprint", - type=str, - help="""The pgp fingerprint for the identity.""", - ) - new_coldkey_parser.add_argument( - "--image", - type=str, - help="""The image url for the identity.""", - ) - new_coldkey_parser.add_argument( - "--info", - type=str, - help="""The info for the identity.""", - ) - new_coldkey_parser.add_argument( - "--twitter", - type=str, - help="""The twitter url for the identity.""", - ) - bittensor.wallet.add_args(new_coldkey_parser) - bittensor.subtensor.add_args(new_coldkey_parser) - - -class GetIdentityCommand: - """ - Executes the :func:`get_identity` command, which retrieves and displays the identity details of a user's coldkey or hotkey associated with the Bittensor network. This function - queries the subtensor chain for information such as the stake, rank, and trust associated - with the provided key. - - Optional Arguments: - - ``key``: The ``ss58`` address of the coldkey or hotkey to query. - - The command performs the following actions: - - - Connects to the subtensor network and retrieves the identity information. - - Displays the information in a structured table format. - - The displayed table includes: - - - **Address**: The ``ss58`` address of the queried key. - - **Item**: Various attributes of the identity such as stake, rank, and trust. - - **Value**: The corresponding values of the attributes. - - Usage: - The user must provide an ``ss58`` address as input to the command. If the address is not - provided in the configuration, the user is prompted to enter one. - - Example usage:: - - btcli wallet get_identity --key - - Note: - This function is designed for CLI use and should be executed in a terminal. It is - primarily used for informational purposes and has no side effects on the network state. - """ - - def run(cli: "bittensor.cli"): - r"""Queries the subtensor chain for user identity.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - GetIdentityCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - console = bittensor.__console__ - - with console.status(":satellite: [bold green]Querying chain identity..."): - identity = subtensor.query_identity(cli.config.key) - - table = Table(title="[bold white italic]On-Chain Identity") - table.add_column("Item", justify="right", style="cyan", no_wrap=True) - table.add_column("Value", style="magenta") - - table.add_row("Address", cli.config.key) - for key, value in identity.items(): - table.add_row(key, str(value) if value is not None else "None") - - console.print(table) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("key") and not config.no_prompt: - config.key = Prompt.ask( - "Enter coldkey or hotkey ss58 address", default=None - ) - if config.key is None: - raise ValueError("key must be set") - if not config.is_set("subtensor.network") and not config.no_prompt: - config.subtensor.network = Prompt.ask( - "Enter subtensor network", - default=bittensor.defaults.subtensor.network, - choices=bittensor.__networks__, - ) - ( - _, - config.subtensor.chain_endpoint, - ) = bittensor.subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - new_coldkey_parser = parser.add_parser( - "get_identity", - help="""Creates a new coldkey (for containing balance) under the specified path. """, - ) - new_coldkey_parser.add_argument( - "--key", - type=str, - default=None, - help="""The coldkey or hotkey ss58 address to query.""", - ) - bittensor.wallet.add_args(new_coldkey_parser) - bittensor.subtensor.add_args(new_coldkey_parser) diff --git a/bittensor/commands/inspect.py b/bittensor/commands/inspect.py deleted file mode 100644 index ef32a7694..000000000 --- a/bittensor/commands/inspect.py +++ /dev/null @@ -1,279 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 bittensor -from tqdm import tqdm -from rich.table import Table -from rich.prompt import Prompt -from .utils import ( - get_delegates_details, - DelegatesDetails, - get_hotkey_wallets_for_wallet, - get_all_wallets_for_path, - filter_netuids_by_registered_hotkeys, -) -from . import defaults - -console = bittensor.__console__ - -import os -import bittensor -from typing import List, Tuple, Optional, Dict - - -def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: - try: - wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [bittensor.wallet(path=path, name=name) for name in wallet_names] - except StopIteration: - # No wallet files found. - wallets = [] - return wallets - - -def _get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: - hotkey_wallets = [] - hotkeys_path = wallet.path + "/" + wallet.name + "/hotkeys" - try: - hotkey_files = next(os.walk(os.path.expanduser(hotkeys_path)))[2] - except StopIteration: - hotkey_files = [] - for hotkey_file_name in hotkey_files: - try: - hotkey_for_name = bittensor.wallet( - path=wallet.path, name=wallet.name, hotkey=hotkey_file_name - ) - if ( - hotkey_for_name.hotkey_file.exists_on_device() - and not hotkey_for_name.hotkey_file.is_encrypted() - ): - hotkey_wallets.append(hotkey_for_name) - except Exception: - pass - return hotkey_wallets - - -class InspectCommand: - """ - Executes the ``inspect`` command, which compiles and displays a detailed report of a user's wallet pairs (coldkey, hotkey) on the Bittensor network. - - This report includes balance and - staking information for both the coldkey and hotkey associated with the wallet. - - Optional arguments: - - ``all``: If set to ``True``, the command will inspect all wallets located within the specified path. If set to ``False``, the command will inspect only the wallet specified by the user. - - The command gathers data on: - - - Coldkey balance and delegated stakes. - - Hotkey stake and emissions per neuron on the network. - - Delegate names and details fetched from the network. - - The resulting table includes columns for: - - - **Coldkey**: The coldkey associated with the user's wallet. - - **Balance**: The balance of the coldkey. - - **Delegate**: The name of the delegate to which the coldkey has staked funds. - - **Stake**: The amount of stake held by both the coldkey and hotkey. - - **Emission**: The emission or rewards earned from staking. - - **Netuid**: The network unique identifier of the subnet where the hotkey is active. - - **Hotkey**: The hotkey associated with the neuron on the network. - - Usage: - This command can be used to inspect a single wallet or all wallets located within a - specified path. It is useful for a comprehensive overview of a user's participation - and performance in the Bittensor network. - - Example usage:: - - btcli wallet inspect - btcli wallet inspect --all - - Note: - The ``inspect`` command is for displaying information only and does not perform any - transactions or state changes on the Bittensor network. It is intended to be used as - part of the Bittensor CLI and not as a standalone function within user code. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Inspect a cold, hot pair.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - InspectCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - if cli.config.get("all", d=False) == True: - wallets = _get_coldkey_wallets_for_path(cli.config.wallet.path) - all_hotkeys = get_all_wallets_for_path(cli.config.wallet.path) - else: - wallets = [bittensor.wallet(config=cli.config)] - all_hotkeys = get_hotkey_wallets_for_wallet(wallets[0]) - - netuids = subtensor.get_all_subnet_netuids() - netuids = filter_netuids_by_registered_hotkeys( - cli, subtensor, netuids, all_hotkeys - ) - bittensor.logging.debug(f"Netuids to check: {netuids}") - - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(subtensor=subtensor) - ) - if registered_delegate_info is None: - bittensor.__console__.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - registered_delegate_info = {} - - neuron_state_dict = {} - for netuid in tqdm(netuids): - neurons = subtensor.neurons_lite(netuid) - neuron_state_dict[netuid] = neurons if neurons != None else [] - - table = Table(show_footer=True, pad_edge=False, box=None, expand=True) - table.add_column( - "[overline white]Coldkey", footer_style="overline white", style="bold white" - ) - table.add_column( - "[overline white]Balance", footer_style="overline white", style="green" - ) - table.add_column( - "[overline white]Delegate", footer_style="overline white", style="blue" - ) - table.add_column( - "[overline white]Stake", footer_style="overline white", style="green" - ) - table.add_column( - "[overline white]Emission", footer_style="overline white", style="green" - ) - table.add_column( - "[overline white]Netuid", footer_style="overline white", style="bold white" - ) - table.add_column( - "[overline white]Hotkey", footer_style="overline white", style="yellow" - ) - table.add_column( - "[overline white]Stake", footer_style="overline white", style="green" - ) - table.add_column( - "[overline white]Emission", footer_style="overline white", style="green" - ) - for wallet in tqdm(wallets): - delegates: List[Tuple[bittensor.DelegateInfo, bittensor.Balance]] = ( - subtensor.get_delegated(coldkey_ss58=wallet.coldkeypub.ss58_address) - ) - if not wallet.coldkeypub_file.exists_on_device(): - continue - cold_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - table.add_row(wallet.name, str(cold_balance), "", "", "", "", "", "", "") - for dele, staked in delegates: - if dele.hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[dele.hotkey_ss58].display - else: - delegate_name = dele.hotkey_ss58 - table.add_row( - "", - "", - str(delegate_name), - str(staked), - str( - dele.total_daily_return.tao - * (staked.tao / dele.total_stake.tao) - ), - "", - "", - "", - "", - ) - - hotkeys = _get_hotkey_wallets_for_wallet(wallet) - for netuid in netuids: - for neuron in neuron_state_dict[netuid]: - if neuron.coldkey == wallet.coldkeypub.ss58_address: - hotkey_name: str = "" - - hotkey_names: List[str] = [ - wallet.hotkey_str - for wallet in filter( - lambda hotkey: hotkey.hotkey.ss58_address - == neuron.hotkey, - hotkeys, - ) - ] - if len(hotkey_names) > 0: - hotkey_name = f"{hotkey_names[0]}-" - - table.add_row( - "", - "", - "", - "", - "", - str(netuid), - f"{hotkey_name}{neuron.hotkey}", - str(neuron.stake), - str(bittensor.Balance.from_tao(neuron.emission)), - ) - - bittensor.__console__.print(table) - - @staticmethod - def check_config(config: "bittensor.config"): - if ( - not config.is_set("wallet.name") - and not config.no_prompt - and not config.get("all", d=None) - ): - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if config.netuids != [] and config.netuids is not None: - if not isinstance(config.netuids, list): - config.netuids = [int(config.netuids)] - else: - config.netuids = [int(netuid) for netuid in config.netuids] - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - inspect_parser = parser.add_parser( - "inspect", help="""Inspect a wallet (cold, hot) pair""" - ) - inspect_parser.add_argument( - "--all", - action="store_true", - help="""Check all coldkey wallets.""", - default=False, - ) - inspect_parser.add_argument( - "--netuids", - dest="netuids", - type=int, - nargs="*", - help="""Set the netuid(s) to filter by.""", - default=None, - ) - - bittensor.wallet.add_args(inspect_parser) - bittensor.subtensor.add_args(inspect_parser) diff --git a/bittensor/commands/list.py b/bittensor/commands/list.py deleted file mode 100644 index b2946efff..000000000 --- a/bittensor/commands/list.py +++ /dev/null @@ -1,128 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 os -import argparse -import bittensor -from rich import print -from rich.tree import Tree - -console = bittensor.__console__ - - -class ListCommand: - """ - Executes the ``list`` command which enumerates all wallets and their respective hotkeys present in the user's Bittensor configuration directory. - - The command organizes the information in a tree structure, displaying each wallet along with the ``ss58`` addresses for the coldkey public key and any hotkeys associated with it. - - Optional arguments: - - ``-p``, ``--path``: The path to the Bittensor configuration directory. Defaults to '~/.bittensor'. - - The output is presented in a hierarchical tree format, with each wallet as a root node, - and any associated hotkeys as child nodes. The ``ss58`` address is displayed for each - coldkey and hotkey that is not encrypted and exists on the device. - - Usage: - Upon invocation, the command scans the wallet directory and prints a list of all wallets, indicating whether the public keys are available (``?`` denotes unavailable or encrypted keys). - - Example usage:: - - btcli wallet list --path ~/.bittensor - - Note: - This command is read-only and does not modify the filesystem or the network state. It is intended for use within the Bittensor CLI to provide a quick overview of the user's wallets. - """ - - @staticmethod - def run(cli): - r"""Lists wallets.""" - try: - wallets = next(os.walk(os.path.expanduser(cli.config.wallet.path)))[1] - except StopIteration: - # No wallet files found. - wallets = [] - ListCommand._run(cli, wallets) - - @staticmethod - def _run(cli: "bittensor.cli", wallets, return_value=False): - root = Tree("Wallets") - for w_name in wallets: - wallet_for_name = bittensor.wallet(path=cli.config.wallet.path, name=w_name) - try: - if ( - wallet_for_name.coldkeypub_file.exists_on_device() - and not wallet_for_name.coldkeypub_file.is_encrypted() - ): - coldkeypub_str = wallet_for_name.coldkeypub.ss58_address - else: - coldkeypub_str = "?" - except: - coldkeypub_str = "?" - - wallet_tree = root.add( - "\n[bold white]{} ({})".format(w_name, coldkeypub_str) - ) - hotkeys_path = os.path.join(cli.config.wallet.path, w_name, "hotkeys") - try: - hotkeys = next(os.walk(os.path.expanduser(hotkeys_path))) - if len(hotkeys) > 1: - for h_name in hotkeys[2]: - hotkey_for_name = bittensor.wallet( - path=cli.config.wallet.path, name=w_name, hotkey=h_name - ) - try: - if ( - hotkey_for_name.hotkey_file.exists_on_device() - and not hotkey_for_name.hotkey_file.is_encrypted() - ): - hotkey_str = hotkey_for_name.hotkey.ss58_address - else: - hotkey_str = "?" - except: - hotkey_str = "?" - wallet_tree.add("[bold grey]{} ({})".format(h_name, hotkey_str)) - except: - continue - - if len(wallets) == 0: - root.add("[bold red]No wallets found.") - - # Uses rich print to display the tree. - if not return_value: - print(root) - else: - return root - - @staticmethod - def check_config(config: "bittensor.config"): - pass - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - list_parser = parser.add_parser("list", help="""List wallets""") - bittensor.wallet.add_args(list_parser) - bittensor.subtensor.add_args(list_parser) - - @staticmethod - def get_tree(cli): - try: - wallets = next(os.walk(os.path.expanduser(cli.config.wallet.path)))[1] - except StopIteration: - # No wallet files found. - wallets = [] - return ListCommand._run(cli=cli, wallets=wallets, return_value=True) diff --git a/bittensor/commands/metagraph.py b/bittensor/commands/metagraph.py deleted file mode 100644 index 79fa48b78..000000000 --- a/bittensor/commands/metagraph.py +++ /dev/null @@ -1,268 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 - -from rich.table import Table - -import bittensor - -from .utils import check_netuid_set - -console = bittensor.__console__ # type: ignore - - -class MetagraphCommand: - """ - Executes the ``metagraph`` command to retrieve and display the entire metagraph for a specified network. - - This metagraph contains detailed information about - all the neurons (nodes) participating in the network, including their stakes, - trust scores, and more. - - Optional arguments: - - ``--netuid``: The netuid of the network to query. Defaults to the default network UID. - - ``--subtensor.network``: The name of the network to query. Defaults to the default network name. - - The table displayed includes the following columns for each neuron: - - - UID: Unique identifier of the neuron. - - STAKE(τ): Total stake of the neuron in Tau (τ). - - RANK: Rank score of the neuron. - - TRUST: Trust score assigned to the neuron by other neurons. - - CONSENSUS: Consensus score of the neuron. - - INCENTIVE: Incentive score representing the neuron's incentive alignment. - - DIVIDENDS: Dividends earned by the neuron. - - EMISSION(p): Emission in Rho (p) received by the neuron. - - VTRUST: Validator trust score indicating the network's trust in the neuron as a validator. - - VAL: Validator status of the neuron. - - UPDATED: Number of blocks since the neuron's last update. - - ACTIVE: Activity status of the neuron. - - AXON: Network endpoint information of the neuron. - - HOTKEY: Partial hotkey (public key) of the neuron. - - COLDKEY: Partial coldkey (public key) of the neuron. - - The command also prints network-wide statistics such as total stake, issuance, and difficulty. - - Usage: - The user must specify the network UID to query the metagraph. If not specified, the default network UID is used. - - Example usage:: - - btcli subnet metagraph --netuid 0 # Root network - btcli subnet metagraph --netuid 1 --subtensor.network test - - Note: - This command provides a snapshot of the network's state at the time of calling. - It is useful for network analysis and diagnostics. It is intended to be used as - part of the Bittensor CLI and not as a standalone function within user code. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Prints an entire metagraph.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - MetagraphCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Prints an entire metagraph.""" - console = bittensor.__console__ - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - cli.config.subtensor.network - ) - ) - metagraph: bittensor.metagraph = subtensor.metagraph(netuid=cli.config.netuid) - metagraph.save() - difficulty = subtensor.difficulty(cli.config.netuid) - subnet_emission = bittensor.Balance.from_tao( - subtensor.get_emission_value_by_subnet(cli.config.netuid) - ) - total_issuance = bittensor.Balance.from_rao(subtensor.total_issuance().rao) - - TABLE_DATA = [] - total_stake = 0.0 - total_rank = 0.0 - total_validator_trust = 0.0 - total_trust = 0.0 - total_consensus = 0.0 - total_incentive = 0.0 - total_dividends = 0.0 - total_emission = 0 - for uid in metagraph.uids: - neuron = metagraph.neurons[uid] - ep = metagraph.axons[uid] - row = [ - str(neuron.uid), - "{:.5f}".format(metagraph.total_stake[uid]), - "{:.5f}".format(metagraph.ranks[uid]), - "{:.5f}".format(metagraph.trust[uid]), - "{:.5f}".format(metagraph.consensus[uid]), - "{:.5f}".format(metagraph.incentive[uid]), - "{:.5f}".format(metagraph.dividends[uid]), - "{}".format(int(metagraph.emission[uid] * 1000000000)), - "{:.5f}".format(metagraph.validator_trust[uid]), - "*" if metagraph.validator_permit[uid] else "", - str((metagraph.block.item() - metagraph.last_update[uid].item())), - str(metagraph.active[uid].item()), - ( - ep.ip + ":" + str(ep.port) - if ep.is_serving - else "[yellow]none[/yellow]" - ), - ep.hotkey[:10], - ep.coldkey[:10], - ] - total_stake += metagraph.total_stake[uid] - total_rank += metagraph.ranks[uid] - total_validator_trust += metagraph.validator_trust[uid] - total_trust += metagraph.trust[uid] - total_consensus += metagraph.consensus[uid] - total_incentive += metagraph.incentive[uid] - total_dividends += metagraph.dividends[uid] - total_emission += int(metagraph.emission[uid] * 1000000000) - TABLE_DATA.append(row) - total_neurons = len(metagraph.uids) - table = Table(show_footer=False) - table.title = "[white]Metagraph: net: {}:{}, block: {}, N: {}/{}, stake: {}, issuance: {}, difficulty: {}".format( - subtensor.network, - metagraph.netuid, - metagraph.block.item(), - sum(metagraph.active.tolist()), - metagraph.n.item(), - bittensor.Balance.from_tao(total_stake), - total_issuance, - difficulty, - ) - table.add_column( - "[overline white]UID", - str(total_neurons), - footer_style="overline white", - style="yellow", - ) - table.add_column( - "[overline white]STAKE(\u03c4)", - "\u03c4{:.5f}".format(total_stake), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]RANK", - "{:.5f}".format(total_rank), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]TRUST", - "{:.5f}".format(total_trust), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]CONSENSUS", - "{:.5f}".format(total_consensus), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]INCENTIVE", - "{:.5f}".format(total_incentive), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]DIVIDENDS", - "{:.5f}".format(total_dividends), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]EMISSION(\u03c1)", - "\u03c1{}".format(int(total_emission)), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]VTRUST", - "{:.5f}".format(total_validator_trust), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]VAL", justify="right", style="green", no_wrap=True - ) - table.add_column("[overline white]UPDATED", justify="right", no_wrap=True) - table.add_column( - "[overline white]ACTIVE", justify="right", style="green", no_wrap=True - ) - table.add_column( - "[overline white]AXON", justify="left", style="dim blue", no_wrap=True - ) - table.add_column("[overline white]HOTKEY", style="dim blue", no_wrap=False) - table.add_column("[overline white]COLDKEY", style="dim purple", no_wrap=False) - table.show_footer = True - - for row in TABLE_DATA: - table.add_row(*row) - table.box = None - table.pad_edge = False - table.width = None - console.print(table) - - @staticmethod - def check_config(config: "bittensor.config"): - check_netuid_set( - config, subtensor=bittensor.subtensor(config=config, log_verbose=False) - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - metagraph_parser = parser.add_parser( - "metagraph", help="""View a subnet metagraph information.""" - ) - metagraph_parser.add_argument( - "--netuid", - dest="netuid", - type=int, - help="""Set the netuid to get the metagraph of""", - default=False, - ) - - bittensor.subtensor.add_args(metagraph_parser) diff --git a/bittensor/commands/misc.py b/bittensor/commands/misc.py deleted file mode 100644 index ded1c7804..000000000 --- a/bittensor/commands/misc.py +++ /dev/null @@ -1,117 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 os -import argparse -import bittensor -from rich.prompt import Prompt -from rich.table import Table - -console = bittensor.__console__ - - -class UpdateCommand: - """ - Executes the ``update`` command to update the local Bittensor package. - - This command performs a series of operations to ensure that the user's local Bittensor installation is updated to the latest version from the master branch of its GitHub repository. It primarily involves pulling the latest changes from the repository and reinstalling the package. - - Usage: - Upon invocation, the command first checks the user's configuration for the ``no_prompt`` setting. If ``no_prompt`` is set to ``True``, or if the user explicitly confirms with ``Y`` when prompted, the command proceeds to update the local Bittensor package. It changes the current directory to the Bittensor package directory, checks out the master branch of the Bittensor repository, pulls the latest changes, and then reinstalls the package using ``pip``. - - The command structure is as follows: - - 1. Change directory to the Bittensor package directory. - 2. Check out the master branch of the Bittensor GitHub repository. - 3. Pull the latest changes with the ``--ff-only`` option to ensure a fast-forward update. - 4. Reinstall the Bittensor package using pip. - - Example usage:: - - btcli legacy update - - Note: - This command is intended to be used within the Bittensor CLI to facilitate easy updates of the Bittensor package. It should be used with caution as it directly affects the local installation of the package. It is recommended to ensure that any important data or configurations are backed up before running this command. - """ - - @staticmethod - def run(cli): - if cli.config.no_prompt or cli.config.answer == "Y": - os.system( - " (cd ~/.bittensor/bittensor/ ; git checkout master ; git pull --ff-only )" - ) - os.system("pip install -e ~/.bittensor/bittensor/") - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.no_prompt: - answer = Prompt.ask( - "This will update the local bittensor package", - choices=["Y", "N"], - default="Y", - ) - config.answer = answer - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - update_parser = parser.add_parser( - "update", add_help=False, help="""Update bittensor """ - ) - - bittensor.subtensor.add_args(update_parser) - - -class AutocompleteCommand: - """Show users how to install and run autocompletion for Bittensor CLI.""" - - @staticmethod - def run(cli): - console = bittensor.__console__ - shell_commands = { - "Bash": "btcli --print-completion bash >> ~/.bashrc", - "Zsh": "btcli --print-completion zsh >> ~/.zshrc", - "Tcsh": "btcli --print-completion tcsh >> ~/.tcshrc", - } - - table = Table(show_header=True, header_style="bold magenta") - table.add_column("Shell", style="dim", width=12) - table.add_column("Command to Enable Autocompletion", justify="left") - - for shell, command in shell_commands.items(): - table.add_row(shell, command) - - console.print( - "To enable autocompletion for Bittensor CLI, run the appropriate command for your shell:" - ) - console.print(table) - - console.print( - "\n[bold]After running the command, execute the following to apply the changes:[/bold]" - ) - console.print(" [yellow]source ~/.bashrc[/yellow] # For Bash and Zsh") - console.print(" [yellow]source ~/.tcshrc[/yellow] # For Tcsh") - - @staticmethod - def add_args(parser): - parser.add_parser( - "autocomplete", - help="Instructions for enabling autocompletion for Bittensor CLI.", - ) - - @staticmethod - def check_config(config): - pass diff --git a/bittensor/commands/network.py b/bittensor/commands/network.py deleted file mode 100644 index 56afecb90..000000000 --- a/bittensor/commands/network.py +++ /dev/null @@ -1,672 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 bittensor -from . import defaults # type: ignore -from rich.prompt import Prompt -from rich.table import Table -from typing import List, Optional, Dict, Union, Tuple -from .utils import ( - get_delegates_details, - DelegatesDetails, - check_netuid_set, - normalize_hyperparameters, -) -from .identity import SetIdentityCommand - -console = bittensor.__console__ - - -class RegisterSubnetworkCommand: - """ - Executes the ``register_subnetwork`` command to register a new subnetwork on the Bittensor network. - - This command facilitates the creation and registration of a subnetwork, which involves interaction with the user's wallet and the Bittensor subtensor. It ensures that the user has the necessary credentials and configurations to successfully register a new subnetwork. - - Usage: - Upon invocation, the command performs several key steps to register a subnetwork: - - 1. It copies the user's current configuration settings. - 2. It accesses the user's wallet using the provided configuration. - 3. It initializes the Bittensor subtensor object with the user's configuration. - 4. It then calls the ``register_subnetwork`` function of the subtensor object, passing the user's wallet and a prompt setting based on the user's configuration. - - If the user's configuration does not specify a wallet name and ``no_prompt`` is not set, the command will prompt the user to enter a wallet name. This name is then used in the registration process. - - The command structure includes: - - - Copying the user's configuration. - - Accessing and preparing the user's wallet. - - Initializing the Bittensor subtensor. - - Registering the subnetwork with the necessary credentials. - - Example usage:: - - btcli subnets create - - Note: - This command is intended for advanced users of the Bittensor network who wish to contribute by adding new subnetworks. It requires a clear understanding of the network's functioning and the roles of subnetworks. Users should ensure that they have secured their wallet and are aware of the implications of adding a new subnetwork to the Bittensor ecosystem. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Register a subnetwork""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - RegisterSubnetworkCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Register a subnetwork""" - wallet = bittensor.wallet(config=cli.config) - - # Call register command. - success = subtensor.register_subnetwork( - wallet=wallet, - prompt=not cli.config.no_prompt, - ) - if success and not cli.config.no_prompt: - # Prompt for user to set identity. - do_set_identity = Prompt.ask( - f"Subnetwork registered successfully. Would you like to set your identity? [y/n]", - choices=["y", "n"], - ) - - if do_set_identity.lower() == "y": - subtensor.close() - config = cli.config.copy() - SetIdentityCommand.check_config(config) - cli.config = config - SetIdentityCommand.run(cli) - - @classmethod - def check_config(cls, config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - parser = parser.add_parser( - "create", - help="""Create a new bittensor subnetwork on this chain.""", - ) - - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) - - -class SubnetLockCostCommand: - """ - Executes the ``lock_cost`` command to view the locking cost required for creating a new subnetwork on the Bittensor network. - - This command is designed to provide users with the current cost of registering a new subnetwork, which is a critical piece of information for anyone considering expanding the network's infrastructure. - - The current implementation anneals the cost of creating a subnet over a period of two days. If the cost is unappealing currently, check back in a day or two to see if it has reached an amenble level. - - Usage: - Upon invocation, the command performs the following operations: - - 1. It copies the user's current Bittensor configuration. - 2. It initializes the Bittensor subtensor object with this configuration. - 3. It then retrieves the subnet lock cost using the ``get_subnet_burn_cost()`` method from the subtensor object. - 4. The cost is displayed to the user in a readable format, indicating the amount of Tao required to lock for registering a new subnetwork. - - In case of any errors during the process (e.g., network issues, configuration problems), the command will catch these exceptions and inform the user that it failed to retrieve the lock cost, along with the specific error encountered. - - The command structure includes: - - - Copying and using the user's configuration for Bittensor. - - Retrieving the current subnet lock cost from the Bittensor network. - - Displaying the cost in a user-friendly manner. - - Example usage:: - - btcli subnets lock_cost - - Note: - This command is particularly useful for users who are planning to contribute to the Bittensor network by adding new subnetworks. Understanding the lock cost is essential for these users to make informed decisions about their potential contributions and investments in the network. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""View locking cost of creating a new subnetwork""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - SubnetLockCostCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""View locking cost of creating a new subnetwork""" - config = cli.config.copy() - try: - bittensor.__console__.print( - f"Subnet lock cost: [green]{bittensor.utils.balance.Balance( subtensor.get_subnet_burn_cost() )}[/green]" - ) - except Exception as e: - bittensor.__console__.print( - f"Subnet lock cost: [red]Failed to get subnet lock cost[/red]" - f"Error: {e}" - ) - - @classmethod - def check_config(cls, config: "bittensor.config"): - pass - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - parser = parser.add_parser( - "lock_cost", - help=""" Return the lock cost to register a subnet""", - ) - - bittensor.subtensor.add_args(parser) - - -class SubnetListCommand: - """ - Executes the ``list`` command to list all subnets and their detailed information on the Bittensor network. - - This command is designed to provide users with comprehensive information about each subnet within the - network, including its unique identifier (netuid), the number of neurons, maximum neuron capacity, - emission rate, tempo, recycle register cost (burn), proof of work (PoW) difficulty, and the name or - SS58 address of the subnet owner. - - Usage: - Upon invocation, the command performs the following actions: - - 1. It initializes the Bittensor subtensor object with the user's configuration. - 2. It retrieves a list of all subnets in the network along with their detailed information. - 3. The command compiles this data into a table format, displaying key information about each subnet. - - In addition to the basic subnet details, the command also fetches delegate information to provide the - name of the subnet owner where available. If the owner's name is not available, the owner's ``SS58`` - address is displayed. - - The command structure includes: - - - Initializing the Bittensor subtensor and retrieving subnet information. - - Calculating the total number of neurons across all subnets. - - Constructing a table that includes columns for ``NETUID``, ``N`` (current neurons), ``MAX_N`` (maximum neurons), ``EMISSION``, ``TEMPO``, ``BURN``, ``POW`` (proof of work difficulty), and ``SUDO`` (owner's name or ``SS58`` address). - - Displaying the table with a footer that summarizes the total number of subnets and neurons. - - Example usage:: - - btcli subnets list - - Note: - This command is particularly useful for users seeking an overview of the Bittensor network's structure and the distribution of its resources and ownership information for each subnet. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""List all subnet netuids in the network.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - SubnetListCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""List all subnet netuids in the network.""" - subnets: List[bittensor.SubnetInfo] = subtensor.get_all_subnets_info() - - rows = [] - total_neurons = 0 - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - subtensor=subtensor - ) - - for subnet in subnets: - total_neurons += subnet.max_n - rows.append( - ( - str(subnet.netuid), - str(subnet.subnetwork_n), - str(bittensor.utils.formatting.millify(subnet.max_n)), - f"{subnet.emission_value / bittensor.utils.RAOPERTAO * 100:0.2f}%", - str(subnet.tempo), - f"{subnet.burn!s:8.8}", - str(bittensor.utils.formatting.millify(subnet.difficulty)), - f"{delegate_info[subnet.owner_ss58].display if subnet.owner_ss58 in delegate_info else subnet.owner_ss58}", - ) - ) - table = Table( - show_footer=True, - width=cli.config.get("width", None), - pad_edge=True, - box=None, - show_edge=True, - ) - table.title = "[white]Subnets - {}".format(subtensor.network) - table.add_column( - "[overline white]NETUID", - str(len(subnets)), - footer_style="overline white", - style="bold green", - justify="center", - ) - table.add_column( - "[overline white]N", - str(total_neurons), - footer_style="overline white", - style="green", - justify="center", - ) - table.add_column("[overline white]MAX_N", style="white", justify="center") - table.add_column("[overline white]EMISSION", style="white", justify="center") - table.add_column("[overline white]TEMPO", style="white", justify="center") - table.add_column("[overline white]RECYCLE", style="white", justify="center") - table.add_column("[overline white]POW", style="white", justify="center") - table.add_column("[overline white]SUDO", style="white") - for row in rows: - table.add_row(*row) - bittensor.__console__.print(table) - - @staticmethod - def check_config(config: "bittensor.config"): - pass - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - list_subnets_parser = parser.add_parser( - "list", help="""List all subnets on the network""" - ) - bittensor.subtensor.add_args(list_subnets_parser) - - -HYPERPARAMS = { - "serving_rate_limit": "sudo_set_serving_rate_limit", - "min_difficulty": "sudo_set_min_difficulty", - "max_difficulty": "sudo_set_max_difficulty", - "weights_version": "sudo_set_weights_version_key", - "weights_rate_limit": "sudo_set_weights_set_rate_limit", - "max_weight_limit": "sudo_set_max_weight_limit", - "immunity_period": "sudo_set_immunity_period", - "min_allowed_weights": "sudo_set_min_allowed_weights", - "activity_cutoff": "sudo_set_activity_cutoff", - "network_registration_allowed": "sudo_set_network_registration_allowed", - "network_pow_registration_allowed": "sudo_set_network_pow_registration_allowed", - "min_burn": "sudo_set_min_burn", - "max_burn": "sudo_set_max_burn", - "adjustment_alpha": "sudo_set_adjustment_alpha", - "rho": "sudo_set_rho", - "kappa": "sudo_set_kappa", - "difficulty": "sudo_set_difficulty", - "bonds_moving_avg": "sudo_set_bonds_moving_average", - "commit_reveal_weights_interval": "sudo_set_commit_reveal_weights_interval", - "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", - "alpha_values": "sudo_set_alpha_values", - "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", -} - - -class SubnetSudoCommand: - """ - Executes the ``set`` command to set hyperparameters for a specific subnet on the Bittensor network. - - This command allows subnet owners to modify various hyperparameters of theirs subnet, such as its tempo, - emission rates, and other network-specific settings. - - Usage: - The command first prompts the user to enter the hyperparameter they wish to change and its new value. - It then uses the user's wallet and configuration settings to authenticate and send the hyperparameter update - to the specified subnet. - - Example usage:: - - btcli sudo set --netuid 1 --param 'tempo' --value '0.5' - - Note: - This command requires the user to specify the subnet identifier (``netuid``) and both the hyperparameter - and its new value. It is intended for advanced users who are familiar with the network's functioning - and the impact of changing these parameters. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Set subnet hyperparameters.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - SubnetSudoCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run( - cli: "bittensor.cli", - subtensor: "bittensor.subtensor", - ): - r"""Set subnet hyperparameters.""" - wallet = bittensor.wallet(config=cli.config) - print("\n") - SubnetHyperparamsCommand.run(cli) - if not cli.config.is_set("param") and not cli.config.no_prompt: - param = Prompt.ask("Enter hyperparameter", choices=HYPERPARAMS) - cli.config.param = str(param) - if not cli.config.is_set("value") and not cli.config.no_prompt: - value = Prompt.ask("Enter new value") - cli.config.value = value - - if ( - cli.config.param == "network_registration_allowed" - or cli.config.param == "network_pow_registration_allowed" - or cli.config.param == "commit_reveal_weights_enabled" - or cli.config.param == "liquid_alpha_enabled" - ): - cli.config.value = ( - True - if (cli.config.value.lower() == "true" or cli.config.value == "1") - else False - ) - - is_allowed_value, value = allowed_value(cli.config.param, cli.config.value) - if not is_allowed_value: - raise ValueError( - f"Hyperparameter {cli.config.param} value is not within bounds. Value is {cli.config.value} but must be {value}" - ) - - subtensor.set_hyperparameter( - wallet, - netuid=cli.config.netuid, - parameter=cli.config.param, - value=value, - prompt=not cli.config.no_prompt, - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("netuid") and not config.no_prompt: - check_netuid_set( - config, bittensor.subtensor(config=config, log_verbose=False) - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser("set", help="""Set hyperparameters for a subnet""") - parser.add_argument( - "--netuid", dest="netuid", type=int, required=False, default=False - ) - parser.add_argument("--param", dest="param", type=str, required=False) - parser.add_argument("--value", dest="value", type=str, required=False) - - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) - - -class SubnetHyperparamsCommand: - """ - Executes the '``hyperparameters``' command to view the current hyperparameters of a specific subnet on the Bittensor network. - - This command is useful for users who wish to understand the configuration and - operational parameters of a particular subnet. - - Usage: - Upon invocation, the command fetches and displays a list of all hyperparameters for the specified subnet. - These include settings like tempo, emission rates, and other critical network parameters that define - the subnet's behavior. - - Example usage:: - - $ btcli subnets hyperparameters --netuid 1 - - Subnet Hyperparameters - NETUID: 1 - finney - HYPERPARAMETER VALUE - rho 10 - kappa 32767 - immunity_period 7200 - min_allowed_weights 8 - max_weight_limit 455 - tempo 99 - min_difficulty 1000000000000000000 - max_difficulty 1000000000000000000 - weights_version 2013 - weights_rate_limit 100 - adjustment_interval 112 - activity_cutoff 5000 - registration_allowed True - target_regs_per_interval 2 - min_burn 1000000000 - max_burn 100000000000 - bonds_moving_avg 900000 - max_regs_per_block 1 - - Note: - The user must specify the subnet identifier (``netuid``) for which they want to view the hyperparameters. - This command is read-only and does not modify the network state or configurations. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""View hyperparameters of a subnetwork.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - SubnetHyperparamsCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""View hyperparameters of a subnetwork.""" - subnet: bittensor.SubnetHyperparameters = subtensor.get_subnet_hyperparameters( - cli.config.netuid - ) - - table = Table( - show_footer=True, - width=cli.config.get("width", None), - pad_edge=True, - box=None, - show_edge=True, - ) - table.title = "[white]Subnet Hyperparameters - NETUID: {} - {}".format( - cli.config.netuid, subtensor.network - ) - table.add_column("[overline white]HYPERPARAMETER", style="white") - table.add_column("[overline white]VALUE", style="green") - table.add_column("[overline white]NORMALIZED", style="cyan") - - normalized_values = normalize_hyperparameters(subnet) - - for param, value, norm_value in normalized_values: - table.add_row(" " + param, value, norm_value) - - bittensor.__console__.print(table) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("netuid") and not config.no_prompt: - check_netuid_set( - config, bittensor.subtensor(config=config, log_verbose=False) - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "hyperparameters", help="""View subnet hyperparameters""" - ) - parser.add_argument( - "--netuid", dest="netuid", type=int, required=False, default=False - ) - bittensor.subtensor.add_args(parser) - - -class SubnetGetHyperparamsCommand: - """ - Executes the ``get`` command to retrieve the hyperparameters of a specific subnet on the Bittensor network. - - This command is similar to the ``hyperparameters`` command but may be used in different contexts within the CLI. - - Usage: - The command connects to the Bittensor network, queries the specified subnet, and returns a detailed list - of all its hyperparameters. This includes crucial operational parameters that determine the subnet's - performance and interaction within the network. - - Example usage:: - - $ btcli sudo get --netuid 1 - - Subnet Hyperparameters - NETUID: 1 - finney - HYPERPARAMETER VALUE - rho 10 - kappa 32767 - immunity_period 7200 - min_allowed_weights 8 - max_weight_limit 455 - tempo 99 - min_difficulty 1000000000000000000 - max_difficulty 1000000000000000000 - weights_version 2013 - weights_rate_limit 100 - adjustment_interval 112 - activity_cutoff 5000 - registration_allowed True - target_regs_per_interval 2 - min_burn 1000000000 - max_burn 100000000000 - bonds_moving_avg 900000 - max_regs_per_block 1 - - Note: - Users need to provide the ``netuid`` of the subnet whose hyperparameters they wish to view. This command is - designed for informational purposes and does not alter any network settings or configurations. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""View hyperparameters of a subnetwork.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - SubnetGetHyperparamsCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""View hyperparameters of a subnetwork.""" - subnet: bittensor.SubnetHyperparameters = subtensor.get_subnet_hyperparameters( - cli.config.netuid - ) - - table = Table( - show_footer=True, - width=cli.config.get("width", None), - pad_edge=True, - box=None, - show_edge=True, - ) - table.title = "[white]Subnet Hyperparameters - NETUID: {} - {}".format( - cli.config.netuid, subtensor.network - ) - table.add_column("[overline white]HYPERPARAMETER", style="white") - table.add_column("[overline white]VALUE", style="green") - table.add_column("[overline white]NORMALIZED", style="cyan") - - normalized_values = normalize_hyperparameters(subnet) - - for param, value, norm_value in normalized_values: - table.add_row(" " + param, value, norm_value) - - bittensor.__console__.print(table) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("netuid") and not config.no_prompt: - check_netuid_set( - config, bittensor.subtensor(config=config, log_verbose=False) - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser("get", help="""View subnet hyperparameters""") - parser.add_argument( - "--netuid", dest="netuid", type=int, required=False, default=False - ) - bittensor.subtensor.add_args(parser) - - -def allowed_value( - param: str, value: Union[str, bool, float] -) -> Tuple[bool, Union[str, list[float], float]]: - """ - Check the allowed values on hyperparameters. Return False if value is out of bounds. - """ - # Reminder error message ends like: Value is {value} but must be {error_message}. (the second part of return statement) - # Check if value is a boolean, only allow boolean and floats - try: - if not isinstance(value, bool): - if param == "alpha_values": - # Split the string into individual values - alpha_low_str, alpha_high_str = value.split(",") - alpha_high = float(alpha_high_str) - alpha_low = float(alpha_low_str) - - # Check alpha_high value - if alpha_high <= 52428 or alpha_high >= 65535: - return ( - False, - f"between 52428 and 65535 for alpha_high (but is {alpha_high})", - ) - - # Check alpha_low value - if alpha_low < 0 or alpha_low > 52428: - return ( - False, - f"between 0 and 52428 for alpha_low (but is {alpha_low})", - ) - - return True, [alpha_low, alpha_high] - except ValueError: - return False, "a number or a boolean" - - return True, value diff --git a/bittensor/commands/overview.py b/bittensor/commands/overview.py deleted file mode 100644 index b572847e4..000000000 --- a/bittensor/commands/overview.py +++ /dev/null @@ -1,778 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 bittensor -from tqdm import tqdm -from concurrent.futures import ProcessPoolExecutor -from collections import defaultdict -from fuzzywuzzy import fuzz -from rich.align import Align -from rich.table import Table -from rich.prompt import Prompt -from typing import List, Optional, Dict, Tuple -from .utils import ( - get_hotkey_wallets_for_wallet, - get_coldkey_wallets_for_path, - get_all_wallets_for_path, - filter_netuids_by_registered_hotkeys, -) -from . import defaults - -console = bittensor.__console__ - - -class OverviewCommand: - """ - Executes the ``overview`` command to present a detailed overview of the user's registered accounts on the Bittensor network. - - This command compiles and displays comprehensive information about each neuron associated with the user's wallets, - including both hotkeys and coldkeys. It is especially useful for users managing multiple accounts or seeking a summary - of their network activities and stake distributions. - - Usage: - The command offers various options to customize the output. Users can filter the displayed data by specific netuids, - sort by different criteria, and choose to include all wallets in the user's configuration directory. The output is - presented in a tabular format with the following columns: - - - COLDKEY: The SS58 address of the coldkey. - - HOTKEY: The SS58 address of the hotkey. - - UID: Unique identifier of the neuron. - - ACTIVE: Indicates if the neuron is active. - - STAKE(τ): Amount of stake in the neuron, in Tao. - - RANK: The rank of the neuron within the network. - - TRUST: Trust score of the neuron. - - CONSENSUS: Consensus score of the neuron. - - INCENTIVE: Incentive score of the neuron. - - DIVIDENDS: Dividends earned by the neuron. - - EMISSION(p): Emission received by the neuron, in Rho. - - VTRUST: Validator trust score of the neuron. - - VPERMIT: Indicates if the neuron has a validator permit. - - UPDATED: Time since last update. - - AXON: IP address and port of the neuron. - - HOTKEY_SS58: Human-readable representation of the hotkey. - - Example usage:: - - btcli wallet overview - btcli wallet overview --all --sort_by stake --sort_order descending - - Note: - This command is read-only and does not modify the network state or account configurations. It provides a quick and - comprehensive view of the user's network presence, making it ideal for monitoring account status, stake distribution, - and overall contribution to the Bittensor network. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Prints an overview for the wallet's colkey.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - OverviewCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _get_total_balance( - total_balance: "bittensor.Balance", - subtensor: "bittensor.subtensor", - cli: "bittensor.cli", - ) -> Tuple[List["bittensor.wallet"], "bittensor.Balance"]: - if cli.config.get("all", d=None): - cold_wallets = get_coldkey_wallets_for_path(cli.config.wallet.path) - for cold_wallet in tqdm(cold_wallets, desc="Pulling balances"): - if ( - cold_wallet.coldkeypub_file.exists_on_device() - and not cold_wallet.coldkeypub_file.is_encrypted() - ): - total_balance = total_balance + subtensor.get_balance( - cold_wallet.coldkeypub.ss58_address - ) - all_hotkeys = get_all_wallets_for_path(cli.config.wallet.path) - else: - # We are only printing keys for a single coldkey - coldkey_wallet = bittensor.wallet(config=cli.config) - if ( - coldkey_wallet.coldkeypub_file.exists_on_device() - and not coldkey_wallet.coldkeypub_file.is_encrypted() - ): - total_balance = subtensor.get_balance( - coldkey_wallet.coldkeypub.ss58_address - ) - if not coldkey_wallet.coldkeypub_file.exists_on_device(): - console.print("[bold red]No wallets found.") - return [], None - all_hotkeys = get_hotkey_wallets_for_wallet(coldkey_wallet) - - return all_hotkeys, total_balance - - @staticmethod - def _get_hotkeys( - cli: "bittensor.cli", all_hotkeys: List["bittensor.wallet"] - ) -> List["bittensor.wallet"]: - if not cli.config.get("all_hotkeys", False): - # We are only showing hotkeys that are specified. - all_hotkeys = [ - hotkey - for hotkey in all_hotkeys - if hotkey.hotkey_str in cli.config.hotkeys - ] - else: - # We are excluding the specified hotkeys from all_hotkeys. - all_hotkeys = [ - hotkey - for hotkey in all_hotkeys - if hotkey.hotkey_str not in cli.config.hotkeys - ] - return all_hotkeys - - @staticmethod - def _get_key_address(all_hotkeys: List["bittensor.wallet"]): - hotkey_coldkey_to_hotkey_wallet = {} - for hotkey_wallet in all_hotkeys: - if hotkey_wallet.hotkey.ss58_address not in hotkey_coldkey_to_hotkey_wallet: - hotkey_coldkey_to_hotkey_wallet[hotkey_wallet.hotkey.ss58_address] = {} - - hotkey_coldkey_to_hotkey_wallet[hotkey_wallet.hotkey.ss58_address][ - hotkey_wallet.coldkeypub.ss58_address - ] = hotkey_wallet - - all_hotkey_addresses = list(hotkey_coldkey_to_hotkey_wallet.keys()) - - return all_hotkey_addresses, hotkey_coldkey_to_hotkey_wallet - - @staticmethod - def _process_neuron_results( - results: List[Tuple[int, List["bittensor.NeuronInfoLite"], Optional[str]]], - neurons: Dict[str, List["bittensor.NeuronInfoLite"]], - netuids: List[int], - ) -> Dict[str, List["bittensor.NeuronInfoLite"]]: - for result in results: - netuid, neurons_result, err_msg = result - if err_msg is not None: - console.print(f"netuid '{netuid}': {err_msg}") - - if len(neurons_result) == 0: - # Remove netuid from overview if no neurons are found. - netuids.remove(netuid) - del neurons[str(netuid)] - else: - # Add neurons to overview. - neurons[str(netuid)] = neurons_result - return neurons - - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Prints an overview for the wallet's colkey.""" - console = bittensor.__console__ - wallet = bittensor.wallet(config=cli.config) - - all_hotkeys = [] - total_balance = bittensor.Balance(0) - - # We are printing for every coldkey. - all_hotkeys, total_balance = OverviewCommand._get_total_balance( - total_balance, subtensor, cli - ) - - # We are printing for a select number of hotkeys from all_hotkeys. - if cli.config.get("hotkeys"): - all_hotkeys = OverviewCommand._get_hotkeys(cli, all_hotkeys) - - # Check we have keys to display. - if len(all_hotkeys) == 0: - console.print("[red]No wallets found.[/red]") - return - - # Pull neuron info for all keys. - neurons: Dict[str, List[bittensor.NeuronInfoLite]] = {} - block = subtensor.block - - netuids = subtensor.get_all_subnet_netuids() - netuids = filter_netuids_by_registered_hotkeys( - cli, subtensor, netuids, all_hotkeys - ) - bittensor.logging.debug(f"Netuids to check: {netuids}") - - for netuid in netuids: - neurons[str(netuid)] = [] - - all_wallet_names = {wallet.name for wallet in all_hotkeys} - all_coldkey_wallets = [ - bittensor.wallet(name=wallet_name) for wallet_name in all_wallet_names - ] - - ( - all_hotkey_addresses, - hotkey_coldkey_to_hotkey_wallet, - ) = OverviewCommand._get_key_address(all_hotkeys) - - with console.status( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - cli.config.subtensor.get( - "network", bittensor.defaults.subtensor.network - ) - ) - ): - # Create a copy of the config without the parser and formatter_class. - ## This is needed to pass to the ProcessPoolExecutor, which cannot pickle the parser. - copy_config = cli.config.copy() - copy_config["__parser"] = None - copy_config["formatter_class"] = None - - # Pull neuron info for all keys. - ## Max len(netuids) or 5 threads. - with ProcessPoolExecutor(max_workers=max(len(netuids), 5)) as executor: - results = executor.map( - OverviewCommand._get_neurons_for_netuid, - [(copy_config, netuid, all_hotkey_addresses) for netuid in netuids], - ) - executor.shutdown(wait=True) # wait for all complete - - neurons = OverviewCommand._process_neuron_results( - results, neurons, netuids - ) - - total_coldkey_stake_from_metagraph = defaultdict( - lambda: bittensor.Balance(0.0) - ) - checked_hotkeys = set() - for neuron_list in neurons.values(): - for neuron in neuron_list: - if neuron.hotkey in checked_hotkeys: - continue - total_coldkey_stake_from_metagraph[neuron.coldkey] += ( - neuron.stake_dict[neuron.coldkey] - ) - checked_hotkeys.add(neuron.hotkey) - - alerts_table = Table(show_header=True, header_style="bold magenta") - alerts_table.add_column("🥩 alert!") - - coldkeys_to_check = [] - for coldkey_wallet in all_coldkey_wallets: - # Check if we have any stake with hotkeys that are not registered. - total_coldkey_stake_from_chain = subtensor.get_total_stake_for_coldkey( - ss58_address=coldkey_wallet.coldkeypub.ss58_address - ) - difference = ( - total_coldkey_stake_from_chain - - total_coldkey_stake_from_metagraph[ - coldkey_wallet.coldkeypub.ss58_address - ] - ) - if difference == 0: - continue # We have all our stake registered. - - coldkeys_to_check.append(coldkey_wallet) - alerts_table.add_row( - "Found {} stake with coldkey {} that is not registered.".format( - difference, coldkey_wallet.coldkeypub.ss58_address - ) - ) - - if coldkeys_to_check: - # We have some stake that is not with a registered hotkey. - if "-1" not in neurons: - neurons["-1"] = [] - - # Use process pool to check each coldkey wallet for de-registered stake. - with ProcessPoolExecutor( - max_workers=max(len(coldkeys_to_check), 5) - ) as executor: - results = executor.map( - OverviewCommand._get_de_registered_stake_for_coldkey_wallet, - [ - (cli.config, all_hotkey_addresses, coldkey_wallet) - for coldkey_wallet in coldkeys_to_check - ], - ) - executor.shutdown(wait=True) # wait for all complete - - for result in results: - coldkey_wallet, de_registered_stake, err_msg = result - if err_msg is not None: - console.print(err_msg) - - if len(de_registered_stake) == 0: - continue # We have no de-registered stake with this coldkey. - - de_registered_neurons = [] - for hotkey_addr, our_stake in de_registered_stake: - # Make a neuron info lite for this hotkey and coldkey. - de_registered_neuron = bittensor.NeuronInfoLite.get_null_neuron() - de_registered_neuron.hotkey = hotkey_addr - de_registered_neuron.coldkey = ( - coldkey_wallet.coldkeypub.ss58_address - ) - de_registered_neuron.total_stake = bittensor.Balance(our_stake) - - de_registered_neurons.append(de_registered_neuron) - - # Add this hotkey to the wallets dict - wallet_ = bittensor.wallet( - name=wallet, - ) - wallet_.hotkey_ss58 = hotkey_addr - wallet.hotkey_str = hotkey_addr[:5] # Max length of 5 characters - # Indicates a hotkey not on local machine but exists in stake_info obj on-chain - if hotkey_coldkey_to_hotkey_wallet.get(hotkey_addr) is None: - hotkey_coldkey_to_hotkey_wallet[hotkey_addr] = {} - hotkey_coldkey_to_hotkey_wallet[hotkey_addr][ - coldkey_wallet.coldkeypub.ss58_address - ] = wallet_ - - # Add neurons to overview. - neurons["-1"].extend(de_registered_neurons) - - # Setup outer table. - grid = Table.grid(pad_edge=False) - - # If there are any alerts, add them to the grid - if len(alerts_table.rows) > 0: - grid.add_row(alerts_table) - - title: str = "" - if not cli.config.get("all", d=None): - title = "[bold white italic]Wallet - {}:{}".format( - cli.config.wallet.name, wallet.coldkeypub.ss58_address - ) - else: - title = "[bold whit italic]All Wallets:" - - # Add title - grid.add_row(Align(title, vertical="middle", align="center")) - - # Generate rows per netuid - hotkeys_seen = set() - total_neurons = 0 - total_stake = 0.0 - for netuid in netuids: - subnet_tempo = subtensor.tempo(netuid=netuid) - last_subnet = netuid == netuids[-1] - TABLE_DATA = [] - total_rank = 0.0 - total_trust = 0.0 - total_consensus = 0.0 - total_validator_trust = 0.0 - total_incentive = 0.0 - total_dividends = 0.0 - total_emission = 0 - - for nn in neurons[str(netuid)]: - hotwallet = hotkey_coldkey_to_hotkey_wallet.get(nn.hotkey, {}).get( - nn.coldkey, None - ) - if not hotwallet: - # Indicates a mismatch between what the chain says the coldkey - # is for this hotkey and the local wallet coldkey-hotkey pair - hotwallet = argparse.Namespace() - hotwallet.name = nn.coldkey[:7] - hotwallet.hotkey_str = nn.hotkey[:7] - nn: bittensor.NeuronInfoLite - uid = nn.uid - active = nn.active - stake = nn.total_stake.tao - rank = nn.rank - trust = nn.trust - consensus = nn.consensus - validator_trust = nn.validator_trust - incentive = nn.incentive - dividends = nn.dividends - emission = int(nn.emission / (subnet_tempo + 1) * 1e9) - last_update = int(block - nn.last_update) - validator_permit = nn.validator_permit - row = [ - hotwallet.name, - hotwallet.hotkey_str, - str(uid), - str(active), - "{:.5f}".format(stake), - "{:.5f}".format(rank), - "{:.5f}".format(trust), - "{:.5f}".format(consensus), - "{:.5f}".format(incentive), - "{:.5f}".format(dividends), - "{:_}".format(emission), - "{:.5f}".format(validator_trust), - "*" if validator_permit else "", - str(last_update), - ( - bittensor.utils.networking.int_to_ip(nn.axon_info.ip) - + ":" - + str(nn.axon_info.port) - if nn.axon_info.port != 0 - else "[yellow]none[/yellow]" - ), - nn.hotkey, - ] - - total_rank += rank - total_trust += trust - total_consensus += consensus - total_incentive += incentive - total_dividends += dividends - total_emission += emission - total_validator_trust += validator_trust - - if not (nn.hotkey, nn.coldkey) in hotkeys_seen: - # Don't double count stake on hotkey-coldkey pairs. - hotkeys_seen.add((nn.hotkey, nn.coldkey)) - total_stake += stake - - # netuid -1 are neurons that are de-registered. - if netuid != "-1": - total_neurons += 1 - - TABLE_DATA.append(row) - - # Add subnet header - if netuid == "-1": - grid.add_row(f"Deregistered Neurons") - else: - grid.add_row(f"Subnet: [bold white]{netuid}[/bold white]") - - table = Table( - show_footer=False, - width=cli.config.get("width", None), - pad_edge=False, - box=None, - ) - if last_subnet: - table.add_column( - "[overline white]COLDKEY", - str(total_neurons), - footer_style="overline white", - style="bold white", - ) - table.add_column( - "[overline white]HOTKEY", - str(total_neurons), - footer_style="overline white", - style="white", - ) - else: - # No footer for non-last subnet. - table.add_column("[overline white]COLDKEY", style="bold white") - table.add_column("[overline white]HOTKEY", style="white") - table.add_column( - "[overline white]UID", - str(total_neurons), - footer_style="overline white", - style="yellow", - ) - table.add_column( - "[overline white]ACTIVE", justify="right", style="green", no_wrap=True - ) - if last_subnet: - table.add_column( - "[overline white]STAKE(\u03c4)", - "\u03c4{:.5f}".format(total_stake), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - else: - # No footer for non-last subnet. - table.add_column( - "[overline white]STAKE(\u03c4)", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]RANK", - "{:.5f}".format(total_rank), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]TRUST", - "{:.5f}".format(total_trust), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]CONSENSUS", - "{:.5f}".format(total_consensus), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]INCENTIVE", - "{:.5f}".format(total_incentive), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]DIVIDENDS", - "{:.5f}".format(total_dividends), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]EMISSION(\u03c1)", - "\u03c1{:_}".format(total_emission), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]VTRUST", - "{:.5f}".format(total_validator_trust), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column("[overline white]VPERMIT", justify="right", no_wrap=True) - table.add_column("[overline white]UPDATED", justify="right", no_wrap=True) - table.add_column( - "[overline white]AXON", justify="left", style="dim blue", no_wrap=True - ) - table.add_column( - "[overline white]HOTKEY_SS58", style="dim blue", no_wrap=False - ) - table.show_footer = True - - sort_by: Optional[str] = cli.config.get("sort_by", None) - sort_order: Optional[str] = cli.config.get("sort_order", None) - - if sort_by is not None and sort_by != "": - column_to_sort_by: int = 0 - highest_matching_ratio: int = 0 - sort_descending: bool = False # Default sort_order to ascending - - for index, column in zip(range(len(table.columns)), table.columns): - # Fuzzy match the column name. Default to the first column. - column_name = column.header.lower().replace("[overline white]", "") - match_ratio = fuzz.ratio(sort_by.lower(), column_name) - # Finds the best matching column - if match_ratio > highest_matching_ratio: - highest_matching_ratio = match_ratio - column_to_sort_by = index - - if sort_order.lower() in {"desc", "descending", "reverse"}: - # Sort descending if the sort_order matches desc, descending, or reverse - sort_descending = True - - def overview_sort_function(row): - data = row[column_to_sort_by] - # Try to convert to number if possible - try: - data = float(data) - except ValueError: - pass - return data - - TABLE_DATA.sort(key=overview_sort_function, reverse=sort_descending) - - for row in TABLE_DATA: - table.add_row(*row) - - grid.add_row(table) - - console.clear() - - caption = "[italic][dim][white]Wallet balance: [green]\u03c4" + str( - total_balance.tao - ) - grid.add_row(Align(caption, vertical="middle", align="center")) - - # Print the entire table/grid - console.print(grid, width=cli.config.get("width", None)) - - @staticmethod - def _get_neurons_for_netuid( - args_tuple: Tuple["bittensor.Config", int, List[str]], - ) -> Tuple[int, List["bittensor.NeuronInfoLite"], Optional[str]]: - subtensor_config, netuid, hot_wallets = args_tuple - - result: List["bittensor.NeuronInfoLite"] = [] - - try: - subtensor = bittensor.subtensor(config=subtensor_config, log_verbose=False) - - all_neurons: List["bittensor.NeuronInfoLite"] = subtensor.neurons_lite( - netuid=netuid - ) - # Map the hotkeys to uids - hotkey_to_neurons = {n.hotkey: n.uid for n in all_neurons} - for hot_wallet_addr in hot_wallets: - uid = hotkey_to_neurons.get(hot_wallet_addr) - if uid is not None: - nn = all_neurons[uid] - result.append(nn) - except Exception as e: - return netuid, [], "Error: {}".format(e) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - return netuid, result, None - - @staticmethod - def _get_de_registered_stake_for_coldkey_wallet( - args_tuple, - ) -> Tuple[ - "bittensor.Wallet", List[Tuple[str, "bittensor.Balance"]], Optional[str] - ]: - subtensor_config, all_hotkey_addresses, coldkey_wallet = args_tuple - - # List of (hotkey_addr, our_stake) tuples. - result: List[Tuple[str, "bittensor.Balance"]] = [] - - try: - subtensor = bittensor.subtensor(config=subtensor_config, log_verbose=False) - - # Pull all stake for our coldkey - all_stake_info_for_coldkey = subtensor.get_stake_info_for_coldkey( - coldkey_ss58=coldkey_wallet.coldkeypub.ss58_address - ) - - ## Filter out hotkeys that are in our wallets - ## Filter out hotkeys that are delegates. - def _filter_stake_info(stake_info: "bittensor.StakeInfo") -> bool: - if stake_info.stake == 0: - return False # Skip hotkeys that we have no stake with. - if stake_info.hotkey_ss58 in all_hotkey_addresses: - return False # Skip hotkeys that are in our wallets. - if subtensor.is_hotkey_delegate(hotkey_ss58=stake_info.hotkey_ss58): - return False # Skip hotkeys that are delegates, they show up in btcli my_delegates table. - - return True - - all_staked_hotkeys = filter(_filter_stake_info, all_stake_info_for_coldkey) - result = [ - ( - stake_info.hotkey_ss58, - stake_info.stake.tao, - ) # stake is a Balance object - for stake_info in all_staked_hotkeys - ] - - except Exception as e: - return coldkey_wallet, [], "Error: {}".format(e) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - return coldkey_wallet, result, None - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - overview_parser = parser.add_parser( - "overview", help="""Show registered account overview.""" - ) - overview_parser.add_argument( - "--all", - dest="all", - action="store_true", - help="""View overview for all wallets.""", - default=False, - ) - overview_parser.add_argument( - "--width", - dest="width", - action="store", - type=int, - help="""Set the output width of the overview. Defaults to automatic width from terminal.""", - default=None, - ) - overview_parser.add_argument( - "--sort_by", - "--wallet.sort_by", - dest="sort_by", - required=False, - action="store", - default="", - type=str, - help="""Sort the hotkeys by the specified column title (e.g. name, uid, axon).""", - ) - overview_parser.add_argument( - "--sort_order", - "--wallet.sort_order", - dest="sort_order", - required=False, - action="store", - default="ascending", - type=str, - help="""Sort the hotkeys in the specified ordering. (ascending/asc or descending/desc/reverse)""", - ) - overview_parser.add_argument( - "--hotkeys", - "--exclude_hotkeys", - "--wallet.hotkeys", - "--wallet.exclude_hotkeys", - required=False, - action="store", - default=[], - type=str, - nargs="*", - help="""Specify the hotkeys by name or ss58 address. (e.g. hk1 hk2 hk3)""", - ) - overview_parser.add_argument( - "--all_hotkeys", - "--wallet.all_hotkeys", - required=False, - action="store_true", - default=False, - help="""To specify all hotkeys. Specifying hotkeys will exclude them from this all.""", - ) - overview_parser.add_argument( - "--netuids", - dest="netuids", - type=int, - nargs="*", - help="""Set the netuid(s) to filter by.""", - default=None, - ) - bittensor.wallet.add_args(overview_parser) - bittensor.subtensor.add_args(overview_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if ( - not config.is_set("wallet.name") - and not config.no_prompt - and not config.get("all", d=None) - ): - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if config.netuids != [] and config.netuids != None: - if not isinstance(config.netuids, list): - config.netuids = [int(config.netuids)] - else: - config.netuids = [int(netuid) for netuid in config.netuids] diff --git a/bittensor/commands/register.py b/bittensor/commands/register.py deleted file mode 100644 index a5a14773a..000000000 --- a/bittensor/commands/register.py +++ /dev/null @@ -1,613 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 sys -import argparse -import bittensor -from rich.prompt import Prompt, Confirm -from .utils import check_netuid_set, check_for_cuda_reg_config -from copy import deepcopy - -from . import defaults - -console = bittensor.__console__ - - -class RegisterCommand: - """ - Executes the ``register`` command to register a neuron on the Bittensor network by recycling some TAO (the network's native token). - - This command is used to add a new neuron to a specified subnet within the network, contributing to the decentralization and robustness of Bittensor. - - Usage: - Before registering, the command checks if the specified subnet exists and whether the user's balance is sufficient to cover the registration cost. - - The registration cost is determined by the current recycle amount for the specified subnet. If the balance is insufficient or the subnet does not exist, the command will exit with an appropriate error message. - - If the preconditions are met, and the user confirms the transaction (if ``no_prompt`` is not set), the command proceeds to register the neuron by recycling the required amount of TAO. - - The command structure includes: - - - Verification of subnet existence. - - Checking the user's balance against the current recycle amount for the subnet. - - User confirmation prompt for proceeding with registration. - - Execution of the registration process. - - Columns Displayed in the confirmation prompt: - - - Balance: The current balance of the user's wallet in TAO. - - Cost to Register: The required amount of TAO needed to register on the specified subnet. - - Example usage:: - - btcli subnets register --netuid 1 - - Note: - This command is critical for users who wish to contribute a new neuron to the network. It requires careful consideration of the subnet selection and an understanding of the registration costs. Users should ensure their wallet is sufficiently funded before attempting to register a neuron. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Register neuron by recycling some TAO.""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - RegisterCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Register neuron by recycling some TAO.""" - wallet = bittensor.wallet(config=cli.config) - - # Verify subnet exists - if not subtensor.subnet_exists(netuid=cli.config.netuid): - bittensor.__console__.print( - f"[red]Subnet {cli.config.netuid} does not exist[/red]" - ) - sys.exit(1) - - # Check current recycle amount - current_recycle = subtensor.recycle(netuid=cli.config.netuid) - balance = subtensor.get_balance(address=wallet.coldkeypub.ss58_address) - - # Check balance is sufficient - if balance < current_recycle: - bittensor.__console__.print( - f"[red]Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO[/red]" - ) - sys.exit(1) - - if not cli.config.no_prompt: - if ( - Confirm.ask( - f"Your balance is: [bold green]{balance}[/bold green]\nThe cost to register by recycle is [bold red]{current_recycle}[/bold red]\nDo you want to continue?", - default=False, - ) - == False - ): - sys.exit(1) - - subtensor.burned_register( - wallet=wallet, netuid=cli.config.netuid, prompt=not cli.config.no_prompt - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - register_parser = parser.add_parser( - "register", help="""Register a wallet to a network.""" - ) - register_parser.add_argument( - "--netuid", - type=int, - help="netuid for subnet to serve this neuron on", - default=argparse.SUPPRESS, - ) - - bittensor.wallet.add_args(register_parser) - bittensor.subtensor.add_args(register_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if ( - not config.is_set("subtensor.network") - and not config.is_set("subtensor.chain_endpoint") - and not config.no_prompt - ): - config.subtensor.network = Prompt.ask( - "Enter subtensor network", - choices=bittensor.__networks__, - default=defaults.subtensor.network, - ) - _, endpoint = bittensor.subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - config.subtensor.chain_endpoint = endpoint - - check_netuid_set( - config, subtensor=bittensor.subtensor(config=config, log_verbose=False) - ) - - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - -class PowRegisterCommand: - """ - Executes the ``pow_register`` command to register a neuron on the Bittensor network using Proof of Work (PoW). - - This method is an alternative registration process that leverages computational work for securing a neuron's place on the network. - - Usage: - The command starts by verifying the existence of the specified subnet. If the subnet does not exist, it terminates with an error message. - On successful verification, the PoW registration process is initiated, which requires solving computational puzzles. - - Optional arguments: - - ``--netuid`` (int): The netuid for the subnet on which to serve the neuron. Mandatory for specifying the target subnet. - - ``--pow_register.num_processes`` (int): The number of processors to use for PoW registration. Defaults to the system's default setting. - - ``--pow_register.update_interval`` (int): The number of nonces to process before checking for the next block during registration. Affects the frequency of update checks. - - ``--pow_register.no_output_in_place`` (bool): When set, disables the output of registration statistics in place. Useful for cleaner logs. - - ``--pow_register.verbose`` (bool): Enables verbose output of registration statistics for detailed information. - - ``--pow_register.cuda.use_cuda`` (bool): Enables the use of CUDA for GPU-accelerated PoW calculations. Requires a CUDA-compatible GPU. - - ``--pow_register.cuda.no_cuda`` (bool): Disables the use of CUDA, defaulting to CPU-based calculations. - - ``--pow_register.cuda.dev_id`` (int): Specifies the CUDA device ID, useful for systems with multiple CUDA-compatible GPUs. - - ``--pow_register.cuda.tpb`` (int): Sets the number of Threads Per Block for CUDA operations, affecting the GPU calculation dynamics. - - The command also supports additional wallet and subtensor arguments, enabling further customization of the registration process. - - Example usage:: - - btcli pow_register --netuid 1 --pow_register.num_processes 4 --cuda.use_cuda - - Note: - This command is suited for users with adequate computational resources to participate in PoW registration. It requires a sound understanding - of the network's operations and PoW mechanics. Users should ensure their systems meet the necessary hardware and software requirements, - particularly when opting for CUDA-based GPU acceleration. - - This command may be disabled according on the subnet owner's directive. For example, on netuid 1 this is permanently disabled. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Register neuron.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - PowRegisterCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Register neuron.""" - wallet = bittensor.wallet(config=cli.config) - - # Verify subnet exists - if not subtensor.subnet_exists(netuid=cli.config.netuid): - bittensor.__console__.print( - f"[red]Subnet {cli.config.netuid} does not exist[/red]" - ) - sys.exit(1) - - registered = subtensor.register( - wallet=wallet, - netuid=cli.config.netuid, - prompt=not cli.config.no_prompt, - tpb=cli.config.pow_register.cuda.get("tpb", None), - update_interval=cli.config.pow_register.get("update_interval", None), - num_processes=cli.config.pow_register.get("num_processes", None), - cuda=cli.config.pow_register.cuda.get( - "use_cuda", defaults.pow_register.cuda.use_cuda - ), - dev_id=cli.config.pow_register.cuda.get("dev_id", None), - output_in_place=cli.config.pow_register.get( - "output_in_place", defaults.pow_register.output_in_place - ), - log_verbose=cli.config.pow_register.get( - "verbose", defaults.pow_register.verbose - ), - ) - if not registered: - sys.exit(1) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - register_parser = parser.add_parser( - "pow_register", help="""Register a wallet to a network using PoW.""" - ) - register_parser.add_argument( - "--netuid", - type=int, - help="netuid for subnet to serve this neuron on", - default=argparse.SUPPRESS, - ) - register_parser.add_argument( - "--pow_register.num_processes", - "-n", - dest="pow_register.num_processes", - help="Number of processors to use for POW registration", - type=int, - default=defaults.pow_register.num_processes, - ) - register_parser.add_argument( - "--pow_register.update_interval", - "--pow_register.cuda.update_interval", - "--cuda.update_interval", - "-u", - help="The number of nonces to process before checking for next block during registration", - type=int, - default=defaults.pow_register.update_interval, - ) - register_parser.add_argument( - "--pow_register.no_output_in_place", - "--no_output_in_place", - dest="pow_register.output_in_place", - help="Whether to not ouput the registration statistics in-place. Set flag to disable output in-place.", - action="store_false", - required=False, - default=defaults.pow_register.output_in_place, - ) - register_parser.add_argument( - "--pow_register.verbose", - help="Whether to ouput the registration statistics verbosely.", - action="store_true", - required=False, - default=defaults.pow_register.verbose, - ) - - ## Registration args for CUDA registration. - register_parser.add_argument( - "--pow_register.cuda.use_cuda", - "--cuda", - "--cuda.use_cuda", - dest="pow_register.cuda.use_cuda", - default=defaults.pow_register.cuda.use_cuda, - help="""Set flag to use CUDA to register.""", - action="store_true", - required=False, - ) - register_parser.add_argument( - "--pow_register.cuda.no_cuda", - "--no_cuda", - "--cuda.no_cuda", - dest="pow_register.cuda.use_cuda", - default=not defaults.pow_register.cuda.use_cuda, - help="""Set flag to not use CUDA for registration""", - action="store_false", - required=False, - ) - - register_parser.add_argument( - "--pow_register.cuda.dev_id", - "--cuda.dev_id", - type=int, - nargs="+", - default=defaults.pow_register.cuda.dev_id, - help="""Set the CUDA device id(s). Goes by the order of speed. (i.e. 0 is the fastest).""", - required=False, - ) - register_parser.add_argument( - "--pow_register.cuda.tpb", - "--cuda.tpb", - type=int, - default=defaults.pow_register.cuda.tpb, - help="""Set the number of Threads Per Block for CUDA.""", - required=False, - ) - - bittensor.wallet.add_args(register_parser) - bittensor.subtensor.add_args(register_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if ( - not config.is_set("subtensor.network") - and not config.is_set("subtensor.chain_endpoint") - and not config.no_prompt - ): - config.subtensor.network = Prompt.ask( - "Enter subtensor network", - choices=bittensor.__networks__, - default=defaults.subtensor.network, - ) - _, endpoint = bittensor.subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - config.subtensor.chain_endpoint = endpoint - - check_netuid_set( - config, subtensor=bittensor.subtensor(config=config, log_verbose=False) - ) - - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - if not config.no_prompt: - check_for_cuda_reg_config(config) - - -class RunFaucetCommand: - """ - Executes the ``faucet`` command to obtain test TAO tokens by performing Proof of Work (PoW). - - IMPORTANT: - **THIS COMMAND IS CURRENTLY DISABLED.** - - This command is particularly useful for users who need test tokens for operations on the Bittensor testnet. - - Usage: - The command uses the PoW mechanism to validate the user's effort and rewards them with test TAO tokens. It is typically used in testnet environments where real value transactions are not necessary. - - Optional arguments: - - ``--faucet.num_processes`` (int): Specifies the number of processors to use for the PoW operation. A higher number of processors may increase the chances of successful computation. - - ``--faucet.update_interval`` (int): Sets the frequency of nonce processing before checking for the next block, which impacts the PoW operation's responsiveness. - - ``--faucet.no_output_in_place`` (bool): When set, it disables in-place output of registration statistics for cleaner log visibility. - - ``--faucet.verbose`` (bool): Enables verbose output for detailed statistical information during the PoW process. - - ``--faucet.cuda.use_cuda`` (bool): Activates the use of CUDA for GPU acceleration in the PoW process, suitable for CUDA-compatible GPUs. - - ``--faucet.cuda.no_cuda`` (bool): Disables the use of CUDA, opting for CPU-based calculations. - - ``--faucet.cuda.dev_id`` (int[]): Allows selection of specific CUDA device IDs for the operation, useful in multi-GPU setups. - - ``--faucet.cuda.tpb`` (int): Determines the number of Threads Per Block for CUDA operations, affecting GPU calculation efficiency. - - These options provide flexibility in configuring the PoW process according to the user's hardware capabilities and preferences. - - Example usage:: - - btcli wallet faucet --faucet.num_processes 4 --faucet.cuda.use_cuda - - Note: - This command is meant for use in testnet environments where users can experiment with the network without using real TAO tokens. - It's important for users to have the necessary hardware setup, especially when opting for CUDA-based GPU calculations. - - **THIS COMMAND IS CURRENTLY DISABLED.** - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Register neuron.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - RunFaucetCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Register neuron.""" - wallet = bittensor.wallet(config=cli.config) - success = subtensor.run_faucet( - wallet=wallet, - prompt=not cli.config.no_prompt, - tpb=cli.config.pow_register.cuda.get("tpb", None), - update_interval=cli.config.pow_register.get("update_interval", None), - num_processes=cli.config.pow_register.get("num_processes", None), - cuda=cli.config.pow_register.cuda.get( - "use_cuda", defaults.pow_register.cuda.use_cuda - ), - dev_id=cli.config.pow_register.cuda.get("dev_id", None), - output_in_place=cli.config.pow_register.get( - "output_in_place", defaults.pow_register.output_in_place - ), - log_verbose=cli.config.pow_register.get( - "verbose", defaults.pow_register.verbose - ), - ) - if not success: - bittensor.logging.error("Faucet run failed.") - sys.exit(1) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - run_faucet_parser = parser.add_parser( - "faucet", help="""Perform PoW to receieve test TAO in your wallet.""" - ) - run_faucet_parser.add_argument( - "--faucet.num_processes", - "-n", - dest="pow_register.num_processes", - help="Number of processors to use for POW registration", - type=int, - default=defaults.pow_register.num_processes, - ) - run_faucet_parser.add_argument( - "--faucet.update_interval", - "--faucet.cuda.update_interval", - "--cuda.update_interval", - "-u", - help="The number of nonces to process before checking for next block during registration", - type=int, - default=defaults.pow_register.update_interval, - ) - run_faucet_parser.add_argument( - "--faucet.no_output_in_place", - "--no_output_in_place", - dest="pow_register.output_in_place", - help="Whether to not ouput the registration statistics in-place. Set flag to disable output in-place.", - action="store_false", - required=False, - default=defaults.pow_register.output_in_place, - ) - run_faucet_parser.add_argument( - "--faucet.verbose", - help="Whether to ouput the registration statistics verbosely.", - action="store_true", - required=False, - default=defaults.pow_register.verbose, - ) - - ## Registration args for CUDA registration. - run_faucet_parser.add_argument( - "--faucet.cuda.use_cuda", - "--cuda", - "--cuda.use_cuda", - dest="pow_register.cuda.use_cuda", - default=defaults.pow_register.cuda.use_cuda, - help="""Set flag to use CUDA to pow_register.""", - action="store_true", - required=False, - ) - run_faucet_parser.add_argument( - "--faucet.cuda.no_cuda", - "--no_cuda", - "--cuda.no_cuda", - dest="pow_register.cuda.use_cuda", - default=not defaults.pow_register.cuda.use_cuda, - help="""Set flag to not use CUDA for registration""", - action="store_false", - required=False, - ) - run_faucet_parser.add_argument( - "--faucet.cuda.dev_id", - "--cuda.dev_id", - type=int, - nargs="+", - default=defaults.pow_register.cuda.dev_id, - help="""Set the CUDA device id(s). Goes by the order of speed. (i.e. 0 is the fastest).""", - required=False, - ) - run_faucet_parser.add_argument( - "--faucet.cuda.tpb", - "--cuda.tpb", - type=int, - default=defaults.pow_register.cuda.tpb, - help="""Set the number of Threads Per Block for CUDA.""", - required=False, - ) - bittensor.wallet.add_args(run_faucet_parser) - bittensor.subtensor.add_args(run_faucet_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.no_prompt: - check_for_cuda_reg_config(config) - - -class SwapHotkeyCommand: - @staticmethod - def run(cli: "bittensor.cli"): - """ - Executes the ``swap_hotkey`` command to swap the hotkeys for a neuron on the network. - - Usage: - The command is used to swap the hotkey of a wallet for another hotkey on that same wallet. - - Optional arguments: - - ``--wallet.name`` (str): Specifies the wallet for which the hotkey is to be swapped. - - ``--wallet.hotkey`` (str): The original hotkey name that is getting swapped out. - - ``--wallet.hotkey_b`` (str): The new hotkey name for which the old is getting swapped out for. - - Example usage:: - - btcli wallet swap_hotkey --wallet.name your_wallet_name --wallet.hotkey original_hotkey --wallet.hotkey_b new_hotkey - """ - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - SwapHotkeyCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Swap your hotkey for all registered axons on the network.""" - wallet = bittensor.wallet(config=cli.config) - - # This creates an unnecessary amount of extra data, but simplifies implementation. - new_config = deepcopy(cli.config) - new_config.wallet.hotkey = new_config.wallet.hotkey_b - new_wallet = bittensor.wallet(config=new_config) - - subtensor.swap_hotkey( - wallet=wallet, - new_wallet=new_wallet, - wait_for_finalization=False, - wait_for_inclusion=True, - prompt=False, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - swap_hotkey_parser = parser.add_parser( - "swap_hotkey", help="""Swap your associated hotkey.""" - ) - - swap_hotkey_parser.add_argument( - "--wallet.hotkey_b", - type=str, - default=defaults.wallet.hotkey, - help="""Name of the new hotkey""", - required=False, - ) - - bittensor.wallet.add_args(swap_hotkey_parser) - bittensor.subtensor.add_args(swap_hotkey_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if ( - not config.is_set("subtensor.network") - and not config.is_set("subtensor.chain_endpoint") - and not config.no_prompt - ): - config.subtensor.network = Prompt.ask( - "Enter subtensor network", - choices=bittensor.__networks__, - default=defaults.subtensor.network, - ) - _, endpoint = bittensor.subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - config.subtensor.chain_endpoint = endpoint - - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter old hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - if not config.is_set("wallet.hotkey_b") and not config.no_prompt: - hotkey = Prompt.ask("Enter new hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey_b = str(hotkey) diff --git a/bittensor/commands/root.py b/bittensor/commands/root.py deleted file mode 100644 index 35da7b1eb..000000000 --- a/bittensor/commands/root.py +++ /dev/null @@ -1,681 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 re -import typing -import argparse -import numpy as np -import bittensor -from typing import List, Optional, Dict -from rich.prompt import Prompt -from rich.table import Table -from .utils import get_delegates_details, DelegatesDetails - -from . import defaults - -console = bittensor.__console__ - - -class RootRegisterCommand: - """ - Executes the ``register`` command to register a wallet to the root network of the Bittensor network. - - This command is used to formally acknowledge a wallet's participation in the network's root layer. - - Usage: - The command registers the user's wallet with the root network, which is a crucial step for participating in network governance and other advanced functions. - - Optional arguments: - - None. The command primarily uses the wallet and subtensor configurations. - - Example usage:: - - btcli root register - - Note: - This command is important for users seeking to engage deeply with the Bittensor network, particularly in aspects related to network governance and decision-making. - - It is a straightforward process but requires the user to have an initialized and configured wallet. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Register to root network.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - RootRegisterCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Register to root network.""" - wallet = bittensor.wallet(config=cli.config) - - subtensor.root_register(wallet=wallet, prompt=not cli.config.no_prompt) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "register", help="""Register a wallet to the root network.""" - ) - - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - -class RootList: - """ - Executes the ``list`` command to display the members of the root network on the Bittensor network. - - This command provides an overview of the neurons that constitute the network's foundational layer. - - Usage: - Upon execution, the command fetches and lists the neurons in the root network, showing their unique identifiers (UIDs), names, addresses, stakes, and whether they are part of the senate (network governance body). - - Optional arguments: - - None. The command uses the subtensor configuration to retrieve data. - - Example usage:: - - $ btcli root list - - UID NAME ADDRESS STAKE(τ) SENATOR - 0 5CaCUPsSSdKWcMJbmdmJdnWVa15fJQuz5HsSGgVdZffpHAUa 27086.37070 Yes - 1 RaoK9 5GmaAk7frPXnAxjbQvXcoEzMGZfkrDee76eGmKoB3wxUburE 520.24199 No - 2 Openτensor Foundaτion 5F4tQyWrhfGVcNhoqeiNsR6KjD4wMZ2kfhLj4oHYuyHbZAc3 1275437.45895 Yes - 3 RoundTable21 5FFApaS75bv5pJHfAp2FVLBj9ZaXuFDjEypsaBNc1wCfe52v 84718.42095 Yes - 4 5HK5tp6t2S59DywmHRWPBVJeJ86T61KjurYqeooqj8sREpeN 168897.40859 Yes - 5 Rizzo 5CXRfP2ekFhe62r7q3vppRajJmGhTi7vwvb2yr79jveZ282w 53383.34400 No - 6 τaosτaτs and BitAPAI 5Hddm3iBFD2GLT5ik7LZnT3XJUnRnN8PoeCFgGQgawUVKNm8 646944.73569 Yes - ... - - Note: - This command is useful for users interested in understanding the composition and governance structure of the Bittensor network's root layer. It provides insights into which neurons hold significant influence and responsibility within the network. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""List the root network""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - RootList._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""List the root network""" - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ) - - senate_members = subtensor.get_senate_members() - root_neurons: typing.List[bittensor.NeuronInfoLite] = subtensor.neurons_lite( - netuid=0 - ) - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - subtensor=subtensor - ) - - table = Table(show_footer=False) - table.title = "[white]Root Network" - table.add_column( - "[overline white]UID", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - table.add_column( - "[overline white]NAME", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - table.add_column( - "[overline white]ADDRESS", - footer_style="overline white", - style="yellow", - no_wrap=True, - ) - table.add_column( - "[overline white]STAKE(\u03c4)", - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]SENATOR", - footer_style="overline white", - style="green", - no_wrap=True, - ) - table.show_footer = True - - for neuron_data in root_neurons: - table.add_row( - str(neuron_data.uid), - ( - delegate_info[neuron_data.hotkey].display - if neuron_data.hotkey in delegate_info - else "" - ), - neuron_data.hotkey, - "{:.5f}".format( - float(subtensor.get_total_stake_for_hotkey(neuron_data.hotkey)) - ), - "Yes" if neuron_data.hotkey in senate_members else "No", - ) - - table.box = None - table.pad_edge = False - table.width = None - bittensor.__console__.print(table) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser("list", help="""List the root network""") - bittensor.subtensor.add_args(parser) - - @staticmethod - def check_config(config: "bittensor.config"): - pass - - -class RootSetBoostCommand: - """ - Executes the ``boost`` command to boost the weights for a specific subnet within the root network on the Bittensor network. - - Usage: - The command allows boosting the weights for different subnets within the root network. - - Optional arguments: - - ``--netuid`` (int): A single netuid for which weights are to be boosted. - - ``--increase`` (float): The cooresponding increase in the weight for this subnet. - - Example usage:: - - $ btcli root boost --netuid 1 --increase 0.01 - - Enter netuid (e.g. 1): 1 - Enter amount (e.g. 0.01): 0.1 - Boosting weight for subnet: 1 by amount: 0.1 - - Normalized weights: - tensor([ - 0.0000, 0.5455, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.4545, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]) -> tensor([0.0000, 0.5455, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.4545, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000] - ) - - Do you want to set the following root weights?: - weights: tensor([ - 0.0000, 0.5455, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.4545, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]) - uids: tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, - 36, 37, 38, 39, 40])? [y/n]: y - True None - ✅ Finalized - ⠙ 📡 Setting root weights on test ...2023-11-28 22:09:14.001 | SUCCESS | Set weights Finalized: True - - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Set weights for root network.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - RootSetBoostCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Set weights for root network.""" - wallet = bittensor.wallet(config=cli.config) - - root = subtensor.metagraph(0, lite=False) - try: - my_uid = root.hotkeys.index(wallet.hotkey.ss58_address) - except ValueError: - bittensor.__console__.print( - "Wallet hotkey: {} not found in root metagraph".format(wallet.hotkey) - ) - exit() - my_weights = root.weights[my_uid] - prev_weight = my_weights[cli.config.netuid] - new_weight = prev_weight + cli.config.amount - - bittensor.__console__.print( - f"Boosting weight for netuid {cli.config.netuid} from {prev_weight} -> {new_weight}" - ) - my_weights[cli.config.netuid] = new_weight - all_netuids = np.arange(len(my_weights)) - - bittensor.__console__.print("Setting root weights...") - subtensor.root_set_weights( - wallet=wallet, - netuids=all_netuids, - weights=my_weights, - version_key=0, - prompt=not cli.config.no_prompt, - wait_for_finalization=True, - wait_for_inclusion=True, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "boost", help="""Boost weight for a specific subnet by increase amount.""" - ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) - parser.add_argument("--increase", dest="amount", type=float, required=False) - - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - if not config.is_set("netuid") and not config.no_prompt: - config.netuid = int(Prompt.ask(f"Enter netuid (e.g. 1)")) - if not config.is_set("amount") and not config.no_prompt: - config.amount = float(Prompt.ask(f"Enter amount (e.g. 0.01)")) - - -class RootSetSlashCommand: - """ - Executes the ``slash`` command to decrease the weights for a specific subnet within the root network on the Bittensor network. - - Usage: - The command allows slashing (decreasing) the weights for different subnets within the root network. - - Optional arguments: - - ``--netuid`` (int): A single netuid for which weights are to be slashed. - - ``--decrease`` (float): The corresponding decrease in the weight for this subnet. - - Example usage:: - - $ btcli root slash --netuid 1 --decrease 0.01 - - Enter netuid (e.g. 1): 1 - Enter decrease amount (e.g. 0.01): 0.2 - Slashing weight for subnet: 1 by amount: 0.2 - - Normalized weights: - tensor([ - 0.0000, 0.4318, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.5682, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]) -> tensor([ - 0.0000, 0.4318, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.5682, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000] - ) - - Do you want to set the following root weights?: - weights: tensor([ - 0.0000, 0.4318, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.5682, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, - 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]) - uids: tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, - 36, 37, 38, 39, 40])? [y/n]: y - ⠙ 📡 Setting root weights on test ...2023-11-28 22:09:14.001 | SUCCESS | Set weights Finalized: True - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Set weights for root network with decreased values.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - RootSetSlashCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - wallet = bittensor.wallet(config=cli.config) - - bittensor.__console__.print( - "Slashing weight for subnet: {} by amount: {}".format( - cli.config.netuid, cli.config.amount - ) - ) - root = subtensor.metagraph(0, lite=False) - try: - my_uid = root.hotkeys.index(wallet.hotkey.ss58_address) - except ValueError: - bittensor.__console__.print( - "Wallet hotkey: {} not found in root metagraph".format(wallet.hotkey) - ) - exit() - my_weights = root.weights[my_uid] - my_weights[cli.config.netuid] -= cli.config.amount - my_weights[my_weights < 0] = 0 # Ensure weights don't go negative - all_netuids = np.arange(len(my_weights)) - - subtensor.root_set_weights( - wallet=wallet, - netuids=all_netuids, - weights=my_weights, - version_key=0, - prompt=not cli.config.no_prompt, - wait_for_finalization=True, - wait_for_inclusion=True, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "slash", help="""Slash weight for a specific subnet by decrease amount.""" - ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) - parser.add_argument("--decrease", dest="amount", type=float, required=False) - - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - if not config.is_set("netuid") and not config.no_prompt: - config.netuid = int(Prompt.ask(f"Enter netuid (e.g. 1)")) - if not config.is_set("amount") and not config.no_prompt: - config.amount = float(Prompt.ask(f"Enter decrease amount (e.g. 0.01)")) - - -class RootSetWeightsCommand: - """ - Executes the ``weights`` command to set the weights for the root network on the Bittensor network. - - This command is used by network senators to influence the distribution of network rewards and responsibilities. - - Usage: - The command allows setting weights for different subnets within the root network. Users need to specify the netuids (network unique identifiers) and corresponding weights they wish to assign. - - Optional arguments: - - ``--netuids`` (str): A comma-separated list of netuids for which weights are to be set. - - ``--weights`` (str): Corresponding weights for the specified netuids, in comma-separated format. - - Example usage:: - - btcli root weights --netuids 1,2,3 --weights 0.3,0.3,0.4 - - Note: - This command is particularly important for network senators and requires a comprehensive understanding of the network's dynamics. - It is a powerful tool that directly impacts the network's operational mechanics and reward distribution. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Set weights for root network.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - RootSetWeightsCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Set weights for root network.""" - wallet = bittensor.wallet(config=cli.config) - subnets: List[bittensor.SubnetInfo] = subtensor.get_all_subnets_info() - - # Get values if not set. - if not cli.config.is_set("netuids"): - example = ( - ", ".join(map(str, [subnet.netuid for subnet in subnets][:3])) + " ..." - ) - cli.config.netuids = Prompt.ask(f"Enter netuids (e.g. {example})") - - if not cli.config.is_set("weights"): - example = ( - ", ".join( - map( - str, - [ - "{:.2f}".format(float(1 / len(subnets))) - for subnet in subnets - ][:3], - ) - ) - + " ..." - ) - cli.config.weights = Prompt.ask(f"Enter weights (e.g. {example})") - - # Parse from string - matched_netuids = list(map(int, re.split(r"[ ,]+", cli.config.netuids))) - netuids = np.array(matched_netuids, dtype=np.int64) - - matched_weights = [ - float(weight) for weight in re.split(r"[ ,]+", cli.config.weights) - ] - weights = np.array(matched_weights, dtype=np.float32) - - # Run the set weights operation. - subtensor.root_set_weights( - wallet=wallet, - netuids=netuids, - weights=weights, - version_key=0, - prompt=not cli.config.no_prompt, - wait_for_finalization=True, - wait_for_inclusion=True, - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser("weights", help="""Set weights for root network.""") - parser.add_argument("--netuids", dest="netuids", type=str, required=False) - parser.add_argument("--weights", dest="weights", type=str, required=False) - - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - -class RootGetWeightsCommand: - """ - Executes the ``get_weights`` command to retrieve the weights set for the root network on the Bittensor network. - - This command provides visibility into how network responsibilities and rewards are distributed among various subnets. - - Usage: - The command outputs a table listing the weights assigned to each subnet within the root network. This information is crucial for understanding the current influence and reward distribution among the subnets. - - Optional arguments: - - None. The command fetches weight information based on the subtensor configuration. - - Example usage:: - - $ btcli root get_weights - - Root Network Weights - UID 0 1 2 3 4 5 8 9 11 13 18 19 - 1 100.00% - - - - - - - - - - - - 2 - 40.00% 5.00% 10.00% 10.00% 10.00% 10.00% 5.00% - - 10.00% - - 3 - - 25.00% - 25.00% - 25.00% - - - 25.00% - - 4 - - 7.00% 7.00% 20.00% 20.00% 20.00% - 6.00% - 20.00% - - 5 - 20.00% - 10.00% 15.00% 15.00% 15.00% 5.00% - - 10.00% 10.00% - 6 - - - - 10.00% 10.00% 25.00% 25.00% - - 30.00% - - 7 - 60.00% - - 20.00% - - - 20.00% - - - - 8 - 49.35% - 7.18% 13.59% 21.14% 1.53% 0.12% 7.06% 0.03% - - - 9 100.00% - - - - - - - - - - - - ... - - Note: - This command is essential for users interested in the governance and operational dynamics of the Bittensor network. It offers transparency into how network rewards and responsibilities are allocated across different subnets. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Get weights for root network.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - RootGetWeightsCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Get weights for root network.""" - weights = subtensor.weights(0) - - table = Table(show_footer=False) - table.title = "[white]Root Network Weights" - table.add_column( - "[white]UID", - header_style="overline white", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - - uid_to_weights = {} - netuids = set() - for matrix in weights: - [uid, weights_data] = matrix - - if not len(weights_data): - uid_to_weights[uid] = {} - normalized_weights = [] - else: - normalized_weights = np.array(weights_data)[:, 1] / max( - np.sum(weights_data, axis=0)[1], 1 - ) - - for weight_data, normalized_weight in zip(weights_data, normalized_weights): - [netuid, _] = weight_data - netuids.add(netuid) - if uid not in uid_to_weights: - uid_to_weights[uid] = {} - - uid_to_weights[uid][netuid] = normalized_weight - - for netuid in netuids: - table.add_column( - f"[white]{netuid}", - header_style="overline white", - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - - for uid in uid_to_weights: - row = [str(uid)] - - uid_weights = uid_to_weights[uid] - for netuid in netuids: - if netuid in uid_weights: - normalized_weight = uid_weights[netuid] - row.append("{:0.2f}%".format(normalized_weight * 100)) - else: - row.append("-") - table.add_row(*row) - - table.show_footer = True - - table.box = None - table.pad_edge = False - table.width = None - bittensor.__console__.print(table) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "get_weights", help="""Get weights for root network.""" - ) - - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) - - @staticmethod - def check_config(config: "bittensor.config"): - pass diff --git a/bittensor/commands/senate.py b/bittensor/commands/senate.py deleted file mode 100644 index da24f3636..000000000 --- a/bittensor/commands/senate.py +++ /dev/null @@ -1,671 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 bittensor -from rich.prompt import Prompt, Confirm -from rich.table import Table -from typing import Optional, Dict -from .utils import get_delegates_details, DelegatesDetails -from . import defaults - -console = bittensor.__console__ - - -class SenateCommand: - """ - Executes the ``senate`` command to view the members of Bittensor's governance protocol, known as the Senate. - - This command lists the delegates involved in the decision-making process of the Bittensor network. - - Usage: - The command retrieves and displays a list of Senate members, showing their names and wallet addresses. - This information is crucial for understanding who holds governance roles within the network. - - Example usage:: - - btcli root senate - - Note: - This command is particularly useful for users interested in the governance structure and participants of the Bittensor network. It provides transparency into the network's decision-making body. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""View Bittensor's governance protocol proposals""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - SenateCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""View Bittensor's governance protocol proposals""" - console = bittensor.__console__ - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - cli.config.subtensor.network - ) - ) - - senate_members = subtensor.get_senate_members() - delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( - subtensor=subtensor - ) - - table = Table(show_footer=False) - table.title = "[white]Senate" - table.add_column( - "[overline white]NAME", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - table.add_column( - "[overline white]ADDRESS", - footer_style="overline white", - style="yellow", - no_wrap=True, - ) - table.show_footer = True - - for ss58_address in senate_members: - table.add_row( - ( - delegate_info[ss58_address].display - if ss58_address in delegate_info - else "" - ), - ss58_address, - ) - - table.box = None - table.pad_edge = False - table.width = None - console.print(table) - - @classmethod - def check_config(cls, config: "bittensor.config"): - None - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - senate_parser = parser.add_parser( - "senate", help="""View senate and it's members""" - ) - - bittensor.wallet.add_args(senate_parser) - bittensor.subtensor.add_args(senate_parser) - - -def format_call_data(call_data: "bittensor.ProposalCallData") -> str: - human_call_data = list() - - for arg in call_data["call_args"]: - arg_value = arg["value"] - - # If this argument is a nested call - func_args = ( - format_call_data( - { - "call_function": arg_value["call_function"], - "call_args": arg_value["call_args"], - } - ) - if isinstance(arg_value, dict) and "call_function" in arg_value - else str(arg_value) - ) - - human_call_data.append("{}: {}".format(arg["name"], func_args)) - - return "{}({})".format(call_data["call_function"], ", ".join(human_call_data)) - - -def display_votes( - vote_data: "bittensor.ProposalVoteData", - delegate_info: "Dict[str, DelegatesDetails]", -) -> str: - vote_list = list() - - for address in vote_data["ayes"]: - vote_list.append( - "{}: {}".format( - delegate_info[address].display if address in delegate_info else address, - "[bold green]Aye[/bold green]", - ) - ) - - for address in vote_data["nays"]: - vote_list.append( - "{}: {}".format( - delegate_info[address].display if address in delegate_info else address, - "[bold red]Nay[/bold red]", - ) - ) - - return "\n".join(vote_list) - - -class ProposalsCommand: - """ - Executes the ``proposals`` command to view active proposals within Bittensor's governance protocol. - - This command displays the details of ongoing proposals, including votes, thresholds, and proposal data. - - Usage: - The command lists all active proposals, showing their hash, voting threshold, number of ayes and nays, detailed votes by address, end block number, and call data associated with each proposal. - - Example usage:: - - btcli root proposals - - Note: - This command is essential for users who are actively participating in or monitoring the governance of the Bittensor network. - It provides a detailed view of the proposals being considered, along with the community's response to each. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""View Bittensor's governance protocol proposals""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - ProposalsCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""View Bittensor's governance protocol proposals""" - console = bittensor.__console__ - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ) - - senate_members = subtensor.get_senate_members() - proposals = subtensor.get_proposals() - - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(subtensor=subtensor) - ) - - table = Table(show_footer=False) - table.title = ( - "[white]Proposals\t\tActive Proposals: {}\t\tSenate Size: {}".format( - len(proposals), len(senate_members) - ) - ) - table.add_column( - "[overline white]HASH", - footer_style="overline white", - style="yellow", - no_wrap=True, - ) - table.add_column( - "[overline white]THRESHOLD", footer_style="overline white", style="white" - ) - table.add_column( - "[overline white]AYES", footer_style="overline white", style="green" - ) - table.add_column( - "[overline white]NAYS", footer_style="overline white", style="red" - ) - table.add_column( - "[overline white]VOTES", - footer_style="overline white", - style="rgb(50,163,219)", - ) - table.add_column( - "[overline white]END", footer_style="overline white", style="blue" - ) - table.add_column( - "[overline white]CALLDATA", footer_style="overline white", style="white" - ) - table.show_footer = True - - for hash in proposals: - call_data, vote_data = proposals[hash] - - table.add_row( - hash, - str(vote_data["threshold"]), - str(len(vote_data["ayes"])), - str(len(vote_data["nays"])), - display_votes(vote_data, registered_delegate_info), - str(vote_data["end"]), - format_call_data(call_data), - ) - - table.box = None - table.pad_edge = False - table.width = None - console.print(table) - - @classmethod - def check_config(cls, config: "bittensor.config"): - None - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - proposals_parser = parser.add_parser( - "proposals", help="""View active triumvirate proposals and their status""" - ) - - bittensor.wallet.add_args(proposals_parser) - bittensor.subtensor.add_args(proposals_parser) - - -class ShowVotesCommand: - """ - Executes the ``proposal_votes`` command to view the votes for a specific proposal in Bittensor's governance protocol. - - IMPORTANT - **THIS COMMAND IS DEPRECATED**. Use ``btcli root proposals`` to see vote status. - - This command provides a detailed breakdown of the votes cast by the senators for a particular proposal. - - Usage: - Users need to specify the hash of the proposal they are interested in. The command then displays the voting addresses and their respective votes (Aye or Nay) for the specified proposal. - - Optional arguments: - - ``--proposal`` (str): The hash of the proposal for which votes need to be displayed. - - Example usage:: - - btcli root proposal_votes --proposal - - Note: - This command is crucial for users seeking detailed insights into the voting behavior of the Senate on specific governance proposals. - It helps in understanding the level of consensus or disagreement within the Senate on key decisions. - - **THIS COMMAND IS DEPRECATED**. Use ``btcli root proposals`` to see vote status. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""View Bittensor's governance protocol proposals active votes""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - ShowVotesCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""View Bittensor's governance protocol proposals active votes""" - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - cli.config.subtensor.network - ) - ) - - proposal_hash = cli.config.proposal_hash - if len(proposal_hash) == 0: - console.print( - 'Aborting: Proposal hash not specified. View all proposals with the "proposals" command.' - ) - return - - proposal_vote_data = subtensor.get_vote_data(proposal_hash) - if proposal_vote_data is None: - console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") - return - - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(subtensor=subtensor) - ) - - table = Table(show_footer=False) - table.title = "[white]Votes for Proposal {}".format(proposal_hash) - table.add_column( - "[overline white]ADDRESS", - footer_style="overline white", - style="yellow", - no_wrap=True, - ) - table.add_column( - "[overline white]VOTE", footer_style="overline white", style="white" - ) - table.show_footer = True - - votes = display_votes(proposal_vote_data, registered_delegate_info).split("\n") - for vote in votes: - split_vote_data = vote.split(": ") # Nasty, but will work. - table.add_row(split_vote_data[0], split_vote_data[1]) - - table.box = None - table.pad_edge = False - table.min_width = 64 - console.print(table) - - @classmethod - def check_config(cls, config: "bittensor.config"): - if config.proposal_hash == "" and not config.no_prompt: - proposal_hash = Prompt.ask("Enter proposal hash") - config.proposal_hash = str(proposal_hash) - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - show_votes_parser = parser.add_parser( - "proposal_votes", help="""View an active proposal's votes by address.""" - ) - show_votes_parser.add_argument( - "--proposal", - dest="proposal_hash", - type=str, - nargs="?", - help="""Set the proposal to show votes for.""", - default="", - ) - bittensor.wallet.add_args(show_votes_parser) - bittensor.subtensor.add_args(show_votes_parser) - - -class SenateRegisterCommand: - """ - Executes the ``senate_register`` command to register as a member of the Senate in Bittensor's governance protocol. - - This command is used by delegates who wish to participate in the governance and decision-making process of the network. - - Usage: - The command checks if the user's hotkey is a delegate and not already a Senate member before registering them to the Senate. - Successful execution allows the user to participate in proposal voting and other governance activities. - - Example usage:: - - btcli root senate_register - - Note: - This command is intended for delegates who are interested in actively participating in the governance of the Bittensor network. - It is a significant step towards engaging in network decision-making processes. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Register to participate in Bittensor's governance protocol proposals""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - SenateRegisterCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Register to participate in Bittensor's governance protocol proposals""" - wallet = bittensor.wallet(config=cli.config) - - # Unlock the wallet. - wallet.hotkey - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return - - # Check if the hotkey is a delegate. - if not subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - console.print( - "Aborting: Hotkey {} isn't a delegate.".format( - wallet.hotkey.ss58_address - ) - ) - return - - if subtensor.is_senate_member(hotkey_ss58=wallet.hotkey.ss58_address): - console.print( - "Aborting: Hotkey {} is already a senate member.".format( - wallet.hotkey.ss58_address - ) - ) - return - - subtensor.register_senate(wallet=wallet, prompt=not cli.config.no_prompt) - - @classmethod - def check_config(cls, config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - senate_register_parser = parser.add_parser( - "senate_register", - help="""Register as a senate member to participate in proposals""", - ) - - bittensor.wallet.add_args(senate_register_parser) - bittensor.subtensor.add_args(senate_register_parser) - - -class SenateLeaveCommand: - """ - Executes the ``senate_leave`` command to discard membership in Bittensor's Senate. - - This command allows a Senate member to voluntarily leave the governance body. - - Usage: - The command checks if the user's hotkey is currently a Senate member before processing the request to leave the Senate. - It effectively removes the user from participating in future governance decisions. - - Example usage:: - - btcli root senate_leave - - Note: - This command is relevant for Senate members who wish to step down from their governance responsibilities within the Bittensor network. - It should be used when a member no longer desires to participate in the Senate activities. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Discard membership in Bittensor's governance protocol proposals""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - SenateLeaveCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.cli"): - r"""Discard membership in Bittensor's governance protocol proposals""" - wallet = bittensor.wallet(config=cli.config) - - # Unlock the wallet. - wallet.hotkey - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return - - if not subtensor.is_senate_member(hotkey_ss58=wallet.hotkey.ss58_address): - console.print( - "Aborting: Hotkey {} isn't a senate member.".format( - wallet.hotkey.ss58_address - ) - ) - return - - subtensor.leave_senate(wallet=wallet, prompt=not cli.config.no_prompt) - - @classmethod - def check_config(cls, config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - senate_leave_parser = parser.add_parser( - "senate_leave", - help="""Discard senate membership in the governance protocol""", - ) - - bittensor.wallet.add_args(senate_leave_parser) - bittensor.subtensor.add_args(senate_leave_parser) - - -class VoteCommand: - """ - Executes the ``senate_vote`` command to cast a vote on an active proposal in Bittensor's governance protocol. - - This command is used by Senate members to vote on various proposals that shape the network's future. - - Usage: - The user needs to specify the hash of the proposal they want to vote on. The command then allows the Senate member to cast an 'Aye' or 'Nay' vote, contributing to the decision-making process. - - Optional arguments: - - ``--proposal`` (str): The hash of the proposal to vote on. - - Example usage:: - - btcli root senate_vote --proposal - - Note: - This command is crucial for Senate members to exercise their voting rights on key proposals. It plays a vital role in the governance and evolution of the Bittensor network. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Vote in Bittensor's governance protocol proposals""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - VoteCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Vote in Bittensor's governance protocol proposals""" - wallet = bittensor.wallet(config=cli.config) - - proposal_hash = cli.config.proposal_hash - if len(proposal_hash) == 0: - console.print( - 'Aborting: Proposal hash not specified. View all proposals with the "proposals" command.' - ) - return - - if not subtensor.is_senate_member(hotkey_ss58=wallet.hotkey.ss58_address): - console.print( - "Aborting: Hotkey {} isn't a senate member.".format( - wallet.hotkey.ss58_address - ) - ) - return - - # Unlock the wallet. - wallet.hotkey - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return - - vote_data = subtensor.get_vote_data(proposal_hash) - - vote_data = subtensor.get_vote_data(proposal_hash) - if vote_data == None: - console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") - return - - vote = Confirm.ask("Desired vote for proposal") - subtensor.vote_senate( - wallet=wallet, - proposal_hash=proposal_hash, - proposal_idx=vote_data["index"], - vote=vote, - prompt=not cli.config.no_prompt, - ) - - @classmethod - def check_config(cls, config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - if config.proposal_hash == "" and not config.no_prompt: - proposal_hash = Prompt.ask("Enter proposal hash") - config.proposal_hash = str(proposal_hash) - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - vote_parser = parser.add_parser( - "senate_vote", help="""Vote on an active proposal by hash.""" - ) - vote_parser.add_argument( - "--proposal", - dest="proposal_hash", - type=str, - nargs="?", - help="""Set the proposal to show votes for.""", - default="", - ) - bittensor.wallet.add_args(vote_parser) - bittensor.subtensor.add_args(vote_parser) diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py deleted file mode 100644 index aa244af65..000000000 --- a/bittensor/commands/stake.py +++ /dev/null @@ -1,1401 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 os -import sys -import re -from typing import List, Union, Optional, Dict, Tuple - -from rich.prompt import Confirm, Prompt -from rich.table import Table -from rich.console import Console -from rich.text import Text -from tqdm import tqdm - -import bittensor -from bittensor.utils.balance import Balance -from .utils import ( - get_hotkey_wallets_for_wallet, - get_delegates_details, - DelegatesDetails, -) -from . import defaults # type: ignore -from ..utils import wallet_utils -from ..utils.formatting import u64_to_float, u16_to_float - -console = bittensor.__console__ - -MAX_CHILDREN = 5 - - -def get_netuid( - cli: "bittensor.cli", subtensor: "bittensor.subtensor", prompt: bool = True -) -> Tuple[bool, int]: - """Retrieve and validate the netuid from the user or configuration.""" - console = Console() - if not cli.config.is_set("netuid") and prompt: - cli.config.netuid = Prompt.ask("Enter netuid") - try: - cli.config.netuid = int(cli.config.netuid) - except ValueError: - console.print( - "[red]Invalid input. Please enter a valid integer for netuid.[/red]" - ) - return False, -1 - netuid = cli.config.netuid - if netuid < 0 or netuid > 65535: - console.print( - "[red]Invalid input. Please enter a valid integer for netuid in subnet range.[/red]" - ) - return False, -1 - if not subtensor.subnet_exists(netuid=netuid): - console.print( - "[red]Network with netuid {} does not exist. Please try again.[/red]".format( - netuid - ) - ) - return False, -1 - return True, netuid - - -def get_hotkey(wallet: "bittensor.wallet", config: "bittensor.config") -> str: - """Retrieve the hotkey from the wallet or config.""" - if wallet and wallet.hotkey: - return wallet.hotkey.ss58_address - elif config.is_set("hotkey"): - return config.hotkey - elif config.is_set("ss58"): - return config.ss58 - else: - return Prompt.ask("Enter hotkey (ss58)") - - -class StakeCommand: - """ - Executes the ``add`` command to stake tokens to one or more hotkeys from a user's coldkey on the Bittensor network. - - This command is used to allocate tokens to different hotkeys, securing their position and influence on the network. - - Usage: - Users can specify the amount to stake, the hotkeys to stake to (either by name or ``SS58`` address), and whether to stake to all hotkeys. The command checks for sufficient balance and hotkey registration - before proceeding with the staking process. - - Optional arguments: - - ``--all`` (bool): When set, stakes all available tokens from the coldkey. - - ``--uid`` (int): The unique identifier of the neuron to which the stake is to be added. - - ``--amount`` (float): The amount of TAO tokens to stake. - - ``--max_stake`` (float): Sets the maximum amount of TAO to have staked in each hotkey. - - ``--hotkeys`` (list): Specifies hotkeys by name or SS58 address to stake to. - - ``--all_hotkeys`` (bool): When set, stakes to all hotkeys associated with the wallet, excluding any specified in --hotkeys. - - The command prompts for confirmation before executing the staking operation. - - Example usage:: - - btcli stake add --amount 100 --wallet.name --wallet.hotkey - - Note: - This command is critical for users who wish to distribute their stakes among different neurons (hotkeys) on the network. - It allows for a strategic allocation of tokens to enhance network participation and influence. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Stake token of amount to hotkey(s).""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - StakeCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Stake token of amount to hotkey(s).""" - config = cli.config.copy() - wallet = bittensor.wallet(config=config) - - # Get the hotkey_names (if any) and the hotkey_ss58s. - hotkeys_to_stake_to: List[Tuple[Optional[str], str]] = [] - if config.get("all_hotkeys"): - # Stake to all hotkeys. - all_hotkeys: List[bittensor.wallet] = get_hotkey_wallets_for_wallet( - wallet=wallet - ) - # Get the hotkeys to exclude. (d)efault to no exclusions. - hotkeys_to_exclude: List[str] = cli.config.get("hotkeys", d=[]) - # Exclude hotkeys that are specified. - hotkeys_to_stake_to = [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys - if wallet.hotkey_str not in hotkeys_to_exclude - ] # definitely wallets - - elif config.get("hotkeys"): - # Stake to specific hotkeys. - for hotkey_ss58_or_hotkey_name in config.get("hotkeys"): - if bittensor.utils.is_valid_ss58_address(hotkey_ss58_or_hotkey_name): - # If the hotkey is a valid ss58 address, we add it to the list. - hotkeys_to_stake_to.append((None, hotkey_ss58_or_hotkey_name)) - else: - # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. - # We then get the hotkey from the wallet and add it to the list. - wallet_ = bittensor.wallet( - config=config, hotkey=hotkey_ss58_or_hotkey_name - ) - hotkeys_to_stake_to.append( - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ) - elif config.wallet.get("hotkey"): - # Only config.wallet.hotkey is specified. - # so we stake to that single hotkey. - hotkey_ss58_or_name = config.wallet.get("hotkey") - if bittensor.utils.is_valid_ss58_address(hotkey_ss58_or_name): - hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] - else: - # Hotkey is not a valid ss58 address, so we assume it is a hotkey name. - wallet_ = bittensor.wallet(config=config, hotkey=hotkey_ss58_or_name) - hotkeys_to_stake_to = [ - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ] - else: - # Only config.wallet.hotkey is specified. - # so we stake to that single hotkey. - assert config.wallet.hotkey is not None - hotkeys_to_stake_to = [ - (None, bittensor.wallet(config=config).hotkey.ss58_address) - ] - - # Get coldkey balance - wallet_balance: Balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - final_hotkeys: List[Tuple[str, str]] = [] - final_amounts: List[Union[float, Balance]] = [] - for hotkey in tqdm(hotkeys_to_stake_to): - hotkey: Tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) - if not subtensor.is_hotkey_registered_any(hotkey_ss58=hotkey[1]): - # Hotkey is not registered. - if len(hotkeys_to_stake_to) == 1: - # Only one hotkey, error - bittensor.__console__.print( - f"[red]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Aborting.[/red]" - ) - return None - else: - # Otherwise, print warning and skip - bittensor.__console__.print( - f"[yellow]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Skipping.[/yellow]" - ) - continue - - stake_amount_tao: float = config.get("amount") - if config.get("max_stake"): - # Get the current stake of the hotkey from this coldkey. - hotkey_stake: Balance = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey[1], coldkey_ss58=wallet.coldkeypub.ss58_address - ) - stake_amount_tao: float = config.get("max_stake") - hotkey_stake.tao - - # If the max_stake is greater than the current wallet balance, stake the entire balance. - stake_amount_tao: float = min(stake_amount_tao, wallet_balance.tao) - if ( - stake_amount_tao <= 0.00001 - ): # Threshold because of fees, might create a loop otherwise - # Skip hotkey if max_stake is less than current stake. - continue - wallet_balance = Balance.from_tao(wallet_balance.tao - stake_amount_tao) - - if wallet_balance.tao < 0: - # No more balance to stake. - break - - final_amounts.append(stake_amount_tao) - final_hotkeys.append(hotkey) # add both the name and the ss58 address. - - if len(final_hotkeys) == 0: - # No hotkeys to stake to. - bittensor.__console__.print( - "Not enough balance to stake to any hotkeys or max_stake is less than current stake." - ) - return None - - # Ask to stake - if not config.no_prompt: - if not Confirm.ask( - f"Do you want to stake to the following keys from {wallet.name}:\n" - + "".join( - [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {bittensor.__tao_symbol__}' if amount else 'All'}[/bold white]\n" - for hotkey, amount in zip(final_hotkeys, final_amounts) - ] - ) - ): - return None - - if len(final_hotkeys) == 1: - # do regular stake - return subtensor.add_stake( - wallet=wallet, - hotkey_ss58=final_hotkeys[0][1], - amount=None if config.get("stake_all") else final_amounts[0], - wait_for_inclusion=True, - prompt=not config.no_prompt, - ) - - subtensor.add_stake_multiple( - wallet=wallet, - hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], - amounts=None if config.get("stake_all") else final_amounts, - wait_for_inclusion=True, - prompt=False, - ) - - @classmethod - def check_config(cls, config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if ( - not config.is_set("wallet.hotkey") - and not config.no_prompt - and not config.wallet.get("all_hotkeys") - and not config.wallet.get("hotkeys") - ): - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - # Get amount. - if ( - not config.get("amount") - and not config.get("stake_all") - and not config.get("max_stake") - ): - if not Confirm.ask( - "Stake all Tao from account: [bold]'{}'[/bold]?".format( - config.wallet.get("name", defaults.wallet.name) - ) - ): - amount = Prompt.ask("Enter Tao amount to stake") - try: - config.amount = float(amount) - except ValueError: - console.print( - ":cross_mark:[red]Invalid Tao amount[/red] [bold white]{}[/bold white]".format( - amount - ) - ) - sys.exit() - else: - config.stake_all = True - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser): - stake_parser = parser.add_parser( - "add", help="""Add stake to your hotkey accounts from your coldkey.""" - ) - stake_parser.add_argument("--all", dest="stake_all", action="store_true") - stake_parser.add_argument("--uid", dest="uid", type=int, required=False) - stake_parser.add_argument("--amount", dest="amount", type=float, required=False) - stake_parser.add_argument( - "--max_stake", - dest="max_stake", - type=float, - required=False, - action="store", - default=None, - help="""Specify the maximum amount of Tao to have staked in each hotkey.""", - ) - stake_parser.add_argument( - "--hotkeys", - "--exclude_hotkeys", - "--wallet.hotkeys", - "--wallet.exclude_hotkeys", - required=False, - action="store", - default=[], - type=str, - nargs="*", - help="""Specify the hotkeys by name or ss58 address. (e.g. hk1 hk2 hk3)""", - ) - stake_parser.add_argument( - "--all_hotkeys", - "--wallet.all_hotkeys", - required=False, - action="store_true", - default=False, - help="""To specify all hotkeys. Specifying hotkeys will exclude them from this all.""", - ) - bittensor.wallet.add_args(stake_parser) - bittensor.subtensor.add_args(stake_parser) - - -def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: - try: - wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [bittensor.wallet(path=path, name=name) for name in wallet_names] - except StopIteration: - # No wallet files found. - wallets = [] - return wallets - - -def _get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: - hotkey_wallets = [] - hotkeys_path = wallet.path + "/" + wallet.name + "/hotkeys" - try: - hotkey_files = next(os.walk(os.path.expanduser(hotkeys_path)))[2] - except StopIteration: - hotkey_files = [] - for hotkey_file_name in hotkey_files: - try: - hotkey_for_name = bittensor.wallet( - path=wallet.path, name=wallet.name, hotkey=hotkey_file_name - ) - if ( - hotkey_for_name.hotkey_file.exists_on_device() - and not hotkey_for_name.hotkey_file.is_encrypted() - ): - hotkey_wallets.append(hotkey_for_name) - except Exception: - pass - return hotkey_wallets - - -class StakeShow: - """ - Executes the ``show`` command to list all stake accounts associated with a user's wallet on the Bittensor network. - - This command provides a comprehensive view of the stakes associated with both hotkeys and delegates linked to the user's coldkey. - - Usage: - The command lists all stake accounts for a specified wallet or all wallets in the user's configuration directory. - It displays the coldkey, balance, account details (hotkey/delegate name), stake amount, and the rate of return. - - Optional arguments: - - ``--all`` (bool): When set, the command checks all coldkey wallets instead of just the specified wallet. - - The command compiles a table showing: - - - Coldkey: The coldkey associated with the wallet. - - Balance: The balance of the coldkey. - - Account: The name of the hotkey or delegate. - - Stake: The amount of TAO staked to the hotkey or delegate. - - Rate: The rate of return on the stake, typically shown in TAO per day. - - Example usage:: - - btcli stake show --all - - Note: - This command is essential for users who wish to monitor their stake distribution and returns across various accounts on the Bittensor network. - It provides a clear and detailed overview of the user's staking activities. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Show all stake accounts.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - StakeShow._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - """Show all stake accounts.""" - if cli.config.get("all", d=False) is True: - wallets = _get_coldkey_wallets_for_path(cli.config.wallet.path) - else: - wallets = [bittensor.wallet(config=cli.config)] - registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( - get_delegates_details(subtensor=subtensor) - ) - - def get_stake_accounts( - wallet, subtensor - ) -> Dict[str, Dict[str, Union[str, Balance]]]: - """Get stake account details for the given wallet. - - Args: - wallet: The wallet object to fetch the stake account details for. - - Returns: - A dictionary mapping SS58 addresses to their respective stake account details. - """ - - wallet_stake_accounts = {} - - # Get this wallet's coldkey balance. - cold_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - # Populate the stake accounts with local hotkeys data. - wallet_stake_accounts.update(get_stakes_from_hotkeys(subtensor, wallet)) - - # Populate the stake accounts with delegations data. - wallet_stake_accounts.update(get_stakes_from_delegates(subtensor, wallet)) - - return { - "name": wallet.name, - "balance": cold_balance, - "accounts": wallet_stake_accounts, - } - - def get_stakes_from_hotkeys( - subtensor, wallet - ) -> Dict[str, Dict[str, Union[str, Balance]]]: - """Fetch stakes from hotkeys for the provided wallet. - - Args: - wallet: The wallet object to fetch the stakes for. - - Returns: - A dictionary of stakes related to hotkeys. - """ - hotkeys = get_hotkey_wallets_for_wallet(wallet) - stakes = {} - for hot in hotkeys: - emission = sum( - [ - n.emission - for n in subtensor.get_all_neurons_for_pubkey( - hot.hotkey.ss58_address - ) - ] - ) - hotkey_stake = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hot.hotkey.ss58_address, - coldkey_ss58=wallet.coldkeypub.ss58_address, - ) - stakes[hot.hotkey.ss58_address] = { - "name": hot.hotkey_str, - "stake": hotkey_stake, - "rate": emission, - } - return stakes - - def get_stakes_from_delegates( - subtensor, wallet - ) -> Dict[str, Dict[str, Union[str, Balance]]]: - """Fetch stakes from delegates for the provided wallet. - - Args: - wallet: The wallet object to fetch the stakes for. - - Returns: - A dictionary of stakes related to delegates. - """ - delegates = subtensor.get_delegated( - coldkey_ss58=wallet.coldkeypub.ss58_address - ) - stakes = {} - for dele, staked in delegates: - for nom in dele.nominators: - if nom[0] == wallet.coldkeypub.ss58_address: - delegate_name = ( - registered_delegate_info[dele.hotkey_ss58].display - if dele.hotkey_ss58 in registered_delegate_info - else dele.hotkey_ss58 - ) - stakes[dele.hotkey_ss58] = { - "name": delegate_name, - "stake": nom[1], - "rate": dele.total_daily_return.tao - * (nom[1] / dele.total_stake.tao), - } - return stakes - - def get_all_wallet_accounts( - wallets, - subtensor, - ) -> List[Dict[str, Dict[str, Union[str, Balance]]]]: - """Fetch stake accounts for all provided wallets using a ThreadPool. - - Args: - wallets: List of wallets to fetch the stake accounts for. - - Returns: - A list of dictionaries, each dictionary containing stake account details for each wallet. - """ - - accounts = [] - # Create a progress bar using tqdm - with tqdm(total=len(wallets), desc="Fetching accounts", ncols=100) as pbar: - for wallet in wallets: - accounts.append(get_stake_accounts(wallet, subtensor)) - pbar.update() - return accounts - - accounts = get_all_wallet_accounts(wallets, subtensor) - - total_stake = 0 - total_balance = 0 - total_rate = 0 - for acc in accounts: - total_balance += acc["balance"].tao - for key, value in acc["accounts"].items(): - total_stake += value["stake"].tao - total_rate += float(value["rate"]) - table = Table(show_footer=True, pad_edge=False, box=None, expand=False) - table.add_column( - "[overline white]Coldkey", footer_style="overline white", style="bold white" - ) - table.add_column( - "[overline white]Balance", - "\u03c4{:.5f}".format(total_balance), - footer_style="overline white", - style="green", - ) - table.add_column( - "[overline white]Account", footer_style="overline white", style="blue" - ) - table.add_column( - "[overline white]Stake", - "\u03c4{:.5f}".format(total_stake), - footer_style="overline white", - style="green", - ) - table.add_column( - "[overline white]Rate", - "\u03c4{:.5f}/d".format(total_rate), - footer_style="overline white", - style="green", - ) - for acc in accounts: - table.add_row(acc["name"], acc["balance"], "", "") - for key, value in acc["accounts"].items(): - table.add_row( - "", "", value["name"], value["stake"], str(value["rate"]) + "/d" - ) - bittensor.__console__.print(table) - - @staticmethod - def check_config(config: "bittensor.config"): - if ( - not config.get("all", d=None) - and not config.is_set("wallet.name") - and not config.no_prompt - ): - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - list_parser = parser.add_parser( - "show", help="""List all stake accounts for wallet.""" - ) - list_parser.add_argument( - "--all", - action="store_true", - help="""Check all coldkey wallets.""", - default=False, - ) - - bittensor.wallet.add_args(list_parser) - bittensor.subtensor.add_args(list_parser) - - -class SetChildKeyTakeCommand: - """ - Executes the ``set_childkey_take`` command to modify your childkey take on a specified subnet on the Bittensor network to the caller. - - This command is used to modify your childkey take on a specified subnet on the Bittensor network. - - Usage: - Users can specify the amount or 'take' for their child hotkeys (``SS58`` address), - the user needs to have access to the ss58 hotkey this call, and the take must be between 0 and 18%. - - The command prompts for confirmation before executing the set_childkey_take operation. - - Example usage:: - - btcli stake set_childkey_take --hotkey --netuid 1 --take 0.18 - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Set childkey take.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - SetChildKeyTakeCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - console = Console() - wallet = bittensor.wallet(config=cli.config) - - # Get values if not set. - exists, netuid = get_netuid(cli, subtensor) - if not exists: - return - - # get parent hotkey - hotkey = get_hotkey(wallet, cli.config) - if not wallet_utils.is_valid_ss58_address(hotkey): - console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") - return - - if not cli.config.is_set("take"): - cli.config.take = Prompt.ask( - "Enter the percentage of take for your child hotkey (between 0 and 0.18 representing 0-18%)" - ) - - # extract take from cli input - try: - take = float(cli.config.take) - except ValueError: - print( - ":cross_mark:[red]Take must be a float value using characters between 0 and 9.[/red]" - ) - return - - if take < 0 or take > 0.18: - console.print( - f":cross_mark:[red]Invalid take: Childkey Take must be between 0 and 0.18 (representing 0% to 18%). Proposed take is {take}.[/red]" - ) - return - - success, message = subtensor.set_childkey_take( - wallet=wallet, - netuid=netuid, - hotkey=hotkey, - take=take, - wait_for_inclusion=cli.config.wait_for_inclusion, - wait_for_finalization=cli.config.wait_for_finalization, - prompt=cli.config.prompt, - ) - - # Result - if success: - console.print(":white_heavy_check_mark: [green]Set childkey take.[/green]") - console.print( - f"The childkey take for {hotkey} is now set to {take * 100:.3f}%." - ) - else: - console.print( - f":cross_mark:[red] Unable to set childkey take.[/red] {message}" - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey_or_ss58 = Prompt.ask( - "Enter hotkey name or ss58", default=defaults.wallet.hotkey - ) - if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): - config.ss58 = str(hotkey_or_ss58) - else: - config.wallet.hotkey = str(hotkey_or_ss58) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - set_childkey_take_parser = parser.add_parser( - "set_childkey_take", help="""Set childkey take.""" - ) - set_childkey_take_parser.add_argument( - "--netuid", dest="netuid", type=int, required=False - ) - set_childkey_take_parser.add_argument( - "--hotkey", dest="hotkey", type=str, required=False - ) - set_childkey_take_parser.add_argument( - "--take", dest="take", type=float, required=False - ) - set_childkey_take_parser.add_argument( - "--wait_for_inclusion", - dest="wait_for_inclusion", - action="store_true", - default=True, - help="""Wait for the transaction to be included in a block.""", - ) - set_childkey_take_parser.add_argument( - "--wait_for_finalization", - dest="wait_for_finalization", - action="store_true", - default=True, - help="""Wait for the transaction to be finalized.""", - ) - set_childkey_take_parser.add_argument( - "--prompt", - dest="prompt", - action="store_true", - default=True, - help="""Prompt for confirmation before proceeding.""", - ) - set_childkey_take_parser.add_argument( - "--y", - "--yes", - "--no_prompt", - dest="prompt", - action="store_false", - help="""Disable prompt for confirmation before proceeding. Defaults to Yes for all prompts.""", - ) - bittensor.wallet.add_args(set_childkey_take_parser) - bittensor.subtensor.add_args(set_childkey_take_parser) - - -class GetChildKeyTakeCommand: - """ - Executes the ``get_childkey_take`` command to get your childkey take on a specified subnet on the Bittensor network to the caller. - - This command is used to get your childkey take on a specified subnet on the Bittensor network. - - Usage: - Users can get the amount or 'take' for their child hotkeys (``SS58`` address) - - Example usage:: - - btcli stake get_childkey_take --hotkey --netuid 1 - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Get childkey take.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - GetChildKeyTakeCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - console = Console() - wallet = bittensor.wallet(config=cli.config) - - # Get values if not set. - exists, netuid = get_netuid(cli, subtensor) - if not exists: - return - - # get parent hotkey - hotkey = get_hotkey(wallet, cli.config) - if not wallet_utils.is_valid_ss58_address(hotkey): - console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") - return - - take_u16 = subtensor.get_childkey_take( - netuid=netuid, - hotkey=hotkey, - ) - - # Result - if take_u16: - take = u16_to_float(take_u16) - console.print(f"The childkey take for {hotkey} is {take * 100:.3f}%.") - else: - console.print(":cross_mark:[red] Unable to get childkey take.[/red]") - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey_or_ss58 = Prompt.ask( - "Enter hotkey name or ss58", default=defaults.wallet.hotkey - ) - if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): - config.ss58 = str(hotkey_or_ss58) - else: - config.wallet.hotkey = str(hotkey_or_ss58) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - get_childkey_take_parser = parser.add_parser( - "get_childkey_take", help="""Get childkey take.""" - ) - get_childkey_take_parser.add_argument( - "--netuid", dest="netuid", type=int, required=False - ) - get_childkey_take_parser.add_argument( - "--hotkey", dest="hotkey", type=str, required=False - ) - bittensor.wallet.add_args(get_childkey_take_parser) - bittensor.subtensor.add_args(get_childkey_take_parser) - - @staticmethod - def get_take(subtensor, hotkey, netuid) -> float: - """ - Get the take value for a given subtensor, hotkey, and netuid. - - @param subtensor: The subtensor object. - @param hotkey: The hotkey to retrieve the take value for. - @param netuid: The netuid to retrieve the take value for. - - @return: The take value as a float. If the take value is not available, it returns 0. - - """ - take_u16 = subtensor.get_childkey_take( - netuid=netuid, - hotkey=hotkey, - ) - if take_u16: - return u16_to_float(take_u16) - else: - return 0 - - -class SetChildrenCommand: - """ - Executes the ``set_children`` command to add children hotkeys on a specified subnet on the Bittensor network to the caller. - - This command is used to delegate authority to different hotkeys, securing their position and influence on the subnet. - - Usage: - Users can specify the amount or 'proportion' to delegate to child hotkeys (``SS58`` address), - the user needs to have sufficient authority to make this call, and the sum of proportions must equal 1, - representing 100% of the proportion allocation. - - The command prompts for confirmation before executing the set_children operation. - - Example usage:: - - btcli stake set_children --children , --hotkey --netuid 1 --proportions 0.4,0.6 - - Note: - This command is critical for users who wish to delegate children hotkeys among different neurons (hotkeys) on the network. - It allows for a strategic allocation of authority to enhance network participation and influence. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Set children hotkeys.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - SetChildrenCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - console = Console() - wallet = bittensor.wallet(config=cli.config) - - # Get values if not set. - exists, netuid = get_netuid(cli, subtensor) - if not exists: - return - - # get parent hotkey - hotkey = get_hotkey(wallet, cli.config) - if not wallet_utils.is_valid_ss58_address(hotkey): - console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") - return - - # get current children - curr_children = GetChildrenCommand.retrieve_children( - subtensor=subtensor, - hotkey=hotkey, - netuid=netuid, - render_table=False, - ) - - if curr_children: - # print the table of current children - hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) - GetChildrenCommand.render_table( - subtensor=subtensor, - hotkey=hotkey, - hotkey_stake=hotkey_stake, - children=curr_children, - netuid=netuid, - prompt=False, - ) - - # get new children - if not cli.config.is_set("children"): - cli.config.children = Prompt.ask( - "Enter child hotkeys (ss58) as comma-separated values" - ) - proposed_children = [str(x) for x in re.split(r"[ ,]+", cli.config.children)] - - # Set max 5 children - if len(proposed_children) > MAX_CHILDREN: - console.print( - ":cross_mark:[red] Too many children. Maximum 5 children per hotkey[/red]" - ) - return - - # Validate children SS58 addresses - for child in proposed_children: - if not wallet_utils.is_valid_ss58_address(child): - console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") - return - - # get proportions for new children - if not cli.config.is_set("proportions"): - cli.config.proportions = Prompt.ask( - "Enter the percentage of proportion for each child as comma-separated values (total from all children must be less than or equal to 1)" - ) - - # extract proportions and child addresses from cli input - proportions = [ - float(x) for x in re.split(r"[ ,]+", str(cli.config.proportions)) - ] - total_proposed = sum(proportions) - if total_proposed > 1: - console.print( - f":cross_mark:[red]Invalid proportion: The sum of all proportions must be less or equal to than 1 (representing 100% of the allocation). Proposed sum addition is proportions is {total_proposed}.[/red]" - ) - return - - if len(proportions) != len(proposed_children): - console.print( - ":cross_mark:[red]Invalid proportion and children length: The count of children and number of proportion values entered do not match.[/red]" - ) - return - - # combine proposed and current children - children_with_proportions = list(zip(proportions, proposed_children)) - - SetChildrenCommand.print_current_stake( - subtensor=subtensor, children=proposed_children, hotkey=hotkey - ) - - success, message = subtensor.set_children( - wallet=wallet, - netuid=netuid, - hotkey=hotkey, - children_with_proportions=children_with_proportions, - wait_for_inclusion=cli.config.wait_for_inclusion, - wait_for_finalization=cli.config.wait_for_finalization, - prompt=cli.config.prompt, - ) - - # Result - if success: - if cli.config.wait_for_finalization and cli.config.wait_for_inclusion: - console.print("New Status:") - GetChildrenCommand.retrieve_children( - subtensor=subtensor, - hotkey=hotkey, - netuid=netuid, - render_table=True, - ) - console.print( - ":white_heavy_check_mark: [green]Set children hotkeys.[/green]" - ) - else: - console.print( - f":cross_mark:[red] Unable to set children hotkeys.[/red] {message}" - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey_or_ss58 = Prompt.ask( - "Enter hotkey name or ss58", default=defaults.wallet.hotkey - ) - if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): - config.ss58 = str(hotkey_or_ss58) - else: - config.wallet.hotkey = str(hotkey_or_ss58) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - set_children_parser = parser.add_parser( - "set_children", help="""Set multiple children hotkeys.""" - ) - set_children_parser.add_argument( - "--netuid", dest="netuid", type=int, required=False - ) - set_children_parser.add_argument( - "--children", dest="children", type=str, required=False - ) - set_children_parser.add_argument( - "--hotkey", dest="hotkey", type=str, required=False - ) - set_children_parser.add_argument( - "--proportions", dest="proportions", type=str, required=False - ) - set_children_parser.add_argument( - "--wait_for_inclusion", - dest="wait_for_inclusion", - action="store_true", - default=True, - help="""Wait for the transaction to be included in a block.""", - ) - set_children_parser.add_argument( - "--wait_for_finalization", - dest="wait_for_finalization", - action="store_true", - default=True, - help="""Wait for the transaction to be finalized.""", - ) - set_children_parser.add_argument( - "--prompt", - dest="prompt", - action="store_true", - default=True, - help="""Prompt for confirmation before proceeding.""", - ) - set_children_parser.add_argument( - "--y", - "--yes", - "--no_prompt", - dest="prompt", - action="store_false", - help="""Disable prompt for confirmation before proceeding. Defaults to Yes for all prompts.""", - ) - bittensor.wallet.add_args(set_children_parser) - bittensor.subtensor.add_args(set_children_parser) - - @staticmethod - def print_current_stake(subtensor, children, hotkey): - console = Console() - parent_stake = subtensor.get_total_stake_for_hotkey(ss58_address=hotkey) - console.print("Current Status:") - console.print(f"My Hotkey: {hotkey} | ", style="cyan", end="", no_wrap=True) - console.print(f"Total Stake: {parent_stake}τ") - for child in children: - child_stake = subtensor.get_total_stake_for_hotkey(child) - console.print( - f"Child Hotkey: {child} | Current Child Stake: {child_stake}τ" - ) - - -class GetChildrenCommand: - """ - Executes the ``get_children_info`` command to get all child hotkeys on a specified subnet on the Bittensor network. - - This command is used to view delegated authority to different hotkeys on the subnet. - - Usage: - Users can specify the subnet and see the children and the proportion that is given to them. - - The command compiles a table showing: - - - ChildHotkey: The hotkey associated with the child. - - ParentHotKey: The hotkey associated with the parent. - - Proportion: The proportion that is assigned to them. - - Expiration: The expiration of the hotkey. - - Example usage:: - - btcli stake get_children --netuid 1 - - Note: - This command is for users who wish to see child hotkeys among different neurons (hotkeys) on the network. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Get children hotkeys.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - return GetChildrenCommand._run(cli, subtensor) - except Exception as e: - console = Console() - console.print(f":cross_mark:[red] An error occurred: {str(e)}[/red]") - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - console = Console() - wallet = bittensor.wallet(config=cli.config) - - # check all - if cli.config.is_set("all"): - cli.config.netuid = None - cli.config.all = True - elif cli.config.is_set("netuid"): - if cli.config.netuid == "all": - cli.config.all = True - else: - cli.config.netuid = int(cli.config.netuid) - exists, netuid = get_netuid(cli, subtensor) - if not exists: - return - else: - netuid_input = Prompt.ask("Enter netuid or 'all'", default="all") - if netuid_input == "all": - cli.config.netuid = None - cli.config.all = True - else: - cli.config.netuid = int(netuid_input) - exists, netuid = get_netuid(cli, subtensor, False) - if not exists: - return - - # get parent hotkey - hotkey = get_hotkey(wallet, cli.config) - if not wallet_utils.is_valid_ss58_address(hotkey): - console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") - return - - try: - netuids = subtensor.get_all_subnet_netuids() if cli.config.all else [netuid] - hotkey_stake = GetChildrenCommand.get_parent_stake_info( - console, subtensor, hotkey - ) - for netuid in netuids: - children = subtensor.get_children(hotkey, netuid) - if children: - GetChildrenCommand.render_table( - subtensor, - hotkey, - hotkey_stake, - children, - netuid, - not cli.config.is_set("all"), - ) - except Exception as e: - console.print( - f":cross_mark:[red] An error occurred while retrieving children: {str(e)}[/red]" - ) - return - - return children - - @staticmethod - def get_parent_stake_info(console, subtensor, hotkey): - hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) - console.print( - f"\nYour Hotkey: {hotkey} | ", style="cyan", end="", no_wrap=True - ) - console.print(f"Total Stake: {hotkey_stake}τ") - return hotkey_stake - - @staticmethod - def retrieve_children( - subtensor: "bittensor.subtensor", hotkey: str, netuid: int, render_table: bool - ) -> list[tuple[int, str]]: - """ - - Static method to retrieve children for a given subtensor. - - Args: - subtensor (bittensor.subtensor): The subtensor object used to interact with the Bittensor network. - hotkey (str): The hotkey of the parent. - netuid (int): The network unique identifier of the subtensor. - render_table (bool): Flag indicating whether to render the retrieved children in a table. - - Returns: - List[str]: A list of children hotkeys. - - """ - try: - children = subtensor.get_children(hotkey, netuid) - if render_table: - hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) - GetChildrenCommand.render_table( - subtensor, hotkey, hotkey_stake, children, netuid, False - ) - return children - except Exception as e: - console = Console() - console.print( - f":cross_mark:[red] An error occurred while retrieving children: {str(e)}[/red]" - ) - return [] - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey_or_ss58 = Prompt.ask( - "Enter hotkey name or ss58", default=defaults.wallet.hotkey - ) - if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): - config.ss58 = str(hotkey_or_ss58) - else: - config.wallet.hotkey = str(hotkey_or_ss58) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "get_children", help="""Get child hotkeys on subnet.""" - ) - parser.add_argument("--netuid", dest="netuid", type=str, required=False) - parser.add_argument("--hotkey", dest="hotkey", type=str, required=False) - parser.add_argument( - "--all", - dest="all", - action="store_true", - help="Retrieve children from all subnets.", - ) - - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) - - @staticmethod - def render_table( - subtensor: "bittensor.subtensor", - hotkey: str, - hotkey_stake: "Balance", - children: list[Tuple[int, str]], - netuid: int, - prompt: bool, - ): - """ - - Render a table displaying information about child hotkeys on a particular subnet. - - Parameters: - - subtensor: An instance of the "bittensor.subtensor" class. - - hotkey: The hotkey of the parent node. - - children: A list of tuples containing information about child hotkeys. Each tuple should contain: - - The proportion of the child's stake relative to the total stake. - - The hotkey of the child node. - - netuid: The ID of the subnet. - - prompt: A boolean indicating whether to display a prompt for adding a child hotkey. - - Returns: - None - - Example Usage: - subtensor = bittensor.subtensor_instance - hotkey = "parent_hotkey" - children = [(0.5, "child1_hotkey"), (0.3, "child2_hotkey"), (0.2, "child3_hotkey")] - netuid = 1234 - prompt = True - render_table(subtensor, hotkey, children, netuid, prompt) - - """ - console = Console() - - # Initialize Rich table for pretty printing - table = Table( - show_header=True, - header_style="bold magenta", - border_style="blue", - style="dim", - ) - - # Add columns to the table with specific styles - table.add_column("Index", style="bold yellow", no_wrap=True, justify="center") - table.add_column("Child Hotkey", style="bold green") - table.add_column("Proportion", style="bold cyan", no_wrap=True, justify="right") - table.add_column( - "Childkey Take", style="bold blue", no_wrap=True, justify="right" - ) - table.add_column( - "Current Stake Weight", style="bold red", no_wrap=True, justify="right" - ) - - if not children: - console.print(table) - console.print( - f"[bold white]There are currently no child hotkeys on subnet {netuid} with Parent HotKey {hotkey}.[/bold white]" - ) - if prompt: - command = f"btcli stake set_children --children --hotkey --netuid {netuid} --proportion " - console.print( - f"[bold cyan]To add a child hotkey you can run the command: [white]{command}[/white][/bold cyan]" - ) - return - - console.print(f"\nChildren for netuid: {netuid} ", style="cyan") - - # calculate totals - total_proportion = 0 - total_stake = 0 - total_stake_weight = 0 - avg_take = 0 - - children_info = [] - for child in children: - proportion = child[0] - child_hotkey = child[1] - child_stake = subtensor.get_total_stake_for_hotkey( - ss58_address=child_hotkey - ) or Balance(0) - - child_take = subtensor.get_childkey_take(child_hotkey, netuid) - child_take = u16_to_float(child_take) - - # add to totals - total_stake += child_stake.tao - avg_take += child_take - - proportion = u64_to_float(proportion) - - children_info.append((proportion, child_hotkey, child_stake, child_take)) - - children_info.sort( - key=lambda x: x[0], reverse=True - ) # sorting by proportion (highest first) - - # add the children info to the table - for i, (proportion, hotkey, stake, child_take) in enumerate(children_info, 1): - proportion_percent = proportion * 100 # Proportion in percent - proportion_tao = hotkey_stake.tao * proportion # Proportion in TAO - - total_proportion += proportion_percent - - # Conditionally format text - proportion_str = f"{proportion_percent:.3f}% ({proportion_tao:.3f}τ)" - stake_weight = stake.tao + proportion_tao - total_stake_weight += stake_weight - take_str = f"{child_take * 100:.3f}%" - - hotkey = Text(hotkey, style="italic red" if proportion == 0 else "") - table.add_row( - str(i), - hotkey, - proportion_str, - take_str, - str(f"{stake_weight:.3f}"), - ) - - avg_take = avg_take / len(children_info) - - # add totals row - table.add_row( - "", - "[dim]Total[/dim]", - f"[dim]{total_proportion:.3f}%[/dim]", - f"[dim](avg) {avg_take * 100:.3f}%[/dim]", - f"[dim]{total_stake_weight:.3f}τ[/dim]", - style="dim", - ) - console.print(table) diff --git a/bittensor/commands/transfer.py b/bittensor/commands/transfer.py deleted file mode 100644 index 24c6e7840..000000000 --- a/bittensor/commands/transfer.py +++ /dev/null @@ -1,133 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 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 sys -import argparse -import bittensor -from rich.prompt import Prompt -from . import defaults - -console = bittensor.__console__ - - -class TransferCommand: - """ - Executes the ``transfer`` command to transfer TAO tokens from one account to another on the Bittensor network. - - This command is used for transactions between different accounts, enabling users to send tokens to other participants on the network. - - Usage: - The command requires specifying the destination address (public key) and the amount of TAO to be transferred. - It checks for sufficient balance and prompts for confirmation before proceeding with the transaction. - - Optional arguments: - - ``--dest`` (str): The destination address for the transfer. This can be in the form of an SS58 or ed2519 public key. - - ``--amount`` (float): The amount of TAO tokens to transfer. - - The command displays the user's current balance before prompting for the amount to transfer, ensuring transparency and accuracy in the transaction. - - Example usage:: - - btcli wallet transfer --dest 5Dp8... --amount 100 - - Note: - This command is crucial for executing token transfers within the Bittensor network. Users should verify the destination address and amount before confirming the transaction to avoid errors or loss of funds. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Transfer token of amount to destination.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - TransferCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Transfer token of amount to destination.""" - wallet = bittensor.wallet(config=cli.config) - subtensor.transfer( - wallet=wallet, - dest=cli.config.dest, - amount=cli.config.amount, - wait_for_inclusion=True, - prompt=not cli.config.no_prompt, - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - # Get destination. - if not config.dest and not config.no_prompt: - dest = Prompt.ask("Enter destination public key: (ss58 or ed2519)") - if not bittensor.utils.is_valid_bittensor_address_or_public_key(dest): - sys.exit() - else: - config.dest = str(dest) - - # Get current balance and print to user. - if not config.no_prompt: - wallet = bittensor.wallet(config=config) - subtensor = bittensor.subtensor(config=config, log_verbose=False) - with bittensor.__console__.status(":satellite: Checking Balance..."): - account_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bittensor.__console__.print( - "Balance: [green]{}[/green]".format(account_balance) - ) - - # Get amount. - if not config.get("amount"): - if not config.no_prompt: - amount = Prompt.ask("Enter TAO amount to transfer") - try: - config.amount = float(amount) - except ValueError: - console.print( - ":cross_mark:[red] Invalid TAO amount[/red] [bold white]{}[/bold white]".format( - amount - ) - ) - sys.exit() - else: - console.print( - ":cross_mark:[red] Invalid TAO amount[/red] [bold white]{}[/bold white]".format( - amount - ) - ) - sys.exit(1) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - transfer_parser = parser.add_parser( - "transfer", help="""Transfer Tao between accounts.""" - ) - transfer_parser.add_argument("--dest", dest="dest", type=str, required=False) - transfer_parser.add_argument( - "--amount", dest="amount", type=float, required=False - ) - - bittensor.wallet.add_args(transfer_parser) - bittensor.subtensor.add_args(transfer_parser) diff --git a/bittensor/commands/unstake.py b/bittensor/commands/unstake.py deleted file mode 100644 index 291aeb6e9..000000000 --- a/bittensor/commands/unstake.py +++ /dev/null @@ -1,443 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 sys -import argparse -from typing import List, Union, Optional, Tuple - -from rich.prompt import Confirm, Prompt -from tqdm import tqdm - -import bittensor -from bittensor.utils.balance import Balance -from . import defaults, GetChildrenCommand -from .utils import get_hotkey_wallets_for_wallet -from ..utils import wallet_utils - -console = bittensor.__console__ - - -class UnStakeCommand: - """ - Executes the ``remove`` command to unstake TAO tokens from one or more hotkeys and transfer them back to the user's coldkey on the Bittensor network. - - This command is used to withdraw tokens previously staked to different hotkeys. - - Usage: - Users can specify the amount to unstake, the hotkeys to unstake from (either by name or ``SS58`` address), and whether to unstake from all hotkeys. The command checks for sufficient stake and prompts for confirmation before proceeding with the unstaking process. - - Optional arguments: - - ``--all`` (bool): When set, unstakes all staked tokens from the specified hotkeys. - - ``--amount`` (float): The amount of TAO tokens to unstake. - - --hotkey_ss58address (str): The SS58 address of the hotkey to unstake from. - - ``--max_stake`` (float): Sets the maximum amount of TAO to remain staked in each hotkey. - - ``--hotkeys`` (list): Specifies hotkeys by name or SS58 address to unstake from. - - ``--all_hotkeys`` (bool): When set, unstakes from all hotkeys associated with the wallet, excluding any specified in --hotkeys. - - The command prompts for confirmation before executing the unstaking operation. - - Example usage:: - - btcli stake remove --amount 100 --hotkeys hk1,hk2 - - Note: - This command is important for users who wish to reallocate their stakes or withdraw them from the network. - It allows for flexible management of token stakes across different neurons (hotkeys) on the network. - """ - - @classmethod - def check_config(cls, config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if ( - not config.get("hotkey_ss58address", d=None) - and not config.is_set("wallet.hotkey") - and not config.no_prompt - and not config.get("all_hotkeys") - and not config.get("hotkeys") - ): - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - # Get amount. - if ( - not config.get("hotkey_ss58address") - and not config.get("amount") - and not config.get("unstake_all") - and not config.get("max_stake") - ): - hotkeys: str = "" - if config.get("all_hotkeys"): - hotkeys = "all hotkeys" - elif config.get("hotkeys"): - hotkeys = str(config.hotkeys).replace("[", "").replace("]", "") - else: - hotkeys = str(config.wallet.hotkey) - if config.no_prompt: - config.unstake_all = True - else: - # I really don't like this logic flow. It can be a bit confusing to read for something - # as serious as unstaking all. - if Confirm.ask(f"Unstake all Tao from: [bold]'{hotkeys}'[/bold]?"): - config.unstake_all = True - else: - config.unstake_all = False - amount = Prompt.ask("Enter Tao amount to unstake") - try: - config.amount = float(amount) - except ValueError: - console.print( - f":cross_mark:[red] Invalid Tao amount[/red] [bold white]{amount}[/bold white]" - ) - sys.exit() - - @staticmethod - def add_args(command_parser): - unstake_parser = command_parser.add_parser( - "remove", - help="""Remove stake from the specified hotkey into the coldkey balance.""", - ) - unstake_parser.add_argument( - "--all", dest="unstake_all", action="store_true", default=False - ) - unstake_parser.add_argument( - "--amount", dest="amount", type=float, required=False - ) - unstake_parser.add_argument( - "--hotkey_ss58address", dest="hotkey_ss58address", type=str, required=False - ) - unstake_parser.add_argument( - "--max_stake", - dest="max_stake", - type=float, - required=False, - action="store", - default=None, - help="""Specify the maximum amount of Tao to have staked in each hotkey.""", - ) - unstake_parser.add_argument( - "--hotkeys", - "--exclude_hotkeys", - "--wallet.hotkeys", - "--wallet.exclude_hotkeys", - required=False, - action="store", - default=[], - type=str, - nargs="*", - help="""Specify the hotkeys by name or ss58 address. (e.g. hk1 hk2 hk3)""", - ) - unstake_parser.add_argument( - "--all_hotkeys", - "--wallet.all_hotkeys", - required=False, - action="store_true", - default=False, - help="""To specify all hotkeys. Specifying hotkeys will exclude them from this all.""", - ) - bittensor.wallet.add_args(unstake_parser) - bittensor.subtensor.add_args(unstake_parser) - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Unstake token of amount from hotkey(s).""" - try: - config = cli.config.copy() - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=config, log_verbose=False - ) - UnStakeCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Unstake token of amount from hotkey(s).""" - config = cli.config.copy() - wallet = bittensor.wallet(config=config) - - # Get the hotkey_names (if any) and the hotkey_ss58s. - hotkeys_to_unstake_from: List[Tuple[Optional[str], str]] = [] - if cli.config.get("hotkey_ss58address"): - # Stake to specific hotkey. - hotkeys_to_unstake_from = [(None, cli.config.get("hotkey_ss58address"))] - elif cli.config.get("all_hotkeys"): - # Stake to all hotkeys. - all_hotkeys: List[bittensor.wallet] = get_hotkey_wallets_for_wallet( - wallet=wallet - ) - # Get the hotkeys to exclude. (d)efault to no exclusions. - hotkeys_to_exclude: List[str] = cli.config.get("hotkeys", d=[]) - # Exclude hotkeys that are specified. - hotkeys_to_unstake_from = [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys - if wallet.hotkey_str not in hotkeys_to_exclude - ] # definitely wallets - - elif cli.config.get("hotkeys"): - # Stake to specific hotkeys. - for hotkey_ss58_or_hotkey_name in cli.config.get("hotkeys"): - if bittensor.utils.is_valid_ss58_address(hotkey_ss58_or_hotkey_name): - # If the hotkey is a valid ss58 address, we add it to the list. - hotkeys_to_unstake_from.append((None, hotkey_ss58_or_hotkey_name)) - else: - # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. - # We then get the hotkey from the wallet and add it to the list. - wallet_ = bittensor.wallet( - config=cli.config, hotkey=hotkey_ss58_or_hotkey_name - ) - hotkeys_to_unstake_from.append( - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ) - elif cli.config.wallet.get("hotkey"): - # Only cli.config.wallet.hotkey is specified. - # so we stake to that single hotkey. - hotkey_ss58_or_name = cli.config.wallet.get("hotkey") - if bittensor.utils.is_valid_ss58_address(hotkey_ss58_or_name): - hotkeys_to_unstake_from = [(None, hotkey_ss58_or_name)] - else: - # Hotkey is not a valid ss58 address, so we assume it is a hotkey name. - wallet_ = bittensor.wallet( - config=cli.config, hotkey=hotkey_ss58_or_name - ) - hotkeys_to_unstake_from = [ - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ] - else: - # Only cli.config.wallet.hotkey is specified. - # so we stake to that single hotkey. - assert cli.config.wallet.hotkey is not None - hotkeys_to_unstake_from = [ - (None, bittensor.wallet(config=cli.config).hotkey.ss58_address) - ] - - final_hotkeys: List[Tuple[str, str]] = [] - final_amounts: List[Union[float, Balance]] = [] - for hotkey in tqdm(hotkeys_to_unstake_from): - hotkey: Tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) - unstake_amount_tao: float = cli.config.get( - "amount" - ) # The amount specified to unstake. - hotkey_stake: Balance = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey[1], coldkey_ss58=wallet.coldkeypub.ss58_address - ) - if unstake_amount_tao == None: - unstake_amount_tao = hotkey_stake.tao - if cli.config.get("max_stake"): - # Get the current stake of the hotkey from this coldkey. - unstake_amount_tao: float = hotkey_stake.tao - cli.config.get( - "max_stake" - ) - cli.config.amount = unstake_amount_tao - if unstake_amount_tao < 0: - # Skip if max_stake is greater than current stake. - continue - else: - if unstake_amount_tao is not None: - # There is a specified amount to unstake. - if unstake_amount_tao > hotkey_stake.tao: - # Skip if the specified amount is greater than the current stake. - continue - - final_amounts.append(unstake_amount_tao) - final_hotkeys.append(hotkey) # add both the name and the ss58 address. - - if len(final_hotkeys) == 0: - # No hotkeys to unstake from. - bittensor.__console__.print( - "Not enough stake to unstake from any hotkeys or max_stake is more than current stake." - ) - return None - - # Ask to unstake - if not cli.config.no_prompt: - if not Confirm.ask( - f"Do you want to unstake from the following keys to {wallet.name}:\n" - + "".join( - [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: {f'{amount} {bittensor.__tao_symbol__}' if amount else 'All'}[/bold white]\n" - for hotkey, amount in zip(final_hotkeys, final_amounts) - ] - ) - ): - return None - - if len(final_hotkeys) == 1: - # do regular unstake - return subtensor.unstake( - wallet=wallet, - hotkey_ss58=final_hotkeys[0][1], - amount=None if cli.config.get("unstake_all") else final_amounts[0], - wait_for_inclusion=True, - prompt=not cli.config.no_prompt, - ) - - subtensor.unstake_multiple( - wallet=wallet, - hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], - amounts=None if cli.config.get("unstake_all") else final_amounts, - wait_for_inclusion=True, - prompt=False, - ) - - -class RevokeChildrenCommand: - """ - Executes the ``revoke_children`` command to remove all children hotkeys on a specified subnet on the Bittensor network. - - This command is used to remove delegated authority from all child hotkeys, removing their position and influence on the subnet. - - Usage: - Users need to specify the parent hotkey and the subnet ID (netuid). - The user needs to have sufficient authority to make this call. - - The command prompts for confirmation before executing the revoke_children operation. - - Example usage:: - - btcli stake revoke_children --hotkey --netuid 1 - - Note: - This command is critical for users who wish to remove children hotkeys on the network. - It allows for a complete removal of delegated authority to enhance network participation and influence. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Revokes all children hotkeys.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - RevokeChildrenCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - wallet = bittensor.wallet(config=cli.config) - - # Get values if not set. - if not cli.config.is_set("netuid"): - cli.config.netuid = int(Prompt.ask("Enter netuid")) - - netuid = cli.config.netuid - total_subnets = subtensor.get_total_subnets() - if total_subnets is not None and not (0 <= netuid < total_subnets): - console.print("Netuid is outside the current subnet range") - return - - # get parent hotkey - if wallet and wallet.hotkey: - hotkey = wallet.hotkey.ss58_address - elif cli.config.is_set("hotkey"): - hotkey = cli.config.hotkey - elif cli.config.is_set("ss58"): - hotkey = cli.config.ss58 - else: - hotkey = Prompt.ask("Enter parent hotkey (ss58)") - - if not wallet_utils.is_valid_ss58_address(hotkey): - console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") - return - - success, message = subtensor.set_children( - wallet=wallet, - netuid=netuid, - children_with_proportions=[], - hotkey=hotkey, - wait_for_inclusion=cli.config.wait_for_inclusion, - wait_for_finalization=cli.config.wait_for_finalization, - prompt=cli.config.prompt, - ) - - # Result - if success: - if cli.config.wait_for_finalization and cli.config.wait_for_inclusion: - GetChildrenCommand.retrieve_children( - subtensor=subtensor, - hotkey=hotkey, - netuid=netuid, - render_table=True, - ) - console.print( - ":white_heavy_check_mark: [green]Revoked all children hotkeys.[/green]" - ) - else: - console.print( - f":cross_mark:[red] Unable to revoke children hotkeys.[/red] {message}" - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey_or_ss58 = Prompt.ask( - "Enter hotkey name or ss58", default=defaults.wallet.hotkey - ) - if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): - config.ss58 = str(hotkey_or_ss58) - else: - config.wallet.hotkey = str(hotkey_or_ss58) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "revoke_children", help="""Revoke all children hotkeys.""" - ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) - parser.add_argument("--hotkey", dest="hotkey", type=str, required=False) - parser.add_argument( - "--wait_for_inclusion", - dest="wait_for_inclusion", - action="store_true", - default=True, - help="""Wait for the transaction to be included in a block.""", - ) - parser.add_argument( - "--wait_for_finalization", - dest="wait_for_finalization", - action="store_true", - default=True, - help="""Wait for the transaction to be finalized.""", - ) - parser.add_argument( - "--prompt", - dest="prompt", - action="store_true", - default=True, - help="""Prompt for confirmation before proceeding.""", - ) - parser.add_argument( - "--y", - "--yes", - "--no_prompt", - dest="prompt", - action="store_false", - help="""Disable prompt for confirmation before proceeding. Defaults to Yes for all prompts.""", - ) - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) diff --git a/bittensor/commands/utils.py b/bittensor/commands/utils.py deleted file mode 100644 index e9d3b9178..000000000 --- a/bittensor/commands/utils.py +++ /dev/null @@ -1,282 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# -# 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 os -import sys -from dataclasses import dataclass -from typing import Any, List, Dict, Optional, Tuple - -from rich.prompt import Confirm, PromptBase - -import bittensor -from bittensor.utils import U64_NORMALIZED_FLOAT, U16_NORMALIZED_FLOAT -from bittensor.utils.balance import Balance -from bittensor.utils.registration import torch -from . import defaults - -console = bittensor.__console__ - - -class IntListPrompt(PromptBase): - """Prompt for a list of integers.""" - - def check_choice(self, value: str) -> bool: - assert self.choices is not None - # check if value is a valid choice or all the values in a list of ints are valid choices - return ( - value == "All" - or value in self.choices - or all( - val.strip() in self.choices for val in value.replace(",", " ").split() - ) - ) - - -def check_netuid_set( - config: "bittensor.config", - subtensor: "bittensor.subtensor", - allow_none: bool = False, -): - if subtensor.network != "nakamoto": - all_netuids = [str(netuid) for netuid in subtensor.get_subnets()] - if len(all_netuids) == 0: - console.print(":cross_mark:[red]There are no open networks.[/red]") - sys.exit() - - # Make sure netuid is set. - if not config.is_set("netuid"): - if not config.no_prompt: - netuid = IntListPrompt.ask( - "Enter netuid", choices=all_netuids, default=str(all_netuids[0]) - ) - else: - netuid = str(defaults.netuid) if not allow_none else "None" - else: - netuid = config.netuid - - if isinstance(netuid, str) and netuid.lower() in ["none"] and allow_none: - config.netuid = None - else: - if isinstance(netuid, list): - netuid = netuid[0] - try: - config.netuid = int(netuid) - except: - raise ValueError('netuid must be an integer or "None" (if applicable)') - - -def check_for_cuda_reg_config(config: "bittensor.config") -> None: - """Checks, when CUDA is available, if the user would like to register with their CUDA device.""" - if torch and torch.cuda.is_available(): - if not config.no_prompt: - if config.pow_register.cuda.get("use_cuda") is None: # flag not set - # Ask about cuda registration only if a CUDA device is available. - cuda = Confirm.ask("Detected CUDA device, use CUDA for registration?\n") - config.pow_register.cuda.use_cuda = cuda - - # Only ask about which CUDA device if the user has more than one CUDA device. - if ( - config.pow_register.cuda.use_cuda - and config.pow_register.cuda.get("dev_id") is None - ): - devices: List[str] = [str(x) for x in range(torch.cuda.device_count())] - device_names: List[str] = [ - torch.cuda.get_device_name(x) - for x in range(torch.cuda.device_count()) - ] - console.print("Available CUDA devices:") - choices_str: str = "" - for i, device in enumerate(devices): - choices_str += " {}: {}\n".format(device, device_names[i]) - console.print(choices_str) - dev_id = IntListPrompt.ask( - "Which GPU(s) would you like to use? Please list one, or comma-separated", - choices=devices, - default="All", - ) - if dev_id.lower() == "all": - dev_id = list(range(torch.cuda.device_count())) - else: - try: - # replace the commas with spaces then split over whitespace., - # then strip the whitespace and convert to ints. - dev_id = [ - int(dev_id.strip()) - for dev_id in dev_id.replace(",", " ").split() - ] - except ValueError: - console.log( - ":cross_mark:[red]Invalid GPU device[/red] [bold white]{}[/bold white]\nAvailable CUDA devices:{}".format( - dev_id, choices_str - ) - ) - sys.exit(1) - config.pow_register.cuda.dev_id = dev_id - else: - # flag was not set, use default value. - if config.pow_register.cuda.get("use_cuda") is None: - config.pow_register.cuda.use_cuda = defaults.pow_register.cuda.use_cuda - - -def get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: - hotkey_wallets = [] - hotkeys_path = wallet.path + "/" + wallet.name + "/hotkeys" - try: - hotkey_files = next(os.walk(os.path.expanduser(hotkeys_path)))[2] - except StopIteration: - hotkey_files = [] - for hotkey_file_name in hotkey_files: - try: - hotkey_for_name = bittensor.wallet( - path=wallet.path, name=wallet.name, hotkey=hotkey_file_name - ) - if ( - hotkey_for_name.hotkey_file.exists_on_device() - and not hotkey_for_name.hotkey_file.is_encrypted() - ): - hotkey_wallets.append(hotkey_for_name) - except Exception: - pass - return hotkey_wallets - - -def get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: - try: - wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [bittensor.wallet(path=path, name=name) for name in wallet_names] - except StopIteration: - # No wallet files found. - wallets = [] - return wallets - - -def get_all_wallets_for_path(path: str) -> List["bittensor.wallet"]: - all_wallets = [] - cold_wallets = get_coldkey_wallets_for_path(path) - for cold_wallet in cold_wallets: - if ( - cold_wallet.coldkeypub_file.exists_on_device() - and not cold_wallet.coldkeypub_file.is_encrypted() - ): - all_wallets.extend(get_hotkey_wallets_for_wallet(cold_wallet)) - return all_wallets - - -def filter_netuids_by_registered_hotkeys( - cli, subtensor, netuids, all_hotkeys -) -> List[int]: - netuids_with_registered_hotkeys = [] - for wallet in all_hotkeys: - netuids_list = subtensor.get_netuids_for_hotkey(wallet.hotkey.ss58_address) - bittensor.logging.debug( - f"Hotkey {wallet.hotkey.ss58_address} registered in netuids: {netuids_list}" - ) - netuids_with_registered_hotkeys.extend(netuids_list) - - if not cli.config.netuids: - netuids = netuids_with_registered_hotkeys - - else: - netuids = [netuid for netuid in netuids if netuid in cli.config.netuids] - netuids.extend(netuids_with_registered_hotkeys) - - return list(set(netuids)) - - -def normalize_hyperparameters( - subnet: bittensor.SubnetHyperparameters, -) -> List[Tuple[str, str, str]]: - """ - Normalizes the hyperparameters of a subnet. - - Args: - subnet: The subnet hyperparameters object. - - Returns: - A list of tuples containing the parameter name, value, and normalized value. - """ - param_mappings = { - "adjustment_alpha": U64_NORMALIZED_FLOAT, - "min_difficulty": U64_NORMALIZED_FLOAT, - "max_difficulty": U64_NORMALIZED_FLOAT, - "difficulty": U64_NORMALIZED_FLOAT, - "bonds_moving_avg": U64_NORMALIZED_FLOAT, - "max_weight_limit": U16_NORMALIZED_FLOAT, - "kappa": U16_NORMALIZED_FLOAT, - "alpha_high": U16_NORMALIZED_FLOAT, - "alpha_low": U16_NORMALIZED_FLOAT, - "min_burn": Balance.from_rao, - "max_burn": Balance.from_rao, - } - - normalized_values: List[Tuple[str, str, str]] = [] - subnet_dict = subnet.__dict__ - - for param, value in subnet_dict.items(): - try: - if param in param_mappings: - norm_value = param_mappings[param](value) - if isinstance(norm_value, float): - norm_value = f"{norm_value:.{10}g}" - else: - norm_value = value - except Exception as e: - bittensor.logging.warning(f"Error normalizing parameter '{param}': {e}") - norm_value = "-" - - normalized_values.append((param, str(value), str(norm_value))) - - return normalized_values - - -@dataclass -class DelegatesDetails: - display: str - additional: List[Tuple[str, str]] - web: str - legal: Optional[str] = None - riot: Optional[str] = None - email: Optional[str] = None - pgp_fingerprint: Optional[str] = None - image: Optional[str] = None - twitter: Optional[str] = None - - @classmethod - def from_chain_data(cls, data: Dict[str, Any]) -> "DelegatesDetails": - return cls( - display=data.get("display", ""), - additional=data.get("additional", []), - web=data.get("web", ""), - legal=data.get("legal"), - riot=data.get("riot"), - email=data.get("email"), - pgp_fingerprint=data.get("pgp_fingerprint"), - image=data.get("image"), - twitter=data.get("image"), - ) - - -def get_delegates_details( - subtensor: "bittensor.subtensor", -) -> Optional[Dict[str, DelegatesDetails]]: - try: - return subtensor.get_delegate_identities() - except Exception as error: - bittensor.logging.exception( - f"Unable to get Delegates Identities. Error: {error}" - ) - return None # Fail silently diff --git a/bittensor/commands/wallets.py b/bittensor/commands/wallets.py deleted file mode 100644 index 15819ece7..000000000 --- a/bittensor/commands/wallets.py +++ /dev/null @@ -1,1101 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 os -import sys -from typing import List, Optional, Tuple - -import requests -from rich.prompt import Confirm, Prompt -from rich.table import Table - -import bittensor - -from ..utils import RAOPERTAO -from . import defaults - - -class RegenColdkeyCommand: - """ - Executes the ``regen_coldkey`` command to regenerate a coldkey for a wallet on the Bittensor network. - - This command is used to create a new coldkey from an existing mnemonic, seed, or JSON file. - - Usage: - Users can specify a mnemonic, a seed string, or a JSON file path to regenerate a coldkey. - The command supports optional password protection for the generated key and can overwrite an existing coldkey. - - Optional arguments: - - ``--mnemonic`` (str): A mnemonic phrase used to regenerate the key. - - ``--seed`` (str): A seed hex string used for key regeneration. - - ``--json`` (str): Path to a JSON file containing an encrypted key backup. - - ``--json_password`` (str): Password to decrypt the JSON file. - - ``--use_password`` (bool): Enables password protection for the generated key. - - ``--overwrite_coldkey`` (bool): Overwrites the existing coldkey with the new one. - - Example usage:: - - btcli wallet regen_coldkey --mnemonic "word1 word2 ... word12" - - Note: - This command is critical for users who need to regenerate their coldkey, possibly for recovery or security reasons. - It should be used with caution to avoid overwriting existing keys unintentionally. - """ - - def run(cli): - r"""Creates a new coldkey under this wallet.""" - wallet = bittensor.wallet(config=cli.config) - - json_str: Optional[str] = None - json_password: Optional[str] = None - if cli.config.get("json"): - file_name: str = cli.config.get("json") - if not os.path.exists(file_name) or not os.path.isfile(file_name): - raise ValueError("File {} does not exist".format(file_name)) - with open(cli.config.get("json"), "r") as f: - json_str = f.read() - # Password can be "", assume if None - json_password = cli.config.get("json_password", "") - wallet.regenerate_coldkey( - mnemonic=cli.config.mnemonic, - seed=cli.config.seed, - json=(json_str, json_password), - use_password=cli.config.use_password, - overwrite=cli.config.overwrite_coldkey, - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if ( - config.mnemonic == None - and config.get("seed", d=None) == None - and config.get("json", d=None) == None - ): - prompt_answer = Prompt.ask("Enter mnemonic, seed, or json file location") - if prompt_answer.startswith("0x"): - config.seed = prompt_answer - elif len(prompt_answer.split(" ")) > 1: - config.mnemonic = prompt_answer - else: - config.json = prompt_answer - - if config.get("json", d=None) and config.get("json_password", d=None) == None: - config.json_password = Prompt.ask( - "Enter json backup password", password=True - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - regen_coldkey_parser = parser.add_parser( - "regen_coldkey", help="""Regenerates a coldkey from a passed value""" - ) - regen_coldkey_parser.add_argument( - "--mnemonic", - required=False, - nargs="+", - help="Mnemonic used to regen your key i.e. horse cart dog ...", - ) - regen_coldkey_parser.add_argument( - "--seed", - required=False, - default=None, - help="Seed hex string used to regen your key i.e. 0x1234...", - ) - regen_coldkey_parser.add_argument( - "--json", - required=False, - default=None, - help="""Path to a json file containing the encrypted key backup. (e.g. from PolkadotJS)""", - ) - regen_coldkey_parser.add_argument( - "--json_password", - required=False, - default=None, - help="""Password to decrypt the json file.""", - ) - regen_coldkey_parser.add_argument( - "--use_password", - dest="use_password", - action="store_true", - help="""Set true to protect the generated bittensor key with a password.""", - default=True, - ) - regen_coldkey_parser.add_argument( - "--no_password", - dest="use_password", - action="store_false", - help="""Set off protects the generated bittensor key with a password.""", - ) - regen_coldkey_parser.add_argument( - "--overwrite_coldkey", - default=False, - action="store_true", - help="""Overwrite the old coldkey with the newly generated coldkey""", - ) - bittensor.wallet.add_args(regen_coldkey_parser) - bittensor.subtensor.add_args(regen_coldkey_parser) - - -class RegenColdkeypubCommand: - """ - Executes the ``regen_coldkeypub`` command to regenerate the public part of a coldkey (coldkeypub) for a wallet on the Bittensor network. - - This command is used when a user needs to recreate their coldkeypub from an existing public key or SS58 address. - - Usage: - The command requires either a public key in hexadecimal format or an ``SS58`` address to regenerate the coldkeypub. It optionally allows overwriting an existing coldkeypub file. - - Optional arguments: - - ``--public_key_hex`` (str): The public key in hex format. - - ``--ss58_address`` (str): The SS58 address of the coldkey. - - ``--overwrite_coldkeypub`` (bool): Overwrites the existing coldkeypub file with the new one. - - Example usage:: - - btcli wallet regen_coldkeypub --ss58_address 5DkQ4... - - Note: - This command is particularly useful for users who need to regenerate their coldkeypub, perhaps due to file corruption or loss. - It is a recovery-focused utility that ensures continued access to wallet functionalities. - """ - - def run(cli): - r"""Creates a new coldkeypub under this wallet.""" - wallet = bittensor.wallet(config=cli.config) - wallet.regenerate_coldkeypub( - ss58_address=cli.config.get("ss58_address"), - public_key=cli.config.get("public_key_hex"), - overwrite=cli.config.overwrite_coldkeypub, - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if config.ss58_address == None and config.public_key_hex == None: - prompt_answer = Prompt.ask( - "Enter the ss58_address or the public key in hex" - ) - if prompt_answer.startswith("0x"): - config.public_key_hex = prompt_answer - else: - config.ss58_address = prompt_answer - if not bittensor.utils.is_valid_bittensor_address_or_public_key( - address=( - config.ss58_address if config.ss58_address else config.public_key_hex - ) - ): - sys.exit(1) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - regen_coldkeypub_parser = parser.add_parser( - "regen_coldkeypub", - help="""Regenerates a coldkeypub from the public part of the coldkey.""", - ) - regen_coldkeypub_parser.add_argument( - "--public_key", - "--pubkey", - dest="public_key_hex", - required=False, - default=None, - type=str, - help="The public key (in hex) of the coldkey to regen e.g. 0x1234 ...", - ) - regen_coldkeypub_parser.add_argument( - "--ss58_address", - "--addr", - "--ss58", - dest="ss58_address", - required=False, - default=None, - type=str, - help="The ss58 address of the coldkey to regen e.g. 5ABCD ...", - ) - regen_coldkeypub_parser.add_argument( - "--overwrite_coldkeypub", - default=False, - action="store_true", - help="""Overwrite the old coldkeypub file with the newly generated coldkeypub""", - ) - bittensor.wallet.add_args(regen_coldkeypub_parser) - bittensor.subtensor.add_args(regen_coldkeypub_parser) - - -class RegenHotkeyCommand: - """ - Executes the ``regen_hotkey`` command to regenerate a hotkey for a wallet on the Bittensor network. - - Similar to regenerating a coldkey, this command creates a new hotkey from a mnemonic, seed, or JSON file. - - Usage: - Users can provide a mnemonic, seed string, or a JSON file to regenerate the hotkey. - The command supports optional password protection and can overwrite an existing hotkey. - - Optional arguments: - - ``--mnemonic`` (str): A mnemonic phrase used to regenerate the key. - - ``--seed`` (str): A seed hex string used for key regeneration. - - ``--json`` (str): Path to a JSON file containing an encrypted key backup. - - ``--json_password`` (str): Password to decrypt the JSON file. - - ``--use_password`` (bool): Enables password protection for the generated key. - - ``--overwrite_hotkey`` (bool): Overwrites the existing hotkey with the new one. - - Example usage:: - - btcli wallet regen_hotkey - btcli wallet regen_hotkey --seed 0x1234... - - Note: - This command is essential for users who need to regenerate their hotkey, possibly for security upgrades or key recovery. - It should be used cautiously to avoid accidental overwrites of existing keys. - """ - - def run(cli): - r"""Creates a new coldkey under this wallet.""" - wallet = bittensor.wallet(config=cli.config) - - json_str: Optional[str] = None - json_password: Optional[str] = None - if cli.config.get("json"): - file_name: str = cli.config.get("json") - if not os.path.exists(file_name) or not os.path.isfile(file_name): - raise ValueError("File {} does not exist".format(file_name)) - with open(cli.config.get("json"), "r") as f: - json_str = f.read() - - # Password can be "", assume if None - json_password = cli.config.get("json_password", "") - - wallet.regenerate_hotkey( - mnemonic=cli.config.mnemonic, - seed=cli.config.seed, - json=(json_str, json_password), - use_password=cli.config.use_password, - overwrite=cli.config.overwrite_hotkey, - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - if ( - config.mnemonic == None - and config.get("seed", d=None) == None - and config.get("json", d=None) == None - ): - prompt_answer = Prompt.ask("Enter mnemonic, seed, or json file location") - if prompt_answer.startswith("0x"): - config.seed = prompt_answer - elif len(prompt_answer.split(" ")) > 1: - config.mnemonic = prompt_answer - else: - config.json = prompt_answer - - if config.get("json", d=None) and config.get("json_password", d=None) == None: - config.json_password = Prompt.ask( - "Enter json backup password", password=True - ) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - regen_hotkey_parser = parser.add_parser( - "regen_hotkey", help="""Regenerates a hotkey from a passed mnemonic""" - ) - regen_hotkey_parser.add_argument( - "--mnemonic", - required=False, - nargs="+", - help="Mnemonic used to regen your key i.e. horse cart dog ...", - ) - regen_hotkey_parser.add_argument( - "--seed", - required=False, - default=None, - help="Seed hex string used to regen your key i.e. 0x1234...", - ) - regen_hotkey_parser.add_argument( - "--json", - required=False, - default=None, - help="""Path to a json file containing the encrypted key backup. (e.g. from PolkadotJS)""", - ) - regen_hotkey_parser.add_argument( - "--json_password", - required=False, - default=None, - help="""Password to decrypt the json file.""", - ) - regen_hotkey_parser.add_argument( - "--use_password", - dest="use_password", - action="store_true", - help="""Set true to protect the generated bittensor key with a password.""", - default=False, - ) - regen_hotkey_parser.add_argument( - "--no_password", - dest="use_password", - action="store_false", - help="""Set off protects the generated bittensor key with a password.""", - ) - regen_hotkey_parser.add_argument( - "--overwrite_hotkey", - dest="overwrite_hotkey", - action="store_true", - default=False, - help="""Overwrite the old hotkey with the newly generated hotkey""", - ) - bittensor.wallet.add_args(regen_hotkey_parser) - bittensor.subtensor.add_args(regen_hotkey_parser) - - -class NewHotkeyCommand: - """ - Executes the ``new_hotkey`` command to create a new hotkey under a wallet on the Bittensor network. - - This command is used to generate a new hotkey for managing a neuron or participating in the network. - - Usage: - The command creates a new hotkey with an optional word count for the mnemonic and supports password protection. - It also allows overwriting an existing hotkey. - - Optional arguments: - - ``--n_words`` (int): The number of words in the mnemonic phrase. - - ``--use_password`` (bool): Enables password protection for the generated key. - - ``--overwrite_hotkey`` (bool): Overwrites the existing hotkey with the new one. - - Example usage:: - - btcli wallet new_hotkey --n_words 24 - - Note: - This command is useful for users who wish to create additional hotkeys for different purposes, - such as running multiple miners or separating operational roles within the network. - """ - - def run(cli): - """Creates a new hotke under this wallet.""" - wallet = bittensor.wallet(config=cli.config) - wallet.create_new_hotkey( - n_words=cli.config.n_words, - use_password=cli.config.use_password, - overwrite=cli.config.overwrite_hotkey, - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - new_hotkey_parser = parser.add_parser( - "new_hotkey", - help="""Creates a new hotkey (for running a miner) under the specified path.""", - ) - new_hotkey_parser.add_argument( - "--n_words", - type=int, - choices=[12, 15, 18, 21, 24], - default=12, - help="""The number of words representing the mnemonic. i.e. horse cart dog ... x 24""", - ) - new_hotkey_parser.add_argument( - "--use_password", - dest="use_password", - action="store_true", - help="""Set true to protect the generated bittensor key with a password.""", - default=False, - ) - new_hotkey_parser.add_argument( - "--no_password", - dest="use_password", - action="store_false", - help="""Set off protects the generated bittensor key with a password.""", - ) - new_hotkey_parser.add_argument( - "--overwrite_hotkey", - action="store_true", - default=False, - help="""Overwrite the old hotkey with the newly generated hotkey""", - ) - bittensor.wallet.add_args(new_hotkey_parser) - bittensor.subtensor.add_args(new_hotkey_parser) - - -class NewColdkeyCommand: - """ - Executes the ``new_coldkey`` command to create a new coldkey under a wallet on the Bittensor network. - - This command generates a coldkey, which is essential for holding balances and performing high-value transactions. - - Usage: - The command creates a new coldkey with an optional word count for the mnemonic and supports password protection. - It also allows overwriting an existing coldkey. - - Optional arguments: - - ``--n_words`` (int): The number of words in the mnemonic phrase. - - ``--use_password`` (bool): Enables password protection for the generated key. - - ``--overwrite_coldkey`` (bool): Overwrites the existing coldkey with the new one. - - Example usage:: - - btcli wallet new_coldkey --n_words 15 - - Note: - This command is crucial for users who need to create a new coldkey for enhanced security or as part of setting up a new wallet. - It's a foundational step in establishing a secure presence on the Bittensor network. - """ - - def run(cli): - r"""Creates a new coldkey under this wallet.""" - wallet = bittensor.wallet(config=cli.config) - wallet.create_new_coldkey( - n_words=cli.config.n_words, - use_password=cli.config.use_password, - overwrite=cli.config.overwrite_coldkey, - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - new_coldkey_parser = parser.add_parser( - "new_coldkey", - help="""Creates a new coldkey (for containing balance) under the specified path. """, - ) - new_coldkey_parser.add_argument( - "--n_words", - type=int, - choices=[12, 15, 18, 21, 24], - default=12, - help="""The number of words representing the mnemonic. i.e. horse cart dog ... x 24""", - ) - new_coldkey_parser.add_argument( - "--use_password", - dest="use_password", - action="store_true", - help="""Set true to protect the generated bittensor key with a password.""", - default=True, - ) - new_coldkey_parser.add_argument( - "--no_password", - dest="use_password", - action="store_false", - help="""Set off protects the generated bittensor key with a password.""", - ) - new_coldkey_parser.add_argument( - "--overwrite_coldkey", - action="store_true", - default=False, - help="""Overwrite the old coldkey with the newly generated coldkey""", - ) - bittensor.wallet.add_args(new_coldkey_parser) - bittensor.subtensor.add_args(new_coldkey_parser) - - -class WalletCreateCommand: - """ - Executes the ``create`` command to generate both a new coldkey and hotkey under a specified wallet on the Bittensor network. - - This command is a comprehensive utility for creating a complete wallet setup with both cold and hotkeys. - - Usage: - The command facilitates the creation of a new coldkey and hotkey with an optional word count for the mnemonics. - It supports password protection for the coldkey and allows overwriting of existing keys. - - Optional arguments: - - ``--n_words`` (int): The number of words in the mnemonic phrase for both keys. - - ``--use_password`` (bool): Enables password protection for the coldkey. - - ``--overwrite_coldkey`` (bool): Overwrites the existing coldkey with the new one. - - ``--overwrite_hotkey`` (bool): Overwrites the existing hotkey with the new one. - - Example usage:: - - btcli wallet create --n_words 21 - - Note: - This command is ideal for new users setting up their wallet for the first time or for those who wish to completely renew their wallet keys. - It ensures a fresh start with new keys for secure and effective participation in the network. - """ - - def run(cli): - r"""Creates a new coldkey and hotkey under this wallet.""" - wallet = bittensor.wallet(config=cli.config) - wallet.create_new_coldkey( - n_words=cli.config.n_words, - use_password=cli.config.use_password, - overwrite=cli.config.overwrite_coldkey, - ) - wallet.create_new_hotkey( - n_words=cli.config.n_words, - use_password=False, - overwrite=cli.config.overwrite_hotkey, - ) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - new_coldkey_parser = parser.add_parser( - "create", - help="""Creates a new coldkey (for containing balance) under the specified path. """, - ) - new_coldkey_parser.add_argument( - "--n_words", - type=int, - choices=[12, 15, 18, 21, 24], - default=12, - help="""The number of words representing the mnemonic. i.e. horse cart dog ... x 24""", - ) - new_coldkey_parser.add_argument( - "--use_password", - dest="use_password", - action="store_true", - help="""Set true to protect the generated bittensor key with a password.""", - default=True, - ) - new_coldkey_parser.add_argument( - "--no_password", - dest="use_password", - action="store_false", - help="""Set off protects the generated bittensor key with a password.""", - ) - new_coldkey_parser.add_argument( - "--overwrite_coldkey", - action="store_true", - default=False, - help="""Overwrite the old coldkey with the newly generated coldkey""", - ) - new_coldkey_parser.add_argument( - "--overwrite_hotkey", - action="store_true", - default=False, - help="""Overwrite the old hotkey with the newly generated hotkey""", - ) - bittensor.wallet.add_args(new_coldkey_parser) - bittensor.subtensor.add_args(new_coldkey_parser) - - -def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: - """Get all coldkey wallet names from path.""" - try: - wallet_names = next(os.walk(os.path.expanduser(path)))[1] - return [bittensor.wallet(path=path, name=name) for name in wallet_names] - except StopIteration: - # No wallet files found. - wallets = [] - return wallets - - -class UpdateWalletCommand: - """ - Executes the ``update`` command to check and potentially update the security of the wallets in the Bittensor network. - - This command is used to enhance wallet security using modern encryption standards. - - Usage: - The command checks if any of the wallets need an update in their security protocols. - It supports updating all legacy wallets or a specific one based on the user's choice. - - Optional arguments: - - ``--all`` (bool): When set, updates all legacy wallets. - - Example usage:: - - btcli wallet update --all - - Note: - This command is important for maintaining the highest security standards for users' wallets. - It is recommended to run this command periodically to ensure wallets are up-to-date with the latest security practices. - """ - - @staticmethod - def run(cli): - """Check if any of the wallets needs an update.""" - config = cli.config.copy() - if config.get("all", d=False) == True: - wallets = _get_coldkey_wallets_for_path(config.wallet.path) - else: - wallets = [bittensor.wallet(config=config)] - - for wallet in wallets: - print("\n===== ", wallet, " =====") - wallet.coldkey_file.check_and_update_encryption() - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - update_wallet_parser = parser.add_parser( - "update", - help="""Updates the wallet security using NaCL instead of ansible vault.""", - ) - update_wallet_parser.add_argument("--all", action="store_true") - bittensor.wallet.add_args(update_wallet_parser) - bittensor.subtensor.add_args(update_wallet_parser) - - @staticmethod - def check_config(config: "bittensor.Config"): - if config.get("all", d=False) == False: - if not config.no_prompt: - if Confirm.ask("Do you want to update all legacy wallets?"): - config["all"] = True - - # Ask the user to specify the wallet if the wallet name is not clear. - if ( - config.get("all", d=False) == False - and config.wallet.get("name") == bittensor.defaults.wallet.name - and not config.no_prompt - ): - wallet_name = Prompt.ask( - "Enter wallet name", default=bittensor.defaults.wallet.name - ) - config.wallet.name = str(wallet_name) - - -def _get_coldkey_ss58_addresses_for_path(path: str) -> Tuple[List[str], List[str]]: - """Get all coldkey ss58 addresses from path.""" - - def list_coldkeypub_files(dir_path): - abspath = os.path.abspath(os.path.expanduser(dir_path)) - coldkey_files = [] - wallet_names = [] - - for potential_wallet_name in os.listdir(abspath): - coldkey_path = os.path.join( - abspath, potential_wallet_name, "coldkeypub.txt" - ) - if os.path.isdir( - os.path.join(abspath, potential_wallet_name) - ) and os.path.exists(coldkey_path): - coldkey_files.append(coldkey_path) - wallet_names.append(potential_wallet_name) - else: - bittensor.logging.warning( - f"{coldkey_path} does not exist. Excluding..." - ) - return coldkey_files, wallet_names - - coldkey_files, wallet_names = list_coldkeypub_files(path) - addresses = [ - bittensor.keyfile(coldkey_path).keypair.ss58_address - for coldkey_path in coldkey_files - ] - return addresses, wallet_names - - -class WalletBalanceCommand: - """ - Executes the ``balance`` command to check the balance of the wallet on the Bittensor network. - - This command provides a detailed view of the wallet's coldkey balances, including free and staked balances. - - Usage: - The command lists the balances of all wallets in the user's configuration directory, showing the wallet name, coldkey address, and the respective free and staked balances. - - Optional arguments: - None. The command uses the wallet and subtensor configurations to fetch balance data. - - Example usages: - - - To display the balance of a single wallet, use the command with the `--wallet.name` argument to specify the wallet name: - - ``` - btcli w balance --wallet.name WALLET - ``` - - - Alternatively, you can invoke the command without specifying a wallet name, which will prompt you to enter the wallets path: - - ``` - btcli w balance - ``` - - - To display the balances of all wallets, use the `--all` argument: - - ``` - btcli w balance --all - ``` - - Note: - When using `btcli`, `w` is used interchangeably with `wallet`. You may use either based on your preference for brevity or clarity. - This command is essential for users to monitor their financial status on the Bittensor network. - It helps in keeping track of assets and ensuring the wallet's financial health. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - """Check the balance of the wallet.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - WalletBalanceCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - wallet = bittensor.wallet(config=cli.config) - - wallet_names = [] - coldkeys = [] - free_balances = [] - staked_balances = [] - total_free_balance = 0 - total_staked_balance = 0 - balances = {} - - if cli.config.get("all", d=None): - coldkeys, wallet_names = _get_coldkey_ss58_addresses_for_path( - cli.config.wallet.path - ) - - free_balances = [ - subtensor.get_balance(coldkeys[i]) for i in range(len(coldkeys)) - ] - - staked_balances = [ - subtensor.get_total_stake_for_coldkey(coldkeys[i]) - for i in range(len(coldkeys)) - ] - - total_free_balance = sum(free_balances) - total_staked_balance = sum(staked_balances) - - balances = { - name: (coldkey, free, staked) - for name, coldkey, free, staked in sorted( - zip(wallet_names, coldkeys, free_balances, staked_balances) - ) - } - else: - coldkey_wallet = bittensor.wallet(config=cli.config) - if ( - coldkey_wallet.coldkeypub_file.exists_on_device() - and not coldkey_wallet.coldkeypub_file.is_encrypted() - ): - coldkeys = [coldkey_wallet.coldkeypub.ss58_address] - wallet_names = [coldkey_wallet.name] - - free_balances = [ - subtensor.get_balance(coldkeys[i]) for i in range(len(coldkeys)) - ] - - staked_balances = [ - subtensor.get_total_stake_for_coldkey(coldkeys[i]) - for i in range(len(coldkeys)) - ] - - total_free_balance = sum(free_balances) - total_staked_balance = sum(staked_balances) - - balances = { - name: (coldkey, free, staked) - for name, coldkey, free, staked in sorted( - zip(wallet_names, coldkeys, free_balances, staked_balances) - ) - } - - if not coldkey_wallet.coldkeypub_file.exists_on_device(): - bittensor.__console__.print("[bold red]No wallets found.") - return - - table = Table(show_footer=False) - table.title = "[white]Wallet Coldkey Balances" - table.add_column( - "[white]Wallet Name", - header_style="overline white", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - - table.add_column( - "[white]Coldkey Address", - header_style="overline white", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - - for typestr in ["Free", "Staked", "Total"]: - table.add_column( - f"[white]{typestr} Balance", - header_style="overline white", - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - - for name, (coldkey, free, staked) in balances.items(): - table.add_row( - name, - coldkey, - str(free), - str(staked), - str(free + staked), - ) - table.add_row() - table.add_row( - "Total Balance Across All Coldkeys", - "", - str(total_free_balance), - str(total_staked_balance), - str(total_free_balance + total_staked_balance), - ) - table.show_footer = True - - table.box = None - table.pad_edge = False - table.width = None - bittensor.__console__.print(table) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - balance_parser = parser.add_parser( - "balance", help="""Checks the balance of the wallet.""" - ) - balance_parser.add_argument( - "--all", - dest="all", - action="store_true", - help="""View balance for all wallets.""", - default=False, - ) - - bittensor.wallet.add_args(balance_parser) - bittensor.subtensor.add_args(balance_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if ( - not config.is_set("wallet.path") - and not config.no_prompt - and not config.get("all", d=None) - ): - path = Prompt.ask("Enter wallets path", default=defaults.wallet.path) - config.wallet.path = str(path) - - if ( - not config.is_set("wallet.name") - and not config.no_prompt - and not config.get("all", d=None) - ): - wallet_name = Prompt.ask( - "Enter wallet name", default=defaults.wallet.name - ) - config.wallet.name = str(wallet_name) - - if not config.is_set("subtensor.network") and not config.no_prompt: - network = Prompt.ask( - "Enter network", - default=defaults.subtensor.network, - choices=bittensor.__networks__, - ) - config.subtensor.network = str(network) - ( - _, - config.subtensor.chain_endpoint, - ) = bittensor.subtensor.determine_chain_endpoint_and_network(str(network)) - - -API_URL = "https://api.subquery.network/sq/TaoStats/bittensor-indexer" -MAX_TXN = 1000 -GRAPHQL_QUERY = """ -query ($first: Int!, $after: Cursor, $filter: TransferFilter, $order: [TransfersOrderBy!]!) { - transfers(first: $first, after: $after, filter: $filter, orderBy: $order) { - nodes { - id - from - to - amount - extrinsicId - blockNumber - } - pageInfo { - endCursor - hasNextPage - hasPreviousPage - } - totalCount - } -} -""" - - -class GetWalletHistoryCommand: - """ - Executes the ``history`` command to fetch the latest transfers of the provided wallet on the Bittensor network. - - This command provides a detailed view of the transfers carried out on the wallet. - - Usage: - The command lists the latest transfers of the provided wallet, showing the From, To, Amount, Extrinsic Id and Block Number. - - Optional arguments: - None. The command uses the wallet and subtensor configurations to fetch latest transfer data associated with a wallet. - - Example usage:: - - btcli wallet history - - Note: - This command is essential for users to monitor their financial status on the Bittensor network. - It helps in fetching info on all the transfers so that user can easily tally and cross check the transactions. - """ - - @staticmethod - def run(cli): - r"""Check the transfer history of the provided wallet.""" - wallet = bittensor.wallet(config=cli.config) - wallet_address = wallet.get_coldkeypub().ss58_address - # Fetch all transfers - transfers = get_wallet_transfers(wallet_address) - - # Create output table - table = create_transfer_history_table(transfers) - - bittensor.__console__.print(table) - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - history_parser = parser.add_parser( - "history", - help="""Fetch transfer history associated with the provided wallet""", - ) - bittensor.wallet.add_args(history_parser) - bittensor.subtensor.add_args(history_parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - - -def get_wallet_transfers(wallet_address) -> List[dict]: - """Get all transfers associated with the provided wallet address.""" - - variables = { - "first": MAX_TXN, - "filter": { - "or": [ - {"from": {"equalTo": wallet_address}}, - {"to": {"equalTo": wallet_address}}, - ] - }, - "order": "BLOCK_NUMBER_DESC", - } - - response = requests.post( - API_URL, json={"query": GRAPHQL_QUERY, "variables": variables} - ) - data = response.json() - - # Extract nodes and pageInfo from the response - transfer_data = data.get("data", {}).get("transfers", {}) - transfers = transfer_data.get("nodes", []) - - return transfers - - -def create_transfer_history_table(transfers): - """Get output transfer table""" - - table = Table(show_footer=False) - # Define the column names - column_names = [ - "Id", - "From", - "To", - "Amount (Tao)", - "Extrinsic Id", - "Block Number", - "URL (taostats)", - ] - taostats_url_base = "https://x.taostats.io/extrinsic" - - # Create a table - table = Table(show_footer=False) - table.title = "[white]Wallet Transfers" - - # Define the column styles - header_style = "overline white" - footer_style = "overline white" - column_style = "rgb(50,163,219)" - no_wrap = True - - # Add columns to the table - for column_name in column_names: - table.add_column( - f"[white]{column_name}", - header_style=header_style, - footer_style=footer_style, - style=column_style, - no_wrap=no_wrap, - justify="left" if column_name == "Id" else "right", - ) - - # Add rows to the table - for item in transfers: - try: - tao_amount = int(item["amount"]) / RAOPERTAO - except: - tao_amount = item["amount"] - table.add_row( - item["id"], - item["from"], - item["to"], - f"{tao_amount:.3f}", - str(item["extrinsicId"]), - item["blockNumber"], - f"{taostats_url_base}/{item['blockNumber']}-{item['extrinsicId']}", - ) - table.add_row() - table.show_footer = True - table.box = None - table.pad_edge = False - table.width = None - return table diff --git a/bittensor/commands/weights.py b/bittensor/commands/weights.py deleted file mode 100644 index b8844433c..000000000 --- a/bittensor/commands/weights.py +++ /dev/null @@ -1,290 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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. - -"""Module that encapsulates the CommitWeightCommand and the RevealWeightCommand. Used to commit and reveal weights -for a specific subnet on the Bittensor Network.""" - -import argparse -import os -import re - -import numpy as np -from rich.prompt import Prompt, Confirm - -import bittensor -import bittensor.utils.weight_utils as weight_utils -from . import defaults # type: ignore - - -class CommitWeightCommand: - """ - Executes the ``commit`` command to commit weights for specific subnet on the Bittensor network. - - Usage: - The command allows committing weights for a specific subnet. Users need to specify the netuid (network unique identifier), corresponding UIDs, and weights they wish to commit. - - Optional arguments: - - ``--netuid`` (int): The netuid of the subnet for which weights are to be commited. - - ``--uids`` (str): Corresponding UIDs for the specified netuid, in comma-separated format. - - ``--weights`` (str): Corresponding weights for the specified UIDs, in comma-separated format. - - Example usage: - $ btcli wt commit --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 - - Note: - This command is used to commit weights for a specific subnet and requires the user to have the necessary permissions. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Commit weights for a specific subnet.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - CommitWeightCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Commit weights for a specific subnet""" - wallet = bittensor.wallet(config=cli.config) - - # Get values if not set - if not cli.config.is_set("netuid"): - cli.config.netuid = int(Prompt.ask("Enter netuid")) - - if not cli.config.is_set("uids"): - cli.config.uids = Prompt.ask("Enter UIDs (comma-separated)") - - if not cli.config.is_set("weights"): - cli.config.weights = Prompt.ask("Enter weights (comma-separated)") - - # Parse from string - netuid = cli.config.netuid - uids = np.array( - [int(x) for x in re.split(r"[ ,]+", cli.config.uids)], dtype=np.int64 - ) - weights = np.array( - [float(x) for x in re.split(r"[ ,]+", cli.config.weights)], dtype=np.float32 - ) - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - if not cli.config.is_set("salt"): - # Generate random salt - salt_length = 8 - salt = list(os.urandom(salt_length)) - - if not Confirm.ask( - f"Have you recorded the [red]salt[/red]: [bold white]'{salt}'[/bold white]? It will be " - f"required to reveal weights." - ): - return False, "User cancelled the operation." - else: - salt = np.array( - [int(x) for x in re.split(r"[ ,]+", cli.config.salt)], - dtype=np.int64, - ).tolist() - - # Run the commit weights operation - success, message = subtensor.commit_weights( - wallet=wallet, - netuid=netuid, - uids=weight_uids, - weights=weight_vals, - salt=salt, - wait_for_inclusion=cli.config.wait_for_inclusion, - wait_for_finalization=cli.config.wait_for_finalization, - prompt=cli.config.prompt, - ) - - # Result - if success: - bittensor.__console__.print("Weights committed successfully") - else: - bittensor.__console__.print(f"Failed to commit weights: {message}") - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "commit", help="""Commit weights for a specific subnet.""" - ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) - parser.add_argument("--uids", dest="uids", type=str, required=False) - parser.add_argument("--weights", dest="weights", type=str, required=False) - parser.add_argument("--salt", dest="salt", type=str, required=False) - parser.add_argument( - "--wait-for-inclusion", - dest="wait_for_inclusion", - action="store_true", - default=False, - ) - parser.add_argument( - "--wait-for-finalization", - dest="wait_for_finalization", - action="store_true", - default=True, - ) - parser.add_argument( - "--prompt", - dest="prompt", - action="store_true", - default=False, - ) - - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.no_prompt and not config.is_set("wallet.name"): - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.no_prompt and not config.is_set("wallet.hotkey"): - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) - - -class RevealWeightCommand: - """ - Executes the ``reveal`` command to reveal weights for a specific subnet on the Bittensor network. - Usage: - The command allows revealing weights for a specific subnet. Users need to specify the netuid (network unique identifier), corresponding UIDs, and weights they wish to reveal. - Optional arguments: - - ``--netuid`` (int): The netuid of the subnet for which weights are to be revealed. - - ``--uids`` (str): Corresponding UIDs for the specified netuid, in comma-separated format. - - ``--weights`` (str): Corresponding weights for the specified UIDs, in comma-separated format. - - ``--salt`` (str): Corresponding salt for the hash function, integers in comma-separated format. - Example usage:: - $ btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189 - Note: - This command is used to reveal weights for a specific subnet and requires the user to have the necessary permissions. - """ - - @staticmethod - def run(cli: "bittensor.cli"): - r"""Reveal weights for a specific subnet.""" - try: - subtensor: "bittensor.subtensor" = bittensor.subtensor( - config=cli.config, log_verbose=False - ) - RevealWeightCommand._run(cli, subtensor) - finally: - if "subtensor" in locals(): - subtensor.close() - bittensor.logging.debug("closing subtensor connection") - - @staticmethod - def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - r"""Reveal weights for a specific subnet.""" - wallet = bittensor.wallet(config=cli.config) - - # Get values if not set. - if not cli.config.is_set("netuid"): - cli.config.netuid = int(Prompt.ask("Enter netuid")) - - if not cli.config.is_set("uids"): - cli.config.uids = Prompt.ask("Enter UIDs (comma-separated)") - - if not cli.config.is_set("weights"): - cli.config.weights = Prompt.ask("Enter weights (comma-separated)") - - if not cli.config.is_set("salt"): - cli.config.salt = Prompt.ask("Enter salt (comma-separated)") - - # Parse from string - netuid = cli.config.netuid - version = bittensor.__version_as_int__ - uids = np.array( - [int(x) for x in re.split(r"[ ,]+", cli.config.uids)], - dtype=np.int64, - ) - weights = np.array( - [float(x) for x in re.split(r"[ ,]+", cli.config.weights)], - dtype=np.float32, - ) - salt = np.array( - [int(x) for x in re.split(r"[ ,]+", cli.config.salt)], - dtype=np.int64, - ) - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - # Run the reveal weights operation. - success, message = subtensor.reveal_weights( - wallet=wallet, - netuid=netuid, - uids=weight_uids, - weights=weight_vals, - salt=salt, - version_key=version, - wait_for_inclusion=cli.config.wait_for_inclusion, - wait_for_finalization=cli.config.wait_for_finalization, - prompt=cli.config.prompt, - ) - - if success: - bittensor.__console__.print("Weights revealed successfully") - else: - bittensor.__console__.print(f"Failed to reveal weights: {message}") - - @staticmethod - def add_args(parser: argparse.ArgumentParser): - parser = parser.add_parser( - "reveal", help="""Reveal weights for a specific subnet.""" - ) - parser.add_argument("--netuid", dest="netuid", type=int, required=False) - parser.add_argument("--uids", dest="uids", type=str, required=False) - parser.add_argument("--weights", dest="weights", type=str, required=False) - parser.add_argument("--salt", dest="salt", type=str, required=False) - parser.add_argument( - "--wait-for-inclusion", - dest="wait_for_inclusion", - action="store_true", - default=False, - ) - parser.add_argument( - "--wait-for-finalization", - dest="wait_for_finalization", - action="store_true", - default=True, - ) - parser.add_argument( - "--prompt", - dest="prompt", - action="store_true", - default=False, - ) - - bittensor.wallet.add_args(parser) - bittensor.subtensor.add_args(parser) - - @staticmethod - def check_config(config: "bittensor.config"): - if not config.is_set("wallet.name") and not config.no_prompt: - wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) - config.wallet.name = str(wallet_name) - if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) diff --git a/tests/e2e_tests/multistep/__init__.py b/bittensor/core/__init__.py similarity index 100% rename from tests/e2e_tests/multistep/__init__.py rename to bittensor/core/__init__.py diff --git a/bittensor/axon.py b/bittensor/core/axon.py similarity index 81% rename from bittensor/axon.py rename to bittensor/core/axon.py index 8cefadfe6..63b79d49c 100644 --- a/bittensor/axon.py +++ b/bittensor/core/axon.py @@ -1,23 +1,20 @@ -"""Create and initialize Axon, which services the forward and backward requests from other neurons.""" - # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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. +"""Create and initialize Axon, which services the forward and backward requests from other neurons.""" import argparse import asyncio @@ -25,7 +22,6 @@ import copy import inspect import json -import os import threading import time import traceback @@ -33,9 +29,10 @@ import uuid import warnings from inspect import signature, Signature, Parameter -from typing import List, Optional, Tuple, Callable, Any, Dict, Awaitable +from typing import Any, Awaitable, Callable, Optional, Tuple import uvicorn +from bittensor_wallet import Wallet from fastapi import APIRouter, Depends, FastAPI from fastapi.responses import JSONResponse from fastapi.routing import serialize_response @@ -44,10 +41,9 @@ from starlette.responses import Response from substrateinterface import Keypair -import bittensor -from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds -from bittensor.constants import V_7_2_0 -from bittensor.errors import ( +from bittensor.core.chain_data import AxonInfo +from bittensor.core.config import Config +from bittensor.core.errors import ( BlacklistedException, InvalidRequestNameError, NotVerifiedException, @@ -58,8 +54,20 @@ SynapseParsingError, UnknownSynapseError, ) -from bittensor.threadpool import PriorityThreadPoolExecutor +from bittensor.core.settings import DEFAULTS, version_as_int +from bittensor.core.stream import StreamingSynapse +from bittensor.core.synapse import Synapse, TerminalInfo +from bittensor.core.threadpool import PriorityThreadPoolExecutor from bittensor.utils import networking +from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds +from bittensor.utils.btlogging import logging + +# Just for annotation checker +if typing.TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + +# Latest version with old style nonce structure (this in not a current SDK version) +V_7_2_0 = 7002000 class FastAPIThreadedServer(uvicorn.Server): @@ -90,6 +98,16 @@ class FastAPIThreadedServer(uvicorn.Server): Stopping the Server During shutdown or maintenance of the Axon server, this class can be used to stop the FastAPI application gracefully, ensuring that all resources are properly released. + Example Usage:: + + self.app = FastAPI() + log_level = "trace" + self.fast_config = uvicorn.Config(self.app, host="0.0.0.0", port=self.config.axon.port, log_level=log_level) + self.fast_server = FastAPIThreadedServer(config=self.fast_config) + self.fast_server.start() + # do something + self.fast_server.stop() + Args: should_exit (bool): Flag to indicate whether the server should stop running. is_running (bool): Flag to indicate whether the server is currently running. @@ -104,7 +122,6 @@ def install_signal_handlers(self): """ Overrides the default signal handlers provided by ``uvicorn.Server``. This method is essential to ensure that the signal handling in the threaded server does not interfere with the main application's flow, especially in a complex asynchronous environment like the Axon server. """ - pass @contextlib.contextmanager def run_in_thread(self): @@ -134,8 +151,7 @@ def _wrapper_run(self): def start(self): """ - Starts the FastAPI server in a separate thread if it is not already running. This method sets up the server to handle HTTP requests concurrently, enabling the Axon server to efficiently manage - incoming network requests. + Starts the FastAPI server in a separate thread if it is not already running. This method sets up the server to handle HTTP requests concurrently, enabling the Axon server to efficiently manage incoming network requests. The method ensures that the server starts running in a non-blocking manner, allowing the Axon server to continue its other operations seamlessly. """ @@ -155,9 +171,9 @@ def stop(self): self.should_exit = True -class axon: +class Axon: """ - The ``axon`` class in Bittensor is a fundamental component that serves as the server-side interface for a neuron within the Bittensor network. + The ``Axon`` class in Bittensor is a fundamental component that serves as the server-side interface for a neuron within the Bittensor network. This class is responsible for managing incoming requests from other neurons and implements various mechanisms to ensure efficient @@ -188,35 +204,35 @@ class is designed to be flexible and customizable, allowing users to specify cus import bittensor # Define your custom synapse class - class MySyanpse( bittensor.Synapse ): + class MySynapse( bittensor.Synapse ): input: int = 1 output: int = None # Define a custom request forwarding function using your synapse class - def forward( synapse: MySyanpse ) -> MySyanpse: + def forward( synapse: MySynapse ) -> MySynapse: # Apply custom logic to synapse and return it synapse.output = 2 return synapse # Define a custom request verification function - def verify_my_synapse( synapse: MySyanpse ): + def verify_my_synapse( synapse: MySynapse ): # Apply custom verification logic to synapse # Optionally raise Exception assert synapse.input == 1 ... - # Define a custom request blacklist fucntion - def blacklist_my_synapse( synapse: MySyanpse ) -> bool: + # Define a custom request blacklist function + def blacklist_my_synapse( synapse: MySynapse ) -> bool: # Apply custom blacklist return False ( if non blacklisted ) or True ( if blacklisted ) - # Define a custom request priority fucntion - def prioritize_my_synape( synapse: MySyanpse ) -> float: + # Define a custom request priority function + def prioritize_my_synapse( synapse: MySynapse ) -> float: # Apply custom priority return 1.0 # Initialize Axon object with a custom configuration - my_axon = bittensor.axon( + my_axon = bittensor.Axon( config=my_config, wallet=my_wallet, port=9090, @@ -230,7 +246,7 @@ def prioritize_my_synape( synapse: MySyanpse ) -> float: forward_fn = forward_my_synapse, verify_fn = verify_my_synapse, blacklist_fn = blacklist_my_synapse, - priority_fn = prioritize_my_synape + priority_fn = prioritize_my_synapse ) # Serve and start your axon. @@ -244,28 +260,28 @@ def prioritize_my_synape( synapse: MySyanpse ) -> float: forward_fn = forward_my_synapse, verify_fn = verify_my_synapse, blacklist_fn = blacklist_my_synapse, - priority_fn = prioritize_my_synape + priority_fn = prioritize_my_synapse ).attach( forward_fn = forward_my_synapse_2, verify_fn = verify_my_synapse_2, blacklist_fn = blacklist_my_synapse_2, - priority_fn = prioritize_my_synape_2 + priority_fn = prioritize_my_synapse_2 ).serve( netuid = ... subtensor = ... ).start() Args: - wallet (bittensor.wallet, optional): Wallet with hotkey and coldkeypub. - config (bittensor.config, optional): Configuration parameters for the axon. - port (int, optional): Port for server binding. - ip (str, optional): Binding IP address. - external_ip (str, optional): External IP address to broadcast. - external_port (int, optional): External port to broadcast. - max_workers (int, optional): Number of active threads for request handling. + wallet (Optional[bittensor_wallet.Wallet]): Wallet with hotkey and coldkeypub. + config (Optional[bittensor.core.config.Config]): Configuration parameters for the axon. + port (Optional[int]): Port for server binding. + ip (Optional[str]): Binding IP address. + external_ip (Optional[str]): External IP address to broadcast. + external_port (Optional[int]): External port to broadcast. + max_workers (Optional[int]): Number of active threads for request handling. Returns: - bittensor.axon: An instance of the axon class configured as per the provided arguments. + bittensor.core.axon.Axon: An instance of the axon class configured as per the provided arguments. Note: This class is a core part of Bittensor's decentralized network for machine intelligence, @@ -288,90 +304,77 @@ def prioritize_my_synape( synapse: MySyanpse ) -> float: Error Handling and Validation The method ensures that the attached functions meet the required signatures, providing error handling to prevent runtime issues. - """ def __init__( self, - wallet: Optional["bittensor.wallet"] = None, - config: Optional["bittensor.config"] = None, + wallet: Optional["Wallet"] = None, + config: Optional["Config"] = None, port: Optional[int] = None, ip: Optional[str] = None, external_ip: Optional[str] = None, external_port: Optional[int] = None, max_workers: Optional[int] = None, ): - r"""Creates a new bittensor.Axon object from passed arguments. + """Creates a new bittensor.Axon object from passed arguments. + Args: - config (:obj:`Optional[bittensor.config]`, `optional`): - bittensor.axon.config() - wallet (:obj:`Optional[bittensor.wallet]`, `optional`): - bittensor wallet with hotkey and coldkeypub. - port (:type:`Optional[int]`, `optional`): - Binding port. - ip (:type:`Optional[str]`, `optional`): - Binding ip. - external_ip (:type:`Optional[str]`, `optional`): - The external ip of the server to broadcast to the network. - external_port (:type:`Optional[int]`, `optional`): - The external port of the server to broadcast to the network. - max_workers (:type:`Optional[int]`, `optional`): - Used to create the threadpool if not passed, specifies the number of active threads servicing requests. + config (:obj:`Optional[bittensor.core.config.Config]`): bittensor.Axon.config() + wallet (:obj:`Optional[bittensor_wallet.Wallet]`): bittensor wallet with hotkey and coldkeypub. + port (:type:`Optional[int]`): Binding port. + ip (:type:`Optional[str]`): Binding ip. + external_ip (:type:`Optional[str]`): The external ip of the server to broadcast to the network. + external_port (:type:`Optional[int]`): The external port of the server to broadcast to the network. + max_workers (:type:`Optional[int]`): Used to create the threadpool if not passed, specifies the number of active threads servicing requests. """ # Build and check config. if config is None: - config = axon.config() + config = Axon.config() config = copy.deepcopy(config) - config.axon.ip = ip or config.axon.get("ip", bittensor.defaults.axon.ip) - config.axon.port = port or config.axon.get("port", bittensor.defaults.axon.port) - config.axon.external_ip = external_ip or config.axon.get( - "external_ip", bittensor.defaults.axon.external_ip - ) - config.axon.external_port = external_port or config.axon.get( - "external_port", bittensor.defaults.axon.external_port - ) - config.axon.max_workers = max_workers or config.axon.get( - "max_workers", bittensor.defaults.axon.max_workers - ) - axon.check_config(config) - self.config = config # type: ignore [method-assign] + config.axon.ip = ip or DEFAULTS.axon.ip + config.axon.port = port or DEFAULTS.axon.port + config.axon.external_ip = external_ip or DEFAULTS.axon.external_ip + config.axon.external_port = external_port or DEFAULTS.axon.external_port + config.axon.max_workers = max_workers or DEFAULTS.axon.max_workers + Axon.check_config(config) + self.config = config # type: ignore # Get wallet or use default. - self.wallet = wallet or bittensor.wallet() + self.wallet = wallet or Wallet() # Build axon objects. self.uuid = str(uuid.uuid1()) - self.ip = self.config.axon.ip - self.port = self.config.axon.port + self.ip = self.config.axon.ip # type: ignore + self.port = self.config.axon.port # type: ignore self.external_ip = ( - self.config.axon.external_ip - if self.config.axon.external_ip is not None - else bittensor.utils.networking.get_external_ip() + self.config.axon.external_ip # type: ignore + if self.config.axon.external_ip is not None # type: ignore + else networking.get_external_ip() ) self.external_port = ( - self.config.axon.external_port - if self.config.axon.external_port is not None - else self.config.axon.port + self.config.axon.external_port # type: ignore + if self.config.axon.external_port is not None # type: ignore + else self.config.axon.port # type: ignore ) - self.full_address = str(self.config.axon.ip) + ":" + str(self.config.axon.port) + self.full_address = str(self.config.axon.ip) + ":" + str(self.config.axon.port) # type: ignore self.started = False # Build middleware - self.thread_pool = bittensor.PriorityThreadPoolExecutor( - max_workers=self.config.axon.max_workers + self.thread_pool = PriorityThreadPoolExecutor( + max_workers=self.config.axon.max_workers # type: ignore ) - self.nonces: Dict[str, int] = {} + self.nonces: dict[str, int] = {} # Request default functions. - self.forward_class_types: Dict[str, List[Signature]] = {} - self.blacklist_fns: Dict[str, Optional[Callable]] = {} - self.priority_fns: Dict[str, Optional[Callable]] = {} - self.forward_fns: Dict[str, Optional[Callable]] = {} - self.verify_fns: Dict[str, Optional[Callable]] = {} + self.forward_class_types: dict[str, list[Signature]] = {} + self.blacklist_fns: dict[str, Optional[Callable]] = {} + self.priority_fns: dict[str, Optional[Callable]] = {} + self.forward_fns: dict[str, Optional[Callable]] = {} + self.verify_fns: dict[str, Optional[Callable]] = {} # Instantiate FastAPI self.app = FastAPI() - log_level = "trace" if bittensor.logging.__trace_on__ else "critical" + log_level = "trace" if logging.__trace_on__ else "critical" self.fast_config = uvicorn.Config( self.app, host="0.0.0.0", port=self.config.axon.port, log_level=log_level ) @@ -384,17 +387,17 @@ def __init__( self.app.add_middleware(self.middleware_cls, axon=self) # Attach default forward. - def ping(r: bittensor.Synapse) -> bittensor.Synapse: + def ping(r: Synapse) -> Synapse: return r self.attach( forward_fn=ping, verify_fn=None, blacklist_fn=None, priority_fn=None ) - def info(self) -> "bittensor.AxonInfo": + def info(self) -> "AxonInfo": """Returns the axon info object associated with this axon.""" - return bittensor.AxonInfo( - version=bittensor.__version_as_int__, + return AxonInfo( + version=version_as_int, ip=self.external_ip, ip_type=networking.ip_version(self.external_ip), port=self.external_port, @@ -411,17 +414,17 @@ def attach( blacklist_fn: Optional[Callable] = None, priority_fn: Optional[Callable] = None, verify_fn: Optional[Callable] = None, - ) -> "bittensor.axon": + ) -> "Axon": """ Attaches custom functions to the Axon server for handling incoming requests. This method enables - the Axon to define specific behaviors for request forwarding, verification, blacklisting, and + the ``Axon`` to define specific behaviors for request forwarding, verification, blacklisting, and prioritization, thereby customizing its interaction within the Bittensor network. Registers an API endpoint to the FastAPI application router. It uses the name of the first argument of the :func:`forward_fn` function as the endpoint name. - The attach method in the Bittensor framework's axon class is a crucial function for registering + The :func:`attach` method in the Bittensor framework's axon class is a crucial function for registering API endpoints to the Axon's FastAPI application router. This method allows the Axon server to define how it handles incoming requests by attaching functions for forwarding, verifying, blacklisting, and prioritizing requests. It's a key part of customizing the server's behavior @@ -429,9 +432,9 @@ def attach( Args: forward_fn (Callable): Function to be called when the API endpoint is accessed. It should have at least one argument. - blacklist_fn (Callable, optional): Function to filter out undesired requests. It should take the same arguments as :func:`forward_fn` and return a boolean value. Defaults to ``None``, meaning no blacklist filter will be used. - priority_fn (Callable, optional): Function to rank requests based on their priority. It should take the same arguments as :func:`forward_fn` and return a numerical value representing the request's priority. Defaults to ``None``, meaning no priority sorting will be applied. - verify_fn (Callable, optional): Function to verify requests. It should take the same arguments as :func:`forward_fn` and return a boolean value. If ``None``, :func:`self.default_verify` function will be used. + blacklist_fn (Optional[Callable]): Function to filter out undesired requests. It should take the same arguments as :func:`forward_fn` and return a boolean value. Defaults to ``None``, meaning no blacklist filter will be used. + priority_fn (Optional[Callable]): Function to rank requests based on their priority. It should take the same arguments as :func:`forward_fn` and return a numerical value representing the request's priority. Defaults to ``None``, meaning no priority sorting will be applied. + verify_fn (Optional[Callable]): Function to verify requests. It should take the same arguments as :func:`forward_fn` and return a boolean value. If ``None``, :func:`self.default_verify` function will be used. Note: The methods :func:`forward_fn`, :func:`blacklist_fn`, :func:`priority_fn`, and :func:`verify_fn` should be designed to receive the same parameters. @@ -451,7 +454,7 @@ def forward_custom(synapse: MyCustomSynapse) -> MyCustomSynapse: # Custom logic for processing the request return synapse - def blacklist_custom(synapse: MyCustomSynapse) -> Tuple[bool, str]: + def blacklist_custom(synapse: MyCustomSynapse) -> tuple[bool, str]: return True, "Allowed!" def priority_custom(synapse: MyCustomSynapse) -> float: @@ -461,7 +464,7 @@ def verify_custom(synapse: MyCustomSynapse): # Custom logic for verifying the request pass - my_axon = bittensor.axon(...) + my_axon = bittensor.Axon(...) my_axon.attach(forward_fn=forward_custom, verify_fn=verify_custom) Note: @@ -480,7 +483,7 @@ def verify_custom(synapse: MyCustomSynapse): param_class = first_param.annotation assert issubclass( - param_class, bittensor.Synapse + param_class, Synapse ), "The first argument of forward_fn must inherit from bittensor.Synapse" request_name = param_class.__name__ @@ -489,7 +492,7 @@ async def endpoint(*args, **kwargs): response = forward_fn(*args, **kwargs) if isinstance(response, Awaitable): response = await response - if isinstance(response, bittensor.Synapse): + if isinstance(response, Synapse): return await self.middleware_cls.synapse_to_response( synapse=response, start_time=start_time ) @@ -514,11 +517,11 @@ async def endpoint(*args, **kwargs): return_annotation = forward_sig.return_annotation if isinstance(return_annotation, type) and issubclass( - return_annotation, bittensor.Synapse + return_annotation, Synapse ): if issubclass( return_annotation, - bittensor.StreamingSynapse, + StreamingSynapse, ): warnings.warn( "The forward_fn return annotation is a subclass of bittensor.StreamingSynapse. " @@ -534,8 +537,8 @@ async def endpoint(*args, **kwargs): # Add the endpoint to the router, making it available on both GET and POST methods self.router.add_api_route( - f"/{request_name}", - endpoint, + path=f"/{request_name}", + endpoint=endpoint, methods=["GET", "POST"], dependencies=[Depends(self.verify_body_integrity)], ) @@ -544,8 +547,8 @@ async def endpoint(*args, **kwargs): # Check the signature of blacklist_fn, priority_fn and verify_fn if they are provided expected_params = [ Parameter( - "synapse", - Parameter.POSITIONAL_OR_KEYWORD, + name="synapse", + kind=Parameter.POSITIONAL_OR_KEYWORD, annotation=forward_sig.parameters[ list(forward_sig.parameters)[0] ].annotation, @@ -557,23 +560,17 @@ async def endpoint(*args, **kwargs): ) assert ( signature(blacklist_fn) == blacklist_sig - ), "The blacklist_fn function must have the signature: blacklist( synapse: {} ) -> Tuple[bool, str]".format( - request_name - ) + ), f"The blacklist_fn function must have the signature: blacklist( synapse: {request_name} ) -> tuple[bool, str]" if priority_fn: priority_sig = Signature(expected_params, return_annotation=float) assert ( signature(priority_fn) == priority_sig - ), "The priority_fn function must have the signature: priority( synapse: {} ) -> float".format( - request_name - ) + ), f"The priority_fn function must have the signature: priority( synapse: {request_name} ) -> float" if verify_fn: verify_sig = Signature(expected_params, return_annotation=None) assert ( signature(verify_fn) == verify_sig - ), "The verify_fn function must have the signature: verify( synapse: {} ) -> None".format( - request_name - ) + ), f"The verify_fn function must have the signature: verify( synapse: {request_name} ) -> None" # Store functions in appropriate attribute dictionaries self.forward_class_types[request_name] = param_class @@ -587,24 +584,22 @@ async def endpoint(*args, **kwargs): return self @classmethod - def config(cls) -> "bittensor.config": + def config(cls) -> "Config": """ Parses the command-line arguments to form a Bittensor configuration object. Returns: - bittensor.config: Configuration object with settings from command-line arguments. + bittensor.core.config.Config: Configuration object with settings from command-line arguments. """ parser = argparse.ArgumentParser() - axon.add_args(parser) # Add specific axon-related arguments - return bittensor.config(parser, args=[]) + Axon.add_args(parser) # Add specific axon-related arguments + return Config(parser, args=[]) @classmethod def help(cls): - """ - Prints the help text (list of command-line arguments and their descriptions) to stdout. - """ + """Prints the help text (list of command-line arguments and their descriptions) to stdout.""" parser = argparse.ArgumentParser() - axon.add_args(parser) # Add specific axon-related arguments + Axon.add_args(parser) # Add specific axon-related arguments print(cls.__new__.__doc__) # Print docstring of the class parser.print_help() # Print parser's help text @@ -615,60 +610,53 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: Optional[str] = None) Args: parser (argparse.ArgumentParser): Argument parser to which the arguments will be added. - prefix (str, optional): Prefix to add to the argument names. Defaults to None. + prefix (Optional[str]): Prefix to add to the argument names. Defaults to None. Note: Environment variables are used to define default values for the arguments. """ prefix_str = "" if prefix is None else prefix + "." try: - # Get default values from environment variables or use default values - default_axon_port = os.getenv("BT_AXON_PORT") or 8091 - default_axon_ip = os.getenv("BT_AXON_IP") or "[::]" - default_axon_external_port = os.getenv("BT_AXON_EXTERNAL_PORT") or None - default_axon_external_ip = os.getenv("BT_AXON_EXTERNAL_IP") or None - default_axon_max_workers = os.getenv("BT_AXON_MAX_WORERS") or 10 - # Add command-line arguments to the parser parser.add_argument( "--" + prefix_str + "axon.port", type=int, help="The local port this axon endpoint is bound to. i.e. 8091", - default=default_axon_port, + default=DEFAULTS.axon.port, ) parser.add_argument( "--" + prefix_str + "axon.ip", type=str, help="""The local ip this axon binds to. ie. [::]""", - default=default_axon_ip, + default=DEFAULTS.axon.ip, ) parser.add_argument( "--" + prefix_str + "axon.external_port", type=int, required=False, help="""The public port this axon broadcasts to the network. i.e. 8091""", - default=default_axon_external_port, + default=DEFAULTS.axon.external_port, ) parser.add_argument( "--" + prefix_str + "axon.external_ip", type=str, required=False, help="""The external ip this axon broadcasts to the network to. ie. [::]""", - default=default_axon_external_ip, + default=DEFAULTS.axon.external_ip, ) parser.add_argument( "--" + prefix_str + "axon.max_workers", type=int, help="""The maximum number connection handler threads working simultaneously on this endpoint. The grpc server distributes new worker threads to service requests up to this number.""", - default=default_axon_max_workers, + default=DEFAULTS.axon.max_workers, ) except argparse.ArgumentError: # Exception handling for re-parsing arguments pass - async def verify_body_integrity(self, request: Request): + async def verify_body_integrity(self, request: "Request"): """ The ``verify_body_integrity`` method in the Bittensor framework is a key security function within the Axon server's middleware. It is responsible for ensuring the integrity of the body of incoming HTTP @@ -683,13 +671,10 @@ async def verify_body_integrity(self, request: Request): request (Request): The incoming FastAPI request object containing both headers and the request body. Returns: - dict: Returns the parsed body of the request as a dictionary if all the hash comparisons match, - indicating that the body is intact and has not been tampered with. + dict: Returns the parsed body of the request as a dictionary if all the hash comparisons match, indicating that the body is intact and has not been tampered with. Raises: - JSONResponse: Raises a JSONResponse with a 400 status code if any of the hash comparisons fail, - indicating a potential integrity issue with the incoming request payload. - The response includes the detailed error message specifying which field has a hash mismatch. + JSONResponse: Raises a JSONResponse with a 400 status code if any of the hash comparisons fail, indicating a potential integrity issue with the incoming request payload. The response includes the detailed error message specifying which field has a hash mismatch. This method performs several key functions: @@ -700,11 +685,9 @@ async def verify_body_integrity(self, request: Request): 5. Comparing the recomputed hash with the hash provided in the request headers for verification. Note: - The integrity verification is an essential step in ensuring the security of the data exchange - within the Bittensor network. It helps prevent tampering and manipulation of data during transit, - thereby maintaining the reliability and trust in the network communication. + The integrity verification is an essential step in ensuring the security of the data exchange within the Bittensor network. It helps prevent tampering and manipulation of data during transit, thereby maintaining the reliability and trust in the network communication. """ - # Await and load the request body so we can inspect it + # Await and load the request body, so we can inspect it body = await request.body() request_body = body.decode() if isinstance(body, bytes) else body @@ -727,41 +710,33 @@ async def verify_body_integrity(self, request: Request): return body_dict @classmethod - def check_config(cls, config: "bittensor.config"): + def check_config(cls, config: "Config"): """ This method checks the configuration for the axon's port and wallet. Args: - config (bittensor.config): The config object holding axon settings. + config (bittensor.core.config.Config): The config object holding axon settings. Raises: AssertionError: If the axon or external ports are not in range [1024, 65535] """ assert ( - config.axon.port > 1024 and config.axon.port < 65535 + 1024 < config.axon.port < 65535 ), "Axon port must be in range [1024, 65535]" assert config.axon.external_port is None or ( - config.axon.external_port > 1024 and config.axon.external_port < 65535 + 1024 < config.axon.external_port < 65535 ), "External port must be in range [1024, 65535]" def to_string(self): - """ - Provides a human-readable representation of the AxonInfo for this Axon. - """ + """Provides a human-readable representation of the AxonInfo for this Axon.""" return self.info().to_string() def __str__(self) -> str: - """ - Provides a human-readable representation of the Axon instance. - """ - return "Axon({}, {}, {}, {}, {})".format( - self.ip, - self.port, - self.wallet.hotkey.ss58_address, - "started" if self.started else "stopped", - list(self.forward_fns.keys()), - ) + """Provides a human-readable representation of the Axon instance.""" + _started = "started" if self.started else "stopped" + _keys = list(self.forward_fns.keys()) + return f"Axon({self.ip}, {self.port}, {self.wallet.hotkey.ss58_address}, {_started}, {_keys})" def __repr__(self) -> str: """ @@ -777,7 +752,7 @@ def __del__(self): """ self.stop() - def start(self) -> "bittensor.axon": + def start(self) -> "Axon": """ Starts the Axon server and its underlying FastAPI server thread, transitioning the state of the Axon instance to ``started``. This method initiates the server's ability to accept and process @@ -788,11 +763,11 @@ def start(self) -> "bittensor.axon": within the Bittensor network. Returns: - bittensor.axon: The Axon instance in the 'started' state. + bittensor.core.axon.Axon: The Axon instance in the 'started' state. Example:: - my_axon = bittensor.axon(...) + my_axon = bittensor.Axon(...) ... # setup axon, attach functions, etc. my_axon.start() # Starts the axon server @@ -803,7 +778,7 @@ def start(self) -> "bittensor.axon": self.started = True return self - def stop(self) -> "bittensor.axon": + def stop(self) -> "Axon": """ Stops the Axon server and its underlying GRPC server thread, transitioning the state of the Axon instance to ``stopped``. This method ceases the server's ability to accept new network requests, @@ -814,11 +789,11 @@ def stop(self) -> "bittensor.axon": shut down or needs to temporarily go offline. Returns: - bittensor.axon: The Axon instance in the 'stopped' state. + bittensor.core.axon.Axon: The Axon instance in the 'stopped' state. Example:: - my_axon = bittensor.axon(...) + my_axon = bittensor.Axon(...) my_axon.start() ... my_axon.stop() # Stops the axon server @@ -831,9 +806,7 @@ def stop(self) -> "bittensor.axon": self.started = False return self - def serve( - self, netuid: int, subtensor: Optional[bittensor.subtensor] = None - ) -> "bittensor.axon": + def serve(self, netuid: int, subtensor: Optional["Subtensor"] = None) -> "Axon": """ Serves the Axon on the specified subtensor connection using the configured wallet. This method registers the Axon with a specific subnet within the Bittensor network, identified by the ``netuid``. @@ -842,14 +815,14 @@ def serve( Args: netuid (int): The unique identifier of the subnet to register on. This ID is essential for the Axon to correctly position itself within the Bittensor network topology. - subtensor (bittensor.subtensor, optional): The subtensor connection to use for serving. If not provided, a new connection is established based on default configurations. + subtensor (Optional[bittensor.core.subtensor.Subtensor]): The subtensor connection to use for serving. If not provided, a new connection is established based on default configurations. Returns: - bittensor.axon: The Axon instance that is now actively serving on the specified subtensor. + bittensor.core.axon.Axon: The Axon instance that is now actively serving on the specified subtensor. Example:: - my_axon = bittensor.axon(...) + my_axon = bittensor.Axon(...) subtensor = bt.subtensor(network="local") # Local by default my_axon.serve(netuid=1, subtensor=subtensor) # Serves the axon on subnet with netuid 1 @@ -861,7 +834,7 @@ def serve( subtensor.serve_axon(netuid=netuid, axon=self) return self - async def default_verify(self, synapse: bittensor.Synapse): + async def default_verify(self, synapse: "Synapse"): """ This method is used to verify the authenticity of a received message using a digital signature. @@ -901,8 +874,7 @@ async def default_verify(self, synapse: bittensor.Synapse): cryptographic keys can participate in secure communication. Args: - synapse: bittensor.Synapse - bittensor request synapse. + synapse(bittensor.core.synapse.Synapse): bittensor request synapse. Raises: Exception: If the ``receiver_hotkey`` doesn't match with ``self.receiver_hotkey``. @@ -979,7 +951,15 @@ async def default_verify(self, synapse: bittensor.Synapse): raise SynapseDendriteNoneException(synapse=synapse) -def create_error_response(synapse: bittensor.Synapse): +def create_error_response(synapse: "Synapse") -> "JSONResponse": + """Creates an error response based on the provided synapse object. + + Args: + synapse (bittensor.core.synapse.Synapse): The synapse object containing details about the request and the associated axon. + + Returns: + JSONResponse: A JSON response with a status code and content indicating the error message. + """ if synapse.axon is None: return JSONResponse( status_code=400, @@ -995,27 +975,39 @@ def create_error_response(synapse: bittensor.Synapse): def log_and_handle_error( - synapse: bittensor.Synapse, + synapse: "Synapse", exception: Exception, - status_code: typing.Optional[int] = None, - start_time: typing.Optional[float] = None, -) -> bittensor.Synapse: + status_code: Optional[int] = None, + start_time: Optional[float] = None, +) -> "Synapse": + """ + Logs the error and updates the synapse object with the appropriate error details. + + Args: + synapse (bittensor.core.synapse.Synapse): The synapse object to be updated with error information. + exception (Exception): The exception that was raised and needs to be logged and handled. + status_code (Optional[int]): The HTTP status code to be set on the synapse object. Defaults to None. + start_time (Optional[float]): The timestamp marking the start of the processing, used to calculate process time. Defaults to None. + + Returns: + Synapse: The updated synapse object with error details. + """ if isinstance(exception, SynapseException): synapse = exception.synapse or synapse - bittensor.logging.trace(f"Forward handled exception: {exception}") + logging.trace(f"Forward handled exception: {exception}") else: - bittensor.logging.trace(f"Forward exception: {traceback.format_exc()}") + logging.trace(f"Forward exception: {traceback.format_exc()}") if synapse.axon is None: - synapse.axon = bittensor.TerminalInfo() + synapse.axon = TerminalInfo() # Set the status code of the synapse to the given status code. error_id = str(uuid.uuid4()) error_type = exception.__class__.__name__ # Log the detailed error message for internal use - bittensor.logging.error(f"{error_type}#{error_id}: {exception}") + logging.error(f"{error_type}#{error_id}: {exception}") if not status_code and synapse.axon.status_code != 100: status_code = synapse.axon.status_code @@ -1064,26 +1056,26 @@ class AxonMiddleware(BaseHTTPMiddleware): Args: app (FastAPI): An instance of the FastAPI application to which this middleware is attached. - axon (bittensor.axon): The Axon instance that will process the requests. + axon (bittensor.core.axon.Axon): The Axon instance that will process the requests. The middleware operates by intercepting incoming requests, performing necessary preprocessing (like verification and priority assessment), executing the request through the Axon's endpoints, and then handling any postprocessing steps such as response header updating and logging. """ - def __init__(self, app: "AxonMiddleware", axon: "bittensor.axon"): + def __init__(self, app: "AxonMiddleware", axon: "Axon"): """ Initialize the AxonMiddleware class. Args: - app (object): An instance of the application where the middleware processor is used. - axon (object): The axon instance used to process the requests. + app (bittensor.core.axon.AxonMiddleware): An instance of the application where the middleware processor is used. + axon (bittensor.core.axon.Axon): The axon instance used to process the requests. """ super().__init__(app) self.axon = axon async def dispatch( - self, request: Request, call_next: RequestResponseEndpoint + self, request: "Request", call_next: "RequestResponseEndpoint" ) -> Response: """ Asynchronously processes incoming HTTP requests and returns the corresponding responses. This @@ -1116,21 +1108,21 @@ async def dispatch( try: # Set up the synapse from its headers. try: - synapse: bittensor.Synapse = await self.preprocess(request) + synapse: "Synapse" = await self.preprocess(request) except Exception as exc: if isinstance(exc, SynapseException) and exc.synapse is not None: synapse = exc.synapse else: - synapse = bittensor.Synapse() + synapse = Synapse() raise # Logs the start of the request processing if synapse.dendrite is not None: - bittensor.logging.trace( + logging.trace( f"axon | <-- | {request.headers.get('content-length', -1)} B | {synapse.name} | {synapse.dendrite.hotkey} | {synapse.dendrite.ip}:{synapse.dendrite.port} | 200 | Success " ) else: - bittensor.logging.trace( + logging.trace( f"axon | <-- | {request.headers.get('content-length', -1)} B | {synapse.name} | None | None | 200 | Success " ) @@ -1149,11 +1141,12 @@ async def dispatch( # Handle errors related to preprocess. except InvalidRequestNameError as e: if synapse.axon is None: - synapse.axon = bittensor.TerminalInfo() + synapse.axon = TerminalInfo() synapse.axon.status_code = 400 synapse.axon.status_message = str(e) synapse = log_and_handle_error(synapse, e, start_time=start_time) response = create_error_response(synapse) + except SynapseException as e: synapse = e.synapse or synapse synapse = log_and_handle_error(synapse, e, start_time=start_time) @@ -1169,22 +1162,22 @@ async def dispatch( # Log the details of the processed synapse, including total size, name, hotkey, IP, port, # status code, and status message, using the debug level of the logger. if synapse.dendrite is not None and synapse.axon is not None: - bittensor.logging.trace( + logging.trace( f"axon | --> | {response.headers.get('content-length', -1)} B | {synapse.name} | {synapse.dendrite.hotkey} | {synapse.dendrite.ip}:{synapse.dendrite.port} | {synapse.axon.status_code} | {synapse.axon.status_message}" ) elif synapse.axon is not None: - bittensor.logging.trace( + logging.trace( f"axon | --> | {response.headers.get('content-length', -1)} B | {synapse.name} | None | None | {synapse.axon.status_code} | {synapse.axon.status_message}" ) else: - bittensor.logging.trace( + logging.trace( f"axon | --> | {response.headers.get('content-length', -1)} B | {synapse.name} | None | None | 200 | Success " ) # Return the response to the requester. return response - async def preprocess(self, request: Request) -> bittensor.Synapse: + async def preprocess(self, request: "Request") -> "Synapse": """ Performs the initial processing of the incoming request. This method is responsible for extracting relevant information from the request and setting up the Synapse object, which @@ -1194,7 +1187,7 @@ async def preprocess(self, request: Request) -> bittensor.Synapse: request (Request): The incoming request to be preprocessed. Returns: - bittensor.Synapse: The Synapse object representing the preprocessed state of the request. + bittensor.core.synapse.Synapse: The Synapse object representing the preprocessed state of the request. The preprocessing involves: @@ -1233,7 +1226,7 @@ async def preprocess(self, request: Request) -> bittensor.Synapse: # Fills the local axon information into the synapse. synapse.axon.__dict__.update( { - "version": str(bittensor.__version_as_int__), + "version": str(version_as_int), "uuid": str(self.axon.uuid), "nonce": time.time_ns(), "status_code": 100, @@ -1252,13 +1245,13 @@ async def preprocess(self, request: Request) -> bittensor.Synapse: # Return the setup synapse. return synapse - async def verify(self, synapse: bittensor.Synapse): + async def verify(self, synapse: "Synapse"): """ Verifies the authenticity and integrity of the request. This method ensures that the incoming request meets the predefined security and validation criteria. Args: - synapse (bittensor.Synapse): The Synapse object representing the request. + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. Raises: Exception: If the verification process fails due to unmet criteria or security concerns. @@ -1293,7 +1286,7 @@ async def verify(self, synapse: bittensor.Synapse): except Exception as e: # If there was an exception during the verification process, we log that # there was a verification exception. - bittensor.logging.trace(f"Verify exception {str(e)}") + logging.trace(f"Verify exception {str(e)}") # Check if the synapse.axon object exists if synapse.axon is not None: @@ -1309,7 +1302,7 @@ async def verify(self, synapse: bittensor.Synapse): f"Not Verified with error: {str(e)}", synapse=synapse ) - async def blacklist(self, synapse: bittensor.Synapse): + async def blacklist(self, synapse: "Synapse"): """ Checks if the request should be blacklisted. This method ensures that requests from disallowed sources or with malicious intent are blocked from processing. This can be extremely useful for @@ -1317,7 +1310,7 @@ async def blacklist(self, synapse: bittensor.Synapse): are prohibited from accessing certain resources. Args: - synapse (bittensor.Synapse): The Synapse object representing the request. + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. Raises: Exception: If the request is found in the blacklist. @@ -1350,7 +1343,7 @@ async def blacklist(self, synapse: bittensor.Synapse): ) if blacklisted: # We log that the key or identifier is blacklisted. - bittensor.logging.trace(f"Blacklisted: {blacklisted}, {reason}") + logging.trace(f"Blacklisted: {blacklisted}, {reason}") # Check if the synapse.axon object exists if synapse.axon is not None: @@ -1365,13 +1358,13 @@ async def blacklist(self, synapse: bittensor.Synapse): f"Forbidden. Key is blacklisted: {reason}.", synapse=synapse ) - async def priority(self, synapse: bittensor.Synapse): + async def priority(self, synapse: "Synapse"): """ Executes the priority function for the request. This method assesses and assigns a priority level to the request, determining its urgency and importance in the processing queue. Args: - synapse (bittensor.Synapse): The Synapse object representing the request. + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. Raises: Exception: If the priority assessment process encounters issues, such as timeouts. @@ -1384,15 +1377,15 @@ async def priority(self, synapse: bittensor.Synapse): priority_fn = self.axon.priority_fns.get(str(synapse.name), None) async def submit_task( - executor: PriorityThreadPoolExecutor, priority: float - ) -> Tuple[float, Any]: + executor: "PriorityThreadPoolExecutor", priority: float + ) -> tuple[float, Any]: """ Submits the given priority function to the specified executor for asynchronous execution. The function will run in the provided executor and return the priority value along with the result. Args: - executor: The executor in which the priority function will be run. - priority: The priority function to be executed. + executor (bittensor.core.threadpool.PriorityThreadPoolExecutor): The executor in which the priority function will be run. + priority (float): The priority function to be executed. Returns: tuple: A tuple containing the priority value and the result of the priority function execution. @@ -1419,7 +1412,7 @@ async def submit_task( except TimeoutError as e: # If the execution of the priority function exceeds the timeout, # it raises an exception to handle the timeout error. - bittensor.logging.trace(f"TimeoutError: {str(e)}") + logging.trace(f"TimeoutError: {str(e)}") # Set the status code of the synapse to 408 which indicates a timeout error. if synapse.axon is not None: @@ -1432,16 +1425,16 @@ async def submit_task( async def run( self, - synapse: bittensor.Synapse, - call_next: RequestResponseEndpoint, - request: Request, - ) -> Response: + synapse: "Synapse", + call_next: "RequestResponseEndpoint", + request: "Request", + ) -> "Response": """ Executes the requested function as part of the request processing pipeline. This method calls the next function in the middleware chain to process the request and generate a response. Args: - synapse (bittensor.Synapse): The Synapse object representing the request. + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. call_next (RequestResponseEndpoint): The next function in the middleware chain to process requests. request (Request): The original HTTP request. @@ -1451,6 +1444,8 @@ async def run( This method is a critical part of the request lifecycle, where the actual processing of the request takes place, leading to the generation of a response. """ + assert isinstance(synapse, Synapse) + try: # The requested function is executed by calling the 'call_next' function, # passing the original request as an argument. This function processes the request @@ -1459,7 +1454,7 @@ async def run( except Exception as e: # Log the exception for debugging purposes. - bittensor.logging.trace(f"Run exception: {str(e)}") + logging.trace(f"Run exception: {str(e)}") raise # Return the starlet response @@ -1468,20 +1463,18 @@ async def run( @classmethod async def synapse_to_response( cls, - synapse: bittensor.Synapse, + synapse: "Synapse", start_time: float, *, - response_override: Optional[Response] = None, - ) -> Response: + response_override: Optional["Response"] = None, + ) -> "Response": """ Converts the Synapse object into a JSON response with HTTP headers. Args: - synapse: The Synapse object representing the request. - start_time: The timestamp when the request processing started. - response_override: - Instead of serializing the synapse, mutate the provided response object. - This is only really useful for StreamingSynapse responses. + synapse (bittensor.core.synapse.Synapse): The Synapse object representing the request. + start_time (float): The timestamp when the request processing started. + response_override: Instead of serializing the synapse, mutate the provided response object. This is only really useful for StreamingSynapse responses. Returns: Response: The final HTTP response, with updated headers, ready to be sent back to the client. @@ -1490,7 +1483,7 @@ async def synapse_to_response( properly formatted and contains all necessary information. """ if synapse.axon is None: - synapse.axon = bittensor.TerminalInfo() + synapse.axon = TerminalInfo() if synapse.axon.status_code is None: synapse.axon.status_code = 200 diff --git a/bittensor/core/chain_data/__init__.py b/bittensor/core/chain_data/__init__.py new file mode 100644 index 000000000..9ad1e3888 --- /dev/null +++ b/bittensor/core/chain_data/__init__.py @@ -0,0 +1,22 @@ +""" +This module provides data structures and functions for working with the Bittensor network, including neuron and subnet +information, SCALE encoding/decoding, and custom RPC type registry. +""" + +from scalecodec.types import GenericCall + +from .axon_info import AxonInfo +from .delegate_info import DelegateInfo +from .delegate_info_lite import DelegateInfoLite +from .ip_info import IPInfo +from .neuron_info import NeuronInfo +from .neuron_info_lite import NeuronInfoLite +from .prometheus_info import PrometheusInfo +from .proposal_vote_data import ProposalVoteData +from .scheduled_coldkey_swap_info import ScheduledColdkeySwapInfo +from .stake_info import StakeInfo +from .subnet_hyperparameters import SubnetHyperparameters +from .subnet_info import SubnetInfo +from .utils import custom_rpc_type_registry + +ProposalCallData = GenericCall diff --git a/bittensor/core/chain_data/axon_info.py b/bittensor/core/chain_data/axon_info.py new file mode 100644 index 000000000..eee9cb82a --- /dev/null +++ b/bittensor/core/chain_data/axon_info.py @@ -0,0 +1,163 @@ +# 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. + +""" +This module defines the `AxonInfo` class, a data structure used to represent information about an axon endpoint +in the bittensor network. +""" + +import json +from dataclasses import asdict, dataclass +from typing import Any, Union + +from bittensor.utils import networking +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import torch, use_torch + + +@dataclass +class AxonInfo: + """ + The `AxonInfo` class represents information about an axon endpoint in the bittensor network. This includes + properties such as IP address, ports, and relevant keys. + + Attributes: + version (int): The version of the axon endpoint. + ip (str): The IP address of the axon endpoint. + port (int): The port number the axon endpoint uses. + ip_type (int): The type of IP protocol (e.g., IPv4 or IPv6). + hotkey (str): The hotkey associated with the axon endpoint. + coldkey (str): The coldkey associated with the axon endpoint. + protocol (int): The protocol version (default is 4). + placeholder1 (int): Reserved field (default is 0). + placeholder2 (int): Reserved field (default is 0). + """ + + version: int + ip: str + port: int + ip_type: int + hotkey: str + coldkey: str + protocol: int = 4 + placeholder1: int = 0 + placeholder2: int = 0 + + @property + def is_serving(self) -> bool: + """True if the endpoint is serving.""" + return self.ip != "0.0.0.0" + + def ip_str(self) -> str: + """Return the whole IP as string""" + return networking.ip__str__(self.ip_type, self.ip, self.port) + + def __eq__(self, other: "AxonInfo"): + if other is None: + return False + + if ( + self.version == other.version + and self.ip == other.ip + and self.port == other.port + and self.ip_type == other.ip_type + and self.coldkey == other.coldkey + and self.hotkey == other.hotkey + ): + return True + + return False + + def __str__(self): + return f"AxonInfo( {self.ip_str()}, {self.hotkey}, {self.coldkey}, {self.version} )" + + def __repr__(self): + return self.__str__() + + def to_string(self) -> str: + """Converts the `AxonInfo` object to a string representation using JSON.""" + try: + return json.dumps(asdict(self)) + except (TypeError, ValueError) as e: + logging.error(f"Error converting AxonInfo to string: {e}") + return AxonInfo(0, "", 0, 0, "", "").to_string() + + @classmethod + def from_string(cls, json_string: str) -> "AxonInfo": + """ + Creates an `AxonInfo` object from its string representation using JSON. + + Args: + json_string (str): The JSON string representation of the AxonInfo object. + + Returns: + AxonInfo: An instance of AxonInfo created from the JSON string. If decoding fails, returns a default `AxonInfo` object with default values. + + Raises: + json.JSONDecodeError: If there is an error in decoding the JSON string. + TypeError: If there is a type error when creating the AxonInfo object. + ValueError: If there is a value error when creating the AxonInfo object. + """ + try: + data = json.loads(json_string) + return cls(**data) + except json.JSONDecodeError as e: + logging.error(f"Error decoding JSON: {e}") + except TypeError as e: + logging.error(f"Type error: {e}") + except ValueError as e: + logging.error(f"Value error: {e}") + return AxonInfo(0, "", 0, 0, "", "") + + @classmethod + def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": + """ + Converts a dictionary to an `AxonInfo` object. + + Args: + neuron_info (dict): A dictionary containing the neuron information. + + Returns: + instance (AxonInfo): An instance of AxonInfo created from the dictionary. + """ + return cls( + version=neuron_info["axon_info"]["version"], + ip=networking.int_to_ip(int(neuron_info["axon_info"]["ip"])), + port=neuron_info["axon_info"]["port"], + ip_type=neuron_info["axon_info"]["ip_type"], + hotkey=neuron_info["hotkey"], + coldkey=neuron_info["coldkey"], + ) + + def to_parameter_dict( + self, + ) -> Union[dict[str, Union[int, str]], "torch.nn.ParameterDict"]: + """Returns a torch tensor or dict of the subnet info, depending on the USE_TORCH flag set.""" + if use_torch(): + return torch.nn.ParameterDict(self.__dict__) + else: + return self.__dict__ + + @classmethod + def from_parameter_dict( + cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] + ) -> "AxonInfo": + """Returns an axon_info object from a torch parameter_dict or a parameter dict.""" + if use_torch(): + return cls(**dict(parameter_dict)) + else: + return cls(**parameter_dict) diff --git a/bittensor/core/chain_data/delegate_info.py b/bittensor/core/chain_data/delegate_info.py new file mode 100644 index 000000000..d77f1e141 --- /dev/null +++ b/bittensor/core/chain_data/delegate_info.py @@ -0,0 +1,105 @@ +from dataclasses import dataclass +from typing import Optional, Any + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils import u16_normalized_float +from bittensor.utils.balance import Balance + + +@dataclass +class DelegateInfo: + """ + Dataclass for delegate information. For a lighter version of this class, see ``DelegateInfoLite``. + + Args: + hotkey_ss58 (str): Hotkey of the delegate for which the information is being fetched. + total_stake (int): Total stake of the delegate. + nominators (list[tuple[str, int]]): List of nominators of the delegate and their stake. + take (float): Take of the delegate as a percentage. + owner_ss58 (str): Coldkey of the owner. + registrations (list[int]): List of subnets that the delegate is registered on. + validator_permits (list[int]): List of subnets that the delegate is allowed to validate on. + return_per_1000 (int): Return per 1000 TAO, for the delegate over a day. + total_daily_return (int): Total daily return of the delegate. + + """ + + hotkey_ss58: str # Hotkey of delegate + total_stake: Balance # Total stake of the delegate + nominators: list[ + tuple[str, Balance] + ] # List of nominators of the delegate and their stake + owner_ss58: str # Coldkey of owner + take: float # Take of the delegate as a percentage + validator_permits: list[ + int + ] # List of subnets that the delegate is allowed to validate on + registrations: tuple[int] # List of subnets that the delegate is registered on + return_per_1000: Balance # Return per 1000 tao of the delegate over a day + total_daily_return: Balance # Total daily return of the delegate + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "DelegateInfo": + """Fixes the decoded values.""" + + return cls( + hotkey_ss58=ss58_encode(decoded["delegate_ss58"], SS58_FORMAT), + owner_ss58=ss58_encode(decoded["owner_ss58"], SS58_FORMAT), + take=u16_normalized_float(decoded["take"]), + nominators=[ + ( + ss58_encode(nom[0], SS58_FORMAT), + Balance.from_rao(nom[1]), + ) + for nom in decoded["nominators"] + ], + total_stake=Balance.from_rao( + sum([nom[1] for nom in decoded["nominators"]]) + ), + validator_permits=decoded["validator_permits"], + registrations=decoded["registrations"], + return_per_1000=Balance.from_rao(decoded["return_per_1000"]), + total_daily_return=Balance.from_rao(decoded["total_daily_return"]), + ) + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DelegateInfo"]: + """Returns a DelegateInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfo) + if decoded is None: + return None + + return DelegateInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["DelegateInfo"]: + """Returns a list of DelegateInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfo, is_vec=True) + + if decoded is None: + return [] + + return [DelegateInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def delegated_list_from_vec_u8( + cls, vec_u8: list[int] + ) -> list[tuple["DelegateInfo", "Balance"]]: + """Returns a list of Tuples of DelegateInfo objects, and Balance, from a ``vec_u8``. + + This is the list of delegates that the user has delegated to, and the amount of stake delegated. + """ + decoded = from_scale_encoding(vec_u8, ChainDataType.DelegatedInfo, is_vec=True) + if decoded is None: + return [] + + return [ + (DelegateInfo.fix_decoded_values(d), Balance.from_rao(s)) + for d, s in decoded + ] diff --git a/bittensor/core/chain_data/delegate_info_lite.py b/bittensor/core/chain_data/delegate_info_lite.py new file mode 100644 index 000000000..bf693c184 --- /dev/null +++ b/bittensor/core/chain_data/delegate_info_lite.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass + + +@dataclass +class DelegateInfoLite: + """ + Dataclass for `DelegateLiteInfo`. This is a lighter version of :func:``DelegateInfo``. + + Args: + delegate_ss58 (str): Hotkey of the delegate for which the information is being fetched. + take (float): Take of the delegate as a percentage. + nominators (int): Count of the nominators of the delegate. + owner_ss58 (str): Coldkey of the owner. + registrations (list[int]): List of subnets that the delegate is registered on. + validator_permits (list[int]): List of subnets that the delegate is allowed to validate on. + return_per_1000 (int): Return per 1000 TAO, for the delegate over a day. + total_daily_return (int): Total daily return of the delegate. + """ + + delegate_ss58: str # Hotkey of delegate + take: float # Take of the delegate as a percentage + nominators: int # Count of the nominators of the delegate. + owner_ss58: str # Coldkey of owner + registrations: list[int] # List of subnets that the delegate is registered on + validator_permits: list[ + int + ] # List of subnets that the delegate is allowed to validate on + return_per_1000: int # Return per 1000 tao for the delegate over a day + total_daily_return: int # Total daily return of the delegate diff --git a/bittensor/core/chain_data/ip_info.py b/bittensor/core/chain_data/ip_info.py new file mode 100644 index 000000000..6bbfabe02 --- /dev/null +++ b/bittensor/core/chain_data/ip_info.py @@ -0,0 +1,81 @@ +from dataclasses import dataclass +from typing import Optional, Any, Union + +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.utils import networking as net +from bittensor.utils.registration import torch, use_torch + + +@dataclass +class IPInfo: + """ + Dataclass representing IP information. + + Attributes: + ip (str): The IP address as a string. + ip_type (int): The type of the IP address (e.g., IPv4, IPv6). + protocol (int): The protocol associated with the IP (e.g., TCP, UDP). + """ + + ip: str + ip_type: int + protocol: int + + def encode(self) -> dict[str, Any]: + """Returns a dictionary of the IPInfo object that can be encoded.""" + return { + "ip": net.ip_to_int( + self.ip + ), # IP type and protocol are encoded together as a u8 + "ip_type_and_protocol": ((self.ip_type << 4) + self.protocol) & 0xFF, + } + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["IPInfo"]: + """Returns a IPInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.IPInfo) + if decoded is None: + return None + + return IPInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["IPInfo"]: + """Returns a list of IPInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.IPInfo, is_vec=True) + + if decoded is None: + return [] + + return [IPInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def fix_decoded_values(cls, decoded: dict) -> "IPInfo": + """Returns a SubnetInfo object from a decoded IPInfo dictionary.""" + return IPInfo( + ip=net.int_to_ip(decoded["ip"]), + ip_type=decoded["ip_type_and_protocol"] >> 4, + protocol=decoded["ip_type_and_protocol"] & 0xF, + ) + + def to_parameter_dict( + self, + ) -> Union[dict[str, Union[str, int]], "torch.nn.ParameterDict"]: + """Returns a torch tensor or dict of the subnet IP info.""" + if use_torch(): + return torch.nn.ParameterDict(self.__dict__) + else: + return self.__dict__ + + @classmethod + def from_parameter_dict( + cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] + ) -> "IPInfo": + """Creates a IPInfo instance from a parameter dictionary.""" + if use_torch(): + return cls(**dict(parameter_dict)) + else: + return cls(**parameter_dict) diff --git a/bittensor/core/chain_data/neuron_info.py b/bittensor/core/chain_data/neuron_info.py new file mode 100644 index 000000000..478cdfa4c --- /dev/null +++ b/bittensor/core/chain_data/neuron_info.py @@ -0,0 +1,176 @@ +from dataclasses import dataclass +from typing import Optional, TYPE_CHECKING + +import bt_decode +import netaddr + +from bittensor.core.chain_data.axon_info import AxonInfo +from bittensor.core.chain_data.prometheus_info import PrometheusInfo +from bittensor.core.chain_data.utils import decode_account_id, process_stake_data +from bittensor.utils import u16_normalized_float +from bittensor.utils.balance import Balance + +# for annotation purposes +if TYPE_CHECKING: + from bittensor.core.chain_data.neuron_info_lite import NeuronInfoLite + + +@dataclass +class NeuronInfo: + """Represents the metadata of a neuron including keys, UID, stake, rankings, and other attributes. + + Attributes: + hotkey (str): The hotkey associated with the neuron. + coldkey (str): The coldkey associated with the neuron. + uid (int): The unique identifier for the neuron. + netuid (int): The network unique identifier for the neuron. + active (int): The active status of the neuron. + stake (Balance): The balance staked to this neuron. + stake_dict (dict[str, Balance]): A dictionary mapping coldkey to the amount staked. + total_stake (Balance): The total amount of stake. + rank (float): The rank score of the neuron. + emission (float): The emission rate. + incentive (float): The incentive value. + consensus (float): The consensus score. + trust (float): The trust score. + validator_trust (float): The validation trust score. + dividends (float): The dividends value. + last_update (int): The timestamp of the last update. + validator_permit (bool): Validator permit status. + weights (list[list[int]]): List of weights associated with the neuron. + bonds (list[list[int]]): List of bonds associated with the neuron. + pruning_score (int): The pruning score of the neuron. + prometheus_info (Optional[PrometheusInfo]): Information related to Prometheus. + axon_info (Optional[AxonInfo]): Information related to Axon. + is_null (bool): Indicator if this is a null neuron. + """ + + hotkey: str + coldkey: str + uid: int + netuid: int + active: int + stake: "Balance" + # mapping of coldkey to amount staked to this Neuron + stake_dict: dict[str, "Balance"] + total_stake: "Balance" + rank: float + emission: float + incentive: float + consensus: float + trust: float + validator_trust: float + dividends: float + last_update: int + validator_permit: bool + weights: list[list[int]] + bonds: list[list[int]] + pruning_score: int + prometheus_info: Optional["PrometheusInfo"] = None + axon_info: Optional["AxonInfo"] = None + is_null: bool = False + + @classmethod + def from_weights_bonds_and_neuron_lite( + cls, + neuron_lite: "NeuronInfoLite", + weights_as_dict: dict[int, list[tuple[int, int]]], + bonds_as_dict: dict[int, list[tuple[int, int]]], + ) -> "NeuronInfo": + """ + Creates an instance of NeuronInfo from NeuronInfoLite and dictionaries of weights and bonds. + + Args: + neuron_lite (NeuronInfoLite): A lite version of the neuron containing basic attributes. + weights_as_dict (dict[int, list[tuple[int, int]]]): A dictionary where the key is the UID and the value is a list of weight tuples associated with the neuron. + bonds_as_dict (dict[int, list[tuple[int, int]]]): A dictionary where the key is the UID and the value is a list of bond tuples associated with the neuron. + + Returns: + NeuronInfo: An instance of NeuronInfo populated with the provided weights and bonds. + """ + n_dict = neuron_lite.__dict__ + n_dict["weights"] = weights_as_dict.get(neuron_lite.uid, []) + n_dict["bonds"] = bonds_as_dict.get(neuron_lite.uid, []) + + return cls(**n_dict) + + @staticmethod + def get_null_neuron() -> "NeuronInfo": + """Returns a null NeuronInfo instance.""" + neuron = NeuronInfo( + uid=0, + netuid=0, + active=0, + stake=Balance.from_rao(0), + stake_dict={}, + total_stake=Balance.from_rao(0), + rank=0, + emission=0, + incentive=0, + consensus=0, + trust=0, + validator_trust=0, + dividends=0, + last_update=0, + validator_permit=False, + weights=[], + bonds=[], + prometheus_info=None, + axon_info=None, + is_null=True, + coldkey="000000000000000000000000000000000000000000000000", + hotkey="000000000000000000000000000000000000000000000000", + pruning_score=0, + ) + return neuron + + @classmethod + def from_vec_u8(cls, vec_u8: bytes) -> "NeuronInfo": + """Instantiates NeuronInfo from a byte vector.""" + n = bt_decode.NeuronInfo.decode(bytes(vec_u8)) + stake_dict = process_stake_data(n.stake) + total_stake = sum(stake_dict.values()) if stake_dict else Balance(0) + axon_info = n.axon_info + coldkey = decode_account_id(n.coldkey) + hotkey = decode_account_id(n.hotkey) + return NeuronInfo( + hotkey=hotkey, + coldkey=coldkey, + uid=n.uid, + netuid=n.netuid, + active=n.active, + stake=total_stake, + stake_dict=stake_dict, + total_stake=total_stake, + rank=u16_normalized_float(n.rank), + emission=n.emission / 1e9, + incentive=u16_normalized_float(n.incentive), + consensus=u16_normalized_float(n.consensus), + trust=u16_normalized_float(n.trust), + validator_trust=u16_normalized_float(n.validator_trust), + dividends=u16_normalized_float(n.dividends), + last_update=n.last_update, + validator_permit=n.validator_permit, + weights=[[e[0], e[1]] for e in n.weights], + bonds=[[e[0], e[1]] for e in n.bonds], + pruning_score=n.pruning_score, + prometheus_info=PrometheusInfo( + block=n.prometheus_info.block, + version=n.prometheus_info.version, + ip=str(netaddr.IPAddress(n.prometheus_info.ip)), + port=n.prometheus_info.port, + ip_type=n.prometheus_info.ip_type, + ), + axon_info=AxonInfo( + version=axon_info.version, + ip=str(netaddr.IPAddress(axon_info.ip)), + port=axon_info.port, + ip_type=axon_info.ip_type, + placeholder1=axon_info.placeholder1, + placeholder2=axon_info.placeholder2, + protocol=axon_info.protocol, + hotkey=hotkey, + coldkey=coldkey, + ), + is_null=False, + ) diff --git a/bittensor/core/chain_data/neuron_info_lite.py b/bittensor/core/chain_data/neuron_info_lite.py new file mode 100644 index 000000000..48d9ed4ca --- /dev/null +++ b/bittensor/core/chain_data/neuron_info_lite.py @@ -0,0 +1,171 @@ +from dataclasses import dataclass +from typing import Optional + +import bt_decode +import netaddr + +from bittensor.core.chain_data.axon_info import AxonInfo +from bittensor.core.chain_data.prometheus_info import PrometheusInfo +from bittensor.core.chain_data.utils import decode_account_id, process_stake_data +from bittensor.utils import u16_normalized_float +from bittensor.utils.balance import Balance + + +@dataclass +class NeuronInfoLite: + """ + NeuronInfoLite is a dataclass representing neuron metadata without weights and bonds. + + Attributes: + hotkey (str): The hotkey string for the neuron. + coldkey (str): The coldkey string for the neuron. + uid (int): A unique identifier for the neuron. + netuid (int): Network unique identifier for the neuron. + active (int): Indicates whether the neuron is active. + stake (Balance): The stake amount associated with the neuron. + stake_dict (dict): Mapping of coldkey to the amount staked to this Neuron. + total_stake (Balance): Total amount of the stake. + rank (float): The rank of the neuron. + emission (float): The emission value of the neuron. + incentive (float): The incentive value of the neuron. + consensus (float): The consensus value of the neuron. + trust (float): Trust value of the neuron. + validator_trust (float): Validator trust value of the neuron. + dividends (float): Dividends associated with the neuron. + last_update (int): Timestamp of the last update. + validator_permit (bool): Indicates if the neuron has a validator permit. + prometheus_info (Optional[PrometheusInfo]): Prometheus information associated with the neuron. + axon_info (Optional[AxonInfo]): Axon information associated with the neuron. + pruning_score (int): The pruning score of the neuron. + is_null (bool): Indicates whether the neuron is null. + + Methods: + get_null_neuron: Returns a NeuronInfoLite object representing a null neuron. + list_from_vec_u8: Decodes a bytes object into a list of NeuronInfoLite instances. + """ + + hotkey: str + coldkey: str + uid: int + netuid: int + active: int + stake: "Balance" + # mapping of coldkey to amount staked to this Neuron + stake_dict: dict[str, "Balance"] + total_stake: "Balance" + rank: float + emission: float + incentive: float + consensus: float + trust: float + validator_trust: float + dividends: float + last_update: int + validator_permit: bool + prometheus_info: Optional["PrometheusInfo"] + axon_info: Optional["AxonInfo"] + pruning_score: int + is_null: bool = False + + @staticmethod + def get_null_neuron() -> "NeuronInfoLite": + """Returns a null NeuronInfoLite instance.""" + neuron = NeuronInfoLite( + uid=0, + netuid=0, + active=0, + stake=Balance.from_rao(0), + stake_dict={}, + total_stake=Balance.from_rao(0), + rank=0, + emission=0, + incentive=0, + consensus=0, + trust=0, + validator_trust=0, + dividends=0, + last_update=0, + validator_permit=False, + prometheus_info=None, + axon_info=None, + is_null=True, + coldkey="000000000000000000000000000000000000000000000000", + hotkey="000000000000000000000000000000000000000000000000", + pruning_score=0, + ) + return neuron + + @classmethod + def list_from_vec_u8(cls, vec_u8: bytes) -> list["NeuronInfoLite"]: + """ + Decodes a bytes object into a list of NeuronInfoLite instances. + + Args: + vec_u8 (bytes): The bytes object to decode into NeuronInfoLite instances. + + Returns: + list[NeuronInfoLite]: A list of NeuronInfoLite instances decoded from the provided bytes object. + """ + decoded = bt_decode.NeuronInfoLite.decode_vec(vec_u8) + results = [] + for item in decoded: + active = item.active + axon_info = item.axon_info + coldkey = decode_account_id(item.coldkey) + consensus = item.consensus + dividends = item.dividends + emission = item.emission + hotkey = decode_account_id(item.hotkey) + incentive = item.incentive + last_update = item.last_update + netuid = item.netuid + prometheus_info = item.prometheus_info + pruning_score = item.pruning_score + rank = item.rank + stake_dict = process_stake_data(item.stake) + stake = sum(stake_dict.values()) if stake_dict else Balance(0) + trust = item.trust + uid = item.uid + validator_permit = item.validator_permit + validator_trust = item.validator_trust + results.append( + NeuronInfoLite( + active=active, + axon_info=AxonInfo( + version=axon_info.version, + ip=str(netaddr.IPAddress(axon_info.ip)), + port=axon_info.port, + ip_type=axon_info.ip_type, + placeholder1=axon_info.placeholder1, + placeholder2=axon_info.placeholder2, + protocol=axon_info.protocol, + hotkey=hotkey, + coldkey=coldkey, + ), + coldkey=coldkey, + consensus=u16_normalized_float(consensus), + dividends=u16_normalized_float(dividends), + emission=emission / 1e9, + hotkey=hotkey, + incentive=u16_normalized_float(incentive), + last_update=last_update, + netuid=netuid, + prometheus_info=PrometheusInfo( + version=prometheus_info.version, + ip=str(netaddr.IPAddress(prometheus_info.ip)), + port=prometheus_info.port, + ip_type=prometheus_info.ip_type, + block=prometheus_info.block, + ), + pruning_score=pruning_score, + rank=u16_normalized_float(rank), + stake_dict=stake_dict, + stake=stake, + total_stake=stake, + trust=u16_normalized_float(trust), + uid=uid, + validator_permit=validator_permit, + validator_trust=u16_normalized_float(validator_trust), + ) + ) + return results diff --git a/bittensor/core/chain_data/prometheus_info.py b/bittensor/core/chain_data/prometheus_info.py new file mode 100644 index 000000000..7cdccf83f --- /dev/null +++ b/bittensor/core/chain_data/prometheus_info.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass + +from bittensor.utils import networking + + +@dataclass +class PrometheusInfo: + """ + Dataclass representing information related to Prometheus. + + Attributes: + block (int): The block number associated with the Prometheus data. + version (int): The version of the Prometheus data. + ip (str): The IP address associated with Prometheus. + port (int): The port number for Prometheus. + ip_type (int): The type of IP address (e.g., IPv4, IPv6). + """ + + block: int + version: int + ip: str + port: int + ip_type: int + + @classmethod + def fix_decoded_values(cls, prometheus_info_decoded: dict) -> "PrometheusInfo": + """Returns a PrometheusInfo object from a prometheus_info_decoded dictionary.""" + prometheus_info_decoded["ip"] = networking.int_to_ip( + int(prometheus_info_decoded["ip"]) + ) + return cls(**prometheus_info_decoded) diff --git a/bittensor/core/chain_data/proposal_vote_data.py b/bittensor/core/chain_data/proposal_vote_data.py new file mode 100644 index 000000000..493bc2d79 --- /dev/null +++ b/bittensor/core/chain_data/proposal_vote_data.py @@ -0,0 +1,21 @@ +from typing import TypedDict + + +# Senate / Proposal data +class ProposalVoteData(TypedDict): + """ + This TypedDict represents the data structure for a proposal vote in the Senate. + + Attributes: + index (int): The index of the proposal. + threshold (int): The threshold required for the proposal to pass. + ayes (List[str]): List of senators who voted 'aye'. + nays (List[str]): List of senators who voted 'nay'. + end (int): The ending timestamp of the voting period. + """ + + index: int + threshold: int + ayes: list[str] + nays: list[str] + end: int diff --git a/bittensor/core/chain_data/scheduled_coldkey_swap_info.py b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py new file mode 100644 index 000000000..7c0f6e7f8 --- /dev/null +++ b/bittensor/core/chain_data/scheduled_coldkey_swap_info.py @@ -0,0 +1,65 @@ +from dataclasses import dataclass +from typing import Optional, Any + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.core.settings import SS58_FORMAT + + +@dataclass +class ScheduledColdkeySwapInfo: + """ + The `ScheduledColdkeySwapInfo` class is a dataclass representing information about scheduled cold key swaps. + + Attributes: + old_coldkey (str): The old cold key before the swap. + new_coldkey (str): The new cold key after the swap. + arbitration_block (int): The block number at which the arbitration of the swap will take place. + """ + + old_coldkey: str + new_coldkey: str + arbitration_block: int + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": + """Fixes the decoded values.""" + return cls( + old_coldkey=ss58_encode(decoded["old_coldkey"], SS58_FORMAT), + new_coldkey=ss58_encode(decoded["new_coldkey"], SS58_FORMAT), + arbitration_block=decoded["arbitration_block"], + ) + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["ScheduledColdkeySwapInfo"]: + """Returns a ScheduledColdkeySwapInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo) + if decoded is None: + return None + + return ScheduledColdkeySwapInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["ScheduledColdkeySwapInfo"]: + """Returns a list of ScheduledColdkeySwapInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True + ) + if decoded is None: + return [] + + return [ScheduledColdkeySwapInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def decode_account_id_list(cls, vec_u8: list[int]) -> Optional[list[str]]: + """Decodes a list of AccountIds from vec_u8.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True + ) + if decoded is None: + return None + return [ss58_encode(account_id, SS58_FORMAT) for account_id in decoded] diff --git a/bittensor/core/chain_data/stake_info.py b/bittensor/core/chain_data/stake_info.py new file mode 100644 index 000000000..8d3b5020f --- /dev/null +++ b/bittensor/core/chain_data/stake_info.py @@ -0,0 +1,79 @@ +from dataclasses import dataclass +from typing import Optional, Any + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.utils import ( + from_scale_encoding, + from_scale_encoding_using_type_string, + ChainDataType, +) +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils.balance import Balance + + +@dataclass +class StakeInfo: + """ + Dataclass for representing stake information linked to hotkey and coldkey pairs. + + Attributes: + hotkey_ss58 (str): The SS58 encoded hotkey address. + coldkey_ss58 (str): The SS58 encoded coldkey address. + stake (Balance): The stake associated with the hotkey-coldkey pair, represented as a Balance object. + """ + + hotkey_ss58: str # Hotkey address + coldkey_ss58: str # Coldkey address + stake: Balance # Stake for the hotkey-coldkey pair + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "StakeInfo": + """Fixes the decoded values.""" + return cls( + hotkey_ss58=ss58_encode(decoded["hotkey"], SS58_FORMAT), + coldkey_ss58=ss58_encode(decoded["coldkey"], SS58_FORMAT), + stake=Balance.from_rao(decoded["stake"]), + ) + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["StakeInfo"]: + """Returns a StakeInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo) + if decoded is None: + return None + + return StakeInfo.fix_decoded_values(decoded) + + @classmethod + def list_of_tuple_from_vec_u8( + cls, vec_u8: list[int] + ) -> dict[str, list["StakeInfo"]]: + """Returns a list of StakeInfo objects from a ``vec_u8``.""" + decoded: Optional[list[tuple[str, list[object]]]] = ( + from_scale_encoding_using_type_string( + input_=vec_u8, type_string="Vec<(AccountId, Vec)>" + ) + ) + + if decoded is None: + return {} + + return { + ss58_encode(address=account_id, ss58_format=SS58_FORMAT): [ + StakeInfo.fix_decoded_values(d) for d in stake_info + ] + for account_id, stake_info in decoded + } + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["StakeInfo"]: + """Returns a list of StakeInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo, is_vec=True) + if decoded is None: + return [] + + return [StakeInfo.fix_decoded_values(d) for d in decoded] diff --git a/bittensor/core/chain_data/subnet_hyperparameters.py b/bittensor/core/chain_data/subnet_hyperparameters.py new file mode 100644 index 000000000..c28f802cf --- /dev/null +++ b/bittensor/core/chain_data/subnet_hyperparameters.py @@ -0,0 +1,112 @@ +from dataclasses import dataclass +from typing import Optional + +import bt_decode + + +@dataclass +class SubnetHyperparameters: + """ + This class represents the hyperparameters for a subnet. + + Attributes: + rho (int): The rate of decay of some value. + kappa (int): A constant multiplier used in calculations. + immunity_period (int): The period during which immunity is active. + min_allowed_weights (int): Minimum allowed weights. + max_weight_limit (float): Maximum weight limit. + tempo (int): The tempo or rate of operation. + min_difficulty (int): Minimum difficulty for some operations. + max_difficulty (int): Maximum difficulty for some operations. + weights_version (int): The version number of the weights used. + weights_rate_limit (int): Rate limit for processing weights. + adjustment_interval (int): Interval at which adjustments are made. + activity_cutoff (int): Activity cutoff threshold. + registration_allowed (bool): Indicates if registration is allowed. + target_regs_per_interval (int): Target number of registrations per interval. + min_burn (int): Minimum burn value. + max_burn (int): Maximum burn value. + bonds_moving_avg (int): Moving average of bonds. + max_regs_per_block (int): Maximum number of registrations per block. + serving_rate_limit (int): Limit on the rate of service. + max_validators (int): Maximum number of validators. + adjustment_alpha (int): Alpha value for adjustments. + difficulty (int): Difficulty level. + commit_reveal_weights_interval (int): Interval for commit-reveal weights. + commit_reveal_weights_enabled (bool): Flag indicating if commit-reveal weights are enabled. + alpha_high (int): High value of alpha. + alpha_low (int): Low value of alpha. + liquid_alpha_enabled (bool): Flag indicating if liquid alpha is enabled. + """ + + rho: int + kappa: int + immunity_period: int + min_allowed_weights: int + max_weight_limit: float + tempo: int + min_difficulty: int + max_difficulty: int + weights_version: int + weights_rate_limit: int + adjustment_interval: int + activity_cutoff: int + registration_allowed: bool + target_regs_per_interval: int + min_burn: int + max_burn: int + bonds_moving_avg: int + max_regs_per_block: int + serving_rate_limit: int + max_validators: int + adjustment_alpha: int + difficulty: int + commit_reveal_weights_interval: int + commit_reveal_weights_enabled: bool + alpha_high: int + alpha_low: int + liquid_alpha_enabled: bool + + @classmethod + def from_vec_u8(cls, vec_u8: bytes) -> Optional["SubnetHyperparameters"]: + """ + Create a `SubnetHyperparameters` instance from a vector of bytes. + + This method decodes the given vector of bytes using the `bt_decode` module and creates a new instance of `SubnetHyperparameters` with the decoded values. + + Args: + vec_u8 (bytes): A vector of bytes to decode into `SubnetHyperparameters`. + + Returns: + Optional[SubnetHyperparameters]: An instance of `SubnetHyperparameters` if decoding is successful, None otherwise. + """ + decoded = bt_decode.SubnetHyperparameters.decode(vec_u8) + return SubnetHyperparameters( + rho=decoded.rho, + kappa=decoded.kappa, + immunity_period=decoded.immunity_period, + min_allowed_weights=decoded.min_allowed_weights, + max_weight_limit=decoded.max_weights_limit, + tempo=decoded.tempo, + min_difficulty=decoded.min_difficulty, + max_difficulty=decoded.max_difficulty, + weights_version=decoded.weights_version, + weights_rate_limit=decoded.weights_rate_limit, + adjustment_interval=decoded.adjustment_interval, + activity_cutoff=decoded.activity_cutoff, + registration_allowed=decoded.registration_allowed, + target_regs_per_interval=decoded.target_regs_per_interval, + min_burn=decoded.min_burn, + max_burn=decoded.max_burn, + bonds_moving_avg=decoded.bonds_moving_avg, + max_regs_per_block=decoded.max_regs_per_block, + serving_rate_limit=decoded.serving_rate_limit, + max_validators=decoded.max_validators, + adjustment_alpha=decoded.adjustment_alpha, + difficulty=decoded.difficulty, + commit_reveal_weights_interval=decoded.commit_reveal_weights_interval, + commit_reveal_weights_enabled=decoded.commit_reveal_weights_enabled, + alpha_high=decoded.alpha_high, + alpha_low=decoded.alpha_low, + liquid_alpha_enabled=decoded.liquid_alpha_enabled, + ) diff --git a/bittensor/core/chain_data/subnet_info.py b/bittensor/core/chain_data/subnet_info.py new file mode 100644 index 000000000..f1ce15187 --- /dev/null +++ b/bittensor/core/chain_data/subnet_info.py @@ -0,0 +1,103 @@ +from dataclasses import dataclass +from typing import Any, Optional, Union + +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.chain_data.utils import from_scale_encoding, ChainDataType +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils import u16_normalized_float +from bittensor.utils.balance import Balance +from bittensor.utils.registration import torch, use_torch + + +@dataclass +class SubnetInfo: + """Dataclass for subnet info.""" + + netuid: int + rho: int + kappa: int + difficulty: int + immunity_period: int + max_allowed_validators: int + min_allowed_weights: int + max_weight_limit: float + scaling_law_power: float + subnetwork_n: int + max_n: int + blocks_since_epoch: int + tempo: int + modality: int + # netuid -> topk percentile prunning score requirement (u16:MAX normalized.) + connection_requirements: dict[str, float] + emission_value: float + burn: Balance + owner_ss58: str + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetInfo"]: + """Returns a SubnetInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetInfo) + if decoded is None: + return None + + return SubnetInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["SubnetInfo"]: + """Returns a list of SubnetInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.SubnetInfo, is_vec=True, is_option=True + ) + + if decoded is None: + return [] + + return [SubnetInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def fix_decoded_values(cls, decoded: dict) -> "SubnetInfo": + """Returns a SubnetInfo object from a decoded SubnetInfo dictionary.""" + return SubnetInfo( + netuid=decoded["netuid"], + rho=decoded["rho"], + kappa=decoded["kappa"], + difficulty=decoded["difficulty"], + immunity_period=decoded["immunity_period"], + max_allowed_validators=decoded["max_allowed_validators"], + min_allowed_weights=decoded["min_allowed_weights"], + max_weight_limit=decoded["max_weights_limit"], + scaling_law_power=decoded["scaling_law_power"], + subnetwork_n=decoded["subnetwork_n"], + max_n=decoded["max_allowed_uids"], + blocks_since_epoch=decoded["blocks_since_last_step"], + tempo=decoded["tempo"], + modality=decoded["network_modality"], + connection_requirements={ + str(int(netuid)): u16_normalized_float(int(req)) + for netuid, req in decoded["network_connect"] + }, + emission_value=decoded["emission_values"], + burn=Balance.from_rao(decoded["burn"]), + owner_ss58=ss58_encode(decoded["owner"], SS58_FORMAT), + ) + + def to_parameter_dict(self) -> Union[dict[str, Any], "torch.nn.ParameterDict"]: + """Returns a torch tensor or dict of the subnet info.""" + if use_torch(): + return torch.nn.ParameterDict(self.__dict__) + else: + return self.__dict__ + + @classmethod + def from_parameter_dict( + cls, parameter_dict: Union[dict[str, Any], "torch.nn.ParameterDict"] + ) -> "SubnetInfo": + """Creates a SubnetInfo instance from a parameter dictionary.""" + if use_torch(): + return cls(**dict(parameter_dict)) + else: + return cls(**parameter_dict) diff --git a/bittensor/core/chain_data/utils.py b/bittensor/core/chain_data/utils.py new file mode 100644 index 000000000..0544ca85a --- /dev/null +++ b/bittensor/core/chain_data/utils.py @@ -0,0 +1,291 @@ +"""Chain data helper functions and data.""" + +from enum import Enum +from typing import Optional, Union + +from scalecodec.base import RuntimeConfiguration, ScaleBytes +from scalecodec.type_registry import load_type_registry_preset +from scalecodec.utils.ss58 import ss58_encode + +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils.balance import Balance + + +class ChainDataType(Enum): + NeuronInfo = 1 + SubnetInfo = 2 + DelegateInfo = 3 + NeuronInfoLite = 4 + DelegatedInfo = 5 + StakeInfo = 6 + IPInfo = 7 + SubnetHyperparameters = 8 + ScheduledColdkeySwapInfo = 9 + AccountId = 10 + + +def from_scale_encoding( + input_: Union[list[int], bytes, "ScaleBytes"], + type_name: "ChainDataType", + is_vec: bool = False, + is_option: bool = False, +) -> Optional[dict]: + """ + Decodes input_ data from SCALE encoding based on the specified type name and modifiers. + + Args: + input_ (Union[List[int], bytes, ScaleBytes]): The input_ data to decode. + type_name (ChainDataType): The type of data being decoded. + is_vec (bool): Whether the data is a vector of the specified type. Default is ``False``. + is_option (bool): Whether the data is an optional value of the specified type. Default is ``False``. + + Returns: + Optional[dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. + """ + type_string = type_name.name + if type_name == ChainDataType.DelegatedInfo: + # DelegatedInfo is a tuple of (DelegateInfo, Compact) + type_string = f"({ChainDataType.DelegateInfo.name}, Compact)" + if is_option: + type_string = f"Option<{type_string}>" + if is_vec: + type_string = f"Vec<{type_string}>" + + return from_scale_encoding_using_type_string(input_, type_string) + + +def from_scale_encoding_using_type_string( + input_: Union[list[int], bytes, ScaleBytes], type_string: str +) -> Optional[dict]: + """ + Decodes SCALE encoded data to a dictionary based on the provided type string. + + Args: + input_ (Union[List[int], bytes, ScaleBytes]): The SCALE encoded input data. + type_string (str): The type string defining the structure of the data. + + Returns: + Optional[dict]: The decoded data as a dictionary, or ``None`` if the decoding fails. + + Raises: + TypeError: If the input_ is not a list[int], bytes, or ScaleBytes. + """ + if isinstance(input_, ScaleBytes): + as_scale_bytes = input_ + else: + if isinstance(input_, list) and all([isinstance(i, int) for i in input_]): + vec_u8 = input_ + as_bytes = bytes(vec_u8) + elif isinstance(input_, bytes): + as_bytes = input_ + else: + raise TypeError("input_ must be a list[int], bytes, or ScaleBytes") + + as_scale_bytes = ScaleBytes(as_bytes) + + rpc_runtime_config = RuntimeConfiguration() + rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) + rpc_runtime_config.update_type_registry(custom_rpc_type_registry) + + obj = rpc_runtime_config.create_scale_object(type_string, data=as_scale_bytes) + + return obj.decode() + + +custom_rpc_type_registry = { + "types": { + "SubnetInfo": { + "type": "struct", + "type_mapping": [ + ["netuid", "Compact"], + ["rho", "Compact"], + ["kappa", "Compact"], + ["difficulty", "Compact"], + ["immunity_period", "Compact"], + ["max_allowed_validators", "Compact"], + ["min_allowed_weights", "Compact"], + ["max_weights_limit", "Compact"], + ["scaling_law_power", "Compact"], + ["subnetwork_n", "Compact"], + ["max_allowed_uids", "Compact"], + ["blocks_since_last_step", "Compact"], + ["tempo", "Compact"], + ["network_modality", "Compact"], + ["network_connect", "Vec<[u16; 2]>"], + ["emission_values", "Compact"], + ["burn", "Compact"], + ["owner", "AccountId"], + ], + }, + "DelegateInfo": { + "type": "struct", + "type_mapping": [ + ["delegate_ss58", "AccountId"], + ["take", "Compact"], + ["nominators", "Vec<(AccountId, Compact)>"], + ["owner_ss58", "AccountId"], + ["registrations", "Vec>"], + ["validator_permits", "Vec>"], + ["return_per_1000", "Compact"], + ["total_daily_return", "Compact"], + ], + }, + "NeuronInfo": { + "type": "struct", + "type_mapping": [ + ["hotkey", "AccountId"], + ["coldkey", "AccountId"], + ["uid", "Compact"], + ["netuid", "Compact"], + ["active", "bool"], + ["axon_info", "axon_info"], + ["prometheus_info", "PrometheusInfo"], + ["stake", "Vec<(AccountId, Compact)>"], + ["rank", "Compact"], + ["emission", "Compact"], + ["incentive", "Compact"], + ["consensus", "Compact"], + ["trust", "Compact"], + ["validator_trust", "Compact"], + ["dividends", "Compact"], + ["last_update", "Compact"], + ["validator_permit", "bool"], + ["weights", "Vec<(Compact, Compact)>"], + ["bonds", "Vec<(Compact, Compact)>"], + ["pruning_score", "Compact"], + ], + }, + "NeuronInfoLite": { + "type": "struct", + "type_mapping": [ + ["hotkey", "AccountId"], + ["coldkey", "AccountId"], + ["uid", "Compact"], + ["netuid", "Compact"], + ["active", "bool"], + ["axon_info", "axon_info"], + ["prometheus_info", "PrometheusInfo"], + ["stake", "Vec<(AccountId, Compact)>"], + ["rank", "Compact"], + ["emission", "Compact"], + ["incentive", "Compact"], + ["consensus", "Compact"], + ["trust", "Compact"], + ["validator_trust", "Compact"], + ["dividends", "Compact"], + ["last_update", "Compact"], + ["validator_permit", "bool"], + ["pruning_score", "Compact"], + ], + }, + "axon_info": { + "type": "struct", + "type_mapping": [ + ["block", "u64"], + ["version", "u32"], + ["ip", "u128"], + ["port", "u16"], + ["ip_type", "u8"], + ["protocol", "u8"], + ["placeholder1", "u8"], + ["placeholder2", "u8"], + ], + }, + "PrometheusInfo": { + "type": "struct", + "type_mapping": [ + ["block", "u64"], + ["version", "u32"], + ["ip", "u128"], + ["port", "u16"], + ["ip_type", "u8"], + ], + }, + "IPInfo": { + "type": "struct", + "type_mapping": [ + ["ip", "Compact"], + ["ip_type_and_protocol", "Compact"], + ], + }, + "StakeInfo": { + "type": "struct", + "type_mapping": [ + ["hotkey", "AccountId"], + ["coldkey", "AccountId"], + ["stake", "Compact"], + ], + }, + "SubnetHyperparameters": { + "type": "struct", + "type_mapping": [ + ["rho", "Compact"], + ["kappa", "Compact"], + ["immunity_period", "Compact"], + ["min_allowed_weights", "Compact"], + ["max_weights_limit", "Compact"], + ["tempo", "Compact"], + ["min_difficulty", "Compact"], + ["max_difficulty", "Compact"], + ["weights_version", "Compact"], + ["weights_rate_limit", "Compact"], + ["adjustment_interval", "Compact"], + ["activity_cutoff", "Compact"], + ["registration_allowed", "bool"], + ["target_regs_per_interval", "Compact"], + ["min_burn", "Compact"], + ["max_burn", "Compact"], + ["bonds_moving_avg", "Compact"], + ["max_regs_per_block", "Compact"], + ["serving_rate_limit", "Compact"], + ["max_validators", "Compact"], + ["adjustment_alpha", "Compact"], + ["difficulty", "Compact"], + ["commit_reveal_weights_interval", "Compact"], + ["commit_reveal_weights_enabled", "bool"], + ["alpha_high", "Compact"], + ["alpha_low", "Compact"], + ["liquid_alpha_enabled", "bool"], + ], + }, + "ScheduledColdkeySwapInfo": { + "type": "struct", + "type_mapping": [ + ["old_coldkey", "AccountId"], + ["new_coldkey", "AccountId"], + ["arbitration_block", "Compact"], + ], + }, + } +} + + +def decode_account_id(account_id_bytes: list) -> str: + """ + Decodes an AccountId from bytes to a Base64 string using SS58 encoding. + + Args: + account_id_bytes (bytes): The AccountId in bytes that needs to be decoded. + + Returns: + str: The decoded AccountId as a Base64 string. + """ + # Convert the AccountId bytes to a Base64 string + return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) + + +def process_stake_data(stake_data: list) -> dict: + """ + Processes stake data to decode account IDs and convert stakes from rao to Balance objects. + + Args: + stake_data (list): A list of tuples where each tuple contains an account ID in bytes and a stake in rao. + + Returns: + dict: A dictionary with account IDs as keys and their corresponding Balance objects as values. + """ + decoded_stake_data = {} + for account_id_bytes, stake_ in stake_data: + account_id = decode_account_id(account_id_bytes) + decoded_stake_data.update({account_id: Balance.from_rao(stake_)}) + return decoded_stake_data diff --git a/bittensor/config.py b/bittensor/core/config.py similarity index 73% rename from bittensor/config.py rename to bittensor/core/config.py index 59ad4451b..5027bbecb 100644 --- a/bittensor/config.py +++ b/bittensor/core/config.py @@ -1,69 +1,59 @@ -""" -Implementation of the config class, which manages the configuration of different Bittensor modules. -""" - # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation - +# 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. +"""Implementation of the config class, which manages the configuration of different Bittensor modules.""" + +import argparse +import copy import os import sys -import yaml -import copy from copy import deepcopy +from typing import Any, TypeVar, Type, Optional + +import yaml from munch import DefaultMunch -from typing import List, Optional, Dict, Any, TypeVar, Type -import argparse class InvalidConfigFile(Exception): """In place of YAMLError""" - pass - -class config(DefaultMunch): +class Config(DefaultMunch): """ Implementation of the config class, which manages the configuration of different Bittensor modules. - """ - __is_set: Dict[str, bool] + Translates the passed parser into a nested Bittensor config. - r""" Translates the passed parser into a nested Bittensor config. - - Args: - parser (argparse.ArgumentParser): - Command line parser object. - strict (bool): - If ``true``, the command line arguments are strictly parsed. - args (list of str): - Command line arguments. - default (Optional[Any]): - Default value for the Config. Defaults to ``None``. - This default will be returned for attributes that are undefined. - Returns: - config (bittensor.config): - Nested config object created from parser arguments. + Args: + parser (argparse.ArgumentParser): Command line parser object. + strict (bool): If ``true``, the command line arguments are strictly parsed. + args (list of str): Command line arguments. + default (Optional[Any]): Default value for the Config. Defaults to ``None``. This default will be returned for attributes that are undefined. + + Returns: + config (bittensor.core.config.Config): Nested config object created from parser arguments. """ + __is_set: dict[str, bool] + def __init__( self, parser: argparse.ArgumentParser = None, - args: Optional[List[str]] = None, + args: Optional[list[str]] = None, strict: bool = False, default: Optional[Any] = None, ) -> None: @@ -71,8 +61,8 @@ def __init__( self["__is_set"] = {} - if parser == None: - return None + if parser is None: + return # Optionally add config specific arguments try: @@ -81,7 +71,7 @@ def __init__( type=str, help="If set, defaults are overridden by passed file.", ) - except: + except Exception: # this can fail if --config has already been added. pass @@ -92,7 +82,7 @@ def __init__( help="""If flagged, config will check that only exact arguments have been set.""", default=False, ) - except: + except Exception: # this can fail if --strict has already been added. pass @@ -103,7 +93,7 @@ def __init__( help="Set ``true`` to stop cli version checking.", default=False, ) - except: + except Exception: # this can fail if --no_version_checking has already been added. pass @@ -115,12 +105,12 @@ def __init__( help="Set ``true`` to stop cli from prompting the user.", default=False, ) - except: + except Exception: # this can fail if --no_version_checking has already been added. pass # Get args from argv if not passed in. - if args == None: + if args is None: args = sys.argv[1:] # Check for missing required arguments before proceeding @@ -142,63 +132,63 @@ def __init__( config_file_path = None # Parse args not strict - config_params = config.__parse_args__(args=args, parser=parser, strict=False) + config_params = Config.__parse_args__(args=args, parser=parser, strict=False) # 2. Optionally check for --strict - ## strict=True when passed in OR when --strict is set + # strict=True when passed in OR when --strict is set strict = config_params.strict or strict - if config_file_path != None: + if config_file_path is not None: config_file_path = os.path.expanduser(config_file_path) try: with open(config_file_path) as f: params_config = yaml.safe_load(f) - print("Loading config defaults from: {}".format(config_file_path)) + print(f"Loading config defaults from: {config_file_path}") parser.set_defaults(**params_config) except Exception as e: - print("Error in loading: {} using default parser settings".format(e)) + print(f"Error in loading: {e} using default parser settings") # 2. Continue with loading in params. - params = config.__parse_args__(args=args, parser=parser, strict=strict) + params = Config.__parse_args__(args=args, parser=parser, strict=strict) _config = self # Splits params and add to config - config.__split_params__(params=params, _config=_config) + Config.__split_params__(params=params, _config=_config) # Make the is_set map _config["__is_set"] = {} - ## Reparse args using default of unset + # Reparse args using default of unset parser_no_defaults = copy.deepcopy(parser) # Only command as the arg, else no args default_param_args = ( [_config.get("command")] - if _config.get("command") != None and _config.get("subcommand") == None + if _config.get("command") is not None and _config.get("subcommand") is None else [] ) - if _config.get("command") != None and _config.get("subcommand") != None: + if _config.get("command") is not None and _config.get("subcommand") is not None: default_param_args = [_config.get("command"), _config.get("subcommand")] - ## Get all args by name + # Get all args by name default_params = parser.parse_args(args=default_param_args) all_default_args = default_params.__dict__.keys() | [] - ## Make a dict with keys as args and values as argparse.SUPPRESS + # Make a dict with keys as args and values as argparse.SUPPRESS defaults_as_suppress = {key: argparse.SUPPRESS for key in all_default_args} - ## Set the defaults to argparse.SUPPRESS, should remove them from the namespace + # Set the defaults to argparse.SUPPRESS, should remove them from the namespace parser_no_defaults.set_defaults(**defaults_as_suppress) parser_no_defaults._defaults.clear() # Needed for quirk of argparse - ### Check for subparsers and do the same - if parser_no_defaults._subparsers != None: + # Check for subparsers and do the same + if parser_no_defaults._subparsers is not None: for action in parser_no_defaults._subparsers._actions: # Should only be the "command" subparser action if isinstance(action, argparse._SubParsersAction): # Set the defaults to argparse.SUPPRESS, should remove them from the namespace # Each choice is the keyword for a command, we need to set the defaults for each of these - ## Note: we also need to clear the _defaults dict for each, this is a quirk of argparse + # Note: we also need to clear the _defaults dict for each, this is a quirk of argparse cmd_parser: argparse.ArgumentParser for cmd_parser in action.choices.values(): # If this choice is also a subparser, set defaults recursively @@ -214,12 +204,12 @@ def __init__( cmd_parser.set_defaults(**defaults_as_suppress) cmd_parser._defaults.clear() # Needed for quirk of argparse - ## Reparse the args, but this time with the defaults as argparse.SUPPRESS - params_no_defaults = config.__parse_args__( + # Reparse the args, but this time with the defaults as argparse.SUPPRESS + params_no_defaults = Config.__parse_args__( args=args, parser=parser_no_defaults, strict=strict ) - ## Diff the params and params_no_defaults to get the is_set map + # Diff the params and params_no_defaults to get the is_set map _config["__is_set"] = { arg_key: True for arg_key in [ @@ -232,20 +222,20 @@ def __init__( } @staticmethod - def __split_params__(params: argparse.Namespace, _config: "config"): - # Splits params on dot syntax i.e neuron.axon_port and adds to _config + def __split_params__(params: argparse.Namespace, _config: "Config"): + # Splits params on dot syntax i.e. neuron.axon_port and adds to _config for arg_key, arg_val in params.__dict__.items(): split_keys = arg_key.split(".") head = _config keys = split_keys while len(keys) > 1: if ( - hasattr(head, keys[0]) and head[keys[0]] != None + hasattr(head, keys[0]) and head[keys[0]] is not None ): # Needs to be Config head = getattr(head, keys[0]) keys = keys[1:] else: - head[keys[0]] = config() + head[keys[0]] = Config() head = head[keys[0]] keys = keys[1:] if len(keys) == 1: @@ -253,20 +243,17 @@ def __split_params__(params: argparse.Namespace, _config: "config"): @staticmethod def __parse_args__( - args: List[str], parser: argparse.ArgumentParser = None, strict: bool = False + args: list[str], parser: argparse.ArgumentParser = None, strict: bool = False ) -> argparse.Namespace: """Parses the passed args use the passed parser. Args: - args (List[str]): - List of arguments to parse. - parser (argparse.ArgumentParser): - Command line parser object. - strict (bool): - If ``true``, the command line arguments are strictly parsed. + args (list[str]): List of arguments to parse. + parser (argparse.ArgumentParser): Command line parser object. + strict (bool): If ``true``, the command line arguments are strictly parsed. + Returns: - Namespace: - Namespace object created from parser arguments. + Namespace: Namespace object created from parser arguments. """ if not strict: params, unrecognized = parser.parse_known_args(args=args) @@ -281,11 +268,11 @@ def __parse_args__( return params - def __deepcopy__(self, memo) -> "config": + def __deepcopy__(self, memo) -> "Config": _default = self.__default__ config_state = self.__getstate__() - config_copy = config() + config_copy = Config() memo[id(self)] = config_copy config_copy.__setstate__(config_state) @@ -306,7 +293,7 @@ def _remove_private_keys(d): d.pop("__is_set", None) for k, v in list(d.items()): if isinstance(v, dict): - config._remove_private_keys(v) + Config._remove_private_keys(v) return d def __str__(self) -> str: @@ -314,14 +301,15 @@ def __str__(self) -> str: visible = copy.deepcopy(self.toDict()) visible.pop("__parser", None) visible.pop("__is_set", None) - cleaned = config._remove_private_keys(visible) + cleaned = Config._remove_private_keys(visible) return "\n" + yaml.dump(cleaned, sort_keys=False) - def copy(self) -> "config": + def copy(self) -> "Config": return copy.deepcopy(self) - def to_string(self, items) -> str: - """Get string from items""" + @staticmethod + def to_string(items) -> str: + """Get string from items.""" return "\n" + yaml.dump(items.toDict()) def update_with_kwargs(self, kwargs): @@ -331,7 +319,8 @@ def update_with_kwargs(self, kwargs): @classmethod def _merge(cls, a, b): - """Merge two configurations recursively. + """ + Merge two configurations recursively. If there is a conflict, the value from the second configuration will take precedence. """ for key in b: @@ -344,28 +333,26 @@ def _merge(cls, a, b): a[key] = b[key] return a - def merge(self, b): + def merge(self, b: "Config"): """ Merges the current config with another config. Args: - b: Another config to merge. + b (bittensor.core.config.Config): Another config to merge. """ - self = self._merge(self, b) + self._merge(self, b) @classmethod - def merge_all(cls, configs: List["config"]) -> "config": + def merge_all(cls, configs: list["Config"]) -> "Config": """ Merge all configs in the list into one config. If there is a conflict, the value from the last configuration in the list will take precedence. Args: - configs (list of config): - List of configs to be merged. + configs (list[bittensor.core.config.Config]): List of configs to be merged. Returns: - config: - Merged config object. + config (bittensor.core.config.Config): Merged config object. """ result = cls() for cfg in configs: @@ -373,23 +360,21 @@ def merge_all(cls, configs: List["config"]) -> "config": return result def is_set(self, param_name: str) -> bool: - """ - Returns a boolean indicating whether the parameter has been set or is still the default. - """ + """Returns a boolean indicating whether the parameter has been set or is still the default.""" if param_name not in self.get("__is_set"): return False else: return self.get("__is_set")[param_name] def __check_for_missing_required_args( - self, parser: argparse.ArgumentParser, args: List[str] - ) -> List[str]: + self, parser: argparse.ArgumentParser, args: list[str] + ) -> list[str]: required_args = self.__get_required_args_from_parser(parser) missing_args = [arg for arg in required_args if not any(arg in s for s in args)] return missing_args @staticmethod - def __get_required_args_from_parser(parser: argparse.ArgumentParser) -> List[str]: + def __get_required_args_from_parser(parser: argparse.ArgumentParser) -> list[str]: required_args = [] for action in parser._actions: if action.required: @@ -402,14 +387,10 @@ def __get_required_args_from_parser(parser: argparse.ArgumentParser) -> List[str T = TypeVar("T", bound="DefaultConfig") -class DefaultConfig(config): - """ - A Config with a set of default values. - """ +class DefaultConfig(Config): + """A Config with a set of default values.""" @classmethod def default(cls: Type[T]) -> T: - """ - Get default config. - """ + """Get default config.""" raise NotImplementedError("Function default is not implemented.") diff --git a/bittensor/dendrite.py b/bittensor/core/dendrite.py similarity index 63% rename from bittensor/dendrite.py rename to bittensor/core/dendrite.py index 683ac595a..0b9bc5381 100644 --- a/bittensor/dendrite.py +++ b/bittensor/core/dendrite.py @@ -1,38 +1,52 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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. -# Standard Library from __future__ import annotations + import asyncio import time -from typing import Optional, List, Union, AsyncGenerator, Any import uuid +from typing import Any, AsyncGenerator, Optional, Union, Type -# 3rd Party import aiohttp -from aiohttp import ClientTimeout - -# Application -import bittensor -from bittensor.constants import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR +from bittensor_wallet import Wallet +from substrateinterface import Keypair + +from bittensor.core.axon import Axon +from bittensor.core.chain_data import AxonInfo +from bittensor.core.settings import version_as_int +from bittensor.core.stream import StreamingSynapse +from bittensor.core.synapse import Synapse, TerminalInfo +from bittensor.utils import networking +from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch +DENDRITE_ERROR_MAPPING: dict[Type[Exception], tuple] = { + aiohttp.ClientConnectorError: ("503", "Service unavailable"), + asyncio.TimeoutError: ("408", "Request timeout"), + aiohttp.ClientResponseError: (None, "Client response error"), + aiohttp.ClientPayloadError: ("400", "Payload error"), + aiohttp.ClientError: ("500", "Client error"), + aiohttp.ServerTimeoutError: ("504", "Server timeout error"), + aiohttp.ServerDisconnectedError: ("503", "Service disconnected"), + aiohttp.ServerConnectionError: ("503", "Service connection error"), +} +DENDRITE_DEFAULT_ERROR = ("422", "Failed to parse response") + class DendriteMixin: """ @@ -40,75 +54,57 @@ class DendriteMixin: In the brain analogy, dendrites receive signals from other neurons (in this case, network servers or axons), and the Dendrite class here is designed - to send requests to those endpoint to recieve inputs. + to send requests to those endpoint to receive inputs. This class includes a wallet or keypair used for signing messages, and methods for making HTTP requests to the network servers. It also provides functionalities such as logging network requests and processing server responses. Args: - keypair: The wallet or keypair used for signing messages. + keypair (Option[Union[bittensor_wallet.Wallet, substrateinterface.Keypair]]): The wallet or keypair used for signing messages. external_ip (str): The external IP address of the local system. synapse_history (list): A list of Synapse objects representing the historical responses. Methods: __str__(): Returns a string representation of the Dendrite object. __repr__(): Returns a string representation of the Dendrite object, acting as a fallback for __str__(). - query(self, *args, **kwargs) -> Union[bittensor.Synapse, List[bittensor.Synapse]]: - Makes synchronous requests to one or multiple target Axons and returns responses. - - forward(self, axons, synapse=bittensor.Synapse(), timeout=12, deserialize=True, run_async=True, streaming=False) -> bittensor.Synapse: - Asynchronously sends requests to one or multiple Axons and collates their responses. - - call(self, target_axon, synapse=bittensor.Synapse(), timeout=12.0, deserialize=True) -> bittensor.Synapse: - Asynchronously sends a request to a specified Axon and processes the response. - - call_stream(self, target_axon, synapse=bittensor.Synapse(), timeout=12.0, deserialize=True) -> AsyncGenerator[bittensor.Synapse, None]: - Sends a request to a specified Axon and yields an AsyncGenerator that contains streaming response chunks before finally yielding the filled Synapse as the final element. - - preprocess_synapse_for_request(self, target_axon_info, synapse, timeout=12.0) -> bittensor.Synapse: - Preprocesses the synapse for making a request, including building headers and signing. - - process_server_response(self, server_response, json_response, local_synapse): - Processes the server response, updates the local synapse state, and merges headers. - - close_session(self): - Synchronously closes the internal aiohttp client session. - - aclose_session(self): - Asynchronously closes the internal aiohttp client session. + query(self, *args, **kwargs) -> Union[Synapse, list[Synapse]]: Makes synchronous requests to one or multiple target Axons and returns responses. + forward(self, axons, synapse=Synapse(), timeout=12, deserialize=True, run_async=True, streaming=False) -> Synapse: Asynchronously sends requests to one or multiple Axons and collates their responses. + call(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> Synapse: Asynchronously sends a request to a specified Axon and processes the response. + call_stream(self, target_axon, synapse=Synapse(), timeout=12.0, deserialize=True) -> AsyncGenerator[Synapse, None]: Sends a request to a specified Axon and yields an AsyncGenerator that contains streaming response chunks before finally yielding the filled Synapse as the final element. + preprocess_synapse_for_request(self, target_axon_info, synapse, timeout=12.0) -> Synapse: Preprocesses the synapse for making a request, including building headers and signing. + process_server_response(self, server_response, json_response, local_synapse): Processes the server response, updates the local synapse state, and merges headers. + close_session(self): Synchronously closes the internal aiohttp client session. + aclose_session(self): Asynchronously closes the internal aiohttp client session. NOTE: When working with async `aiohttp `_ client sessions, it is recommended to use a context manager. Example with a context manager:: - >>> aysnc with dendrite(wallet = bittensor.wallet()) as d: - >>> print(d) - >>> d( ) # ping axon - >>> d( [] ) # ping multiple - >>> d( bittensor.axon(), bittensor.Synapse ) + async with dendrite(wallet = bittensor_wallet.Wallet()) as d: + print(d) + d( ) # ping axon + d( [] ) # ping multiple + d( Axon(), Synapse ) However, you are able to safely call :func:`dendrite.query()` without a context manager in a synchronous setting. Example without a context manager:: - >>> d = dendrite(wallet = bittensor.wallet() ) - >>> print(d) - >>> d( ) # ping axon - >>> d( [] ) # ping multiple - >>> d( bittensor.axon(), bittensor.Synapse ) + d = dendrite(wallet = bittensor_wallet.Wallet() ) + print(d) + d( ) # ping axon + d( [] ) # ping multiple + d( bittensor.core.axon.Axon, bittensor.core.synapse.Synapse ) """ - def __init__( - self, wallet: Optional[Union[bittensor.wallet, bittensor.Keypair]] = None - ): + def __init__(self, wallet: Optional[Union["Wallet", "Keypair"]] = None): """ Initializes the Dendrite object, setting up essential properties. Args: - wallet (Optional[Union['bittensor.wallet', 'bittensor.keypair']], optional): - The user's wallet or keypair used for signing messages. Defaults to ``None``, in which case a new :func:`bittensor.wallet().hotkey` is generated and used. + wallet (Optional[Union[bittensor_wallet.Wallet, substrateinterface.Keypair]]): The user's wallet or keypair used for signing messages. Defaults to ``None``, in which case a new :func:`bittensor_wallet.Wallet().hotkey` is generated and used. """ # Initialize the parent class super(DendriteMixin, self).__init__() @@ -117,12 +113,12 @@ def __init__( self.uuid = str(uuid.uuid1()) # Get the external IP - self.external_ip = bittensor.utils.networking.get_external_ip() + self.external_ip = networking.get_external_ip() # If a wallet or keypair is provided, use its hotkey. If not, generate a new one. self.keypair = ( - wallet.hotkey if isinstance(wallet, bittensor.wallet) else wallet - ) or bittensor.wallet().hotkey + wallet.hotkey if isinstance(wallet, Wallet) else wallet + ) or Wallet().hotkey self.synapse_history: list = [] @@ -147,17 +143,17 @@ async def session(self) -> aiohttp.ClientSession: Example usage:: - import bittensor as bt # Import bittensor - wallet = bt.wallet( ... ) # Initialize a wallet - dendrite = bt.dendrite( wallet ) # Initialize a dendrite instance with the wallet + import bittensor # Import bittensor + wallet = bittensor.Wallet( ... ) # Initialize a wallet + dendrite = bittensor.Dendrite(wallet=wallet) # Initialize a dendrite instance with the wallet - async with (await dendrite.session).post( # Use the session to make an HTTP POST request - url, # URL to send the request to - headers={...}, # Headers dict to be sent with the request - json={...}, # JSON body data to be sent with the request - timeout=10, # Timeout duration in seconds + async with (await dendrite.session).post( # Use the session to make an HTTP POST request + url, # URL to send the request to + headers={...}, # Headers dict to be sent with the request + json={...}, # JSON body data to be sent with the request + timeout=10, # Timeout duration in seconds ) as response: - json_response = await response.json() # Extract the JSON response from the server + json_response = await response.json() # Extract the JSON response from the server """ if self._session is None: @@ -193,16 +189,17 @@ async def aclose_session(self): releases resources associated with the session, such as open connections and internal buffers, which is essential for resource management in asynchronous applications. - Usage: - When finished with dendrite in an asynchronous context - await :func:`dendrite_instance.aclose_session()`. - - Example:: + Example: + Usage:: + When finished with dendrite in an asynchronous context + await :func:`dendrite_instance.aclose_session()`. - async with dendrite_instance: - # Operations using dendrite - pass - # The session will be closed automatically after the above block + Example: + Usage:: + async with dendrite_instance: + # Operations using dendrite + pass + # The session will be closed automatically after the above block """ if self._session: await self._session.close() @@ -245,14 +242,14 @@ def log_exception(self, exception: Exception): """ error_id = str(uuid.uuid4()) error_type = exception.__class__.__name__ - bittensor.logging.error(f"{error_type}#{error_id}: {exception}") + logging.error(f"{error_type}#{error_id}: {exception}") def process_error_message( self, - synapse: Union[bittensor.Synapse, bittensor.StreamingSynapse], + synapse: Union["Synapse", "StreamingSynapse"], request_name: str, exception: Exception, - ) -> Union[bittensor.Synapse, bittensor.StreamingSynapse]: + ) -> Union["Synapse", "StreamingSynapse"]: """ Handles exceptions that occur during network requests, updating the synapse with appropriate status codes and messages. @@ -260,12 +257,12 @@ def process_error_message( message in the synapse object. It covers common network errors such as connection issues and timeouts. Args: - synapse: The synapse object associated with the request. - request_name: The name of the request during which the exception occurred. - exception: The exception object caught during the request. + synapse (bittensor.core.synapse.Synapse): The synapse object associated with the request. + request_name (str): The name of the request during which the exception occurred. + exception (Exception): The exception object caught during the request. Returns: - bittensor.Synapse: The updated synapse object with the error status code and message. + Synapse (bittensor.core.synapse.Synapse): The updated synapse object with the error status code and message. Note: This method updates the synapse object in-place. @@ -291,7 +288,7 @@ def process_error_message( return synapse - def _log_outgoing_request(self, synapse): + def _log_outgoing_request(self, synapse: "Synapse"): """ Logs information about outgoing requests for debugging purposes. @@ -301,17 +298,20 @@ def _log_outgoing_request(self, synapse): To turn on debug messages, set the environment variable BITTENSOR_DEBUG to ``1``, or call the bittensor debug method like so:: + Example:: + import bittensor bittensor.debug() Args: - synapse: The synapse object representing the request being sent. + synapse (bittensor.core.synapse.Synapse): The synapse object representing the request being sent. """ - bittensor.logging.trace( - f"dendrite | --> | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | 0 | Success" - ) + if synapse.axon is not None: + logging.trace( + f"dendrite | --> | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | 0 | Success" + ) - def _log_incoming_response(self, synapse): + def _log_incoming_response(self, synapse: "Synapse"): """ Logs information about incoming responses for debugging and monitoring. @@ -320,30 +320,28 @@ def _log_incoming_response(self, synapse): This logging is vital for troubleshooting and understanding the network interactions in Bittensor. Args: - synapse: The synapse object representing the received response. + synapse (bittensor.core.synapse.Synapse): The synapse object representing the received response. """ - bittensor.logging.trace( - f"dendrite | <-- | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | {synapse.dendrite.status_code} | {synapse.dendrite.status_message}" - ) + if synapse.axon is not None and synapse.dendrite is not None: + logging.trace( + f"dendrite | <-- | {synapse.get_total_size()} B | {synapse.name} | {synapse.axon.hotkey} | {synapse.axon.ip}:{str(synapse.axon.port)} | {synapse.dendrite.status_code} | {synapse.dendrite.status_message}" + ) def query( self, *args, **kwargs - ) -> List[ - Union[AsyncGenerator[Any, Any], bittensor.Synapse, bittensor.StreamingSynapse] - ]: + ) -> list[Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]]: """ Makes a synchronous request to multiple target Axons and returns the server responses. Cleanup is automatically handled and sessions are closed upon completed requests. Args: - axons (Union[List[Union['bittensor.AxonInfo', 'bittensor.axon']], Union['bittensor.AxonInfo', 'bittensor.axon']]): - The list of target Axon information. - synapse (bittensor.Synapse, optional): The Synapse object. Defaults to :func:`bittensor.Synapse()`. - timeout (float, optional): The request timeout duration in seconds. - Defaults to ``12.0`` seconds. + axons (Union[list[Union[bittensor.core.chain_data.axon_info.AxonInfo, 'bittensor.core.axon.Axon']], Union['bittensor.core.chain_data.axon_info.AxonInfo', 'bittensor.core.axon.Axon']]): The list of target Axon information. + synapse (Optional[bittensor.core.synapse.Synapse]): The Synapse object. Defaults to :func:`Synapse()`. + timeout (Optional[float]): The request timeout duration in seconds. Defaults to ``12.0`` seconds. + Returns: - Union[bittensor.Synapse, List[bittensor.Synapse]]: If a single target axon is provided, returns the response from that axon. If multiple target axons are provided, returns a list of responses from all target axons. + Union[bittensor.core.synapse.Synapse, list[bittensor.core.synapse.Synapse]]: If a single target axon is provided, returns the response from that axon. If multiple target axons are provided, returns a list of responses from all target axons. """ result = None try: @@ -352,7 +350,7 @@ def query( except Exception: new_loop = asyncio.new_event_loop() asyncio.set_event_loop(new_loop) - result = loop.run_until_complete(self.forward(*args, **kwargs)) + result = new_loop.run_until_complete(self.forward(*args, **kwargs)) new_loop.close() finally: self.close_session() @@ -360,18 +358,13 @@ def query( async def forward( self, - axons: Union[ - List[Union[bittensor.AxonInfo, bittensor.axon]], - Union[bittensor.AxonInfo, bittensor.axon], - ], - synapse: bittensor.Synapse = bittensor.Synapse(), + axons: Union[list[Union["AxonInfo", "Axon"]], Union["AxonInfo", "Axon"]], + synapse: "Synapse" = Synapse(), timeout: float = 12, deserialize: bool = True, run_async: bool = True, streaming: bool = False, - ) -> List[ - Union[AsyncGenerator[Any, Any], bittensor.Synapse, bittensor.StreamingSynapse] - ]: + ) -> list[Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]]: """ Asynchronously sends requests to one or multiple Axons and collates their responses. @@ -386,12 +379,15 @@ async def forward( For example:: - >>> ... - >>> wallet = bittensor.wallet() # Initialize a wallet - >>> synapse = bittensor.Synapse(...) # Create a synapse object that contains query data - >>> dendrte = bittensor.dendrite(wallet = wallet) # Initialize a dendrite instance - >>> axons = metagraph.axons # Create a list of axons to query - >>> responses = await dendrite(axons, synapse) # Send the query to all axons and await the responses + ... + import bittensor + wallet = bittensor.Wallet() # Initialize a wallet + synapse = bittensor.Synapse(...) # Create a synapse object that contains query data + dendrite = bittensor.Dendrite(wallet = wallet) # Initialize a dendrite instance + netuid = ... # Provide subnet ID + metagraph = bittensor.Metagraph(netuid) # Initialize a metagraph instance + axons = metagraph.axons # Create a list of axons to query + responses = await dendrite(axons, synapse) # Send the query to all axons and await the responses When querying an Axon that sends back data in chunks using the Dendrite, this function returns an AsyncGenerator that yields each chunk as it is received. The generator can be @@ -399,23 +395,22 @@ async def forward( For example:: - >>> ... - >>> dendrte = bittensor.dendrite(wallet = wallet) - >>> async for chunk in dendrite.forward(axons, synapse, timeout, deserialize, run_async, streaming): - >>> # Process each chunk here - >>> print(chunk) + ... + dendrite = bittensor.Dendrite(wallet = wallet) + async for chunk in dendrite.forward(axons, synapse, timeout, deserialize, run_async, streaming): + # Process each chunk here + print(chunk) Args: - axons (Union[List[Union['bittensor.AxonInfo', 'bittensor.axon']], Union['bittensor.AxonInfo', 'bittensor.axon']]): - The target Axons to send requests to. Can be a single Axon or a list of Axons. - synapse (bittensor.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`bittensor.Synapse` instance. - timeout (float, optional): Maximum duration to wait for a response from an Axon in seconds. Defaults to ``12.0``. - deserialize (bool, optional): Determines if the received response should be deserialized. Defaults to ``True``. - run_async (bool, optional): If ``True``, sends requests concurrently. Otherwise, sends requests sequentially. Defaults to ``True``. - streaming (bool, optional): Indicates if the response is expected to be in streaming format. Defaults to ``False``. + axons (Union[list[Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]], Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]]): The target Axons to send requests to. Can be a single Axon or a list of Axons. + synapse (bittensor.core.synapse.Synapse): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. + timeout (float): Maximum duration to wait for a response from an Axon in seconds. Defaults to ``12.0``. + deserialize (bool): Determines if the received response should be deserialized. Defaults to ``True``. + run_async (bool): If ``True``, sends requests concurrently. Otherwise, sends requests sequentially. Defaults to ``True``. + streaming (bool): Indicates if the response is expected to be in streaming format. Defaults to ``False``. Returns: - Union[AsyncGenerator, bittensor.Synapse, List[bittensor.Synapse]]: If a single Axon is targeted, returns its response. + Union[AsyncGenerator, bittensor.core.synapse.Synapse, list[bittensor.core.synapse.Synapse]]: If a single `Axon` is targeted, returns its response. If multiple Axons are targeted, returns a list of their responses. """ is_list = True @@ -425,20 +420,16 @@ async def forward( axons = [axons] # Check if synapse is an instance of the StreamingSynapse class or if streaming flag is set. - is_streaming_subclass = issubclass( - synapse.__class__, bittensor.StreamingSynapse - ) + is_streaming_subclass = issubclass(synapse.__class__, StreamingSynapse) if streaming != is_streaming_subclass: - bittensor.logging.warning( + logging.warning( f"Argument streaming is {streaming} while issubclass(synapse, StreamingSynapse) is {synapse.__class__.__name__}. This may cause unexpected behavior." ) streaming = is_streaming_subclass or streaming async def query_all_axons( is_stream: bool, - ) -> Union[ - AsyncGenerator[Any, Any], bittensor.Synapse, bittensor.StreamingSynapse - ]: + ) -> Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]: """ Handles the processing of requests to all targeted axons, accommodating both streaming and non-streaming responses. @@ -451,32 +442,22 @@ async def query_all_axons( If ``True``, responses are handled in streaming mode. Returns: - List[Union[AsyncGenerator, bittensor.Synapse, bittensor.StreamingSynapse]]: A list - containing the responses from each axon. The type of each response depends on the - streaming mode and the type of synapse used. + list[Union[AsyncGenerator, bittensor.core.synapse.Synapse, bittensor.core.stream.StreamingSynapse]]: A list containing the responses from each axon. The type of each response depends on the streaming mode and the type of synapse used. """ async def single_axon_response( - target_axon, - ) -> Union[ - AsyncGenerator[Any, Any], bittensor.Synapse, bittensor.StreamingSynapse - ]: + target_axon: Union["AxonInfo", "Axon"], + ) -> Union["AsyncGenerator[Any, Any]", "Synapse", "StreamingSynapse"]: """ Manages the request and response process for a single axon, supporting both streaming and non-streaming modes. - This function is responsible for initiating a request to a single axon. Depending on the - ``is_stream`` flag, it either uses ``call_stream`` for streaming responses or ``call`` for - standard responses. The function handles the response processing, catering to the specifics - of streaming or non-streaming data. + This function is responsible for initiating a request to a single axon. Depending on the ``is_stream`` flag, it either uses ``call_stream`` for streaming responses or ``call`` for standard responses. The function handles the response processing, catering to the specifics of streaming or non-streaming data. Args: - target_axon: The target axon object to which the request is to be sent. This object contains the necessary information like IP address and port to formulate the request. + target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon): The target axon object to which the request is to be sent. This object contains the necessary information like IP address and port to formulate the request. Returns: - Union[AsyncGenerator, bittensor.Synapse, bittensor.StreamingSynapse]: The response - from the targeted axon. In streaming mode, an AsyncGenerator is returned, yielding - data chunks. In non-streaming mode, a Synapse or StreamingSynapse object is returned - containing the response. + Union[AsyncGenerator, bittensor.core.synapse.Synapse, bittensor.core.stream.StreamingSynapse]: The response from the targeted axon. In streaming mode, an AsyncGenerator is returned, yielding data chunks. In non-streaming mode, a Synapse or StreamingSynapse object is returned containing the response. """ if is_stream: # If in streaming mode, return the async_generator @@ -512,34 +493,30 @@ async def single_axon_response( async def call( self, - target_axon: Union[bittensor.AxonInfo, bittensor.axon], - synapse: bittensor.Synapse = bittensor.Synapse(), + target_axon: Union["AxonInfo", "Axon"], + synapse: "Synapse" = Synapse(), timeout: float = 12.0, deserialize: bool = True, - ) -> bittensor.Synapse: + ) -> "Synapse": """ Asynchronously sends a request to a specified Axon and processes the response. - This function establishes a connection with a specified Axon, sends the encapsulated - data through the Synapse object, waits for a response, processes it, and then - returns the updated Synapse object. + This function establishes a connection with a specified Axon, sends the encapsulated data through the Synapse object, waits for a response, processes it, and then returns the updated Synapse object. Args: - target_axon (Union['bittensor.AxonInfo', 'bittensor.axon']): The target Axon to send the request to. - synapse (bittensor.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`bittensor.Synapse` instance. - timeout (float, optional): Maximum duration to wait for a response from the Axon in seconds. Defaults to ``12.0``. - deserialize (bool, optional): Determines if the received response should be deserialized. Defaults to ``True``. + target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]): The target Axon to send the request to. + synapse (bittensor.core.synapse.Synapse): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. + timeout (float): Maximum duration to wait for a response from the Axon in seconds. Defaults to ``12.0``. + deserialize (bool): Determines if the received response should be deserialized. Defaults to ``True``. Returns: - bittensor.Synapse: The Synapse object, updated with the response data from the Axon. + bittensor.core.synapse.Synapse: The Synapse object, updated with the response data from the Axon. """ # Record start time start_time = time.time() target_axon = ( - target_axon.info() - if isinstance(target_axon, bittensor.axon) - else target_axon + target_axon.info() if isinstance(target_axon, Axon) else target_axon ) # Build request endpoint from the synapse class @@ -555,10 +532,10 @@ async def call( # Make the HTTP POST request async with (await self.session).post( - url, + url=url, headers=synapse.to_headers(), json=synapse.model_dump(), - timeout=ClientTimeout(total=timeout), + timeout=aiohttp.ClientTimeout(total=timeout), ) as response: # Extract the JSON response from the server json_response = await response.json() @@ -575,20 +552,18 @@ async def call( self._log_incoming_response(synapse) # Log synapse event history - self.synapse_history.append( - bittensor.Synapse.from_headers(synapse.to_headers()) - ) + self.synapse_history.append(Synapse.from_headers(synapse.to_headers())) # Return the updated synapse object after deserializing if requested return synapse.deserialize() if deserialize else synapse async def call_stream( self, - target_axon: Union[bittensor.AxonInfo, bittensor.axon], - synapse: bittensor.StreamingSynapse = bittensor.Synapse(), # type: ignore + target_axon: Union["AxonInfo", "Axon"], + synapse: "StreamingSynapse" = Synapse(), # type: ignore timeout: float = 12.0, deserialize: bool = True, - ) -> AsyncGenerator[Any, Any]: + ) -> "AsyncGenerator[Any, Any]": """ Sends a request to a specified Axon and yields streaming responses. @@ -598,22 +573,20 @@ async def call_stream( data to be transmitted. Args: - target_axon (Union['bittensor.AxonInfo', 'bittensor.axon']): The target Axon to send the request to. - synapse (bittensor.Synapse, optional): The Synapse object encapsulating the data. Defaults to a new :func:`bittensor.Synapse` instance. - timeout (float, optional): Maximum duration to wait for a response (or a chunk of the response) from the Axon in seconds. Defaults to ``12.0``. - deserialize (bool, optional): Determines if each received chunk should be deserialized. Defaults to ``True``. + target_axon (Union[bittensor.core.chain_data.axon_info.AxonInfo, bittensor.core.axon.Axon]): The target Axon to send the request to. + synapse (bittensor.core.synapse.Synapse): The Synapse object encapsulating the data. Defaults to a new :func:`Synapse` instance. + timeout (float): Maximum duration to wait for a response (or a chunk of the response) from the Axon in seconds. Defaults to ``12.0``. + deserialize (bool): Determines if each received chunk should be deserialized. Defaults to ``True``. Yields: object: Each yielded object contains a chunk of the arbitrary response data from the Axon. - bittensor.Synapse: After the AsyncGenerator has been exhausted, yields the final filled Synapse. + bittensor.core.synapse.Synapse: After the AsyncGenerator has been exhausted, yields the final filled Synapse. """ # Record start time start_time = time.time() target_axon = ( - target_axon.info() - if isinstance(target_axon, bittensor.axon) - else target_axon + target_axon.info() if isinstance(target_axon, Axon) else target_axon ) # Build request endpoint from the synapse class @@ -637,7 +610,7 @@ async def call_stream( url, headers=synapse.to_headers(), json=synapse.model_dump(), - timeout=ClientTimeout(total=timeout), + timeout=aiohttp.ClientTimeout(total=timeout), ) as response: # Use synapse subclass' process_streaming_response method to yield the response chunks async for chunk in synapse.process_streaming_response(response): # type: ignore @@ -657,9 +630,7 @@ async def call_stream( self._log_incoming_response(synapse) # Log synapse event history - self.synapse_history.append( - bittensor.Synapse.from_headers(synapse.to_headers()) - ) + self.synapse_history.append(Synapse.from_headers(synapse.to_headers())) # Return the updated synapse object after deserializing if requested if deserialize: @@ -669,35 +640,33 @@ async def call_stream( def preprocess_synapse_for_request( self, - target_axon_info: bittensor.AxonInfo, - synapse: bittensor.Synapse, + target_axon_info: "AxonInfo", + synapse: "Synapse", timeout: float = 12.0, - ) -> bittensor.Synapse: + ) -> "Synapse": """ - Preprocesses the synapse for making a request. This includes building - headers for Dendrite and Axon and signing the request. + Preprocesses the synapse for making a request. This includes building headers for Dendrite and Axon and signing the request. Args: - target_axon_info (bittensor.AxonInfo): The target axon information. - synapse (bittensor.Synapse): The synapse object to be preprocessed. - timeout (float, optional): The request timeout duration in seconds. - Defaults to ``12.0`` seconds. + target_axon_info (bittensor.core.chain_data.axon_info.AxonInfo): The target axon information. + synapse (bittensor.core.synapse.Synapse): The synapse object to be preprocessed. + timeout (float): The request timeout duration in seconds. Defaults to ``12.0`` seconds. Returns: - bittensor.Synapse: The preprocessed synapse. + bittensor.core.synapse.Synapse: The preprocessed synapse. """ # Set the timeout for the synapse synapse.timeout = timeout - synapse.dendrite = bittensor.TerminalInfo( + synapse.dendrite = TerminalInfo( ip=self.external_ip, - version=bittensor.__version_as_int__, + version=version_as_int, nonce=time.time_ns(), uuid=self.uuid, hotkey=self.keypair.ss58_address, ) # Build the Axon headers using the target axon's details - synapse.axon = bittensor.TerminalInfo( + synapse.axon = TerminalInfo( ip=target_axon_info.ip, port=target_axon_info.port, hotkey=target_axon_info.hotkey, @@ -711,18 +680,17 @@ def preprocess_synapse_for_request( def process_server_response( self, - server_response: aiohttp.ClientResponse, + server_response: "aiohttp.ClientResponse", json_response: dict, - local_synapse: bittensor.Synapse, + local_synapse: "Synapse", ): """ - Processes the server response, updates the local synapse state with the - server's state and merges headers set by the server. + Processes the server response, updates the local synapse state with the server's state and merges headers set by the server. Args: server_response (object): The `aiohttp `_ response object from the server. json_response (dict): The parsed JSON response from the server. - local_synapse (bittensor.Synapse): The local synapse object to be updated. + local_synapse (bittensor.core.synapse.Synapse): The local synapse object to be updated. Raises: None: But errors in attribute setting are silently ignored. @@ -744,12 +712,12 @@ def process_server_response( else: # If the server responded with an error, update the local synapse state if local_synapse.axon is None: - local_synapse.axon = bittensor.TerminalInfo() + local_synapse.axon = TerminalInfo() local_synapse.axon.status_code = server_response.status local_synapse.axon.status_message = json_response.get("message") # Extract server headers and overwrite None values in local synapse headers - server_headers = bittensor.Synapse.from_headers(server_response.headers) # type: ignore + server_headers = Synapse.from_headers(server_response.headers) # type: ignore # Merge dendrite headers local_synapse.dendrite.__dict__.update( @@ -778,7 +746,7 @@ def __str__(self) -> str: Returns: str: The string representation of the Dendrite object in the format :func:`dendrite()`. """ - return "dendrite({})".format(self.keypair.ss58_address) + return f"dendrite({self.keypair.ss58_address})" def __repr__(self) -> str: """ @@ -793,14 +761,12 @@ async def __aenter__(self): """ Asynchronous context manager entry method. - Enables the use of the ``async with`` statement with the Dendrite instance. When entering the context, - the current instance of the class is returned, making it accessible within the asynchronous context. + Enables the use of the ``async with`` statement with the Dendrite instance. When entering the context, the current instance of the class is returned, making it accessible within the asynchronous context. Returns: Dendrite: The current instance of the Dendrite class. Usage:: - async with Dendrite() as dendrite: await dendrite.some_async_method() """ @@ -810,17 +776,18 @@ async def __aexit__(self, exc_type, exc_value, traceback): """ Asynchronous context manager exit method. - Ensures proper cleanup when exiting the ``async with`` context. This method will close the `aiohttp `_ client session - asynchronously, releasing any tied resources. + Ensures proper cleanup when exiting the ``async with`` context. This method will close the `aiohttp `_ client session asynchronously, releasing any tied resources. Args: - exc_type (Type[BaseException], optional): The type of exception that was raised. - exc_value (BaseException, optional): The instance of exception that was raised. - traceback (TracebackType, optional): A traceback object encapsulating the call stack at the point where the exception was raised. + exc_type (Type[BaseException]): The type of exception that was raised. + exc_value (BaseException): The instance of exception that was raised. + traceback (TracebackType): A traceback object encapsulating the call stack at the point where the exception was raised. Usage:: + import bittensor - async with bt.dendrite( wallet ) as dendrite: + wallet = bittensor.Wallet() + async with bittensor.Dendrite(wallet=wallet) as dendrite: await dendrite.some_async_method() Note: @@ -832,8 +799,7 @@ def __del__(self): """ Dendrite destructor. - This method is invoked when the Dendrite instance is about to be destroyed. The destructor ensures that the - aiohttp client session is closed before the instance is fully destroyed, releasing any remaining resources. + This method is invoked when the Dendrite instance is about to be destroyed. The destructor ensures that the aiohttp client session is closed before the instance is fully destroyed, releasing any remaining resources. Note: Relying on the destructor for cleanup can be unpredictable. It is recommended to explicitly close sessions using the provided methods or the ``async with`` context manager. @@ -851,10 +817,8 @@ def __del__(self): BaseModel: Union["torch.nn.Module", object] = torch.nn.Module if use_torch() else object -class dendrite(DendriteMixin, BaseModel): # type: ignore - def __init__( - self, wallet: Optional[Union[bittensor.wallet, bittensor.Keypair]] = None - ): +class Dendrite(DendriteMixin, BaseModel): # type: ignore + def __init__(self, wallet: Optional[Union["Wallet", "Keypair"]] = None): if use_torch(): torch.nn.Module.__init__(self) DendriteMixin.__init__(self, wallet) @@ -865,4 +829,4 @@ def __init__( async def call(self, *args, **kwargs): return await self.forward(*args, **kwargs) - dendrite.__call__ = call + Dendrite.__call__ = call diff --git a/bittensor/errors.py b/bittensor/core/errors.py similarity index 51% rename from bittensor/errors.py rename to bittensor/core/errors.py index b8366ee68..6fd9729e8 100644 --- a/bittensor/errors.py +++ b/bittensor/core/errors.py @@ -1,185 +1,129 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Foundation - +# 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. -from __future__ import annotations -import typing +from __future__ import annotations -if typing.TYPE_CHECKING: - import bittensor +from bittensor.core.synapse import Synapse class ChainError(BaseException): - r"""Base error for any chain related errors.""" - - pass + """Base error for any chain related errors.""" class ChainConnectionError(ChainError): - r"""Error for any chain connection related errors.""" - - pass + """Error for any chain connection related errors.""" class ChainTransactionError(ChainError): - r"""Error for any chain transaction related errors.""" - - pass + """Error for any chain transaction related errors.""" class ChainQueryError(ChainError): - r"""Error for any chain query related errors.""" - - pass + """Error for any chain query related errors.""" class StakeError(ChainTransactionError): - r"""Error raised when a stake transaction fails.""" - - pass + """Error raised when a stake transaction fails.""" class UnstakeError(ChainTransactionError): - r"""Error raised when an unstake transaction fails.""" - - pass + """Error raised when an unstake transaction fails.""" class IdentityError(ChainTransactionError): - r"""Error raised when an identity transaction fails.""" - - pass + """Error raised when an identity transaction fails.""" class NominationError(ChainTransactionError): - r"""Error raised when a nomination transaction fails.""" - - pass + """Error raised when a nomination transaction fails.""" class TakeError(ChainTransactionError): - r"""Error raised when a increase / decrease take transaction fails.""" - - pass + """Error raised when an increase / decrease take transaction fails.""" class TransferError(ChainTransactionError): - r"""Error raised when a transfer transaction fails.""" - - pass + """Error raised when a transfer transaction fails.""" class RegistrationError(ChainTransactionError): - r"""Error raised when a neuron registration transaction fails.""" - - pass + """Error raised when a neuron registration transaction fails.""" class NotRegisteredError(ChainTransactionError): - r"""Error raised when a neuron is not registered, and the transaction requires it to be.""" - - pass + """Error raised when a neuron is not registered, and the transaction requires it to be.""" class NotDelegateError(StakeError): - r"""Error raised when a hotkey you are trying to stake to is not a delegate.""" - - pass - - -class KeyFileError(Exception): - """Error thrown when the keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid.""" - - pass + """Error raised when a hotkey you are trying to stake to is not a delegate.""" class MetadataError(ChainTransactionError): - r"""Error raised when metadata commitment transaction fails.""" - - pass + """Error raised when metadata commitment transaction fails.""" class InvalidRequestNameError(Exception): - r"""This exception is raised when the request name is invalid. Ususally indicates a broken URL.""" - - pass + """This exception is raised when the request name is invalid. Usually indicates a broken URL.""" class SynapseException(Exception): - def __init__( - self, message="Synapse Exception", synapse: "bittensor.Synapse" | None = None - ): + def __init__(self, message="Synapse Exception", synapse: "Synapse" | None = None): self.message = message self.synapse = synapse super().__init__(self.message) class UnknownSynapseError(SynapseException): - r"""This exception is raised when the request name is not found in the Axon's forward_fns dictionary.""" - - pass + """This exception is raised when the request name is not found in the Axon's forward_fns dictionary.""" class SynapseParsingError(Exception): - r"""This exception is raised when the request headers are unable to be parsed into the synapse type.""" - - pass + """This exception is raised when the request headers are unable to be parsed into the synapse type.""" class NotVerifiedException(SynapseException): - r"""This exception is raised when the request is not verified.""" - - pass + """This exception is raised when the request is not verified.""" class BlacklistedException(SynapseException): - r"""This exception is raised when the request is blacklisted.""" - - pass + """This exception is raised when the request is blacklisted.""" class PriorityException(SynapseException): - r"""This exception is raised when the request priority is not met.""" - - pass + """This exception is raised when the request priority is not met.""" class PostProcessException(SynapseException): - r"""This exception is raised when the response headers cannot be updated.""" - - pass + """This exception is raised when the response headers cannot be updated.""" class RunException(SynapseException): - r"""This exception is raised when the requested function cannot be executed. Indicates a server error.""" - - pass + """This exception is raised when the requested function cannot be executed. Indicates a server error.""" class InternalServerError(SynapseException): - r"""This exception is raised when the requested function fails on the server. Indicates a server error.""" - - pass + """This exception is raised when the requested function fails on the server. Indicates a server error.""" class SynapseDendriteNoneException(SynapseException): def __init__( self, message="Synapse Dendrite is None", - synapse: "bittensor.Synapse" | None = None, + synapse: "Synapse" | None = None, ): self.message = message super().__init__(self.message, synapse) diff --git a/bittensor/extrinsics/__init__.py b/bittensor/core/extrinsics/__init__.py similarity index 95% rename from bittensor/extrinsics/__init__.py rename to bittensor/core/extrinsics/__init__.py index 5780b2ee8..640a13250 100644 --- a/bittensor/extrinsics/__init__.py +++ b/bittensor/core/extrinsics/__init__.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Foundation - +# 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 diff --git a/bittensor/core/extrinsics/commit_weights.py b/bittensor/core/extrinsics/commit_weights.py new file mode 100644 index 000000000..5e9f2e9e1 --- /dev/null +++ b/bittensor/core/extrinsics/commit_weights.py @@ -0,0 +1,274 @@ +# 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. + +"""Module commit weights and reveal weights extrinsic.""" + +from typing import Optional, TYPE_CHECKING + +from retry import retry +from rich.prompt import Confirm + +from bittensor.core.extrinsics.utils import submit_extrinsic +from bittensor.utils import format_error_message +from bittensor.utils.btlogging import logging +from bittensor.utils.networking import ensure_connected + +# For annotation purposes +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + + +# # Chain call for `commit_weights_extrinsic` +@ensure_connected +def do_commit_weights( + self: "Subtensor", + wallet: "Wallet", + netuid: int, + commit_hash: str, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, Optional[dict]]: + """ + 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: + self (bittensor.core.subtensor.Subtensor): 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. + + Returns: + tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + 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. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + + return make_substrate_call_with_retry() + + +def commit_weights_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + commit_hash: str, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, +) -> tuple[bool, str]: + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This function is a wrapper around the `do_commit_weights` method, handling user prompts and error messages. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): 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. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. + + Returns: + tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. + """ + if prompt and not Confirm.ask(f"Would you like to commit weights?"): + return False, "User cancelled the operation." + + success, error_message = do_commit_weights( + self=subtensor, + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if success: + success_message = "Successfully committed weights." + logging.info(success_message) + return True, success_message + else: + error_message = format_error_message(error_message) + logging.error(f"Failed to commit weights: {error_message}") + return False, error_message + + +# Chain call for `reveal_weights_extrinsic` +@ensure_connected +def do_reveal_weights( + self: "Subtensor", + wallet: "Wallet", + netuid: int, + uids: list[int], + values: list[int], + salt: list[int], + version_key: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, Optional[dict]]: + """ + 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. + + Args: + self (bittensor.core.subtensor.Subtensor): 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. + values (list[int]): List of weight values corresponding to each UID. + salt (list[int]): List of salt values corresponding to the hash function. + 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. + + Returns: + tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency and accountability for the neuron's weight distribution. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": values, + "salt": salt, + "version_key": version_key, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + ) + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + + return make_substrate_call_with_retry() + + +def reveal_weights_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + uids: list[int], + weights: list[int], + salt: list[int], + version_key: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, +) -> 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, handling user prompts and error messages. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): 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. + weights (list[int]): List of weight values corresponding to each UID. + salt (list[int]): List of salt values corresponding to the hash function. + 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. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. + + Returns: + tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, 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. + """ + + if prompt and not Confirm.ask(f"Would you like to reveal weights?"): + return False, "User cancelled the operation." + + success, error_message = do_reveal_weights( + self=subtensor, + wallet=wallet, + netuid=netuid, + uids=uids, + values=weights, + salt=salt, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if success: + success_message = "Successfully revealed weights." + logging.info(success_message) + return True, success_message + else: + error_message = format_error_message(error_message) + logging.error(f"Failed to reveal weights: {error_message}") + return False, error_message diff --git a/bittensor/core/extrinsics/prometheus.py b/bittensor/core/extrinsics/prometheus.py new file mode 100644 index 000000000..a6ab1cfb1 --- /dev/null +++ b/bittensor/core/extrinsics/prometheus.py @@ -0,0 +1,187 @@ +# 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 json +from typing import Optional, TYPE_CHECKING + +from retry import retry + +from bittensor.core.extrinsics.utils import submit_extrinsic +from bittensor.core.settings import version_as_int, bt_console +from bittensor.utils import networking as net, format_error_message +from bittensor.utils.btlogging import logging +from bittensor.utils.networking import ensure_connected + +# For annotation purposes +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.subtensor import Subtensor + from bittensor.core.types import PrometheusServeCallParams + + +# Chain call for `prometheus_extrinsic` +@ensure_connected +def do_serve_prometheus( + self: "Subtensor", + wallet: "Wallet", + call_params: "PrometheusServeCallParams", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +) -> tuple[bool, Optional[dict]]: + """ + Sends a serve prometheus extrinsic to the chain. + + Args: + self (bittensor.core.subtensor.Subtensor): Bittensor subtensor object + wallet (bittensor_wallet.Wallet): Wallet object. + call_params (bittensor.core.types.PrometheusServeCallParams): Prometheus serve call parameters. + wait_for_inclusion (bool): If ``true``, waits for inclusion. + wait_for_finalization (bool): If ``true``, waits for finalization. + + Returns: + success (bool): ``True`` if serve prometheus was successful. + error (Optional[str]): Error message if serve prometheus failed, ``None`` otherwise. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="serve_prometheus", + call_params=call_params, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.hotkey + ) + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if wait_for_inclusion or wait_for_finalization: + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + else: + return True, None + + return make_substrate_call_with_retry() + + +def prometheus_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + port: int, + netuid: int, + ip: int = None, + wait_for_inclusion: bool = False, + wait_for_finalization=True, +) -> bool: + """Subscribes a Bittensor endpoint to the Subtensor chain. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Bittensor subtensor object. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + ip (str): Endpoint host port i.e., ``192.122.31.4``. + port (int): Endpoint port number i.e., `9221`. + netuid (int): Network `uid` to serve on. + 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. + + Returns: + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + """ + + # Get external ip + if ip is None: + try: + external_ip = net.get_external_ip() + bt_console.print( + f":white_heavy_check_mark: [green]Found external ip: {external_ip}[/green]" + ) + logging.success(prefix="External IP", suffix="{external_ip}") + except Exception as e: + raise RuntimeError( + f"Unable to attain your external ip. Check your internet connection. error: {e}" + ) from e + else: + external_ip = ip + + call_params: "PrometheusServeCallParams" = { + "version": version_as_int, + "ip": net.ip_to_int(external_ip), + "port": port, + "ip_type": net.ip_version(external_ip), + } + + with bt_console.status(":satellite: Checking Prometheus..."): + neuron = subtensor.get_neuron_for_pubkey_and_subnet( + wallet.hotkey.ss58_address, netuid=netuid + ) + neuron_up_to_date = not neuron.is_null and call_params == { + "version": neuron.prometheus_info.version, + "ip": net.ip_to_int(neuron.prometheus_info.ip), + "port": neuron.prometheus_info.port, + "ip_type": neuron.prometheus_info.ip_type, + } + + if neuron_up_to_date: + bt_console.print( + f":white_heavy_check_mark: [green]Prometheus already Served[/green]\n" + f"[green not bold]- Status: [/green not bold] |" + f"[green not bold] ip: [/green not bold][white not bold]{neuron.prometheus_info.ip}[/white not bold] |" + f"[green not bold] ip_type: [/green not bold][white not bold]{neuron.prometheus_info.ip_type}[/white not bold] |" + f"[green not bold] port: [/green not bold][white not bold]{neuron.prometheus_info.port}[/white not bold] | " + f"[green not bold] version: [/green not bold][white not bold]{neuron.prometheus_info.version}[/white not bold] |" + ) + + bt_console.print( + f":white_heavy_check_mark: [white]Prometheus already served.[/white]" + ) + return True + + # Add netuid, not in prometheus_info + call_params["netuid"] = netuid + + with bt_console.status( + f":satellite: Serving prometheus on: [white]{subtensor.network}:{netuid}[/white] ..." + ): + success, error_message = do_serve_prometheus( + self=subtensor, + wallet=wallet, + call_params=call_params, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + ) + + if wait_for_inclusion or wait_for_finalization: + if success is True: + json_ = json.dumps(call_params, indent=4, sort_keys=True) + bt_console.print( + f":white_heavy_check_mark: [green]Served prometheus[/green]\n [bold white]{json_}[/bold white]" + ) + return True + else: + bt_console.print( + f":cross_mark: [red]Failed[/red]: {format_error_message(error_message)}" + ) + return False + else: + return True diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py new file mode 100644 index 000000000..490f9c268 --- /dev/null +++ b/bittensor/core/extrinsics/serving.py @@ -0,0 +1,319 @@ +# 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 json +from typing import Optional, TYPE_CHECKING + +from retry import retry +from rich.prompt import Confirm + +from bittensor.core.errors import MetadataError +from bittensor.core.extrinsics.utils import submit_extrinsic +from bittensor.core.settings import version_as_int, bt_console +from bittensor.utils import format_error_message, networking as net +from bittensor.utils.btlogging import logging +from bittensor.utils.networking import ensure_connected + +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.axon import Axon + from bittensor.core.subtensor import Subtensor + from bittensor.core.types import AxonServeCallParams + from bittensor_wallet import Wallet + + +# Chain call for `serve_extrinsic` and `serve_axon_extrinsic` +@ensure_connected +def do_serve_axon( + self: "Subtensor", + wallet: "Wallet", + call_params: "AxonServeCallParams", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +) -> tuple[bool, Optional[dict]]: + """ + 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. + + Args: + self (bittensor.core.subtensor.Subtensor): Subtensor instance object. + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron. + 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. + + Returns: + tuple[bool, Optional[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. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="serve_axon", + call_params=call_params, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.hotkey + ) + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + if wait_for_inclusion or wait_for_finalization: + response.process_events() + if response.is_success: + return True, None + else: + return False, response.error_message + else: + return True, None + + return make_substrate_call_with_retry() + + +def serve_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + ip: str, + port: int, + protocol: int, + netuid: int, + placeholder1: int = 0, + placeholder2: int = 0, + wait_for_inclusion: bool = False, + wait_for_finalization=True, + prompt: bool = False, +) -> bool: + """Subscribes a Bittensor endpoint to the subtensor chain. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance object. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + ip (str): Endpoint host port i.e., ``192.122.31.4``. + port (int): Endpoint port number i.e., ``9221``. + protocol (int): An ``int`` representation of the protocol. + netuid (int): The network uid to serve on. + placeholder1 (int): A placeholder for future use. + placeholder2 (int): A placeholder for future use. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + + Returns: + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + """ + # Decrypt hotkey + wallet.unlock_hotkey() + params: "AxonServeCallParams" = { + "version": version_as_int, + "ip": net.ip_to_int(ip), + "port": port, + "ip_type": net.ip_version(ip), + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + "coldkey": wallet.coldkeypub.ss58_address, + "protocol": protocol, + "placeholder1": placeholder1, + "placeholder2": placeholder2, + } + logging.debug("Checking axon ...") + neuron = subtensor.get_neuron_for_pubkey_and_subnet( + wallet.hotkey.ss58_address, netuid=netuid + ) + neuron_up_to_date = not neuron.is_null and params == { + "version": neuron.axon_info.version, + "ip": net.ip_to_int(neuron.axon_info.ip), + "port": neuron.axon_info.port, + "ip_type": neuron.axon_info.ip_type, + "netuid": neuron.netuid, + "hotkey": neuron.hotkey, + "coldkey": neuron.coldkey, + "protocol": neuron.axon_info.protocol, + "placeholder1": neuron.axon_info.placeholder1, + "placeholder2": neuron.axon_info.placeholder2, + } + output = params.copy() + output["coldkey"] = wallet.coldkeypub.ss58_address + output["hotkey"] = wallet.hotkey.ss58_address + if neuron_up_to_date: + logging.debug( + f"Axon already served on: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) " + ) + return True + + if prompt: + output = params.copy() + output["coldkey"] = wallet.coldkeypub.ss58_address + output["hotkey"] = wallet.hotkey.ss58_address + if not Confirm.ask( + f"Do you want to serve axon:\n [bold white]{json.dumps(output, indent=4, sort_keys=True)}[/bold white]" + ): + return False + + logging.debug( + f"Serving axon with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) -> {subtensor.network}:{netuid}" + ) + success, error_message = do_serve_axon( + self=subtensor, + wallet=wallet, + call_params=params, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + ) + + 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: + return True + + +def serve_axon_extrinsic( + subtensor: "Subtensor", + netuid: int, + axon: "Axon", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +) -> bool: + """Serves the axon to the network. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor instance object. + netuid (int): The ``netuid`` being served on. + axon (bittensor.core.axon.Axon): Axon to serve. + 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. + + Returns: + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + """ + axon.wallet.unlock_hotkey() + axon.wallet.unlock_coldkeypub() + external_port = axon.external_port + + # ---- Get external ip ---- + if axon.external_ip is None: + try: + external_ip = net.get_external_ip() + bt_console.print( + f":white_heavy_check_mark: [green]Found external ip: {external_ip}[/green]" + ) + logging.success(prefix="External IP", suffix=f"{external_ip}") + except Exception as e: + raise RuntimeError( + f"Unable to attain your external ip. Check your internet connection. error: {e}" + ) from e + else: + external_ip = axon.external_ip + + # ---- Subscribe to chain ---- + serve_success = serve_extrinsic( + subtensor=subtensor, + wallet=axon.wallet, + ip=external_ip, + port=external_port, + netuid=netuid, + protocol=4, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + return serve_success + + +# Community uses this extrinsic directly and via `subtensor.commit` +@net.ensure_connected +def publish_metadata( + self: "Subtensor", + wallet: "Wallet", + netuid: int, + data_type: str, + data: bytes, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, +) -> bool: + """ + Publishes metadata on the Bittensor network using the specified wallet and network identifier. + + Args: + self (bittensor.core.subtensor.Subtensor): The subtensor instance representing the Bittensor blockchain connection. + wallet (bittensor_wallet.Wallet): The wallet object used for authentication in the transaction. + netuid (int): Network UID on which the metadata is to be published. + data_type (str): The data type of the information being submitted. It should be one of the following: ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing algorithm used for the data. + data (str): The actual metadata content to be published. This should be formatted or hashed according to the ``type`` specified. (Note: max ``str`` length is 128 bytes) + wait_for_inclusion (bool): If ``True``, the function will wait for the extrinsic to be included in a block before returning. Defaults to ``False``. + wait_for_finalization (bool): If ``True``, the function will wait for the extrinsic to be finalized on the chain before returning. Defaults to ``True``. + + 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 failure. + """ + + wallet.unlock_hotkey() + + with self.substrate as substrate: + call = substrate.compose_call( + call_module="Commitments", + call_function="set_commitment", + call_params={ + "netuid": netuid, + "info": {"fields": [[{f"{data_type}": data}]]}, + }, + ) + + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.hotkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True + response.process_events() + if response.is_success: + return True + else: + raise MetadataError(format_error_message(response.error_message)) + + +# Community uses this function directly +@net.ensure_connected +def get_metadata(self, netuid: int, hotkey: str, block: Optional[int] = None) -> str: + @retry(delay=2, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + with self.substrate as substrate: + return substrate.query( + module="Commitments", + storage_function="CommitmentOf", + params=[netuid, hotkey], + block_hash=None if block is None else substrate.get_block_hash(block), + ) + + commit_data = make_substrate_call_with_retry() + return commit_data.value diff --git a/bittensor/core/extrinsics/set_weights.py b/bittensor/core/extrinsics/set_weights.py new file mode 100644 index 000000000..7680061c5 --- /dev/null +++ b/bittensor/core/extrinsics/set_weights.py @@ -0,0 +1,194 @@ +# 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 logging +from typing import Union, Optional, TYPE_CHECKING + +import numpy as np +from numpy.typing import NDArray +from retry import retry +from rich.prompt import Confirm + +from bittensor.core.extrinsics.utils import submit_extrinsic +from bittensor.core.settings import bt_console, version_as_int +from bittensor.utils import format_error_message, weight_utils +from bittensor.utils.btlogging import logging +from bittensor.utils.networking import ensure_connected +from bittensor.utils.registration import torch, use_torch + +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + from bittensor_wallet import Wallet + + +# Chain call for `do_set_weights` +@ensure_connected +def do_set_weights( + self: "Subtensor", + wallet: "Wallet", + uids: list[int], + vals: list[int], + netuid: int, + version_key: int = version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +) -> tuple[bool, Optional[dict]]: # (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. + + Args: + self (bittensor.core.subtensor.Subtensor): Subtensor interface + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron setting the weights. + uids (list[int]): List of neuron UIDs for which weights are being set. + vals (list[int]): List of weight values corresponding to each UID. + netuid (int): Unique identifier for the network. + 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. + + Returns: + tuple[bool, Optional[str]]: A tuple containing a success flag and an optional response 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. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_weights", + call_params={ + "dests": uids, + "weights": vals, + "netuid": netuid, + "version_key": version_key, + }, + ) + # Period dictates how long the extrinsic will stay as part of waiting pool + extrinsic = self.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.hotkey, + era={"period": 5}, + ) + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # 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." + + response.process_events() + if response.is_success: + return True, "Successfully set weights." + else: + return False, response.error_message + + return make_substrate_call_with_retry() + + +# Community uses this extrinsic directly and via `subtensor.set_weights` +def set_weights_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + version_key: int = 0, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, +) -> tuple[bool, str]: + """Sets the given weights and values on chain for wallet hotkey account. + + Args: + subtensor (bittensor.core.subtensor.Subtensor): Subtensor endpoint to use. + wallet (bittensor_wallet.Wallet): Bittensor wallet object. + netuid (int): The ``netuid`` of the subnet to set weights for. + 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. + 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. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + + Returns: + tuple[bool, str]: A tuple containing a success flag and an optional response message. + """ + # First convert types. + if use_torch(): + if isinstance(uids, list): + uids = torch.tensor(uids, dtype=torch.int64) + if isinstance(weights, list): + weights = torch.tensor(weights, dtype=torch.float32) + else: + 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 + ) + + # Ask before moving on. + if prompt: + if not Confirm.ask( + f"Do you want to set weights:\n[bold white] weights: {[float(v / 65535) for v in weight_vals]}\n" + f"uids: {weight_uids}[/bold white ]?" + ): + return False, "Prompt refused." + + with bt_console.status( + f":satellite: Setting weights on [white]{subtensor.network}[/white] ..." + ): + try: + success, error_message = do_set_weights( + self=subtensor, + wallet=wallet, + netuid=netuid, + uids=weight_uids, + vals=weight_vals, + version_key=version_key, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, "Not waiting for finalization or inclusion." + + if success is True: + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") + logging.success( + msg=str(success), + prefix="Set weights", + suffix="Finalized: ", + ) + return True, "Successfully set weights and Finalized." + else: + error_message = format_error_message(error_message) + logging.error(error_message) + return False, error_message + + except Exception as e: + bt_console.print(f":cross_mark: [red]Failed[/red]: error:{e}") + logging.debug(str(e)) + return False, str(e) diff --git a/bittensor/core/extrinsics/transfer.py b/bittensor/core/extrinsics/transfer.py new file mode 100644 index 000000000..896fecbf9 --- /dev/null +++ b/bittensor/core/extrinsics/transfer.py @@ -0,0 +1,215 @@ +# 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. + +from typing import Optional, Union, TYPE_CHECKING + +from retry import retry +from rich.prompt import Confirm + +from bittensor.core.extrinsics.utils import submit_extrinsic +from bittensor.core.settings import bt_console, NETWORK_EXPLORER_MAP +from bittensor.utils import ( + get_explorer_url_for_network, + format_error_message, + is_valid_bittensor_address_or_public_key, +) +from bittensor.utils.balance import Balance +from bittensor.utils.networking import ensure_connected + +# For annotation purposes +if TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + from bittensor_wallet import Wallet + + +# Chain call for `transfer_extrinsic` +@ensure_connected +def do_transfer( + self: "Subtensor", + wallet: "Wallet", + dest: str, + transfer_balance: "Balance", + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, Optional[str], Optional[dict]]: + """Sends a transfer extrinsic to the chain. + + Args: + self (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (bittensor_wallet.Wallet): Wallet object. + dest (str): Destination public key address. + transfer_balance (bittensor.utils.balance.Balance): Amount to transfer. + wait_for_inclusion (bool): If ``true``, waits for inclusion. + wait_for_finalization (bool): If ``true``, waits for finalization. + + Returns: + success (bool): ``True`` if transfer was successful. + block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is ``True``. + error (dict): Error message from subtensor if transfer failed. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4) + def make_substrate_call_with_retry(): + call = self.substrate.compose_call( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": dest, "value": transfer_balance.rao}, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = submit_extrinsic( + substrate=self.substrate, + extrinsic=extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, None, None + + # Otherwise continue with finalization. + response.process_events() + if response.is_success: + block_hash = response.block_hash + return True, block_hash, None + else: + return False, None, response.error_message + + return make_substrate_call_with_retry() + + +# Community uses this extrinsic directly and via `subtensor.transfer` +def transfer_extrinsic( + subtensor: "Subtensor", + wallet: "Wallet", + dest: str, + amount: Union["Balance", float], + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + keep_alive: bool = True, + prompt: bool = False, +) -> bool: + """Transfers funds from this wallet to the destination public key address. + + Args: + subtensor (subtensor.core.subtensor.Subtensor): The Subtensor instance object. + wallet (bittensor_wallet.Wallet): Bittensor wallet object to make transfer from. + dest (str, ss58_address or ed25519): Destination public key address of receiver. + amount (Union[Balance, int]): Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. + 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. + keep_alive (bool): If set, keeps the account alive by keeping the balance above the existential deposit. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + + Returns: + success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. + """ + # Validate destination address. + if not is_valid_bittensor_address_or_public_key(dest): + bt_console.print( + f":cross_mark: [red]Invalid destination address[/red]:[bold white]\n {dest}[/bold white]" + ) + return False + + if isinstance(dest, bytes): + # Convert bytes to hex string. + dest = "0x" + dest.hex() + + # Unlock wallet coldkey. + wallet.unlock_coldkey() + + # Convert to bittensor.Balance + if not isinstance(amount, Balance): + transfer_balance = Balance.from_tao(amount) + else: + transfer_balance = amount + + # Check balance. + with bt_console.status(":satellite: Checking Balance..."): + account_balance = subtensor.get_balance(wallet.coldkey.ss58_address) + # check existential deposit. + existential_deposit = subtensor.get_existential_deposit() + + with bt_console.status(":satellite: Transferring..."): + fee = subtensor.get_transfer_fee( + wallet=wallet, dest=dest, value=transfer_balance.rao + ) + + if not keep_alive: + # Check if the transfer should keep_alive the account + existential_deposit = Balance(0) + + # Check if we have enough balance. + if account_balance < (transfer_balance + fee + existential_deposit): + bt_console.print( + ":cross_mark: [red]Not enough balance[/red]:[bold white]\n" + f" balance: {account_balance}\n" + f" amount: {transfer_balance}\n" + f" for fee: {fee}[/bold white]" + ) + return False + + # Ask before moving on. + if prompt: + if not Confirm.ask( + "Do you want to transfer:[bold white]\n" + f" amount: {transfer_balance}\n" + f" from: {wallet.name}:{wallet.coldkey.ss58_address}\n" + f" to: {dest}\n" + f" for fee: {fee}[/bold white]" + ): + return False + + with bt_console.status(":satellite: Transferring..."): + success, block_hash, error_message = do_transfer( + self=subtensor, + wallet=wallet, + dest=dest, + transfer_balance=transfer_balance, + wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + ) + + if success: + bt_console.print(":white_heavy_check_mark: [green]Finalized[/green]") + bt_console.print(f"[green]Block Hash: {block_hash}[/green]") + + explorer_urls = get_explorer_url_for_network( + subtensor.network, block_hash, NETWORK_EXPLORER_MAP + ) + if explorer_urls != {} and explorer_urls: + bt_console.print( + f"[green]Opentensor Explorer Link: {explorer_urls.get('opentensor')}[/green]" + ) + bt_console.print( + f"[green]Taostats Explorer Link: {explorer_urls.get('taostats')}[/green]" + ) + else: + bt_console.print( + f":cross_mark: [red]Failed[/red]: {format_error_message(error_message)}" + ) + + if success: + with bt_console.status(":satellite: Checking Balance..."): + new_balance = subtensor.get_balance(wallet.coldkey.ss58_address) + bt_console.print( + f"Balance:\n [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" + ) + return True + + return False diff --git a/bittensor/core/extrinsics/utils.py b/bittensor/core/extrinsics/utils.py new file mode 100644 index 000000000..6c896372b --- /dev/null +++ b/bittensor/core/extrinsics/utils.py @@ -0,0 +1,49 @@ +"""Module with helper functions for extrinsics.""" + +from typing import TYPE_CHECKING +from substrateinterface.exceptions import SubstrateRequestException +from bittensor.utils.btlogging import logging +from bittensor.utils import format_error_message + +if TYPE_CHECKING: + from substrateinterface import SubstrateInterface + from scalecodec.types import GenericExtrinsic + + +def submit_extrinsic( + substrate: "SubstrateInterface", + extrinsic: "GenericExtrinsic", + wait_for_inclusion: bool, + wait_for_finalization: bool, +): + """ + 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: + substrate (substrateinterface.SubstrateInterface): The substrate interface 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: + response = 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], substrate=substrate)) + # Re-rise the exception for retrying of the extrinsic call. If we remove the retry logic, the raise will need + # to be removed. + raise + return response diff --git a/bittensor/metagraph.py b/bittensor/core/metagraph.py similarity index 80% rename from bittensor/metagraph.py rename to bittensor/core/metagraph.py index e06f712e7..208eaa6b9 100644 --- a/bittensor/metagraph.py +++ b/bittensor/core/metagraph.py @@ -1,35 +1,45 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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. -from abc import ABC, abstractmethod import os import pickle -import numpy as np -from numpy.typing import NDArray -import bittensor +import typing +from abc import ABC, abstractmethod from os import listdir from os.path import join -from typing import List, Optional, Union, Tuple, cast +from typing import Optional, Union -from bittensor.chain_data import AxonInfo +import numpy as np +from numpy.typing import NDArray + +from bittensor.utils.btlogging import logging from bittensor.utils.registration import torch, use_torch -from bittensor.utils import weight_utils +from bittensor.utils.weight_utils import ( + convert_weight_uids_and_vals_to_tensor, + convert_bond_uids_and_vals_to_tensor, + convert_root_weight_uids_and_vals_to_tensor, +) +from . import settings +from .chain_data import AxonInfo + +# For annotation purposes +if typing.TYPE_CHECKING: + from bittensor.core.subtensor import Subtensor + METAGRAPH_STATE_DICT_NDARRAY_KEYS = [ "version", @@ -49,11 +59,32 @@ "validator_permit", "uids", ] +"""List of keys for the metagraph state dictionary used in NDArray serialization. + +This list defines the set of keys expected in the metagraph's state dictionary when serializing and deserializing NumPy ndarray objects. Each key corresponds to a specific attribute or metric associated with the nodes in the metagraph. + +- **version** (`str`): The version identifier of the metagraph state. +- **n** (`int`): The total number of nodes in the metagraph. +- **block** (`int`): The current block number in the blockchain or ledger. +- **stake** (`ndarray`): An array representing the stake of each node. +- **total_stake** (`float`): The sum of all individual stakes in the metagraph. +- **ranks** (`ndarray`): An array of rank scores assigned to each node. +- **trust** (`ndarray`): An array of trust scores for the nodes. +- **consensus** (`ndarray`): An array indicating consensus levels among nodes. +- **validator_trust** (`ndarray`): Trust scores specific to validator nodes. +- **incentive** (`ndarray`): Incentive values allocated to nodes. +- **emission** (`float`): The rate of emission for new tokens or units. +- **dividends** (`ndarray`): Dividend amounts distributed to nodes. +- **active** (`ndarray`): Boolean array indicating active (`True`) or inactive (`False`) nodes. +- **last_update** (`int`): Timestamp of the last state update. +- **validator_permit** (`ndarray`): Boolean array indicating nodes permitted to validate. +- **uids** (`ndarray`): Unique identifiers for each node in the metagraph. +""" def get_save_dir(network: str, netuid: int) -> str: """ - Return directory path from ``network`` and ``netuid``. + Returns a directory path given ``network`` and ``netuid`` inputs. Args: network (str): Network name. @@ -63,13 +94,19 @@ def get_save_dir(network: str, netuid: int) -> str: str: Directory path. """ return os.path.expanduser( - f"~/.bittensor/metagraphs/network-{str(network)}/netuid-{str(netuid)}/" + os.path.join( + "~", + ".bittensor", + "metagraphs", + f"network-{str(network)}", + f"netuid-{str(netuid)}", + ) ) def latest_block_path(dir_path: str) -> str: """ - Get the latest block path from the directory. + Get the latest block path from the provided directory path. Args: dir_path (str): Directory path. @@ -86,7 +123,7 @@ def latest_block_path(dir_path: str) -> str: if block_number > latest_block: latest_block = block_number latest_file_full_path = full_path_filename - except Exception as e: + except Exception: pass if not latest_file_full_path: raise ValueError(f"Metagraph not found at: {dir_path}") @@ -130,10 +167,11 @@ class MetagraphMixin(ABC): The metagraph plays a pivotal role in Bittensor's decentralized AI operations, influencing everything from data propagation to reward distribution. It embodies the principles of decentralized governance and collaborative intelligence, ensuring that the network remains adaptive, secure, and efficient. - Example Usage: + Example: Initializing the metagraph to represent the current state of the Bittensor network:: - metagraph = bt.metagraph(netuid=config.netuid, network=subtensor.network, sync=False) + from bittensor.core.metagraph import Metagraph + metagraph = Metagraph(netuid=config.netuid, network=subtensor.network, sync=False) Synchronizing the metagraph with the network to reflect the latest state and neuron data:: @@ -144,7 +182,8 @@ class MetagraphMixin(ABC): total_stake = metagraph.S neuron_ranks = metagraph.R neuron_incentives = metagraph.I - ... + axons = metagraph.axons + neurons = metagraph.neurons Maintaining a local copy of hotkeys for querying and interacting with network entities:: @@ -153,7 +192,7 @@ class MetagraphMixin(ABC): netuid: int network: str - version: Union["torch.nn.Parameter", Tuple[NDArray]] + version: Union["torch.nn.Parameter", tuple[NDArray]] n: Union["torch.nn.Parameter", NDArray] block: Union["torch.nn.Parameter", NDArray] stake: Union["torch.nn.Parameter", NDArray] @@ -171,7 +210,7 @@ class MetagraphMixin(ABC): weights: Union["torch.nn.Parameter", NDArray] bonds: Union["torch.nn.Parameter", NDArray] uids: Union["torch.nn.Parameter", NDArray] - axons: List[AxonInfo] + axons: list[AxonInfo] @property def S(self) -> Union[NDArray, "torch.nn.Parameter"]: @@ -316,7 +355,7 @@ def W(self) -> Union[NDArray, "torch.nn.Parameter"]: return self.weights @property - def hotkeys(self) -> List[str]: + def hotkeys(self) -> list[str]: """ Represents a list of ``hotkeys`` for each neuron in the Bittensor network. @@ -334,7 +373,7 @@ def hotkeys(self) -> List[str]: return [axon.hotkey for axon in self.axons] @property - def coldkeys(self) -> List[str]: + def coldkeys(self) -> list[str]: """ Contains a list of ``coldkeys`` for each neuron in the Bittensor network. @@ -350,7 +389,7 @@ def coldkeys(self) -> List[str]: return [axon.coldkey for axon in self.axons] @property - def addresses(self) -> List[str]: + def addresses(self) -> list[str]: """ Provides a list of IP addresses for each neuron in the Bittensor network. These addresses are used for network communication, allowing neurons to connect, interact, and exchange information with each other. @@ -374,16 +413,19 @@ def __init__( Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. This method is the entry point for creating a metagraph object, which is a central component in representing the state of the Bittensor network. + Args: netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within potentially multiple network configurations. network (str): The name of the network, which can indicate specific configurations or versions of the Bittensor network. lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain less detailed information but can be quicker to initialize and sync. sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + Example: Initializing a metagraph object for the Bittensor network with a specific network UID:: + metagraph = metagraph(netuid=123, network="finney", lite=True, sync=True) + """ - pass def __str__(self) -> str: """ @@ -398,9 +440,7 @@ def __str__(self) -> str: print(metagraph) # Output: "metagraph(netuid:1, n:100, block:500, network:finney)" """ - return "metagraph(netuid:{}, n:{}, block:{}, network:{})".format( - self.netuid, self.n.item(), self.block.item(), self.network - ) + return f"metagraph(netuid:{self.netuid}, n:{self.n.item()}, block:{self.block.item()}, network:{self.network})" def __repr__(self) -> str: """ @@ -442,7 +482,7 @@ def metadata(self) -> dict: "n": self.n.item(), "block": self.block.item(), "network": self.network, - "version": bittensor.__version__, + "version": settings.__version__, } def state_dict(self): @@ -475,53 +515,57 @@ def sync( self, block: Optional[int] = None, lite: bool = True, - subtensor: Optional["bittensor.subtensor"] = None, + subtensor: Optional["Subtensor"] = None, ): """ - Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes - to reflect the latest data from the network, ensuring the metagraph represents the most current state of the network. + Synchronizes the metagraph with the Bittensor network's current state. It updates the metagraph's attributes to reflect the latest data from the network, ensuring the metagraph represents the most current state of the network. Args: - block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the latest block. - This allows for historical analysis or specific state examination of the network. - lite (bool): If True, a lite version of the metagraph is used for quicker synchronization. This is beneficial - when full detail is not necessary, allowing for reduced computational and time overhead. - subtensor (Optional[bittensor.subtensor]): An instance of the subtensor class from Bittensor, providing an - interface to the underlying blockchain data. If provided, this - instance is used for data retrieval during synchronization. - - Returns: - metagraph: The metagraph instance, updated to the state of the specified block or the latest network state. + block (Optional[int]): A specific block number to synchronize with. If None, the metagraph syncs with the latest block. This allows for historical analysis or specific state examination of the network. + lite (bool): If True, a lite version of the metagraph is used for quicker synchronization. This is beneficial when full detail is not necessary, allowing for reduced computational and time overhead. + subtensor (Optional[bittensor.core.subtensor.Subtensor]): An instance of the subtensor class from Bittensor, providing an interface to the underlying blockchain data. If provided, this instance is used for data retrieval during synchronization. Example: Sync the metagraph with the latest block from the subtensor, using the lite version for efficiency:: + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() metagraph.sync(subtensor=subtensor) Sync with a specific block number for detailed analysis:: + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() metagraph.sync(block=12345, lite=False, subtensor=subtensor) NOTE: - If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for subtensor. - Light nodes are configured only to store the previous 300 blocks if connecting to finney or test networks. + If attempting to access data beyond the previous 300 blocks, you **must** use the ``archive`` network for subtensor. Light nodes are configured only to store the previous 300 blocks if connecting to finney or test networks. For example:: - subtensor = bittensor.subtensor(network='archive') + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor(network='archive') + current_block = subtensor.get_current_block() + history_block = current_block - 1200 + + metagraph.sync(block=history_block, lite=False, subtensor=subtensor) """ # Initialize subtensor subtensor = self._initialize_subtensor(subtensor) if ( - subtensor.chain_endpoint != bittensor.__archive_entrypoint__ # type: ignore - or subtensor.network != "archive" # type: ignore + subtensor.chain_endpoint != settings.ARCHIVE_ENTRYPOINT + or subtensor.network != settings.NETWORKS[3] ): - cur_block = subtensor.get_current_block() # type: ignore + cur_block = subtensor.get_current_block() if block and block < (cur_block - 300): - bittensor.logging.warning( - "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' network for subtensor and retry." + logging.warning( + "Attempting to sync longer than 300 blocks ago on a non-archive node. Please use the 'archive' " + "network for subtensor and retry." ) # Assign neurons based on 'lite' flag @@ -534,7 +578,7 @@ def sync( if not lite: self._set_weights_and_bonds(subtensor=subtensor) - def _initialize_subtensor(self, subtensor): + def _initialize_subtensor(self, subtensor: "Subtensor"): """ Initializes the subtensor to be used for syncing the metagraph. @@ -543,10 +587,10 @@ def _initialize_subtensor(self, subtensor): If no subtensor is provided, this method is responsible for creating a new instance of the subtensor, configured according to the current network settings. Args: - subtensor: The subtensor instance provided for initialization. If ``None``, a new subtensor instance is created using the current network configuration. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance provided for initialization. If ``None``, a new subtensor instance is created using the current network configuration. Returns: - subtensor: The initialized subtensor instance, ready to be used for syncing the metagraph. + subtensor (bittensor.core.subtensor.Subtensor): The initialized subtensor instance, ready to be used for syncing the metagraph. Internal Usage: Used internally during the sync process to ensure a valid subtensor instance is available:: @@ -555,26 +599,33 @@ def _initialize_subtensor(self, subtensor): """ if not subtensor: # TODO: Check and test the initialization of the new subtensor - subtensor = bittensor.subtensor(network=self.network) + # Lazy import due to circular import (subtensor -> metagraph, metagraph -> subtensor) + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor(network=self.network) return subtensor - def _assign_neurons(self, block, lite, subtensor): + def _assign_neurons(self, block: int, lite: bool, subtensor: "Subtensor"): """ Assigns neurons to the metagraph based on the provided block number and the lite flag. This method is responsible for fetching and setting the neuron data in the metagraph, which includes neuron attributes like UID, stake, trust, and other relevant information. Args: - block: The block number for which the neuron data needs to be fetched. If ``None``, the latest block data is used. - lite: A boolean flag indicating whether to use a lite version of the neuron data. The lite version typically includes essential information and is quicker to fetch and process. - subtensor: The subtensor instance used for fetching neuron data from the network. + block (int): The block number for which the neuron data needs to be fetched. If ``None``, the latest block data is used. + lite (bool): A boolean flag indicating whether to use a lite version of the neuron data. The lite version typically includes essential information and is quicker to fetch and process. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for fetching neuron data from the network. Internal Usage: Used internally during the sync process to fetch and set neuron data:: + from bittensor.core.subtensor import Subtensor + + block = 12345 + lite = False + subtensor = Subtensor() self._assign_neurons(block, lite, subtensor) """ - # TODO: Check and test the conditions for assigning neurons if lite: self.neurons = subtensor.neurons_lite(block=block, netuid=self.netuid) else: @@ -605,7 +656,7 @@ def _create_tensor(data, dtype) -> Union[NDArray, "torch.nn.Parameter"]: else np.array(data, dtype=dtype) ) - def _set_weights_and_bonds(self, subtensor: Optional[bittensor.subtensor] = None): + def _set_weights_and_bonds(self, subtensor: "Optional[Subtensor]" = None): """ Computes and sets the weights and bonds for each neuron in the metagraph. This method is responsible for processing the raw weight and bond data obtained from the network and converting it into a structured format suitable for the metagraph model. @@ -622,7 +673,7 @@ def _set_weights_and_bonds(self, subtensor: Optional[bittensor.subtensor] = None self.weights = self._process_root_weights( [neuron.weights for neuron in self.neurons], "weights", - subtensor, # type: ignore + subtensor, ) else: self.weights = self._process_weights_or_bonds( @@ -650,7 +701,7 @@ def _process_weights_or_bonds( self.weights = self._process_weights_or_bonds(raw_weights_data, "weights") """ - data_array: list[Union[NDArray[np.float32], "torch.Tensor"]] = [] + data_array = [] for item in data: if len(item) == 0: if use_torch(): @@ -662,28 +713,21 @@ def _process_weights_or_bonds( # TODO: Validate and test the conversion of uids and values to tensor if attribute == "weights": data_array.append( - weight_utils.convert_weight_uids_and_vals_to_tensor( + convert_weight_uids_and_vals_to_tensor( len(self.neurons), list(uids), list(values), ) ) else: - da_item = weight_utils.convert_bond_uids_and_vals_to_tensor( - len(self.neurons), list(uids), list(values) + data_array.append( + convert_bond_uids_and_vals_to_tensor( + len(self.neurons), list(uids), list(values) + ).astype(np.float32) ) - if use_torch(): - data_array.append(cast("torch.LongTensor", da_item)) - else: - data_array.append( - cast(NDArray[np.float32], da_item).astype(np.float32) - ) tensor_param: Union["torch.nn.Parameter", NDArray] = ( ( - torch.nn.Parameter( - torch.stack(cast(list["torch.Tensor"], data_array)), - requires_grad=False, - ) + torch.nn.Parameter(torch.stack(data_array), requires_grad=False) if len(data_array) else torch.nn.Parameter() ) @@ -695,7 +739,7 @@ def _process_weights_or_bonds( ) ) if len(data_array) == 0: - bittensor.logging.warning( + logging.warning( f"Empty {attribute}_array on metagraph.sync(). The '{attribute}' tensor is empty." ) return tensor_param @@ -705,15 +749,15 @@ def _set_metagraph_attributes(self, block, subtensor): pass def _process_root_weights( - self, data, attribute: str, subtensor: bittensor.subtensor + self, data: list, attribute: str, subtensor: "Subtensor" ) -> Union[NDArray, "torch.nn.Parameter"]: """ Specifically processes the root weights data for the metagraph. This method is similar to :func:`_process_weights_or_bonds` but is tailored for processing root weights, which have a different structure and significance in the network. Args: - data: The raw root weights data to be processed. - attribute: A string indicating the attribute type, here it's typically ``weights``. - subtensor: The subtensor instance used for additional data and context needed in processing. + data (list): The raw root weights data to be processed. + attribute (str): A string indicating the attribute type, here it's typically ``weights``. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for additional data and context needed in processing. Returns: A tensor parameter encapsulating the processed root weights data. @@ -721,10 +765,7 @@ def _process_root_weights( Internal Usage: Used internally to process and set root weights for the metagraph:: - self.root_weights = self._process_root_weights( - raw_root_weights_data, "weights", subtensor - ) - + self.root_weights = self._process_root_weights(raw_root_weights_data, "weights", subtensor) """ data_array = [] n_subnets = subtensor.get_total_subnets() or 0 @@ -734,12 +775,12 @@ def _process_root_weights( if use_torch(): data_array.append(torch.zeros(n_subnets)) else: - data_array.append(np.zeros(n_subnets, dtype=np.float32)) # type: ignore + data_array.append(np.zeros(n_subnets, dtype=np.float32)) else: uids, values = zip(*item) # TODO: Validate and test the conversion of uids and values to tensor data_array.append( - weight_utils.convert_root_weight_uids_and_vals_to_tensor( # type: ignore + convert_root_weight_uids_and_vals_to_tensor( n_subnets, list(uids), list(values), subnets ) ) @@ -758,17 +799,17 @@ def _process_root_weights( ) ) if len(data_array) == 0: - bittensor.logging.warning( + logging.warning( f"Empty {attribute}_array on metagraph.sync(). The '{attribute}' tensor is empty." ) return tensor_param - def save(self) -> "metagraph": # type: ignore + def save(self) -> "Metagraph": """ Saves the current state of the metagraph to a file on disk. This function is crucial for persisting the current state of the network's metagraph, which can later be reloaded or analyzed. The save operation includes all neuron attributes and parameters, ensuring a complete snapshot of the metagraph's state. Returns: - metagraph: The metagraph instance after saving its state. + metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after saving its state. Example: Save the current state of the metagraph to the default directory:: @@ -793,9 +834,7 @@ def save(self) -> "metagraph": # type: ignore state_dict["axons"] = self.axons state_dict["neurons"] = self.neurons torch.save(state_dict, graph_filename) - state_dict = torch.load( - graph_filename - ) # verifies that the file can be loaded correctly + torch.load(graph_filename) # verifies that the file can be loaded correctly else: graph_filename = f"{save_directory}/block-{self.block.item()}.pt" state_dict = self.state_dict() @@ -814,7 +853,7 @@ def load(self): The method delegates to ``load_from_path``, supplying it with the directory path constructed from the metagraph's current ``network`` and ``netuid`` properties. This abstraction simplifies the process of loading the metagraph's state for the user, requiring no direct path specifications. Returns: - metagraph: The metagraph instance after loading its state from the default directory. + metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after loading its state from the default directory. Example: Load the metagraph state from the last saved snapshot in the default directory:: @@ -829,7 +868,7 @@ def load(self): self.load_from_path(get_save_dir(self.network, self.netuid)) @abstractmethod - def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore + def load_from_path(self, dir_path: str) -> "Metagraph": """ Loads the state of the metagraph from a specified directory path. This method is crucial for restoring the metagraph to a specific state based on saved data. It locates the latest block file in the given directory and loads all metagraph parameters from it. This is particularly useful for analyses that require historical states of the network or for restoring previous states of the metagraph in different @@ -841,7 +880,7 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore dir_path (str): The directory path where the metagraph's state files are stored. This path should contain one or more saved state files, typically named in a format that includes the block number. Returns: - metagraph: The metagraph instance after loading its state from the specified directory path. + metagraph (bittensor.core.metagraph.Metagraph): The metagraph instance after loading its state from the specified directory path. Example: Load the metagraph state from a specific directory:: @@ -856,35 +895,42 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore contain valid data for the metagraph. It is essential to ensure that the directory path and the state files within it are accurate and consistent with the expected metagraph structure. """ - pass BaseClass: Union["torch.nn.Module", object] = torch.nn.Module if use_torch() else object +""" +Base class that extends :class:`torch.nn.Module` if PyTorch is used; otherwise, it defaults to object. +""" -class TorchMetaGraph(MetagraphMixin, BaseClass): # type: ignore +class TorchMetaGraph(MetagraphMixin, BaseClass): def __init__( self, netuid: int, network: str = "finney", lite: bool = True, sync: bool = True ): """ Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. - This method is the entry point for creating a metagraph object, - which is a central component in representing the state of the Bittensor network. + This class requires Torch to be installed. + This method is the entry point for creating a metagraph object, which is a central component in representing the state of the Bittensor network. + Args: netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within potentially multiple network configurations. network (str): The name of the network, which can indicate specific configurations or versions of the Bittensor network. lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain less detailed information but can be quicker to initialize and sync. sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + Example: Initializing a metagraph object for the Bittensor network with a specific network UID:: - metagraph = metagraph(netuid=123, network="finney", lite=True, sync=True) + + from bittensor.core.metagraph import Metagraph + + metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) """ torch.nn.Module.__init__(self) MetagraphMixin.__init__(self, netuid, network, lite, sync) self.netuid = netuid self.network = network self.version = torch.nn.Parameter( - torch.tensor([bittensor.__version_as_int__], dtype=torch.int64), + torch.tensor([settings.version_as_int], dtype=torch.int64), requires_grad=False, ) self.n: torch.nn.Parameter = torch.nn.Parameter( @@ -938,29 +984,32 @@ def __init__( self.uids = torch.nn.Parameter( torch.tensor([], dtype=torch.int64), requires_grad=False ) - self.axons: List[AxonInfo] = [] + self.axons: list[AxonInfo] = [] if sync: self.sync(block=None, lite=lite) - def _set_metagraph_attributes(self, block, subtensor): + def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): """ Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. This method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other neuron-specific information. Args: - block: The block number for which the metagraph attributes need to be set. If ``None``, the latest block data is used. - subtensor: The subtensor instance used for fetching the latest network data. + block (int): The block number for which the metagraph attributes need to be set. If ``None``, the latest block data is used. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for fetching the latest network data. Internal Usage: Used internally during the sync process to update the metagraph's attributes:: + from bittensor.core.subtensor import Subtensor + + subtensor = Subtensor() + block = subtensor.get_current_block() + self._set_metagraph_attributes(block, subtensor) """ self.n = self._create_tensor(len(self.neurons), dtype=torch.int64) - self.version = self._create_tensor( - [bittensor.__version_as_int__], dtype=torch.int64 - ) + self.version = self._create_tensor([settings.version_as_int], dtype=torch.int64) self.block = self._create_tensor( block if block else subtensor.block, dtype=torch.int64 ) @@ -1005,7 +1054,27 @@ def _set_metagraph_attributes(self, block, subtensor): ) self.axons = [n.axon_info for n in self.neurons] - def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore + def load_from_path(self, dir_path: str) -> "Metagraph": + """ + Loads the metagraph state from a specified directory path. + + Args: + dir_path (str): The directory path where the state file is located. + + Returns: + metagraph (bittensor.core.metagraph.Metagraph): The current metagraph instance with the loaded state. + + Example:: + + from bittensor.core.metagraph import Metagraph + + netuid = 1 + metagraph = Metagraph(netuid=netuid) + + metagraph.load_from_path("/path/to/dir") + + """ + graph_file = latest_block_path(dir_path) state_dict = torch.load(graph_file) self.n = torch.nn.Parameter(state_dict["n"], requires_grad=False) @@ -1053,12 +1122,30 @@ class NonTorchMetagraph(MetagraphMixin): def __init__( self, netuid: int, network: str = "finney", lite: bool = True, sync: bool = True ): + """ + Initializes a new instance of the metagraph object, setting up the basic structure and parameters based on the provided arguments. + This class doesn't require installed Torch. + This method is the entry point for creating a metagraph object, which is a central component in representing the state of the Bittensor network. + + Args: + netuid (int): The unique identifier for the network, distinguishing this instance of the metagraph within potentially multiple network configurations. + network (str): The name of the network, which can indicate specific configurations or versions of the Bittensor network. + lite (bool): A flag indicating whether to use a lite version of the metagraph. The lite version may contain less detailed information but can be quicker to initialize and sync. + sync (bool): A flag indicating whether to synchronize the metagraph with the network upon initialization. Synchronization involves updating the metagraph's parameters to reflect the current state of the network. + + Example: + Initializing a metagraph object for the Bittensor network with a specific network UID:: + + from bittensor.core.metagraph import Metagraph + + metagraph = Metagraph(netuid=123, network="finney", lite=True, sync=True) + """ # super(metagraph, self).__init__() MetagraphMixin.__init__(self, netuid, network, lite, sync) self.netuid = netuid self.network = network - self.version = (np.array([bittensor.__version_as_int__], dtype=np.int64),) + self.version = (np.array([settings.version_as_int], dtype=np.int64),) self.n = np.array([0], dtype=np.int64) self.block = np.array([0], dtype=np.int64) self.stake = np.array([], dtype=np.float32) @@ -1076,19 +1163,19 @@ def __init__( self.weights = np.array([], dtype=np.float32) self.bonds = np.array([], dtype=np.int64) self.uids = np.array([], dtype=np.int64) - self.axons: List[AxonInfo] = [] + self.axons: list[AxonInfo] = [] if sync: self.sync(block=None, lite=lite) - def _set_metagraph_attributes(self, block, subtensor): + def _set_metagraph_attributes(self, block: int, subtensor: "Subtensor"): """ Sets various attributes of the metagraph based on the latest network data fetched from the subtensor. This method updates parameters like the number of neurons, block number, stakes, trusts, ranks, and other neuron-specific information. Args: - block: The block number for which the metagraph attributes need to be set. If ``None``, the latest block data is used. - subtensor: The subtensor instance used for fetching the latest network data. + block (int): The block number for which the metagraph attributes need to be set. If ``None``, the latest block data is used. + subtensor (bittensor.core.subtensor.Subtensor): The subtensor instance used for fetching the latest network data. Internal Usage: Used internally during the sync process to update the metagraph's attributes:: @@ -1097,9 +1184,7 @@ def _set_metagraph_attributes(self, block, subtensor): """ # TODO: Check and test the setting of each attribute self.n = self._create_tensor(len(self.neurons), dtype=np.int64) - self.version = self._create_tensor( - [bittensor.__version_as_int__], dtype=np.int64 - ) + self.version = self._create_tensor([settings.version_as_int], dtype=np.int64) self.block = self._create_tensor( block if block else subtensor.block, dtype=np.int64 ) @@ -1144,16 +1229,30 @@ def _set_metagraph_attributes(self, block, subtensor): ) self.axons = [n.axon_info for n in self.neurons] - def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore + def load_from_path(self, dir_path: str) -> "Metagraph": + """ + Loads the state of the Metagraph from a specified directory path. + + Args: + dir_path (str): The directory path where the metagraph's state file is located. + + Returns: + metagraph (:func:`bittensor.core.metagraph.Metagraph`): An instance of the Metagraph with the state loaded from the file. + + Raises: + pickle.UnpicklingError: If there is an error unpickling the state file. + RuntimeError: If there is an error loading the state file using PyTorch. + ImportError: If there is an error importing PyTorch. + """ graph_filename = latest_block_path(dir_path) try: with open(graph_filename, "rb") as graph_file: state_dict = pickle.load(graph_file) except pickle.UnpicklingError: - bittensor.__console__.print( + settings.bt_console.print( "Unable to load file. Attempting to restore metagraph using torch." ) - bittensor.__console__.print( + settings.bt_console.print( ":warning:[yellow]Warning:[/yellow] This functionality exists to load " "metagraph state from legacy saves, but will not be supported in the future." ) @@ -1165,7 +1264,7 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore state_dict[key] = state_dict[key].detach().numpy() del real_torch except (RuntimeError, ImportError): - bittensor.__console__.print("Unable to load file. It may be corrupted.") + settings.bt_console.print("Unable to load file. It may be corrupted.") raise self.n = state_dict["n"] @@ -1192,4 +1291,9 @@ def load_from_path(self, dir_path: str) -> "metagraph": # type: ignore return self -metagraph = TorchMetaGraph if use_torch() else NonTorchMetagraph +Metagraph = TorchMetaGraph if use_torch() else NonTorchMetagraph +"""Metagraph class that uses :class:`TorchMetaGraph` if PyTorch is available; otherwise, it falls back to :class:`NonTorchMetagraph`. + +- **With PyTorch**: When `use_torch()` returns `True`, `Metagraph` is set to :class:`TorchMetaGraph`, which utilizes PyTorch functionalities. +- **Without PyTorch**: When `use_torch()` returns `False`, `Metagraph` is set to :class:`NonTorchMetagraph`, which does not rely on PyTorch. +""" diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py new file mode 100644 index 000000000..cfccf362b --- /dev/null +++ b/bittensor/core/settings.py @@ -0,0 +1,241 @@ +# 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. + +__version__ = "8.0.0" + +import os +import re +import warnings +from pathlib import Path + +from munch import munchify +from rich.console import Console +from rich.traceback import install + +# Rich console. +__console__ = Console() +__use_console__ = True + +# Remove overdue locals in debug training. +install(show_locals=False) + + +def turn_console_off(): + global __use_console__ + global __console__ + from io import StringIO + + __use_console__ = False + __console__ = Console(file=StringIO(), stderr=False) + + +def turn_console_on(): + global __use_console__ + global __console__ + __use_console__ = True + __console__ = Console() + + +turn_console_off() + +bt_console = __console__ + + +HOME_DIR = Path.home() +USER_BITTENSOR_DIR = HOME_DIR / ".bittensor" +WALLETS_DIR = USER_BITTENSOR_DIR / "wallets" +MINERS_DIR = USER_BITTENSOR_DIR / "miners" + +# Bittensor networks name +NETWORKS = ["local", "finney", "test", "archive"] + +DEFAULT_ENDPOINT = "wss://entrypoint-finney.opentensor.ai:443" +DEFAULT_NETWORK = NETWORKS[1] + +# Create dirs if they don't exist +WALLETS_DIR.mkdir(parents=True, exist_ok=True) +MINERS_DIR.mkdir(parents=True, exist_ok=True) + + +# Bittensor endpoints (Needs to use wss://) +FINNEY_ENTRYPOINT = "wss://entrypoint-finney.opentensor.ai:443" +FINNEY_TEST_ENTRYPOINT = "wss://test.finney.opentensor.ai:443/" +ARCHIVE_ENTRYPOINT = "wss://archive.chain.opentensor.ai:443/" +LOCAL_ENTRYPOINT = os.getenv("BT_SUBTENSOR_CHAIN_ENDPOINT") or "ws://127.0.0.1:9946" + +# Currency Symbols Bittensor +TAO_SYMBOL: str = chr(0x03C4) +RAO_SYMBOL: str = chr(0x03C1) + +# Pip address for versioning +PIPADDRESS = "https://pypi.org/pypi/bittensor/json" + +# Substrate chain block time (seconds). +BLOCKTIME = 12 + +# Substrate ss58_format +SS58_FORMAT = 42 + +# Wallet ss58 address length +SS58_ADDRESS_LENGTH = 48 + +# Raw GitHub url for delegates registry file +DELEGATES_DETAILS_URL = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" + +# Block Explorers map network to explorer url +# Must all be polkadotjs explorer urls +NETWORK_EXPLORER_MAP = { + "opentensor": { + "local": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", + "endpoint": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", + "finney": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", + }, + "taostats": { + "local": "https://x.taostats.io", + "endpoint": "https://x.taostats.io", + "finney": "https://x.taostats.io", + }, +} + +# --- Type Registry --- +TYPE_REGISTRY: dict = { + "types": { + "Balance": "u64", # Need to override default u128 + }, + "runtime_api": { + "NeuronInfoRuntimeApi": { + "methods": { + "get_neuron_lite": { + "params": [ + { + "name": "netuid", + "type": "u16", + }, + { + "name": "uid", + "type": "u16", + }, + ], + "type": "Vec", + }, + "get_neurons_lite": { + "params": [ + { + "name": "netuid", + "type": "u16", + }, + ], + "type": "Vec", + }, + } + }, + "SubnetInfoRuntimeApi": { + "methods": { + "get_subnet_hyperparams": { + "params": [ + { + "name": "netuid", + "type": "u16", + }, + ], + "type": "Vec", + } + } + }, + "SubnetRegistrationRuntimeApi": { + "methods": {"get_network_registration_cost": {"params": [], "type": "u64"}} + }, + }, +} + + +_BT_AXON_PORT = os.getenv("BT_AXON_PORT") +_BT_AXON_MAX_WORKERS = os.getenv("BT_AXON_MAX_WORKERS") +_BT_PRIORITY_MAX_WORKERS = os.getenv("BT_PRIORITY_MAX_WORKERS") +_BT_PRIORITY_MAXSIZE = os.getenv("BT_PRIORITY_MAXSIZE") + +DEFAULTS = munchify( + { + "axon": { + "port": int(_BT_AXON_PORT) if _BT_AXON_PORT else 8091, + "ip": os.getenv("BT_AXON_IP") or "[::]", + "external_port": os.getenv("BT_AXON_EXTERNAL_PORT") or None, + "external_ip": os.getenv("BT_AXON_EXTERNAL_IP") or None, + "max_workers": int(_BT_AXON_MAX_WORKERS) if _BT_AXON_MAX_WORKERS else 10, + }, + "logging": { + "debug": os.getenv("BT_LOGGING_DEBUG") or False, + "trace": os.getenv("BT_LOGGING_TRACE") or False, + "record_log": os.getenv("BT_LOGGING_RECORD_LOG") or False, + "logging_dir": os.getenv("BT_LOGGING_LOGGING_DIR") or str(MINERS_DIR), + }, + "priority": { + "max_workers": int(_BT_PRIORITY_MAX_WORKERS) + if _BT_PRIORITY_MAX_WORKERS + else 5, + "maxsize": int(_BT_PRIORITY_MAXSIZE) if _BT_PRIORITY_MAXSIZE else 10, + }, + "subtensor": { + "chain_endpoint": DEFAULT_ENDPOINT, + "network": DEFAULT_NETWORK, + "_mock": False, + }, + "wallet": { + "name": "default", + "hotkey": "default", + "path": str(WALLETS_DIR), + }, + } +) + + +# Parsing version without any literals. +__version__ = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) + +version_split = __version__.split(".") +_version_info = tuple(int(part) for part in version_split) +_version_int_base = 1000 +assert max(_version_info) < _version_int_base + +version_as_int: int = sum( + e * (_version_int_base**i) for i, e in enumerate(reversed(_version_info)) +) +assert version_as_int < 2**31 # fits in int32 + + +def __apply_nest_asyncio(): + """ + Apply nest_asyncio if the environment variable NEST_ASYNCIO is set to "1" or not set. + If not set, warn the user that the default will change in the future. + """ + nest_asyncio_env = os.getenv("NEST_ASYNCIO") + if nest_asyncio_env == "1" or nest_asyncio_env is None: + if nest_asyncio_env is None: + warnings.warn( + """NEST_ASYNCIO implicitly set to '1'. In the future, the default value will be '0'. + If you use `nest_asyncio`, make sure to add it explicitly to your project dependencies, + as it will be removed from `bittensor` package dependencies in the future. + To silence this warning, explicitly set the environment variable, e.g. `export NEST_ASYNCIO=0`.""", + DeprecationWarning, + ) + # Install and apply nest asyncio to allow the async functions to run in a .ipynb + import nest_asyncio + + nest_asyncio.apply() + + +__apply_nest_asyncio() diff --git a/bittensor/stream.py b/bittensor/core/stream.py similarity index 52% rename from bittensor/stream.py rename to bittensor/core/stream.py index 3a82edc15..9e880ffa8 100644 --- a/bittensor/stream.py +++ b/bittensor/core/stream.py @@ -1,13 +1,29 @@ -import typing +# 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. -from aiohttp import ClientResponse -import bittensor +from abc import ABC, abstractmethod +from typing import Callable, Awaitable, Optional +from aiohttp import ClientResponse +from pydantic import ConfigDict, BaseModel from starlette.responses import StreamingResponse as _StreamingResponse from starlette.types import Send, Receive, Scope -from typing import Callable, Awaitable -from pydantic import ConfigDict, BaseModel -from abc import ABC, abstractmethod + +from .synapse import Synapse class BTStreamingResponseModel(BaseModel): @@ -23,15 +39,13 @@ class BTStreamingResponseModel(BaseModel): passing the token streamer to the BTStreamingResponse class. Attributes: - token_streamer: Callable[[Send], Awaitable[None]] - The token streamer callable, which takes a send function (provided by the ASGI server) and returns an awaitable. - It is responsible for generating the content of the streaming response. + token_streamer: Callable[[Send], Awaitable[None]] The token streamer callable, which takes a send function (provided by the ASGI server) and returns an awaitable. It is responsible for generating the content of the streaming response. """ token_streamer: Callable[[Send], Awaitable[None]] -class StreamingSynapse(bittensor.Synapse, ABC): +class StreamingSynapse(Synapse, ABC): """ The :func:`StreamingSynapse` class is designed to be subclassed for handling streaming responses in the Bittensor network. It provides abstract methods that must be implemented by the subclass to deserialize, process streaming responses, @@ -53,33 +67,31 @@ class BTStreamingResponse(_StreamingResponse): def __init__( self, - model: BTStreamingResponseModel, + model: "BTStreamingResponseModel", *, - synapse: typing.Optional["StreamingSynapse"] = None, + synapse: "Optional[StreamingSynapse]" = None, **kwargs, ): """ Initializes the BTStreamingResponse with the given token streamer model. Args: - model: A BTStreamingResponseModel instance containing the token streamer callable, which is responsible for generating the content of the response. - synapse: The response Synapse to be used to update the response headers etc. + model (bittensor.core.stream.BTStreamingResponseModel): A BTStreamingResponseModel instance containing the token streamer callable, which is responsible for generating the content of the response. + synapse (bittensor.core.stream.StreamingSynapse): The response Synapse to be used to update the response headers etc. **kwargs: Additional keyword arguments passed to the parent StreamingResponse class. """ super().__init__(content=iter(()), **kwargs) self.token_streamer = model.token_streamer self.synapse = synapse - async def stream_response(self, send: Send): + async def stream_response(self, send: "Send"): """ Asynchronously streams the response by sending headers and calling the token streamer. - This method is responsible for initiating the response by sending the appropriate headers, including the - content type for event-streaming. It then calls the token streamer to generate the content and sends the - response body to the client. + This method is responsible for initiating the response by sending the appropriate headers, including the content type for event-streaming. It then calls the token streamer to generate the content and sends the response body to the client. Args: - send: A callable to send the response, provided by the ASGI server. + send (starlette.types.Send): A callable to send the response, provided by the ASGI server. """ headers = [(b"content-type", b"text/event-stream")] + self.raw_headers @@ -91,61 +103,55 @@ async def stream_response(self, send: Send): await send({"type": "http.response.body", "body": b"", "more_body": False}) - async def __call__(self, scope: Scope, receive: Receive, send: Send): + async def __call__(self, scope: "Scope", receive: "Receive", send: "Send"): """ - Asynchronously calls the stream_response method, allowing the BTStreamingResponse object to be used as an ASGI - application. + Asynchronously calls the :func:`stream_response method`, allowing the :func:`BTStreamingResponse` object to be used as an ASGI application. - This method is part of the ASGI interface and is called by the ASGI server to handle the request and send the - response. It delegates to the :func:`stream_response` method to perform the actual streaming process. + This method is part of the ASGI interface and is called by the ASGI server to handle the request and send the response. It delegates to the :func:`stream_response` method to perform the actual streaming process. Args: - scope: The scope of the request, containing information about the client, server, and request itself. - receive: A callable to receive the request, provided by the ASGI server. - send: A callable to send the response, provided by the ASGI server. + scope (starlette.types.Scope): The scope of the request, containing information about the client, server, and request itself. + receive (starlette.types.Receive): A callable to receive the request, provided by the ASGI server. + send (starlette.types.Send): A callable to send the response, provided by the ASGI server. """ await self.stream_response(send) @abstractmethod - async def process_streaming_response(self, response: ClientResponse): + async def process_streaming_response(self, response: "ClientResponse"): """ Abstract method that must be implemented by the subclass. This method should provide logic to handle the streaming response, such as parsing and accumulating data. - It is called as the response is being streamed from the network, and should be implemented to handle the specific - streaming data format and requirements of the subclass. + It is called as the response is being streamed from the network, and should be implemented to handle the specific streaming data format and requirements of the subclass. Args: - response: The response object to be processed, typically containing chunks of data. + response (aiohttp.ClientResponse): The response object to be processed, typically containing chunks of data. """ ... @abstractmethod - def extract_response_json(self, response: ClientResponse) -> dict: + def extract_response_json(self, response: "ClientResponse") -> dict: """ Abstract method that must be implemented by the subclass. This method should provide logic to extract JSON data from the response, including headers and content. - It is called after the response has been processed and is responsible for retrieving structured data - that can be used by the application. + It is called after the response has been processed and is responsible for retrieving structured data that can be used by the application. Args: - response: The response object from which to extract JSON data. + response (aiohttp.ClientResponse): The response object from which to extract JSON data. """ - ... def create_streaming_response( self, token_streamer: Callable[[Send], Awaitable[None]] - ) -> BTStreamingResponse: + ) -> "BTStreamingResponse": """ Creates a streaming response using the provided token streamer. This method can be used by the subclass to create a response object that can be sent back to the client. - The token streamer should be implemented to generate the content of the response according to the specific - requirements of the subclass. + The token streamer should be implemented to generate the content of the response according to the specific requirements of the subclass. Args: - token_streamer: A callable that takes a send function and returns an awaitable. It's responsible for generating the content of the response. + token_streamer (Callable[[starlette.types.Send], Awaitable[None]]): A callable that takes a send function and returns an awaitable. It's responsible for generating the content of the response. Returns: - BTStreamingResponse: The streaming response object, ready to be sent to the client. + BTStreamingResponse (bittensor.core.stream.StreamingSynapse.BTStreamingResponse): The streaming response object, ready to be sent to the client. """ model_instance = BTStreamingResponseModel(token_streamer=token_streamer) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py new file mode 100644 index 000000000..533964595 --- /dev/null +++ b/bittensor/core/subtensor.py @@ -0,0 +1,1733 @@ +# 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. + +""" +The ``bittensor.core.subtensor`` module in Bittensor serves as a crucial interface for interacting with the Bittensor +blockchain, facilitating a range of operations essential for the decentralized machine learning network. +""" + +import argparse +import copy +import socket +from typing import Union, Optional, TypedDict, Any + +import numpy as np +import scalecodec +from bittensor_wallet import Wallet +from numpy.typing import NDArray +from retry import retry +from scalecodec.base import RuntimeConfiguration +from scalecodec.exceptions import RemainingScaleBytesNotEmptyException +from scalecodec.type_registry import load_type_registry_preset +from scalecodec.types import ScaleType +from substrateinterface.base import QueryMapResult, SubstrateInterface + +from bittensor.core import settings +from bittensor.core.axon import Axon +from bittensor.core.chain_data import ( + NeuronInfo, + PrometheusInfo, + SubnetHyperparameters, + NeuronInfoLite, + custom_rpc_type_registry, +) +from bittensor.core.config import Config +from bittensor.core.extrinsics.commit_weights import ( + commit_weights_extrinsic, + reveal_weights_extrinsic, +) +from bittensor.core.extrinsics.prometheus import ( + do_serve_prometheus, + prometheus_extrinsic, +) +from bittensor.core.extrinsics.serving import ( + do_serve_axon, + serve_axon_extrinsic, + publish_metadata, + get_metadata, +) +from bittensor.core.extrinsics.set_weights import set_weights_extrinsic +from bittensor.core.extrinsics.transfer import ( + transfer_extrinsic, +) +from bittensor.core.metagraph import Metagraph +from bittensor.utils import torch +from bittensor.utils import u16_normalized_float, networking +from bittensor.utils.balance import Balance +from bittensor.utils.btlogging import logging +from bittensor.utils.weight_utils import generate_weight_hash + +KEY_NONCE: dict[str, int] = {} + + +class ParamWithTypes(TypedDict): + name: str # Name of the parameter. + type: str # ScaleType string of the parameter. + + +class Subtensor: + """ + The Subtensor class in Bittensor serves as a crucial interface for interacting with the Bittensor blockchain, + facilitating a range of operations essential for the decentralized machine learning network. + + This class enables neurons (network participants) to engage in activities such as registering on the network, + managing staked weights, setting inter-neuronal weights, and participating in consensus mechanisms. + + The Bittensor network operates on a digital ledger where each neuron holds stakes (S) and learns a set + of inter-peer weights (W). These weights, set by the neurons themselves, play a critical role in determining + the ranking and incentive mechanisms within the network. Higher-ranked neurons, as determined by their + contributions and trust within the network, receive more incentives. + + The Subtensor class connects to various Bittensor networks like the main ``finney`` network or local test + networks, providing a gateway to the blockchain layer of Bittensor. It leverages a staked weighted trust + system and consensus to ensure fair and distributed incentive mechanisms, where incentives (I) are + primarily allocated to neurons that are trusted by the majority of the network. + + Additionally, Bittensor introduces a speculation-based reward mechanism in the form of bonds (B), allowing + neurons to accumulate bonds in other neurons, speculating on their future value. This mechanism aligns + with market-based speculation, incentivizing neurons to make judicious decisions in their inter-neuronal + investments. + + Example Usage:: + + from bittensor.core.subtensor import Subtensor + + # Connect to the main Bittensor network (Finney). + finney_subtensor = Subtensor(network='finney') + + # Close websocket connection with the Bittensor network. + finney_subtensor.close() + + # Register a new neuron on the network. + wallet = bittensor_wallet.Wallet(...) # Assuming a wallet instance is created. + netuid = 1 + success = finney_subtensor.register(wallet=wallet, netuid=netuid) + + # Set inter-neuronal weights for collaborative learning. + success = finney_subtensor.set_weights(wallet=wallet, netuid=netuid, uids=[...], weights=[...]) + + # Get the metagraph for a specific subnet using given subtensor connection + metagraph = finney_subtensor.metagraph(netuid=netuid) + + By facilitating these operations, the Subtensor class is instrumental in maintaining the decentralized + intelligence and dynamic learning environment of the Bittensor network, as envisioned in its foundational + principles and mechanisms described in the `NeurIPS paper + `_. paper. + """ + + def __init__( + self, + network: Optional[str] = None, + config: Optional["Config"] = None, + _mock: bool = False, + log_verbose: bool = True, + connection_timeout: int = 600, + ) -> None: + """ + Initializes a Subtensor interface for interacting with the Bittensor blockchain. + + NOTE: + Currently subtensor defaults to the ``finney`` network. This will change in a future release. + + We strongly encourage users to run their own local subtensor node whenever possible. This increases decentralization and resilience of the network. In a future release, local subtensor will become the default and the fallback to ``finney`` removed. Please plan ahead for this change. We will provide detailed instructions on how to run a local subtensor node in the documentation in a subsequent release. + + Args: + network (Optional[str]): The network name to connect to (e.g., ``finney``, ``local``). This can also be the chain endpoint (e.g., ``wss://entrypoint-finney.opentensor.ai:443``) and will be correctly parsed into the network and chain endpoint. If not specified, defaults to the main Bittensor network. + config (Optional[bittensor.core.config.Config]): Configuration object for the subtensor. If not provided, a default configuration is used. + _mock (bool): If set to ``True``, uses a mocked connection for testing purposes. Default is ``False``. + log_verbose (bool): Whether to enable verbose logging. If set to ``True``, detailed log information about the connection and network operations will be provided. Default is ``True``. + connection_timeout (int): The maximum time in seconds to keep the connection alive. Default is ``600``. + + This initialization sets up the connection to the specified Bittensor network, allowing for various blockchain operations such as neuron registration, stake management, and setting weights. + """ + # Determine config.subtensor.chain_endpoint and config.subtensor.network config. + # If chain_endpoint is set, we override the network flag, otherwise, the chain_endpoint is assigned by the + # network. + # Argument importance: network > chain_endpoint > config.subtensor.chain_endpoint > config.subtensor.network + + if config is None: + config = Subtensor.config() + self._config = copy.deepcopy(config) + + # Setup config.subtensor.network and config.subtensor.chain_endpoint + self.chain_endpoint, self.network = Subtensor.setup_config( + network, self._config + ) + + if ( + self.network == "finney" + or self.chain_endpoint == settings.FINNEY_ENTRYPOINT + ) and log_verbose: + logging.info( + f"You are connecting to {self.network} network with endpoint {self.chain_endpoint}." + ) + logging.warning( + "We strongly encourage running a local subtensor node whenever possible. " + "This increases decentralization and resilience of the network." + ) + logging.warning( + "In a future release, local subtensor will become the default endpoint. " + "To get ahead of this change, please run a local subtensor node and point to it." + ) + + self.log_verbose = log_verbose + self._connection_timeout = connection_timeout + self._get_substrate() + + def __str__(self) -> str: + if self.network == self.chain_endpoint: + # Connecting to chain endpoint without network known. + return f"subtensor({self.chain_endpoint})" + else: + # Connecting to network with endpoint known. + return f"subtensor({self.network}, {self.chain_endpoint})" + + def __repr__(self) -> str: + return self.__str__() + + def close(self): + """Cleans up resources for this subtensor instance like active websocket connection and active extensions.""" + self.substrate.close() + + def _get_substrate(self): + """Establishes a connection to the Substrate node using configured parameters.""" + try: + # Set up params. + self.substrate = SubstrateInterface( + ss58_format=settings.SS58_FORMAT, + use_remote_preset=True, + url=self.chain_endpoint, + type_registry=settings.TYPE_REGISTRY, + ) + if self.log_verbose: + logging.info( + f"Connected to {self.network} network and {self.chain_endpoint}." + ) + + try: + self.substrate.websocket.settimeout(self._connection_timeout) + except (AttributeError, TypeError, socket.error, OSError) as e: + logging.warning(f"Error setting timeout: {e}") + + except ConnectionRefusedError: + logging.error( + f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint.", + ) + logging.info( + "You can check if you have connectivity by running this command: nc -vz localhost " + f"{self.chain_endpoint.split(':')[2]}" + ) + + @staticmethod + def config() -> "Config": + """ + Creates and returns a Bittensor configuration object. + + Returns: + config (bittensor.core.config.Config): A Bittensor configuration object configured with arguments added by the `subtensor.add_args` method. + """ + parser = argparse.ArgumentParser() + Subtensor.add_args(parser) + return Config(parser, args=[]) + + @staticmethod + def setup_config(network: Optional[str], config: "Config"): + """ + Sets up and returns the configuration for the Subtensor network and endpoint. + + This method determines the appropriate network and chain endpoint based on the provided network string or + configuration object. It evaluates the network and endpoint in the following order of precedence: + 1. Provided network string. + 2. Configured chain endpoint in the `config` object. + 3. Configured network in the `config` object. + 4. Default chain endpoint. + 5. Default network. + + Args: + network (Optional[str]): The name of the Subtensor network. If None, the network and endpoint will be determined from the `config` object. + config (bittensor.core.config.Config): The configuration object containing the network and chain endpoint settings. + + Returns: + tuple: A tuple containing the formatted WebSocket endpoint URL and the evaluated network name. + """ + if network is not None: + ( + evaluated_network, + evaluated_endpoint, + ) = Subtensor.determine_chain_endpoint_and_network(network) + else: + if config.is_set("subtensor.chain_endpoint"): + ( + evaluated_network, + evaluated_endpoint, + ) = Subtensor.determine_chain_endpoint_and_network( + config.subtensor.chain_endpoint + ) + + elif config.is_set("subtensor.network"): + ( + evaluated_network, + evaluated_endpoint, + ) = Subtensor.determine_chain_endpoint_and_network( + config.subtensor.network + ) + + elif config.subtensor.get("chain_endpoint"): + ( + evaluated_network, + evaluated_endpoint, + ) = Subtensor.determine_chain_endpoint_and_network( + config.subtensor.chain_endpoint + ) + + elif config.subtensor.get("network"): + ( + evaluated_network, + evaluated_endpoint, + ) = Subtensor.determine_chain_endpoint_and_network( + config.subtensor.network + ) + + else: + ( + evaluated_network, + evaluated_endpoint, + ) = Subtensor.determine_chain_endpoint_and_network( + settings.DEFAULTS.subtensor.network + ) + + return ( + networking.get_formatted_ws_endpoint_url(evaluated_endpoint), + evaluated_network, + ) + + @classmethod + def help(cls): + """Print help to stdout.""" + parser = argparse.ArgumentParser() + cls.add_args(parser) + print(cls.__new__.__doc__) + parser.print_help() + + @classmethod + def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = None): + """ + Adds command-line arguments to the provided ArgumentParser for configuring the Subtensor settings. + + Args: + parser (argparse.ArgumentParser): The ArgumentParser object to which the Subtensor arguments will be added. + prefix (Optional[str]): An optional prefix for the argument names. If provided, the prefix is prepended to each argument name. + + Arguments added: + --subtensor.network: The Subtensor network flag. Possible values are 'finney', 'test', 'archive', and 'local'. Overrides the chain endpoint if set. + --subtensor.chain_endpoint: The Subtensor chain endpoint flag. If set, it overrides the network flag. + --subtensor._mock: If true, uses a mocked connection to the chain. + + Example: + parser = argparse.ArgumentParser() + Subtensor.add_args(parser) + """ + prefix_str = "" if prefix is None else f"{prefix}." + try: + default_network = settings.DEFAULT_NETWORK + default_chain_endpoint = settings.FINNEY_ENTRYPOINT + + parser.add_argument( + f"--{prefix_str}subtensor.network", + default=default_network, + type=str, + help="""The subtensor network flag. The likely choices are: + -- finney (main network) + -- test (test network) + -- archive (archive network +300 blocks) + -- local (local running network) + If this option is set it overloads subtensor.chain_endpoint with + an entry point node from that network. + """, + ) + parser.add_argument( + f"--{prefix_str}subtensor.chain_endpoint", + default=default_chain_endpoint, + type=str, + help="""The subtensor endpoint flag. If set, overrides the --network flag.""", + ) + parser.add_argument( + f"--{prefix_str}subtensor._mock", + default=False, + type=bool, + help="""If true, uses a mocked connection to the chain.""", + ) + + except argparse.ArgumentError: + # re-parsing arguments. + pass + + # Inner private functions + @networking.ensure_connected + def _encode_params( + self, + call_definition: list["ParamWithTypes"], + params: Union[list[Any], dict[str, Any]], + ) -> str: + """Returns a hex encoded string of the params using their types.""" + param_data = scalecodec.ScaleBytes(b"") + + for i, param in enumerate(call_definition["params"]): # type: ignore + scale_obj = self.substrate.create_scale_object(param["type"]) + if type(params) is list: + param_data += scale_obj.encode(params[i]) + else: + if param["name"] not in params: + raise ValueError(f"Missing param {param['name']} in params dict.") + + param_data += scale_obj.encode(params[param["name"]]) + + return param_data.to_hex() + + def _get_hyperparameter( + self, param_name: str, netuid: int, block: Optional[int] = None + ) -> Optional[Any]: + """ + Retrieves a specified hyperparameter for a specific subnet. + + Args: + param_name (str): The name of the hyperparameter to retrieve. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[Union[int, float]]: The value of the specified hyperparameter if the subnet exists, ``None`` otherwise. + """ + if not self.subnet_exists(netuid, block): + return None + + result = self.query_subtensor(param_name, block, [netuid]) + if result is None or not hasattr(result, "value"): + return None + + return result.value + + # Calls methods + @networking.ensure_connected + def query_subtensor( + self, name: str, block: Optional[int] = None, params: Optional[list] = None + ) -> "ScaleType": + """ + Queries named storage from the Subtensor module on the Bittensor blockchain. This function is used to retrieve specific data or parameters from the blockchain, such as stake, rank, or other neuron-specific attributes. + + Args: + name (str): The name of the storage function to query. + block (Optional[int]): The blockchain block number at which to perform the query. + params (Optional[list[object]]): A list of parameters to pass to the query function. + + Returns: + query_response (scalecodec.ScaleType): An object containing the requested data. + + This query function is essential for accessing detailed information about the network and its neurons, providing valuable insights into the state and dynamics of the Bittensor ecosystem. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry() -> "ScaleType": + return self.substrate.query( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), + ) + + return make_substrate_call_with_retry() + + @networking.ensure_connected + def query_map_subtensor( + self, name: str, block: Optional[int] = None, params: Optional[list] = None + ) -> "QueryMapResult": + """ + Queries map storage from the Subtensor module on the Bittensor blockchain. This function is designed to retrieve a map-like data structure, which can include various neuron-specific details or network-wide attributes. + + Args: + name (str): The name of the map storage function to query. + block (Optional[int]): The blockchain block number at which to perform the query. + params (Optional[list[object]]): A list of parameters to pass to the query function. + + Returns: + QueryMapResult (substrateinterface.base.QueryMapResult): An object containing the map-like data structure, or ``None`` if not found. + + This function is particularly useful for analyzing and understanding complex network structures and relationships within the Bittensor ecosystem, such as inter-neuronal connections and stake distributions. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + return self.substrate.query_map( + module="SubtensorModule", + storage_function=name, + params=params, + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), + ) + + return make_substrate_call_with_retry() + + def query_runtime_api( + self, + runtime_api: str, + method: str, + params: Optional[Union[list[int], dict[str, int]]], + block: Optional[int] = None, + ) -> Optional[str]: + """ + Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying runtime and retrieve data encoded in Scale Bytes format. This function is essential for advanced users who need to interact with specific runtime methods and decode complex data types. + + Args: + runtime_api (str): The name of the runtime API to query. + method (str): The specific method within the runtime API to call. + params (Optional[list[ParamWithTypes]]): The parameters to pass to the method call. + block (Optional[int]): The blockchain block number at which to perform the query. + + Returns: + Optional[str]: The Scale Bytes encoded result from the runtime API call, or ``None`` if the call fails. + + This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. + """ + call_definition = settings.TYPE_REGISTRY["runtime_api"][runtime_api]["methods"][ + method + ] + + json_result = self.state_call( + method=f"{runtime_api}_{method}", + data=( + "0x" + if params is None + else self._encode_params(call_definition=call_definition, params=params) + ), + block=block, + ) + + if json_result is None: + return None + + return_type = call_definition["type"] + + as_scale_bytes = scalecodec.ScaleBytes(json_result["result"]) + + rpc_runtime_config = RuntimeConfiguration() + rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) + rpc_runtime_config.update_type_registry(custom_rpc_type_registry) + + obj = rpc_runtime_config.create_scale_object(return_type, as_scale_bytes) + if obj.data.to_hex() == "0x0400": # RPC returned None result + return None + + return obj.decode() + + @networking.ensure_connected + def state_call( + self, method: str, data: str, block: Optional[int] = None + ) -> dict[Any, Any]: + """ + Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. This function is typically used for advanced queries that require specific method calls and data inputs. + + Args: + method (str): The method name for the state call. + data (str): The data to be passed to the method. + block (Optional[int]): The blockchain block number at which to perform the state call. + + Returns: + result (dict[Any, Any]): The result of the rpc call. + + The state call function provides a more direct and flexible way of querying blockchain data, useful for specific use cases where standard queries are insufficient. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry() -> dict[Any, Any]: + block_hash = None if block is None else self.substrate.get_block_hash(block) + return self.substrate.rpc_request( + method="state_call", + params=[method, data, block_hash] if block_hash else [method, data], + ) + + return make_substrate_call_with_retry() + + @networking.ensure_connected + def query_map( + self, + module: str, + name: str, + block: Optional[int] = None, + params: Optional[list] = None, + ) -> "QueryMapResult": + """ + Queries map storage from any module on the Bittensor blockchain. This function retrieves data structures that represent key-value mappings, essential for accessing complex and structured data within the blockchain modules. + + Args: + module (str): The name of the module from which to query the map storage. + name (str): The specific storage function within the module to query. + block (Optional[int]): The blockchain block number at which to perform the query. + params (Optional[list[object]]): Parameters to be passed to the query. + + Returns: + result (substrateinterface.base.QueryMapResult): A data structure representing the map storage if found, ``None`` otherwise. + + This function is particularly useful for retrieving detailed and structured data from various blockchain modules, offering insights into the network's state and the relationships between its different components. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry() -> "QueryMapResult": + return self.substrate.query_map( + module=module, + storage_function=name, + params=params, + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), + ) + + return make_substrate_call_with_retry() + + @networking.ensure_connected + def query_constant( + self, module_name: str, constant_name: str, block: Optional[int] = None + ) -> Optional["ScaleType"]: + """ + Retrieves a constant from the specified module on the Bittensor blockchain. This function is used to access fixed parameters or values defined within the blockchain's modules, which are essential for understanding the network's configuration and rules. + + Args: + module_name (str): The name of the module containing the constant. + constant_name (str): The name of the constant to retrieve. + block (Optional[int]): The blockchain block number at which to query the constant. + + Returns: + Optional[scalecodec.ScaleType]: The value of the constant if found, ``None`` otherwise. + + Constants queried through this function can include critical network parameters such as inflation rates, consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor network's operational parameters. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + return self.substrate.get_constant( + module_name=module_name, + constant_name=constant_name, + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), + ) + + return make_substrate_call_with_retry() + + @networking.ensure_connected + def query_module( + self, + module: str, + name: str, + block: Optional[int] = None, + params: Optional[list] = None, + ) -> "ScaleType": + """ + Queries any module storage on the Bittensor blockchain with the specified parameters and block number. This function is a generic query interface that allows for flexible and diverse data retrieval from various blockchain modules. + + Args: + module (str): The name of the module from which to query data. + name (str): The name of the storage function within the module. + block (Optional[int]): The blockchain block number at which to perform the query. + params (Optional[list[object]]): A list of parameters to pass to the query function. + + Returns: + Optional[scalecodec.ScaleType]: An object containing the requested data if found, ``None`` otherwise. + + This versatile query function is key to accessing a wide range of data and insights from different parts of the Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry() -> "ScaleType": + return self.substrate.query( + module=module, + storage_function=name, + params=params, + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), + ) + + return make_substrate_call_with_retry() + + # Common subtensor methods + def metagraph( + self, netuid: int, lite: bool = True, block: Optional[int] = None + ) -> "Metagraph": # type: ignore + """ + Returns a synced metagraph for a specified subnet within the Bittensor network. The metagraph represents the network's structure, including neuron connections and interactions. + + Args: + netuid (int): The network UID of the subnet to query. + lite (bool): If true, returns a metagraph using a lightweight sync (no weights, no bonds). Default is ``True``. + block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. + + Returns: + bittensor.core.metagraph.Metagraph: The metagraph representing the subnet's structure and neuron relationships. + + The metagraph is an essential tool for understanding the topology and dynamics of the Bittensor network's decentralized architecture, particularly in relation to neuron interconnectivity and consensus processes. + """ + metagraph = Metagraph( + network=self.network, netuid=netuid, lite=lite, sync=False + ) + metagraph.sync(block=block, lite=lite, subtensor=self) + + return metagraph + + @staticmethod + def determine_chain_endpoint_and_network( + network: str, + ) -> tuple[Optional[str], Optional[str]]: + """Determines the chain endpoint and network from the passed network or chain_endpoint. + + Args: + network (str): The network flag. The choices are: ``finney`` (main network), ``archive`` (archive network +300 blocks), ``local`` (local running network), ``test`` (test network). + + Returns: + tuple[Optional[str], Optional[str]]: The network and chain endpoint flag. If passed, overrides the ``network`` argument. + """ + + if network is None: + return None, None + if network in ["finney", "local", "test", "archive"]: + if network == "finney": + # Kiru Finney staging network. + return network, settings.FINNEY_ENTRYPOINT + elif network == "local": + return network, settings.LOCAL_ENTRYPOINT + elif network == "test": + return network, settings.FINNEY_TEST_ENTRYPOINT + elif network == "archive": + return network, settings.ARCHIVE_ENTRYPOINT + else: + if ( + network == settings.FINNEY_ENTRYPOINT + or "entrypoint-finney.opentensor.ai" in network + ): + return "finney", settings.FINNEY_ENTRYPOINT + elif ( + network == settings.FINNEY_TEST_ENTRYPOINT + or "test.finney.opentensor.ai" in network + ): + return "test", settings.FINNEY_TEST_ENTRYPOINT + elif ( + network == settings.ARCHIVE_ENTRYPOINT + or "archive.chain.opentensor.ai" in network + ): + return "archive", settings.ARCHIVE_ENTRYPOINT + elif "127.0.0.1" in network or "localhost" in network: + return "local", network + else: + return "unknown", network + return None, None + + def get_netuids_for_hotkey( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> list[int]: + """ + Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function identifies the specific subnets within the Bittensor network where the neuron associated with the hotkey is active. + + Args: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number at which to perform the query. + + Returns: + list[int]: A list of netuids where the neuron is a member. + """ + result = self.query_map_subtensor("IsNetworkMember", block, [hotkey_ss58]) + return ( + [record[0].value for record in result if record[1]] + if result and hasattr(result, "records") + else [] + ) + + @networking.ensure_connected + def get_current_block(self) -> int: + """ + Returns the current block number on the Bittensor blockchain. This function provides the latest block number, indicating the most recent state of the blockchain. + + Returns: + int: The current chain block number. + + Knowing the current block number is essential for querying real-time data and performing time-sensitive operations on the blockchain. It serves as a reference point for network activities and data synchronization. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + return self.substrate.get_block_number(None) # type: ignore + + return make_substrate_call_with_retry() + + def is_hotkey_registered_any( + self, hotkey_ss58: str, block: Optional[int] = None + ) -> bool: + """ + Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. + + Args: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int]): The blockchain block number at which to perform the check. + + Returns: + bool: ``True`` if the hotkey is registered on any subnet, False otherwise. + + This function is essential for determining the network-wide presence and participation of a neuron. + """ + return len(self.get_netuids_for_hotkey(hotkey_ss58, block)) > 0 + + def is_hotkey_registered_on_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> bool: + """ + Checks if a neuron's hotkey is registered on a specific subnet within the Bittensor network. + + Args: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number at which to perform the check. + + Returns: + bool: ``True`` if the hotkey is registered on the specified subnet, False otherwise. + + This function helps in assessing the participation of a neuron in a particular subnet, indicating its specific area of operation or influence within the network. + """ + return self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block) is not None + + def is_hotkey_registered( + self, + hotkey_ss58: str, + netuid: Optional[int] = None, + block: Optional[int] = None, + ) -> bool: + """ + Determines whether a given hotkey (public key) is registered in the Bittensor network, either globally across any subnet or specifically on a specified subnet. This function checks the registration status of a neuron identified by its hotkey, which is crucial for validating its participation and activities within the network. + + Args: + hotkey_ss58 (str): The SS58 address of the neuron's hotkey. + netuid (Optional[int]): The unique identifier of the subnet to check the registration. If ``None``, the registration is checked across all subnets. + block (Optional[int]): The blockchain block number at which to perform the query. + + Returns: + bool: ``True`` if the hotkey is registered in the specified context (either any subnet or a specific subnet), ``False`` otherwise. + + This function is important for verifying the active status of neurons in the Bittensor network. It aids in understanding whether a neuron is eligible to participate in network processes such as consensus, validation, and incentive distribution based on its registration status. + """ + if netuid is None: + return self.is_hotkey_registered_any(hotkey_ss58, block) + else: + return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) + + # Not used in Bittensor, but is actively used by the community in almost all subnets + def set_weights( + self, + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], "torch.LongTensor", list], + weights: Union[NDArray[np.float32], "torch.FloatTensor", list], + version_key: int = settings.version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, + max_retries: int = 5, + ) -> tuple[bool, str]: + """ + Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or trust a neuron places on other neurons in the network, which is a fundamental aspect of Bittensor's decentralized learning architecture. + + Args: + 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``. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. Default is ``False``. + max_retries (int): The number of maximum attempts to set weights. Default is ``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. + + 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】. + """ + uid = self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to set weights!" + while ( + self.blocks_since_last_update(netuid, uid) > self.weights_rate_limit(netuid) # type: ignore + and retries < max_retries + ): + try: + logging.info( + f"Setting weights for subnet #{netuid}. Attempt {retries + 1} of {max_retries}." + ) + success, message = set_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=uids, + weights=weights, + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + except Exception as e: + logging.error(f"Error setting weights: {e}") + finally: + retries += 1 + + return success, message + + def serve_axon( + self, + netuid: int, + axon: "Axon", + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> bool: + """ + Registers an ``Axon`` serving endpoint on the Bittensor network for a specific neuron. This function is used to set up the Axon, a key component of a neuron that handles incoming queries and data processing tasks. + + Args: + netuid (int): The unique identifier of the subnetwork. + axon (bittensor.core.axon.Axon): The Axon instance to be registered for serving. + 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 ``True``. + + Returns: + bool: ``True`` if the Axon serve registration is successful, False otherwise. + + By registering an Axon, the neuron becomes an active part of the network's distributed computing infrastructure, contributing to the collective intelligence of Bittensor. + """ + return serve_axon_extrinsic( + self, netuid, axon, wait_for_inclusion, wait_for_finalization + ) + + # metagraph + @property + def block(self) -> int: + """Returns current chain block. + + Returns: + block (int): Current chain block. + """ + return self.get_current_block() + + 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. + + Args: + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. + + Returns: + Optional[int]: The number of blocks since the last update, or ``None`` if the subnetwork or UID does not exist. + """ + call = self._get_hyperparameter(param_name="LastUpdate", netuid=netuid) + return None if call is None else self.get_current_block() - int(call[uid]) + + @networking.ensure_connected + def get_block_hash(self, block_id: int) -> str: + """ + Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique identifier representing the cryptographic hash of the block's content, ensuring its integrity and immutability. + + Args: + block_id (int): The block number for which the hash is to be retrieved. + + Returns: + str: The cryptographic hash of the specified block. + + The block hash is a fundamental aspect of blockchain technology, providing a secure reference to each block's data. It is crucial for verifying transactions, ensuring data consistency, and maintaining the trustworthiness of the blockchain. + """ + return self.substrate.get_block_hash(block_id=block_id) + + def weights_rate_limit(self, netuid: int) -> Optional[int]: + """ + Returns network WeightsSetRateLimit hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + + Returns: + Optional[int]: The value of the WeightsSetRateLimit hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. + """ + call = self._get_hyperparameter(param_name="WeightsSetRateLimit", netuid=netuid) + return None if call is None else int(call) + + # Keep backwards compatibility for community usage. + # Make some commitment on-chain about arbitrary data. + def commit(self, wallet, netuid: int, data: str): + """ + Commits arbitrary data to the Bittensor network by publishing metadata. + + Args: + 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. + """ + publish_metadata(self, wallet, netuid, f"Raw{len(data)}", data.encode()) + + # Keep backwards compatibility for community usage. + def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + """ + Returns network SubnetworkN hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. + + Returns: + Optional[int]: The value of the SubnetworkN hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. + """ + call = self._get_hyperparameter( + param_name="SubnetworkN", netuid=netuid, block=block + ) + return None if call is None else int(call) + + # Community uses this method + def transfer( + self, + wallet: "Wallet", + dest: str, + amount: Union["Balance", float], + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> bool: + """ + Executes a transfer of funds from the provided wallet to the specified destination address. This function is used to move TAO tokens within the Bittensor network, facilitating transactions between neurons. + + Args: + wallet (bittensor_wallet.Wallet): The wallet from which funds are being transferred. + dest (str): The destination public key address. + amount (Union[bittensor.utils.balance.Balance, float]): The amount of TAO to be transferred. + 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``. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. Default is ``False``. + + Returns: + transfer_extrinsic (bool): ``True`` if the transfer is successful, False otherwise. + + This function is essential for the fluid movement of tokens in the network, supporting various economic activities such as staking, delegation, and reward distribution. + """ + return transfer_extrinsic( + subtensor=self, + wallet=wallet, + dest=dest, + amount=amount, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + + # Community uses this method via `bittensor.api.extrinsics.prometheus.prometheus_extrinsic` + def get_neuron_for_pubkey_and_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Optional["NeuronInfo"]: + """ + Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific subnet UID (netuid). This function provides detailed neuron information for a particular subnet within the Bittensor network. + + Args: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number at which to perform the query. + + Returns: + Optional[bittensor.core.chain_data.neuron_info.NeuronInfo]: Detailed information about the neuron if found, ``None`` otherwise. + + This function is crucial for accessing specific neuron data and understanding its status, stake, and other attributes within a particular subnet of the Bittensor ecosystem. + """ + return self.neuron_for_uid( + self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block=block), + netuid, + block=block, + ) + + @networking.ensure_connected + def neuron_for_uid( + self, uid: Optional[int], netuid: int, block: Optional[int] = None + ) -> "NeuronInfo": + """ + Retrieves detailed information about a specific neuron identified by its unique identifier (UID) within a specified subnet (netuid) of the Bittensor network. This function provides a comprehensive view of a neuron's attributes, including its stake, rank, and operational status. + + Args: + uid (Optional[int]): The unique identifier of the neuron. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + bittensor.core.chain_data.neuron_info.NeuronInfo: Detailed information about the neuron if found, ``None`` otherwise. + + This function is crucial for analyzing individual neurons' contributions and status within a specific subnet, offering insights into their roles in the network's consensus and validation mechanisms. + """ + if uid is None: + return NeuronInfo.get_null_neuron() + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + block_hash = None if block is None else self.substrate.get_block_hash(block) + params = [netuid, uid] + if block_hash: + params = params + [block_hash] + return self.substrate.rpc_request( + method="neuronInfo_getNeuron", + params=params, # custom rpc method + ) + + json_body = make_substrate_call_with_retry() + + if not (result := json_body.get("result", None)): + return NeuronInfo.get_null_neuron() + + return NeuronInfo.from_vec_u8(result) + + # Community uses this method + def serve_prometheus( + self, + wallet: "Wallet", + port: int, + netuid: int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + ) -> bool: + """ + Serves Prometheus metrics by submitting an extrinsic to a blockchain network via the specified wallet. The function allows configuring whether to wait for the transaction's inclusion in a block and its finalization. + + Args: + wallet (bittensor_wallet.Wallet): Bittensor wallet instance used for submitting the extrinsic. + port (int): The port number on which Prometheus metrics are served. + netuid (int): The unique identifier of the subnetwork. + wait_for_inclusion (bool): If True, waits for the transaction to be included in a block. Defaults to ``False``. + wait_for_finalization (bool): If True, waits for the transaction to be finalized. Defaults to ``True``. + + Returns: + bool: Returns True if the Prometheus extrinsic is successfully processed, otherwise False. + """ + return prometheus_extrinsic( + self, + wallet=wallet, + port=port, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Community uses this method + def get_subnet_hyperparameters( + self, netuid: int, block: Optional[int] = None + ) -> Optional[Union[list, "SubnetHyperparameters"]]: + """ + Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters define the operational settings and rules governing the subnet's behavior. + + Args: + netuid (int): The network UID of the subnet to query. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[bittensor.core.chain_data.subnet_hyperparameters.SubnetHyperparameters]: The subnet's hyperparameters, or ``None`` if not available. + + Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how they interact with the network's consensus and incentive mechanisms. + """ + hex_bytes_result = self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], + block=block, + ) + + if hex_bytes_result is None: + return [] + + if hex_bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + else: + bytes_result = bytes.fromhex(hex_bytes_result) + + return SubnetHyperparameters.from_vec_u8(bytes_result) # type: ignore + + # Community uses this method + # Returns network ImmunityPeriod hyper parameter. + def immunity_period( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the duration during which new neurons are protected from certain network penalties or restrictions. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, ``None`` otherwise. + + The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new participants have a grace period to establish themselves and contribute to the network without facing immediate punitive actions. + """ + call = self._get_hyperparameter( + param_name="ImmunityPeriod", netuid=netuid, block=block + ) + return None if call is None else int(call) + + # Community uses this method + def get_uid_for_hotkey_on_subnet( + self, hotkey_ss58: str, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. + + Args: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The UID of the neuron if it is registered on the subnet, ``None`` otherwise. + + The UID is a critical identifier within the network, linking the neuron's hotkey to its operational and governance activities on a particular subnet. + """ + _result = self.query_subtensor("Uids", block, [netuid, hotkey_ss58]) + return getattr(_result, "value", None) + + # Community uses this method + def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: + """ + Returns network Tempo hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. + + Returns: + Optional[int]: The value of the Tempo hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. + """ + call = self._get_hyperparameter(param_name="Tempo", netuid=netuid, block=block) + return None if call is None else int(call) + + # Community uses this method + def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: + """ + Retrieves the on-chain commitment for a specific neuron in the Bittensor network. + + Args: + netuid (int): The unique identifier of the subnetwork. + uid (int): The unique identifier of the neuron. + block (Optional[int]): The block number to retrieve the commitment from. If None, the latest block is used. Default is ``None``. + + Returns: + str: The commitment data as a string. + """ + metagraph = self.metagraph(netuid) + hotkey = metagraph.hotkeys[uid] # type: ignore + + metadata = get_metadata(self, netuid, hotkey, block) + commitment = metadata["info"]["fields"][0] # type: ignore + hex_data = commitment[list(commitment.keys())[0]][2:] # type: ignore + + return bytes.fromhex(hex_data).decode() + + # Community uses this via `bittensor.utils.weight_utils.process_weights_for_netuid` function. + def min_allowed_weights( + self, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Returns network MinAllowedWeights hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. + + Returns: + Optional[int]: The value of the MinAllowedWeights hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. + """ + call = self._get_hyperparameter( + param_name="MinAllowedWeights", block=block, netuid=netuid + ) + return None if call is None else int(call) + + # Community uses this via `bittensor.utils.weight_utils.process_weights_for_netuid` function. + def max_weight_limit( + self, netuid: int, block: Optional[int] = None + ) -> Optional[float]: + """ + Returns network MaxWeightsLimit hyperparameter. + + Args: + netuid (int): The unique identifier of the subnetwork. + block (Optional[int]): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. + + Returns: + Optional[float]: The value of the MaxWeightsLimit hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. + """ + call = self._get_hyperparameter( + param_name="MaxWeightsLimit", block=block, netuid=netuid + ) + return None if call is None else u16_normalized_float(int(call)) + + # # Community uses this method. It is used in subtensor in neuron_info, and serving. + def get_prometheus_info( + self, netuid: int, hotkey_ss58: str, block: Optional[int] = None + ) -> Optional["PrometheusInfo"]: + """ + Returns the prometheus information for this hotkey account. + + Args: + netuid (int): The unique identifier of the subnetwork. + hotkey_ss58 (str): The SS58 address of the hotkey. + block (Optional[int]): The block number to retrieve the prometheus information from. If ``None``, the latest block is used. Default is ``None``. + + Returns: + Optional[bittensor.core.chain_data.prometheus_info.PrometheusInfo]: A PrometheusInfo object containing the prometheus information, or ``None`` if the prometheus information is not found. + """ + result = self.query_subtensor("Prometheus", block, [netuid, hotkey_ss58]) + if result is not None and hasattr(result, "value"): + return PrometheusInfo( + ip=networking.int_to_ip(result.value["ip"]), + ip_type=result.value["ip_type"], + port=result.value["port"], + version=result.value["version"], + block=result.value["block"], + ) + return None + + # Community uses this method + def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: + """ + Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number at which to check the subnet's existence. + + Returns: + bool: ``True`` if the subnet exists, False otherwise. + + This function is critical for verifying the presence of specific subnets in the network, enabling a deeper understanding of the network's structure and composition. + """ + _result = self.query_subtensor("NetworksAdded", block, [netuid]) + return getattr(_result, "value", False) + + # Metagraph uses this method + def bonds( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[int, list[tuple[int, int]]]]: + """ + Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. Bonds represent the investments or commitments made by neurons in one another, indicating a level of trust and perceived value. This bonding mechanism is integral to the network's market-based approach to measuring and rewarding machine intelligence. + + Args: + netuid (int): The network UID of the subnet to query. + block (Optional[int]): The blockchain block number for the query. + + Returns: + list[tuple[int, list[tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its bonds with other neurons. + + Understanding bond distributions is crucial for analyzing the trust dynamics and market behavior within the subnet. It reflects how neurons recognize and invest in each other's intelligence and contributions, supporting diverse and niche systems within the Bittensor ecosystem. + """ + b_map = [] + b_map_encoded = self.query_map_subtensor( + name="Bonds", block=block, params=[netuid] + ) + if b_map_encoded.records: + for uid, b in b_map_encoded: + b_map.append((uid.serialize(), b.serialize())) + + return b_map + + # Metagraph uses this method + def neurons(self, netuid: int, block: Optional[int] = None) -> list["NeuronInfo"]: + """ + Retrieves a list of all neurons within a specified subnet of the Bittensor network. This function provides a snapshot of the subnet's neuron population, including each neuron's attributes and network interactions. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + list[bittensor.core.chain_data.neuron_info.NeuronInfo]: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. + + Understanding the distribution and status of neurons within a subnet is key to comprehending the network's decentralized structure and the dynamics of its consensus and governance processes. + """ + neurons_lite = self.neurons_lite(netuid=netuid, block=block) + weights = self.weights(block=block, netuid=netuid) + bonds = self.bonds(block=block, netuid=netuid) + + weights_as_dict = {uid: w for uid, w in weights} + bonds_as_dict = {uid: b for uid, b in bonds} + + neurons = [ + NeuronInfo.from_weights_bonds_and_neuron_lite( + neuron_lite, weights_as_dict, bonds_as_dict + ) + for neuron_lite in neurons_lite + ] + + return neurons + + # Metagraph uses this method + def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: + """ + Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. + + Args: + block (Optional[int]): The blockchain block number for the query. + + Returns: + Optional[int]: The total number of subnets in the network. + + Understanding the total number of subnets is essential for assessing the network's growth and the extent of its decentralized infrastructure. + """ + _result = self.query_subtensor("TotalNetworks", block) + return getattr(_result, "value", None) + + # Metagraph uses this method + def get_subnets(self, block: Optional[int] = None) -> list[int]: + """ + Retrieves a list of all subnets currently active within the Bittensor network. This function provides an overview of the various subnets and their identifiers. + + Args: + block (Optional[int]): The blockchain block number for the query. + + Returns: + list[int]: A list of network UIDs representing each active subnet. + + This function is valuable for understanding the network's structure and the diversity of subnets available for neuron participation and collaboration. + """ + result = self.query_map_subtensor("NetworksAdded", block) + return ( + [network[0].value for network in result.records if network[1]] + if result and hasattr(result, "records") + else [] + ) + + # Metagraph uses this method + def neurons_lite( + self, netuid: int, block: Optional[int] = None + ) -> list["NeuronInfoLite"]: + """ + Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. This function provides a streamlined view of the neurons, focusing on key attributes such as stake and network participation. + + Args: + netuid (int): The unique identifier of the subnet. + block (Optional[int]): The blockchain block number for the query. + + Returns: + list[bittensor.core.chain_data.neuron_info_lite.NeuronInfoLite]: A list of simplified neuron information for the subnet. + + This function offers a quick overview of the neuron population within a subnet, facilitating efficient analysis of the network's decentralized structure and neuron dynamics. + """ + hex_bytes_result = self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons_lite", + params=[netuid], + block=block, + ) + + if hex_bytes_result is None: + return [] + + if hex_bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + else: + bytes_result = bytes.fromhex(hex_bytes_result) + + return NeuronInfoLite.list_from_vec_u8(bytes_result) # type: ignore + + # Used in the `neurons` method which is used in metagraph.py + def weights( + self, netuid: int, block: Optional[int] = None + ) -> list[tuple[int, list[tuple[int, int]]]]: + """ + Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the network's trust and value assignment mechanisms. + + Args: + netuid (int): The network UID of the subnet to query. + block (Optional[int]): The blockchain block number for the query. + + Returns: + list[tuple[int, list[tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its assigned weights. + + The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, influencing their influence and reward allocation within the subnet. + """ + w_map = [] + w_map_encoded = self.query_map_subtensor( + name="Weights", block=block, params=[netuid] + ) + if w_map_encoded.records: + for uid, w in w_map_encoded: + w_map.append((uid.serialize(), w.serialize())) + + return w_map + + # Used by community via `transfer_extrinsic` + @networking.ensure_connected + def get_balance(self, address: str, block: Optional[int] = None) -> "Balance": + """ + Retrieves the token balance of a specific address within the Bittensor network. This function queries the blockchain to determine the amount of Tao held by a given account. + + Args: + address (str): The Substrate address in ``ss58`` format. + block (Optional[int]): The blockchain block number at which to perform the query. + + Returns: + bittensor.utils.balance.Balance: The account balance at the specified block, represented as a Balance object. + + This function is important for monitoring account holdings and managing financial transactions within the Bittensor ecosystem. It helps in assessing the economic status and capacity of network participants. + """ + try: + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logging) + def make_substrate_call_with_retry(): + return self.substrate.query( + module="System", + storage_function="Account", + params=[address], + block_hash=( + None if block is None else self.substrate.get_block_hash(block) + ), + ) + + result = make_substrate_call_with_retry() + + except RemainingScaleBytesNotEmptyException: + logging.error( + "Received a corrupted message. This likely points to an error with the network or subnet." + ) + return Balance(1000) + return Balance(result.value["data"]["free"]) + + # Used in community via `bittensor.core.subtensor.Subtensor.transfer` + @networking.ensure_connected + def get_transfer_fee( + self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] + ) -> "Balance": + """ + Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. This function simulates the transfer to estimate the associated cost, taking into account the current network conditions and transaction complexity. + + Args: + wallet (bittensor_wallet.Wallet): The wallet from which the transfer is initiated. + dest (str): The ``SS58`` address of the destination account. + value (Union[bittensor.utils.balance.Balance, float, int]): The amount of tokens to be transferred, specified as a Balance object, or in Tao (float) or Rao (int) units. + + Returns: + bittensor.utils.balance.Balance: The estimated transaction fee for the transfer, represented as a Balance object. + + Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the wallet has sufficient funds to cover both the transfer amount and the associated costs. This function provides a crucial tool for managing financial operations within the Bittensor network. + """ + if isinstance(value, float): + value = Balance.from_tao(value) + elif isinstance(value, int): + value = Balance.from_rao(value) + + if isinstance(value, Balance): + call = self.substrate.compose_call( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": dest, "value": value.rao}, + ) + + try: + payment_info = self.substrate.get_payment_info( + call=call, keypair=wallet.coldkeypub + ) + except Exception as e: + settings.bt_console.print( + f":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n {e}[/bold white]" + ) + payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao + + fee = Balance.from_rao(payment_info["partialFee"]) + return fee + else: + fee = Balance.from_rao(int(2e7)) + logging.error( + "To calculate the transaction fee, the value must be Balance, float, or int. Received type: %s. Fee " + "is %s", + type(value), + 2e7, + ) + return fee + + # Used in community via `bittensor.core.subtensor.Subtensor.transfer` + def get_existential_deposit( + self, block: Optional[int] = None + ) -> Optional["Balance"]: + """ + Retrieves the existential deposit amount for the Bittensor blockchain. The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. Accounts with balances below this threshold can be reaped to conserve network resources. + + Args: + block (Optional[int]): Block number at which to query the deposit amount. If ``None``, the current block is used. + + Returns: + Optional[bittensor.utils.balance.Balance]: The existential deposit amount, or ``None`` if the query fails. + + The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of storage and preventing the proliferation of dust accounts. + """ + result = self.query_constant( + module_name="Balances", constant_name="ExistentialDeposit", block=block + ) + if result is None or not hasattr(result, "value"): + return None + return Balance.from_rao(result.value) + + # Community uses this method + def commit_weights( + self, + wallet: "Wallet", + netuid: int, + salt: list[int], + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + version_key: int = settings.version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, + max_retries: int = 5, + ) -> tuple[bool, str]: + """ + Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. + This action serves as a commitment or snapshot of the neuron's current weight distribution. + + Args: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron committing the weights. + netuid (int): The unique identifier of the subnet. + salt (list[int]): list of randomly generated integers as salt to generated weighted hash. + 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.``. + 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``. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. Default is ``False``. + max_retries (int): The number of maximum attempts to commit weights. Default is ``5``. + + Returns: + tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. + """ + retries = 0 + success = False + 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}, version_key={version_key}" + ) + + # Generate the hash of the weights + commit_hash = generate_weight_hash( + address=wallet.hotkey.ss58_address, + netuid=netuid, + uids=list(uids), + values=list(weights), + salt=salt, + version_key=version_key, + ) + + logging.info(f"Commit Hash: {commit_hash}") + + while retries < max_retries: + try: + success, message = commit_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + if success: + break + except Exception as e: + logging.error(f"Error committing weights: {e}") + finally: + retries += 1 + + return success, message + + # Community uses this method + def reveal_weights( + self, + wallet: "Wallet", + netuid: int, + uids: Union[NDArray[np.int64], list], + weights: Union[NDArray[np.int64], list], + salt: Union[NDArray[np.int64], list], + version_key: int = settings.version_as_int, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + prompt: bool = False, + max_retries: int = 5, + ) -> tuple[bool, str]: + """ + Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. + This action serves as a revelation of the neuron's previously committed weight distribution. + + Args: + wallet (bittensor_wallet.Wallet): The wallet associated with the neuron revealing the weights. + netuid (int): The unique identifier of the subnet. + uids (np.ndarray): NumPy array of neuron UIDs for which weights are being revealed. + 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``. + 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``. + prompt (bool): If ``True``, prompts for user confirmation before proceeding. Default is ``False``. + max_retries (int): The number of maximum attempts to reveal weights. Default is ``5``. + + Returns: + tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string + value describing the success or potential error. + + This function allows neurons to reveal their previously committed weight distribution, ensuring transparency + and accountability within the Bittensor network. + """ + + retries = 0 + success = False + message = "No attempt made. Perhaps it is too soon to reveal weights!" + + while retries < max_retries: + try: + success, message = reveal_weights_extrinsic( + subtensor=self, + wallet=wallet, + netuid=netuid, + uids=list(uids), + weights=list(weights), + salt=list(salt), + version_key=version_key, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + if success: + break + except Exception as e: + logging.error(f"Error revealing weights: {e}") + finally: + retries += 1 + + return success, message + + # Subnet 27 uses this method + _do_serve_prometheus = do_serve_prometheus + # Subnet 27 uses this method name + _do_serve_axon = do_serve_axon diff --git a/bittensor/synapse.py b/bittensor/core/synapse.py similarity index 92% rename from bittensor/synapse.py rename to bittensor/core/synapse.py index f08b5bcb3..a96a92e1a 100644 --- a/bittensor/synapse.py +++ b/bittensor/core/synapse.py @@ -1,26 +1,25 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation - +# 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 base64 import json import sys import warnings +from typing import cast, Any, ClassVar, Optional, Union from pydantic import ( BaseModel, @@ -29,19 +28,20 @@ field_validator, model_validator, ) -import bittensor -from typing import Optional, Any, Dict, ClassVar, Tuple + +from bittensor.utils import get_hash +from bittensor.utils.btlogging import logging -def get_size(obj, seen=None) -> int: +def get_size(obj: Any, seen: Optional[set] = None) -> int: """ Recursively finds size of objects. This function traverses every item of a given object and sums their sizes to compute the total size. Args: - obj (any type): The object to get the size of. - seen (set): Set of object ids that have been calculated. + obj (Any): The object to get the size of. + seen (Optional[set]): Set of object ids that have been calculated. Returns: int: The total size of the object. @@ -79,10 +79,10 @@ def cast_int(raw: str) -> int: int or None: The converted integer, or ``None`` if the input was ``None``. """ - return int(raw) if raw is not None else raw # type: ignore + return int(raw) if raw is not None else raw -def cast_float(raw: str) -> float: +def cast_float(raw: str) -> Optional[float]: """ Converts a string to a float, if the string is not ``None``. @@ -95,7 +95,7 @@ def cast_float(raw: str) -> float: float or None: The converted float, or ``None`` if the input was ``None``. """ - return float(raw) if raw is not None else raw # type: ignore + return float(raw) if raw is not None else raw class TerminalInfo(BaseModel): @@ -128,6 +128,8 @@ class TerminalInfo(BaseModel): Usage:: # Creating a TerminalInfo instance + from bittensor.core.synapse import TerminalInfo + terminal_info = TerminalInfo( status_code=200, status_message="Success", @@ -317,11 +319,14 @@ class Synapse(BaseModel): Example usage:: # Creating a Synapse instance with default values + from bittensor.core.synapse import Synapse + synapse = Synapse() # Setting properties and input synapse.timeout = 15.0 synapse.name = "MySynapse" + # Not setting fields that are not defined in your synapse class will result in an error, e.g.: synapse.dummy_input = 1 # This will raise an error because dummy_input is not defined in the Synapse class @@ -350,10 +355,10 @@ class Synapse(BaseModel): timeout (float): Total query length, set by the dendrite terminal. total_size (int): Total size of request body in bytes. header_size (int): Size of request header in bytes. - dendrite (TerminalInfo): Information about the dendrite terminal. - axon (TerminalInfo): Information about the axon terminal. + dendrite (:func:`TerminalInfo`): Information about the dendrite terminal. + axon (:func:`TerminalInfo`): Information about the axon terminal. computed_body_hash (str): Computed hash of the request body. - required_hash_fields (List[str]): Fields required to compute the body hash. + required_hash_fields (list[str]): Fields required to compute the body hash. Methods: deserialize: Custom deserialization logic for subclasses. @@ -369,7 +374,6 @@ class Synapse(BaseModel): """ model_config = ConfigDict(validate_assignment=True) - _model_json_schema: ClassVar[Dict[str, Any]] def deserialize(self) -> "Synapse": """ @@ -409,7 +413,7 @@ def deserialize(self) -> "CustomSynapse": return self @model_validator(mode="before") - def set_name_type(cls, values) -> dict: + def set_name_type(cls, values: dict) -> dict: values["name"] = cls.__name__ # type: ignore return values @@ -457,7 +461,7 @@ def set_name_type(cls, values) -> dict: dendrite: Optional[TerminalInfo] = Field( title="dendrite", description="Dendrite Terminal Information", - examples=["bittensor.TerminalInfo"], + examples=["TerminalInfo"], default=TerminalInfo(), frozen=False, repr=False, @@ -467,7 +471,7 @@ def set_name_type(cls, values) -> dict: axon: Optional[TerminalInfo] = Field( title="axon", description="Axon Terminal Information", - examples=["bittensor.TerminalInfo"], + examples=["TerminalInfo"], default=TerminalInfo(), frozen=False, repr=False, @@ -482,7 +486,7 @@ def set_name_type(cls, values) -> dict: repr=False, ) - required_hash_fields: ClassVar[Tuple[str, ...]] = () + required_hash_fields: ClassVar[tuple[str, ...]] = () _extract_total_size = field_validator("total_size", mode="before")(cast_int) @@ -581,27 +585,11 @@ def failed_verification(self) -> bool: """ return self.dendrite is not None and self.dendrite.status_code == 401 - @classmethod - def _get_cached_model_json_schema(cls) -> dict: - """ - Returns the JSON schema for the Synapse model. - - This method returns a cached version of the JSON schema for the Synapse model. - The schema is stored in the class variable ``_model_json_schema`` and is only - generated once to improve performance. - - Returns: - dict: The JSON schema for the Synapse model. - """ - if "_model_json_schema" not in cls.__dict__: - cls._model_json_schema = cls.model_json_schema() - return cls._model_json_schema - def get_required_fields(self): """ Get the required fields from the model's JSON schema. """ - schema = self._get_cached_model_json_schema() + schema = self.__class__.model_json_schema() return schema.get("required", []) def to_headers(self) -> dict: @@ -652,15 +640,16 @@ def to_headers(self) -> dict: # Getting the fields of the instance instance_fields = self.model_dump() - required = set(self.get_required_fields()) # Iterating over the fields of the instance for field, value in instance_fields.items(): # If the object is not optional, serializing it, encoding it, and adding it to the headers + required = self.get_required_fields() + # Skipping the field if it's already in the headers or its value is None if field in headers or value is None: continue - elif field in required: + elif required and field in required: try: # create an empty (dummy) instance of type(value) to pass pydantic validation on the axon side serialized_value = json.dumps(value.__class__.__call__()) @@ -733,9 +722,9 @@ def body_hash(self) -> str: if required_hash_fields: instance_fields = instance_fields or self.model_dump() for field in required_hash_fields: - hashes.append(bittensor.utils.hash(str(instance_fields[field]))) + hashes.append(get_hash(str(instance_fields[field]))) - return bittensor.utils.hash("".join(hashes)) + return get_hash("".join(hashes)) @classmethod def parse_headers_to_inputs(cls, headers: dict) -> dict: @@ -772,7 +761,10 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: """ # Initialize the input dictionary with empty sub-dictionaries for 'axon' and 'dendrite' - inputs_dict: Dict[str, Dict[str, str]] = {"axon": {}, "dendrite": {}} + inputs_dict: dict[str, Union[dict, Optional[str]]] = { + "axon": {}, + "dendrite": {}, + } # Iterate over each item in the headers for key, value in headers.items(): @@ -780,21 +772,19 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: if "bt_header_axon_" in key: try: new_key = key.split("bt_header_axon_")[1] - inputs_dict["axon"][new_key] = value + axon_dict = cast(dict, inputs_dict["axon"]) + axon_dict[new_key] = value except Exception as e: - bittensor.logging.error( - f"Error while parsing 'axon' header {key}: {e}" - ) + logging.error(f"Error while parsing 'axon' header {key}: {str(e)}") continue # Handle 'dendrite' headers elif "bt_header_dendrite_" in key: try: new_key = key.split("bt_header_dendrite_")[1] - inputs_dict["dendrite"][new_key] = value + dendrite_dict = cast(dict, inputs_dict["dendrite"]) + dendrite_dict[new_key] = value except Exception as e: - bittensor.logging.error( - f"Error while parsing 'dendrite' header {key}: {e}" - ) + logging.error(f"Error while parsing 'dendrite' header {key}: {e}") continue # Handle 'input_obj' headers elif "bt_header_input_obj" in key: @@ -808,14 +798,12 @@ def parse_headers_to_inputs(cls, headers: dict) -> dict: base64.b64decode(value.encode()).decode("utf-8") ) except json.JSONDecodeError as e: - bittensor.logging.error( + logging.error( f"Error while json decoding 'input_obj' header {key}: {e}" ) continue except Exception as e: - bittensor.logging.error( - f"Error while parsing 'input_obj' header {key}: {e}" - ) + logging.error(f"Error while parsing 'input_obj' header {key}: {e}") continue else: pass # TODO: log unexpected keys @@ -852,7 +840,7 @@ def from_headers(cls, headers: dict) -> "Synapse": headers (dict): The dictionary of headers containing serialized Synapse information. Returns: - Synapse: A new instance of Synapse, reconstructed from the parsed header information, replicating the original instance's state. + bittensor.core.synapse.Synapse: A new instance of Synapse, reconstructed from the parsed header information, replicating the original instance's state. """ # Get the inputs dictionary from the headers diff --git a/bittensor/tensor.py b/bittensor/core/tensor.py similarity index 91% rename from bittensor/tensor.py rename to bittensor/core/tensor.py index ab46560d9..4ec71cc44 100644 --- a/bittensor/tensor.py +++ b/bittensor/core/tensor.py @@ -1,29 +1,30 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation - +# 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 numpy as np import base64 +from typing import Optional, Union + import msgpack import msgpack_numpy -from typing import Optional, Union, List -from bittensor.utils.registration import torch, use_torch +import numpy as np from pydantic import ConfigDict, BaseModel, Field, field_validator +from bittensor.utils.registration import torch, use_torch + class DTypes(dict): def __init__(self, *args, **kwargs): @@ -73,9 +74,7 @@ def _add_torch(self): def cast_dtype(raw: Union[None, np.dtype, "torch.dtype", str]) -> Optional[str]: """ - Casts the raw value to a string representing the - `numpy data type `_, or the - `torch data type `_ if using torch. + Casts the raw value to a string representing the `numpy data type `_, or the `torch data type `_ if using torch. Args: raw (Union[None, numpy.dtype, torch.dtype, str]): The raw value to cast. @@ -105,12 +104,12 @@ def cast_dtype(raw: Union[None, np.dtype, "torch.dtype", str]) -> Optional[str]: ) -def cast_shape(raw: Union[None, List[int], str]) -> Optional[Union[str, list]]: +def cast_shape(raw: Union[None, list[int], str]) -> Optional[Union[str, list]]: """ Casts the raw value to a string representing the tensor shape. Args: - raw (Union[None, List[int], str]): The raw value to cast. + raw (Union[None, list[int], str]): The raw value to cast. Returns: str: The string representing the tensor shape. @@ -130,12 +129,12 @@ def cast_shape(raw: Union[None, List[int], str]) -> Optional[Union[str, list]]: return shape else: raise Exception( - f"{raw} of type {type(raw)} does not have a valid type in Union[None, List[int], str]" + f"{raw} of type {type(raw)} does not have a valid type in Union[None, list[int], str]" ) class tensor: - def __new__(cls, tensor: Union[list, np.ndarray, "torch.Tensor"]): + def __new__(cls, tensor: Union[list, "np.ndarray", "torch.Tensor"]): if isinstance(tensor, list) or isinstance(tensor, np.ndarray): tensor = torch.tensor(tensor) if use_torch() else np.array(tensor) return Tensor.serialize(tensor_=tensor) @@ -148,7 +147,7 @@ class Tensor(BaseModel): Args: buffer (Optional[str]): Tensor buffer data. dtype (str): Tensor data type. - shape (List[int]): Tensor shape. + shape (list[int]): Tensor shape. """ model_config = ConfigDict(validate_assignment=True) @@ -156,10 +155,10 @@ class Tensor(BaseModel): def tensor(self) -> Union[np.ndarray, "torch.Tensor"]: return self.deserialize() - def tolist(self) -> List[object]: + def tolist(self) -> list[object]: return self.deserialize().tolist() - def numpy(self) -> "numpy.ndarray": + def numpy(self) -> "np.ndarray": return ( self.deserialize().detach().numpy() if use_torch() else self.deserialize() ) @@ -200,7 +199,7 @@ def serialize(tensor_: Union["np.ndarray", "torch.Tensor"]) -> "Tensor": tensor_ (np.array or torch.Tensor): The tensor to serialize. Returns: - Tensor: The serialized tensor. + :func:`Tensor`: The serialized tensor. Raises: Exception: If the serialization process encounters an error. @@ -235,7 +234,7 @@ def serialize(tensor_: Union["np.ndarray", "torch.Tensor"]) -> "Tensor": ) # Represents the shape of the tensor. - shape: List[int] = Field( + shape: list[int] = Field( title="shape", description="Tensor shape. This field defines the dimensions of the tensor as a list of integers, such as [10, 10] for a 2D tensor with shape (10, 10).", examples=[10, 10], diff --git a/bittensor/threadpool.py b/bittensor/core/threadpool.py similarity index 94% rename from bittensor/threadpool.py rename to bittensor/core/threadpool.py index 3e49786ff..17e553509 100644 --- a/bittensor/threadpool.py +++ b/bittensor/core/threadpool.py @@ -5,22 +5,22 @@ __author__ = "Brian Quinlan (brian@sweetapp.com)" +import argparse +import itertools +import logging import os -import sys -import time import queue import random -import weakref -import logging -import argparse -import bittensor -import itertools +import sys import threading - -from typing import Callable +import time +import weakref from concurrent.futures import _base +from typing import Callable -from bittensor.btlogging.defines import BITTENSOR_LOGGER_NAME +from bittensor.core.config import Config +from bittensor.core.settings import BLOCKTIME +from bittensor.utils.btlogging.defines import BITTENSOR_LOGGER_NAME # Workers are created as daemon threads. This is done to allow the interpreter # to exit when there are still idle threads in a ThreadPoolExecutor's thread @@ -54,7 +54,7 @@ def run(self): """Run the given work item""" # Checks if future is canceled or if work item is stale if (not self.future.set_running_or_notify_cancel()) or ( - time.time() - self.start_time > bittensor.__blocktime__ + time.time() - self.start_time > BLOCKTIME ): return @@ -120,7 +120,7 @@ class BrokenThreadPool(_base.BrokenExecutor): class PriorityThreadPoolExecutor(_base.Executor): - """Base threadpool executor with a priority queue""" + """Base threadpool executor with a priority queue.""" # Used to assign unique thread names when thread_name_prefix is not supplied. _counter = itertools.count().__next__ @@ -168,16 +168,16 @@ def __init__( @classmethod def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): """Accept specific arguments from parser""" - prefix_str = "" if prefix == None else prefix + "." + prefix_str = "" if prefix is None else prefix + "." try: default_max_workers = ( os.getenv("BT_PRIORITY_MAX_WORKERS") - if os.getenv("BT_PRIORITY_MAX_WORKERS") != None + if os.getenv("BT_PRIORITY_MAX_WORKERS") is not None else 5 ) default_maxsize = ( os.getenv("BT_PRIORITY_MAXSIZE") - if os.getenv("BT_PRIORITY_MAXSIZE") != None + if os.getenv("BT_PRIORITY_MAXSIZE") is not None else 10 ) parser.add_argument( @@ -197,14 +197,14 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): pass @classmethod - def config(cls) -> "bittensor.config": + def config(cls) -> "Config": """Get config from the argument parser. - Return: :func:`bittensor.config` object. + Return: :func:`bittensor.Config` object. """ parser = argparse.ArgumentParser() PriorityThreadPoolExecutor.add_args(parser) - return bittensor.config(parser, args=[]) + return Config(parser, args=[]) @property def is_empty(self): diff --git a/bittensor/types.py b/bittensor/core/types.py similarity index 89% rename from bittensor/types.py rename to bittensor/core/types.py index 8aa9b7cde..9fd2b4d05 100644 --- a/bittensor/types.py +++ b/bittensor/core/types.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Technologies Inc - +# 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 @@ -19,9 +19,7 @@ class AxonServeCallParams(TypedDict): - """ - Axon serve chain call parameters. - """ + """Axon serve chain call parameters.""" version: int ip: int @@ -31,9 +29,7 @@ class AxonServeCallParams(TypedDict): class PrometheusServeCallParams(TypedDict): - """ - Prometheus serve chain call parameters. - """ + """Prometheus serve chain call parameters.""" version: int ip: int diff --git a/bittensor/extrinsics/commit_weights.py b/bittensor/extrinsics/commit_weights.py deleted file mode 100644 index a9192952e..000000000 --- a/bittensor/extrinsics/commit_weights.py +++ /dev/null @@ -1,127 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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. - -"""Module commit weights and reveal weights extrinsic.""" - -from typing import Tuple, List - -from rich.prompt import Confirm - -import bittensor - -from bittensor.utils import format_error_message - - -def commit_weights_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - netuid: int, - commit_hash: str, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> Tuple[bool, str]: - """ - Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. - This function is a wrapper around the `_do_commit_weights` method, handling user prompts and error messages. - Args: - subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor.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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - Returns: - Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. - """ - if prompt and not Confirm.ask(f"Would you like to commit weights?"): - return False, "User cancelled the operation." - - success, error_message = subtensor._do_commit_weights( - wallet=wallet, - netuid=netuid, - commit_hash=commit_hash, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success: - bittensor.logging.info("Successfully committed weights.") - return True, "Successfully committed weights." - else: - bittensor.logging.error(f"Failed to commit weights: {error_message}") - return False, format_error_message(error_message) - - -def reveal_weights_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - netuid: int, - uids: List[int], - weights: List[int], - salt: List[int], - version_key: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> 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, handling user prompts and error messages. - Args: - subtensor (bittensor.subtensor): The subtensor instance used for blockchain interaction. - wallet (bittensor.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. - weights (List[int]): List of weight values corresponding to each UID. - salt (List[int]): List of salt values corresponding to the hash function. - version_key (int): 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - Returns: - Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, 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. - """ - - if prompt and not Confirm.ask(f"Would you like to reveal weights?"): - return False, "User cancelled the operation." - - success, error_message = subtensor._do_reveal_weights( - wallet=wallet, - netuid=netuid, - uids=uids, - values=weights, - salt=salt, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success: - bittensor.logging.info("Successfully revealed weights.") - return True, "Successfully revealed weights." - else: - bittensor.logging.error(f"Failed to reveal weights: {error_message}") - return False, error_message diff --git a/bittensor/extrinsics/delegation.py b/bittensor/extrinsics/delegation.py deleted file mode 100644 index e61a97efb..000000000 --- a/bittensor/extrinsics/delegation.py +++ /dev/null @@ -1,528 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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 logging -import bittensor -from ..errors import ( - NominationError, - NotDelegateError, - NotRegisteredError, - StakeError, - TakeError, -) -from rich.prompt import Confirm -from typing import Union, Optional -from bittensor.utils.balance import Balance -from bittensor.btlogging.defines import BITTENSOR_LOGGER_NAME - -logger = logging.getLogger(BITTENSOR_LOGGER_NAME) - - -def nominate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - wait_for_finalization: bool = False, - wait_for_inclusion: bool = True, -) -> bool: - r"""Becomes a delegate for the hotkey. - - Args: - wallet (bittensor.wallet): The wallet to become a delegate for. - Returns: - success (bool): ``True`` if the transaction was successful. - """ - # Unlock the coldkey. - - try: - wallet.coldkey - - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - wallet.hotkey - # Check if the hotkey is already a delegate. - if subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - logger.error( - "Hotkey {} is already a delegate.".format(wallet.hotkey.ss58_address) - ) - return False - - if not subtensor.is_hotkey_registered_any(wallet.hotkey.ss58_address): - logger.error( - "Hotkey {} is not registered to any network".format( - wallet.hotkey.ss58_address - ) - ) - return False - - with bittensor.__console__.status( - ":satellite: Sending nominate call on [white]{}[/white] ...".format( - subtensor.network - ) - ): - try: - success = subtensor._do_nominate( - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success is True: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - bittensor.logging.success( - prefix="Become Delegate", - suffix="Finalized: " + str(success), - ) - - # Raises NominationError if False - return success - - except Exception as e: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - bittensor.logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) - except NominationError as e: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - bittensor.logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) - - return False - - -def delegate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - delegate_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - r"""Delegates the specified amount of stake to the passed delegate. - - Args: - wallet (bittensor.wallet): Bittensor wallet object. - delegate_ss58 (Optional[str]): The ``ss58`` address of the delegate. - amount (Union[Balance, float]): Amount to stake as bittensor balance, or ``float`` interpreted as Tao. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - - Raises: - NotRegisteredError: If the wallet is not registered on the chain. - NotDelegateError: If the hotkey is not a delegate on the chain. - """ - # Decrypt keys, - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - if not subtensor.is_hotkey_delegate(delegate_ss58): - raise NotDelegateError("Hotkey: {} is not a delegate.".format(delegate_ss58)) - - # Get state. - my_prev_coldkey_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - delegate_take = subtensor.get_delegate_take(delegate_ss58) - delegate_owner = subtensor.get_hotkey_owner(delegate_ss58) - my_prev_delegated_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=delegate_ss58 - ) - - # Convert to bittensor.Balance - if amount is None: - # Stake it all. - staking_balance = bittensor.Balance.from_tao(my_prev_coldkey_balance.tao) - elif not isinstance(amount, bittensor.Balance): - staking_balance = bittensor.Balance.from_tao(amount) - else: - staking_balance = amount - - # Remove existential balance to keep key alive. - if staking_balance > bittensor.Balance.from_rao(1000): - staking_balance = staking_balance - bittensor.Balance.from_rao(1000) - else: - staking_balance = staking_balance - - # Check enough balance to stake. - if staking_balance > my_prev_coldkey_balance: - bittensor.__console__.print( - ":cross_mark: [red]Not enough balance[/red]:[bold white]\n balance:{}\n amount: {}\n coldkey: {}[/bold white]".format( - my_prev_coldkey_balance, staking_balance, wallet.name - ) - ) - return False - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to delegate:[bold white]\n amount: {}\n to: {}\n owner: {}[/bold white]".format( - staking_balance, delegate_ss58, delegate_owner - ) - ): - return False - - try: - with bittensor.__console__.status( - ":satellite: Staking to: [bold white]{}[/bold white] ...".format( - subtensor.network - ) - ): - staking_response: bool = subtensor._do_delegation( - wallet=wallet, - delegate_ss58=delegate_ss58, - amount=staking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response 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 - - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - with bittensor.__console__.status( - ":satellite: Checking Balance on: [white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance(address=wallet.coldkey.ss58_address) - block = subtensor.get_current_block() - new_delegate_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=delegate_ss58, - block=block, - ) # Get current stake - - bittensor.__console__.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - my_prev_coldkey_balance, new_balance - ) - ) - bittensor.__console__.print( - "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - my_prev_delegated_stake, new_delegate_stake - ) - ) - return True - else: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: Error unknown." - ) - return False - - except NotRegisteredError as e: - bittensor.__console__.print( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) - ) - return False - except StakeError as e: - bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - return False - - -def undelegate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - delegate_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - r"""Un-delegates stake from the passed delegate. - - Args: - wallet (bittensor.wallet): Bittensor wallet object. - delegate_ss58 (Optional[str]): The ``ss58`` address of the delegate. - amount (Union[Balance, float]): Amount to unstake as bittensor balance, or ``float`` interpreted as Tao. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - - Raises: - NotRegisteredError: If the wallet is not registered on the chain. - NotDelegateError: If the hotkey is not a delegate on the chain. - """ - # Decrypt keys, - wallet.coldkey - if not subtensor.is_hotkey_delegate(delegate_ss58): - raise NotDelegateError("Hotkey: {} is not a delegate.".format(delegate_ss58)) - - # Get state. - my_prev_coldkey_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - delegate_take = subtensor.get_delegate_take(delegate_ss58) - delegate_owner = subtensor.get_hotkey_owner(delegate_ss58) - my_prev_delegated_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=delegate_ss58 - ) - - # Convert to bittensor.Balance - if amount is None: - # Stake it all. - unstaking_balance = bittensor.Balance.from_tao(my_prev_delegated_stake.tao) - - elif not isinstance(amount, bittensor.Balance): - unstaking_balance = bittensor.Balance.from_tao(amount) - - else: - unstaking_balance = amount - - # Check enough stake to unstake. - if unstaking_balance > my_prev_delegated_stake: - bittensor.__console__.print( - ":cross_mark: [red]Not enough delegated stake[/red]:[bold white]\n stake:{}\n amount: {}\n coldkey: {}[/bold white]".format( - my_prev_delegated_stake, unstaking_balance, wallet.name - ) - ) - return False - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to un-delegate:[bold white]\n amount: {}\n from: {}\n owner: {}[/bold white]".format( - unstaking_balance, delegate_ss58, delegate_owner - ) - ): - return False - - try: - with bittensor.__console__.status( - ":satellite: Unstaking from: [bold white]{}[/bold white] ...".format( - subtensor.network - ) - ): - staking_response: bool = subtensor._do_undelegation( - wallet=wallet, - delegate_ss58=delegate_ss58, - amount=unstaking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response 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 - - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - with bittensor.__console__.status( - ":satellite: Checking Balance on: [white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance(address=wallet.coldkey.ss58_address) - block = subtensor.get_current_block() - new_delegate_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=delegate_ss58, - block=block, - ) # Get current stake - - bittensor.__console__.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - my_prev_coldkey_balance, new_balance - ) - ) - bittensor.__console__.print( - "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - my_prev_delegated_stake, new_delegate_stake - ) - ) - return True - else: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: Error unknown." - ) - return False - - except NotRegisteredError as e: - bittensor.__console__.print( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) - ) - return False - except StakeError as e: - bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - return False - - -def decrease_take_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - hotkey_ss58: Optional[str] = None, - take: int = 0, - wait_for_finalization: bool = False, - wait_for_inclusion: bool = True, -) -> bool: - r"""Decrease delegate take for the hotkey. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - hotkey_ss58 (Optional[str]): - The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. - take (float): - The ``take`` of the hotkey. - Returns: - success (bool): ``True`` if the transaction was successful. - """ - # Unlock the coldkey. - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - wallet.hotkey - - with bittensor.__console__.status( - ":satellite: Sending decrease_take_extrinsic call on [white]{}[/white] ...".format( - subtensor.network - ) - ): - try: - success = subtensor._do_decrease_take( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - take=take, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success is True: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - bittensor.logging.success( - prefix="Decrease Delegate Take", - suffix="Finalized: " + str(success), - ) - - return success - - except (TakeError, Exception) as e: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - bittensor.logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) - - return False - - -def increase_take_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - hotkey_ss58: Optional[str] = None, - take: int = 0, - wait_for_finalization: bool = False, - wait_for_inclusion: bool = True, -) -> bool: - r"""Increase delegate take for the hotkey. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - hotkey_ss58 (Optional[str]): - The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. - take (float): - The ``take`` of the hotkey. - Returns: - success (bool): ``True`` if the transaction was successful. - """ - # Unlock the coldkey. - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - wallet.hotkey - - with bittensor.__console__.status( - ":satellite: Sending increase_take_extrinsic call on [white]{}[/white] ...".format( - subtensor.network - ) - ): - try: - success = subtensor._do_increase_take( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - take=take, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success is True: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - bittensor.logging.success( - prefix="Increase Delegate Take", - suffix="Finalized: " + str(success), - ) - - return success - - except Exception as e: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - bittensor.logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) - except TakeError as e: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - bittensor.logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) - - return False diff --git a/bittensor/extrinsics/network.py b/bittensor/extrinsics/network.py deleted file mode 100644 index 5aecaa459..000000000 --- a/bittensor/extrinsics/network.py +++ /dev/null @@ -1,250 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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 time - -import substrateinterface -from rich.prompt import Confirm - -import bittensor -from bittensor.utils import format_error_message -from ..commands.network import HYPERPARAMS - - -def _find_event_attributes_in_extrinsic_receipt( - response: "substrateinterface.base.ExtrinsicReceipt", event_name: str -) -> list: - """ - Searches for the attributes of a specified event within an extrinsic receipt. - - Args: - response (substrateinterface.base.ExtrinsicReceipt): The receipt of the extrinsic to be searched. - event_name (str): The name of the event to search for. - - Returns: - list: A list of attributes for the specified event. Returns [-1] if the event is not found. - """ - for event in response.triggered_events: - # Access the event details - event_details = event.value["event"] - # Check if the event_id is 'NetworkAdded' - if event_details["event_id"] == event_name: - # Once found, you can access the attributes of the event_name - return event_details["attributes"] - return [-1] - - -def register_subnetwork_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - r"""Registers a new subnetwork. - - Args: - wallet (bittensor.wallet): - bittensor wallet object. - 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. - prompt (bool): - If true, the call waits for confirmation from the user before proceeding. - 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``. - """ - your_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - burn_cost = bittensor.utils.balance.Balance(subtensor.get_subnet_burn_cost()) - if burn_cost > your_balance: - bittensor.__console__.print( - f"Your balance of: [green]{your_balance}[/green] is not enough to pay the subnet lock cost of: [green]{burn_cost}[/green]" - ) - return False - - if prompt: - bittensor.__console__.print(f"Your balance is: [green]{your_balance}[/green]") - if not Confirm.ask( - f"Do you want to register a subnet for [green]{ burn_cost }[/green]?" - ): - return False - - try: - wallet.coldkey # unlock coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - with bittensor.__console__.status(":satellite: Registering subnet..."): - with subtensor.substrate as substrate: - # create extrinsic call - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="register_network", - call_params={"immunity_period": 0, "reg_allowed": True}, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if registration successful - response.process_events() - if not response.is_success: - bittensor.__console__.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" - ) - time.sleep(0.5) - - # Successful registration, final check for membership - else: - attributes = _find_event_attributes_in_extrinsic_receipt( - response, "NetworkAdded" - ) - bittensor.__console__.print( - f":white_heavy_check_mark: [green]Registered subnetwork with netuid: {attributes[0]}[/green]" - ) - return True - - -def set_hyperparameter_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - netuid: int, - parameter: str, - value, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - r"""Sets a hyperparameter for a specific subnetwork. - - Args: - wallet (bittensor.wallet): - bittensor wallet object. - netuid (int): - Subnetwork ``uid``. - parameter (str): - Hyperparameter name. - value (any): - New hyperparameter value. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - 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``. - """ - if subtensor.get_subnet_owner(netuid) != wallet.coldkeypub.ss58_address: - bittensor.__console__.print( - ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" - ) - return False - - wallet.coldkey # unlock coldkey - - extrinsic = HYPERPARAMS.get(parameter) - if extrinsic is None: - bittensor.__console__.print( - ":cross_mark: [red]Invalid hyperparameter specified.[/red]" - ) - return False - - with bittensor.__console__.status( - f":satellite: Setting hyperparameter {parameter} to {value} on subnet: {netuid} ..." - ): - with subtensor.substrate as substrate: - extrinsic_params = substrate.get_metadata_call_function( - "AdminUtils", extrinsic - ) - call_params = {"netuid": netuid} - - # if input value is a list, iterate through the list and assign values - if isinstance(value, list): - # Create an iterator for the list of values - value_iterator = iter(value) - # Iterate over all value arguments and add them to the call_params dictionary - for value_argument in extrinsic_params["fields"]: - if "netuid" not in str(value_argument["name"]): - # Assign the next value from the iterator - try: - call_params[str(value_argument["name"])] = next( - value_iterator - ) - except StopIteration: - raise ValueError( - "Not enough values provided in the list for all parameters" - ) - - else: - value_argument = extrinsic_params["fields"][ - len(extrinsic_params["fields"]) - 1 - ] - call_params[str(value_argument["name"])] = value - - # create extrinsic call - call = substrate.compose_call( - call_module="AdminUtils", - call_function=extrinsic, - call_params=call_params, - ) - - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if registration successful - response.process_events() - if not response.is_success: - bittensor.__console__.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" - ) - time.sleep(0.5) - - # Successful registration, final check for membership - else: - bittensor.__console__.print( - f":white_heavy_check_mark: [green]Hyper parameter {parameter} changed to {value}[/green]" - ) - return True diff --git a/bittensor/extrinsics/prometheus.py b/bittensor/extrinsics/prometheus.py deleted file mode 100644 index 97f7c1771..000000000 --- a/bittensor/extrinsics/prometheus.py +++ /dev/null @@ -1,140 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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 bittensor - -import json -import bittensor.utils.networking as net - - -def prometheus_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - port: int, - netuid: int, - ip: int = None, - wait_for_inclusion: bool = False, - wait_for_finalization=True, -) -> bool: - r"""Subscribes an Bittensor endpoint to the substensor chain. - - Args: - subtensor (bittensor.subtensor): - Bittensor subtensor object. - wallet (bittensor.wallet): - Bittensor wallet object. - ip (str): - Endpoint host port i.e., ``192.122.31.4``. - port (int): - Endpoint port number i.e., `9221`. - netuid (int): - Network `uid` to serve on. - 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. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. - If we did not wait for finalization / inclusion, the response is ``true``. - """ - - # ---- Get external ip ---- - if ip is None: - try: - external_ip = net.get_external_ip() - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Found external ip: {}[/green]".format( - external_ip - ) - ) - bittensor.logging.success( - prefix="External IP", suffix="{}".format(external_ip) - ) - except Exception as E: - raise RuntimeError( - "Unable to attain your external ip. Check your internet connection. error: {}".format( - E - ) - ) from E - else: - external_ip = ip - - call_params: "bittensor.PrometheusServeCallParams" = { - "version": bittensor.__version_as_int__, - "ip": net.ip_to_int(external_ip), - "port": port, - "ip_type": net.ip_version(external_ip), - } - - with bittensor.__console__.status(":satellite: Checking Prometheus..."): - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid - ) - neuron_up_to_date = not neuron.is_null and call_params == { - "version": neuron.prometheus_info.version, - "ip": net.ip_to_int(neuron.prometheus_info.ip), - "port": neuron.prometheus_info.port, - "ip_type": neuron.prometheus_info.ip_type, - } - - if neuron_up_to_date: - bittensor.__console__.print( - f":white_heavy_check_mark: [green]Prometheus already Served[/green]\n" - f"[green not bold]- Status: [/green not bold] |" - f"[green not bold] ip: [/green not bold][white not bold]{net.int_to_ip(neuron.prometheus_info.ip)}[/white not bold] |" - f"[green not bold] ip_type: [/green not bold][white not bold]{neuron.prometheus_info.ip_type}[/white not bold] |" - f"[green not bold] port: [/green not bold][white not bold]{neuron.prometheus_info.port}[/white not bold] | " - f"[green not bold] version: [/green not bold][white not bold]{neuron.prometheus_info.version}[/white not bold] |" - ) - - bittensor.__console__.print( - ":white_heavy_check_mark: [white]Prometheus already served.[/white]".format( - external_ip - ) - ) - return True - - # Add netuid, not in prometheus_info - call_params["netuid"] = netuid - - with bittensor.__console__.status( - ":satellite: Serving prometheus on: [white]{}:{}[/white] ...".format( - subtensor.network, netuid - ) - ): - success, err = subtensor._do_serve_prometheus( - wallet=wallet, - call_params=call_params, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - ) - - if wait_for_inclusion or wait_for_finalization: - if success is True: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Served prometheus[/green]\n [bold white]{}[/bold white]".format( - json.dumps(call_params, indent=4, sort_keys=True) - ) - ) - return True - else: - bittensor.__console__.print(f":cross_mark: [red]Failed[/red]: {err}") - return False - else: - return True diff --git a/bittensor/extrinsics/registration.py b/bittensor/extrinsics/registration.py deleted file mode 100644 index 40bde3fc8..000000000 --- a/bittensor/extrinsics/registration.py +++ /dev/null @@ -1,543 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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 time -from typing import List, Union, Optional, Tuple - -from rich.prompt import Confirm - -import bittensor -from bittensor.utils import format_error_message - -from bittensor.utils.registration import ( - POWSolution, - create_pow, - torch, - log_no_torch_error, -) - - -def register_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[List[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, -) -> bool: - r"""Registers the wallet to the chain. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - netuid (int): - The ``netuid`` of the subnet to register on. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - max_allowed_attempts (int): - Maximum number of attempts to register the wallet. - cuda (bool): - If ``true``, the wallet should be registered using CUDA device(s). - dev_id (Union[List[int], int]): - The CUDA device id to use, or a list of device ids. - tpb (int): - The number of threads per block (CUDA). - num_processes (int): - The number of processes to use to register. - update_interval (int): - The number of nonces to solve between updates. - log_verbose (bool): - If ``true``, the registration process will log more information. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not subtensor.subnet_exists(netuid): - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: error: [bold white]subnet:{}[/bold white] does not exist.".format( - netuid - ) - ) - return False - - with bittensor.__console__.status( - f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]..." - ): - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid - ) - if not neuron.is_null: - bittensor.logging.debug( - f"Wallet {wallet} is already registered on {neuron.netuid} with {neuron.uid}" - ) - return True - - if prompt: - if not Confirm.ask( - "Continue Registration?\n hotkey: [bold white]{}[/bold white]\n coldkey: [bold white]{}[/bold white]\n network: [bold white]{}[/bold white]".format( - wallet.hotkey.ss58_address, - wallet.coldkeypub.ss58_address, - subtensor.network, - ) - ): - return False - - if not torch: - log_no_torch_error() - return False - - # Attempt rolling registration. - attempts = 1 - while True: - bittensor.__console__.print( - ":satellite: Registering...({}/{})".format(attempts, max_allowed_attempts) - ) - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - if prompt: - bittensor.__console__.print("CUDA is not available.") - return False - pow_result: Optional[POWSolution] = create_pow( - subtensor, - wallet, - netuid, - output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - pow_result: Optional[POWSolution] = create_pow( - subtensor, - wallet, - netuid, - output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # pow failed - if not pow_result: - # might be registered already on this subnet - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - bittensor.__console__.print( - f":white_heavy_check_mark: [green]Already registered on netuid:{netuid}[/green]" - ) - return True - - # pow successful, proceed to submit pow to chain for registration - else: - with bittensor.__console__.status(":satellite: Submitting POW..."): - # check if pow result is still valid - while not pow_result.is_stale(subtensor=subtensor): - result: Tuple[bool, Optional[str]] = subtensor._do_pow_register( - netuid=netuid, - wallet=wallet, - pow_result=pow_result, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - success, err_msg = result - - if not success: - # Look error here - # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs - if "HotKeyAlreadyRegisteredInSubNet" in err_msg: - bittensor.__console__.print( - f":white_heavy_check_mark: [green]Already Registered on [bold]subnet:{netuid}[/bold][/green]" - ) - return True - - bittensor.__console__.print( - f":cross_mark: [red]Failed[/red]: {err_msg}" - ) - time.sleep(0.5) - - # Successful registration, final check for neuron and pubkey - else: - bittensor.__console__.print(":satellite: Checking Balance...") - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Registered[/green]" - ) - return True - else: - # neuron not found, try again - bittensor.__console__.print( - ":cross_mark: [red]Unknown error. Neuron not found.[/red]" - ) - continue - else: - # Exited loop because pow is no longer valid. - bittensor.__console__.print("[red]POW is stale.[/red]") - # Try again. - continue - - if attempts < max_allowed_attempts: - # Failed registration, retry pow - attempts += 1 - bittensor.__console__.print( - ":satellite: Failed registration, retrying pow ...({}/{})".format( - attempts, max_allowed_attempts - ) - ) - else: - # Failed to register after max attempts. - bittensor.__console__.print("[red]No more attempts.[/red]") - return False - - -def burned_register_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - r"""Registers the wallet to chain by recycling TAO. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - netuid (int): - The ``netuid`` of the subnet to register on. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not subtensor.subnet_exists(netuid): - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: error: [bold white]subnet:{}[/bold white] does not exist.".format( - netuid - ) - ) - return False - - try: - wallet.coldkey # unlock coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - with bittensor.__console__.status( - f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]..." - ): - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid - ) - - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - recycle_amount = subtensor.recycle(netuid=netuid) - if not neuron.is_null: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Already Registered[/green]:\n" - "uid: [bold white]{}[/bold white]\n" - "netuid: [bold white]{}[/bold white]\n" - "hotkey: [bold white]{}[/bold white]\n" - "coldkey: [bold white]{}[/bold white]".format( - neuron.uid, neuron.netuid, neuron.hotkey, neuron.coldkey - ) - ) - return True - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask(f"Recycle {recycle_amount} to register on subnet:{netuid}?"): - return False - - with bittensor.__console__.status(":satellite: Recycling TAO for Registration..."): - success, err_msg = subtensor._do_burned_register( - netuid=netuid, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not success: - bittensor.__console__.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - time.sleep(0.5) - return False - # Successful registration, final check for neuron and pubkey - else: - bittensor.__console__.print(":satellite: Checking Balance...") - block = subtensor.get_current_block() - new_balance = subtensor.get_balance( - wallet.coldkeypub.ss58_address, block=block - ) - - bittensor.__console__.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_balance, new_balance - ) - ) - is_registered = subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Registered[/green]" - ) - return True - else: - # neuron not found, try again - bittensor.__console__.print( - ":cross_mark: [red]Unknown error. Neuron not found.[/red]" - ) - return False - - -class MaxSuccessException(Exception): - pass - - -class MaxAttemptsException(Exception): - pass - - -def run_faucet_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[List[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, -) -> Tuple[bool, str]: - r"""Runs a continual POW to get a faucet of TAO on the test net. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - 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. - max_allowed_attempts (int): - Maximum number of attempts to register the wallet. - cuda (bool): - If ``true``, the wallet should be registered using CUDA device(s). - dev_id (Union[List[int], int]): - The CUDA device id to use, or a list of device ids. - tpb (int): - The number of threads per block (CUDA). - num_processes (int): - The number of processes to use to register. - update_interval (int): - The number of nonces to solve between updates. - log_verbose (bool): - If ``true``, the registration process will log more information. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if prompt: - if not Confirm.ask( - "Run Faucet ?\n coldkey: [bold white]{}[/bold white]\n network: [bold white]{}[/bold white]".format( - wallet.coldkeypub.ss58_address, - subtensor.network, - ) - ): - return False, "" - - if not torch: - log_no_torch_error() - return False, "Requires torch" - - # Unlock coldkey - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False, "" - - # Get previous balance. - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - # Attempt rolling registration. - attempts = 1 - successes = 1 - while True: - try: - pow_result = None - while pow_result is None or pow_result.is_stale(subtensor=subtensor): - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - if prompt: - bittensor.__console__.print("CUDA is not available.") - return False, "CUDA is not available." - pow_result: Optional[POWSolution] = create_pow( - subtensor, - wallet, - -1, - output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - pow_result: Optional[POWSolution] = create_pow( - subtensor, - wallet, - -1, - output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - call = subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="faucet", - call_params={ - "block_number": pow_result.block_number, - "nonce": pow_result.nonce, - "work": [int(byte_) for byte_ in pow_result.seal], - }, - ) - extrinsic = subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - bittensor.__console__.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" - ) - if attempts == max_allowed_attempts: - raise MaxAttemptsException - attempts += 1 - # Wait a bit before trying again - time.sleep(1) - - # Successful registration - else: - new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bittensor.__console__.print( - f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" - ) - old_balance = new_balance - - if successes == 3: - raise MaxSuccessException - - attempts = 1 # Reset attempts on success - successes += 1 - - except KeyboardInterrupt: - return True, "Done" - - except MaxSuccessException: - return True, f"Max successes reached: {3}" - - except MaxAttemptsException: - return False, f"Max attempts reached: {max_allowed_attempts}" - - -def swap_hotkey_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - new_wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - try: - wallet.coldkey # unlock coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - if prompt: - # Prompt user for confirmation. - if not Confirm.ask( - f"Swap {wallet.hotkey} for new hotkey: {new_wallet.hotkey}?" - ): - return False - - with bittensor.__console__.status(":satellite: Swapping hotkeys..."): - success, err_msg = subtensor._do_swap_hotkey( - wallet=wallet, - new_wallet=new_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not success: - bittensor.__console__.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - time.sleep(0.5) - return False - - else: - bittensor.__console__.print( - f"Hotkey {wallet.hotkey} swapped for new hotkey: {new_wallet.hotkey}" - ) - return True diff --git a/bittensor/extrinsics/root.py b/bittensor/extrinsics/root.py deleted file mode 100644 index c0a4fcabd..000000000 --- a/bittensor/extrinsics/root.py +++ /dev/null @@ -1,237 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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 bittensor - -import time -import logging -import numpy as np -from numpy.typing import NDArray -from rich.prompt import Confirm -from typing import Union, List -import bittensor.utils.weight_utils as weight_utils -from bittensor.btlogging.defines import BITTENSOR_LOGGER_NAME -from bittensor.utils.registration import torch, legacy_torch_api_compat - -logger = logging.getLogger(BITTENSOR_LOGGER_NAME) - - -def root_register_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - r"""Registers the wallet to root network. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - - try: - wallet.coldkey # unlock coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - is_registered = subtensor.is_hotkey_registered( - netuid=0, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Already registered on root network.[/green]" - ) - return True - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask("Register to root network?"): - return False - - with bittensor.__console__.status(":satellite: Registering to root network..."): - success, err_msg = subtensor._do_root_register( - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not success: - bittensor.__console__.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - time.sleep(0.5) - - # Successful registration, final check for neuron and pubkey - else: - is_registered = subtensor.is_hotkey_registered( - netuid=0, hotkey_ss58=wallet.hotkey.ss58_address - ) - if is_registered: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Registered[/green]" - ) - return True - else: - # neuron not found, try again - bittensor.__console__.print( - ":cross_mark: [red]Unknown error. Neuron not found.[/red]" - ) - - -@legacy_torch_api_compat -def set_root_weights_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - netuids: Union[NDArray[np.int64], "torch.LongTensor", List[int]], - weights: Union[NDArray[np.float32], "torch.FloatTensor", List[float]], - version_key: int = 0, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - r"""Sets the given weights and values on chain for wallet hotkey account. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - netuids (Union[NDArray[np.int64], torch.LongTensor, List[int]]): - The ``netuid`` of the subnet to set weights for. - weights (Union[NDArray[np.float32], torch.FloatTensor, list]): - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - - try: - wallet.coldkey # unlock coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - 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) - - # Get weight restrictions. - min_allowed_weights = subtensor.min_allowed_weights(netuid=0) - max_weight_limit = subtensor.max_weight_limit(netuid=0) - - # Get non zero values. - non_zero_weight_idx = np.argwhere(weights > 0).squeeze(axis=1) - non_zero_weight_uids = netuids[non_zero_weight_idx] - non_zero_weights = weights[non_zero_weight_idx] - if non_zero_weights.size < min_allowed_weights: - raise ValueError( - "The minimum number of weights required to set weights is {}, got {}".format( - min_allowed_weights, non_zero_weights.size - ) - ) - - # Normalize the weights to max value. - formatted_weights = bittensor.utils.weight_utils.normalize_max_weight( - x=weights, limit=max_weight_limit - ) - bittensor.__console__.print( - f"\nRaw Weights -> Normalized weights: \n\t{weights} -> \n\t{formatted_weights}\n" - ) - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to set the following root weights?:\n[bold white] weights: {}\n uids: {}[/bold white ]?".format( - formatted_weights, netuids - ) - ): - return False - - with bittensor.__console__.status( - ":satellite: Setting root weights on [white]{}[/white] ...".format( - subtensor.network - ) - ): - try: - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - netuids, weights - ) - success, error_message = subtensor._do_set_root_weights( - wallet=wallet, - netuid=0, - uids=weight_uids, - vals=weight_vals, - version_key=version_key, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - ) - - bittensor.__console__.print(success, error_message) - - if not wait_for_finalization and not wait_for_inclusion: - return True - - if success is True: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - bittensor.logging.success( - prefix="Set weights", - suffix="Finalized: " + str(success), - ) - return True - else: - bittensor.__console__.print( - f":cross_mark: [red]Failed[/red]: {error_message}" - ) - bittensor.logging.warning( - prefix="Set weights", - suffix="Failed: " + str(error_message), - ) - return False - - except Exception as e: - # TODO( devs ): lets remove all of the bittensor.__console__ calls and replace with the bittensor logger. - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - bittensor.logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) - return False diff --git a/bittensor/extrinsics/senate.py b/bittensor/extrinsics/senate.py deleted file mode 100644 index f586cec39..000000000 --- a/bittensor/extrinsics/senate.py +++ /dev/null @@ -1,275 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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 time - -from rich.prompt import Confirm - -import bittensor -from bittensor.utils import format_error_message - - -def register_senate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - r"""Registers the wallet to chain for senate voting. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - 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``. - """ - try: - wallet.coldkey # unlock coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - wallet.hotkey # unlock hotkey - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask(f"Register delegate hotkey to senate?"): - return False - - with bittensor.__console__.status(":satellite: Registering with senate..."): - with subtensor.substrate as substrate: - # create extrinsic call - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="join_senate", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if registration successful - response.process_events() - if not response.is_success: - bittensor.__console__.print( - f":cross_mark: [red]Failed[/red]:{format_error_message(response.error_message)}" - ) - time.sleep(0.5) - - # Successful registration, final check for membership - else: - is_registered = wallet.is_senate_member(subtensor) - - if is_registered: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Registered[/green]" - ) - return True - else: - # neuron not found, try again - bittensor.__console__.print( - ":cross_mark: [red]Unknown error. Senate membership not found.[/red]" - ) - - -def leave_senate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - r"""Removes the wallet from chain for senate voting. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - 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``. - """ - try: - wallet.coldkey # unlock coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - wallet.hotkey # unlock hotkey - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask(f"Remove delegate hotkey from senate?"): - return False - - with bittensor.__console__.status(":satellite: Leaving senate..."): - with subtensor.substrate as substrate: - # create extrinsic call - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="leave_senate", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if registration successful - response.process_events() - if not response.is_success: - bittensor.__console__.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" - ) - time.sleep(0.5) - - # Successful registration, final check for membership - else: - is_registered = wallet.is_senate_member(subtensor) - - if not is_registered: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Left senate[/green]" - ) - return True - else: - # neuron not found, try again - bittensor.__console__.print( - ":cross_mark: [red]Unknown error. Senate membership still found.[/red]" - ) - - -def vote_senate_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - proposal_hash: str, - proposal_idx: int, - vote: bool, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - r"""Votes ayes or nays on proposals. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - 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``. - """ - wallet.coldkey # unlock coldkey - wallet.hotkey # unlock hotkey - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask("Cast a vote of {}?".format(vote)): - return False - - with bittensor.__console__.status(":satellite: Casting vote.."): - with subtensor.substrate as substrate: - # create extrinsic call - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="vote", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "proposal": proposal_hash, - "index": proposal_idx, - "approve": vote, - }, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if vote successful - response.process_events() - if not response.is_success: - bittensor.__console__.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(response.error_message)}" - ) - time.sleep(0.5) - - # Successful vote, final check for data - else: - vote_data = subtensor.get_vote_data(proposal_hash) - has_voted = ( - vote_data["ayes"].count(wallet.hotkey.ss58_address) > 0 - or vote_data["nays"].count(wallet.hotkey.ss58_address) > 0 - ) - - if has_voted: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Vote cast.[/green]" - ) - return True - else: - # hotkey not found in ayes/nays - bittensor.__console__.print( - ":cross_mark: [red]Unknown error. Couldn't find vote.[/red]" - ) diff --git a/bittensor/extrinsics/serving.py b/bittensor/extrinsics/serving.py deleted file mode 100644 index 734561835..000000000 --- a/bittensor/extrinsics/serving.py +++ /dev/null @@ -1,286 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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 json -from typing import Optional - -from retry import retry -from rich.prompt import Confirm - -import bittensor -import bittensor.utils.networking as net -from bittensor.utils import format_error_message -from bittensor.utils.networking import ensure_connected -from ..errors import MetadataError - - -def serve_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - ip: str, - port: int, - protocol: int, - netuid: int, - placeholder1: int = 0, - placeholder2: int = 0, - wait_for_inclusion: bool = False, - wait_for_finalization=True, - prompt: bool = False, -) -> bool: - r"""Subscribes a Bittensor endpoint to the subtensor chain. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - ip (str): - Endpoint host port i.e., ``192.122.31.4``. - port (int): - Endpoint port number i.e., ``9221``. - protocol (int): - An ``int`` representation of the protocol. - netuid (int): - The network uid to serve on. - placeholder1 (int): - A placeholder for future use. - placeholder2 (int): - A placeholder for future use. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - # Decrypt hotkey - wallet.hotkey - params: "bittensor.AxonServeCallParams" = { - "version": bittensor.__version_as_int__, - "ip": net.ip_to_int(ip), - "port": port, - "ip_type": net.ip_version(ip), - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - "coldkey": wallet.coldkeypub.ss58_address, - "protocol": protocol, - "placeholder1": placeholder1, - "placeholder2": placeholder2, - } - bittensor.logging.debug("Checking axon ...") - neuron = subtensor.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid - ) - neuron_up_to_date = not neuron.is_null and params == { - "version": neuron.axon_info.version, - "ip": net.ip_to_int(neuron.axon_info.ip), - "port": neuron.axon_info.port, - "ip_type": neuron.axon_info.ip_type, - "netuid": neuron.netuid, - "hotkey": neuron.hotkey, - "coldkey": neuron.coldkey, - "protocol": neuron.axon_info.protocol, - "placeholder1": neuron.axon_info.placeholder1, - "placeholder2": neuron.axon_info.placeholder2, - } - output = params.copy() - output["coldkey"] = wallet.coldkeypub.ss58_address - output["hotkey"] = wallet.hotkey.ss58_address - if neuron_up_to_date: - bittensor.logging.debug( - f"Axon already served on: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) " - ) - return True - - if prompt: - output = params.copy() - output["coldkey"] = wallet.coldkeypub.ss58_address - output["hotkey"] = wallet.hotkey.ss58_address - if not Confirm.ask( - "Do you want to serve axon:\n [bold white]{}[/bold white]".format( - json.dumps(output, indent=4, sort_keys=True) - ) - ): - return False - - bittensor.logging.debug( - f"Serving axon with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) -> {subtensor.network}:{netuid}" - ) - success, error_message = subtensor._do_serve_axon( - wallet=wallet, - call_params=params, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - ) - - if wait_for_inclusion or wait_for_finalization: - if success is True: - bittensor.logging.debug( - f"Axon served with: AxonInfo({wallet.hotkey.ss58_address},{ip}:{port}) on {subtensor.network}:{netuid} " - ) - return True - else: - bittensor.logging.error(f"Failed: {error_message}") - return False - else: - return True - - -def serve_axon_extrinsic( - subtensor: "bittensor.subtensor", - netuid: int, - axon: "bittensor.Axon", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - r"""Serves the axon to the network. - - Args: - netuid ( int ): - The ``netuid`` being served on. - axon (bittensor.Axon): - Axon to serve. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - axon.wallet.hotkey - axon.wallet.coldkeypub - external_port = axon.external_port - - # ---- Get external ip ---- - if axon.external_ip is None: - try: - external_ip = net.get_external_ip() - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Found external ip: {}[/green]".format( - external_ip - ) - ) - bittensor.logging.success( - prefix="External IP", suffix="{}".format(external_ip) - ) - except Exception as E: - raise RuntimeError( - "Unable to attain your external ip. Check your internet connection. error: {}".format( - E - ) - ) from E - else: - external_ip = axon.external_ip - - # ---- Subscribe to chain ---- - serve_success = subtensor.serve( - wallet=axon.wallet, - ip=external_ip, - port=external_port, - netuid=netuid, - protocol=4, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - return serve_success - - -def publish_metadata( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - netuid: int, - data_type: str, - data: bytes, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, -) -> bool: - """ - Publishes metadata on the Bittensor network using the specified wallet and network identifier. - - Args: - subtensor (bittensor.subtensor): - The subtensor instance representing the Bittensor blockchain connection. - wallet (bittensor.wallet): - The wallet object used for authentication in the transaction. - netuid (int): - Network UID on which the metadata is to be published. - data_type (str): - The data type of the information being submitted. It should be one of the following: ``'Sha256'``, ``'Blake256'``, ``'Keccak256'``, or ``'Raw0-128'``. This specifies the format or hashing algorithm used for the data. - data (str): - The actual metadata content to be published. This should be formatted or hashed according to the ``type`` specified. (Note: max ``str`` length is 128 bytes) - wait_for_inclusion (bool, optional): - If ``True``, the function will wait for the extrinsic to be included in a 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``. - - 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 failure. - """ - - wallet.hotkey - - with subtensor.substrate as substrate: - call = substrate.compose_call( - call_module="Commitments", - call_function="set_commitment", - call_params={ - "netuid": netuid, - "info": {"fields": [[{f"{data_type}": data}]]}, - }, - ) - - extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.hotkey) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise MetadataError(format_error_message(response.error_message)) - - -@ensure_connected -def get_metadata(self, netuid: int, hotkey: str, block: Optional[int] = None) -> str: - @retry(delay=2, tries=3, backoff=2, max_delay=4) - def make_substrate_call_with_retry(): - with self.substrate as substrate: - return substrate.query( - module="Commitments", - storage_function="CommitmentOf", - params=[netuid, hotkey], - block_hash=None if block is None else substrate.get_block_hash(block), - ) - - commit_data = make_substrate_call_with_retry() - return commit_data.value diff --git a/bittensor/extrinsics/set_weights.py b/bittensor/extrinsics/set_weights.py deleted file mode 100644 index ea51fab23..000000000 --- a/bittensor/extrinsics/set_weights.py +++ /dev/null @@ -1,136 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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 bittensor - -import logging -import numpy as np -from numpy.typing import NDArray -from rich.prompt import Confirm -from typing import Union, Tuple -import bittensor.utils.weight_utils as weight_utils -from bittensor.btlogging.defines import BITTENSOR_LOGGER_NAME -from bittensor.utils.registration import torch, use_torch - -logger = logging.getLogger(BITTENSOR_LOGGER_NAME) - - -def set_weights_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - netuid: int, - uids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], - version_key: int = 0, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> Tuple[bool, str]: - r"""Sets the given weights and values on chain for wallet hotkey account. - - Args: - subtensor (bittensor.subtensor): - Subtensor endpoint to use. - wallet (bittensor.wallet): - Bittensor wallet object. - netuid (int): - The ``netuid`` of the subnet to set weights for. - 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. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - 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 use_torch(): - if isinstance(uids, list): - uids = torch.tensor(uids, dtype=torch.int64) - if isinstance(weights, list): - weights = torch.tensor(weights, dtype=torch.float32) - else: - 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 - ) - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to set weights:\n[bold white] weights: {}\n uids: {}[/bold white ]?".format( - [float(v / 65535) for v in weight_vals], weight_uids - ) - ): - return False, "Prompt refused." - - with bittensor.__console__.status( - ":satellite: Setting weights on [white]{}[/white] ...".format(subtensor.network) - ): - try: - success, error_message = subtensor._do_set_weights( - wallet=wallet, - netuid=netuid, - uids=weight_uids, - vals=weight_vals, - version_key=version_key, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." - - if success is True: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - bittensor.logging.success( - prefix="Set weights", - suffix="Finalized: " + str(success), - ) - return True, "Successfully set weights and Finalized." - else: - bittensor.logging.error( - msg=error_message, - prefix="Set weights", - suffix="Failed: ", - ) - return False, error_message - - except Exception as e: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: error:{}".format(e) - ) - bittensor.logging.warning( - prefix="Set weights", suffix="Failed: " + str(e) - ) - return False, str(e) diff --git a/bittensor/extrinsics/staking.py b/bittensor/extrinsics/staking.py deleted file mode 100644 index b6d5cf5d6..000000000 --- a/bittensor/extrinsics/staking.py +++ /dev/null @@ -1,760 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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. - -from rich.prompt import Confirm -from time import sleep -from typing import List, Union, Optional, Tuple - -import bittensor -from ..utils.formatting import float_to_u64, float_to_u16 - -from bittensor.utils.balance import Balance - -console = bittensor.__console__ - - -def _check_threshold_amount( - subtensor: "bittensor.subtensor", stake_balance: Balance -) -> Tuple[bool, Balance]: - """ - Checks if the new stake balance will be above the minimum required stake threshold. - - Args: - stake_balance (Balance): - the balance to check for threshold limits. - - Returns: - success, threshold (bool, Balance): - ``true`` if the staking balance is above the threshold, or ``false`` if the - staking balance is below the threshold. - The threshold balance required to stake. - """ - min_req_stake: Balance = subtensor.get_minimum_required_stake() - - if min_req_stake > stake_balance: - return False, min_req_stake - else: - return True, min_req_stake - - -def add_stake_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - hotkey_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - r"""Adds the specified amount of stake to passed hotkey ``uid``. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - hotkey_ss58 (Optional[str]): - The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey. - amount (Union[Balance, float]): - Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - - Raises: - bittensor.errors.NotRegisteredError: - If the wallet is not registered on the chain. - bittensor.errors.NotDelegateError: - If the hotkey is not a delegate on the chain. - """ - # Decrypt keys, - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - # Default to wallet's own hotkey if the value is not passed. - if hotkey_ss58 is None: - hotkey_ss58 = wallet.hotkey.ss58_address - - # Flag to indicate if we are using the wallet's own hotkey. - own_hotkey: bool - - with bittensor.__console__.status( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - # Get hotkey owner - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner - if not own_hotkey: - # This is not the wallet's own hotkey so we are delegating. - if not subtensor.is_hotkey_delegate(hotkey_ss58): - raise bittensor.errors.NotDelegateError( - "Hotkey: {} is not a delegate.".format(hotkey_ss58) - ) - - # Get hotkey take - hotkey_take = subtensor.get_delegate_take(hotkey_ss58) - - # Get current stake - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) - - # Grab the existential deposit. - existential_deposit = subtensor.get_existential_deposit() - - # Convert to bittensor.Balance - if amount is None: - # Stake it all. - staking_balance = bittensor.Balance.from_tao(old_balance.tao) - elif not isinstance(amount, bittensor.Balance): - staking_balance = bittensor.Balance.from_tao(amount) - else: - staking_balance = amount - - # Leave existential balance to keep key alive. - if staking_balance > old_balance - existential_deposit: - # If we are staking all, we need to leave at least the existential deposit. - staking_balance = old_balance - existential_deposit - else: - staking_balance = staking_balance - - # Check enough to stake. - if staking_balance > old_balance: - bittensor.__console__.print( - ":cross_mark: [red]Not enough stake[/red]:[bold white]\n balance:{}\n amount: {}\n coldkey: {}[/bold white]".format( - old_balance, staking_balance, wallet.name - ) - ) - return False - - # If nominating, we need to check if the new stake balance will be above the minimum required stake threshold. - if not own_hotkey: - new_stake_balance = old_stake + staking_balance - is_above_threshold, threshold = _check_threshold_amount( - subtensor, new_stake_balance - ) - if not is_above_threshold: - bittensor.__console__.print( - f":cross_mark: [red]New stake balance of {new_stake_balance} is below the minimum required nomination stake threshold {threshold}.[/red]" - ) - return False - - # Ask before moving on. - if prompt: - if not own_hotkey: - # We are delegating. - if not Confirm.ask( - "Do you want to delegate:[bold white]\n amount: {}\n to: {}\n take: {}\n owner: {}[/bold white]".format( - staking_balance, wallet.hotkey_str, hotkey_take, hotkey_owner - ) - ): - return False - else: - if not Confirm.ask( - "Do you want to stake:[bold white]\n amount: {}\n to: {}[/bold white]".format( - staking_balance, wallet.hotkey_str - ) - ): - return False - - try: - with bittensor.__console__.status( - ":satellite: Staking to: [bold white]{}[/bold white] ...".format( - subtensor.network - ) - ): - staking_response: bool = __do_add_stake_single( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=staking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response 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 - - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - with bittensor.__console__.status( - ":satellite: Checking Balance on: [white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance( - address=wallet.coldkeypub.ss58_address - ) - block = subtensor.get_current_block() - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block=block, - ) # Get current stake - - bittensor.__console__.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_balance, new_balance - ) - ) - bittensor.__console__.print( - "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_stake, new_stake - ) - ) - return True - else: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: Error unknown." - ) - return False - - except bittensor.errors.NotRegisteredError: - bittensor.__console__.print( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) - ) - return False - except bittensor.errors.StakeError as e: - bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - return False - - -def add_stake_multiple_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - hotkey_ss58s: List[str], - amounts: Optional[List[Union[Balance, float]]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - r"""Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object for the coldkey. - hotkey_ss58s (List[str]): - List of hotkeys to stake to. - amounts (List[Union[Balance, float]]): - List of amounts to stake. If ``None``, stake all to the first hotkey. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any wallet was staked. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not isinstance(hotkey_ss58s, list) or not all( - isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s - ): - raise TypeError("hotkey_ss58s must be a list of str") - - if len(hotkey_ss58s) == 0: - return True - - if amounts is not None and len(amounts) != len(hotkey_ss58s): - raise ValueError("amounts must be a list of the same length as hotkey_ss58s") - - if amounts is not None and not all( - isinstance(amount, (Balance, float)) for amount in amounts - ): - raise TypeError( - "amounts must be a [list of bittensor.Balance or float] or None" - ) - - if amounts is None: - amounts = [None] * len(hotkey_ss58s) - else: - # Convert to Balance - amounts = [ - bittensor.Balance.from_tao(amount) if isinstance(amount, float) else amount - for amount in amounts - ] - - if sum(amount.tao for amount in amounts) == 0: - # Staking 0 tao - return True - - # Decrypt coldkey. - wallet.coldkey - - old_stakes = [] - with bittensor.__console__.status( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - # Get the old stakes. - for hotkey_ss58 in hotkey_ss58s: - old_stakes.append( - subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) - ) - - # Remove existential balance to keep key alive. - ## Keys must maintain a balance of at least 1000 rao to stay alive. - total_staking_rao = sum( - [amount.rao if amount is not None else 0 for amount in amounts] - ) - if total_staking_rao == 0: - # Staking all to the first wallet. - if old_balance.rao > 1000: - old_balance -= bittensor.Balance.from_rao(1000) - - elif total_staking_rao < 1000: - # Staking less than 1000 rao to the wallets. - pass - else: - # Staking more than 1000 rao to the wallets. - ## Reduce the amount to stake to each wallet to keep the balance above 1000 rao. - percent_reduction = 1 - (1000 / total_staking_rao) - amounts = [ - Balance.from_tao(amount.tao * percent_reduction) for amount in amounts - ] - - successful_stakes = 0 - for idx, (hotkey_ss58, amount, old_stake) in enumerate( - zip(hotkey_ss58s, amounts, old_stakes) - ): - staking_all = False - # Convert to bittensor.Balance - if amount == None: - # Stake it all. - staking_balance = bittensor.Balance.from_tao(old_balance.tao) - staking_all = True - else: - # Amounts are cast to balance earlier in the function - assert isinstance(amount, bittensor.Balance) - staking_balance = amount - - # Check enough to stake - if staking_balance > old_balance: - bittensor.__console__.print( - ":cross_mark: [red]Not enough balance[/red]: [green]{}[/green] to stake: [blue]{}[/blue] from coldkey: [white]{}[/white]".format( - old_balance, staking_balance, wallet.name - ) - ) - continue - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to stake:\n[bold white] amount: {}\n hotkey: {}[/bold white ]?".format( - staking_balance, wallet.hotkey_str - ) - ): - continue - - try: - staking_response: bool = __do_add_stake_single( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=staking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response == True: # If we successfully staked. - # We only wait here if we expect finalization. - - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_rate_limit_blocks = subtensor.tx_rate_limit() - if tx_rate_limit_blocks > 0: - bittensor.__console__.print( - ":hourglass: [yellow]Waiting for tx rate limit: [white]{}[/white] blocks[/yellow]".format( - tx_rate_limit_blocks - ) - ) - sleep(tx_rate_limit_blocks * 12) # 12 seconds per block - - if not wait_for_finalization and not wait_for_inclusion: - old_balance -= staking_balance - successful_stakes += 1 - if staking_all: - # If staked all, no need to continue - break - - continue - - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - - block = subtensor.get_current_block() - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block=block, - ) - new_balance = subtensor.get_balance( - wallet.coldkeypub.ss58_address, block=block - ) - bittensor.__console__.print( - "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - hotkey_ss58, old_stake, new_stake - ) - ) - old_balance = new_balance - successful_stakes += 1 - if staking_all: - # If staked all, no need to continue - break - - else: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: Error unknown." - ) - continue - - except bittensor.errors.NotRegisteredError: - bittensor.__console__.print( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - hotkey_ss58 - ) - ) - continue - except bittensor.errors.StakeError as e: - bittensor.__console__.print( - ":cross_mark: [red]Stake Error: {}[/red]".format(e) - ) - continue - - if successful_stakes != 0: - with bittensor.__console__.status( - ":satellite: Checking Balance on: ([white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bittensor.__console__.print( - "Balance: [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_balance, new_balance - ) - ) - return True - - return False - - -def __do_add_stake_single( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - hotkey_ss58: str, - amount: "bittensor.Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> bool: - r""" - Executes a stake call to the chain using the wallet and the amount specified. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - hotkey_ss58 (str): - Hotkey to stake to. - amount (bittensor.Balance): - Amount to stake as Bittensor balance object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - Raises: - bittensor.errors.StakeError: - If the extrinsic fails to be finalized or included in the block. - bittensor.errors.NotDelegateError: - If the hotkey is not a delegate. - bittensor.errors.NotRegisteredError: - If the hotkey is not registered in any subnets. - - """ - # Decrypt keys, - wallet.coldkey - - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner - if not own_hotkey: - # We are delegating. - # Verify that the hotkey is a delegate. - if not subtensor.is_hotkey_delegate(hotkey_ss58=hotkey_ss58): - raise bittensor.errors.NotDelegateError( - "Hotkey: {} is not a delegate.".format(hotkey_ss58) - ) - - success = subtensor._do_stake( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - return success - - -def set_childkey_take_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - hotkey: str, - netuid: int, - take: float, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> Tuple[bool, str]: - """ - Sets childkey take. - - Args: - subtensor (bittensor.subtensor): Subtensor endpoint to use. - wallet (bittensor.wallet): Bittensor wallet object. - hotkey (str): Childkey hotkey. - take (float): Childkey take value. - netuid (int): Unique identifier of for the subnet. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - Raises: - bittensor.errors.ChildHotkeyError: If the extrinsic fails to be finalized or included in the block. - bittensor.errors.NotRegisteredError: If the hotkey is not registered in any subnets. - - """ - - # Ask before moving on. - if prompt: - if not Confirm.ask( - f"Do you want to set childkey take to: [bold white]{take*100}%[/bold white]?" - ): - return False, "Operation Cancelled" - - # Decrypt coldkey. - wallet.coldkey - - with bittensor.__console__.status( - f":satellite: Setting childkey take on [white]{subtensor.network}[/white] ..." - ): - try: - if 0 < take <= 0.18: - take_u16 = float_to_u16(take) - else: - return False, "Invalid take value" - - success, error_message = subtensor._do_set_childkey_take( - wallet=wallet, - hotkey=hotkey, - netuid=netuid, - take=take_u16, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return ( - True, - "Not waiting for finalization or inclusion. Set childkey take initiated.", - ) - - if success: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - bittensor.logging.success( - prefix="Setting childkey take", - suffix="Finalized: " + str(success), - ) - return True, "Successfully set childkey take and Finalized." - else: - bittensor.__console__.print( - f":cross_mark: [red]Failed[/red]: {error_message}" - ) - bittensor.logging.warning( - prefix="Setting childkey take", - suffix="Failed: " + str(error_message), - ) - return False, error_message - - except Exception as e: - return False, f"Exception occurred while setting childkey take: {str(e)}" - - -def set_children_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - hotkey: str, - netuid: int, - children_with_proportions: List[Tuple[float, str]], - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> Tuple[bool, str]: - """ - Sets children hotkeys with proportions assigned from the parent. - - Args: - subtensor (bittensor.subtensor): Subtensor endpoint to use. - wallet (bittensor.wallet): Bittensor wallet object. - hotkey (str): Parent hotkey. - children_with_proportions (List[str]): Children hotkeys. - netuid (int): Unique identifier of for the subnet. - 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. - prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - Raises: - bittensor.errors.ChildHotkeyError: If the extrinsic fails to be finalized or included in the block. - bittensor.errors.NotRegisteredError: If the hotkey is not registered in any subnets. - - """ - # Check if all children are being revoked - all_revoked = len(children_with_proportions) == 0 - - operation = "Revoking all child hotkeys" if all_revoked else "Setting child hotkeys" - - # Ask before moving on. - if prompt: - if all_revoked: - if not Confirm.ask( - f"Do you want to revoke all children hotkeys for hotkey {hotkey}?" - ): - return False, "Operation Cancelled" - else: - if not Confirm.ask( - "Do you want to set children hotkeys with proportions:\n[bold white]{}[/bold white]?".format( - "\n".join( - f" {child[1]}: {child[0]}" - for child in children_with_proportions - ) - ) - ): - return False, "Operation Cancelled" - - # Decrypt coldkey. - wallet.coldkey - - user_hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. - if hotkey != user_hotkey_ss58: - raise ValueError("Cannot set/revoke child hotkeys for others.") - - with bittensor.__console__.status( - f":satellite: {operation} on [white]{subtensor.network}[/white] ..." - ): - try: - if not all_revoked: - normalized_children = prepare_child_proportions( - children_with_proportions - ) - else: - normalized_children = [] - - success, error_message = subtensor._do_set_children( - wallet=wallet, - hotkey=hotkey, - netuid=netuid, - children=normalized_children, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return ( - True, - f"Not waiting for finalization or inclusion. {operation} initiated.", - ) - - if success: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - bittensor.logging.success( - prefix=operation, - suffix="Finalized: " + str(success), - ) - return True, f"Successfully {operation.lower()} and Finalized." - else: - bittensor.__console__.print( - f":cross_mark: [red]Failed[/red]: {error_message}" - ) - bittensor.logging.warning( - prefix=operation, - suffix="Failed: " + str(error_message), - ) - return False, error_message - - except Exception as e: - return False, f"Exception occurred while {operation.lower()}: {str(e)}" - - -def prepare_child_proportions(children_with_proportions): - """ - Convert proportions to u64 and normalize, ensuring total does not exceed u64 max. - """ - children_u64 = [ - (float_to_u64(proportion), child) - for proportion, child in children_with_proportions - ] - total = sum(proportion for proportion, _ in children_u64) - - if total > (2**64 - 1): - excess = total - (2**64 - 1) - if excess > (2**64 * 0.01): # Example threshold of 1% of u64 max - raise ValueError("Excess is too great to normalize proportions") - largest_child_index = max( - range(len(children_u64)), key=lambda i: children_u64[i][0] - ) - children_u64[largest_child_index] = ( - children_u64[largest_child_index][0] - excess, - children_u64[largest_child_index][1], - ) - - return children_u64 diff --git a/bittensor/extrinsics/transfer.py b/bittensor/extrinsics/transfer.py deleted file mode 100644 index aa340ab40..000000000 --- a/bittensor/extrinsics/transfer.py +++ /dev/null @@ -1,164 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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 bittensor - -from rich.prompt import Confirm -from typing import Union -from ..utils.balance import Balance -from ..utils import is_valid_bittensor_address_or_public_key - - -def transfer_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - dest: str, - amount: Union[Balance, float], - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - keep_alive: bool = True, - prompt: bool = False, -) -> bool: - r"""Transfers funds from this wallet to the destination public key address. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object to make transfer from. - dest (str, ss58_address or ed25519): - Destination public key address of reciever. - amount (Union[Balance, int]): - Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - 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. - keep_alive (bool): - If set, keeps the account alive by keeping the balance above the existential deposit. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - # Validate destination address. - if not is_valid_bittensor_address_or_public_key(dest): - bittensor.__console__.print( - ":cross_mark: [red]Invalid destination address[/red]:[bold white]\n {}[/bold white]".format( - dest - ) - ) - return False - - if isinstance(dest, bytes): - # Convert bytes to hex string. - dest = "0x" + dest.hex() - - try: - # Unlock wallet coldkey. - wallet.coldkey - - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - # Convert to bittensor.Balance - if not isinstance(amount, bittensor.Balance): - transfer_balance = bittensor.Balance.from_tao(amount) - else: - transfer_balance = amount - - # Check balance. - with bittensor.__console__.status(":satellite: Checking Balance..."): - account_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - # check existential deposit. - existential_deposit = subtensor.get_existential_deposit() - - with bittensor.__console__.status(":satellite: Transferring..."): - fee = subtensor.get_transfer_fee( - wallet=wallet, dest=dest, value=transfer_balance.rao - ) - - if not keep_alive: - # Check if the transfer should keep_alive the account - existential_deposit = bittensor.Balance(0) - - # Check if we have enough balance. - if account_balance < (transfer_balance + fee + existential_deposit): - bittensor.__console__.print( - ":cross_mark: [red]Not enough balance[/red]:[bold white]\n balance: {}\n amount: {}\n for fee: {}[/bold white]".format( - account_balance, transfer_balance, fee - ) - ) - return False - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to transfer:[bold white]\n amount: {}\n from: {}:{}\n to: {}\n for fee: {}[/bold white]".format( - transfer_balance, wallet.name, wallet.coldkey.ss58_address, dest, fee - ) - ): - return False - - with bittensor.__console__.status(":satellite: Transferring..."): - success, block_hash, err_msg = subtensor._do_transfer( - wallet, - dest, - transfer_balance, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - ) - - if success: - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - bittensor.__console__.print( - "[green]Block Hash: {}[/green]".format(block_hash) - ) - - explorer_urls = bittensor.utils.get_explorer_url_for_network( - subtensor.network, block_hash, bittensor.__network_explorer_map__ - ) - if explorer_urls != {} and explorer_urls: - bittensor.__console__.print( - "[green]Opentensor Explorer Link: {}[/green]".format( - explorer_urls.get("opentensor") - ) - ) - bittensor.__console__.print( - "[green]Taostats Explorer Link: {}[/green]".format( - explorer_urls.get("taostats") - ) - ) - else: - bittensor.__console__.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - - if success: - with bittensor.__console__.status(":satellite: Checking Balance..."): - new_balance = subtensor.get_balance(wallet.coldkey.ss58_address) - bittensor.__console__.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - account_balance, new_balance - ) - ) - return True - - return False diff --git a/bittensor/extrinsics/unstaking.py b/bittensor/extrinsics/unstaking.py deleted file mode 100644 index a5de71b7d..000000000 --- a/bittensor/extrinsics/unstaking.py +++ /dev/null @@ -1,470 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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 bittensor -from rich.prompt import Confirm -from time import sleep -from typing import List, Union, Optional -from bittensor.utils.balance import Balance - - -def __do_remove_stake_single( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - hotkey_ss58: str, - amount: "bittensor.Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> bool: - r""" - Executes an unstake call to the chain using the wallet and the amount specified. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - hotkey_ss58 (str): - Hotkey address to unstake from. - amount (bittensor.Balance): - Amount to unstake as Bittensor balance object. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - Raises: - bittensor.errors.StakeError: - If the extrinsic fails to be finalized or included in the block. - bittensor.errors.NotRegisteredError: - If the hotkey is not registered in any subnets. - - """ - # Decrypt keys, - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - success = subtensor._do_unstake( - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - return success - - -def check_threshold_amount( - subtensor: "bittensor.subtensor", stake_balance: Balance -) -> bool: - """ - Checks if the remaining stake balance is above the minimum required stake threshold. - - Args: - stake_balance (Balance): - the balance to check for threshold limits. - - Returns: - success (bool): - ``true`` if the unstaking is above the threshold or 0, or ``false`` if the - unstaking is below the threshold, but not 0. - """ - min_req_stake: Balance = subtensor.get_minimum_required_stake() - - if min_req_stake > stake_balance > 0: - bittensor.__console__.print( - f":cross_mark: [yellow]Remaining stake balance of {stake_balance} less than minimum of {min_req_stake} TAO[/yellow]" - ) - return False - else: - return True - - -def unstake_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - hotkey_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - r"""Removes stake into the wallet coldkey from the specified hotkey ``uid``. - - Args: - wallet (bittensor.wallet): - Bittensor wallet object. - hotkey_ss58 (Optional[str]): - The ``ss58`` address of the hotkey to unstake from. By default, the wallet hotkey is used. - amount (Union[Balance, float]): - Amount to stake as Bittensor balance, or ``float`` interpreted as Tao. - 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. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or uncluded in the block. If we did not wait for finalization / inclusion, the response is ``true``. - """ - # Decrypt keys, - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - if hotkey_ss58 is None: - hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. - - with bittensor.__console__.status( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) - - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkey: bool = wallet.coldkeypub.ss58_address == hotkey_owner - - # Convert to bittensor.Balance - if amount is None: - # Unstake it all. - unstaking_balance = old_stake - elif not isinstance(amount, bittensor.Balance): - unstaking_balance = bittensor.Balance.from_tao(amount) - else: - unstaking_balance = amount - - # Check enough to unstake. - stake_on_uid = old_stake - if unstaking_balance > stake_on_uid: - bittensor.__console__.print( - ":cross_mark: [red]Not enough stake[/red]: [green]{}[/green] to unstake: [blue]{}[/blue] from hotkey: [white]{}[/white]".format( - stake_on_uid, unstaking_balance, wallet.hotkey_str - ) - ) - return False - - # If nomination stake, check threshold. - if not own_hotkey and not check_threshold_amount( - subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) - ): - bittensor.__console__.print( - ":warning: [yellow]This action will unstake the entire staked balance![/yellow]" - ) - unstaking_balance = stake_on_uid - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to unstake:\n[bold white] amount: {}\n hotkey: {}[/bold white ]?".format( - unstaking_balance, wallet.hotkey_str - ) - ): - return False - - try: - with bittensor.__console__.status( - ":satellite: Unstaking from chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - staking_response: bool = __do_remove_stake_single( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=unstaking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response 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 - - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - with bittensor.__console__.status( - ":satellite: Checking Balance on: [white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance( - address=wallet.coldkeypub.ss58_address - ) - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) # Get stake on hotkey. - bittensor.__console__.print( - "Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_balance, new_balance - ) - ) - bittensor.__console__.print( - "Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_stake, new_stake - ) - ) - return True - else: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: Unknown Error." - ) - return False - - except bittensor.errors.NotRegisteredError: - bittensor.__console__.print( - ":cross_mark: [red]Hotkey: {} is not registered.[/red]".format( - wallet.hotkey_str - ) - ) - return False - except bittensor.errors.StakeError as e: - bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) - return False - - -def unstake_multiple_extrinsic( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - hotkey_ss58s: List[str], - amounts: Optional[List[Union[Balance, float]]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, -) -> bool: - r"""Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. - - Args: - wallet (bittensor.wallet): - The wallet with the coldkey to unstake to. - hotkey_ss58s (List[str]): - List of hotkeys to unstake from. - amounts (List[Union[Balance, float]]): - List of amounts to unstake. If ``None``, unstake all. - wait_for_inclusion (bool): - If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. - wait_for_finalization (bool): - If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. - prompt (bool): - If ``true``, the call waits for confirmation from the user before proceeding. - Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or included in the block. Flag is ``true`` if any wallet was unstaked. If we did not wait for finalization / inclusion, the response is ``true``. - """ - if not isinstance(hotkey_ss58s, list) or not all( - isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s - ): - raise TypeError("hotkey_ss58s must be a list of str") - - if len(hotkey_ss58s) == 0: - return True - - if amounts is not None and len(amounts) != len(hotkey_ss58s): - raise ValueError("amounts must be a list of the same length as hotkey_ss58s") - - if amounts is not None and not all( - isinstance(amount, (Balance, float)) for amount in amounts - ): - raise TypeError( - "amounts must be a [list of bittensor.Balance or float] or None" - ) - - if amounts is None: - amounts = [None] * len(hotkey_ss58s) - else: - # Convert to Balance - amounts = [ - bittensor.Balance.from_tao(amount) if isinstance(amount, float) else amount - for amount in amounts - ] - - if sum(amount.tao for amount in amounts) == 0: - # Staking 0 tao - return True - - # Unlock coldkey. - try: - wallet.coldkey - except bittensor.KeyFileError: - bittensor.__console__.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is invalid[/red]:[bold white]\n [/bold white]" - ) - return False - - old_stakes = [] - own_hotkeys = [] - with bittensor.__console__.status( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - old_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - for hotkey_ss58 in hotkey_ss58s: - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=hotkey_ss58 - ) # Get stake on hotkey. - old_stakes.append(old_stake) # None if not registered. - - hotkey_owner = subtensor.get_hotkey_owner(hotkey_ss58) - own_hotkeys.append(wallet.coldkeypub.ss58_address == hotkey_owner) - - successful_unstakes = 0 - for idx, (hotkey_ss58, amount, old_stake, own_hotkey) in enumerate( - zip(hotkey_ss58s, amounts, old_stakes, own_hotkeys) - ): - # Covert to bittensor.Balance - if amount is None: - # Unstake it all. - unstaking_balance = old_stake - elif not isinstance(amount, bittensor.Balance): - unstaking_balance = bittensor.Balance.from_tao(amount) - else: - unstaking_balance = amount - - # Check enough to unstake. - stake_on_uid = old_stake - if unstaking_balance > stake_on_uid: - bittensor.__console__.print( - ":cross_mark: [red]Not enough stake[/red]: [green]{}[/green] to unstake: [blue]{}[/blue] from hotkey: [white]{}[/white]".format( - stake_on_uid, unstaking_balance, wallet.hotkey_str - ) - ) - continue - - # If nomination stake, check threshold. - if not own_hotkey and not check_threshold_amount( - subtensor=subtensor, stake_balance=(stake_on_uid - unstaking_balance) - ): - bittensor.__console__.print( - ":warning: [yellow]This action will unstake the entire staked balance![/yellow]" - ) - unstaking_balance = stake_on_uid - - # Ask before moving on. - if prompt: - if not Confirm.ask( - "Do you want to unstake:\n[bold white] amount: {}\n hotkey: {}[/bold white ]?".format( - unstaking_balance, wallet.hotkey_str - ) - ): - continue - - try: - with bittensor.__console__.status( - ":satellite: Unstaking from chain: [white]{}[/white] ...".format( - subtensor.network - ) - ): - staking_response: bool = __do_remove_stake_single( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=unstaking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if staking_response is True: # If we successfully unstaked. - # We only wait here if we expect finalization. - - if idx < len(hotkey_ss58s) - 1: - # Wait for tx rate limit. - tx_rate_limit_blocks = subtensor.tx_rate_limit() - if tx_rate_limit_blocks > 0: - bittensor.__console__.print( - ":hourglass: [yellow]Waiting for tx rate limit: [white]{}[/white] blocks[/yellow]".format( - tx_rate_limit_blocks - ) - ) - sleep(tx_rate_limit_blocks * 12) # 12 seconds per block - - if not wait_for_finalization and not wait_for_inclusion: - successful_unstakes += 1 - continue - - bittensor.__console__.print( - ":white_heavy_check_mark: [green]Finalized[/green]" - ) - with bittensor.__console__.status( - ":satellite: Checking Balance on: [white]{}[/white] ...".format( - subtensor.network - ) - ): - block = subtensor.get_current_block() - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - block=block, - ) - bittensor.__console__.print( - "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - hotkey_ss58, stake_on_uid, new_stake - ) - ) - successful_unstakes += 1 - else: - bittensor.__console__.print( - ":cross_mark: [red]Failed[/red]: Unknown Error." - ) - continue - - except bittensor.errors.NotRegisteredError: - bittensor.__console__.print( - ":cross_mark: [red]{} is not registered.[/red]".format(hotkey_ss58) - ) - continue - except bittensor.errors.StakeError as e: - bittensor.__console__.print( - ":cross_mark: [red]Stake Error: {}[/red]".format(e) - ) - continue - - if successful_unstakes != 0: - with bittensor.__console__.status( - ":satellite: Checking Balance on: ([white]{}[/white] ...".format( - subtensor.network - ) - ): - new_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - bittensor.__console__.print( - "Balance: [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( - old_balance, new_balance - ) - ) - return True - - return False diff --git a/bittensor/keyfile.py b/bittensor/keyfile.py deleted file mode 100644 index d2c75c104..000000000 --- a/bittensor/keyfile.py +++ /dev/null @@ -1,866 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 os -import base64 -import json -import stat -import getpass -import bittensor -from bittensor.errors import KeyFileError -from typing import Optional -from pathlib import Path - -from ansible_vault import Vault -from ansible.parsing.vault import AnsibleVaultError -from cryptography.exceptions import InvalidSignature, InvalidKey -from cryptography.fernet import Fernet, InvalidToken -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from nacl import pwhash, secret -from nacl.exceptions import CryptoError -from password_strength import PasswordPolicy -from substrateinterface.utils.ss58 import ss58_encode -from termcolor import colored -from rich.prompt import Confirm - - -NACL_SALT = b"\x13q\x83\xdf\xf1Z\t\xbc\x9c\x90\xb5Q\x879\xe9\xb1" - - -def serialized_keypair_to_keyfile_data(keypair: "bittensor.Keypair") -> bytes: - """Serializes keypair object into keyfile data. - - Args: - keypair (bittensor.Keypair): The keypair object to be serialized. - Returns: - data (bytes): Serialized keypair data. - """ - json_data = { - "accountId": "0x" + keypair.public_key.hex() if keypair.public_key else None, - "publicKey": "0x" + keypair.public_key.hex() if keypair.public_key else None, - "privateKey": "0x" + keypair.private_key.hex() if keypair.private_key else None, - "secretPhrase": keypair.mnemonic if keypair.mnemonic else None, - "secretSeed": ( - "0x" - + ( - keypair.seed_hex - if isinstance(keypair.seed_hex, str) - else keypair.seed_hex.hex() - ) - if keypair.seed_hex - else None - ), - "ss58Address": keypair.ss58_address if keypair.ss58_address else None, - } - data = json.dumps(json_data).encode() - return data - - -def deserialize_keypair_from_keyfile_data(keyfile_data: bytes) -> "bittensor.Keypair": - """Deserializes Keypair object from passed keyfile data. - - Args: - keyfile_data (bytes): The keyfile data as bytes to be loaded. - Returns: - keypair (bittensor.Keypair): The Keypair loaded from bytes. - Raises: - KeyFileError: Raised if the passed bytes cannot construct a keypair object. - """ - keyfile_data = keyfile_data.decode() - try: - keyfile_dict = dict(json.loads(keyfile_data)) - except: - string_value = str(keyfile_data) - if string_value[:2] == "0x": - string_value = ss58_encode(string_value) - keyfile_dict = { - "accountId": None, - "publicKey": None, - "privateKey": None, - "secretPhrase": None, - "secretSeed": None, - "ss58Address": string_value, - } - else: - raise bittensor.KeyFileError( - "Keypair could not be created from keyfile data: {}".format( - string_value - ) - ) - - if "secretSeed" in keyfile_dict and keyfile_dict["secretSeed"] is not None: - return bittensor.Keypair.create_from_seed(keyfile_dict["secretSeed"]) - - elif "secretPhrase" in keyfile_dict and keyfile_dict["secretPhrase"] is not None: - return bittensor.Keypair.create_from_mnemonic( - mnemonic=keyfile_dict["secretPhrase"] - ) - - elif keyfile_dict.get("privateKey", None) is not None: - # May have the above dict keys also, but we want to preserve the first two - return bittensor.Keypair.create_from_private_key( - keyfile_dict["privateKey"], ss58_format=bittensor.__ss58_format__ - ) - - if "ss58Address" in keyfile_dict and keyfile_dict["ss58Address"] is not None: - return bittensor.Keypair(ss58_address=keyfile_dict["ss58Address"]) - - else: - raise bittensor.KeyFileError( - "Keypair could not be created from keyfile data: {}".format(keyfile_dict) - ) - - -def validate_password(password: str) -> bool: - """Validates the password against a password policy. - - Args: - password (str): The password to verify. - Returns: - valid (bool): ``True`` if the password meets validity requirements. - """ - policy = PasswordPolicy.from_names(strength=0.20, entropybits=10, length=6) - if not password: - return False - tested_pass = policy.password(password) - result = tested_pass.test() - if len(result) > 0: - print( - colored( - "Password not strong enough. Try increasing the length of the password or the password complexity" - ) - ) - return False - password_verification = getpass.getpass("Retype your password: ") - if password != password_verification: - print("Passwords do not match") - return False - return True - - -def ask_password_to_encrypt() -> str: - """Prompts the user to enter a password for key encryption. - - Returns: - password (str): The valid password entered by the user. - """ - valid = False - while not valid: - password = getpass.getpass("Specify password for key encryption: ") - valid = validate_password(password) - return password - - -def keyfile_data_is_encrypted_nacl(keyfile_data: bytes) -> bool: - """Returns true if the keyfile data is NaCl encrypted. - - Args: - keyfile_data ( bytes, required ): - Bytes to validate. - Returns: - is_nacl (bool): - ``True`` if data is ansible encrypted. - """ - return keyfile_data[: len("$NACL")] == b"$NACL" - - -def keyfile_data_is_encrypted_ansible(keyfile_data: bytes) -> bool: - """Returns true if the keyfile data is ansible encrypted. - - Args: - keyfile_data (bytes): The bytes to validate. - Returns: - is_ansible (bool): True if the data is ansible encrypted. - """ - return keyfile_data[:14] == b"$ANSIBLE_VAULT" - - -def keyfile_data_is_encrypted_legacy(keyfile_data: bytes) -> bool: - """Returns true if the keyfile data is legacy encrypted. - Args: - keyfile_data (bytes): The bytes to validate. - Returns: - is_legacy (bool): ``True`` if the data is legacy encrypted. - """ - return keyfile_data[:6] == b"gAAAAA" - - -def keyfile_data_is_encrypted(keyfile_data: bytes) -> bool: - """Returns ``true`` if the keyfile data is encrypted. - - Args: - keyfile_data (bytes): The bytes to validate. - Returns: - is_encrypted (bool): ``True`` if the data is encrypted. - """ - return ( - keyfile_data_is_encrypted_nacl(keyfile_data) - or keyfile_data_is_encrypted_ansible(keyfile_data) - or keyfile_data_is_encrypted_legacy(keyfile_data) - ) - - -def keyfile_data_encryption_method(keyfile_data: bytes) -> bool: - """Returns ``true`` if the keyfile data is encrypted. - - Args: - keyfile_data ( bytes, required ): - Bytes to validate - Returns: - encryption_method (bool): - ``True`` if data is encrypted. - """ - - if keyfile_data_is_encrypted_nacl(keyfile_data): - return "NaCl" - elif keyfile_data_is_encrypted_ansible(keyfile_data): - return "Ansible Vault" - elif keyfile_data_is_encrypted_legacy(keyfile_data): - return "legacy" - - -def legacy_encrypt_keyfile_data(keyfile_data: bytes, password: str = None) -> bytes: - password = ask_password_to_encrypt() if password is None else password - console = bittensor.__console__ - with console.status( - ":exclamation_mark: Encrypting key with legacy encrpytion method..." - ): - vault = Vault(password) - return vault.vault.encrypt(keyfile_data) - - -def encrypt_keyfile_data(keyfile_data: bytes, password: str = None) -> bytes: - """Encrypts the passed keyfile data using ansible vault. - - Args: - keyfile_data (bytes): The bytes to encrypt. - password (str, optional): The password used to encrypt the data. If ``None``, asks for user input. - Returns: - encrypted_data (bytes): The encrypted data. - """ - password = bittensor.ask_password_to_encrypt() if password is None else password - password = bytes(password, "utf-8") - kdf = pwhash.argon2i.kdf - key = kdf( - secret.SecretBox.KEY_SIZE, - password, - NACL_SALT, - opslimit=pwhash.argon2i.OPSLIMIT_SENSITIVE, - memlimit=pwhash.argon2i.MEMLIMIT_SENSITIVE, - ) - box = secret.SecretBox(key) - encrypted = box.encrypt(keyfile_data) - return b"$NACL" + encrypted - - -def get_coldkey_password_from_environment(coldkey_name: str) -> Optional[str]: - """Retrieves the cold key password from the environment variables. - - Args: - coldkey_name (str): The name of the cold key. - Returns: - password (str): The password retrieved from the environment variables, or ``None`` if not found. - """ - envs = { - normalized_env_name: env_value - for env_name, env_value in os.environ.items() - if (normalized_env_name := env_name.upper()).startswith("BT_COLD_PW_") - } - return envs.get(f"BT_COLD_PW_{coldkey_name.replace('-', '_').upper()}") - - -def decrypt_keyfile_data( - keyfile_data: bytes, password: str = None, coldkey_name: Optional[str] = None -) -> bytes: - """Decrypts the passed keyfile data using ansible vault. - - Args: - keyfile_data (bytes): The bytes to decrypt. - password (str, optional): The password used to decrypt the data. If ``None``, asks for user input. - coldkey_name (str, optional): The name of the cold key. If provided, retrieves the password from environment variables. - Returns: - decrypted_data (bytes): The decrypted data. - Raises: - KeyFileError: Raised if the file is corrupted or if the password is incorrect. - """ - if coldkey_name is not None and password is None: - password = get_coldkey_password_from_environment(coldkey_name) - - try: - password = ( - getpass.getpass("Enter password to unlock key: ") - if password is None - else password - ) - console = bittensor.__console__ - with console.status(":key: Decrypting key..."): - # NaCl SecretBox decrypt. - if keyfile_data_is_encrypted_nacl(keyfile_data): - password = bytes(password, "utf-8") - kdf = pwhash.argon2i.kdf - key = kdf( - secret.SecretBox.KEY_SIZE, - password, - NACL_SALT, - opslimit=pwhash.argon2i.OPSLIMIT_SENSITIVE, - memlimit=pwhash.argon2i.MEMLIMIT_SENSITIVE, - ) - box = secret.SecretBox(key) - try: - decrypted_keyfile_data = box.decrypt(keyfile_data[len("$NACL") :]) - except CryptoError: - raise bittensor.KeyFileError("Invalid password") - # Ansible decrypt. - elif keyfile_data_is_encrypted_ansible(keyfile_data): - vault = Vault(password) - try: - decrypted_keyfile_data = vault.load(keyfile_data) - except AnsibleVaultError: - raise bittensor.KeyFileError("Invalid password") - # Legacy decrypt. - elif keyfile_data_is_encrypted_legacy(keyfile_data): - __SALT = ( - b"Iguesscyborgslikemyselfhaveatendencytobeparanoidaboutourorigins" - ) - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - salt=__SALT, - length=32, - iterations=10000000, - backend=default_backend(), - ) - key = base64.urlsafe_b64encode(kdf.derive(password.encode())) - cipher_suite = Fernet(key) - decrypted_keyfile_data = cipher_suite.decrypt(keyfile_data) - # Unknown. - else: - raise bittensor.KeyFileError( - "keyfile data: {} is corrupt".format(keyfile_data) - ) - - except (InvalidSignature, InvalidKey, InvalidToken): - raise bittensor.KeyFileError("Invalid password") - - if not isinstance(decrypted_keyfile_data, bytes): - decrypted_keyfile_data = json.dumps(decrypted_keyfile_data).encode() - return decrypted_keyfile_data - - -class keyfile: - """Defines an interface for a substrate interface keypair stored on device.""" - - def __init__(self, path: str): - self.path = os.path.expanduser(path) - self.name = Path(self.path).parent.stem - - def __str__(self): - if not self.exists_on_device(): - return "keyfile (empty, {})>".format(self.path) - if self.is_encrypted(): - return "Keyfile ({} encrypted, {})>".format( - keyfile_data_encryption_method(self._read_keyfile_data_from_file()), - self.path, - ) - else: - return "keyfile (decrypted, {})>".format(self.path) - - def __repr__(self): - return self.__str__() - - @property - def keypair(self) -> "bittensor.Keypair": - """Returns the keypair from path, decrypts data if the file is encrypted. - - Returns: - keypair (bittensor.Keypair): The keypair stored under the path. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, writable, corrupted, or if the password is incorrect. - """ - return self.get_keypair() - - @property - def data(self) -> bytes: - """Returns the keyfile data under path. - - Returns: - keyfile_data (bytes): The keyfile data stored under the path. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, or writable. - """ - return self._read_keyfile_data_from_file() - - @property - def keyfile_data(self) -> bytes: - """Returns the keyfile data under path. - - Returns: - keyfile_data (bytes): The keyfile data stored under the path. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, or writable. - """ - return self._read_keyfile_data_from_file() - - def set_keypair( - self, - keypair: "bittensor.Keypair", - encrypt: bool = True, - overwrite: bool = False, - password: str = None, - ): - """Writes the keypair to the file and optionally encrypts data. - - Args: - keypair (bittensor.Keypair): The keypair to store under the path. - encrypt (bool, optional): If ``True``, encrypts the file under the path. Default is ``True``. - overwrite (bool, optional): If ``True``, forces overwrite of the current file. Default is ``False``. - password (str, optional): The password used to encrypt the file. If ``None``, asks for user input. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, writable, or if the password is incorrect. - """ - self.make_dirs() - keyfile_data = serialized_keypair_to_keyfile_data(keypair) - if encrypt: - keyfile_data = bittensor.encrypt_keyfile_data(keyfile_data, password) - self._write_keyfile_data_to_file(keyfile_data, overwrite=overwrite) - - def get_keypair(self, password: str = None) -> "bittensor.Keypair": - """Returns the keypair from the path, decrypts data if the file is encrypted. - - Args: - password (str, optional): The password used to decrypt the file. If ``None``, asks for user input. - Returns: - keypair (bittensor.Keypair): The keypair stored under the path. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, writable, corrupted, or if the password is incorrect. - """ - keyfile_data = self._read_keyfile_data_from_file() - if keyfile_data_is_encrypted(keyfile_data): - decrypted_keyfile_data = decrypt_keyfile_data( - keyfile_data, password, coldkey_name=self.name - ) - else: - decrypted_keyfile_data = keyfile_data - return deserialize_keypair_from_keyfile_data(decrypted_keyfile_data) - - def make_dirs(self): - """Creates directories for the path if they do not exist.""" - directory = os.path.dirname(self.path) - if not os.path.exists(directory): - os.makedirs(directory) - - def exists_on_device(self) -> bool: - """Returns ``True`` if the file exists on the device. - - Returns: - on_device (bool): ``True`` if the file is on the device. - """ - if not os.path.isfile(self.path): - return False - return True - - def is_readable(self) -> bool: - """Returns ``True`` if the file under path is readable. - - Returns: - readable (bool): ``True`` if the file is readable. - """ - if not self.exists_on_device(): - return False - if not os.access(self.path, os.R_OK): - return False - return True - - def is_writable(self) -> bool: - """Returns ``True`` if the file under path is writable. - - Returns: - writable (bool): ``True`` if the file is writable. - """ - if os.access(self.path, os.W_OK): - return True - return False - - def is_encrypted(self) -> bool: - """Returns ``True`` if the file under path is encrypted. - - Returns: - encrypted (bool): ``True`` if the file is encrypted. - """ - if not self.exists_on_device(): - return False - if not self.is_readable(): - return False - return keyfile_data_is_encrypted(self._read_keyfile_data_from_file()) - - def _may_overwrite(self) -> bool: - """Asks the user if it is okay to overwrite the file. - - Returns: - may_overwrite (bool): ``True`` if the user allows overwriting the file. - """ - choice = input("File {} already exists. Overwrite? (y/N) ".format(self.path)) - return choice == "y" - - def check_and_update_encryption( - self, print_result: bool = True, no_prompt: bool = False - ): - """Check the version of keyfile and update if needed. - - Args: - print_result (bool): - Print the checking result or not. - no_prompt (bool): - Skip if no prompt. - Raises: - KeyFileError: - Raised if the file does not exists, is not readable, writable. - Returns: - result (bool): - Return ``True`` if the keyfile is the most updated with nacl, else ``False``. - """ - if not self.exists_on_device(): - if print_result: - bittensor.__console__.print(f"Keyfile does not exist. {self.path}") - return False - if not self.is_readable(): - if print_result: - bittensor.__console__.print(f"Keyfile is not redable. {self.path}") - return False - if not self.is_writable(): - if print_result: - bittensor.__console__.print(f"Keyfile is not writable. {self.path}") - return False - - update_keyfile = False - if not no_prompt: - keyfile_data = self._read_keyfile_data_from_file() - - # If the key is not nacl encrypted. - if keyfile_data_is_encrypted( - keyfile_data - ) and not keyfile_data_is_encrypted_nacl(keyfile_data): - terminate = False - bittensor.__console__.print( - f"You may update the keyfile to improve the security for storing your keys.\nWhile the key and the password stays the same, it would require providing your password once.\n:key:{self}\n" - ) - update_keyfile = Confirm.ask("Update keyfile?") - if update_keyfile: - stored_mnemonic = False - while not stored_mnemonic: - bittensor.__console__.print( - f"\nPlease make sure you have the mnemonic stored in case an error occurs during the transfer.", - style="white on red", - ) - stored_mnemonic = Confirm.ask("Have you stored the mnemonic?") - if not stored_mnemonic and not Confirm.ask( - "You must proceed with a stored mnemonic, retry and continue this keyfile update?" - ): - terminate = True - break - - decrypted_keyfile_data = None - while decrypted_keyfile_data == None and not terminate: - try: - password = getpass.getpass( - "\nEnter password to update keyfile: " - ) - decrypted_keyfile_data = decrypt_keyfile_data( - keyfile_data, coldkey_name=self.name, password=password - ) - except KeyFileError: - if not Confirm.ask( - "Invalid password, retry and continue this keyfile update?" - ): - terminate = True - break - - if not terminate: - encrypted_keyfile_data = encrypt_keyfile_data( - decrypted_keyfile_data, password=password - ) - self._write_keyfile_data_to_file( - encrypted_keyfile_data, overwrite=True - ) - - if print_result or update_keyfile: - keyfile_data = self._read_keyfile_data_from_file() - if not keyfile_data_is_encrypted(keyfile_data): - if print_result: - bittensor.__console__.print( - f"\nKeyfile is not encrypted. \n:key: {self}" - ) - return False - elif keyfile_data_is_encrypted_nacl(keyfile_data): - if print_result: - bittensor.__console__.print( - f"\n:white_heavy_check_mark: Keyfile is updated. \n:key: {self}" - ) - return True - else: - if print_result: - bittensor.__console__.print( - f'\n:cross_mark: Keyfile is outdated, please update with "btcli wallet update" \n:key: {self}' - ) - return False - return False - - def encrypt(self, password: str = None): - """Encrypts the file under the path. - - Args: - password (str, optional): The password for encryption. If ``None``, asks for user input. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, or writable. - """ - if not self.exists_on_device(): - raise bittensor.KeyFileError( - "Keyfile at: {} does not exist".format(self.path) - ) - if not self.is_readable(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not readable".format(self.path) - ) - if not self.is_writable(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not writable".format(self.path) - ) - keyfile_data = self._read_keyfile_data_from_file() - if not keyfile_data_is_encrypted(keyfile_data): - as_keypair = deserialize_keypair_from_keyfile_data(keyfile_data) - keyfile_data = serialized_keypair_to_keyfile_data(as_keypair) - keyfile_data = encrypt_keyfile_data(keyfile_data, password) - self._write_keyfile_data_to_file(keyfile_data, overwrite=True) - - def decrypt(self, password: str = None): - """Decrypts the file under the path. - - Args: - password (str, optional): The password for decryption. If ``None``, asks for user input. - Raises: - KeyFileError: Raised if the file does not exist, is not readable, writable, corrupted, or if the password is incorrect. - """ - if not self.exists_on_device(): - raise bittensor.KeyFileError( - "Keyfile at: {} does not exist".format(self.path) - ) - if not self.is_readable(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not readable".format(self.path) - ) - if not self.is_writable(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not writable".format(self.path) - ) - keyfile_data = self._read_keyfile_data_from_file() - if keyfile_data_is_encrypted(keyfile_data): - keyfile_data = decrypt_keyfile_data( - keyfile_data, password, coldkey_name=self.name - ) - as_keypair = deserialize_keypair_from_keyfile_data(keyfile_data) - keyfile_data = serialized_keypair_to_keyfile_data(as_keypair) - self._write_keyfile_data_to_file(keyfile_data, overwrite=True) - - def _read_keyfile_data_from_file(self) -> bytes: - """Reads the keyfile data from the file. - - Returns: - keyfile_data (bytes): The keyfile data stored under the path. - Raises: - KeyFileError: Raised if the file does not exist or is not readable. - """ - if not self.exists_on_device(): - raise bittensor.KeyFileError( - "Keyfile at: {} does not exist".format(self.path) - ) - if not self.is_readable(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not readable".format(self.path) - ) - with open(self.path, "rb") as file: - data = file.read() - return data - - def _write_keyfile_data_to_file(self, keyfile_data: bytes, overwrite: bool = False): - """Writes the keyfile data to the file. - - Args: - keyfile_data (bytes): The byte data to store under the path. - overwrite (bool, optional): If ``True``, overwrites the data without asking for permission from the user. Default is ``False``. - Raises: - KeyFileError: Raised if the file is not writable or the user responds No to the overwrite prompt. - """ - # Check overwrite. - if self.exists_on_device() and not overwrite: - if not self._may_overwrite(): - raise bittensor.KeyFileError( - "Keyfile at: {} is not writable".format(self.path) - ) - with open(self.path, "wb") as keyfile: - keyfile.write(keyfile_data) - # Set file permissions. - os.chmod(self.path, stat.S_IRUSR | stat.S_IWUSR) - - -class Mockkeyfile: - """ - The Mockkeyfile is a mock object representing a keyfile that does not exist on the device. - - It is designed for use in testing scenarios and simulations where actual filesystem operations are not required. - The keypair stored in the Mockkeyfile is treated as non-encrypted and the data is stored as a serialized string. - """ - - def __init__(self, path: str): - """ - Initializes a Mockkeyfile object. - - Args: - path (str): The path of the mock keyfile. - """ - self.path = path - self._mock_keypair = None - self._mock_data = None - - def __str__(self): - """ - Returns a string representation of the Mockkeyfile. The representation will indicate if the keyfile is empty, encrypted, or decrypted. - - Returns: - str: The string representation of the Mockkeyfile. - """ - return f"Mockkeyfile({self.path})" - - def __repr__(self): - """ - Returns a string representation of the Mockkeyfile, same as :func:`__str__()`. - - Returns: - str: The string representation of the Mockkeyfile. - """ - return self.__str__() - - @property - def keypair(self): - """ - Returns the mock keypair stored in the keyfile. - - Returns: - bittensor.Keypair: The mock keypair. - """ - return self._mock_keypair - - @property - def data(self): - """ - Returns the serialized keypair data stored in the keyfile. - - Returns: - bytes: The serialized keypair data. - """ - return self._mock_data - - def set_keypair(self, keypair, encrypt=True, overwrite=False, password=None): - """ - Sets the mock keypair in the keyfile. The ``encrypt`` and ``overwrite`` parameters are ignored. - - Args: - keypair (bittensor.Keypair): The mock keypair to be set. - encrypt (bool, optional): Ignored in this context. Defaults to ``True``. - overwrite (bool, optional): Ignored in this context. Defaults to ``False``. - password (str, optional): Ignored in this context. Defaults to ``None``. - """ - self._mock_keypair = keypair - self._mock_data = None # You may need to serialize the keypair here - - def get_keypair(self, password=None): - """ - Returns the mock keypair stored in the keyfile. The ``password`` parameter is ignored. - - Args: - password (str, optional): Ignored in this context. Defaults to ``None``. - - Returns: - bittensor.Keypair: The mock keypair stored in the keyfile. - """ - return self._mock_keypair - - def make_dirs(self): - """ - Creates the directories for the mock keyfile. Does nothing in this class, since no actual filesystem operations are needed. - """ - pass - - def exists_on_device(self): - """ - Returns ``True`` indicating that the mock keyfile exists on the device (although it is not created on the actual file system). - - Returns: - bool: Always returns ``True`` for Mockkeyfile. - """ - return True - - def is_readable(self): - """ - Returns ``True`` indicating that the mock keyfile is readable (although it is not read from the actual file system). - - Returns: - bool: Always returns ``True`` for Mockkeyfile. - """ - return True - - def is_writable(self): - """ - Returns ``True`` indicating that the mock keyfile is writable (although it is not written to the actual file system). - - Returns: - bool: Always returns ``True`` for Mockkeyfile. - """ - return True - - def is_encrypted(self): - """ - Returns ``False`` indicating that the mock keyfile is not encrypted. - - Returns: - bool: Always returns ``False`` for Mockkeyfile. - """ - return False - - def encrypt(self, password=None): - """ - Raises a ValueError since encryption is not supported for the mock keyfile. - - Args: - password (str, optional): Ignored in this context. Defaults to ``None``. - - Raises: - ValueError: Always raises this exception for Mockkeyfile. - """ - raise ValueError("Cannot encrypt a Mockkeyfile") - - def decrypt(self, password=None): - """ - Returns without doing anything since the mock keyfile is not encrypted. - - Args: - password (str, optional): Ignored in this context. Defaults to ``None``. - """ - pass - - def check_and_update_encryption(self, no_prompt=None, print_result=False): - return diff --git a/bittensor/mock/keyfile_mock.py b/bittensor/mock/keyfile_mock.py deleted file mode 100644 index e13126cc1..000000000 --- a/bittensor/mock/keyfile_mock.py +++ /dev/null @@ -1,90 +0,0 @@ -# The MIT License (MIT) - -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies - -# 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. - -from bittensor import serialized_keypair_to_keyfile_data, keyfile, Keypair - - -class MockKeyfile(keyfile): - """Defines an interface to a mocked keyfile object (nothing is created on device) keypair is treated as non encrypted and the data is just the string version.""" - - def __init__(self, path: str): - super().__init__(path) - - self._mock_keypair = Keypair.create_from_mnemonic( - mnemonic="arrive produce someone view end scout bargain coil slight festival excess struggle" - ) - self._mock_data = serialized_keypair_to_keyfile_data(self._mock_keypair) - - def __str__(self): - if not self.exists_on_device(): - return "Keyfile (empty, {})>".format(self.path) - if self.is_encrypted(): - return "Keyfile (encrypted, {})>".format(self.path) - else: - return "Keyfile (decrypted, {})>".format(self.path) - - def __repr__(self): - return self.__str__() - - @property - def keypair(self) -> "Keypair": - return self._mock_keypair - - @property - def data(self) -> bytes: - return bytes(self._mock_data) - - @property - def keyfile_data(self) -> bytes: - return bytes(self._mock_data) - - def set_keypair( - self, - keypair: "Keypair", - encrypt: bool = True, - overwrite: bool = False, - password: str = None, - ): - self._mock_keypair = keypair - self._mock_data = serialized_keypair_to_keyfile_data(self._mock_keypair) - - def get_keypair(self, password: str = None) -> "Keypair": - return self._mock_keypair - - def make_dirs(self): - return - - def exists_on_device(self) -> bool: - return True - - def is_readable(self) -> bool: - return True - - def is_writable(self) -> bool: - return True - - def is_encrypted(self) -> bool: - return False - - def encrypt(self, password: str = None): - raise ValueError("Cannot encrypt a mock keyfile") - - def decrypt(self, password: str = None): - return diff --git a/bittensor/mock/wallet_mock.py b/bittensor/mock/wallet_mock.py deleted file mode 100644 index 35179f8c9..000000000 --- a/bittensor/mock/wallet_mock.py +++ /dev/null @@ -1,127 +0,0 @@ -# The MIT License (MIT) - -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies - -# 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 os -import bittensor -from typing import Optional -from Crypto.Hash import keccak - -from .keyfile_mock import MockKeyfile - - -class MockWallet(bittensor.wallet): - """ - Mocked Version of the bittensor wallet class, meant to be used for testing - """ - - def __init__(self, **kwargs): - r"""Init bittensor wallet object containing a hot and coldkey. - Args: - _mock (required=True, default=False): - If true creates a mock wallet with random keys. - """ - super().__init__(**kwargs) - # For mocking. - self._is_mock = True - self._mocked_coldkey_keyfile = None - self._mocked_hotkey_keyfile = None - - @property - def hotkey_file(self) -> "bittensor.keyfile": - if self._is_mock: - if self._mocked_hotkey_keyfile == None: - self._mocked_hotkey_keyfile = MockKeyfile(path="MockedHotkey") - return self._mocked_hotkey_keyfile - else: - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - hotkey_path = os.path.join(wallet_path, "hotkeys", self.hotkey_str) - return bittensor.keyfile(path=hotkey_path) - - @property - def coldkey_file(self) -> "bittensor.keyfile": - if self._is_mock: - if self._mocked_coldkey_keyfile == None: - self._mocked_coldkey_keyfile = MockKeyfile(path="MockedColdkey") - return self._mocked_coldkey_keyfile - else: - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkey_path = os.path.join(wallet_path, "coldkey") - return bittensor.keyfile(path=coldkey_path) - - @property - def coldkeypub_file(self) -> "bittensor.keyfile": - if self._is_mock: - if self._mocked_coldkey_keyfile == None: - self._mocked_coldkey_keyfile = MockKeyfile(path="MockedColdkeyPub") - return self._mocked_coldkey_keyfile - else: - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkeypub_path = os.path.join(wallet_path, "coldkeypub.txt") - return bittensor.keyfile(path=coldkeypub_path) - - -def get_mock_wallet( - coldkey: "bittensor.Keypair" = None, hotkey: "bittensor.Keypair" = None -): - wallet = MockWallet(name="mock_wallet", hotkey="mock", path="/tmp/mock_wallet") - - if not coldkey: - coldkey = bittensor.Keypair.create_from_mnemonic( - bittensor.Keypair.generate_mnemonic() - ) - if not hotkey: - hotkey = bittensor.Keypair.create_from_mnemonic( - bittensor.Keypair.generate_mnemonic() - ) - - wallet.set_coldkey(coldkey, encrypt=False, overwrite=True) - wallet.set_coldkeypub(coldkey, encrypt=False, overwrite=True) - wallet.set_hotkey(hotkey, encrypt=False, overwrite=True) - - return wallet - - -def get_mock_keypair(uid: int, test_name: Optional[str] = None) -> bittensor.Keypair: - """ - Returns a mock keypair from a uid and optional test_name. - If test_name is not provided, the uid is the only seed. - If test_name is provided, the uid is hashed with the test_name to create a unique seed for the test. - """ - if test_name is not None: - hashed_test_name: bytes = keccak.new( - digest_bits=256, data=test_name.encode("utf-8") - ).digest() - hashed_test_name_as_int: int = int.from_bytes( - hashed_test_name, byteorder="big", signed=False - ) - uid = uid + hashed_test_name_as_int - - return bittensor.Keypair.create_from_seed( - seed_hex=int.to_bytes(uid, 32, "big", signed=False), - ss58_format=bittensor.__ss58_format__, - ) - - -def get_mock_hotkey(uid: int) -> str: - return get_mock_keypair(uid).ss58_address - - -def get_mock_coldkey(uid: int) -> str: - return get_mock_keypair(uid).ss58_address diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py deleted file mode 100644 index c477a0b77..000000000 --- a/bittensor/subtensor.py +++ /dev/null @@ -1,5909 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 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. - -""" -The ``bittensor.subtensor`` module in Bittensor serves as a crucial interface for interacting with the Bittensor -blockchain, facilitating a range of operations essential for the decentralized machine learning network. -""" - -from __future__ import annotations - -import argparse -import copy -import socket -import time -from typing import List, Dict, Union, Optional, Tuple, TypedDict, Any - -import numpy as np -import scalecodec -from numpy.typing import NDArray -from retry import retry -from scalecodec.base import RuntimeConfiguration -from scalecodec.exceptions import RemainingScaleBytesNotEmptyException -from scalecodec.type_registry import load_type_registry_preset -from scalecodec.types import GenericCall, ScaleType -from substrateinterface.base import QueryMapResult, SubstrateInterface, ExtrinsicReceipt -from substrateinterface.exceptions import SubstrateRequestException - -import bittensor -from bittensor.btlogging import logging as _logger -from bittensor.utils import torch, weight_utils, format_error_message -from .chain_data import ( - DelegateInfoLite, - NeuronInfo, - DelegateInfo, - PrometheusInfo, - SubnetInfo, - SubnetHyperparameters, - StakeInfo, - NeuronInfoLite, - AxonInfo, - ProposalVoteData, - IPInfo, - custom_rpc_type_registry, -) -from .commands.utils import DelegatesDetails -from .errors import ( - IdentityError, - NominationError, - StakeError, - TakeError, -) -from .extrinsics.commit_weights import ( - commit_weights_extrinsic, - reveal_weights_extrinsic, -) -from .extrinsics.delegation import ( - delegate_extrinsic, - nominate_extrinsic, - undelegate_extrinsic, - increase_take_extrinsic, - decrease_take_extrinsic, -) -from .extrinsics.network import ( - register_subnetwork_extrinsic, - set_hyperparameter_extrinsic, -) -from .extrinsics.prometheus import prometheus_extrinsic -from .extrinsics.registration import ( - register_extrinsic, - burned_register_extrinsic, - run_faucet_extrinsic, - swap_hotkey_extrinsic, -) -from .extrinsics.root import root_register_extrinsic, set_root_weights_extrinsic -from .extrinsics.senate import ( - register_senate_extrinsic, - leave_senate_extrinsic, - vote_senate_extrinsic, -) -from .extrinsics.serving import ( - serve_extrinsic, - serve_axon_extrinsic, - publish_metadata, - get_metadata, -) -from .extrinsics.set_weights import set_weights_extrinsic -from .extrinsics.staking import ( - add_stake_extrinsic, - add_stake_multiple_extrinsic, - set_children_extrinsic, - set_childkey_take_extrinsic, -) -from .extrinsics.transfer import transfer_extrinsic -from .extrinsics.unstaking import ( - unstake_extrinsic, - unstake_multiple_extrinsic, -) -from .types import AxonServeCallParams, PrometheusServeCallParams -from .utils import ( - U16_NORMALIZED_FLOAT, - ss58_to_vec_u8, - U64_NORMALIZED_FLOAT, - networking, -) -from .utils.balance import Balance -from .utils.registration import POWSolution -from .utils.registration import legacy_torch_api_compat -from .utils.subtensor import get_subtensor_errors, format_parent, format_children - -KEY_NONCE: Dict[str, int] = {} - - -class ParamWithTypes(TypedDict): - name: str # Name of the parameter. - type: str # ScaleType string of the parameter. - - -class Subtensor: - """ - The Subtensor class in Bittensor serves as a crucial interface for interacting with the Bittensor blockchain, - facilitating a range of operations essential for the decentralized machine learning network. - - This class enables neurons (network participants) to engage in activities such as registering on the network, - managing staked weights, setting inter-neuronal weights, and participating in consensus mechanisms. - - The Bittensor network operates on a digital ledger where each neuron holds stakes (S) and learns a set - of inter-peer weights (W). These weights, set by the neurons themselves, play a critical role in determining - the ranking and incentive mechanisms within the network. Higher-ranked neurons, as determined by their - contributions and trust within the network, receive more incentives. - - The Subtensor class connects to various Bittensor networks like the main ``finney`` network or local test - networks, providing a gateway to the blockchain layer of Bittensor. It leverages a staked weighted trust - system and consensus to ensure fair and distributed incentive mechanisms, where incentives (I) are - primarily allocated to neurons that are trusted by the majority of the network. - - Additionally, Bittensor introduces a speculation-based reward mechanism in the form of bonds (B), allowing - neurons to accumulate bonds in other neurons, speculating on their future value. This mechanism aligns - with market-based speculation, incentivizing neurons to make judicious decisions in their inter-neuronal - investments. - - Example Usage:: - - # Connect to the main Bittensor network (Finney). - finney_subtensor = subtensor(network='finney') - - # Close websocket connection with the Bittensor network. - finney_subtensor.close() - - # (Re)creates the websocket connection with the Bittensor network. - finney_subtensor.connect_websocket() - - # Register a new neuron on the network. - wallet = bittensor.wallet(...) # Assuming a wallet instance is created. - success = finney_subtensor.register(wallet=wallet, netuid=netuid) - - # Set inter-neuronal weights for collaborative learning. - success = finney_subtensor.set_weights(wallet=wallet, netuid=netuid, uids=[...], weights=[...]) - - # Speculate by accumulating bonds in other promising neurons. - success = finney_subtensor.delegate(wallet=wallet, delegate_ss58=other_neuron_ss58, amount=bond_amount) - - # Get the metagraph for a specific subnet using given subtensor connection - metagraph = subtensor.metagraph(netuid=netuid) - - By facilitating these operations, the Subtensor class is instrumental in maintaining the decentralized - intelligence and dynamic learning environment of the Bittensor network, as envisioned in its foundational - principles and mechanisms described in the `NeurIPS paper - `_. paper. - """ - - def __init__( - self, - network: Optional[str] = None, - config: Optional[bittensor.config] = None, - _mock: bool = False, - log_verbose: bool = True, - connection_timeout: int = 600, - ) -> None: - """ - Initializes a Subtensor interface for interacting with the Bittensor blockchain. - - NOTE: - Currently subtensor defaults to the ``finney`` network. This will change in a future release. - - We strongly encourage users to run their own local subtensor node whenever possible. This increases - decentralization and resilience of the network. In a future release, local subtensor will become the - default and the fallback to ``finney`` removed. Please plan ahead for this change. We will provide detailed - instructions on how to run a local subtensor node in the documentation in a subsequent release. - - Args: - network (str, optional): The network name to connect to (e.g., ``finney``, ``local``). This can also be the - chain endpoint (e.g., ``wss://entrypoint-finney.opentensor.ai:443``) and will be correctly parsed into - the network and chain endpoint. If not specified, defaults to the main Bittensor network. - config (bittensor.config, optional): Configuration object for the subtensor. If not provided, a default - configuration is used. - _mock (bool, optional): If set to ``True``, uses a mocked connection for testing purposes. - - This initialization sets up the connection to the specified Bittensor network, allowing for various - blockchain operations such as neuron registration, stake management, and setting weights. - - """ - # Determine config.subtensor.chain_endpoint and config.subtensor.network config. - # If chain_endpoint is set, we override the network flag, otherwise, the chain_endpoint is assigned by the - # network. - # Argument importance: network > chain_endpoint > config.subtensor.chain_endpoint > config.subtensor.network - - # Check if network is a config object. (Single argument passed as first positional) - if isinstance(network, bittensor.config): - if network.subtensor is None: - _logger.warning( - "If passing a bittensor config object, it must not be empty. Using default subtensor config." - ) - config = None - else: - config = network - network = None - - if config is None: - config = Subtensor.config() - self.config = copy.deepcopy(config) # type: ignore - - # Setup config.subtensor.network and config.subtensor.chain_endpoint - self.chain_endpoint, self.network = Subtensor.setup_config(network, config) # type: ignore - - if ( - self.network == "finney" - or self.chain_endpoint == bittensor.__finney_entrypoint__ - ) and log_verbose: - _logger.info( - f"You are connecting to {self.network} network with endpoint {self.chain_endpoint}." - ) - _logger.warning( - "We strongly encourage running a local subtensor node whenever possible. " - "This increases decentralization and resilience of the network." - ) - _logger.warning( - "In a future release, local subtensor will become the default endpoint. " - "To get ahead of this change, please run a local subtensor node and point to it." - ) - - self.log_verbose = log_verbose - self._connection_timeout = connection_timeout - self._get_substrate() - - self._subtensor_errors: Dict[str, Dict[str, str]] = {} - - def __str__(self) -> str: - if self.network == self.chain_endpoint: - # Connecting to chain endpoint without network known. - return "subtensor({})".format(self.chain_endpoint) - else: - # Connecting to network with endpoint known. - return "subtensor({}, {})".format(self.network, self.chain_endpoint) - - def __repr__(self) -> str: - return self.__str__() - - def _get_substrate(self): - """Establishes a connection to the Substrate node using configured parameters.""" - try: - # Set up params. - self.substrate = SubstrateInterface( - ss58_format=bittensor.__ss58_format__, - use_remote_preset=True, - url=self.chain_endpoint, - type_registry=bittensor.__type_registry__, - ) - if self.log_verbose: - _logger.info( - f"Connected to {self.network} network and {self.chain_endpoint}." - ) - - except ConnectionRefusedError: - _logger.error( - f"Could not connect to {self.network} network with {self.chain_endpoint} chain endpoint. Exiting...", - ) - _logger.info( - "You can check if you have connectivity by running this command: nc -vz localhost " - f"{self.chain_endpoint.split(':')[2]}" - ) - return - - try: - self.substrate.websocket.settimeout(self._connection_timeout) - except AttributeError as e: - _logger.warning(f"AttributeError: {e}") - except TypeError as e: - _logger.warning(f"TypeError: {e}") - except (socket.error, OSError) as e: - _logger.warning(f"Socket error: {e}") - - @staticmethod - def config() -> "bittensor.config": - """ - Creates and returns a Bittensor configuration object. - - Returns: - config (bittensor.config): A Bittensor configuration object configured with arguments added by the - `subtensor.add_args` method. - """ - parser = argparse.ArgumentParser() - Subtensor.add_args(parser) - return bittensor.config(parser, args=[]) - - @classmethod - def help(cls): - """Print help to stdout.""" - parser = argparse.ArgumentParser() - cls.add_args(parser) - print(cls.__new__.__doc__) - parser.print_help() - - @classmethod - def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = None): - """ - Adds command-line arguments to the provided ArgumentParser for configuring the Subtensor settings. - - Args: - parser (argparse.ArgumentParser): The ArgumentParser object to which the Subtensor arguments will be added. - prefix (Optional[str]): An optional prefix for the argument names. If provided, the prefix is prepended to - each argument name. - - Arguments added: - --subtensor.network: The Subtensor network flag. Possible values are 'finney', 'test', 'archive', and - 'local'. Overrides the chain endpoint if set. - --subtensor.chain_endpoint: The Subtensor chain endpoint flag. If set, it overrides the network flag. - --subtensor._mock: If true, uses a mocked connection to the chain. - - Example: - parser = argparse.ArgumentParser() - Subtensor.add_args(parser) - """ - prefix_str = "" if prefix is None else f"{prefix}." - try: - default_network = bittensor.__networks__[1] - default_chain_endpoint = bittensor.__finney_entrypoint__ - - parser.add_argument( - f"--{prefix_str}subtensor.network", - default=default_network, - type=str, - help="""The subtensor network flag. The likely choices are: - -- finney (main network) - -- test (test network) - -- archive (archive network +300 blocks) - -- local (local running network) - If this option is set it overloads subtensor.chain_endpoint with - an entry point node from that network. - """, - ) - parser.add_argument( - f"--{prefix_str}subtensor.chain_endpoint", - default=default_chain_endpoint, - type=str, - help="""The subtensor endpoint flag. If set, overrides the --network flag.""", - ) - parser.add_argument( - f"--{prefix_str}subtensor._mock", - default=False, - type=bool, - help="""If true, uses a mocked connection to the chain.""", - ) - - except argparse.ArgumentError: - # re-parsing arguments. - pass - - @staticmethod - def determine_chain_endpoint_and_network(network: str): - """Determines the chain endpoint and network from the passed network or chain_endpoint. - - Args: - network (str): The network flag. The choices are: ``-- finney`` (main network), ``-- archive`` - (archive network +300 blocks), ``-- local`` (local running network), ``-- test`` (test network). - Returns: - network (str): The network flag. - chain_endpoint (str): The chain endpoint flag. If set, overrides the ``network`` argument. - """ - if network is None: - return None, None - if network in ["finney", "local", "test", "archive"]: - if network == "finney": - # Kiru Finney staging network. - return network, bittensor.__finney_entrypoint__ - elif network == "local": - return network, bittensor.__local_entrypoint__ - elif network == "test": - return network, bittensor.__finney_test_entrypoint__ - elif network == "archive": - return network, bittensor.__archive_entrypoint__ - else: - if ( - network == bittensor.__finney_entrypoint__ - or "entrypoint-finney.opentensor.ai" in network - ): - return "finney", bittensor.__finney_entrypoint__ - elif ( - network == bittensor.__finney_test_entrypoint__ - or "test.finney.opentensor.ai" in network - ): - return "test", bittensor.__finney_test_entrypoint__ - elif ( - network == bittensor.__archive_entrypoint__ - or "archive.chain.opentensor.ai" in network - ): - return "archive", bittensor.__archive_entrypoint__ - elif "127.0.0.1" in network or "localhost" in network: - return "local", network - else: - return "unknown network", network - - @staticmethod - def setup_config(network: str, config: "bittensor.config"): - """ - Sets up and returns the configuration for the Subtensor network and endpoint. - - This method determines the appropriate network and chain endpoint based on the provided network string or - configuration object. It evaluates the network and endpoint in the following order of precedence: - 1. Provided network string. - 2. Configured chain endpoint in the `config` object. - 3. Configured network in the `config` object. - 4. Default chain endpoint. - 5. Default network. - - Args: - network (str): The name of the Subtensor network. If None, the network and endpoint will be determined from - the `config` object. - config (bittensor.config): The configuration object containing the network and chain endpoint settings. - - Returns: - tuple: A tuple containing the formatted WebSocket endpoint URL and the evaluated network name. - """ - if network is not None: - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network(network) - else: - if config.get("__is_set", {}).get("subtensor.chain_endpoint"): - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.chain_endpoint - ) - - elif config.get("__is_set", {}).get("subtensor.network"): - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - - elif config.subtensor.get("chain_endpoint"): - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.chain_endpoint - ) - - elif config.subtensor.get("network"): - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - config.subtensor.network - ) - - else: - ( - evaluated_network, - evaluated_endpoint, - ) = Subtensor.determine_chain_endpoint_and_network( - bittensor.defaults.subtensor.network - ) - - return ( - networking.get_formatted_ws_endpoint_url(evaluated_endpoint), - evaluated_network, - ) - - def close(self): - """Cleans up resources for this subtensor instance like active websocket connection and active extensions.""" - self.substrate.close() - - ############## - # Delegation # - ############## - def nominate( - self, - wallet: "bittensor.wallet", - wait_for_finalization: bool = False, - wait_for_inclusion: bool = True, - ) -> bool: - """ - Becomes a delegate for the hotkey associated with the given wallet. This method is used to nominate - a neuron (identified by the hotkey in the wallet) as a delegate on the Bittensor network, allowing it - to participate in consensus and validation processes. - - Args: - wallet (bittensor.wallet): The wallet containing the hotkey to be nominated. - wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the - blockchain. - wait_for_inclusion (bool, optional): If ``True``, waits until the transaction is included in a block. - - Returns: - bool: ``True`` if the nomination process is successful, ``False`` otherwise. - - This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the - dynamic selection and participation of validators in the network's consensus process. - """ - return nominate_extrinsic( - subtensor=self, - wallet=wallet, - wait_for_finalization=wait_for_finalization, - wait_for_inclusion=wait_for_inclusion, - ) - - def delegate( - self, - wallet: "bittensor.wallet", - delegate_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Becomes a delegate for the hotkey associated with the given wallet. This method is used to nominate - a neuron (identified by the hotkey in the wallet) as a delegate on the Bittensor network, allowing it - to participate in consensus and validation processes. - - Args: - wallet (bittensor.wallet): The wallet containing the hotkey to be nominated. - delegate_ss58 (Optional[str]): The ``SS58`` address of the delegate neuron. - amount (Union[Balance, float]): The amount of TAO to undelegate. - wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the - blockchain. - wait_for_inclusion (bool, optional): If ``True``, waits until the transaction is included in a block. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the nomination process is successful, False otherwise. - - This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the - dynamic selection and participation of validators in the network's consensus process. - """ - return delegate_extrinsic( - subtensor=self, - wallet=wallet, - delegate_ss58=delegate_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def undelegate( - self, - wallet: "bittensor.wallet", - delegate_ss58: Optional[str] = None, - amount: Optional[Union[Balance, float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Removes a specified amount of stake from a delegate neuron using the provided wallet. This action - reduces the staked amount on another neuron, effectively withdrawing support or speculation. - - Args: - wallet (bittensor.wallet): The wallet used for the undelegation process. - delegate_ss58 (Optional[str]): The ``SS58`` address of the delegate neuron. - amount (Union[Balance, float]): The amount of TAO to undelegate. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the undelegation is successful, False otherwise. - - This function reflects the dynamic and speculative nature of the Bittensor network, allowing neurons - to adjust their stakes and investments based on changing perceptions and performances within the network. - """ - return undelegate_extrinsic( - subtensor=self, - wallet=wallet, - delegate_ss58=delegate_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def set_take( - self, - wallet: "bittensor.wallet", - delegate_ss58: Optional[str] = None, - take: float = 0.0, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """ - Set delegate hotkey take - Args: - wallet (bittensor.wallet): The wallet containing the hotkey to be nominated. - delegate_ss58 (str, optional): Hotkey - take (float): Delegate take on subnet ID - wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the - blockchain. - wait_for_inclusion (bool, optional): If ``True``, waits until the transaction is included in a block. - - Returns: - bool: ``True`` if the process is successful, False otherwise. - - This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the - dynamic selection and participation of validators in the network's consensus process. - """ - # Ensure delegate_ss58 is not None - if delegate_ss58 is None: - raise ValueError("delegate_ss58 cannot be None") - - # Calculate u16 representation of the take - takeu16 = int(take * 0xFFFF) - - # Check if the new take is greater or lower than existing take or if existing is set - delegate = self.get_delegate_by_hotkey(delegate_ss58) - current_take = None - if delegate is not None: - current_take = int(float(delegate.take) * 65535.0) - - if takeu16 == current_take: - bittensor.__console__.print("Nothing to do, take hasn't changed") - return True - if current_take is None or current_take < takeu16: - bittensor.__console__.print( - "Current take is either not set or is lower than the new one. Will use increase_take" - ) - return increase_take_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=delegate_ss58, - take=takeu16, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - else: - bittensor.__console__.print( - "Current take is higher than the new one. Will use decrease_take" - ) - return decrease_take_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=delegate_ss58, - take=takeu16, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - @networking.ensure_connected - def send_extrinsic( - self, - wallet: "bittensor.wallet", - module: str, - function: str, - params: dict, - period: int = 5, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - max_retries: int = 3, - wait_time: int = 3, - max_wait: int = 20, - ) -> Optional[ExtrinsicReceipt]: - """ - Sends an extrinsic to the Bittensor blockchain using the provided wallet and parameters. This method - constructs and submits the extrinsic, handling retries and blockchain communication. - - Args: - wallet (bittensor.wallet): The wallet associated with the extrinsic. - module (str): The module name for the extrinsic. - function (str): The function name for the extrinsic. - params (dict): The parameters for the extrinsic. - period (int, optional): The number of blocks for the extrinsic to live in the mempool. Defaults to 5. - 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. - max_retries (int, optional): The maximum number of retries for the extrinsic. Defaults to 3. - wait_time (int, optional): The wait time between retries. Defaults to 3. - max_wait (int, optional): The maximum wait time for the extrinsic. Defaults to 20. - - Returns: - Optional[ExtrinsicReceipt]: The receipt of the extrinsic if successful, None otherwise. - """ - call = self.substrate.compose_call( - call_module=module, - call_function=function, - call_params=params, - ) - - hotkey = wallet.get_hotkey().ss58_address - # Periodically update the nonce cache - if hotkey not in KEY_NONCE or self.get_current_block() % 5 == 0: - KEY_NONCE[hotkey] = self.substrate.get_account_nonce(hotkey) - - nonce = KEY_NONCE[hotkey] - - # <3 parity tech - old_init_runtime = self.substrate.init_runtime - self.substrate.init_runtime = lambda: None - self.substrate.init_runtime = old_init_runtime - response = None - - for attempt in range(1, max_retries + 1): - try: - # Create the extrinsic with new nonce - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - era={"period": period}, - nonce=nonce, - ) - - # Submit the extrinsic - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # Return immediately if we don't wait - if not wait_for_inclusion and not wait_for_finalization: - KEY_NONCE[hotkey] = nonce + 1 # update the nonce cache - return response - - # If we wait for finalization or inclusion, check if it is successful - if response.is_success: - KEY_NONCE[hotkey] = nonce + 1 # update the nonce cache - return response - else: - # Wait for a while - wait = min(wait_time * attempt, max_wait) - time.sleep(wait) - # Incr the nonce and try again - nonce = nonce + 1 - continue - - # This dies because user is spamming... incr and try again - except SubstrateRequestException as e: - if "Priority is too low" in e.args[0]["message"]: - wait = min(wait_time * attempt, max_wait) - _logger.warning( - f"Priority is too low, retrying with new nonce: {nonce} in {wait} seconds." - ) - nonce = nonce + 1 - time.sleep(wait) - continue - else: - _logger.error(f"Error sending extrinsic: {e}") - response = None - - return response - - ############### - # Set Weights # - ############### - # TODO: still needed? Can't find any usage of this method. - def set_weights( - self, - wallet: "bittensor.wallet", - netuid: int, - uids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], - version_key: int = bittensor.__version_as_int__, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, - max_retries: int = 5, - ) -> Tuple[bool, str]: - """ - Sets the inter-neuronal weights for the specified neuron. This process involves specifying the - influence or trust a neuron places on other neurons in the network, which is a fundamental aspect - of Bittensor's decentralized learning architecture. - - Args: - wallet (bittensor.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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_retries (int, optional): The number of maximum attempts to set weights. (Default: 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. - - 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】. - """ - uid = self.get_uid_for_hotkey_on_subnet(wallet.hotkey.ss58_address, netuid) - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to set weights!" - while ( - self.blocks_since_last_update(netuid, uid) > self.weights_rate_limit(netuid) # type: ignore - and retries < max_retries - ): - try: - success, message = set_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=uids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - except Exception as e: - _logger.error(f"Error setting weights: {e}") - finally: - retries += 1 - - return success, message - - @networking.ensure_connected - def _do_set_weights( - self, - wallet: "bittensor.wallet", - uids: List[int], - vals: List[int], - netuid: int, - version_key: int = bittensor.__version_as_int__, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[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. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron setting the weights. - uids (List[int]): List of neuron UIDs for which weights are being set. - vals (List[int]): List of weight values corresponding to each UID. - netuid (int): Unique identifier for the network. - 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. - - Returns: - Tuple[bool, Optional[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. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_weights", - call_params={ - "dests": uids, - "weights": vals, - "netuid": netuid, - "version_key": version_key, - }, - ) - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - era={"period": 5}, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # 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." - - response.process_events() - if response.is_success: - return True, "Successfully set weights." - else: - return False, format_error_message(response.error_message) - - return make_substrate_call_with_retry() - - ################## - # Commit Weights # - ################## - def commit_weights( - self, - wallet: "bittensor.wallet", - netuid: int, - salt: List[int], - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], - version_key: int = bittensor.__version_as_int__, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, - max_retries: int = 5, - ) -> Tuple[bool, str]: - """ - Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. - This action serves as a commitment or snapshot of the neuron's current weight distribution. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron committing the weights. - netuid (int): The unique identifier of the subnet. - salt (List[int]): list of randomly generated integers as salt to generated weighted hash. - 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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_retries (int, optional): The number of maximum attempts to commit weights. (Default: 5) - - Returns: - Tuple[bool, str]: ``True`` if the weight commitment is successful, False otherwise. And `msg`, 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. - """ - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to commit weights!" - - _logger.info( - "Committing weights with params: netuid={}, uids={}, weights={}, version_key={}".format( - netuid, uids, weights, version_key - ) - ) - - # Generate the hash of the weights - commit_hash = weight_utils.generate_weight_hash( - address=wallet.hotkey.ss58_address, - netuid=netuid, - uids=list(uids), - values=list(weights), - salt=salt, - version_key=version_key, - ) - - _logger.info("Commit Hash: {}".format(commit_hash)) - - while retries < max_retries: - try: - success, message = commit_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - commit_hash=commit_hash, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - if success: - break - except Exception as e: - bittensor.logging.error(f"Error committing weights: {e}") - finally: - retries += 1 - - return success, message - - @networking.ensure_connected - def _do_commit_weights( - self, - wallet: "bittensor.wallet", - netuid: int, - commit_hash: str, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[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: - wallet (bittensor.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, 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. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - 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. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="commit_weights", - call_params={ - "netuid": netuid, - "commit_hash": commit_hash, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message - - return make_substrate_call_with_retry() - - ################## - # Reveal Weights # - ################## - def reveal_weights( - self, - wallet: "bittensor.wallet", - netuid: int, - uids: Union[NDArray[np.int64], list], - weights: Union[NDArray[np.int64], list], - salt: Union[NDArray[np.int64], list], - version_key: int = bittensor.__version_as_int__, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, - max_retries: int = 5, - ) -> Tuple[bool, str]: - """ - Reveals the weights for a specific subnet on the Bittensor blockchain using the provided wallet. - This action serves as a revelation of the neuron's previously committed weight distribution. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron revealing the weights. - netuid (int): The unique identifier of the subnet. - uids (np.ndarray): NumPy array of neuron UIDs for which weights are being revealed. - 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, 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_retries (int, optional): The number of maximum attempts to reveal weights. (Default: 5) - - Returns: - Tuple[bool, str]: ``True`` if the weight revelation is successful, False otherwise. And `msg`, a string - value describing the success or potential error. - - This function allows neurons to reveal their previously committed weight distribution, ensuring transparency - and accountability within the Bittensor network. - """ - - retries = 0 - success = False - message = "No attempt made. Perhaps it is too soon to reveal weights!" - - while retries < max_retries: - try: - success, message = reveal_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - uids=list(uids), - weights=list(weights), - salt=list(salt), - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - if success: - break - except Exception as e: - bittensor.logging.error(f"Error revealing weights: {e}") - finally: - retries += 1 - - return success, message - - @networking.ensure_connected - def _do_reveal_weights( - self, - wallet: "bittensor.wallet", - netuid: int, - uids: List[int], - values: List[int], - salt: List[int], - version_key: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[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. - - Args: - wallet (bittensor.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. - values (List[int]): List of weight values corresponding to each UID. - salt (List[int]): List of salt values corresponding to the hash function. - version_key (int): 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. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. - - This method ensures that the weight revelation is securely recorded on the Bittensor blockchain, providing transparency - and accountability for the neuron's weight distribution. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="reveal_weights", - call_params={ - "netuid": netuid, - "uids": uids, - "values": values, - "salt": salt, - "version_key": version_key, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.hotkey, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - response.process_events() - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - - return make_substrate_call_with_retry() - - ################ - # Registration # - ################ - def register( - self, - wallet: "bittensor.wallet", - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[List[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, - ) -> bool: - """ - Registers a neuron on the Bittensor network using the provided wallet. Registration - is a critical step for a neuron to become an active participant in the network, enabling - it to stake, set weights, and receive incentives. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron to be registered. - netuid (int): The unique identifier of the subnet. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. - Defaults to `False`. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. - Defaults to `True`. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_allowed_attempts (int): Maximum number of attempts to register the wallet. - output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning - the progress is printed on the same lines. Defaults to `True`. - cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). Defaults to `False`. - dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. Defaults to `0` (zero). - tpb (int): The number of threads per block (CUDA). Default to `256`. - 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`. - - Returns: - bool: ``True`` if the registration is successful, False otherwise. - - This function facilitates the entry of new neurons into the network, supporting the decentralized - growth and scalability of the Bittensor ecosystem. - """ - return register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - max_allowed_attempts=max_allowed_attempts, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - def swap_hotkey( - self, - wallet: "bittensor.wallet", - new_wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - ) -> bool: - """ - Swaps an old hotkey with a new hotkey for the specified wallet. - - This method initiates an extrinsic to change the hotkey associated with a wallet to a new hotkey. It provides - options to wait for inclusion and finalization of the transaction, and to prompt the user for confirmation. - - Args: - wallet (bittensor.wallet): The wallet whose hotkey is to be swapped. - new_wallet (bittensor.wallet): The new wallet with the hotkey to be set. - 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`. - prompt (bool): Whether to prompt the user for confirmation before proceeding. Default is `False`. - - Returns: - bool: True if the hotkey swap was successful, False otherwise. - """ - return swap_hotkey_extrinsic( - subtensor=self, - wallet=wallet, - new_wallet=new_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def run_faucet( - self, - wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[List[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, - ) -> bool: - """ - Facilitates a faucet transaction, allowing new neurons to receive an initial amount of TAO - for participating in the network. This function is particularly useful for newcomers to the - Bittensor network, enabling them to start with a small stake on testnet only. - - Args: - wallet (bittensor.wallet): The wallet for which the faucet transaction is to be run. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. - Defaults to `False`. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. - Defaults to `True`. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - max_allowed_attempts (int): Maximum number of attempts to register the wallet. - output_in_place (bool): If true, prints the progress of the proof of work to the console in-place. Meaning - the progress is printed on the same lines. Defaults to `True`. - cuda (bool): If ``true``, the wallet should be registered using CUDA device(s). Defaults to `False`. - dev_id (Union[List[int], int]): The CUDA device id to use, or a list of device ids. Defaults to `0` (zero). - tpb (int): The number of threads per block (CUDA). Default to `256`. - 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`. - - Returns: - bool: ``True`` if the faucet transaction is successful, False otherwise. - - This function is part of Bittensor's onboarding process, ensuring that new neurons have - the necessary resources to begin their journey in the decentralized AI network. - - Note: - This is for testnet ONLY and is disabled currently. You must build your own staging subtensor chain with the - ``--features pow-faucet`` argument to enable this. - """ - result, _ = run_faucet_extrinsic( - subtensor=self, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - max_allowed_attempts=max_allowed_attempts, - output_in_place=output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - return result - - def burned_register( - self, - wallet: "bittensor.wallet", - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - ) -> bool: - """ - Registers a neuron on the Bittensor network by recycling TAO. This method of registration - involves recycling TAO tokens, allowing them to be re-mined by performing work on the network. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron to be registered. - netuid (int): The unique identifier of the subnet. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. - Defaults to `False`. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. - Defaults to `True`. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. Defaults to `False`. - - Returns: - bool: ``True`` if the registration is successful, False otherwise. - """ - return burned_register_extrinsic( - subtensor=self, - wallet=wallet, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - @networking.ensure_connected - def _do_pow_register( - self, - netuid: int, - wallet: "bittensor.wallet", - pow_result: POWSolution, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """Sends a (POW) register extrinsic to the chain. - - Args: - netuid (int): The subnet to register on. - wallet (bittensor.wallet): The wallet to 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`. - - Returns: - success (bool): ``True`` if the extrinsic was included in a block. - error (Optional[str]): ``None`` on success or not waiting for inclusion/finalization, otherwise the error - message. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="register", - call_params={ - "netuid": netuid, - "block_number": pow_result.block_number, - "nonce": pow_result.nonce, - "work": [int(byte_) for byte_ in pow_result.seal], - "hotkey": wallet.hotkey.ss58_address, - "coldkey": wallet.coldkeypub.ss58_address, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - - return make_substrate_call_with_retry() - - @networking.ensure_connected - def _do_burned_register( - self, - netuid: int, - wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """ - Performs a burned register extrinsic call to the Subtensor chain. - - This method sends a registration transaction to the Subtensor blockchain using the burned register mechanism. It - retries the call up to three times with exponential backoff in case of failures. - - Args: - netuid (int): The network unique identifier to register on. - wallet (bittensor.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. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional error message. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="burned_register", - call_params={ - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - - return make_substrate_call_with_retry() - - @networking.ensure_connected - def _do_swap_hotkey( - self, - wallet: "bittensor.wallet", - new_wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """ - Performs a hotkey swap extrinsic call to the Subtensor chain. - - Args: - wallet (bittensor.wallet): The wallet whose hotkey is to be swapped. - new_wallet (bittensor.wallet): The wallet with the new hotkey to be set. - 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`. - - Returns: - Tuple[bool, Optional[str]]: A tuple containing a boolean indicating success or failure, and an optional - error message. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="swap_hotkey", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "new_hotkey": new_wallet.hotkey.ss58_address, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - - return make_substrate_call_with_retry() - - ############ - # Transfer # - ############ - def transfer( - self, - wallet: "bittensor.wallet", - dest: str, - amount: Union[Balance, float], - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Executes a transfer of funds from the provided wallet to the specified destination address. - This function is used to move TAO tokens within the Bittensor network, facilitating transactions - between neurons. - - Args: - wallet (bittensor.wallet): The wallet from which funds are being transferred. - dest (str): The destination public key address. - amount (Union[Balance, float]): The amount of TAO to be transferred. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - transfer_extrinsic (bool): ``True`` if the transfer is successful, False otherwise. - - This function is essential for the fluid movement of tokens in the network, supporting - various economic activities such as staking, delegation, and reward distribution. - """ - return transfer_extrinsic( - subtensor=self, - wallet=wallet, - dest=dest, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - @networking.ensure_connected - def get_transfer_fee( - self, wallet: "bittensor.wallet", dest: str, value: Union["Balance", float, int] - ) -> "Balance": - """ - Calculates the transaction fee for transferring tokens from a wallet to a specified destination address. - This function simulates the transfer to estimate the associated cost, taking into account the current - network conditions and transaction complexity. - - Args: - wallet (bittensor.wallet): The wallet from which the transfer is initiated. - dest (str): The ``SS58`` address of the destination account. - value (Union[Balance, float, int]): The amount of tokens to be transferred, specified as a Balance object, - or in Tao (float) or Rao (int) units. - - Returns: - Balance: The estimated transaction fee for the transfer, represented as a Balance object. - - Estimating the transfer fee is essential for planning and executing token transactions, ensuring that the - wallet has sufficient funds to cover both the transfer amount and the associated costs. This function - provides a crucial tool for managing financial operations within the Bittensor network. - """ - if isinstance(value, float): - value = Balance.from_tao(value) - elif isinstance(value, int): - value = Balance.from_rao(value) - - if isinstance(value, Balance): - call = self.substrate.compose_call( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": dest, "value": value.rao}, - ) - - try: - payment_info = self.substrate.get_payment_info( - call=call, keypair=wallet.coldkeypub - ) - except Exception as e: - bittensor.__console__.print( - ":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n {}[/bold white]".format( - e - ) - ) - payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao - - fee = Balance.from_rao(payment_info["partialFee"]) - return fee - else: - fee = Balance.from_rao(int(2e7)) - _logger.error( - "To calculate the transaction fee, the value must be Balance, float, or int. Received type: %s. Fee " - "is %s", - type(value), - 2e7, - ) - return fee - - @networking.ensure_connected - def _do_transfer( - self, - wallet: "bittensor.wallet", - dest: str, - transfer_balance: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[str], Optional[str]]: - """Sends a transfer extrinsic to the chain. - - Args: - wallet (:func:`bittensor.wallet`): Wallet object. - dest (str): Destination public key address. - transfer_balance (:func:`Balance`): Amount to transfer. - wait_for_inclusion (bool): If ``true``, waits for inclusion. - wait_for_finalization (bool): If ``true``, waits for finalization. - Returns: - success (bool): ``True`` if transfer was successful. - block_hash (str): Block hash of the transfer. On success and if wait_for_ finalization/inclusion is - ``True``. - error (str): Error message if transfer failed. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="Balances", - call_function="transfer_allow_death", - call_params={"dest": dest, "value": transfer_balance.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None, None - - # Otherwise continue with finalization. - response.process_events() - if response.is_success: - block_hash = response.block_hash - return True, block_hash, None - else: - return False, None, format_error_message(response.error_message) - - return make_substrate_call_with_retry() - - def get_existential_deposit( - self, block: Optional[int] = None - ) -> Optional["Balance"]: - """ - Retrieves the existential deposit amount for the Bittensor blockchain. The existential deposit - is the minimum amount of TAO required for an account to exist on the blockchain. Accounts with - balances below this threshold can be reaped to conserve network resources. - - Args: - block (Optional[int]): Block number at which to query the deposit amount. If ``None``, the current block is - used. - - Returns: - Optional[Balance]: The existential deposit amount, or ``None`` if the query fails. - - The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring - efficient use of storage and preventing the proliferation of dust accounts. - """ - result = self.query_constant( - module_name="Balances", constant_name="ExistentialDeposit", block=block - ) - - if result is None or not hasattr(result, "value"): - return None - - return Balance.from_rao(result.value) - - ########### - # Network # - ########### - def register_subnetwork( - self, - wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization=True, - prompt: bool = False, - ) -> bool: - """ - Registers a new subnetwork on the Bittensor network using the provided wallet. This function - is used for the creation and registration of subnetworks, which are specialized segments of the - overall Bittensor network. - - Args: - wallet (bittensor.wallet): The wallet to be used for registration. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the subnetwork registration is successful, False otherwise. - - This function allows for the expansion and diversification of the Bittensor network, supporting - its decentralized and adaptable architecture. - """ - return register_subnetwork_extrinsic( - self, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def set_hyperparameter( - self, - wallet: "bittensor.wallet", - netuid: int, - parameter: str, - value, - wait_for_inclusion: bool = False, - wait_for_finalization=True, - prompt: bool = False, - ) -> bool: - """ - Sets a specific hyperparameter for a given subnetwork on the Bittensor blockchain. This action - involves adjusting network-level parameters, influencing the behavior and characteristics of the - subnetwork. - - Args: - wallet (bittensor.wallet): The wallet used for setting the hyperparameter. - netuid (int): The unique identifier of the subnetwork. - parameter (str): The name of the hyperparameter to be set. - value: The new value for the hyperparameter. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the hyperparameter setting is successful, False otherwise. - - This function plays a critical role in the dynamic governance and adaptability of the Bittensor - network, allowing for fine-tuning of network operations and characteristics. - """ - return set_hyperparameter_extrinsic( - self, - wallet=wallet, - netuid=netuid, - parameter=parameter, - value=value, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - ########### - # Serving # - ########### - def serve( - self, - wallet: "bittensor.wallet", - ip: str, - port: int, - protocol: int, - netuid: int, - placeholder1: int = 0, - placeholder2: int = 0, - wait_for_inclusion: bool = False, - wait_for_finalization=True, - ) -> bool: - """ - Registers a neuron's serving endpoint on the Bittensor network. This function announces the - IP address and port where the neuron is available to serve requests, facilitating peer-to-peer - communication within the network. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron being served. - ip (str): The IP address of the serving neuron. - port (int): The port number on which the neuron is serving. - protocol (int): The protocol type used by the neuron (e.g., GRPC, HTTP). - netuid (int): The unique identifier of the subnetwork. - placeholder1 (int, optional): Placeholder parameter for future extensions. Default is ``0``. - placeholder2 (int, optional): Placeholder parameter for future extensions. Default is ``0``. - wait_for_inclusion (bool, optional): Waits for the transaction to be included in a block. Default is - ``False``. - wait_for_finalization (bool, optional): Waits for the transaction to be finalized on the blockchain. Default - is ``True``. - - Returns: - bool: ``True`` if the serve registration is successful, False otherwise. - - This function is essential for establishing the neuron's presence in the network, enabling - it to participate in the decentralized machine learning processes of Bittensor. - """ - return serve_extrinsic( - self, - wallet, - ip, - port, - protocol, - netuid, - placeholder1, - placeholder2, - wait_for_inclusion, - wait_for_finalization, - ) - - def serve_axon( - self, - netuid: int, - axon: "bittensor.axon", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> bool: - """ - Registers an Axon serving endpoint on the Bittensor network for a specific neuron. This function - is used to set up the Axon, a key component of a neuron that handles incoming queries and data - processing tasks. - - Args: - netuid (int): The unique identifier of the subnetwork. - axon (bittensor.Axon): The Axon instance to be registered for serving. - 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. - - Returns: - bool: ``True`` if the Axon serve registration is successful, False otherwise. - - By registering an Axon, the neuron becomes an active part of the network's distributed - computing infrastructure, contributing to the collective intelligence of Bittensor. - """ - return serve_axon_extrinsic( - self, netuid, axon, wait_for_inclusion, wait_for_finalization - ) - - @networking.ensure_connected - def _do_serve_axon( - self, - wallet: "bittensor.wallet", - call_params: AxonServeCallParams, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[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. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron. - call_params (AxonServeCallParams): Parameters required for the serve axon call. - 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. - - Returns: - Tuple[bool, Optional[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. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="serve_axon", - call_params=call_params, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - else: - return True, None - - return make_substrate_call_with_retry() - - def serve_prometheus( - self, - wallet: "bittensor.wallet", - port: int, - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> bool: - return prometheus_extrinsic( - self, - wallet=wallet, - port=port, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - @networking.ensure_connected - def _do_serve_prometheus( - self, - wallet: "bittensor.wallet", - call_params: PrometheusServeCallParams, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """ - Sends a serve prometheus extrinsic to the chain. - Args: - wallet (:func:`bittensor.wallet`): Wallet object. - call_params (:func:`PrometheusServeCallParams`): Prometheus serve call parameters. - wait_for_inclusion (bool): If ``true``, waits for inclusion. - wait_for_finalization (bool): If ``true``, waits for finalization. - Returns: - success (bool): ``True`` if serve prometheus was successful. - error (:func:`Optional[str]`): Error message if serve prometheus failed, ``None`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="serve_prometheus", - call_params=call_params, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - return True, None - else: - return False, format_error_message(response.error_message) - else: - return True, None - - return make_substrate_call_with_retry() - - @networking.ensure_connected - def _do_associate_ips( - self, - wallet: "bittensor.wallet", - ip_info_list: List["IPInfo"], - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - """ - Sends an associate IPs extrinsic to the chain. - - Args: - wallet (:func:`bittensor.wallet`): Wallet object. - ip_info_list (:func:`List[IPInfo]`): List of IPInfo objects. - netuid (int): Netuid to associate IPs to. - wait_for_inclusion (bool): If ``true``, waits for inclusion. - wait_for_finalization (bool): If ``true``, waits for finalization. - - Returns: - success (bool): ``True`` if associate IPs was successful. - error (:func:`Optional[str]`): Error message if associate IPs failed, None otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="associate_ips", - call_params={ - "ip_info_list": [ip_info.encode() for ip_info in ip_info_list], - "netuid": netuid, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if wait_for_inclusion or wait_for_finalization: - response.process_events() - if response.is_success: - return True, None - else: - return False, response.error_message - else: - return True, None - - return make_substrate_call_with_retry() - - ########### - # Staking # - ########### - def add_stake( - self, - wallet: "bittensor.wallet", - hotkey_ss58: Optional[str] = None, - amount: Optional[Union["Balance", float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. Staking - is a fundamental process in the Bittensor network that enables neurons to participate actively - and earn incentives. - - Args: - wallet (bittensor.wallet): The wallet to be used for staking. - hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey associated with the neuron. - amount (Union[Balance, float]): The amount of TAO to stake. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the staking is successful, False otherwise. - - This function enables neurons to increase their stake in the network, enhancing their influence - and potential rewards in line with Bittensor's consensus and reward mechanisms. - """ - return add_stake_extrinsic( - subtensor=self, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - def add_stake_multiple( - self, - wallet: "bittensor.wallet", - hotkey_ss58s: List[str], - amounts: Optional[List[Union["Balance", float]]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Adds stakes to multiple neurons identified by their hotkey SS58 addresses. This bulk operation - allows for efficient staking across different neurons from a single wallet. - - Args: - wallet (bittensor.wallet): The wallet used for staking. - hotkey_ss58s (List[str]): List of ``SS58`` addresses of hotkeys to stake to. - amounts (List[Union[Balance, float]], optional): Corresponding amounts of TAO to stake for each hotkey. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the staking is successful for all specified neurons, False otherwise. - - This function is essential for managing stakes across multiple neurons, reflecting the dynamic - and collaborative nature of the Bittensor network. - """ - return add_stake_multiple_extrinsic( - self, - wallet, - hotkey_ss58s, - amounts, - wait_for_inclusion, - wait_for_finalization, - prompt, - ) - - @networking.ensure_connected - def _do_stake( - self, - wallet: "bittensor.wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """Sends a stake extrinsic to the chain. - - Args: - wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. - hotkey_ss58 (str): Hotkey ``ss58`` address to stake to. - amount (:func:`Balance`): Amount to stake. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. - Returns: - success (bool): ``True`` if the extrinsic was successful. - Raises: - StakeError: If the extrinsic failed. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={"hotkey": hotkey_ss58, "amount_staked": amount.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - response.process_events() - if response.is_success: - return True - else: - raise StakeError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() - - ############# - # Unstaking # - ############# - def unstake_multiple( - self, - wallet: "bittensor.wallet", - hotkey_ss58s: List[str], - amounts: Optional[List[Union["Balance", float]]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts - efficiently. This function is useful for managing the distribution of stakes across multiple neurons. - - Args: - wallet (bittensor.wallet): The wallet linked to the coldkey from which the stakes are being withdrawn. - hotkey_ss58s (List[str]): A list of hotkey ``SS58`` addresses to unstake from. - amounts (List[Union[Balance, float]], optional): The amounts of TAO to unstake from each hotkey. If not - provided, unstakes all available stakes. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the batch unstaking is successful, False otherwise. - - This function allows for strategic reallocation or withdrawal of stakes, aligning with the dynamic - stake management aspect of the Bittensor network. - """ - return unstake_multiple_extrinsic( - self, - wallet, - hotkey_ss58s, - amounts, - wait_for_inclusion, - wait_for_finalization, - prompt, - ) - - def unstake( - self, - wallet: "bittensor.wallet", - hotkey_ss58: Optional[str] = None, - amount: Optional[Union["Balance", float]] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting - individual neuron stakes within the Bittensor network. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron from which the stake is being removed. - hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. - amount (Union[Balance, float], optional): The amount of TAO to unstake. If not specified, unstakes all. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the unstaking process is successful, False otherwise. - - This function supports flexible stake management, allowing neurons to adjust their network participation - and potential reward accruals. - """ - return unstake_extrinsic( - self, - wallet, - hotkey_ss58, - amount, - wait_for_inclusion, - wait_for_finalization, - prompt, - ) - - @networking.ensure_connected - def _do_unstake( - self, - wallet: "bittensor.wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """Sends an unstake extrinsic to the chain. - - Args: - wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. - hotkey_ss58 (str): Hotkey ``ss58`` address to unstake from. - amount (:func:`Balance`): Amount to unstake. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. - Returns: - success (bool): ``True`` if the extrinsic was successful. - Raises: - StakeError: If the extrinsic failed. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={"hotkey": hotkey_ss58, "amount_unstaked": amount.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - response.process_events() - if response.is_success: - return True - else: - raise StakeError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() - - ################### - # Child hotkeys # - ################### - - def set_childkey_take( - self, - wallet: "bittensor.wallet", - hotkey: str, - take: float, - netuid: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> tuple[bool, str]: - """Sets a childkey take extrinsic on the subnet. - - Args: - wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. - hotkey: (str): Hotkey ``ss58`` address of the child for which take is getting set. - netuid (int): Unique identifier of for the subnet. - take (float): Value of childhotkey take on subnet. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - Returns: - success (bool): ``True`` if the extrinsic was successful. - Raises: - ChildHotkeyError: If the extrinsic failed. - """ - - return set_childkey_take_extrinsic( - self, - wallet=wallet, - hotkey=hotkey, - take=take, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - @networking.ensure_connected - def _do_set_childkey_take( - self, - wallet: "bittensor.wallet", - hotkey: str, - take: int, - netuid: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> tuple[bool, Optional[str]]: - """Sends a set_children hotkey extrinsic on the chain. - - Args: - wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. - hotkey: (str): Hotkey ``ss58`` address of the wallet for which take is getting set. - take: (int): The take that this ss58 hotkey will have if assigned as a child hotkey as u16 value. - netuid (int): Unique identifier for the network. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. - Returns: - success (bool): ``True`` if the extrinsic was successful. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_childkey_take", - call_params={ - "hotkey": hotkey, - "take": take, - "netuid": netuid, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - else: - return True, None - - return make_substrate_call_with_retry() - - def set_children( - self, - wallet: "bittensor.wallet", - hotkey: str, - children_with_proportions: List[Tuple[float, str]], - netuid: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> tuple[bool, str]: - """Sets a children hotkeys extrinsic on the subnet. - - Args: - wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. - hotkey: (str): Hotkey ``ss58`` address of the parent. - netuid (int): Unique identifier of for the subnet. - children_with_proportions (List[Tuple[float, str]]): List of (proportion, child_ss58) pairs. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - Returns: - success (bool): ``True`` if the extrinsic was successful. - Raises: - ChildHotkeyError: If the extrinsic failed. - """ - - return set_children_extrinsic( - self, - wallet=wallet, - hotkey=hotkey, - children_with_proportions=children_with_proportions, - netuid=netuid, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - @networking.ensure_connected - def _do_set_children( - self, - wallet: "bittensor.wallet", - hotkey: str, - children: List[Tuple[int, str]], - netuid: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> tuple[bool, Optional[str]]: - """Sends a set_children hotkey extrinsic on the chain. - - Args: - wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. - hotkey: (str): Hotkey ``ss58`` address of the parent. - children: (List[Tuple[int, str]]): A list of tuples containing the hotkey ``ss58`` addresses of the children and their proportions as u16 MAX standardized values. - netuid (int): Unique identifier for the network. - wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. - wait_for_finalization (bool): If ``true``, waits for finalization before returning. - Returns: - success (bool): ``True`` if the extrinsic was successful. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_children", - call_params={ - "hotkey": hotkey, - "children": children, - "netuid": netuid, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if not wait_for_finalization and not wait_for_inclusion: - return True, None - - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - else: - return True, None - - return make_substrate_call_with_retry() - - ################## - # Coldkey Swap # - ################## - - def check_in_arbitration(self, ss58_address: str) -> int: - """ - Checks storage function to see if the provided coldkey is in arbitration. - If 0, `swap` has not been called on this key. If 1, swap has been called once, so - the key is not in arbitration. If >1, `swap` has been called with multiple destinations, and - the key is thus in arbitration. - """ - return self.query_module( - "SubtensorModule", "ColdkeySwapDestinations", params=[ss58_address] - ).decode() - - def get_remaining_arbitration_period( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the remaining arbitration period for a given coldkey. - Args: - coldkey_ss58 (str): The SS58 address of the coldkey. - block (Optional[int], optional): The block number to query. If None, uses the latest block. - Returns: - Optional[int]: The remaining arbitration period in blocks, or 0 if not found. - """ - arbitration_block = self.query_subtensor( - name="ColdkeyArbitrationBlock", - block=block, - params=[coldkey_ss58], - ) - - if block is None: - block = self.block - - if arbitration_block.value > block: - return arbitration_block.value - block - else: - return 0 - - ########## - # Senate # - ########## - - def register_senate( - self, - wallet: "bittensor.wallet", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting - individual neuron stakes within the Bittensor network. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron from which the stake is being removed. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the unstaking process is successful, False otherwise. - - This function supports flexible stake management, allowing neurons to adjust their network participation - and potential reward accruals. - """ - return register_senate_extrinsic( - self, wallet, wait_for_inclusion, wait_for_finalization, prompt - ) - - def leave_senate( - self, - wallet: "bittensor.wallet", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting - individual neuron stakes within the Bittensor network. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron from which the stake is being removed. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the unstaking process is successful, False otherwise. - - This function supports flexible stake management, allowing neurons to adjust their network participation - and potential reward accruals. - """ - return leave_senate_extrinsic( - self, wallet, wait_for_inclusion, wait_for_finalization, prompt - ) - - def vote_senate( - self, - wallet: "bittensor.wallet", - proposal_hash: str, - proposal_idx: int, - vote: bool, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting - individual neuron stakes within the Bittensor network. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron from which the stake is being removed. - proposal_hash (str): The hash of the proposal being voted on. - proposal_idx (int): The index of the proposal being voted on. - vote (bool): The vote to be cast (True for yes, False for no). - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the unstaking process is successful, False otherwise. - - This function supports flexible stake management, allowing neurons to adjust their network participation - and potential reward accruals. - """ - return vote_senate_extrinsic( - self, - wallet, - proposal_hash, - proposal_idx, - vote, - wait_for_inclusion, - wait_for_finalization, - prompt, - ) - - def is_senate_member(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - """ - Checks if a given neuron (identified by its hotkey SS58 address) is a member of the Bittensor senate. - The senate is a key governance body within the Bittensor network, responsible for overseeing and - approving various network operations and proposals. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number at which to check senate membership. - - Returns: - bool: ``True`` if the neuron is a senate member at the given block, False otherwise. - - This function is crucial for understanding the governance dynamics of the Bittensor network and for - identifying the neurons that hold decision-making power within the network. - """ - senate_members = self.query_module( - module="SenateMembers", name="Members", block=block - ) - if not hasattr(senate_members, "serialize"): - return False - senate_members_serialized = senate_members.serialize() - - if not hasattr(senate_members_serialized, "count"): - return False - - return senate_members_serialized.count(hotkey_ss58) > 0 - - def get_vote_data( - self, proposal_hash: str, block: Optional[int] = None - ) -> Optional[ProposalVoteData]: - """ - Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes - information about how senate members have voted on the proposal. - - Args: - proposal_hash (str): The hash of the proposal for which voting data is requested. - block (Optional[int]): The blockchain block number to query the voting data. - - Returns: - Optional[ProposalVoteData]: An object containing the proposal's voting data, or ``None`` if not found. - - This function is important for tracking and understanding the decision-making processes within - the Bittensor network, particularly how proposals are received and acted upon by the governing body. - """ - vote_data = self.query_module( - module="Triumvirate", name="Voting", block=block, params=[proposal_hash] - ) - if not hasattr(vote_data, "serialize"): - return None - return vote_data.serialize() if vote_data is not None else None - - get_proposal_vote_data = get_vote_data - - def get_senate_members(self, block: Optional[int] = None) -> Optional[List[str]]: - """ - Retrieves the list of current senate members from the Bittensor blockchain. Senate members are - responsible for governance and decision-making within the network. - - Args: - block (Optional[int]): The blockchain block number at which to retrieve the senate members. - - Returns: - Optional[List[str]]: A list of ``SS58`` addresses of current senate members, or ``None`` if not available. - - Understanding the composition of the senate is key to grasping the governance structure and - decision-making authority within the Bittensor network. - """ - senate_members = self.query_module("SenateMembers", "Members", block=block) - if not hasattr(senate_members, "serialize"): - return None - return senate_members.serialize() if senate_members is not None else None - - def get_proposal_call_data( - self, proposal_hash: str, block: Optional[int] = None - ) -> Optional["GenericCall"]: - """ - Retrieves the call data of a specific proposal on the Bittensor blockchain. This data provides - detailed information about the proposal, including its purpose and specifications. - - Args: - proposal_hash (str): The hash of the proposal. - block (Optional[int]): The blockchain block number at which to query the proposal call data. - - Returns: - Optional[GenericCall]: An object containing the proposal's call data, or ``None`` if not found. - - This function is crucial for analyzing the types of proposals made within the network and the - specific changes or actions they intend to implement or address. - """ - proposal_data = self.query_module( - module="Triumvirate", name="ProposalOf", block=block, params=[proposal_hash] - ) - if not hasattr(proposal_data, "serialize"): - return None - - return proposal_data.serialize() if proposal_data is not None else None - - def get_proposal_hashes(self, block: Optional[int] = None) -> Optional[List[str]]: - """ - Retrieves the list of proposal hashes currently present on the Bittensor blockchain. Each hash - uniquely identifies a proposal made within the network. - - Args: - block (Optional[int]): The blockchain block number to query the proposal hashes. - - Returns: - Optional[List[str]]: A list of proposal hashes, or ``None`` if not available. - - This function enables tracking and reviewing the proposals made in the network, offering insights - into the active governance and decision-making processes. - """ - proposal_hashes = self.query_module( - module="Triumvirate", name="Proposals", block=block - ) - if not hasattr(proposal_hashes, "serialize"): - return None - - return proposal_hashes.serialize() if proposal_hashes is not None else None - - def get_proposals( - self, block: Optional[int] = None - ) -> Optional[Dict[str, Tuple["GenericCall", "ProposalVoteData"]]]: - """ - Retrieves all active proposals on the Bittensor blockchain, along with their call and voting data. - This comprehensive view allows for a thorough understanding of the proposals and their reception - by the senate. - - Args: - block (Optional[int]): The blockchain block number to query the proposals. - - Returns: - Optional[Dict[str, Tuple[bittensor.ProposalCallData, bittensor.ProposalVoteData]]]: A dictionary mapping - proposal hashes to their corresponding call and vote data, or ``None`` if not available. - - This function is integral for analyzing the governance activity on the Bittensor network, - providing a holistic view of the proposals and their impact or potential changes within the network. - """ - proposal_hashes: Optional[List[str]] = self.get_proposal_hashes(block=block) - if proposal_hashes is None: - return None - return { - proposal_hash: ( # type: ignore - self.get_proposal_call_data(proposal_hash, block=block), - self.get_proposal_vote_data(proposal_hash, block=block), - ) - for proposal_hash in proposal_hashes - } - - ######## - # Root # - ######## - - def root_register( - self, - wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - ) -> bool: - """ - Registers the neuron associated with the wallet on the root network. This process is integral for - participating in the highest layer of decision-making and governance within the Bittensor network. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron to be registered on the root 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the registration on the root network is successful, False otherwise. - - This function enables neurons to engage in the most critical and influential aspects of the network's - governance, signifying a high level of commitment and responsibility in the Bittensor ecosystem. - """ - return root_register_extrinsic( - subtensor=self, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - @networking.ensure_connected - def _do_root_register( - self, - wallet: "bittensor.wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - # create extrinsic call - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="root_register", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - # process if registration successful, try again if pow is still valid - response.process_events() - if not response.is_success: - return False, format_error_message(response.error_message) - # Successful registration - else: - return True, None - - return make_substrate_call_with_retry() - - @legacy_torch_api_compat - def root_set_weights( - self, - wallet: "bittensor.wallet", - netuids: Union[NDArray[np.int64], "torch.LongTensor", list], - weights: Union[NDArray[np.float32], "torch.FloatTensor", list], - version_key: int = 0, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - prompt: bool = False, - ) -> bool: - """ - Sets the weights for neurons on the root network. This action is crucial for defining the influence - and interactions of neurons at the root level of the Bittensor network. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron setting the weights. - netuids (Union[NDArray[np.int64], torch.LongTensor, list]): The list of neuron UIDs for which weights are - being set. - weights (Union[NDArray[np.float32], torch.FloatTensor, list]): The corresponding weights to be set for each - UID. - 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. - prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. - - Returns: - bool: ``True`` if the setting of root-level weights is successful, False otherwise. - - This function plays a pivotal role in shaping the root network's collective intelligence and decision-making - processes, reflecting the principles of decentralized governance and collaborative learning in Bittensor. - """ - return set_root_weights_extrinsic( - subtensor=self, - wallet=wallet, - netuids=netuids, - weights=weights, - version_key=version_key, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - @networking.ensure_connected - def _do_set_root_weights( - self, - wallet: "bittensor.wallet", - uids: List[int], - vals: List[int], - netuid: int = 0, - version_key: int = bittensor.__version_as_int__, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[str]]: # (success, error_message) - """ - Internal method to send a transaction to the Bittensor blockchain, setting weights - for specified neurons on root. This method constructs and submits the transaction, handling - retries and blockchain communication. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron setting the weights. - uids (List[int]): List of neuron UIDs for which weights are being set. - vals (List[int]): List of weight values corresponding to each UID. - netuid (int): Unique identifier for the network. - 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. - - Returns: - Tuple[bool, Optional[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 to the root network. - """ - - @retry(delay=2, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_root_weights", - call_params={ - "dests": uids, - "weights": vals, - "netuid": netuid, - "version_key": version_key, - "hotkey": wallet.hotkey.ss58_address, - }, - ) - # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = self.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.coldkey, - era={"period": 5}, - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # 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." - - response.process_events() - if response.is_success: - return True, "Successfully set weights." - else: - return False, response.error_message - - return make_substrate_call_with_retry() - - ################## - # Registry Calls # - ################## - - # Queries subtensor registry named storage with params and block. - @networking.ensure_connected - def query_identity( - self, - key: str, - block: Optional[int] = None, - ) -> dict: - """ - Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves - detailed identity information about a specific neuron, which is a crucial aspect of the network's decentralized - identity and governance system. - - NOTE: - See the `Bittensor CLI documentation `_ for supported identity - parameters. - - Args: - key (str): The key used to query the neuron's identity, typically the neuron's ``SS58`` address. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - result (dict): An object containing the identity information of the neuron if found, ``None`` otherwise. - - The identity information can include various attributes such as the neuron's stake, rank, and other - network-specific details, providing insights into the neuron's role and status within the Bittensor network. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry() -> "ScaleType": - return self.substrate.query( - module="Registry", - storage_function="IdentityOf", - params=[key], - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - identity_info = make_substrate_call_with_retry() - - return bittensor.utils.wallet_utils.decode_hex_identity_dict( - identity_info.value["info"] - ) - - @networking.ensure_connected - def update_identity( - self, - wallet: "bittensor.wallet", - identified: Optional[str] = None, - params: Optional[dict] = None, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """ - Updates the identity of a neuron on the Bittensor blockchain. This function allows neurons to modify their - identity attributes, reflecting changes in their roles, stakes, or other network-specific parameters. - - NOTE: - See the `Bittensor CLI documentation `_ for supported identity - parameters. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron whose identity is being updated. - identified (str, optional): The identified ``SS58`` address of the neuron. Defaults to the wallet's coldkey - address. - params (dict, optional): A dictionary of parameters to update in the neuron's identity. - 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. - - Returns: - bool: ``True`` if the identity update is successful, False otherwise. - - This function plays a vital role in maintaining the accuracy and currency of neuron identities in the - Bittensor network, ensuring that the network's governance and consensus mechanisms operate effectively. - """ - if identified is None: - identified = wallet.coldkey.ss58_address - - params = {} if params is None else params - - call_params = bittensor.utils.wallet_utils.create_identity_dict(**params) - call_params["identified"] = identified - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry() -> bool: - call = self.substrate.compose_call( - call_module="Registry", - call_function="set_identity", - call_params=call_params, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise IdentityError(response.error_message) - - return make_substrate_call_with_retry() - - def get_delegate_identities( - self, block: Optional[int] = None - ) -> Dict[str, DelegatesDetails]: - """ - Retrieves the identities of all delegates from the blockchain. - - This method makes a substrate call to retrieve the identities of all delegates and retries the call up to three times with exponential backoff in case of failures. - - Args: - block (Optional[int], optional): The block number to retrieve the delegate identities from. If ``None``, the latest block is used. Default is ``None``. - - Returns: - Dict[str, DelegatesDetails]: A dictionary where the keys are delegate SS58 addresses and the values are DelegatesDetails objects containing the details of each delegate. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry() -> "QueryMapResult": - return self.substrate.query_map( - module="Registry", - storage_function="IdentityOf", - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - identities_info = make_substrate_call_with_retry() - - result = { - ss58_address.value: DelegatesDetails.from_chain_data( - bittensor.utils.wallet_utils.decode_hex_identity_dict( - identity.value["info"] - ) - ) - for ss58_address, identity in identities_info - } - return result - - # Make some commitment on-chain about arbitrary data. - def commit(self, wallet, netuid: int, data: str): - """ - Commits arbitrary data to the Bittensor network by publishing metadata. - - Args: - wallet (bittensor.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. - """ - publish_metadata(self, wallet, netuid, f"Raw{len(data)}", data.encode()) - - def get_commitment(self, netuid: int, uid: int, block: Optional[int] = None) -> str: - """ - Retrieves the on-chain commitment for a specific neuron in the Bittensor network. - - Args: - netuid (int): The unique identifier of the subnetwork. - uid (int): The unique identifier of the neuron. - block (Optional[int]): The block number to retrieve the commitment from. If None, the latest block - is used. Default is ``None``. - - Returns: - str: The commitment data as a string. - """ - metagraph = self.metagraph(netuid) - hotkey = metagraph.hotkeys[uid] # type: ignore - - metadata = get_metadata(self, netuid, hotkey, block) - commitment = metadata["info"]["fields"][0] # type: ignore - hex_data = commitment[list(commitment.keys())[0]][2:] # type: ignore - - return bytes.fromhex(hex_data).decode() - - ################## - # Standard Calls # - ################## - - # Queries subtensor named storage with params and block. - @networking.ensure_connected - def query_subtensor( - self, - name: str, - block: Optional[int] = None, - params: Optional[list] = None, - ) -> "ScaleType": - """ - Queries named storage from the Subtensor module on the Bittensor blockchain. This function is used to retrieve - specific data or parameters from the blockchain, such as stake, rank, or other neuron-specific attributes. - - Args: - name (str): The name of the storage function to query. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): A list of parameters to pass to the query function. - - Returns: - query_response (ScaleType): An object containing the requested data. - - This query function is essential for accessing detailed information about the network and its neurons, - providing valuable insights into the state and dynamics of the Bittensor ecosystem. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry() -> "ScaleType": - return self.substrate.query( - module="SubtensorModule", - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - return make_substrate_call_with_retry() - - # Queries subtensor map storage with params and block. - @networking.ensure_connected - def query_map_subtensor( - self, - name: str, - block: Optional[int] = None, - params: Optional[list] = None, - ) -> "QueryMapResult": - """ - Queries map storage from the Subtensor module on the Bittensor blockchain. This function is designed to - retrieve a map-like data structure, which can include various neuron-specific details or network-wide - attributes. - - Args: - name (str): The name of the map storage function to query. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): A list of parameters to pass to the query function. - - Returns: - QueryMapResult: An object containing the map-like data structure, or ``None`` if not found. - - This function is particularly useful for analyzing and understanding complex network structures and - relationships within the Bittensor ecosystem, such as inter-neuronal connections and stake distributions. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - return self.substrate.query_map( - module="SubtensorModule", - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - return make_substrate_call_with_retry() - - @networking.ensure_connected - def query_constant( - self, module_name: str, constant_name: str, block: Optional[int] = None - ) -> Optional["ScaleType"]: - """ - Retrieves a constant from the specified module on the Bittensor blockchain. This function is used to - access fixed parameters or values defined within the blockchain's modules, which are essential for - understanding the network's configuration and rules. - - Args: - module_name (str): The name of the module containing the constant. - constant_name (str): The name of the constant to retrieve. - block (Optional[int]): The blockchain block number at which to query the constant. - - Returns: - Optional[ScaleType]: The value of the constant if found, ``None`` otherwise. - - Constants queried through this function can include critical network parameters such as inflation rates, - consensus rules, or validation thresholds, providing a deeper understanding of the Bittensor network's - operational parameters. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - return self.substrate.get_constant( - module_name=module_name, - constant_name=constant_name, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - return make_substrate_call_with_retry() - - # Queries any module storage with params and block. - @networking.ensure_connected - def query_module( - self, - module: str, - name: str, - block: Optional[int] = None, - params: Optional[list] = None, - ) -> "ScaleType": - """ - Queries any module storage on the Bittensor blockchain with the specified parameters and block number. - This function is a generic query interface that allows for flexible and diverse data retrieval from - various blockchain modules. - - Args: - module (str): The name of the module from which to query data. - name (str): The name of the storage function within the module. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): A list of parameters to pass to the query function. - - Returns: - Optional[ScaleType]: An object containing the requested data if found, ``None`` otherwise. - - This versatile query function is key to accessing a wide range of data and insights from different - parts of the Bittensor blockchain, enhancing the understanding and analysis of the network's state and dynamics. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry() -> "ScaleType": - return self.substrate.query( - module=module, - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - return make_substrate_call_with_retry() - - # Queries any module map storage with params and block. - @networking.ensure_connected - def query_map( - self, - module: str, - name: str, - block: Optional[int] = None, - params: Optional[list] = None, - ) -> QueryMapResult: - """ - Queries map storage from any module on the Bittensor blockchain. This function retrieves data structures - that represent key-value mappings, essential for accessing complex and structured data within the blockchain - modules. - - Args: - module (str): The name of the module from which to query the map storage. - name (str): The specific storage function within the module to query. - block (Optional[int]): The blockchain block number at which to perform the query. - params (Optional[List[object]], optional): Parameters to be passed to the query. - - Returns: - result (QueryMapResult): A data structure representing the map storage if found, ``None`` otherwise. - - This function is particularly useful for retrieving detailed and structured data from various blockchain - modules, offering insights into the network's state and the relationships between its different components. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry() -> "QueryMapResult": - return self.substrate.query_map( - module=module, - storage_function=name, - params=params, - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - return make_substrate_call_with_retry() - - @networking.ensure_connected - def state_call( - self, - method: str, - data: str, - block: Optional[int] = None, - ) -> Dict[Any, Any]: - """ - Makes a state call to the Bittensor blockchain, allowing for direct queries of the blockchain's state. - This function is typically used for advanced queries that require specific method calls and data inputs. - - Args: - method (str): The method name for the state call. - data (str): The data to be passed to the method. - block (Optional[int]): The blockchain block number at which to perform the state call. - - Returns: - result (Dict[Any, Any]): The result of the rpc call. - - The state call function provides a more direct and flexible way of querying blockchain data, - useful for specific use cases where standard queries are insufficient. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry() -> Dict[Any, Any]: - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="state_call", - params=[method, data, block_hash] if block_hash else [method, data], - ) - - return make_substrate_call_with_retry() - - def query_runtime_api( - self, - runtime_api: str, - method: str, - params: Optional[Union[List[int], Dict[str, int]]], - block: Optional[int] = None, - ) -> Optional[str]: - """ - Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying - runtime and retrieve data encoded in Scale Bytes format. This function is essential for advanced users - who need to interact with specific runtime methods and decode complex data types. - - Args: - runtime_api (str): The name of the runtime API to query. - method (str): The specific method within the runtime API to call. - params (Optional[List[ParamWithTypes]], optional): The parameters to pass to the method call. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - Optional[bytes]: The Scale Bytes encoded result from the runtime API call, or ``None`` if the call fails. - - This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed - and specific interactions with the network's runtime environment. - """ - call_definition = bittensor.__type_registry__["runtime_api"][runtime_api][ # type: ignore - "methods" # type: ignore - ][method] # type: ignore - - json_result = self.state_call( - method=f"{runtime_api}_{method}", - data=( - "0x" - if params is None - else self._encode_params(call_definition=call_definition, params=params) - ), - block=block, - ) - - if json_result is None: - return None - - return_type = call_definition["type"] - - as_scale_bytes = scalecodec.ScaleBytes(json_result["result"]) # type: ignore - - rpc_runtime_config = RuntimeConfiguration() - rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) - rpc_runtime_config.update_type_registry(custom_rpc_type_registry) - - obj = rpc_runtime_config.create_scale_object(return_type, as_scale_bytes) - if obj.data.to_hex() == "0x0400": # RPC returned None result - return None - - return obj.decode() - - @networking.ensure_connected - def _encode_params( - self, - call_definition: List["ParamWithTypes"], - params: Union[List[Any], Dict[str, Any]], - ) -> str: - """Returns a hex encoded string of the params using their types.""" - param_data = scalecodec.ScaleBytes(b"") - - for i, param in enumerate(call_definition["params"]): # type: ignore - scale_obj = self.substrate.create_scale_object(param["type"]) - if type(params) is list: - param_data += scale_obj.encode(params[i]) - else: - if param["name"] not in params: - raise ValueError(f"Missing param {param['name']} in params dict.") - - param_data += scale_obj.encode(params[param["name"]]) - - return param_data.to_hex() - - ########################## - # Hyper parameter calls. # - ########################## - - def _get_hyperparameter( - self, param_name: str, netuid: int, block: Optional[int] = None - ) -> Optional[Any]: - """ - Retrieves a specified hyperparameter for a specific subnet. - - Args: - param_name (str): The name of the hyperparameter to retrieve. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[Union[int, float]]: The value of the specified hyperparameter if the subnet exists, ``None`` - otherwise. - """ - if not self.subnet_exists(netuid, block): - return None - - result = self.query_subtensor(param_name, block, [netuid]) - if result is None or not hasattr(result, "value"): - return None - - return result.value - - def rho(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the 'Rho' hyperparameter for a specified subnet within the Bittensor network. 'Rho' represents the - global inflation rate, which directly influences the network's token emission rate and economic model. - - Note: - This is currently fixed such that the Bittensor blockchain emmits 7200 Tao per day. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to query the parameter. - - Returns: - Optional[int]: The value of the 'Rho' hyperparameter if the subnet exists, ``None`` otherwise. - - Mathematical Context: - Rho (p) is calculated based on the network's target inflation and actual neuron staking. - It adjusts the emission rate of the TAO token to balance the network's economy and dynamics. - The formula for Rho is defined as: p = (Staking_Target / Staking_Actual) * Inflation_Target. - Here, Staking_Target and Staking_Actual represent the desired and actual total stakes in the network, - while Inflation_Target is the predefined inflation rate goal. - - 'Rho' is essential for understanding the network's economic dynamics, affecting the reward distribution - and incentive structures across the network's neurons. - """ - call = self._get_hyperparameter(param_name="Rho", netuid=netuid, block=block) - return None if call is None else int(call) - - def kappa(self, netuid: int, block: Optional[int] = None) -> Optional[float]: - """ - Retrieves the 'Kappa' hyperparameter for a specified subnet. 'Kappa' is a critical parameter in - the Bittensor network that controls the distribution of stake weights among neurons, impacting their - rankings and incentive allocations. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[float]: The value of the 'Kappa' hyperparameter if the subnet exists, None otherwise. - - Mathematical Context: - Kappa (κ) is used in the calculation of neuron ranks, which determine their share of network incentives. - It is derived from the softmax function applied to the inter-neuronal weights set by each neuron. - The formula for Kappa is: κ_i = exp(w_i) / Σ(exp(w_j)), where w_i represents the weight set by neuron i, - and the denominator is the sum of exponential weights set by all neurons. - This mechanism ensures a normalized and probabilistic distribution of ranks based on relative weights. - - Understanding 'Kappa' is crucial for analyzing stake dynamics and the consensus mechanism within the network, - as it plays a significant role in neuron ranking and incentive allocation processes. - """ - call = self._get_hyperparameter(param_name="Kappa", netuid=netuid, block=block) - return None if call is None else U16_NORMALIZED_FLOAT(int(call)) - - def difficulty(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the 'Difficulty' hyperparameter for a specified subnet in the Bittensor network. - This parameter is instrumental in determining the computational challenge required for neurons - to participate in consensus and validation processes. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[int]: The value of the 'Difficulty' hyperparameter if the subnet exists, ``None`` otherwise. - - The 'Difficulty' parameter directly impacts the network's security and integrity by setting the - computational effort required for validating transactions and participating in the network's consensus - mechanism. - """ - call = self._get_hyperparameter( - param_name="Difficulty", netuid=netuid, block=block - ) - if call is None: - return None - return int(call) - - def recycle(self, netuid: int, block: Optional[int] = None) -> Optional["Balance"]: - """ - Retrieves the 'Burn' hyperparameter for a specified subnet. The 'Burn' parameter represents the - amount of Tao that is effectively recycled within the Bittensor network. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[Balance]: The value of the 'Burn' hyperparameter if the subnet exists, None otherwise. - - Understanding the 'Burn' rate is essential for analyzing the network registration usage, particularly - how it is correlated with user activity and the overall cost of participation in a given subnet. - """ - call = self._get_hyperparameter(param_name="Burn", netuid=netuid, block=block) - return None if call is None else Balance.from_rao(int(call)) - - # Returns network ImmunityPeriod hyper parameter. - def immunity_period( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the 'ImmunityPeriod' hyperparameter for a specific subnet. This parameter defines the - duration during which new neurons are protected from certain network penalties or restrictions. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[int]: The value of the 'ImmunityPeriod' hyperparameter if the subnet exists, ``None`` otherwise. - - The 'ImmunityPeriod' is a critical aspect of the network's governance system, ensuring that new - participants have a grace period to establish themselves and contribute to the network without facing - immediate punitive actions. - """ - call = self._get_hyperparameter( - param_name="ImmunityPeriod", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_batch_size( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorBatchSize hyper parameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int]): The block number to retrieve the parameter from. If None, the latest block - is used. Default is ``None``. - - Returns: - Optional[int]: The value of the ValidatorBatchSize hyperparameter, or None if the subnetwork does not exist - or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorBatchSize", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_prune_len( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorPruneLen hyper parameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int]): The block number to retrieve the parameter from. If None, the latest block - is used. Default is ``None``. - - Returns: - Optional[int]: The value of the ValidatorPruneLen hyperparameter, or None if the subnetwork does not exist - or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorPruneLen", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_logits_divergence( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network ValidatorLogitsDivergence hyper parameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int]): The block number to retrieve the parameter from. If None, the latest block - is used. Default is ``None``. - - Returns: - Optional[float]: The value of the ValidatorLogitsDivergence hyperparameter, or None if the subnetwork does - not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorLogitsDivergence", netuid=netuid, block=block - ) - return None if call is None else U16_NORMALIZED_FLOAT(int(call)) - - def validator_sequence_length( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorSequenceLength hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the ValidatorSequenceLength hyperparameter, or ``None`` if the subnetwork does - not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorSequenceLength", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_epochs_per_reset( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorEpochsPerReset hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the ValidatorEpochsPerReset hyperparameter, or ``None`` if the subnetwork does - not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorEpochsPerReset", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_epoch_length( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorEpochLen hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the ValidatorEpochLen hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorEpochLen", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def validator_exclude_quantile( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network ValidatorExcludeQuantile hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the ValidatorExcludeQuantile hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="ValidatorExcludeQuantile", netuid=netuid, block=block - ) - return None if call is None else U16_NORMALIZED_FLOAT(int(call)) - - def max_allowed_validators( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network ValidatorExcludeQuantile hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the ValidatorExcludeQuantile hyperparameter, or ``None`` if the subnetwork - does not exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="MaxAllowedValidators", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def min_allowed_weights( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network MinAllowedWeights hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the MinAllowedWeights hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="MinAllowedWeights", block=block, netuid=netuid - ) - return None if call is None else int(call) - - def max_weight_limit( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network MaxWeightsLimit hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the MaxWeightsLimit hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="MaxWeightsLimit", block=block, netuid=netuid - ) - return None if call is None else U16_NORMALIZED_FLOAT(int(call)) - - def adjustment_alpha( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network AdjustmentAlpha hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the AdjustmentAlpha hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="AdjustmentAlpha", block=block, netuid=netuid - ) - return None if call is None else U64_NORMALIZED_FLOAT(int(call)) - - def bonds_moving_avg( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network BondsMovingAverage hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the BondsMovingAverage hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="BondsMovingAverage", netuid=netuid, block=block - ) - return None if call is None else U64_NORMALIZED_FLOAT(int(call)) - - def scaling_law_power( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """Returns network ScalingLawPower hyper parameter""" - call = self._get_hyperparameter( - param_name="ScalingLawPower", netuid=netuid, block=block - ) - return None if call is None else int(call) / 100.0 - - def synergy_scaling_law_power( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Returns network ScalingLawPower hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[float]: The value of the ScalingLawPower hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="SynergyScalingLawPower", netuid=netuid, block=block - ) - return None if call is None else int(call) / 100.0 - - def subnetwork_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Returns network SubnetworkN hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the SubnetworkN hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="SubnetworkN", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def max_n(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Returns network MaxAllowedUids hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the MaxAllowedUids hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="MaxAllowedUids", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def blocks_since_epoch( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns network BlocksSinceEpoch hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the BlocksSinceEpoch hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter( - param_name="BlocksSinceEpoch", netuid=netuid, block=block - ) - return None if call is None else int(call) - - 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. - - Args: - netuid (int): The unique identifier of the subnetwork. - uid (int): The unique identifier of the neuron. - - Returns: - Optional[int]: The number of blocks since the last update, or ``None`` if the subnetwork or UID does not - exist. - """ - call = self._get_hyperparameter(param_name="LastUpdate", netuid=netuid) - return None if call is None else self.get_current_block() - int(call[uid]) - - def weights_rate_limit(self, netuid: int) -> Optional[int]: - """ - Returns network WeightsSetRateLimit hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - - Returns: - Optional[int]: The value of the WeightsSetRateLimit hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter(param_name="WeightsSetRateLimit", netuid=netuid) - return None if call is None else int(call) - - def tempo(self, netuid: int, block: Optional[int] = None) -> Optional[int]: - """ - Returns network Tempo hyperparameter. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the Tempo hyperparameter, or ``None`` if the subnetwork does not - exist or the parameter is not found. - """ - call = self._get_hyperparameter(param_name="Tempo", netuid=netuid, block=block) - return None if call is None else int(call) - - ##################### - # Account functions # - ##################### - - def get_total_stake_for_hotkey( - self, ss58_address: str, block: Optional[int] = None - ) -> Optional["Balance"]: - """ - Returns the total stake held on a hotkey including delegative. - - Args: - ss58_address (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to retrieve the stake from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[Balance]: The total stake held on the hotkey, or ``None`` if the hotkey does not - exist or the stake is not found. - """ - _result = self.query_subtensor("TotalHotkeyStake", block, [ss58_address]) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def get_total_stake_for_coldkey( - self, ss58_address: str, block: Optional[int] = None - ) -> Optional["Balance"]: - """ - Returns the total stake held on a coldkey. - - Args: - ss58_address (str): The SS58 address of the coldkey. - block (Optional[int], optional): The block number to retrieve the stake from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[Balance]: The total stake held on the coldkey, or ``None`` if the coldkey does not - exist or the stake is not found. - """ - _result = self.query_subtensor("TotalColdkeyStake", block, [ss58_address]) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def get_stake_for_coldkey_and_hotkey( - self, hotkey_ss58: str, coldkey_ss58: str, block: Optional[int] = None - ) -> Optional["Balance"]: - """ - Returns the stake under a coldkey - hotkey pairing. - - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - coldkey_ss58 (str): The SS58 address of the coldkey. - block (Optional[int], optional): The block number to retrieve the stake from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[Balance]: The stake under the coldkey - hotkey pairing, or ``None`` if the pairing does not - exist or the stake is not found. - """ - _result = self.query_subtensor("Stake", block, [hotkey_ss58, coldkey_ss58]) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def get_stake( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> List[Tuple[str, "Balance"]]: - """ - Returns a list of stake tuples (coldkey, balance) for each delegating coldkey including the owner. - - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to retrieve the stakes from. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - List[Tuple[str, Balance]]: A list of tuples, each containing a coldkey SS58 address and the corresponding - balance staked by that coldkey. - """ - return [ - (r[0].value, Balance.from_rao(r[1].value)) - for r in self.query_map_subtensor("Stake", block, [hotkey_ss58]) - ] - - def does_hotkey_exist(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - """ - Returns true if the hotkey is known by the chain and there are accounts. - - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to check the hotkey against. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - bool: ``True`` if the hotkey is known by the chain and there are accounts, ``False`` otherwise. - """ - _result = self.query_subtensor("Owner", block, [hotkey_ss58]) - return ( - False - if getattr(_result, "value", None) is None - else _result.value != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ) - - def get_hotkey_owner( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[str]: - """ - Returns the coldkey owner of the passed hotkey. - - Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to check the hotkey owner against. If ``None``, the latest - block is used. Default is ``None``. - - Returns: - Optional[str]: The SS58 address of the coldkey owner, or ``None`` if the hotkey does not exist or the owner - is not found. - """ - _result = self.query_subtensor("Owner", block, [hotkey_ss58]) - return ( - None - if getattr(_result, "value", None) is None - or not self.does_hotkey_exist(hotkey_ss58, block) - else _result.value - ) - - # TODO: check if someone still use this method. bittensor not. - def get_axon_info( - self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[AxonInfo]: - """ - Returns the axon information for this hotkey account. - - Args: - netuid (int): The unique identifier of the subnetwork. - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to retrieve the axon information from. If ``None``, the - latest block is used. Default is ``None``. - - Returns: - Optional[AxonInfo]: An AxonInfo object containing the axon information, or ``None`` if the axon information - is not found. - """ - result = self.query_subtensor("Axons", block, [netuid, hotkey_ss58]) - if result is not None and hasattr(result, "value"): - return AxonInfo( - ip=networking.int_to_ip(result.value["ip"]), - ip_type=result.value["ip_type"], - port=result.value["port"], - protocol=result.value["protocol"], - version=result.value["version"], - placeholder1=result.value["placeholder1"], - placeholder2=result.value["placeholder2"], - hotkey=hotkey_ss58, - coldkey="", - ) - return None - - # It is used in subtensor in neuron_info, and serving - def get_prometheus_info( - self, netuid: int, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[PrometheusInfo]: - """ - Returns the prometheus information for this hotkey account. - - Args: - netuid (int): The unique identifier of the subnetwork. - hotkey_ss58 (str): The SS58 address of the hotkey. - block (Optional[int], optional): The block number to retrieve the prometheus information from. If ``None``, - the latest block is used. Default is ``None``. - - Returns: - Optional[PrometheusInfo]: A PrometheusInfo object containing the prometheus information, or ``None`` if the - prometheus information is not found. - """ - result = self.query_subtensor("Prometheus", block, [netuid, hotkey_ss58]) - if result is not None and hasattr(result, "value"): - return PrometheusInfo( - ip=networking.int_to_ip(result.value["ip"]), - ip_type=result.value["ip_type"], - port=result.value["port"], - version=result.value["version"], - block=result.value["block"], - ) - return None - - ##################### - # Global Parameters # - ##################### - - @property - def block(self) -> int: - r"""Returns current chain block. - Returns: - block (int): - Current chain block. - """ - return self.get_current_block() - - def total_issuance(self, block: Optional[int] = None) -> Optional[Balance]: - """ - Retrieves the total issuance of the Bittensor network's native token (Tao) as of a specific - blockchain block. This represents the total amount of currency that has been issued or mined on the network. - - Args: - block (Optional[int], optional): The blockchain block number at which to perform the query. - - Returns: - Balance: The total issuance of TAO, represented as a Balance object. - - The total issuance is a key economic indicator in the Bittensor network, reflecting the overall supply - of the currency and providing insights into the network's economic health and inflationary trends. - """ - _result = self.query_subtensor("TotalIssuance", block) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def total_stake(self, block: Optional[int] = None) -> Optional[Balance]: - """ - Retrieves the total amount of TAO staked on the Bittensor network as of a specific blockchain block. - This represents the cumulative stake across all neurons in the network, indicating the overall level - of participation and investment by the network's participants. - - Args: - block (Optional[int], optional): The blockchain block number at which to perform the query. - - Returns: - Balance: The total amount of TAO staked on the network, represented as a Balance object. - - The total stake is an important metric for understanding the network's security, governance dynamics, - and the level of commitment by its participants. It is also a critical factor in the network's - consensus and incentive mechanisms. - """ - _result = self.query_subtensor("TotalStake", block) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def serving_rate_limit( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the serving rate limit for a specific subnet within the Bittensor network. - This rate limit determines how often you can change your node's IP address on the blockchain. Expressed in - number of blocks. Applies to both subnet validator and subnet miner nodes. Used when you move your node to a new - machine. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number at which to perform the query. - - Returns: - Optional[int]: The serving rate limit of the subnet if it exists, ``None`` otherwise. - - The serving rate limit is a crucial parameter for maintaining network efficiency and preventing - overuse of resources by individual neurons. It helps ensure a balanced distribution of service - requests across the network. - """ - call = self._get_hyperparameter( - param_name="ServingRateLimit", netuid=netuid, block=block - ) - return None if call is None else int(call) - - def tx_rate_limit(self, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the transaction rate limit for the Bittensor network as of a specific blockchain block. - This rate limit sets the maximum number of transactions that can be processed within a given time frame. - - Args: - block (Optional[int], optional): The blockchain block number at which to perform the query. - - Returns: - Optional[int]: The transaction rate limit of the network, None if not available. - - The transaction rate limit is an essential parameter for ensuring the stability and scalability - of the Bittensor network. It helps in managing network load and preventing congestion, thereby - maintaining efficient and timely transaction processing. - """ - _result = self.query_subtensor("TxRateLimit", block) - return getattr(_result, "value", None) - - ###################### - # Network Parameters # - ###################### - - def subnet_exists(self, netuid: int, block: Optional[int] = None) -> bool: - """ - Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number at which to check the subnet's existence. - - Returns: - bool: ``True`` if the subnet exists, False otherwise. - - This function is critical for verifying the presence of specific subnets in the network, - enabling a deeper understanding of the network's structure and composition. - """ - _result = self.query_subtensor("NetworksAdded", block, [netuid]) - return getattr(_result, "value", False) - - def get_all_subnet_netuids(self, block: Optional[int] = None) -> List[int]: - """ - Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. - - Args: - block (Optional[int], optional): The blockchain block number at which to retrieve the subnet netuids. - - Returns: - List[int]: A list of subnet netuids. - - This function provides a comprehensive view of the subnets within the Bittensor network, - offering insights into its diversity and scale. - """ - result = self.query_map_subtensor("NetworksAdded", block) - return ( - [] - if result is None or not hasattr(result, "records") - else [netuid.value for netuid, exists in result if exists] - ) - - def get_total_subnets(self, block: Optional[int] = None) -> Optional[int]: - """ - Retrieves the total number of subnets within the Bittensor network as of a specific blockchain block. - - Args: - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - int: The total number of subnets in the network. - - Understanding the total number of subnets is essential for assessing the network's growth and - the extent of its decentralized infrastructure. - """ - _result = self.query_subtensor("TotalNetworks", block) - return getattr(_result, "value", None) - - def get_subnet_modality( - self, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Returns the NetworkModality hyperparameter for a specific subnetwork. - - Args: - netuid (int): The unique identifier of the subnetwork. - block (Optional[int], optional): The block number to retrieve the parameter from. If ``None``, the latest block is used. Default is ``None``. - - Returns: - Optional[int]: The value of the NetworkModality hyperparameter, or ``None`` if the subnetwork does not exist or the parameter is not found. - """ - _result = self.query_subtensor("NetworkModality", block, [netuid]) - return getattr(_result, "value", None) - - def get_subnet_connection_requirement( - self, netuid_0: int, netuid_1: int, block: Optional[int] = None - ) -> Optional[int]: - _result = self.query_subtensor("NetworkConnect", block, [netuid_0, netuid_1]) - return getattr(_result, "value", None) - - def get_emission_value_by_subnet( - self, netuid: int, block: Optional[int] = None - ) -> Optional[float]: - """ - Retrieves the emission value of a specific subnet within the Bittensor network. The emission value - represents the rate at which the subnet emits or distributes the network's native token (Tao). - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Optional[float]: The emission value of the subnet, None if not available. - - The emission value is a critical economic parameter, influencing the incentive distribution and - reward mechanisms within the subnet. - """ - _result = self.query_subtensor("EmissionValues", block, [netuid]) - return ( - None - if getattr(_result, "value", None) is None - else Balance.from_rao(_result.value) - ) - - def get_subnet_connection_requirements( - self, netuid: int, block: Optional[int] = None - ) -> Dict[str, int]: - """ - Retrieves the connection requirements for a specific subnet within the Bittensor network. This - function provides details on the criteria that must be met for neurons to connect to the subnet. - - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Dict[str, int]: A dictionary detailing the connection requirements for the subnet. - - Understanding these requirements is crucial for neurons looking to participate in or interact - with specific subnets, ensuring compliance with their connection standards. - """ - result = self.query_map_subtensor("NetworkConnect", block, [netuid]) - return ( - {str(netuid.value): exists.value for netuid, exists in result.records} - if result and hasattr(result, "records") - else {} - ) - - def get_subnets(self, block: Optional[int] = None) -> List[int]: - """ - Retrieves a list of all subnets currently active within the Bittensor network. This function - provides an overview of the various subnets and their identifiers. - - Args: - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - List[int]: A list of network UIDs representing each active subnet. - - This function is valuable for understanding the network's structure and the diversity of subnets - available for neuron participation and collaboration. - """ - result = self.query_map_subtensor("NetworksAdded", block) - return ( - [network[0].value for network in result.records] - if result and hasattr(result, "records") - else [] - ) - - @networking.ensure_connected - def get_all_subnets_info(self, block: Optional[int] = None) -> List[SubnetInfo]: - """ - Retrieves detailed information about all subnets within the Bittensor network. This function - provides comprehensive data on each subnet, including its characteristics and operational parameters. - - Args: - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - List[SubnetInfo]: A list of SubnetInfo objects, each containing detailed information about a subnet. - - Gaining insights into the subnets' details assists in understanding the network's composition, - the roles of different subnets, and their unique features. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="subnetInfo_getSubnetsInfo", # custom rpc method - params=[block_hash] if block_hash else [], - ) - - json_body = make_substrate_call_with_retry() - - if not (result := json_body.get("result", None)): - return [] - - return SubnetInfo.list_from_vec_u8(result) - - @networking.ensure_connected - def get_subnet_info( - self, netuid: int, block: Optional[int] = None - ) -> Optional[SubnetInfo]: - """ - Retrieves detailed information about a specific subnet within the Bittensor network. This function - provides key data on the subnet, including its operational parameters and network status. - - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Optional[SubnetInfo]: Detailed information about the subnet, or ``None`` if not found. - - This function is essential for neurons and stakeholders interested in the specifics of a particular - subnet, including its governance, performance, and role within the broader network. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="subnetInfo_getSubnetInfo", # custom rpc method - params=[netuid, block_hash] if block_hash else [netuid], - ) - - json_body = make_substrate_call_with_retry() - - if not (result := json_body.get("result", None)): - return None - - return SubnetInfo.from_vec_u8(result) - - def get_subnet_hyperparameters( - self, netuid: int, block: Optional[int] = None - ) -> Optional[Union[List, SubnetHyperparameters]]: - """ - Retrieves the hyperparameters for a specific subnet within the Bittensor network. These hyperparameters - define the operational settings and rules governing the subnet's behavior. - - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Optional[SubnetHyperparameters]: The subnet's hyperparameters, or ``None`` if not available. - - Understanding the hyperparameters is crucial for comprehending how subnets are configured and - managed, and how they interact with the network's consensus and incentive mechanisms. - """ - hex_bytes_result = self.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, - ) - - if hex_bytes_result is None: - return [] - - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) - - return SubnetHyperparameters.from_vec_u8(bytes_result) # type: ignore - - def get_subnet_owner( - self, netuid: int, block: Optional[int] = None - ) -> Optional[str]: - """ - Retrieves the owner's address of a specific subnet within the Bittensor network. The owner is - typically the entity responsible for the creation and maintenance of the subnet. - - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Optional[str]: The SS58 address of the subnet's owner, or ``None`` if not available. - - Knowing the subnet owner provides insights into the governance and operational control of the subnet, - which can be important for decision-making and collaboration within the network. - """ - _result = self.query_subtensor("SubnetOwner", block, [netuid]) - return getattr(_result, "value", None) - - ############## - # Nomination # - ############## - def is_hotkey_delegate(self, hotkey_ss58: str, block: Optional[int] = None) -> bool: - """ - Determines whether a given hotkey (public key) is a delegate on the Bittensor network. This function - checks if the neuron associated with the hotkey is part of the network's delegation system. - - Args: - hotkey_ss58 (str): The SS58 address of the neuron's hotkey. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - bool: ``True`` if the hotkey is a delegate, ``False`` otherwise. - - Being a delegate is a significant status within the Bittensor network, indicating a neuron's - involvement in consensus and governance processes. - """ - return hotkey_ss58 in [ - info.hotkey_ss58 for info in self.get_delegates(block=block) - ] - - def get_delegate_take( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[float]: - """ - Retrieves 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. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Optional[float]: The delegate take percentage, None if not available. - - The delegate take is a critical parameter in the network's incentive structure, influencing - the distribution of rewards among neurons and their nominators. - """ - _result = self.query_subtensor("Delegates", block, [hotkey_ss58]) - return ( - None - if getattr(_result, "value", None) is None - else U16_NORMALIZED_FLOAT(_result.value) - ) - - def get_nominators_for_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Union[List[Tuple[str, Balance]], int]: - """ - Retrieves a list of nominators and their stakes for a neuron identified by its hotkey. - Nominators are neurons that stake their tokens on a delegate to support its operations. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Union[List[Tuple[str, Balance]], int]: A list of tuples containing each nominator's address and staked amount or 0. - - This function provides insights into the neuron's support network within the Bittensor ecosystem, - indicating its trust and collaboration relationships. - """ - result = self.query_map_subtensor("Stake", block, [hotkey_ss58]) - return ( - [(record[0].value, record[1].value) for record in result.records] - if result and hasattr(result, "records") - else 0 - ) - - @networking.ensure_connected - def get_delegate_by_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional[DelegateInfo]: - """ - Retrieves detailed information about a delegate neuron based on its hotkey. This function provides - a comprehensive view of the delegate's status, including its stakes, nominators, and reward distribution. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the delegate's hotkey. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Optional[DelegateInfo]: Detailed information about the delegate neuron, ``None`` if not found. - - This function is essential for understanding the roles and influence of delegate neurons within - the Bittensor network's consensus and governance structures. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(encoded_hotkey_: List[int]): - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="delegateInfo_getDelegate", # custom rpc method - params=( - [encoded_hotkey_, block_hash] if block_hash else [encoded_hotkey_] - ), - ) - - encoded_hotkey = ss58_to_vec_u8(hotkey_ss58) - json_body = make_substrate_call_with_retry(encoded_hotkey) - - if not (result := json_body.get("result", None)): - return None - - return DelegateInfo.from_vec_u8(result) - - @networking.ensure_connected - def get_delegates_lite(self, block: Optional[int] = None) -> List[DelegateInfoLite]: - """ - Retrieves a lighter list of all delegate neurons within the Bittensor network. This function provides an - overview of the neurons that are actively involved in the network's delegation system. - - Analyzing the delegate population offers insights into the network's governance dynamics and the distribution - of trust and responsibility among participating neurons. - - This is a lighter version of :func:`get_delegates`. - - Args: - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - List[DelegateInfoLite]: A list of ``DelegateInfoLite`` objects detailing each delegate's characteristics. - - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="delegateInfo_getDelegatesLite", # custom rpc method - params=[block_hash] if block_hash else [], - ) - - json_body = make_substrate_call_with_retry() - - if not (result := json_body.get("result", None)): - return [] - - return [DelegateInfoLite(**d) for d in result] - - @networking.ensure_connected - def get_delegates(self, block: Optional[int] = None) -> List[DelegateInfo]: - """ - Retrieves a list of all delegate neurons within the Bittensor network. This function provides an overview of the - neurons that are actively involved in the network's delegation system. - - Analyzing the delegate population offers insights into the network's governance dynamics and the distribution of - trust and responsibility among participating neurons. - - Args: - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - List[DelegateInfo]: A list of DelegateInfo objects detailing each delegate's characteristics. - - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="delegateInfo_getDelegates", # custom rpc method - params=[block_hash] if block_hash else [], - ) - - json_body = make_substrate_call_with_retry() - - if not (result := json_body.get("result", None)): - return [] - - return DelegateInfo.list_from_vec_u8(result) - - @networking.ensure_connected - def get_delegated( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> List[Tuple[DelegateInfo, Balance]]: - """ - Retrieves a list of delegates and their associated stakes for a given coldkey. This function - identifies the delegates that a specific account has staked tokens on. - - Args: - coldkey_ss58 (str): The ``SS58`` address of the account's coldkey. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - List[Tuple[DelegateInfo, Balance]]: A list of tuples, each containing a delegate's information and staked - amount. - - This function is important for account holders to understand their stake allocations and their - involvement in the network's delegation and consensus mechanisms. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(encoded_coldkey_: List[int]): - block_hash = None if block is None else self.substrate.get_block_hash(block) - - return self.substrate.rpc_request( - method="delegateInfo_getDelegated", - params=( - [block_hash, encoded_coldkey_] if block_hash else [encoded_coldkey_] - ), - ) - - encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - json_body = make_substrate_call_with_retry(encoded_coldkey) - - if not (result := json_body.get("result", None)): - return [] - - return DelegateInfo.delegated_list_from_vec_u8(result) - - ############################ - # Child Hotkey Information # - ############################ - - def get_childkey_take( - self, hotkey: str, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Get the childkey take of a hotkey on a specific network. - Args: - - hotkey (str): The hotkey to search for. - - netuid (int): The netuid to search for. - - block (Optional[int]): Optional parameter specifying the block number. Defaults to None. - - Returns: - - Optional[int]: The value of the "ChildkeyTake" if found, or None if any error occurs. - """ - try: - childkey_take = self.query_subtensor( - name="ChildkeyTake", - block=block, - params=[hotkey, netuid], - ) - if childkey_take: - return int(childkey_take.value) - - except SubstrateRequestException as e: - print(f"Error querying ChildKeys: {e}") - return None - except Exception as e: - print(f"Unexpected error in get_children: {e}") - return None - return None - - @networking.ensure_connected - def get_children(self, hotkey, netuid) -> list[tuple[int, str]] | list[Any] | None: - """ - Get the children of a hotkey on a specific network. - Args: - hotkey (str): The hotkey to query. - netuid (int): The network ID. - Returns: - list or None: List of (proportion, child_address) tuples, or None if an error occurred. - """ - try: - children = self.substrate.query( - module="SubtensorModule", - storage_function="ChildKeys", - params=[hotkey, netuid], - ) - if children: - return format_children(children) - else: - return [] - except SubstrateRequestException as e: - print(f"Error querying ChildKeys: {e}") - return None - except Exception as e: - print(f"Unexpected error in get_children: {e}") - return None - - @networking.ensure_connected - def get_parents(self, child_hotkey, netuid): - """ - Get the parents of a child hotkey on a specific network. - Args: - child_hotkey (str): The child hotkey to query. - netuid (int): The network ID. - Returns: - list or None: List of (proportion, parent_address) tuples, or None if an error occurred. - """ - try: - parents = self.substrate.query( - module="SubtensorModule", - storage_function="ParentKeys", - params=[child_hotkey, netuid], - ) - if not parents: - print("No parents found.") - return [] - - formatted_parents = [ - format_parent(proportion, parent) - for proportion, parent in parents - if proportion != 0 - ] - return formatted_parents - except SubstrateRequestException as e: - print(f"Error querying ParentKeys: {e}") - except Exception as e: - print(f"Unexpected error in get_parents: {e}") - - return None - - ##################### - # Stake Information # - ##################### - - def get_stake_info_for_coldkey( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> Optional[List[StakeInfo]]: - """ - Retrieves stake information associated with a specific coldkey. This function provides details - about the stakes held by an account, including the staked amounts and associated delegates. - - Args: - coldkey_ss58 (str): The ``SS58`` address of the account's coldkey. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - List[StakeInfo]: A list of StakeInfo objects detailing the stake allocations for the account. - - Stake information is vital for account holders to assess their investment and participation - in the network's delegation and consensus processes. - """ - encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - - hex_bytes_result = self.query_runtime_api( - runtime_api="StakeInfoRuntimeApi", - method="get_stake_info_for_coldkey", - params=[encoded_coldkey], # type: ignore - block=block, - ) - - if hex_bytes_result is None: - return None - - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) - # TODO: review if this is the correct type / works - return StakeInfo.list_from_vec_u8(bytes_result) # type: ignore - - def get_stake_info_for_coldkeys( - self, coldkey_ss58_list: List[str], block: Optional[int] = None - ) -> Optional[Dict[str, List[StakeInfo]]]: - """ - Retrieves stake information for a list of coldkeys. This function aggregates stake data for multiple - accounts, providing a collective view of their stakes and delegations. - - Args: - coldkey_ss58_list (List[str]): A list of ``SS58`` addresses of the accounts' coldkeys. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Dict[str, List[StakeInfo]]: A dictionary mapping each coldkey to a list of its StakeInfo objects. - - This function is useful for analyzing the stake distribution and delegation patterns of multiple - accounts simultaneously, offering a broader perspective on network participation and investment strategies. - """ - # TODO: review - ss58_to_vec_u8 returns List[int] but the runtime api expects List[List[int]] - encoded_coldkeys = [ - ss58_to_vec_u8(coldkey_ss58) for coldkey_ss58 in coldkey_ss58_list - ] - - hex_bytes_result = self.query_runtime_api( - runtime_api="StakeInfoRuntimeApi", - method="get_stake_info_for_coldkeys", - params=[encoded_coldkeys], # type: ignore - block=block, - ) - - if hex_bytes_result is None: - return None - - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) - - return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore - - @networking.ensure_connected - def get_minimum_required_stake( - self, - ) -> Balance: - """ - Returns the minimum required stake for nominators in the Subtensor network. - - This method retries the substrate call up to three times with exponential backoff in case of failures. - - Returns: - Balance: The minimum required stake as a Balance object. - - Raises: - Exception: If the substrate call fails after the maximum number of retries. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - return self.substrate.query( - module="SubtensorModule", storage_function="NominatorMinRequiredStake" - ) - - result = make_substrate_call_with_retry() - return Balance.from_rao(result.decode()) - - ################################# - # Neuron information per subnet # - ################################# - - def is_hotkey_registered_any( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> bool: - """ - Checks if a neuron's hotkey is registered on any subnet within the Bittensor network. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number at which to perform the check. - - Returns: - bool: ``True`` if the hotkey is registered on any subnet, False otherwise. - - This function is essential for determining the network-wide presence and participation of a neuron. - """ - return len(self.get_netuids_for_hotkey(hotkey_ss58, block)) > 0 - - def is_hotkey_registered_on_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> bool: - """ - Checks if a neuron's hotkey is registered on a specific subnet within the Bittensor network. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to perform the check. - - Returns: - bool: ``True`` if the hotkey is registered on the specified subnet, False otherwise. - - This function helps in assessing the participation of a neuron in a particular subnet, - indicating its specific area of operation or influence within the network. - """ - return self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block) is not None - - def is_hotkey_registered( - self, - hotkey_ss58: str, - netuid: Optional[int] = None, - block: Optional[int] = None, - ) -> bool: - """ - Determines whether a given hotkey (public key) is registered in the Bittensor network, either - globally across any subnet or specifically on a specified subnet. This function checks the registration - status of a neuron identified by its hotkey, which is crucial for validating its participation and - activities within the network. - - Args: - hotkey_ss58 (str): The SS58 address of the neuron's hotkey. - netuid (Optional[int]): The unique identifier of the subnet to check the registration. If ``None``, the - registration is checked across all subnets. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - bool: ``True`` if the hotkey is registered in the specified context (either any subnet or a specific - subnet), ``False`` otherwise. - - This function is important for verifying the active status of neurons in the Bittensor network. It aids - in understanding whether a neuron is eligible to participate in network processes such as consensus, - validation, and incentive distribution based on its registration status. - """ - if netuid is None: - return self.is_hotkey_registered_any(hotkey_ss58, block) - else: - return self.is_hotkey_registered_on_subnet(hotkey_ss58, netuid, block) - - def get_uid_for_hotkey_on_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional[int]: - """ - Retrieves the unique identifier (UID) for a neuron's hotkey on a specific subnet. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[int]: The UID of the neuron if it is registered on the subnet, ``None`` otherwise. - - The UID is a critical identifier within the network, linking the neuron's hotkey to its - operational and governance activities on a particular subnet. - """ - _result = self.query_subtensor("Uids", block, [netuid, hotkey_ss58]) - return getattr(_result, "value", None) - - def get_all_uids_for_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> List[int]: - """ - Retrieves all unique identifiers (UIDs) associated with a given hotkey across different subnets - within the Bittensor network. This function helps in identifying all the neuron instances that are - linked to a specific hotkey. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - List[int]: A list of UIDs associated with the given hotkey across various subnets. - - This function is important for tracking a neuron's presence and activities across different - subnets within the Bittensor ecosystem. - """ - return [ - self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block) or 0 - for netuid in self.get_netuids_for_hotkey(hotkey_ss58, block) - ] - - def get_netuids_for_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> List[int]: - """ - Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function - identifies the specific subnets within the Bittensor network where the neuron associated with - the hotkey is active. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - List[int]: A list of netuids where the neuron is a member. - """ - result = self.query_map_subtensor("IsNetworkMember", block, [hotkey_ss58]) - return ( - [record[0].value for record in result.records if record[1]] - if result and hasattr(result, "records") - else [] - ) - - def get_neuron_for_pubkey_and_subnet( - self, hotkey_ss58: str, netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfo]: - """ - Retrieves information about a neuron based on its public key (hotkey SS58 address) and the specific - subnet UID (netuid). This function provides detailed neuron information for a particular subnet within - the Bittensor network. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - Optional[NeuronInfo]: Detailed information about the neuron if found, ``None`` otherwise. - - This function is crucial for accessing specific neuron data and understanding its status, stake, - and other attributes within a particular subnet of the Bittensor ecosystem. - """ - return self.neuron_for_uid( - self.get_uid_for_hotkey_on_subnet(hotkey_ss58, netuid, block=block), - netuid, - block=block, - ) - - def get_all_neurons_for_pubkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> List[NeuronInfo]: - """ - Retrieves information about all neuron instances associated with a given public key (hotkey ``SS58`` - address) across different subnets of the Bittensor network. This function aggregates neuron data - from various subnets to provide a comprehensive view of a neuron's presence and status within the network. - - Args: - hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. - block (Optional[int]): The blockchain block number for the query. - - Returns: - List[NeuronInfo]: A list of NeuronInfo objects detailing the neuron's presence across various subnets. - - This function is valuable for analyzing a neuron's overall participation, influence, and - contributions across the Bittensor network. - """ - netuids = self.get_netuids_for_hotkey(hotkey_ss58, block) - uids = [self.get_uid_for_hotkey_on_subnet(hotkey_ss58, net) for net in netuids] - return [self.neuron_for_uid(uid, net) for uid, net in list(zip(uids, netuids))] - - def neuron_has_validator_permit( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> Optional[bool]: - """ - Checks if a neuron, identified by its unique identifier (UID), has a validator permit on a specific - subnet within the Bittensor network. This function determines whether the neuron is authorized to - participate in validation processes on the subnet. - - Args: - uid (int): The unique identifier of the neuron. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number for the query. - - Returns: - Optional[bool]: ``True`` if the neuron has a validator permit, False otherwise. - - This function is essential for understanding a neuron's role and capabilities within a specific - subnet, particularly regarding its involvement in network validation and governance. - """ - _result = self.query_subtensor("ValidatorPermit", block, [netuid, uid]) - return getattr(_result, "value", None) - - def neuron_for_wallet( - self, wallet: "bittensor.wallet", netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfo]: - """ - Retrieves information about a neuron associated with a given wallet on a specific subnet. - This function provides detailed data about the neuron's status, stake, and activities based on - the wallet's hotkey address. - - Args: - wallet (bittensor.wallet): The wallet associated with the neuron. - netuid (int): The unique identifier of the subnet. - block (Optional[int]): The blockchain block number at which to perform the query. - - Returns: - Optional[NeuronInfo]: Detailed information about the neuron if found, ``None`` otherwise. - - This function is important for wallet owners to understand and manage their neuron's presence - and activities within a particular subnet of the Bittensor network. - """ - return self.get_neuron_for_pubkey_and_subnet( - wallet.hotkey.ss58_address, netuid=netuid, block=block - ) - - @networking.ensure_connected - def neuron_for_uid( - self, uid: Optional[int], netuid: int, block: Optional[int] = None - ) -> NeuronInfo: - """ - Retrieves detailed information about a specific neuron identified by its unique identifier (UID) - within a specified subnet (netuid) of the Bittensor network. This function provides a comprehensive - view of a neuron's attributes, including its stake, rank, and operational status. - - Args: - uid (int): The unique identifier of the neuron. - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - NeuronInfo: Detailed information about the neuron if found, ``None`` otherwise. - - This function is crucial for analyzing individual neurons' contributions and status within a specific - subnet, offering insights into their roles in the network's consensus and validation mechanisms. - """ - if uid is None: - return NeuronInfo.get_null_neuron() - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - block_hash = None if block is None else self.substrate.get_block_hash(block) - params = [netuid, uid] - if block_hash: - params = params + [block_hash] - return self.substrate.rpc_request( - method="neuronInfo_getNeuron", - params=params, # custom rpc method - ) - - json_body = make_substrate_call_with_retry() - - if not (result := json_body.get("result", None)): - return NeuronInfo.get_null_neuron() - - return NeuronInfo.from_vec_u8(result) - - def neurons(self, netuid: int, block: Optional[int] = None) -> List[NeuronInfo]: - """ - Retrieves a list of all neurons within a specified subnet of the Bittensor network. This function - provides a snapshot of the subnet's neuron population, including each neuron's attributes and network - interactions. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - List[NeuronInfo]: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet. - - Understanding the distribution and status of neurons within a subnet is key to comprehending the - network's decentralized structure and the dynamics of its consensus and governance processes. - """ - neurons_lite = self.neurons_lite(netuid=netuid, block=block) - weights = self.weights(block=block, netuid=netuid) - bonds = self.bonds(block=block, netuid=netuid) - - weights_as_dict = {uid: w for uid, w in weights} - bonds_as_dict = {uid: b for uid, b in bonds} - - neurons = [ - NeuronInfo.from_weights_bonds_and_neuron_lite( - neuron_lite, weights_as_dict, bonds_as_dict - ) - for neuron_lite in neurons_lite - ] - - return neurons - - def neuron_for_uid_lite( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfoLite]: - """ - Retrieves a lightweight version of information about a neuron in a specific subnet, identified by - its UID. The 'lite' version focuses on essential attributes such as stake and network activity. - - Args: - uid (int): The unique identifier of the neuron. - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - Optional[NeuronInfoLite]: A simplified version of neuron information if found, ``None`` otherwise. - - This function is useful for quick and efficient analyses of neuron status and activities within a - subnet without the need for comprehensive data retrieval. - """ - if uid is None: - return NeuronInfoLite.get_null_neuron() - - hex_bytes_result = self.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neuron_lite", - params={ - "netuid": netuid, - "uid": uid, - }, - block=block, - ) - - if hex_bytes_result is None: - return NeuronInfoLite.get_null_neuron() - - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) - - return NeuronInfoLite.from_vec_u8(bytes_result) # type: ignore - - def neurons_lite( - self, netuid: int, block: Optional[int] = None - ) -> List[NeuronInfoLite]: - """ - Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. - This function provides a streamlined view of the neurons, focusing on key attributes such as stake - and network participation. - - Args: - netuid (int): The unique identifier of the subnet. - block (Optional[int], optional): The blockchain block number for the query. - - Returns: - List[NeuronInfoLite]: A list of simplified neuron information for the subnet. - - This function offers a quick overview of the neuron population within a subnet, facilitating - efficient analysis of the network's decentralized structure and neuron dynamics. - """ - hex_bytes_result = self.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neurons_lite", - params=[netuid], - block=block, - ) - - if hex_bytes_result is None: - return [] - - if hex_bytes_result.startswith("0x"): - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - else: - bytes_result = bytes.fromhex(hex_bytes_result) - - return NeuronInfoLite.list_from_vec_u8(bytes_result) # type: ignore - - def metagraph( - self, - netuid: int, - lite: bool = True, - block: Optional[int] = None, - ) -> "bittensor.metagraph": # type: ignore - """ - Returns a synced metagraph for a specified subnet within the Bittensor network. The metagraph - represents the network's structure, including neuron connections and interactions. - - Args: - netuid (int): The network UID of the subnet to query. - lite (bool, default=True): If true, returns a metagraph using a lightweight sync (no weights, no bonds). - block (Optional[int]): Block number for synchronization, or ``None`` for the latest block. - - Returns: - bittensor.Metagraph: The metagraph representing the subnet's structure and neuron relationships. - - The metagraph is an essential tool for understanding the topology and dynamics of the Bittensor - network's decentralized architecture, particularly in relation to neuron interconnectivity and consensus - processes. - """ - metagraph_ = bittensor.metagraph( - network=self.network, netuid=netuid, lite=lite, sync=False - ) - metagraph_.sync(block=block, lite=lite, subtensor=self) - - return metagraph_ - - def incentive(self, netuid: int, block: Optional[int] = None) -> List[int]: - """ - Retrieves the list of incentives for neurons within a specific subnet of the Bittensor network. - This function provides insights into the reward distribution mechanisms and the incentives allocated - to each neuron based on their contributions and activities. - - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. - - Returns: - List[int]: The list of incentives for neurons within the subnet, indexed by UID. - - Understanding the incentive structure is crucial for analyzing the network's economic model and - the motivational drivers for neuron participation and collaboration. - """ - i_map = [] - i_map_encoded = self.query_map_subtensor(name="Incentive", block=block) - if i_map_encoded.records: - for netuid_, incentives_map in i_map_encoded: - if netuid_ == netuid: - i_map = incentives_map.serialize() - break - - return i_map - - def weights( - self, netuid: int, block: Optional[int] = None - ) -> List[Tuple[int, List[Tuple[int, int]]]]: - """ - Retrieves the weight distribution set by neurons within a specific subnet of the Bittensor network. - This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the - network's trust and value assignment mechanisms. - - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. - - Returns: - List[Tuple[int, List[Tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its assigned weights. - - The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons, - influencing their influence and reward allocation within the subnet. - """ - w_map = [] - w_map_encoded = self.query_map_subtensor( - name="Weights", block=block, params=[netuid] - ) - if w_map_encoded.records: - for uid, w in w_map_encoded: - w_map.append((uid.serialize(), w.serialize())) - - return w_map - - def bonds( - self, netuid: int, block: Optional[int] = None - ) -> List[Tuple[int, List[Tuple[int, int]]]]: - """ - Retrieves the bond distribution set by neurons within a specific subnet of the Bittensor network. - Bonds represent the investments or commitments made by neurons in one another, indicating a level - of trust and perceived value. This bonding mechanism is integral to the network's market-based approach - to measuring and rewarding machine intelligence. - - Args: - netuid (int): The network UID of the subnet to query. - block (Optional[int]): The blockchain block number for the query. - - Returns: - List[Tuple[int, List[Tuple[int, int]]]]: A list of tuples mapping each neuron's UID to its bonds with other - neurons. - - Understanding bond distributions is crucial for analyzing the trust dynamics and market behavior - within the subnet. It reflects how neurons recognize and invest in each other's intelligence and - contributions, supporting diverse and niche systems within the Bittensor ecosystem. - """ - b_map = [] - b_map_encoded = self.query_map_subtensor( - name="Bonds", block=block, params=[netuid] - ) - if b_map_encoded.records: - for uid, b in b_map_encoded: - b_map.append((uid.serialize(), b.serialize())) - - return b_map - - def get_subnet_burn_cost(self, block: Optional[int] = None) -> Optional[str]: - """ - Retrieves the burn cost for registering a new subnet within the Bittensor network. This cost - represents the amount of Tao that needs to be locked or burned to establish a new subnet. - - Args: - block (Optional[int]): The blockchain block number for the query. - - Returns: - int: The burn cost for subnet registration. - - The subnet burn cost is an important economic parameter, reflecting the network's mechanisms for - controlling the proliferation of subnets and ensuring their commitment to the network's long-term viability. - """ - lock_cost = self.query_runtime_api( - runtime_api="SubnetRegistrationRuntimeApi", - method="get_network_registration_cost", - params=[], - block=block, - ) - - if lock_cost is None: - return None - - return lock_cost - - ############## - # Extrinsics # - ############## - - @networking.ensure_connected - def _do_delegation( - self, - wallet: "bittensor.wallet", - delegate_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """ - Delegates a specified amount of stake to a delegate's hotkey. - - This method sends a transaction to add stake to a delegate's hotkey and retries the call up to three times - with exponential backoff in case of failures. - - Args: - wallet (bittensor.wallet): The wallet from which the stake will be delegated. - delegate_ss58 (str): The SS58 address of the delegate's hotkey. - amount (Balance): The amount of stake to be delegated. - wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. - - Returns: - bool: ``True`` if the delegation is successful, ``False`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={"hotkey": delegate_ss58, "amount_staked": amount.rao}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise StakeError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() - - @networking.ensure_connected - def _do_undelegation( - self, - wallet: "bittensor.wallet", - delegate_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """ - Removes a specified amount of stake from a delegate's hotkey. - - This method sends a transaction to remove stake from a delegate's hotkey and retries the call up to three times - with exponential backoff in case of failures. - - Args: - wallet (bittensor.wallet): The wallet from which the stake will be removed. - delegate_ss58 (str): The SS58 address of the delegate's hotkey. - amount (Balance): The amount of stake to be removed. - wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. - - Returns: - bool: ``True`` if the undelegation is successful, ``False`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": delegate_ss58, - "amount_unstaked": amount.rao, - }, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise StakeError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() - - @networking.ensure_connected - def _do_nominate( - self, - wallet: "bittensor.wallet", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """ - Nominates the wallet's hotkey to become a delegate. - - This method sends a transaction to nominate the wallet's hotkey to become a delegate and retries the call up to - three times with exponential backoff in case of failures. - - Args: - wallet (bittensor.wallet): The wallet whose hotkey will be nominated. - wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. - - Returns: - bool: ``True`` if the nomination is successful, ``False`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - call = self.substrate.compose_call( - call_module="SubtensorModule", - call_function="become_delegate", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - extrinsic = self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) # sign with coldkey - response = self.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise NominationError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() - - @networking.ensure_connected - def _do_increase_take( - self, - wallet: "bittensor.wallet", - hotkey_ss58: str, - take: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """ - Increases the take rate for a delegate's hotkey. - - This method sends a transaction to increase the take rate for a delegate's hotkey and retries the call up to - three times with exponential backoff in case of failures. - - Args: - wallet (bittensor.wallet): The wallet from which the transaction will be signed. - hotkey_ss58 (str): The SS58 address of the delegate's hotkey. - take (int): The new take rate to be set. - wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. - - Returns: - bool: ``True`` if the take rate increase is successful, ``False`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4) - def make_substrate_call_with_retry(): - with self.substrate as substrate: - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="increase_take", - call_params={ - "hotkey": hotkey_ss58, - "take": take, - }, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) # sign with coldkey - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise TakeError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() - - @networking.ensure_connected - def _do_decrease_take( - self, - wallet: "bittensor.wallet", - hotkey_ss58: str, - take: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - """ - Decreases the take rate for a delegate's hotkey. - - This method sends a transaction to decrease the take rate for a delegate's hotkey and retries the call up to - three times with exponential backoff in case of failures. - - Args: - wallet (bittensor.wallet): The wallet from which the transaction will be signed. - hotkey_ss58 (str): The SS58 address of the delegate's hotkey. - take (int): The new take rate to be set. - wait_for_inclusion (bool, optional): Whether to wait for the transaction to be included in a block. Default is ``True``. - wait_for_finalization (bool, optional): Whether to wait for the transaction to be finalized. Default is ``False``. - - Returns: - bool: ``True`` if the take rate decrease is successful, ``False`` otherwise. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4) - def make_substrate_call_with_retry(): - with self.substrate as substrate: - call = substrate.compose_call( - call_module="SubtensorModule", - call_function="decrease_take", - call_params={ - "hotkey": hotkey_ss58, - "take": take, - }, - ) - extrinsic = substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) # sign with coldkey - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - response.process_events() - if response.is_success: - return True - else: - raise TakeError(format_error_message(response.error_message)) - - return make_substrate_call_with_retry() - - ########## - # Legacy # - ########## - - @networking.ensure_connected - def get_balance(self, address: str, block: Optional[int] = None) -> Balance: - """ - Retrieves the token balance of a specific address within the Bittensor network. This function queries - the blockchain to determine the amount of Tao held by a given account. - - Args: - address (str): The Substrate address in ``ss58`` format. - block (int, optional): The blockchain block number at which to perform the query. - - Returns: - Balance: The account balance at the specified block, represented as a Balance object. - - This function is important for monitoring account holdings and managing financial transactions - within the Bittensor ecosystem. It helps in assessing the economic status and capacity of network participants. - """ - try: - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - return self.substrate.query( - module="System", - storage_function="Account", - params=[address], - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - result = make_substrate_call_with_retry() - except RemainingScaleBytesNotEmptyException: - _logger.error( - "Received a corrupted message. This likely points to an error with the network or subnet." - ) - return Balance(1000) - return Balance(result.value["data"]["free"]) - - @networking.ensure_connected - def get_current_block(self) -> int: - """ - Returns the current block number on the Bittensor blockchain. This function provides the latest block - number, indicating the most recent state of the blockchain. - - Returns: - int: The current chain block number. - - Knowing the current block number is essential for querying real-time data and performing time-sensitive - operations on the blockchain. It serves as a reference point for network activities and data synchronization. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - return self.substrate.get_block_number(None) # type: ignore - - return make_substrate_call_with_retry() - - @networking.ensure_connected - def get_balances(self, block: Optional[int] = None) -> Dict[str, Balance]: - """ - Retrieves the token balances of all accounts within the Bittensor network as of a specific blockchain block. - This function provides a comprehensive view of the token distribution among different accounts. - - Args: - block (int, optional): The blockchain block number at which to perform the query. - - Returns: - Dict[str, Balance]: A dictionary mapping each account's ``ss58`` address to its balance. - - This function is valuable for analyzing the overall economic landscape of the Bittensor network, - including the distribution of financial resources and the financial status of network participants. - """ - - @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) - def make_substrate_call_with_retry(): - return self.substrate.query_map( - module="System", - storage_function="Account", - block_hash=( - None if block is None else self.substrate.get_block_hash(block) - ), - ) - - result = make_substrate_call_with_retry() - return_dict = {} - for r in result: - bal = Balance(int(r[1]["data"]["free"].value)) - return_dict[r[0].value] = bal - return return_dict - - # TODO: check with the team if this is used anywhere externally. not in bittensor - @staticmethod - def _null_neuron() -> NeuronInfo: - neuron = NeuronInfo( - uid=0, - netuid=0, - active=0, - stake=Balance(0), - rank=0, - emission=0, - incentive=0, - consensus=0, - trust=0, - validator_trust=0, - dividends=0, - last_update=0, - validator_permit=False, - weights=[], - bonds=[], - prometheus_info=None, - axon_info=None, - is_null=True, - coldkey="000000000000000000000000000000000000000000000000", - hotkey="000000000000000000000000000000000000000000000000", - ) # type: ignore - return neuron - - @networking.ensure_connected - def get_block_hash(self, block_id: int) -> str: - """ - Retrieves the hash of a specific block on the Bittensor blockchain. The block hash is a unique - identifier representing the cryptographic hash of the block's content, ensuring its integrity and - immutability. - - Args: - block_id (int): The block number for which the hash is to be retrieved. - - Returns: - str: The cryptographic hash of the specified block. - - The block hash is a fundamental aspect of blockchain technology, providing a secure reference to - each block's data. It is crucial for verifying transactions, ensuring data consistency, and - maintaining the trustworthiness of the blockchain. - """ - return self.substrate.get_block_hash(block_id=block_id) - - def get_error_info_by_index(self, error_index: int) -> Tuple[str, str]: - """ - Returns the error name and description from the Subtensor error list. - - Args: - error_index (int): The index of the error to retrieve. - - Returns: - Tuple[str, str]: A tuple containing the error name and description from substrate metadata. If the error index is not found, returns ("Unknown Error", "") and logs a warning. - """ - unknown_error = ("Unknown Error", "") - - if not self._subtensor_errors: - self._subtensor_errors = get_subtensor_errors(self.substrate) - - name, description = self._subtensor_errors.get(str(error_index), unknown_error) - - if name == unknown_error[0]: - _logger.warning( - f"Subtensor returned an error with an unknown index: {error_index}" - ) - - return name, description - - -# TODO: remove this after fully migrate `bittensor.subtensor` to `bittensor.Subtensor` in `bittensor/__init__.py` -subtensor = Subtensor diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 700a65613..58a7e0a7c 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 @@ -17,136 +16,31 @@ # DEALINGS IN THE SOFTWARE. import hashlib -from typing import Callable, List, Dict, Literal, Tuple +from typing import List, Dict, Literal, Union, Optional, TYPE_CHECKING -import numpy as np import scalecodec +from substrateinterface import Keypair +from substrateinterface.utils import ss58 -import bittensor +from bittensor.core.settings import SS58_FORMAT +from bittensor.utils.btlogging import logging from .registration import torch, use_torch from .version import version_checking, check_version, VersionCheckError -from .wallet_utils import * # noqa F401 + +if TYPE_CHECKING: + from substrateinterface import SubstrateInterface RAOPERTAO = 1e9 U16_MAX = 65535 U64_MAX = 18446744073709551615 -def ss58_to_vec_u8(ss58_address: str) -> List[int]: - ss58_bytes: bytes = bittensor.utils.ss58_address_to_bytes(ss58_address) - encoded_address: List[int] = [int(byte) for byte in ss58_bytes] +def ss58_to_vec_u8(ss58_address: str) -> list[int]: + ss58_bytes: bytes = ss58_address_to_bytes(ss58_address) + encoded_address: list[int] = [int(byte) for byte in ss58_bytes] return encoded_address -def _unbiased_topk( - values: Union[np.ndarray, "torch.Tensor"], - k: int, - dim=0, - sorted=True, - largest=True, - axis=0, - return_type: str = "numpy", -) -> Union[Tuple[np.ndarray, np.ndarray], Tuple["torch.Tensor", "torch.LongTensor"]]: - """Selects topk as in torch.topk but does not bias lower indices when values are equal. - Args: - values: (np.ndarray) if using numpy, (torch.Tensor) if using torch: - Values to index into. - k: (int): - Number to take. - dim: (int): - Dimension to index into (used by Torch) - sorted: (bool): - Whether to sort indices. - largest: (bool): - Whether to take the largest value. - axis: (int): - Axis along which to index into (used by Numpy) - return_type: (str): - Whether or use torch or numpy approach - - Return: - topk: (np.ndarray) if using numpy, (torch.Tensor) if using torch: - topk k values. - indices: (np.ndarray) if using numpy, (torch.LongTensor) if using torch: - indices of the topk values. - """ - if return_type == "torch": - permutation = torch.randperm(values.shape[dim]) - permuted_values = values[permutation] - topk, indices = torch.topk( - permuted_values, k, dim=dim, sorted=sorted, largest=largest - ) - return topk, permutation[indices] - else: - if dim != 0 and axis == 0: - # Ensures a seamless transition for calls made to this function that specified args by keyword - axis = dim - - permutation = np.random.permutation(values.shape[axis]) - permuted_values = np.take(values, permutation, axis=axis) - indices = np.argpartition(permuted_values, -k, axis=axis)[-k:] - if not sorted: - indices = np.sort(indices, axis=axis) - if not largest: - indices = indices[::-1] - topk = np.take(permuted_values, indices, axis=axis) - return topk, permutation[indices] - - -def unbiased_topk( - values: Union[np.ndarray, "torch.Tensor"], - k: int, - dim: int = 0, - sorted: bool = True, - largest: bool = True, - axis: int = 0, -) -> Union[Tuple[np.ndarray, np.ndarray], Tuple["torch.Tensor", "torch.LongTensor"]]: - """Selects topk as in torch.topk but does not bias lower indices when values are equal. - Args: - values: (np.ndarray) if using numpy, (torch.Tensor) if using torch: - Values to index into. - k: (int): - Number to take. - dim: (int): - Dimension to index into (used by Torch) - sorted: (bool): - Whether to sort indices. - largest: (bool): - Whether to take the largest value. - axis: (int): - Axis along which to index into (used by Numpy) - - Return: - topk: (np.ndarray) if using numpy, (torch.Tensor) if using torch: - topk k values. - indices: (np.ndarray) if using numpy, (torch.LongTensor) if using torch: - indices of the topk values. - """ - if use_torch(): - return _unbiased_topk( - values, k, dim, sorted, largest, axis, return_type="torch" - ) - else: - return _unbiased_topk( - values, k, dim, sorted, largest, axis, return_type="numpy" - ) - - -def strtobool_with_default( - default: bool, -) -> Callable[[str], Union[bool, Literal["==SUPRESS=="]]]: - """ - Creates a strtobool function with a default value. - - Args: - default(bool): The default value to return if the string is empty. - - Returns: - The strtobool function with the default value. - """ - return lambda x: strtobool(x) if x != "" else default - - def strtobool(val: str) -> Union[bool, Literal["==SUPRESS=="]]: """ Converts a string to a boolean value. @@ -165,21 +59,21 @@ def strtobool(val: str) -> Union[bool, Literal["==SUPRESS=="]]: raise ValueError("invalid truth value %r" % (val,)) -def get_explorer_root_url_by_network_from_map( - network: str, network_map: Dict[str, Dict[str, str]] -) -> Optional[Dict[str, str]]: - r""" +def _get_explorer_root_url_by_network_from_map( + network: str, network_map: dict[str, dict[str, str]] +) -> Optional[dict[str, str]]: + """ Returns the explorer root url for the given network name from the given network map. Args: network(str): The network to get the explorer url for. - network_map(Dict[str, str]): The network map to get the explorer url from. + network_map(dict[str, str]): The network map to get the explorer url from. Returns: The explorer url for the given network. Or None if the network is not in the network map. """ - explorer_urls: Optional[Dict[str, str]] = {} + explorer_urls: Optional[dict[str, str]] = {} for entity_nm, entity_network_map in network_map.items(): if network in entity_network_map: explorer_urls[entity_nm] = entity_network_map[network] @@ -188,34 +82,34 @@ def get_explorer_root_url_by_network_from_map( def get_explorer_url_for_network( - network: str, block_hash: str, network_map: Dict[str, str] -) -> Optional[List[str]]: - r""" + network: str, block_hash: str, network_map: dict[str, dict[str, str]] +) -> Optional[dict[str, str]]: + """ Returns the explorer url for the given block hash and network. Args: network(str): The network to get the explorer url for. block_hash(str): The block hash to get the explorer url for. - network_map(Dict[str, Dict[str, str]]): The network maps to get the explorer urls from. + network_map(dict[str, dict[str, str]]): The network maps to get the explorer urls from. Returns: The explorer url for the given block hash and network. Or None if the network is not known. """ - explorer_urls: Optional[Dict[str, str]] = {} + explorer_urls: Optional[dict[str, str]] = {} # Will be None if the network is not known. i.e. not in network_map - explorer_root_urls: Optional[Dict[str, str]] = ( - get_explorer_root_url_by_network_from_map(network, network_map) + explorer_root_urls: Optional[dict[str, str]] = ( + _get_explorer_root_url_by_network_from_map(network, network_map) ) if explorer_root_urls != {}: # We are on a known network. - explorer_opentensor_url = "{root_url}/query/{block_hash}".format( - root_url=explorer_root_urls.get("opentensor"), block_hash=block_hash + explorer_opentensor_url = ( + f"{explorer_root_urls.get('opentensor')}/query/{block_hash}" ) - explorer_taostats_url = "{root_url}/extrinsic/{block_hash}".format( - root_url=explorer_root_urls.get("taostats"), block_hash=block_hash + explorer_taostats_url = ( + f"{explorer_root_urls.get('taostats')}/extrinsic/{block_hash}" ) explorer_urls["opentensor"] = explorer_opentensor_url explorer_urls["taostats"] = explorer_taostats_url @@ -225,32 +119,19 @@ def get_explorer_url_for_network( def ss58_address_to_bytes(ss58_address: str) -> bytes: """Converts a ss58 address to a bytes object.""" - account_id_hex: str = scalecodec.ss58_decode( - ss58_address, bittensor.__ss58_format__ - ) + account_id_hex: str = scalecodec.ss58_decode(ss58_address, SS58_FORMAT) return bytes.fromhex(account_id_hex) -def U16_NORMALIZED_FLOAT(x: int) -> float: +def u16_normalized_float(x: int) -> float: return float(x) / float(U16_MAX) -def U64_NORMALIZED_FLOAT(x: int) -> float: +def u64_normalized_float(x: int) -> float: return float(x) / float(U64_MAX) -def u8_key_to_ss58(u8_key: List[int]) -> str: - r""" - Converts a u8-encoded account key to an ss58 address. - - Args: - u8_key (List[int]): The u8-encoded account key. - """ - # First byte is length, then 32 bytes of key. - return scalecodec.ss58_encode(bytes(u8_key).hex(), bittensor.__ss58_format__) - - -def hash(content, encoding="utf-8"): +def get_hash(content, encoding="utf-8"): sha3 = hashlib.sha3_256() # Update the hash object with the concatenated string @@ -260,23 +141,139 @@ def hash(content, encoding="utf-8"): return sha3.hexdigest() -def format_error_message(error_message: dict) -> str: +def format_error_message( + error_message: dict, substrate: "SubstrateInterface" = None +) -> str: """ - Formats an error message from the Subtensor error information to using in extrinsics. + Formats an error message from the Subtensor error information for use in extrinsics. Args: error_message (dict): A dictionary containing the error information from Subtensor. + substrate (SubstrateInterface, optional): The substrate interface to use. Returns: str: A formatted error message string. """ - err_type = "UnknownType" err_name = "UnknownError" + err_type = "UnknownType" err_description = "Unknown Description" if isinstance(error_message, dict): - 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_docs[0] if len(err_docs) > 0 else err_description - return f"Subtensor returned `{err_name} ({err_type})` error. This means: `{err_description}`" + # subtensor error structure + if ( + error_message.get("code") + and error_message.get("message") + and error_message.get("data") + ): + err_name = "SubstrateRequestException" + err_type = error_message.get("message") + err_data = error_message.get("data") + + # subtensor custom error marker + if err_data.startswith("Custom error:") and substrate: + if not substrate.metadata: + substrate.get_metadata() + + if substrate.metadata: + try: + pallet = substrate.metadata.get_metadata_pallet( + "SubtensorModule" + ) + error_index = int(err_data.split("Custom error:")[-1]) + + error_dict = pallet.errors[error_index].value + err_type = error_dict.get("message", err_type) + err_docs = error_dict.get("docs", []) + err_description = err_docs[0] if err_docs else err_description + except Exception: + logging.error("Substrate pallets data unavailable.") + else: + err_description = err_data + + elif ( + error_message.get("type") + and error_message.get("name") + and error_message.get("docs") + ): + 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 + + return f"Subtensor returned `{err_name}({err_type})` error. This means: `{err_description}`." + + +# Subnet 24 uses this function +def is_valid_ss58_address(address: str) -> bool: + """ + Checks if the given address is a valid ss58 address. + + Args: + address(str): The address to check. + + Returns: + True if the address is a valid ss58 address for Bittensor, False otherwise. + """ + try: + return ss58.is_valid_ss58_address( + address, valid_ss58_format=SS58_FORMAT + ) or ss58.is_valid_ss58_address( + address, valid_ss58_format=42 + ) # Default substrate ss58 format (legacy) + except IndexError: + return False + + +def _is_valid_ed25519_pubkey(public_key: Union[str, bytes]) -> bool: + """ + Checks if the given public_key is a valid ed25519 key. + + Args: + public_key(Union[str, bytes]): The public_key to check. + + Returns: + True if the public_key is a valid ed25519 key, False otherwise. + + """ + try: + if isinstance(public_key, str): + if len(public_key) != 64 and len(public_key) != 66: + raise ValueError("a public_key should be 64 or 66 characters") + elif isinstance(public_key, bytes): + if len(public_key) != 32: + raise ValueError("a public_key should be 32 bytes") + else: + raise ValueError("public_key must be a string or bytes") + + keypair = Keypair(public_key=public_key, ss58_format=SS58_FORMAT) + + ss58_addr = keypair.ss58_address + return ss58_addr is not None + + except (ValueError, IndexError): + return False + + +def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool: + """ + Checks if the given address is a valid destination address. + + Args: + address(Union[str, bytes]): The address to check. + + Returns: + True if the address is a valid destination address, False otherwise. + """ + if isinstance(address, str): + # Check if ed25519 + if address.startswith("0x"): + return _is_valid_ed25519_pubkey(address) + else: + # Assume ss58 address + return is_valid_ss58_address(address) + elif isinstance(address, bytes): + # Check if ed25519 + return _is_valid_ed25519_pubkey(address) + else: + # Invalid address type + return False diff --git a/bittensor/utils/_register_cuda.py b/bittensor/utils/_register_cuda.py deleted file mode 100644 index 05619416e..000000000 --- a/bittensor/utils/_register_cuda.py +++ /dev/null @@ -1,126 +0,0 @@ -import binascii -import hashlib -import math -from typing import Tuple - -import numpy as np -from Crypto.Hash import keccak - -from contextlib import redirect_stdout -import io - - -def solve_cuda( - nonce_start: np.int64, - update_interval: np.int64, - tpb: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - dev_id: int = 0, -) -> Tuple[np.int64, bytes]: - """ - Solves the PoW problem using CUDA. - Args: - nonce_start: int64 - Starting nonce. - update_interval: int64 - Number of nonces to solve before updating block information. - tpb: int - Threads per block. - block_and_hotkey_hash_bytes: bytes - Keccak(Bytes of the block hash + bytes of the hotkey) 64 bytes. - difficulty: int256 - Difficulty of the PoW problem. - limit: int256 - Upper limit of the nonce. - dev_id: int (default=0) - The CUDA device ID - Returns: - Tuple[int64, bytes] - Tuple of the nonce and the seal corresponding to the solution. - Returns -1 for nonce if no solution is found. - """ - - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - - upper = int(limit // difficulty) - - upper_bytes = upper.to_bytes(32, byteorder="little", signed=False) - - def _hex_bytes_to_u8_list(hex_bytes: bytes): - hex_chunks = [ - int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2) - ] - return hex_chunks - - def _create_seal_hash(block_and_hotkey_hash_hex: bytes, nonce: int) -> bytes: - nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little")) - pre_seal = nonce_bytes + block_and_hotkey_hash_hex - seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest() - kec = keccak.new(digest_bits=256) - seal = kec.update(seal_sh256).digest() - return seal - - def _seal_meets_difficulty(seal: bytes, difficulty: int): - seal_number = int.from_bytes(seal, "big") - product = seal_number * difficulty - limit = int(math.pow(2, 256)) - 1 - - return product < limit - - # Call cython function - # int blockSize, uint64 nonce_start, uint64 update_interval, const unsigned char[:] limit, - # const unsigned char[:] block_bytes, int dev_id - block_and_hotkey_hash_hex = binascii.hexlify(block_and_hotkey_hash_bytes)[:64] - - solution = cubit.solve_cuda( - tpb, - nonce_start, - update_interval, - upper_bytes, - block_and_hotkey_hash_hex, - dev_id, - ) # 0 is first GPU - seal = None - if solution != -1: - seal = _create_seal_hash(block_and_hotkey_hash_hex, solution) - if _seal_meets_difficulty(seal, difficulty): - return solution, seal - else: - return -1, b"\x00" * 32 - - return solution, seal - - -def reset_cuda(): - """ - Resets the CUDA environment. - """ - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - - cubit.reset_cuda() - - -def log_cuda_errors() -> str: - """ - Logs any CUDA errors. - """ - try: - import cubit - except ImportError: - raise ImportError("Please install cubit") - - f = io.StringIO() - with redirect_stdout(f): - cubit.log_cuda_errors() - - s = f.getvalue() - - return s diff --git a/bittensor/utils/axon_utils.py b/bittensor/utils/axon_utils.py index 5912f389a..3c73c8080 100644 --- a/bittensor/utils/axon_utils.py +++ b/bittensor/utils/axon_utils.py @@ -1,29 +1,37 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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. - from typing import Optional -from bittensor.constants import ALLOWED_DELTA, NANOSECONDS_IN_SECOND +ALLOWED_DELTA = 4_000_000_000 # Delta of 4 seconds for nonce validation +NANOSECONDS_IN_SECOND = 1_000_000_000 def allowed_nonce_window_ns(current_time_ns: int, synapse_timeout: Optional[float]): + """ + Calculates the allowed window for a nonce in nanoseconds. + + Args: + current_time_ns (int): The current time in nanoseconds. + synapse_timeout (Optional[float]): The optional timeout for the synapse in seconds. If None, it defaults to 0. + + Returns: + int: The allowed nonce window in nanoseconds. + """ synapse_timeout_ns = (synapse_timeout or 0) * NANOSECONDS_IN_SECOND allowed_window_ns = current_time_ns - ALLOWED_DELTA - synapse_timeout_ns return allowed_window_ns @@ -32,6 +40,18 @@ def allowed_nonce_window_ns(current_time_ns: int, synapse_timeout: Optional[floa def calculate_diff_seconds( current_time: int, synapse_timeout: Optional[float], synapse_nonce: int ): + """ + Calculates the difference in seconds between the current time and the synapse nonce, + and also returns the allowed delta in seconds. + + Args: + current_time (int): The current time in nanoseconds. + synapse_timeout (Optional[float]): The optional timeout for the synapse in seconds. + synapse_nonce (int): The nonce value for the synapse in nanoseconds. + + Returns: + tuple: A tuple containing the difference in seconds (float) and the allowed delta in seconds (float). + """ synapse_timeout_ns = (synapse_timeout or 0) * NANOSECONDS_IN_SECOND diff_seconds = (current_time - synapse_nonce) / NANOSECONDS_IN_SECOND allowed_delta_seconds = (ALLOWED_DELTA + synapse_timeout_ns) / NANOSECONDS_IN_SECOND diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 63ca6cd5b..016db373a 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -1,16 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021-2022 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 @@ -19,7 +17,7 @@ from typing import Union -import bittensor +from bittensor.core import settings class Balance: @@ -29,14 +27,14 @@ class Balance: It provides methods to convert between these units, as well as to perform arithmetic and comparison operations. Attributes: - unit: A string representing the symbol for the tao unit. - rao_unit: A string representing the symbol for the rao unit. - rao: An integer that stores the balance in rao units. - tao: A float property that gives the balance in tao units. + unit (str): A string representing the symbol for the tao unit. + rao_unit (str): A string representing the symbol for the rao unit. + rao (int): An integer that stores the balance in rao units. + tao (float): A float property that gives the balance in tao units. """ - unit: str = bittensor.__tao_symbol__ # This is the tao unit - rao_unit: str = bittensor.__rao_symbol__ # This is the rao unit + unit: str = settings.TAO_SYMBOL # This is the tao unit + rao_unit: str = settings.RAO_SYMBOL # This is the rao unit rao: int tao: float @@ -61,29 +59,20 @@ def tao(self): return self.rao / pow(10, 9) def __int__(self): - """ - Convert the Balance object to an int. The resulting value is in rao. - """ + """Convert the Balance object to an int. The resulting value is in rao.""" return self.rao def __float__(self): - """ - Convert the Balance object to a float. The resulting value is in tao. - """ + """Convert the Balance object to a float. The resulting value is in tao.""" return self.tao def __str__(self): - """ - Returns the Balance object as a string in the format "symbolvalue", where the value is in tao. - """ + """Returns the Balance object as a string in the format "symbolvalue", where the value is in tao.""" return f"{self.unit}{float(self.tao):,.9f}" def __rich__(self): - return "[green]{}[/green][green]{}[/green][green].[/green][dim green]{}[/dim green]".format( - self.unit, - format(float(self.tao), "f").split(".")[0], - format(float(self.tao), "f").split(".")[1], - ) + int_tao, fract_tao = format(float(self.tao), "f").split(".") + return f"[green]{self.unit}[/green][green]{int_tao}[/green][green].[/green][dim green]{fract_tao}[/dim green]" def __str_rao__(self): return f"{self.rao_unit}{int(self.rao)}" @@ -226,12 +215,6 @@ def __rfloordiv__(self, other: Union[int, float, "Balance"]): except (ValueError, TypeError): raise NotImplementedError("Unsupported type") - def __int__(self) -> int: - return self.rao - - def __float__(self) -> float: - return self.tao - def __nonzero__(self) -> bool: return bool(self.rao) @@ -247,9 +230,9 @@ def __abs__(self): @staticmethod def from_float(amount: float): """ - Given tao (float), return Balance object with rao(int) and tao(float), where rao = int(tao*pow(10,9)) + Given tao, return :func:`Balance` object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) Args: - amount: The amount in tao. + amount (float): The amount in tao. Returns: A Balance object representing the given amount. @@ -260,10 +243,10 @@ def from_float(amount: float): @staticmethod def from_tao(amount: float): """ - Given tao (float), return Balance object with rao(int) and tao(float), where rao = int(tao*pow(10,9)) + Given tao, return Balance object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) Args: - amount: The amount in tao. + amount (float): The amount in tao. Returns: A Balance object representing the given amount. @@ -274,10 +257,10 @@ def from_tao(amount: float): @staticmethod def from_rao(amount: int): """ - Given rao (int), return Balance object with rao(int) and tao(float), where rao = int(tao*pow(10,9)) + Given rao, return Balance object with rao(``int``) and tao(``float``), where rao = int(tao*pow(10,9)) Args: - amount: The amount in rao. + amount (int): The amount in rao. Returns: A Balance object representing the given amount. diff --git a/bittensor/btlogging/__init__.py b/bittensor/utils/btlogging/__init__.py similarity index 85% rename from bittensor/btlogging/__init__.py rename to bittensor/utils/btlogging/__init__.py index 6bf6d2bf3..a5e6d2518 100644 --- a/bittensor/btlogging/__init__.py +++ b/bittensor/utils/btlogging/__init__.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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 @@ -18,11 +18,10 @@ """ btlogging sub-package standardized logging for Bittensor. -This module provides logging functionality for the Bittensor package. It includes custom loggers, handlers, and -formatters to ensure consistent logging throughout the project. +This module provides logging functionality for the Bittensor package. It includes custom loggers, handlers, and formatters to ensure consistent logging throughout the project. """ -from bittensor.btlogging.loggingmachine import LoggingMachine +from .loggingmachine import LoggingMachine logging = LoggingMachine(LoggingMachine.config()) diff --git a/bittensor/btlogging/defines.py b/bittensor/utils/btlogging/defines.py similarity index 96% rename from bittensor/btlogging/defines.py rename to bittensor/utils/btlogging/defines.py index c87177ffd..9e1dada25 100644 --- a/bittensor/btlogging/defines.py +++ b/bittensor/utils/btlogging/defines.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 OpenTensor Foundation - +# 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 diff --git a/bittensor/btlogging/format.py b/bittensor/utils/btlogging/format.py similarity index 89% rename from bittensor/btlogging/format.py rename to bittensor/utils/btlogging/format.py index 5f2c8cb86..1aa505c82 100644 --- a/bittensor/btlogging/format.py +++ b/bittensor/utils/btlogging/format.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 OpenTensor Foundation - +# 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 @@ -23,11 +23,9 @@ import logging import time -from typing import Dict - +from typing import Optional from colorama import init, Fore, Back, Style - init(autoreset=True) TRACE_LEVEL_NUM: int = 5 @@ -52,14 +50,14 @@ def _success(self, message: str, *args, **kws): logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") logging.Logger.trace = _trace -emoji_map: Dict[str, str] = { +emoji_map: dict[str, str] = { ":white_heavy_check_mark:": "✅", ":cross_mark:": "❌", ":satellite:": "🛰️", } -color_map: Dict[str, str] = { +color_map: dict[str, str] = { "": Fore.RED, "": Style.RESET_ALL, "": Fore.BLUE, @@ -69,7 +67,7 @@ def _success(self, message: str, *args, **kws): } -log_level_color_prefix: Dict[int, str] = { +log_level_color_prefix: dict[int, str] = { logging.NOTSET: Fore.RESET, logging.TRACE: Fore.MAGENTA, logging.DEBUG: Fore.BLUE, @@ -81,12 +79,12 @@ def _success(self, message: str, *args, **kws): } -LOG_FORMATS: Dict[int, str] = { +LOG_FORMATS: dict[int, str] = { level: f"{Fore.BLUE}%(asctime)s{Fore.RESET} | {Style.BRIGHT}{color}%(levelname)s\033[0m | %(message)s" for level, color in log_level_color_prefix.items() } -LOG_TRACE_FORMATS: Dict[int, str] = { +LOG_TRACE_FORMATS: dict[int, str] = { level: f"{Fore.BLUE}%(asctime)s{Fore.RESET}" f" | {Style.BRIGHT}{color}%(levelname)s{Fore.RESET}{Back.RESET}{Style.RESET_ALL}" f" | %(name)s:%(filename)s:%(lineno)s" @@ -117,13 +115,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.trace = False - def formatTime(self, record, datefmt=None) -> str: + def formatTime(self, record, datefmt: Optional[str] = None) -> str: """ Override formatTime to add milliseconds. Args: record (logging.LogRecord): The log record. - datefmt (str, optional): The date format string. + datefmt (Optional[str]): The date format string. Returns: s (str): The formatted time string with milliseconds. @@ -134,10 +132,10 @@ def formatTime(self, record, datefmt=None) -> str: s = time.strftime(datefmt, created) else: s = time.strftime("%Y-%m-%d %H:%M:%S", created) - s += ".{:03d}".format(int(record.msecs)) + s += f".{int(record.msecs):03d}" return s - def format(self, record) -> str: + def format(self, record: "logging.LogRecord") -> str: """ Override format to apply custom formatting including emojis and colors. @@ -188,13 +186,15 @@ class BtFileFormatter(logging.Formatter): centers the level name. """ - def formatTime(self, record, datefmt=None) -> str: + def formatTime( + self, record: "logging.LogRecord", datefmt: Optional[str] = None + ) -> str: """ Override formatTime to add milliseconds. Args: record (logging.LogRecord): The log record. - datefmt (str, optional): The date format string. + datefmt (Optional[str]): The date format string. Returns: s (str): The formatted time string with milliseconds. @@ -205,10 +205,10 @@ def formatTime(self, record, datefmt=None) -> str: s = time.strftime(datefmt, created) else: s = time.strftime("%Y-%m-%d %H:%M:%S", created) - s += ".{:03d}".format(int(record.msecs)) + s += f".{int(record.msecs):03d}" return s - def format(self, record) -> str: + def format(self, record: "logging.LogRecord") -> str: """ Override format to center the level name. diff --git a/bittensor/btlogging/helpers.py b/bittensor/utils/btlogging/helpers.py similarity index 97% rename from bittensor/btlogging/helpers.py rename to bittensor/utils/btlogging/helpers.py index 532c1f716..3fdca4ee0 100644 --- a/bittensor/btlogging/helpers.py +++ b/bittensor/utils/btlogging/helpers.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 OpenTensor Foundation - +# 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 @@ -23,7 +23,7 @@ from typing import Generator -def all_loggers() -> Generator[logging.Logger, None, None]: +def all_loggers() -> Generator["logging.Logger", None, None]: """Generator that yields all logger instances in the application. Iterates through the logging root manager's logger dictionary and yields all active `Logger` instances. It skips diff --git a/bittensor/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py similarity index 90% rename from bittensor/btlogging/loggingmachine.py rename to bittensor/utils/btlogging/loggingmachine.py index ef58cecdf..b2cfb2918 100644 --- a/bittensor/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 OpenTensor Foundation - +# 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 @@ -28,14 +28,14 @@ import multiprocessing as mp import os import sys -from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler from logging import Logger +from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler from typing import NamedTuple from statemachine import State, StateMachine -import bittensor.config -from bittensor.btlogging.defines import ( +from bittensor.core.config import Config +from .defines import ( BITTENSOR_LOGGER_NAME, DATE_FORMAT, DEFAULT_LOG_BACKUP_COUNT, @@ -43,8 +43,14 @@ DEFAULT_MAX_ROTATING_LOG_FILE_SIZE, TRACE_LOG_FORMAT, ) -from bittensor.btlogging.format import BtFileFormatter, BtStreamFormatter -from bittensor.btlogging.helpers import all_loggers +from .format import BtFileFormatter, BtStreamFormatter +from .helpers import all_loggers + + +def _concat_message(msg="", prefix="", suffix=""): + """Concatenates a message with optional prefix and suffix.""" + msg = f"{f'{prefix} - ' if prefix else ''}{msg}{f' - {suffix}' if suffix else ''}" + return msg class LoggingConfig(NamedTuple): @@ -90,7 +96,7 @@ class LoggingMachine(StateMachine, Logger): | Disabled.to(Disabled) ) - def __init__(self, config: bittensor.config, name: str = BITTENSOR_LOGGER_NAME): + def __init__(self, config: "Config", name: str = BITTENSOR_LOGGER_NAME): # basics super(LoggingMachine, self).__init__() self._queue = mp.Queue(-1) @@ -127,8 +133,15 @@ def _enable_initial_state(self, config): else: self.enable_default() - def _extract_logging_config(self, config) -> dict: - """Extract btlogging's config from bittensor config""" + def _extract_logging_config(self, config: "Config") -> dict: + """Extract btlogging's config from bittensor config + + Args: + config (bittensor.core.config.Config): Bittensor config instance. + + Returns: + (dict): btlogging's config from Bittensor config or Bittensor config. + """ if hasattr(config, "logging"): return config.logging else: @@ -154,8 +167,12 @@ def _configure_handlers(self, config) -> list[stdlogging.Handler]: def get_config(self): return self._config - def set_config(self, config): - """Set config after initialization, if desired.""" + def set_config(self, config: "Config"): + """Set config after initialization, if desired. + + Args: + config (bittensor.core.config.Config): Bittensor config instance. + """ self._config = config if config.logging_dir and config.record_log: expanded_dir = os.path.expanduser(config.logging_dir) @@ -186,7 +203,7 @@ def get_queue(self): """ return self._queue - def _initialize_bt_logger(self, name): + def _initialize_bt_logger(self, name: str): """ Initialize logging for bittensor. @@ -198,7 +215,7 @@ def _initialize_bt_logger(self, name): logger.addHandler(queue_handler) return logger - def _deinitialize_bt_logger(self, name): + def _deinitialize_bt_logger(self, name: str): """Find the logger by name and remove the queue handler associated with it.""" logger = stdlogging.getLogger(name) for handler in list(logger.handlers): @@ -223,6 +240,9 @@ def register_primary_logger(self, name: str): This adds a logger to the _primary_loggers set to ensure it doesn't get disabled when disabling third-party loggers. A queue handler is also associated with it. + + Args: + name (str): the name for primary logger. """ self._primary_loggers.add(name) self._initialize_bt_logger(name) @@ -233,6 +253,9 @@ def deregister_primary_logger(self, name: str): This function removes the logger from the _primary_loggers set and deinitializes its queue handler + + Args: + name (str): the name of primary logger. """ self._primary_loggers.remove(name) self._deinitialize_bt_logger(name) @@ -279,7 +302,7 @@ def after_transition(self, event, state): def before_enable_default(self): """Logs status before enable Default.""" self._logger.info(f"Enabling default logging.") - self._logger.setLevel(stdlogging.INFO) + self._logger.setLevel(stdlogging.WARNING) for logger in all_loggers(): if logger.name in self._primary_loggers: continue @@ -363,17 +386,17 @@ def __trace_on__(self) -> bool: def trace(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps trace message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" + msg = _concat_message(msg, prefix, suffix) self._logger.trace(msg, *args, **kwargs) def debug(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps debug message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" + msg = _concat_message(msg, prefix, suffix) self._logger.debug(msg, *args, **kwargs) def info(self, msg="", prefix="", suffix="", *args, **kwargs): """Wraps info message with prefix and suffix.""" - msg = f"{prefix} - {msg} - {suffix}" + msg = _concat_message(msg, prefix, suffix) self._logger.info(msg, *args, **kwargs) def success(self, msg="", prefix="", suffix="", *args, **kwargs): @@ -430,7 +453,7 @@ def get_level(self) -> int: """Returns Logging level.""" return self._logger.level - def check_config(self, config: bittensor.config): + def check_config(self, config: "Config"): assert config.logging def help(self): @@ -444,9 +467,9 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): default_logging_debug = os.getenv("BT_LOGGING_DEBUG") or False default_logging_trace = os.getenv("BT_LOGGING_TRACE") or False default_logging_record_log = os.getenv("BT_LOGGING_RECORD_LOG") or False - default_logging_logging_dir = ( - os.getenv("BT_LOGGING_LOGGING_DIR") or "~/.bittensor/miners" - ) + default_logging_logging_dir = os.getenv( + "BT_LOGGING_LOGGING_DIR" + ) or os.path.join("~", ".bittensor", "miners") parser.add_argument( "--" + prefix_str + "logging.debug", action="store_true", @@ -476,19 +499,19 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): pass @classmethod - def config(cls) -> bittensor.config: + def config(cls) -> "Config": """Get config from the argument parser. Return: - config (bittensor.config): config object + config (bittensor.core.config.Config): config object """ parser = argparse.ArgumentParser() cls.add_args(parser) - return bittensor.config(parser, args=[]) + return Config(parser, args=[]) def __call__( self, - config: bittensor.config = None, + config: "Config" = None, debug: bool = None, trace: bool = None, record_log: bool = None, diff --git a/bittensor/utils/deprecated.py b/bittensor/utils/deprecated.py new file mode 100644 index 000000000..6075a93d8 --- /dev/null +++ b/bittensor/utils/deprecated.py @@ -0,0 +1,150 @@ +# 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. + +""" +The Bittensor Compatibility Module is designed to ensure seamless integration and functionality with legacy versions of +the Bittensor framework, specifically up to and including version 7.3.0. This module addresses changes and deprecated +features in recent versions, allowing users to maintain compatibility with older systems and projects. +""" + +import importlib +import sys + +from bittensor_wallet.errors import KeyFileError # noqa: F401 +from bittensor_wallet.keyfile import ( # noqa: F401 + 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, +) +from bittensor_wallet.wallet import display_mnemonic_msg, Wallet # noqa: F401 +from substrateinterface import Keypair # noqa: F401 + +from bittensor.core import settings +from bittensor.core.axon import Axon +from bittensor.core.chain_data import ( # noqa: F401 + AxonInfo, + NeuronInfo, + NeuronInfoLite, + PrometheusInfo, + DelegateInfo, + StakeInfo, + SubnetInfo, + SubnetHyperparameters, + IPInfo, + ProposalCallData, + ProposalVoteData, +) +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 + BlacklistedException, + ChainConnectionError, + ChainError, + ChainQueryError, + ChainTransactionError, + IdentityError, + InternalServerError, + InvalidRequestNameError, + MetadataError, + NominationError, + NotDelegateError, + NotRegisteredError, + NotVerifiedException, + PostProcessException, + PriorityException, + RegistrationError, + RunException, + StakeError, + SynapseDendriteNoneException, + SynapseParsingError, + TransferError, + UnknownSynapseError, + UnstakeError, +) +from bittensor.core.metagraph import Metagraph +from bittensor.core.settings import BLOCKTIME +from bittensor.core.stream import StreamingSynapse # noqa: F401 +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 + ss58_to_vec_u8, + version_checking, + strtobool, + get_explorer_url_for_network, + ss58_address_to_bytes, + u16_normalized_float, + u64_normalized_float, + get_hash, +) +from bittensor.utils.balance import Balance as Balance # noqa: F401 +from bittensor.utils.mock.subtensor_mock import MockSubtensor as MockSubtensor # noqa: F401 +from bittensor.utils.subnets import SubnetsAPI # noqa: F401 + +# Backwards compatibility with previous bittensor versions. +axon = Axon +config = Config +dendrite = Dendrite +keyfile = Keyfile +metagraph = Metagraph +wallet = Wallet +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 + +# Makes the `bittensor.core.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. +extrinsics_subpackage = importlib.import_module("bittensor.core.extrinsics") +sys.modules["bittensor.extrinsics"] = extrinsics_subpackage diff --git a/bittensor/utils/formatting.py b/bittensor/utils/formatting.py deleted file mode 100644 index 22fbe74c1..000000000 --- a/bittensor/utils/formatting.py +++ /dev/null @@ -1,123 +0,0 @@ -import math -from typing import List - - -def get_human_readable(num, suffix="H"): - for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]: - if abs(num) < 1000.0: - return f"{num:3.1f}{unit}{suffix}" - num /= 1000.0 - return f"{num:.1f}Y{suffix}" - - -def millify(n: int): - millnames = ["", " K", " M", " B", " T"] - n = float(n) - millidx = max( - 0, - min( - len(millnames) - 1, int(math.floor(0 if n == 0 else math.log10(abs(n)) / 3)) - ), - ) - - return "{:.2f}{}".format(n / 10 ** (3 * millidx), millnames[millidx]) - - -def convert_blocks_to_time(blocks: int, block_time: int = 12) -> tuple[int, int, int]: - """ - Converts number of blocks into number of hours, minutes, seconds. - :param blocks: number of blocks - :param block_time: time per block, by default this is 12 - :return: tuple containing number of hours, number of minutes, number of seconds - """ - seconds = blocks * block_time - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - remaining_seconds = seconds % 60 - return hours, minutes, remaining_seconds - - -def float_to_u16(value: float) -> int: - # Ensure the input is within the expected range - if value is None: - return 0 - if not (0 <= value <= 1): - raise ValueError("Input value must be between 0 and 1") - - # Calculate the u16 representation - u16_max = 65535 - return int(value * u16_max) - - -def u16_to_float(value: int) -> float: - # Ensure the input is within the expected range - if value is None: - return 0.0 - if not (0 <= value <= 65535): - raise ValueError("Input value must be between 0 and 65535") - - # Calculate the float representation - u16_max = 65535 - return value / u16_max - - -def float_to_u64(value: float) -> int: - if value == 0.0: - return 0 - # Ensure the input is within the expected range - if not (0 <= value <= 1): - raise ValueError("Input value must be between 0 and 1") - - # Convert the float to a u64 value, take the floor value - return int(math.floor((value * (2**64 - 1)))) - - -def u64_to_float(value: int) -> float: - u64_max = 2**64 - 1 - # Allow for a small margin of error (e.g., 1) to account for potential rounding issues - if not (0 <= value <= u64_max + 1): - raise ValueError( - f"Input value ({value}) must be between 0 and {u64_max} (2^64 - 1)" - ) - return min(value / u64_max, 1.0) # Ensure the result is never greater than 1.0 - - -def normalize_u64_values(values: List[int]) -> List[int]: - """ - Normalize a list of u64 values so that their sum equals u64::MAX (2^64 - 1). - """ - if not values: - raise ValueError("Input list cannot be empty") - - if any(v < 0 for v in values): - raise ValueError("Input values must be non-negative") - - total = sum(values) - if total == 0: - raise ValueError("Sum of input values cannot be zero") - - u64_max = 2**64 - 1 - normalized = [int((v / total) * u64_max) for v in values] - - # Adjust values to ensure sum is exactly u64::MAX - current_sum = sum(normalized) - diff = u64_max - current_sum - - for i in range(abs(diff)): - if diff > 0: - normalized[i % len(normalized)] += 1 - else: - normalized[i % len(normalized)] = max( - 0, normalized[i % len(normalized)] - 1 - ) - - # Final check and adjustment - final_sum = sum(normalized) - if final_sum > u64_max: - normalized[-1] -= final_sum - u64_max - - assert ( - sum(normalized) == u64_max - ), f"Sum of normalized values ({sum(normalized)}) is not equal to u64::MAX ({u64_max})" - - return normalized diff --git a/bittensor/mock/__init__.py b/bittensor/utils/mock/__init__.py similarity index 95% rename from bittensor/mock/__init__.py rename to bittensor/utils/mock/__init__.py index b4f0efd5c..218579a15 100644 --- a/bittensor/mock/__init__.py +++ b/bittensor/utils/mock/__init__.py @@ -15,4 +15,4 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -from .subtensor_mock import MockSubtensor as MockSubtensor +from .subtensor_mock import MockSubtensor diff --git a/bittensor/mock/subtensor_mock.py b/bittensor/utils/mock/subtensor_mock.py similarity index 55% rename from bittensor/mock/subtensor_mock.py rename to bittensor/utils/mock/subtensor_mock.py index 5c2c3b42d..817be0843 100644 --- a/bittensor/mock/subtensor_mock.py +++ b/bittensor/utils/mock/subtensor_mock.py @@ -1,56 +1,46 @@ # The MIT License (MIT) -# Copyright © 2022-2023 Opentensor Foundation - +# 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. -from random import randint +from collections.abc import Mapping +from dataclasses import dataclass +from hashlib import sha256 from types import SimpleNamespace -from typing import Any, Dict, List, Optional, Tuple, TypedDict, Union +from typing import Any, Optional, Union, TypedDict from unittest.mock import MagicMock -from dataclasses import dataclass -from abc import abstractclassmethod -from collections.abc import Mapping -from hashlib import sha256 -from ..wallet import wallet +from bittensor_wallet import Wallet -from ..chain_data import ( +from bittensor.core.chain_data import ( NeuronInfo, NeuronInfoLite, PrometheusInfo, - DelegateInfo, - SubnetInfo, AxonInfo, ) -from ..errors import ChainQueryError -from ..subtensor import Subtensor -from ..utils import RAOPERTAO, U16_NORMALIZED_FLOAT -from ..utils.balance import Balance -from ..utils.registration import POWSolution - -from typing import TypedDict - +from bittensor.core.errors import ChainQueryError +from bittensor.core.subtensor import Subtensor +from bittensor.utils import RAOPERTAO, u16_normalized_float +from bittensor.utils.balance import Balance # Mock Testing Constant __GLOBAL_MOCK_STATE__ = {} class AxonServeCallParams(TypedDict): - """ - Axon serve chain call parameters. - """ + """Axon serve chain call parameters.""" version: int ip: int @@ -60,9 +50,7 @@ class AxonServeCallParams(TypedDict): class PrometheusServeCallParams(TypedDict): - """ - Prometheus serve chain call parameters. - """ + """Prometheus serve chain call parameters.""" version: int ip: int @@ -75,7 +63,7 @@ class PrometheusServeCallParams(TypedDict): class InfoDict(Mapping): - @abstractclassmethod + @classmethod def default(cls): raise NotImplementedError @@ -136,12 +124,12 @@ class MockSubtensorValue: class MockMapResult: - records: Optional[List[Tuple[MockSubtensorValue, MockSubtensorValue]]] + records: Optional[list[tuple[MockSubtensorValue, MockSubtensorValue]]] def __init__( self, records: Optional[ - List[Tuple[Union[Any, MockSubtensorValue], Union[Any, MockSubtensorValue]]] + list[tuple[Union[Any, MockSubtensorValue], Union[Any, MockSubtensorValue]]] ] = None, ): _records = [ @@ -170,25 +158,25 @@ def __iter__(self): class MockSystemState(TypedDict): - Account: Dict[str, Dict[int, int]] # address -> block -> balance + Account: dict[str, dict[int, int]] # address -> block -> balance class MockSubtensorState(TypedDict): - Rho: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> rho - Kappa: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> kappa - Difficulty: Dict[int, Dict[BlockNumber, int]] # netuid -> block -> difficulty - ImmunityPeriod: Dict[ - int, Dict[BlockNumber, int] + Rho: dict[int, dict[BlockNumber, int]] # netuid -> block -> rho + Kappa: dict[int, dict[BlockNumber, int]] # netuid -> block -> kappa + Difficulty: dict[int, dict[BlockNumber, int]] # netuid -> block -> difficulty + ImmunityPeriod: dict[ + int, dict[BlockNumber, int] ] # netuid -> block -> immunity_period - ValidatorBatchSize: Dict[ - int, Dict[BlockNumber, int] + ValidatorBatchSize: dict[ + int, dict[BlockNumber, int] ] # netuid -> block -> validator_batch_size - Active: Dict[int, Dict[BlockNumber, bool]] # (netuid, uid), block -> active - Stake: Dict[str, Dict[str, Dict[int, int]]] # (hotkey, coldkey) -> block -> stake + Active: dict[int, dict[BlockNumber, bool]] # (netuid, uid), block -> active + Stake: dict[str, dict[str, dict[int, int]]] # (hotkey, coldkey) -> block -> stake - Delegates: Dict[str, Dict[int, float]] # address -> block -> delegate_take + Delegates: dict[str, dict[int, float]] # address -> block -> delegate_take - NetworksAdded: Dict[int, Dict[BlockNumber, bool]] # netuid -> block -> added + NetworksAdded: dict[int, dict[BlockNumber, bool]] # netuid -> block -> added class MockChainState(TypedDict): @@ -278,10 +266,11 @@ def setup(self) -> None: self.block_number = 0 self.network = "mock" - self.chain_endpoint = "mock_endpoint" + self.chain_endpoint = "ws://mock_endpoint.bt" self.substrate = MagicMock() def __init__(self, *args, **kwargs) -> None: + super().__init__() self.__dict__ = __GLOBAL_MOCK_STATE__ if not hasattr(self, "chain_state") or getattr(self, "chain_state") is None: @@ -382,104 +371,6 @@ 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): @@ -490,43 +381,12 @@ 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]]: + ) -> tuple[bool, Optional[str]]: """ Returns: - Tuple[bool, Optional[str]]: (success, err_msg) + tuple[bool, Optional[str]]: (success, err_msg) """ balance = self._convert_to_balance(balance) @@ -568,7 +428,7 @@ def do_block_step(self) -> None: + 1 ) - def _handle_type_default(self, name: str, params: List[object]) -> object: + def _handle_type_default(self, name: str, params: list[object]) -> object: defaults_mapping = { "TotalStake": 0, "TotalHotkeyStake": 0, @@ -578,7 +438,7 @@ def _handle_type_default(self, name: str, params: List[object]) -> object: return defaults_mapping.get(name, None) - def commit(self, wallet: "wallet", netuid: int, data: str) -> None: + def commit(self, wallet: "Wallet", netuid: int, data: str) -> None: uid = self.get_uid_for_hotkey_on_subnet( hotkey_ss58=wallet.hotkey.ss58_address, netuid=netuid, @@ -600,7 +460,7 @@ def query_subtensor( self, name: str, block: Optional[int] = None, - params: Optional[List[object]] = [], + params: Optional[list[object]] = [], ) -> MockSubtensorValue: if block: if self.block_number < block: @@ -636,7 +496,7 @@ def query_map_subtensor( self, name: str, block: Optional[int] = None, - params: Optional[List[object]] = [], + params: Optional[list[object]] = [], ) -> Optional[MockMapResult]: """ Note: Double map requires one param @@ -743,13 +603,6 @@ def get_balance(self, address: str, block: int = None) -> "Balance": else: return Balance(0) - def get_balances(self, block: int = None) -> Dict[str, "Balance"]: - balances = {} - for address in self.chain_state["System"]["Account"]: - balances[address] = self.get_balance(address, block) - - return balances - # ==== Neuron RPC methods ==== def neuron_for_uid( @@ -775,7 +628,7 @@ def neuron_for_uid( else: return neuron_info - def neurons(self, netuid: int, block: Optional[int] = None) -> List[NeuronInfo]: + def neurons(self, netuid: int, block: Optional[int] = None) -> list[NeuronInfo]: if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: raise Exception("Subnet does not exist") @@ -792,7 +645,7 @@ def neurons(self, netuid: int, block: Optional[int] = None) -> List[NeuronInfo]: @staticmethod def _get_most_recent_storage( - storage: Dict[BlockNumber, Any], block_number: Optional[int] = None + storage: dict[BlockNumber, Any], block_number: Optional[int] = None ) -> Any: if block_number is None: items = list(storage.items()) @@ -921,13 +774,13 @@ def _neuron_subnet_exists( weights = [[int(weight[0]), int(weight[1])] for weight in weights] bonds = [[int(bond[0]), int(bond[1])] for bond in bonds] - rank = U16_NORMALIZED_FLOAT(rank) + rank = u16_normalized_float(rank) emission = emission / RAOPERTAO - incentive = U16_NORMALIZED_FLOAT(incentive) - consensus = U16_NORMALIZED_FLOAT(consensus) - trust = U16_NORMALIZED_FLOAT(trust) - validator_trust = U16_NORMALIZED_FLOAT(validator_trust) - dividends = U16_NORMALIZED_FLOAT(dividends) + incentive = u16_normalized_float(incentive) + consensus = u16_normalized_float(consensus) + trust = u16_normalized_float(trust) + validator_trust = u16_normalized_float(validator_trust) + dividends = u16_normalized_float(dividends) prometheus_info = PrometheusInfo.fix_decoded_values(prometheus_info) axon_info_ = AxonInfo.from_neuron_info( {"hotkey": hotkey, "coldkey": coldkey, "axon_info": axon_info_} @@ -961,35 +814,9 @@ def _neuron_subnet_exists( return neuron_info - def neuron_for_uid_lite( - self, uid: int, netuid: int, block: Optional[int] = None - ) -> Optional[NeuronInfoLite]: - if block: - if self.block_number < block: - raise Exception("Cannot query block in the future") - - else: - block = self.block_number - - if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: - raise Exception("Subnet does not exist") - - neuron_info = self._neuron_subnet_exists(uid, netuid, block) - if neuron_info is None: - return None - - else: - neuron_info_dict = neuron_info.__dict__ - del neuron_info - del neuron_info_dict["weights"] - del neuron_info_dict["bonds"] - - neuron_info_lite = NeuronInfoLite(**neuron_info_dict) - return neuron_info_lite - def neurons_lite( self, netuid: int, block: Optional[int] = None - ) -> List[NeuronInfoLite]: + ) -> list[NeuronInfoLite]: if netuid not in self.chain_state["SubtensorModule"]["NetworksAdded"]: raise Exception("Subnet does not exist") @@ -1004,85 +831,19 @@ def neurons_lite( return neurons - # Extrinsics - def _do_delegation( - self, - wallet: "wallet", - delegate_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - # Check if delegate - if not self.is_hotkey_delegate(hotkey_ss58=delegate_ss58): - raise Exception("Not a delegate") - - # do stake - success = self._do_stake( - wallet=wallet, - hotkey_ss58=delegate_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - return success - - def _do_undelegation( - self, - wallet: "wallet", - delegate_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - # Check if delegate - if not self.is_hotkey_delegate(hotkey_ss58=delegate_ss58): - raise Exception("Not a delegate") - - # do unstake - self._do_unstake( - wallet=wallet, - hotkey_ss58=delegate_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - def _do_nominate( - self, - wallet: "wallet", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - hotkey_ss58 = wallet.hotkey.ss58_address - coldkey_ss58 = wallet.coldkeypub.ss58_address - - subtensor_state = self.chain_state["SubtensorModule"] - if self.is_hotkey_delegate(hotkey_ss58=hotkey_ss58): - return True - - else: - subtensor_state["Delegates"][hotkey_ss58] = {} - subtensor_state["Delegates"][hotkey_ss58][self.block_number] = ( - 0.18 # Constant for now - ) - - return True - def get_transfer_fee( - self, wallet: "wallet", dest: str, value: Union["Balance", float, int] + self, wallet: "Wallet", dest: str, value: Union["Balance", float, int] ) -> "Balance": return Balance(700) - def _do_transfer( + def do_transfer( self, - wallet: "wallet", + wallet: "Wallet", dest: str, transfer_balance: "Balance", wait_for_inclusion: bool = True, wait_for_finalization: bool = False, - ) -> Tuple[bool, Optional[str], Optional[str]]: + ) -> tuple[bool, Optional[str], Optional[str]]: bal = self.get_balance(wallet.coldkeypub.ss58_address) dest_bal = self.get_balance(dest) transfer_fee = self.get_transfer_fee(wallet, dest, transfer_balance) @@ -1107,204 +868,6 @@ def _do_transfer( return True, None, None - def _do_pow_register( - self, - netuid: int, - wallet: "wallet", - pow_result: "POWSolution", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - # Assume pow result is valid - - subtensor_state = self.chain_state["SubtensorModule"] - if netuid not in subtensor_state["NetworksAdded"]: - raise Exception("Subnet does not exist") - - self._register_neuron( - netuid=netuid, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - return True, None - - def _do_burned_register( - self, - netuid: int, - wallet: "wallet", - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: - subtensor_state = self.chain_state["SubtensorModule"] - if netuid not in subtensor_state["NetworksAdded"]: - raise Exception("Subnet does not exist") - - bal = self.get_balance(wallet.coldkeypub.ss58_address) - burn = self.recycle(netuid=netuid) - existential_deposit = self.get_existential_deposit() - - if bal < burn + existential_deposit: - raise Exception("Insufficient funds") - - self._register_neuron( - netuid=netuid, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - # Burn the funds - self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"][ - "free" - ][self.block_number] = (bal - burn).rao - - return True, None - - def _do_stake( - self, - wallet: "wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - subtensor_state = self.chain_state["SubtensorModule"] - - bal = self.get_balance(wallet.coldkeypub.ss58_address) - curr_stake = self.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address - ) - if curr_stake is None: - curr_stake = Balance(0) - existential_deposit = self.get_existential_deposit() - - if bal < amount + existential_deposit: - raise Exception("Insufficient funds") - - stake_state = subtensor_state["Stake"] - - # Stake the funds - if not hotkey_ss58 in stake_state: - stake_state[hotkey_ss58] = {} - if not wallet.coldkeypub.ss58_address in stake_state[hotkey_ss58]: - stake_state[hotkey_ss58][wallet.coldkeypub.ss58_address] = {} - - stake_state[hotkey_ss58][wallet.coldkeypub.ss58_address][self.block_number] = ( - amount.rao - ) - - # Add to total_stake storage - subtensor_state["TotalStake"][self.block_number] = ( - self._get_most_recent_storage(subtensor_state["TotalStake"]) + amount.rao - ) - - total_hotkey_stake_state = subtensor_state["TotalHotkeyStake"] - if not hotkey_ss58 in total_hotkey_stake_state: - total_hotkey_stake_state[hotkey_ss58] = {} - - total_coldkey_stake_state = subtensor_state["TotalColdkeyStake"] - if not wallet.coldkeypub.ss58_address in total_coldkey_stake_state: - total_coldkey_stake_state[wallet.coldkeypub.ss58_address] = {} - - curr_total_hotkey_stake = self.query_subtensor( - name="TotalHotkeyStake", - params=[hotkey_ss58], - block=min(self.block_number - 1, 0), - ) - curr_total_coldkey_stake = self.query_subtensor( - name="TotalColdkeyStake", - params=[wallet.coldkeypub.ss58_address], - block=min(self.block_number - 1, 0), - ) - - total_hotkey_stake_state[hotkey_ss58][self.block_number] = ( - curr_total_hotkey_stake.value + amount.rao - ) - total_coldkey_stake_state[wallet.coldkeypub.ss58_address][self.block_number] = ( - curr_total_coldkey_stake.value + amount.rao - ) - - # Remove from free balance - self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"][ - "free" - ][self.block_number] = (bal - amount).rao - - return True - - def _do_unstake( - self, - wallet: "wallet", - hotkey_ss58: str, - amount: "Balance", - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - ) -> bool: - subtensor_state = self.chain_state["SubtensorModule"] - - bal = self.get_balance(wallet.coldkeypub.ss58_address) - curr_stake = self.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address - ) - if curr_stake is None: - curr_stake = Balance(0) - - if curr_stake < amount: - raise Exception("Insufficient funds") - - stake_state = subtensor_state["Stake"] - - if curr_stake.rao == 0: - return True - - # Unstake the funds - # We know that the hotkey has stake, so we can just remove it - stake_state[hotkey_ss58][wallet.coldkeypub.ss58_address][self.block_number] = ( - curr_stake - amount - ).rao - # Add to the free balance - if wallet.coldkeypub.ss58_address not in self.chain_state["System"]["Account"]: - self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address] = { - "data": {"free": {}} - } - - # Remove from total stake storage - subtensor_state["TotalStake"][self.block_number] = ( - self._get_most_recent_storage(subtensor_state["TotalStake"]) - amount.rao - ) - - total_hotkey_stake_state = subtensor_state["TotalHotkeyStake"] - if not hotkey_ss58 in total_hotkey_stake_state: - total_hotkey_stake_state[hotkey_ss58] = {} - total_hotkey_stake_state[hotkey_ss58][self.block_number] = ( - 0 # Shouldn't happen - ) - - total_coldkey_stake_state = subtensor_state["TotalColdkeyStake"] - if not wallet.coldkeypub.ss58_address in total_coldkey_stake_state: - total_coldkey_stake_state[wallet.coldkeypub.ss58_address] = {} - total_coldkey_stake_state[wallet.coldkeypub.ss58_address][ - self.block_number - ] = amount.rao # Shouldn't happen - - total_hotkey_stake_state[hotkey_ss58][self.block_number] = ( - self._get_most_recent_storage( - subtensor_state["TotalHotkeyStake"][hotkey_ss58] - ) - - amount.rao - ) - total_coldkey_stake_state[wallet.coldkeypub.ss58_address][self.block_number] = ( - self._get_most_recent_storage( - subtensor_state["TotalColdkeyStake"][wallet.coldkeypub.ss58_address] - ) - - amount.rao - ) - - self.chain_state["System"]["Account"][wallet.coldkeypub.ss58_address]["data"][ - "free" - ][self.block_number] = (bal + amount).rao - - return True - @staticmethod def min_required_stake(): """ @@ -1314,156 +877,32 @@ def min_required_stake(): # valid minimum threshold as of 2024/05/01 return 100_000_000 # RAO - def get_minimum_required_stake(self): - return Balance.from_rao(self.min_required_stake()) - - def get_delegate_by_hotkey( - self, hotkey_ss58: str, block: Optional[int] = None - ) -> Optional["DelegateInfo"]: - subtensor_state = self.chain_state["SubtensorModule"] - - if hotkey_ss58 not in subtensor_state["Delegates"]: - return None - - newest_state = self._get_most_recent_storage( - subtensor_state["Delegates"][hotkey_ss58], block - ) - if newest_state is None: - return None - - nom_result = [] - nominators = subtensor_state["Stake"][hotkey_ss58] - for nominator in nominators: - nom_amount = self.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey_ss58, coldkey_ss58=nominator, block=block - ) - if nom_amount is not None and nom_amount.rao > 0: - nom_result.append((nominator, nom_amount)) - - registered_subnets = [] - for subnet in self.get_all_subnet_netuids(block=block): - uid = self.get_uid_for_hotkey_on_subnet( - hotkey_ss58=hotkey_ss58, netuid=subnet, block=block - ) - - if uid is not None: - registered_subnets.append((subnet, uid)) - - info = DelegateInfo( - hotkey_ss58=hotkey_ss58, - total_stake=self.get_total_stake_for_hotkey(ss58_address=hotkey_ss58) - or Balance(0), - nominators=nom_result, - owner_ss58=self.get_hotkey_owner(hotkey_ss58=hotkey_ss58, block=block), - take=0.18, - validator_permits=[ - subnet - for subnet, uid in registered_subnets - if self.neuron_has_validator_permit(uid=uid, netuid=subnet, block=block) - ], - registrations=[subnet for subnet, _ in registered_subnets], - return_per_1000=Balance.from_tao(1234567), # Doesn't matter for mock? - total_daily_return=Balance.from_tao(1234567), # Doesn't matter for mock? - ) - - return info - - def get_delegates(self, block: Optional[int] = None) -> List["DelegateInfo"]: - subtensor_state = self.chain_state["SubtensorModule"] - delegates_info = [] - for hotkey in subtensor_state["Delegates"]: - info = self.get_delegate_by_hotkey(hotkey_ss58=hotkey, block=block) - if info is not None: - delegates_info.append(info) - - return delegates_info - - def get_delegated( - self, coldkey_ss58: str, block: Optional[int] = None - ) -> List[Tuple["DelegateInfo", "Balance"]]: - """Returns the list of delegates that a given coldkey is staked to.""" - delegates = self.get_delegates(block=block) - - result = [] - for delegate in delegates: - if coldkey_ss58 in delegate.nominators: - result.append((delegate, delegate.nominators[coldkey_ss58])) - - return result - - def get_all_subnets_info(self, block: Optional[int] = None) -> List[SubnetInfo]: - subtensor_state = self.chain_state["SubtensorModule"] - result = [] - for subnet in subtensor_state["NetworksAdded"]: - info = self.get_subnet_info(netuid=subnet, block=block) - if info is not None: - result.append(info) - - return result - - def get_subnet_info( - self, netuid: int, block: Optional[int] = None - ) -> Optional[SubnetInfo]: - if not self.subnet_exists(netuid=netuid, block=block): - return None - - def query_subnet_info(name: str) -> Optional[object]: - return self.query_subtensor(name=name, block=block, params=[netuid]).value - - info = SubnetInfo( - netuid=netuid, - rho=query_subnet_info(name="Rho"), - kappa=query_subnet_info(name="Kappa"), - difficulty=query_subnet_info(name="Difficulty"), - immunity_period=query_subnet_info(name="ImmunityPeriod"), - max_allowed_validators=query_subnet_info(name="MaxAllowedValidators"), - min_allowed_weights=query_subnet_info(name="MinAllowedWeights"), - max_weight_limit=query_subnet_info(name="MaxWeightLimit"), - scaling_law_power=query_subnet_info(name="ScalingLawPower"), - subnetwork_n=query_subnet_info(name="SubnetworkN"), - max_n=query_subnet_info(name="MaxAllowedUids"), - blocks_since_epoch=query_subnet_info(name="BlocksSinceLastStep"), - tempo=query_subnet_info(name="Tempo"), - modality=query_subnet_info(name="NetworkModality"), - connection_requirements={ - str(netuid_.value): percentile.value - for netuid_, percentile in self.query_map_subtensor( - name="NetworkConnect", block=block, params=[netuid] - ).records - }, - emission_value=query_subnet_info(name="EmissionValues"), - burn=query_subnet_info(name="Burn"), - owner_ss58=query_subnet_info(name="SubnetOwner"), - ) - - return info - - def _do_serve_prometheus( + def do_serve_prometheus( self, - wallet: "wallet", + wallet: "Wallet", call_params: "PrometheusServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: + ) -> tuple[bool, Optional[str]]: return True, None - def _do_set_weights( + def do_set_weights( self, - wallet: "wallet", + wallet: "Wallet", netuid: int, uids: int, - vals: List[int], + vals: list[int], version_key: int, wait_for_inclusion: bool = False, wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: + ) -> tuple[bool, Optional[str]]: return True, None - def _do_serve_axon( + def do_serve_axon( self, - wallet: "wallet", + wallet: "Wallet", call_params: "AxonServeCallParams", wait_for_inclusion: bool = False, wait_for_finalization: bool = True, - ) -> Tuple[bool, Optional[str]]: + ) -> tuple[bool, Optional[str]]: return True, None diff --git a/bittensor/utils/networking.py b/bittensor/utils/networking.py index f4b729fe9..4c0c47585 100644 --- a/bittensor/utils/networking.py +++ b/bittensor/utils/networking.py @@ -1,55 +1,53 @@ -"""Utils for handling local network with ip and ports.""" - # The MIT License (MIT) -# Copyright © 2021-2022 Yuma Rao -# Copyright © 2022-2023 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies - +# 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. +"""Utils for handling local network with ip and ports.""" + import json import os import socket import urllib from functools import wraps +from typing import Optional import netaddr import requests -from bittensor.btlogging import logging +from bittensor.utils.btlogging import logging def int_to_ip(int_val: int) -> str: - r"""Maps an integer to a unique ip-string + """Maps an integer to a unique ip-string Args: - int_val (:type:`int128`, `required`): + int_val (int): The integer representation of an ip. Must be in the range (0, 3.4028237e+38). Returns: - str_val (:tyep:`str`, `required): + str_val (str): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 Raises: - netaddr.core.AddrFormatError (Exception): - Raised when the passed int_vals is not a valid ip int value. + netaddr.core.AddrFormatError (Exception): Raised when the passed int_vals is not a valid ip int value. """ return str(netaddr.IPAddress(int_val)) def ip_to_int(str_val: str) -> int: - r"""Maps an ip-string to a unique integer. + """Maps an ip-string to a unique integer. arg: str_val (:tyep:`str`, `required): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 @@ -66,7 +64,7 @@ def ip_to_int(str_val: str) -> int: def ip_version(str_val: str) -> int: - r"""Returns the ip version (IPV4 or IPV6). + """Returns the ip version (IPV4 or IPV6). arg: str_val (:tyep:`str`, `required): The string representation of an ip. Of form *.*.*.* for ipv4 or *::*:*:*:* for ipv6 @@ -92,7 +90,7 @@ class ExternalIPNotFound(Exception): def get_external_ip() -> str: - r"""Checks CURL/URLLIB/IPIFY/AWS for your external ip. + """Checks CURL/URLLIB/IPIFY/AWS for your external ip. Returns: external_ip (:obj:`str` `required`): Your routers external facing ip as a string. @@ -158,19 +156,21 @@ def get_external_ip() -> str: raise ExternalIPNotFound -def get_formatted_ws_endpoint_url(endpoint_url: str) -> str: +def get_formatted_ws_endpoint_url(endpoint_url: Optional[str]) -> Optional[str]: """ Returns a formatted websocket endpoint url. Note: The port (or lack thereof) is left unchanged Args: - endpoint_url (str, `required`): + endpoint_url (Optional[str]): The endpoint url to format. Returns: - formatted_endpoint_url (str, `required`): - The formatted endpoint url. In the form of ws:// or wss:// + formatted_endpoint_url (Optional[str]): The formatted endpoint url. In the form of ws:// or wss:// """ + if endpoint_url is None: + return None + if endpoint_url[0:6] != "wss://" and endpoint_url[0:5] != "ws://": - endpoint_url = "ws://{}".format(endpoint_url) + endpoint_url = f"ws://{endpoint_url}" return endpoint_url @@ -180,6 +180,7 @@ def ensure_connected(func): @wraps(func) def wrapper(self, *args, **kwargs): + """Wrapper function where `self` argument is Subtensor instance with the substrate connection.""" # Check the socket state before method execution if ( # connection was closed correctly diff --git a/bittensor/utils/registration.py b/bittensor/utils/registration.py index d606929dc..4d0cdb93d 100644 --- a/bittensor/utils/registration.py +++ b/bittensor/utils/registration.py @@ -1,28 +1,27 @@ -import binascii +# 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 functools -import hashlib -import math -import multiprocessing -import multiprocessing.queues # this must be imported separately, or could break type annotations import os -import random -import time -import typing -from dataclasses import dataclass -from datetime import timedelta -from queue import Empty, Full -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING -import backoff import numpy -import bittensor -from Crypto.Hash import keccak -from rich import console as rich_console -from rich import status as rich_status - -from .formatting import get_human_readable, millify -from ._register_cuda import solve_cuda +from bittensor.utils.btlogging import logging def use_torch() -> bool: @@ -35,11 +34,10 @@ def legacy_torch_api_compat(func): Convert function operating on numpy Input&Output to legacy torch Input&Output API if `use_torch()` is True. Args: - func (function): - Function with numpy Input/Output to be decorated. + func (function): Function with numpy Input/Output to be decorated. + Returns: - decorated (function): - Decorated function. + decorated (function): Decorated function. """ @functools.wraps(func) @@ -74,7 +72,7 @@ def _get_real_torch(): def log_no_torch_error(): - bittensor.logging.error( + logging.error( "This command requires torch. You can install torch for bittensor" ' with `pip install bittensor[torch]` or `pip install ".[torch]"`' " if installing from source, and then run the command with USE_TORCH=1 {command}" @@ -82,6 +80,8 @@ def log_no_torch_error(): class LazyLoadedTorch: + """A lazy-loading proxy for the torch module.""" + def __bool__(self): return bool(_get_real_torch()) @@ -93,1078 +93,7 @@ def __getattr__(self, name): raise ImportError("torch not installed") -if typing.TYPE_CHECKING: +if TYPE_CHECKING: import torch else: torch = LazyLoadedTorch() - - -class CUDAException(Exception): - """An exception raised when an error occurs in the CUDA environment.""" - - pass - - -def _hex_bytes_to_u8_list(hex_bytes: bytes): - hex_chunks = [int(hex_bytes[i : i + 2], 16) for i in range(0, len(hex_bytes), 2)] - return hex_chunks - - -def _create_seal_hash(block_and_hotkey_hash_bytes: bytes, nonce: int) -> bytes: - nonce_bytes = binascii.hexlify(nonce.to_bytes(8, "little")) - pre_seal = nonce_bytes + binascii.hexlify(block_and_hotkey_hash_bytes)[:64] - seal_sh256 = hashlib.sha256(bytearray(_hex_bytes_to_u8_list(pre_seal))).digest() - kec = keccak.new(digest_bits=256) - seal = kec.update(seal_sh256).digest() - return seal - - -def _seal_meets_difficulty(seal: bytes, difficulty: int, limit: int): - seal_number = int.from_bytes(seal, "big") - product = seal_number * difficulty - return product < limit - - -@dataclass -class POWSolution: - """A solution to the registration PoW problem.""" - - nonce: int - block_number: int - difficulty: int - seal: bytes - - def is_stale(self, subtensor: "bittensor.subtensor") -> bool: - """Returns True if the POW is stale. - This means the block the POW is solved for is within 3 blocks of the current block. - """ - return self.block_number < subtensor.get_current_block() - 3 - - -class _SolverBase(multiprocessing.Process): - """ - A process that solves the registration PoW problem. - - Args: - proc_num: int - The number of the process being created. - num_proc: int - The total number of processes running. - update_interval: int - The number of nonces to try to solve before checking for a new block. - finished_queue: multiprocessing.Queue - The queue to put the process number when a process finishes each update_interval. - Used for calculating the average time per update_interval across all processes. - solution_queue: multiprocessing.Queue - The queue to put the solution the process has found during the pow solve. - newBlockEvent: multiprocessing.Event - The event to set by the main process when a new block is finalized in the network. - The solver process will check for the event after each update_interval. - The solver process will get the new block hash and difficulty and start solving for a new nonce. - stopEvent: multiprocessing.Event - The event to set by the main process when all the solver processes should stop. - The solver process will check for the event after each update_interval. - The solver process will stop when the event is set. - Used to stop the solver processes when a solution is found. - curr_block: multiprocessing.Array - The array containing this process's current block hash. - The main process will set the array to the new block hash when a new block is finalized in the network. - The solver process will get the new block hash from this array when newBlockEvent is set. - curr_block_num: multiprocessing.Value - The value containing this process's current block number. - The main process will set the value to the new block number when a new block is finalized in the network. - The solver process will get the new block number from this value when newBlockEvent is set. - curr_diff: multiprocessing.Array - The array containing this process's current difficulty. - The main process will set the array to the new difficulty when a new block is finalized in the network. - The solver process will get the new difficulty from this array when newBlockEvent is set. - check_block: multiprocessing.Lock - The lock to prevent this process from getting the new block data while the main process is updating the data. - limit: int - The limit of the pow solve for a valid solution. - """ - - proc_num: int - num_proc: int - update_interval: int - finished_queue: multiprocessing.Queue - solution_queue: multiprocessing.Queue - newBlockEvent: multiprocessing.Event - stopEvent: multiprocessing.Event - hotkey_bytes: bytes - curr_block: multiprocessing.Array - curr_block_num: multiprocessing.Value - curr_diff: multiprocessing.Array - check_block: multiprocessing.Lock - limit: int - - def __init__( - self, - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ): - multiprocessing.Process.__init__(self, daemon=True) - self.proc_num = proc_num - self.num_proc = num_proc - self.update_interval = update_interval - self.finished_queue = finished_queue - self.solution_queue = solution_queue - self.newBlockEvent = multiprocessing.Event() - self.newBlockEvent.clear() - self.curr_block = curr_block - self.curr_block_num = curr_block_num - self.curr_diff = curr_diff - self.check_block = check_block - self.stopEvent = stopEvent - self.limit = limit - - def run(self): - raise NotImplementedError("_SolverBase is an abstract class") - - @staticmethod - def create_shared_memory() -> ( - Tuple[multiprocessing.Array, multiprocessing.Value, multiprocessing.Array] - ): - """Creates shared memory for the solver processes to use.""" - curr_block = multiprocessing.Array("h", 32, lock=True) # byte array - curr_block_num = multiprocessing.Value("i", 0, lock=True) # int - curr_diff = multiprocessing.Array("Q", [0, 0], lock=True) # [high, low] - - return curr_block, curr_block_num, curr_diff - - -class _Solver(_SolverBase): - def run(self): - block_number: int - block_and_hotkey_hash_bytes: bytes - block_difficulty: int - nonce_limit = int(math.pow(2, 64)) - 1 - - # Start at random nonce - nonce_start = random.randint(0, nonce_limit) - nonce_end = nonce_start + self.update_interval - while not self.stopEvent.is_set(): - if self.newBlockEvent.is_set(): - with self.check_block: - block_number = self.curr_block_num.value - block_and_hotkey_hash_bytes = bytes(self.curr_block) - block_difficulty = _registration_diff_unpack(self.curr_diff) - - self.newBlockEvent.clear() - - # Do a block of nonces - solution = _solve_for_nonce_block( - nonce_start, - nonce_end, - block_and_hotkey_hash_bytes, - block_difficulty, - self.limit, - block_number, - ) - if solution is not None: - self.solution_queue.put(solution) - - try: - # Send time - self.finished_queue.put_nowait(self.proc_num) - except Full: - pass - - nonce_start = random.randint(0, nonce_limit) - nonce_start = nonce_start % nonce_limit - nonce_end = nonce_start + self.update_interval - - -class _CUDASolver(_SolverBase): - dev_id: int - tpb: int - - def __init__( - self, - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id: int, - tpb: int, - ): - super().__init__( - proc_num, - num_proc, - update_interval, - finished_queue, - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - self.dev_id = dev_id - self.tpb = tpb - - def run(self): - block_number: int = 0 # dummy value - block_and_hotkey_hash_bytes: bytes = b"0" * 32 # dummy value - block_difficulty: int = int(math.pow(2, 64)) - 1 # dummy value - nonce_limit = int(math.pow(2, 64)) - 1 # U64MAX - - # Start at random nonce - nonce_start = random.randint(0, nonce_limit) - while not self.stopEvent.is_set(): - if self.newBlockEvent.is_set(): - with self.check_block: - block_number = self.curr_block_num.value - block_and_hotkey_hash_bytes = bytes(self.curr_block) - block_difficulty = _registration_diff_unpack(self.curr_diff) - - self.newBlockEvent.clear() - - # Do a block of nonces - solution = _solve_for_nonce_block_cuda( - nonce_start, - self.update_interval, - block_and_hotkey_hash_bytes, - block_difficulty, - self.limit, - block_number, - self.dev_id, - self.tpb, - ) - if solution is not None: - self.solution_queue.put(solution) - - try: - # Signal that a nonce_block was finished using queue - # send our proc_num - self.finished_queue.put(self.proc_num) - except Full: - pass - - # increase nonce by number of nonces processed - nonce_start += self.update_interval * self.tpb - nonce_start = nonce_start % nonce_limit - - -def _solve_for_nonce_block_cuda( - nonce_start: int, - update_interval: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - block_number: int, - dev_id: int, - tpb: int, -) -> Optional[POWSolution]: - """Tries to solve the POW on a CUDA device for a block of nonces (nonce_start, nonce_start + update_interval * tpb""" - solution, seal = solve_cuda( - nonce_start, - update_interval, - tpb, - block_and_hotkey_hash_bytes, - difficulty, - limit, - dev_id, - ) - - if solution != -1: - # Check if solution is valid (i.e. not -1) - return POWSolution(solution, block_number, difficulty, seal) - - return None - - -def _solve_for_nonce_block( - nonce_start: int, - nonce_end: int, - block_and_hotkey_hash_bytes: bytes, - difficulty: int, - limit: int, - block_number: int, -) -> Optional[POWSolution]: - """Tries to solve the POW for a block of nonces (nonce_start, nonce_end)""" - for nonce in range(nonce_start, nonce_end): - # Create seal. - seal = _create_seal_hash(block_and_hotkey_hash_bytes, nonce) - - # Check if seal meets difficulty - if _seal_meets_difficulty(seal, difficulty, limit): - # Found a solution, save it. - return POWSolution(nonce, block_number, difficulty, seal) - - return None - - -def _registration_diff_unpack(packed_diff: multiprocessing.Array) -> int: - """Unpacks the packed two 32-bit integers into one 64-bit integer. Little endian.""" - return int(packed_diff[0] << 32 | packed_diff[1]) - - -def _registration_diff_pack(diff: int, packed_diff: multiprocessing.Array): - """Packs the difficulty into two 32-bit integers. Little endian.""" - packed_diff[0] = diff >> 32 - packed_diff[1] = diff & 0xFFFFFFFF # low 32 bits - - -def _hash_block_with_hotkey(block_bytes: bytes, hotkey_bytes: bytes) -> bytes: - """Hashes the block with the hotkey using Keccak-256 to get 32 bytes""" - kec = keccak.new(digest_bits=256) - kec = kec.update(bytearray(block_bytes + hotkey_bytes)) - block_and_hotkey_hash_bytes = kec.digest() - return block_and_hotkey_hash_bytes - - -def _update_curr_block( - curr_diff: multiprocessing.Array, - curr_block: multiprocessing.Array, - curr_block_num: multiprocessing.Value, - block_number: int, - block_bytes: bytes, - diff: int, - hotkey_bytes: bytes, - lock: multiprocessing.Lock, -): - with lock: - curr_block_num.value = block_number - # Hash the block with the hotkey - block_and_hotkey_hash_bytes = _hash_block_with_hotkey(block_bytes, hotkey_bytes) - for i in range(32): - curr_block[i] = block_and_hotkey_hash_bytes[i] - _registration_diff_pack(diff, curr_diff) - - -def get_cpu_count() -> int: - try: - return len(os.sched_getaffinity(0)) - except AttributeError: - # OSX does not have sched_getaffinity - return os.cpu_count() - - -@dataclass -class RegistrationStatistics: - """Statistics for a registration.""" - - time_spent_total: float - rounds_total: int - time_average: float - time_spent: float - hash_rate_perpetual: float - hash_rate: float - difficulty: int - block_number: int - block_hash: bytes - - -class RegistrationStatisticsLogger: - """Logs statistics for a registration.""" - - console: rich_console.Console - status: Optional[rich_status.Status] - - def __init__( - self, console: rich_console.Console, output_in_place: bool = True - ) -> None: - self.console = console - - if output_in_place: - self.status = self.console.status("Solving") - else: - self.status = None - - def start(self) -> None: - if self.status is not None: - self.status.start() - - def stop(self) -> None: - if self.status is not None: - self.status.stop() - - def get_status_message( - cls, stats: RegistrationStatistics, verbose: bool = False - ) -> str: - message = ( - "Solving\n" - + f"Time Spent (total): [bold white]{timedelta(seconds=stats.time_spent_total)}[/bold white]\n" - + ( - f"Time Spent This Round: {timedelta(seconds=stats.time_spent)}\n" - + f"Time Spent Average: {timedelta(seconds=stats.time_average)}\n" - if verbose - else "" - ) - + f"Registration Difficulty: [bold white]{millify(stats.difficulty)}[/bold white]\n" - + f"Iters (Inst/Perp): [bold white]{get_human_readable(stats.hash_rate, 'H')}/s / " - + f"{get_human_readable(stats.hash_rate_perpetual, 'H')}/s[/bold white]\n" - + f"Block Number: [bold white]{stats.block_number}[/bold white]\n" - + f"Block Hash: [bold white]{stats.block_hash.encode('utf-8')}[/bold white]\n" - ) - return message - - def update(self, stats: RegistrationStatistics, verbose: bool = False) -> None: - if self.status is not None: - self.status.update(self.get_status_message(stats, verbose=verbose)) - else: - self.console.log(self.get_status_message(stats, verbose=verbose)) - - -def _solve_for_difficulty_fast( - subtensor, - wallet: "bittensor.wallet", - netuid: int, - output_in_place: bool = True, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional[POWSolution]: - """ - Solves the POW for registration using multiprocessing. - Args: - subtensor - Subtensor to connect to for block information and to submit. - wallet: - wallet to use for registration. - netuid: int - The netuid of the subnet to register to. - output_in_place: bool - If true, prints the status in place. Otherwise, prints the status on a new line. - num_processes: int - Number of processes to use. - update_interval: int - Number of nonces to solve before updating block information. - n_samples: int - The number of samples of the hash_rate to keep for the EWMA - alpha_: float - The alpha for the EWMA for the hash_rate calculation - log_verbose: bool - If true, prints more verbose logging of the registration metrics. - Note: The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - Note: - - We can also modify the update interval to do smaller blocks of work, - while still updating the block information after a different number of nonces, - to increase the transparency of the process while still keeping the speed. - """ - if num_processes is None: - # get the number of allowed processes for this process - num_processes = min(1, get_cpu_count()) - - if update_interval is None: - update_interval = 50_000 - - limit = int(math.pow(2, 256)) - 1 - - curr_block, curr_block_num, curr_diff = _Solver.create_shared_memory() - - # Establish communication queues - ## See the _Solver class for more information on the queues. - stopEvent = multiprocessing.Event() - stopEvent.clear() - - solution_queue = multiprocessing.Queue() - finished_queues = [multiprocessing.Queue() for _ in range(num_processes)] - check_block = multiprocessing.Lock() - - hotkey_bytes = ( - wallet.coldkeypub.public_key if netuid == -1 else wallet.hotkey.public_key - ) - # Start consumers - solvers = [ - _Solver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - # Set to current block - _update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - console = bittensor.__console__ - logger = RegistrationStatisticsLogger(console, output_in_place) - logger.start() - - solution = None - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - while netuid == -1 or not subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=0.25) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - old_block_number=old_block_number, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - curr_stats=curr_stats, - update_curr_block=_update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - for finished_queue in finished_queues: - try: - proc_num = finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - hash_rate_ = (num_time * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * update_interval - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, solution contains the nonce or wallet is registered - stopEvent.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - _terminate_workers_and_wait_for_exit(solvers) - - return solution - - -@backoff.on_exception(backoff.constant, Exception, interval=1, max_tries=3) -def _get_block_with_retry( - subtensor: "bittensor.subtensor", netuid: int -) -> Tuple[int, int, bytes]: - """ - Gets the current block number, difficulty, and block hash from the substrate node. - - Args: - subtensor (:obj:`bittensor.subtensor`, `required`): - The subtensor object to use to get the block number, difficulty, and block hash. - - netuid (:obj:`int`, `required`): - The netuid of the network to get the block number, difficulty, and block hash from. - - Returns: - block_number (:obj:`int`): - The current block number. - - difficulty (:obj:`int`): - The current difficulty of the subnet. - - block_hash (:obj:`bytes`): - The current block hash. - - Raises: - Exception: If the block hash is None. - ValueError: If the difficulty is None. - """ - block_number = subtensor.get_current_block() - difficulty = 1_000_000 if netuid == -1 else subtensor.difficulty(netuid=netuid) - block_hash = subtensor.get_block_hash(block_number) - if block_hash is None: - raise Exception( - "Network error. Could not connect to substrate to get block hash" - ) - if difficulty is None: - raise ValueError("Chain error. Difficulty is None") - return block_number, difficulty, block_hash - - -class _UsingSpawnStartMethod: - def __init__(self, force: bool = False): - self._old_start_method = None - self._force = force - - def __enter__(self): - self._old_start_method = multiprocessing.get_start_method(allow_none=True) - if self._old_start_method is None: - self._old_start_method = "spawn" # default to spawn - - multiprocessing.set_start_method("spawn", force=self._force) - - def __exit__(self, *args): - # restore the old start method - multiprocessing.set_start_method(self._old_start_method, force=True) - - -def _check_for_newest_block_and_update( - subtensor: "bittensor.subtensor", - netuid: int, - old_block_number: int, - hotkey_bytes: bytes, - curr_diff: multiprocessing.Array, - curr_block: multiprocessing.Array, - curr_block_num: multiprocessing.Value, - update_curr_block: Callable, - check_block: "multiprocessing.Lock", - solvers: List[_Solver], - curr_stats: RegistrationStatistics, -) -> int: - """ - Checks for a new block and updates the current block information if a new block is found. - - Args: - subtensor (:obj:`bittensor.subtensor`, `required`): - The subtensor object to use for getting the current block. - netuid (:obj:`int`, `required`): - The netuid to use for retrieving the difficulty. - old_block_number (:obj:`int`, `required`): - The old block number to check against. - hotkey_bytes (:obj:`bytes`, `required`): - The bytes of the hotkey's pubkey. - curr_diff (:obj:`multiprocessing.Array`, `required`): - The current difficulty as a multiprocessing array. - curr_block (:obj:`multiprocessing.Array`, `required`): - Where the current block is stored as a multiprocessing array. - curr_block_num (:obj:`multiprocessing.Value`, `required`): - Where the current block number is stored as a multiprocessing value. - update_curr_block (:obj:`Callable`, `required`): - A function that updates the current block. - check_block (:obj:`multiprocessing.Lock`, `required`): - A mp lock that is used to check for a new block. - solvers (:obj:`List[_Solver]`, `required`): - A list of solvers to update the current block for. - curr_stats (:obj:`RegistrationStatistics`, `required`): - The current registration statistics to update. - - Returns: - (int) The current block number. - """ - block_number = subtensor.get_current_block() - if block_number != old_block_number: - old_block_number = block_number - # update block information - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - block_bytes = bytes.fromhex(block_hash[2:]) - - update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - # Set new block events for each solver - - for worker in solvers: - worker.newBlockEvent.set() - - # update stats - curr_stats.block_number = block_number - curr_stats.block_hash = block_hash - curr_stats.difficulty = difficulty - - return old_block_number - - -def _solve_for_difficulty_fast_cuda( - subtensor: "bittensor.subtensor", - wallet: "bittensor.wallet", - netuid: int, - output_in_place: bool = True, - update_interval: int = 50_000, - tpb: int = 512, - dev_id: Union[List[int], int] = 0, - n_samples: int = 10, - alpha_: float = 0.80, - log_verbose: bool = False, -) -> Optional[POWSolution]: - """ - Solves the registration fast using CUDA - Args: - subtensor: bittensor.subtensor - The subtensor node to grab blocks - wallet: bittensor.wallet - The wallet to register - netuid: int - The netuid of the subnet to register to. - output_in_place: bool - If true, prints the output in place, otherwise prints to new lines - update_interval: int - The number of nonces to try before checking for more blocks - tpb: int - The number of threads per block. CUDA param that should match the GPU capability - dev_id: Union[List[int], int] - The CUDA device IDs to execute the registration on, either a single device or a list of devices - n_samples: int - The number of samples of the hash_rate to keep for the EWMA - alpha_: float - The alpha for the EWMA for the hash_rate calculation - log_verbose: bool - If true, prints more verbose logging of the registration metrics. - Note: The hash rate is calculated as an exponentially weighted moving average in order to make the measure more robust. - """ - if isinstance(dev_id, int): - dev_id = [dev_id] - elif dev_id is None: - dev_id = [0] - - if update_interval is None: - update_interval = 50_000 - - if not torch.cuda.is_available(): - raise Exception("CUDA not available") - - limit = int(math.pow(2, 256)) - 1 - - # Set mp start to use spawn so CUDA doesn't complain - with _UsingSpawnStartMethod(force=True): - curr_block, curr_block_num, curr_diff = _CUDASolver.create_shared_memory() - - ## Create a worker per CUDA device - num_processes = len(dev_id) - - # Establish communication queues - stopEvent = multiprocessing.Event() - stopEvent.clear() - solution_queue = multiprocessing.Queue() - finished_queues = [multiprocessing.Queue() for _ in range(num_processes)] - check_block = multiprocessing.Lock() - - hotkey_bytes = wallet.hotkey.public_key - # Start workers - solvers = [ - _CUDASolver( - i, - num_processes, - update_interval, - finished_queues[i], - solution_queue, - stopEvent, - curr_block, - curr_block_num, - curr_diff, - check_block, - limit, - dev_id[i], - tpb, - ) - for i in range(num_processes) - ] - - # Get first block - block_number, difficulty, block_hash = _get_block_with_retry( - subtensor=subtensor, netuid=netuid - ) - - block_bytes = bytes.fromhex(block_hash[2:]) - old_block_number = block_number - - # Set to current block - _update_curr_block( - curr_diff, - curr_block, - curr_block_num, - block_number, - block_bytes, - difficulty, - hotkey_bytes, - check_block, - ) - - # Set new block events for each solver to start at the initial block - for worker in solvers: - worker.newBlockEvent.set() - - for worker in solvers: - worker.start() # start the solver processes - - start_time = time.time() # time that the registration started - time_last = start_time # time that the last work blocks completed - - curr_stats = RegistrationStatistics( - time_spent_total=0.0, - time_average=0.0, - rounds_total=0, - time_spent=0.0, - hash_rate_perpetual=0.0, - hash_rate=0.0, # EWMA hash_rate (H/s) - difficulty=difficulty, - block_number=block_number, - block_hash=block_hash, - ) - - start_time_perpetual = time.time() - - console = bittensor.__console__ - logger = RegistrationStatisticsLogger(console, output_in_place) - logger.start() - - hash_rates = [0] * n_samples # The last n true hash_rates - weights = [alpha_**i for i in range(n_samples)] # weights decay by alpha - - solution = None - while netuid == -1 or not subtensor.is_hotkey_registered( - netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address - ): - # Wait until a solver finds a solution - try: - solution = solution_queue.get(block=True, timeout=0.15) - if solution is not None: - break - except Empty: - # No solution found, try again - pass - - # check for new block - old_block_number = _check_for_newest_block_and_update( - subtensor=subtensor, - netuid=netuid, - hotkey_bytes=hotkey_bytes, - curr_diff=curr_diff, - curr_block=curr_block, - curr_block_num=curr_block_num, - old_block_number=old_block_number, - curr_stats=curr_stats, - update_curr_block=_update_curr_block, - check_block=check_block, - solvers=solvers, - ) - - num_time = 0 - # Get times for each solver - for finished_queue in finished_queues: - try: - proc_num = finished_queue.get(timeout=0.1) - num_time += 1 - - except Empty: - continue - - time_now = time.time() # get current time - time_since_last = time_now - time_last # get time since last work block(s) - if num_time > 0 and time_since_last > 0.0: - # create EWMA of the hash_rate to make measure more robust - - hash_rate_ = (num_time * tpb * update_interval) / time_since_last - hash_rates.append(hash_rate_) - hash_rates.pop(0) # remove the 0th data point - curr_stats.hash_rate = sum( - [hash_rates[i] * weights[i] for i in range(n_samples)] - ) / (sum(weights)) - - # update time last to now - time_last = time_now - - curr_stats.time_average = ( - curr_stats.time_average * curr_stats.rounds_total - + curr_stats.time_spent - ) / (curr_stats.rounds_total + num_time) - curr_stats.rounds_total += num_time - - # Update stats - curr_stats.time_spent = time_since_last - new_time_spent_total = time_now - start_time_perpetual - curr_stats.hash_rate_perpetual = ( - curr_stats.rounds_total * (tpb * update_interval) - ) / new_time_spent_total - curr_stats.time_spent_total = new_time_spent_total - - # Update the logger - logger.update(curr_stats, verbose=log_verbose) - - # exited while, found_solution contains the nonce or wallet is registered - - stopEvent.set() # stop all other processes - logger.stop() - - # terminate and wait for all solvers to exit - _terminate_workers_and_wait_for_exit(solvers) - - return solution - - -def _terminate_workers_and_wait_for_exit( - workers: List[Union[multiprocessing.Process, multiprocessing.queues.Queue]], -) -> None: - for worker in workers: - if isinstance(worker, multiprocessing.queues.Queue): - worker.join_thread() - else: - worker.terminate() - worker.join() - worker.close() - - -def create_pow( - subtensor, - wallet, - netuid: int, - output_in_place: bool = True, - cuda: bool = False, - dev_id: Union[List[int], int] = 0, - tpb: int = 256, - num_processes: int = None, - update_interval: int = None, - log_verbose: bool = False, -) -> Optional[Dict[str, Any]]: - """ - Creates a proof of work for the given subtensor and wallet. - Args: - subtensor (:obj:`bittensor.subtensor.subtensor`, `required`): - The subtensor to create a proof of work for. - wallet (:obj:`bittensor.wallet.wallet`, `required`): - The wallet to create a proof of work for. - netuid (:obj:`int`, `required`): - The netuid for the subnet to create a proof of work for. - output_in_place (:obj:`bool`, `optional`, defaults to :obj:`True`): - If true, prints the progress of the proof of work to the console - in-place. Meaning the progress is printed on the same lines. - cuda (:obj:`bool`, `optional`, defaults to :obj:`False`): - If true, uses CUDA to solve the proof of work. - dev_id (:obj:`Union[List[int], int]`, `optional`, defaults to :obj:`0`): - The CUDA device id(s) to use. If cuda is true and dev_id is a list, - then multiple CUDA devices will be used to solve the proof of work. - tpb (:obj:`int`, `optional`, defaults to :obj:`256`): - The number of threads per block to use when solving the proof of work. - Should be a multiple of 32. - num_processes (:obj:`int`, `optional`, defaults to :obj:`None`): - The number of processes to use when solving the proof of work. - If None, then the number of processes is equal to the number of - CPU cores. - update_interval (:obj:`int`, `optional`, defaults to :obj:`None`): - The number of nonces to run before checking for a new block. - log_verbose (:obj:`bool`, `optional`, defaults to :obj:`False`): - If true, prints the progress of the proof of work more verbosely. - Returns: - :obj:`Optional[Dict[str, Any]]`: The proof of work solution or None if - the wallet is already registered or there is a different error. - - Raises: - :obj:`ValueError`: If the subnet does not exist. - """ - if netuid != -1: - if not subtensor.subnet_exists(netuid=netuid): - raise ValueError(f"Subnet {netuid} does not exist") - - if cuda: - solution: Optional[POWSolution] = _solve_for_difficulty_fast_cuda( - subtensor, - wallet, - netuid=netuid, - output_in_place=output_in_place, - dev_id=dev_id, - tpb=tpb, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - solution: Optional[POWSolution] = _solve_for_difficulty_fast( - subtensor, - wallet, - netuid=netuid, - output_in_place=output_in_place, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - return solution diff --git a/bittensor/subnets.py b/bittensor/utils/subnets.py similarity index 65% rename from bittensor/subnets.py rename to bittensor/utils/subnets.py index 836a20dcb..2b42bead9 100644 --- a/bittensor/subnets.py +++ b/bittensor/utils/subnets.py @@ -1,70 +1,73 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 bittensor as bt from abc import ABC, abstractmethod -from typing import Any, List, Union, Optional +from typing import Any, Union, Optional, TYPE_CHECKING + +from bittensor.core.axon import Axon +from bittensor.core.dendrite import Dendrite +from bittensor.utils.btlogging import logging + +# For annotation purposes +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor.core.synapse import Synapse +# Community uses this class class SubnetsAPI(ABC): - def __init__(self, wallet: "bt.wallet"): + """This class is not used within the bittensor package, but is actively used by the community.""" + + def __init__(self, wallet: "Wallet"): self.wallet = wallet - self.dendrite = bt.dendrite(wallet=wallet) + self.dendrite = Dendrite(wallet=wallet) async def __call__(self, *args, **kwargs): return await self.query_api(*args, **kwargs) @abstractmethod def prepare_synapse(self, *args, **kwargs) -> Any: - """ - Prepare the synapse-specific payload. - """ - ... + """Prepare the synapse-specific payload.""" @abstractmethod - def process_responses(self, responses: List[Union["bt.Synapse", Any]]) -> Any: - """ - Process the responses from the network. - """ - ... + def process_responses(self, responses: list[Union["Synapse", Any]]) -> Any: + """Process the responses from the network.""" async def query_api( self, - axons: Union[bt.axon, List[bt.axon]], + axons: Union["Axon", list["Axon"]], deserialize: Optional[bool] = False, timeout: Optional[int] = 12, - **kwargs: Optional[Any], + **kwargs, ) -> Any: """ Queries the API nodes of a subnet using the given synapse and bespoke query function. Args: - axons (Union[bt.axon, List[bt.axon]]): The list of axon(s) to query. - deserialize (bool, optional): Whether to deserialize the responses. Defaults to False. - timeout (int, optional): The timeout in seconds for the query. Defaults to 12. + axons (Union[bt.axon, list[bt.axon]]): The list of axon(s) to query. + deserialize (Optional[bool]): Whether to deserialize the responses. Defaults to False. + timeout (Optional[int]): The timeout in seconds for the query. Defaults to 12. **kwargs: Keyword arguments for the prepare_synapse_fn. Returns: Any: The result of the process_responses_fn. """ synapse = self.prepare_synapse(**kwargs) - bt.logging.debug(f"Querying validator axons with synapse {synapse.name}...") + logging.debug(f"Querying validator axons with synapse {synapse.name}...") responses = await self.dendrite( axons=axons, synapse=synapse, diff --git a/bittensor/utils/subtensor.py b/bittensor/utils/subtensor.py deleted file mode 100644 index 0df9c64d8..000000000 --- a/bittensor/utils/subtensor.py +++ /dev/null @@ -1,173 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - -# 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. - -"""Module providing common helper functions for working with Subtensor.""" - -import json -import logging -import os -from typing import Dict, Optional, Union, Any, List, Tuple - -from substrateinterface.base import SubstrateInterface - -_logger = logging.getLogger("subtensor.errors_handler") - -_USER_HOME_DIR = os.path.expanduser("~") -_BT_DIR = os.path.join(_USER_HOME_DIR, ".bittensor") -_ERRORS_FILE_PATH = os.path.join(_BT_DIR, "subtensor_errors_map.json") -_ST_BUILD_ID = "subtensor_build_id" - -# Create directory if it doesn't exist -os.makedirs(_BT_DIR, exist_ok=True) - - -# Pallet's typing class `PalletMetadataV14` is defined only at -# https://github.com/polkascan/py-scale-codec/blob/master/scalecodec/type_registry/core.json#L1024 -# A class object is created dynamically at runtime. -# Circleci linter complains about string represented classes like 'PalletMetadataV14'. -def _get_errors_from_pallet(pallet) -> Optional[Dict[str, Dict[str, str]]]: - """Extracts and returns error information from the given pallet metadata. - - Args: - pallet (PalletMetadataV14): The pallet metadata containing error definitions. - - Returns: - dict[str, str]: A dictionary of errors indexed by their IDs. - - Raises: - ValueError: If the pallet does not contain error definitions or the list is empty. - """ - if not hasattr(pallet, "errors") or not pallet.errors: - _logger.warning( - "The pallet does not contain any error definitions or the list is empty." - ) - return None - - return { - str(error["index"]): { - "name": error["name"], - "description": " ".join(error["docs"]), - } - for error in pallet.errors - } - - -def _save_errors_to_cache(uniq_version: str, errors: Dict[str, Dict[str, str]]): - """Saves error details and unique version identifier to a JSON file. - - Args: - uniq_version (str): Unique version identifier for the Subtensor build. - errors (dict[str, str]): Error information to be cached. - """ - data = {_ST_BUILD_ID: uniq_version, "errors": errors} - try: - with open(_ERRORS_FILE_PATH, "w") as json_file: - json.dump(data, json_file, indent=4) - except IOError as e: - _logger.warning(f"Error saving to file: {e}") - - -def _get_errors_from_cache() -> Optional[Dict[str, Dict[str, Dict[str, str]]]]: - """Retrieves and returns the cached error information from a JSON file, if it exists. - - Returns: - A dictionary containing error information. - """ - if not os.path.exists(_ERRORS_FILE_PATH): - return None - - try: - with open(_ERRORS_FILE_PATH, "r") as json_file: - data = json.load(json_file) - except IOError as e: - _logger.warning(f"Error reading from file: {e}") - return None - - return data - - -def get_subtensor_errors( - substrate: SubstrateInterface, -) -> Union[Dict[str, Dict[str, str]], Dict[Any, Any]]: - """Fetches or retrieves cached Subtensor error definitions using metadata. - - Args: - substrate (SubstrateInterface): Instance of SubstrateInterface to access metadata. - - Returns: - dict[str, str]: A dictionary containing error information. - """ - if not substrate.metadata: - substrate.get_metadata() - - cached_errors_map = _get_errors_from_cache() - # TODO: Talk to the Nucleus team about a unique identification for each assembly (subtensor). Before that, use - # the metadata value for `subtensor_build_id` - subtensor_build_id = substrate.metadata[0].value - - if not cached_errors_map or subtensor_build_id != cached_errors_map.get( - _ST_BUILD_ID - ): - pallet = substrate.metadata.get_metadata_pallet("SubtensorModule") - subtensor_errors_map = _get_errors_from_pallet(pallet) - - if not subtensor_errors_map: - return {} - - _save_errors_to_cache( - uniq_version=substrate.metadata[0].value, errors=subtensor_errors_map - ) - _logger.info(f"File {_ERRORS_FILE_PATH} has been updated.") - return subtensor_errors_map - else: - return cached_errors_map.get("errors", {}) - - -def format_parent(proportion, parent) -> Tuple[str, str]: - """ - Formats raw parent data into a list of tuples. - Args: - parent: The raw parent data. - proportion: proportion of parent data. - Returns: - list: List of (proportion, child_address) tuples. - """ - int_proportion = ( - proportion.value if hasattr(proportion, "value") else int(proportion) - ) - return int_proportion, parent.value - - -def format_children(children) -> List[Tuple[int, str]]: - """ - Formats raw children data into a list of tuples. - Args: - children: The raw children data. - Returns: - list: List of (proportion, child_address) tuples. - """ - formatted_children = [] - for proportion, child in children: - # Convert U64 to int - int_proportion = ( - proportion.value if hasattr(proportion, "value") else int(proportion) - ) - if int_proportion > 0: - formatted_children.append((int_proportion, child.value)) - return formatted_children diff --git a/bittensor/utils/test_utils.py b/bittensor/utils/test_utils.py deleted file mode 100644 index fdaa1bda9..000000000 --- a/bittensor/utils/test_utils.py +++ /dev/null @@ -1,22 +0,0 @@ -import socket -from random import randint -from typing import Set - -max_tries = 10 - - -def get_random_unused_port(allocated_ports: Set = set()): - def port_in_use(port: int) -> bool: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - return s.connect_ex(("localhost", port)) == 0 - - tries = 0 - while tries < max_tries: - tries += 1 - port = randint(2**14, 2**16 - 1) - - if port not in allocated_ports and not port_in_use(port): - allocated_ports.add(port) - return port - - raise RuntimeError(f"Tried {max_tries} random ports and could not find an open one") diff --git a/bittensor/utils/version.py b/bittensor/utils/version.py index 2a5aa3cd5..1134361ad 100644 --- a/bittensor/utils/version.py +++ b/bittensor/utils/version.py @@ -1,16 +1,35 @@ -from typing import Optional -from pathlib import Path +# 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 time -from packaging.version import Version +from pathlib import Path +from typing import Optional -import bittensor import requests +from packaging.version import Version + +from bittensor.core.settings import __version__, PIPADDRESS +from bittensor.utils.btlogging import logging VERSION_CHECK_THRESHOLD = 86400 class VersionCheckError(Exception): - pass + """Exception raised for errors in the version check process.""" def _get_version_file_path() -> Path: @@ -20,36 +39,43 @@ def _get_version_file_path() -> Path: def _get_version_from_file(version_file: Path) -> Optional[str]: try: mtime = version_file.stat().st_mtime - bittensor.logging.debug(f"Found version file, last modified: {mtime}") + logging.debug(f"Found version file, last modified: {mtime}") diff = time.time() - mtime if diff >= VERSION_CHECK_THRESHOLD: - bittensor.logging.debug("Version file expired") + logging.debug("Version file expired") return None return version_file.read_text() except FileNotFoundError: - bittensor.logging.debug("No bitensor version file found") + logging.debug("No bittensor version file found") return None except OSError: - bittensor.logging.exception("Failed to read version file") + logging.exception("Failed to read version file") return None def _get_version_from_pypi(timeout: int = 15) -> str: - bittensor.logging.debug( - f"Checking latest Bittensor version at: {bittensor.__pipaddress__}" - ) + logging.debug(f"Checking latest Bittensor version at: {PIPADDRESS}") try: - response = requests.get(bittensor.__pipaddress__, timeout=timeout) + response = requests.get(PIPADDRESS, timeout=timeout) latest_version = response.json()["info"]["version"] return latest_version except requests.exceptions.RequestException: - bittensor.logging.exception("Failed to get latest version from pypi") + logging.exception("Failed to get latest version from pypi") raise def get_and_save_latest_version(timeout: int = 15) -> str: + """ + Retrieves and saves the latest version of Bittensor. + + Args: + timeout (int): The timeout for the request to PyPI in seconds. Default is ``15``. + + Returns: + str: The latest version of Bittensor. + """ version_file = _get_version_file_path() if last_known_version := _get_version_from_file(version_file): @@ -60,34 +86,39 @@ def get_and_save_latest_version(timeout: int = 15) -> str: try: version_file.write_text(latest_version) except OSError: - bittensor.logging.exception("Failed to save latest version to file") + logging.exception("Failed to save latest version to file") return latest_version def check_version(timeout: int = 15): """ - Check if the current version of Bittensor is up to date with the latest version on PyPi. + Check if the current version of Bittensor is up-to-date with the latest version on PyPi. Raises a VersionCheckError if the version check fails. + + Args: + timeout (int): The timeout for the request to PyPI in seconds. Default is ``15``. """ try: latest_version = get_and_save_latest_version(timeout) - if Version(latest_version) > Version(bittensor.__version__): + if Version(latest_version) > Version(__version__): print( - "\u001b[33mBittensor Version: Current {}/Latest {}\nPlease update to the latest version at your earliest convenience. " - "Run the following command to upgrade:\n\n\u001b[0mpython -m pip install --upgrade bittensor".format( - bittensor.__version__, latest_version - ) + f"\u001b[33mBittensor Version: Current {__version__}/Latest {latest_version}\n" + f"Please update to the latest version at your earliest convenience. " + "Run the following command to upgrade:\n\n\u001b[0mpython -m pip install --upgrade bittensor" ) + pass except Exception as e: raise VersionCheckError("Version check failed") from e def version_checking(timeout: int = 15): - """ - Deprecated, kept for backwards compatibility. Use check_version() instead. + """Deprecated, kept for backwards compatibility. Use check_version() instead. + + Args: + timeout (int): The timeout for calling :func:``check_version`` function. Default is ``15``. """ from warnings import warn @@ -100,4 +131,4 @@ def version_checking(timeout: int = 15): try: check_version(timeout) except VersionCheckError: - bittensor.logging.exception("Version check failed") + logging.exception("Version check failed") diff --git a/bittensor/utils/wallet_utils.py b/bittensor/utils/wallet_utils.py deleted file mode 100644 index e7fda95ad..000000000 --- a/bittensor/utils/wallet_utils.py +++ /dev/null @@ -1,209 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc -# -# 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. - -from typing import Any, Dict, Union, Optional - -from substrateinterface import Keypair as Keypair -from substrateinterface.utils import ss58 - -from .. import __ss58_format__ - - -def get_ss58_format(ss58_address: str) -> int: - """Returns the ss58 format of the given ss58 address.""" - return ss58.get_ss58_format(ss58_address) - - -def is_valid_ss58_address(address: str) -> bool: - """ - Checks if the given address is a valid ss58 address. - - Args: - address(str): The address to check. - - Returns: - True if the address is a valid ss58 address for Bittensor, False otherwise. - """ - try: - return ss58.is_valid_ss58_address( - address, valid_ss58_format=__ss58_format__ - ) or ss58.is_valid_ss58_address( - address, valid_ss58_format=42 - ) # Default substrate ss58 format (legacy) - except IndexError: - return False - - -def is_valid_ed25519_pubkey(public_key: Union[str, bytes]) -> bool: - """ - Checks if the given public_key is a valid ed25519 key. - - Args: - public_key(Union[str, bytes]): The public_key to check. - - Returns: - True if the public_key is a valid ed25519 key, False otherwise. - - """ - try: - if isinstance(public_key, str): - if len(public_key) != 64 and len(public_key) != 66: - raise ValueError("a public_key should be 64 or 66 characters") - elif isinstance(public_key, bytes): - if len(public_key) != 32: - raise ValueError("a public_key should be 32 bytes") - else: - raise ValueError("public_key must be a string or bytes") - - keypair = Keypair(public_key=public_key, ss58_format=__ss58_format__) - - ss58_addr = keypair.ss58_address - return ss58_addr is not None - - except (ValueError, IndexError): - return False - - -def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool: - """ - Checks if the given address is a valid destination address. - - Args: - address(Union[str, bytes]): The address to check. - - Returns: - True if the address is a valid destination address, False otherwise. - """ - if isinstance(address, str): - # Check if ed25519 - if address.startswith("0x"): - return is_valid_ed25519_pubkey(address) - else: - # Assume ss58 address - return is_valid_ss58_address(address) - elif isinstance(address, bytes): - # Check if ed25519 - return is_valid_ed25519_pubkey(address) - else: - # Invalid address type - return False - - -def create_identity_dict( - display: str = "", - legal: str = "", - web: str = "", - riot: str = "", - email: str = "", - pgp_fingerprint: Optional[str] = None, - image: str = "", - info: str = "", - twitter: str = "", -) -> dict: - """ - Creates a dictionary with structure for identity extrinsic. Must fit within 64 bits. - - Args: - display (str): String to be converted and stored under 'display'. - legal (str): String to be converted and stored under 'legal'. - web (str): String to be converted and stored under 'web'. - riot (str): String to be converted and stored under 'riot'. - email (str): String to be converted and stored under 'email'. - pgp_fingerprint (str): String to be converted and stored under 'pgp_fingerprint'. - image (str): String to be converted and stored under 'image'. - info (str): String to be converted and stored under 'info'. - twitter (str): String to be converted and stored under 'twitter'. - - Returns: - dict: A dictionary with the specified structure and byte string conversions. - - Raises: - ValueError: If pgp_fingerprint is not exactly 20 bytes long when encoded. - """ - if pgp_fingerprint and len(pgp_fingerprint.encode()) != 20: - raise ValueError("pgp_fingerprint must be exactly 20 bytes long when encoded") - - return { - "info": { - "additional": [[]], - "display": {f"Raw{len(display.encode())}": display.encode()}, - "legal": {f"Raw{len(legal.encode())}": legal.encode()}, - "web": {f"Raw{len(web.encode())}": web.encode()}, - "riot": {f"Raw{len(riot.encode())}": riot.encode()}, - "email": {f"Raw{len(email.encode())}": email.encode()}, - "pgp_fingerprint": pgp_fingerprint.encode() if pgp_fingerprint else None, - "image": {f"Raw{len(image.encode())}": image.encode()}, - "info": {f"Raw{len(info.encode())}": info.encode()}, - "twitter": {f"Raw{len(twitter.encode())}": twitter.encode()}, - } - } - - -def decode_hex_identity_dict(info_dictionary) -> Dict[str, Any]: - """ - Decodes hex-encoded strings in a dictionary. - - This function traverses the given dictionary, identifies hex-encoded strings, and decodes them into readable strings. It handles nested dictionaries and lists within the dictionary. - - Args: - info_dictionary (dict): The dictionary containing hex-encoded strings to decode. - - Returns: - dict: The dictionary with decoded strings. - - Examples: - input_dict = { - ... "name": {"value": "0x6a6f686e"}, - ... "additional": [ - ... [{"data": "0x64617461"}] - ... ] - ... } - decode_hex_identity_dict(input_dict) - {'name': 'john', 'additional': [('data', 'data')]} - """ - - def get_decoded(data: str) -> str: - """Decodes a hex-encoded string.""" - try: - return bytes.fromhex(data[2:]).decode() - except UnicodeDecodeError: - print(f"Could not decode: {key}: {item}") - - for key, value in info_dictionary.items(): - if isinstance(value, dict): - item = list(value.values())[0] - if isinstance(item, str) and item.startswith("0x"): - try: - info_dictionary[key] = get_decoded(item) - except UnicodeDecodeError: - print(f"Could not decode: {key}: {item}") - else: - info_dictionary[key] = item - if key == "additional": - additional = [] - for item in value: - additional.append( - tuple( - get_decoded(data=next(iter(sub_item.values()))) - for sub_item in item - ) - ) - info_dictionary[key] = additional - - return info_dictionary diff --git a/bittensor/utils/weight_utils.py b/bittensor/utils/weight_utils.py index de26d98c0..d7c86bcdc 100644 --- a/bittensor/utils/weight_utils.py +++ b/bittensor/utils/weight_utils.py @@ -1,53 +1,57 @@ -""" -Conversion for weight between chain representation and np.array or torch.Tensor -""" - # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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. +"""Conversion for weight between chain representation and np.array or torch.Tensor""" + import hashlib import logging -from typing import Tuple, List, Union +import typing +from typing import Union, Optional import numpy as np + from numpy.typing import NDArray -from scalecodec import ScaleBytes, U16, Vec +from scalecodec import U16, ScaleBytes, Vec from substrateinterface import Keypair -import bittensor -from bittensor.utils.registration import torch, use_torch, legacy_torch_api_compat +from bittensor.utils.btlogging import logging +from bittensor.utils.registration import legacy_torch_api_compat, torch, use_torch + +if typing.TYPE_CHECKING: + from bittensor.core.metagraph import Metagraph + from bittensor.core.subtensor import Subtensor + U32_MAX = 4294967295 U16_MAX = 65535 +# Uses in `bittensor.utils.weight_utils.process_weights_for_netuid` @legacy_torch_api_compat def normalize_max_weight( x: Union[NDArray[np.float32], "torch.FloatTensor"], limit: float = 0.1 ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: - r"""Normalizes the tensor x so that sum(x) = 1 and the max value is not greater than the limit. + """Normalizes the tensor x so that sum(x) = 1 and the max value is not greater than the limit. Args: - x (:obj:`np.float32`): - Tensor to be max_value normalized. - limit: float: - Max value after normalization. + x (:obj:`np.float32`): Tensor to be max_value normalized. + limit: float: Max value after normalization. + Returns: - y (:obj:`np.float32`): - Normalized x tensor. + y (:obj:`np.float32`): Normalized x tensor. """ epsilon = 1e-7 # For numerical stability after normalization @@ -85,20 +89,20 @@ def normalize_max_weight( return y +# Metagraph uses this function. def convert_weight_uids_and_vals_to_tensor( - n: int, uids: List[int], weights: List[int] + n: int, uids: list[int], weights: list[int] ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: - r"""Converts weights and uids from chain representation into a np.array (inverse operation from convert_weights_and_uids_for_emit) + """ + Converts weights and uids from chain representation into a np.array (inverse operation from convert_weights_and_uids_for_emit). + Args: - n: int: - number of neurons on network. - uids (:obj:`List[int],`): - Tensor of uids as destinations for passed weights. - weights (:obj:`List[int],`): - Tensor of weights. + n (int): number of neurons on network. + uids (list[int]): Tensor of uids as destinations for passed weights. + weights (list[int]): Tensor of weights. + Returns: - row_weights ( np.float32 or torch.FloatTensor ): - Converted row weights. + row_weights (np.float32 or torch.FloatTensor): Converted row weights. """ row_weights = ( torch.zeros([n], dtype=torch.float32) @@ -115,24 +119,20 @@ def convert_weight_uids_and_vals_to_tensor( return row_weights +# Metagraph uses this function. def convert_root_weight_uids_and_vals_to_tensor( - n: int, uids: List[int], weights: List[int], subnets: List[int] + n: int, uids: list[int], weights: list[int], subnets: list[int] ) -> Union[NDArray[np.float32], "torch.FloatTensor"]: - r"""Converts root weights and uids from chain representation into a np.array or torch FloatTensor (inverse operation from convert_weights_and_uids_for_emit) + """Converts root weights and uids from chain representation into a np.array or torch FloatTensor (inverse operation from convert_weights_and_uids_for_emit) Args: - n: int: - number of neurons on network. - uids (:obj:`List[int],`): - Tensor of uids as destinations for passed weights. - weights (:obj:`List[int],`): - Tensor of weights. - subnets (:obj:`List[int],`): - list of subnets on the network + n (int): number of neurons on network. + uids (list[int]): Tensor of uids as destinations for passed weights. + weights (list[int]): Tensor of weights. + subnets (list[int]): list of subnets on the network. + Returns: - row_weights ( np.float32 ): - Converted row weights. + row_weights (np.float32): Converted row weights. """ - row_weights = ( torch.zeros([n], dtype=torch.float32) if use_torch() @@ -155,20 +155,19 @@ def convert_root_weight_uids_and_vals_to_tensor( return row_weights +# Metagraph uses this function. def convert_bond_uids_and_vals_to_tensor( - n: int, uids: List[int], bonds: List[int] + n: int, uids: list[int], bonds: list[int] ) -> Union[NDArray[np.int64], "torch.LongTensor"]: - r"""Converts bond and uids from chain representation into a np.array. + """Converts bond and uids from chain representation into a np.array. + Args: - n: int: - number of neurons on network. - uids (:obj:`List[int],`): - Tensor of uids as destinations for passed bonds. - bonds (:obj:`List[int],`): - Tensor of bonds. + n (int): number of neurons on network. + uids (list[int]): Tensor of uids as destinations for passed bonds. + bonds (list[int]): Tensor of bonds. + Returns: - row_bonds ( np.float32 ): - Converted row bonds. + row_bonds (np.float32): Converted row bonds. """ row_bonds = ( torch.zeros([n], dtype=torch.int64) @@ -180,36 +179,31 @@ def convert_bond_uids_and_vals_to_tensor( return row_bonds +# This is used by the community via `bittensor.api.extrinsics.set_weights.set_weights_extrinsic` def convert_weights_and_uids_for_emit( uids: Union[NDArray[np.int64], "torch.LongTensor"], weights: Union[NDArray[np.float32], "torch.FloatTensor"], -) -> Tuple[List[int], List[int]]: - r"""Converts weights into integer u32 representation that sum to MAX_INT_WEIGHT. +) -> tuple[list[int], list[int]]: + """Converts weights into integer u32 representation that sum to MAX_INT_WEIGHT. + Args: - uids (:obj:`np.int64,`): - Tensor of uids as destinations for passed weights. - weights (:obj:`np.float32,`): - Tensor of weights. + uids (np.int64):Tensor of uids as destinations for passed weights. + weights (np.float32):Tensor of weights. + Returns: - weight_uids (List[int]): - Uids as a list. - weight_vals (List[int]): - Weights as a list. + weight_uids (list[int]): Uids as a list. + weight_vals (list[int]): Weights as a list. """ # Checks. weights = weights.tolist() uids = uids.tolist() if min(weights) < 0: - raise ValueError( - "Passed weight is negative cannot exist on chain {}".format(weights) - ) + raise ValueError(f"Passed weight is negative cannot exist on chain {weights}") if min(uids) < 0: - raise ValueError("Passed uid is negative cannot exist on chain {}".format(uids)) + raise ValueError(f"Passed uid is negative cannot exist on chain {uids}") if len(uids) != len(weights): raise ValueError( - "Passed weights and uids must have the same length, got {} and {}".format( - len(uids), len(weights) - ) + f"Passed weights and uids must have the same length, got {len(uids)} and {len(weights)}" ) if sum(weights) == 0: return [], [] # Nothing to set on chain. @@ -234,22 +228,38 @@ def convert_weights_and_uids_for_emit( return weight_uids, weight_vals +# The community uses / bittensor does not def process_weights_for_netuid( uids: Union[NDArray[np.int64], "torch.Tensor"], weights: Union[NDArray[np.float32], "torch.Tensor"], netuid: int, - subtensor: "bittensor.subtensor", - metagraph: "bittensor.metagraph" = None, + subtensor: "Subtensor", + metagraph: Optional["Metagraph"] = None, exclude_quantile: int = 0, ) -> Union[ - Tuple["torch.Tensor", "torch.FloatTensor"], - Tuple[NDArray[np.int64], NDArray[np.float32]], + tuple["torch.Tensor", "torch.FloatTensor"], + tuple[NDArray[np.int64], NDArray[np.float32]], ]: - bittensor.logging.debug("process_weights_for_netuid()") - bittensor.logging.debug("weights", weights) - bittensor.logging.debug("netuid", netuid) - bittensor.logging.debug("subtensor", subtensor) - bittensor.logging.debug("metagraph", metagraph) + """ + Processes weight tensors for a given subnet id using the provided weight and UID arrays, applying constraints and normalization based on the subtensor and metagraph data. This function can handle both NumPy arrays and PyTorch tensors. + + Args: + uids (Union[NDArray[np.int64], "torch.Tensor"]): Array of unique identifiers of the neurons. + weights (Union[NDArray[np.float32], "torch.Tensor"]): Array of weights associated with the user IDs. + netuid (int): The network uid to process weights for. + subtensor (Subtensor): Subtensor instance to access blockchain data. + metagraph (Optional[Metagraph]): Metagraph instance for additional network data. If None, it is fetched from the subtensor using the netuid. + exclude_quantile (int): Quantile threshold for excluding lower weights. Defaults to ``0``. + + Returns: + Union[tuple["torch.Tensor", "torch.FloatTensor"], tuple[NDArray[np.int64], NDArray[np.float32]]]: tuple containing the array of user IDs and the corresponding normalized weights. The data type of the return matches the type of the input weights (NumPy or PyTorch). + """ + + logging.debug("process_weights_for_netuid()") + logging.debug("weights", *weights) + logging.debug("netuid", netuid) + logging.debug("subtensor", subtensor) + logging.debug("metagraph", metagraph) # Get latest metagraph from chain if metagraph is None. if metagraph is None: @@ -268,9 +278,9 @@ def process_weights_for_netuid( quantile = exclude_quantile / U16_MAX min_allowed_weights = subtensor.min_allowed_weights(netuid=netuid) max_weight_limit = subtensor.max_weight_limit(netuid=netuid) - bittensor.logging.debug("quantile", quantile) - bittensor.logging.debug("min_allowed_weights", min_allowed_weights) - bittensor.logging.debug("max_weight_limit", max_weight_limit) + logging.debug("quantile", quantile) + logging.debug("min_allowed_weights", min_allowed_weights) + logging.debug("max_weight_limit", max_weight_limit) # Find all non zero weights. non_zero_weight_idx = ( @@ -282,13 +292,13 @@ def process_weights_for_netuid( non_zero_weights = weights[non_zero_weight_idx] nzw_size = non_zero_weights.numel() if use_torch() else non_zero_weights.size if nzw_size == 0 or metagraph.n < min_allowed_weights: - bittensor.logging.warning("No non-zero weights returning all ones.") + logging.warning("No non-zero weights returning all ones.") final_weights = ( torch.ones((metagraph.n)).to(metagraph.n) / metagraph.n if use_torch() else np.ones((metagraph.n), dtype=np.int64) / metagraph.n ) - bittensor.logging.debug("final_weights", final_weights) + logging.debug("final_weights", final_weights) final_weights_count = ( torch.tensor(list(range(len(final_weights)))) if use_torch() @@ -301,7 +311,7 @@ def process_weights_for_netuid( ) elif nzw_size < min_allowed_weights: - bittensor.logging.warning( + logging.warning( "No non-zero weights less then min allowed weight, returning all ones." ) # ( const ): Should this be np.zeros( ( metagraph.n ) ) to reset everyone to build up weight? @@ -311,10 +321,8 @@ def process_weights_for_netuid( else np.ones((metagraph.n), dtype=np.int64) * 1e-5 ) # creating minimum even non-zero weights weights[non_zero_weight_idx] += non_zero_weights - bittensor.logging.debug("final_weights", weights) - normalized_weights = bittensor.utils.weight_utils.normalize_max_weight( - x=weights, limit=max_weight_limit - ) + logging.debug("final_weights", *weights) + normalized_weights = normalize_max_weight(x=weights, limit=max_weight_limit) nw_arange = ( torch.tensor(list(range(len(normalized_weights)))) if use_torch() @@ -322,7 +330,7 @@ def process_weights_for_netuid( ) return nw_arange, normalized_weights - bittensor.logging.debug("non_zero_weights", non_zero_weights) + logging.debug("non_zero_weights", *non_zero_weights) # Compute the exclude quantile and find the weights in the lowest quantile max_exclude = max(0, len(non_zero_weights) - min_allowed_weights) / len( @@ -334,21 +342,21 @@ def process_weights_for_netuid( if use_torch() else np.quantile(non_zero_weights, exclude_quantile) ) - bittensor.logging.debug("max_exclude", max_exclude) - bittensor.logging.debug("exclude_quantile", exclude_quantile) - bittensor.logging.debug("lowest_quantile", lowest_quantile) + logging.debug("max_exclude", max_exclude) + logging.debug("exclude_quantile", exclude_quantile) + logging.debug("lowest_quantile", lowest_quantile) # Exclude all weights below the allowed quantile. non_zero_weight_uids = non_zero_weight_uids[lowest_quantile <= non_zero_weights] non_zero_weights = non_zero_weights[lowest_quantile <= non_zero_weights] - bittensor.logging.debug("non_zero_weight_uids", non_zero_weight_uids) - bittensor.logging.debug("non_zero_weights", non_zero_weights) + logging.debug("non_zero_weight_uids", *non_zero_weight_uids) + logging.debug("non_zero_weights", *non_zero_weights) # Normalize weights and return. - normalized_weights = bittensor.utils.weight_utils.normalize_max_weight( + normalized_weights = normalize_max_weight( x=non_zero_weights, limit=max_weight_limit ) - bittensor.logging.debug("final_weights", normalized_weights) + logging.debug("final_weights", normalized_weights) return non_zero_weight_uids, normalized_weights @@ -356,10 +364,10 @@ def process_weights_for_netuid( def generate_weight_hash( address: str, netuid: int, - uids: List[int], - values: List[int], + uids: list[int], + values: list[int], version_key: int, - salt: List[int], + salt: list[int], ) -> str: """ Generate a valid commit hash from the provided weights. @@ -367,9 +375,9 @@ def generate_weight_hash( Args: address (str): The account identifier. Wallet ss58_address. netuid (int): The network unique identifier. - uids (List[int]): The list of UIDs. - salt (List[int]): The salt to add to hash. - values (List[int]): The list of weight values. + uids (list[int]): The list of UIDs. + salt (list[int]): The salt to add to hash. + values (list[int]): The list of weight values. version_key (int): The version key. Returns: diff --git a/bittensor/wallet.py b/bittensor/wallet.py deleted file mode 100644 index 28da5d865..000000000 --- a/bittensor/wallet.py +++ /dev/null @@ -1,873 +0,0 @@ -"""Implementation of the wallet class, which manages balances with staking and transfer. Also manages hotkey and coldkey.""" - -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao - -# 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 copy -import os -from typing import Dict, Optional, Tuple, Union, overload - -from substrateinterface import Keypair -from termcolor import colored - -import bittensor -from bittensor.utils import is_valid_bittensor_address_or_public_key - - -def display_mnemonic_msg(keypair: Keypair, key_type: str): - """ - Display the mnemonic and a warning message to keep the mnemonic safe. - - Args: - keypair (Keypair): Keypair object. - key_type (str): Type of the key (coldkey or hotkey). - """ - mnemonic = keypair.mnemonic - mnemonic_green = colored(mnemonic, "green") - print( - colored( - "\nIMPORTANT: Store this mnemonic in a secure (preferable offline place), as anyone " - "who has possession of this mnemonic can use it to regenerate the key and access your tokens. \n", - "red", - ) - ) - print("The mnemonic to the new {} is:\n\n{}\n".format(key_type, mnemonic_green)) - print( - "You can use the mnemonic to recreate the key in case it gets lost. The command to use to regenerate the key using this mnemonic is:" - ) - print("btcli w regen_{} --mnemonic {}".format(key_type, mnemonic)) - print("") - - -class wallet: - """ - The wallet class in the Bittensor framework handles wallet functionality, crucial for participating in the Bittensor network. - - It manages two types of keys: coldkey and hotkey, each serving different purposes in network operations. Each wallet contains a coldkey and a hotkey. - - The coldkey is the user's primary key for holding stake in their wallet and is the only way that users - can access Tao. Coldkeys can hold tokens and should be encrypted on your device. - - The coldkey is the primary key used for securing the wallet's stake in the Bittensor network (Tao) and - is critical for financial transactions like staking and unstaking tokens. It's recommended to keep the - coldkey encrypted and secure, as it holds the actual tokens. - - The hotkey, in contrast, is used for operational tasks like subscribing to and setting weights in the - network. It's linked to the coldkey through the metagraph and does not directly hold tokens, thereby - offering a safer way to interact with the network during regular operations. - - Args: - name (str): The name of the wallet, used to identify it among possibly multiple wallets. - path (str): File system path where wallet keys are stored. - hotkey_str (str): String identifier for the hotkey. - _hotkey, _coldkey, _coldkeypub (bittensor.Keypair): Internal representations of the hotkey and coldkey. - - Methods: - create_if_non_existent, create, recreate: Methods to handle the creation of wallet keys. - get_coldkey, get_hotkey, get_coldkeypub: Methods to retrieve specific keys. - set_coldkey, set_hotkey, set_coldkeypub: Methods to set or update keys. - hotkey_file, coldkey_file, coldkeypub_file: Properties that return respective key file objects. - regenerate_coldkey, regenerate_hotkey, regenerate_coldkeypub: Methods to regenerate keys from different sources. - config, help, add_args: Utility methods for configuration and assistance. - - The wallet class is a fundamental component for users to interact securely with the Bittensor network, facilitating both operational tasks and transactions involving value transfer across the network. - - Example Usage:: - - # Create a new wallet with default coldkey and hotkey names - my_wallet = wallet() - - # Access hotkey and coldkey - hotkey = my_wallet.get_hotkey() - coldkey = my_wallet.get_coldkey() - - # Set a new coldkey - my_wallet.new_coldkey(n_words=24) # number of seed words to use - - # Update wallet hotkey - my_wallet.set_hotkey(new_hotkey) - - # Print wallet details - print(my_wallet) - - # Access coldkey property, must use password to unlock - my_wallet.coldkey - """ - - @classmethod - def config(cls) -> "bittensor.config": - """ - Get config from the argument parser. - - Returns: - bittensor.config: Config object. - """ - parser = argparse.ArgumentParser() - cls.add_args(parser) - return bittensor.config(parser, args=[]) - - @classmethod - def help(cls): - """ - Print help to stdout. - """ - parser = argparse.ArgumentParser() - cls.add_args(parser) - print(cls.__new__.__doc__) - parser.print_help() - - @classmethod - def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None): - """ - Accept specific arguments from parser. - - Args: - parser (argparse.ArgumentParser): Argument parser object. - prefix (str): Argument prefix. - """ - prefix_str = "" if prefix is None else prefix + "." - try: - default_name = os.getenv("BT_WALLET_NAME") or "default" - default_hotkey = os.getenv("BT_WALLET_NAME") or "default" - default_path = os.getenv("BT_WALLET_PATH") or "~/.bittensor/wallets/" - parser.add_argument( - "--" + prefix_str + "wallet.name", - required=False, - default=default_name, - help="The name of the wallet to unlock for running bittensor " - "(name mock is reserved for mocking this wallet)", - ) - parser.add_argument( - "--" + prefix_str + "wallet.hotkey", - required=False, - default=default_hotkey, - help="The name of the wallet's hotkey.", - ) - parser.add_argument( - "--" + prefix_str + "wallet.path", - required=False, - default=default_path, - help="The path to your bittensor wallets", - ) - except argparse.ArgumentError as e: - pass - - def __init__( - self, - name: str = None, - hotkey: str = None, - path: str = None, - config: "bittensor.config" = None, - ): - r""" - Initialize the bittensor wallet object containing a hot and coldkey. - - Args: - name (str, optional): The name of the wallet to unlock for running bittensor. Defaults to ``default``. - hotkey (str, optional): The name of hotkey used to running the miner. Defaults to ``default``. - path (str, optional): The path to your bittensor wallets. Defaults to ``~/.bittensor/wallets/``. - config (bittensor.config, optional): bittensor.wallet.config(). Defaults to ``None``. - """ - # Fill config from passed args using command line defaults. - if config is None: - config = wallet.config() - self.config = copy.deepcopy(config) - self.config.wallet.name = name or self.config.wallet.get( - "name", bittensor.defaults.wallet.name - ) - self.config.wallet.hotkey = hotkey or self.config.wallet.get( - "hotkey", bittensor.defaults.wallet.hotkey - ) - self.config.wallet.path = path or self.config.wallet.get( - "path", bittensor.defaults.wallet.path - ) - - self.name = self.config.wallet.name - self.path = self.config.wallet.path - self.hotkey_str = self.config.wallet.hotkey - - self._hotkey = None - self._coldkey = None - self._coldkeypub = None - - def __str__(self): - """ - Returns the string representation of the Wallet object. - - Returns: - str: The string representation. - """ - return "wallet({}, {}, {})".format(self.name, self.hotkey_str, self.path) - - def __repr__(self): - """ - Returns the string representation of the wallet object. - - Returns: - str: The string representation. - """ - return self.__str__() - - def create_if_non_existent( - self, coldkey_use_password: bool = True, hotkey_use_password: bool = False - ) -> "wallet": - """ - Checks for existing coldkeypub and hotkeys, and creates them if non-existent. - - Args: - coldkey_use_password (bool, optional): Whether to use a password for coldkey. Defaults to ``True``. - hotkey_use_password (bool, optional): Whether to use a password for hotkey. Defaults to ``False``. - - Returns: - wallet: The wallet object. - """ - return self.create(coldkey_use_password, hotkey_use_password) - - def create( - self, coldkey_use_password: bool = True, hotkey_use_password: bool = False - ) -> "wallet": - """ - Checks for existing coldkeypub and hotkeys, and creates them if non-existent. - - Args: - coldkey_use_password (bool, optional): Whether to use a password for coldkey. Defaults to ``True``. - hotkey_use_password (bool, optional): Whether to use a password for hotkey. Defaults to ``False``. - - Returns: - wallet: The wallet object. - """ - # ---- Setup Wallet. ---- - if ( - not self.coldkey_file.exists_on_device() - and not self.coldkeypub_file.exists_on_device() - ): - self.create_new_coldkey(n_words=12, use_password=coldkey_use_password) - if not self.hotkey_file.exists_on_device(): - self.create_new_hotkey(n_words=12, use_password=hotkey_use_password) - return self - - def recreate( - self, coldkey_use_password: bool = True, hotkey_use_password: bool = False - ) -> "wallet": - """ - Checks for existing coldkeypub and hotkeys and creates them if non-existent. - - Args: - coldkey_use_password (bool, optional): Whether to use a password for coldkey. Defaults to ``True``. - hotkey_use_password (bool, optional): Whether to use a password for hotkey. Defaults to ``False``. - - Returns: - wallet: The wallet object. - """ - # ---- Setup Wallet. ---- - self.create_new_coldkey(n_words=12, use_password=coldkey_use_password) - self.create_new_hotkey(n_words=12, use_password=hotkey_use_password) - return self - - @property - def hotkey_file(self) -> "bittensor.keyfile": - """ - Property that returns the hotkey file. - - Returns: - bittensor.keyfile: The hotkey file. - """ - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - hotkey_path = os.path.join(wallet_path, "hotkeys", self.hotkey_str) - return bittensor.keyfile(path=hotkey_path) - - @property - def coldkey_file(self) -> "bittensor.keyfile": - """ - Property that returns the coldkey file. - - Returns: - bittensor.keyfile: The coldkey file. - """ - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkey_path = os.path.join(wallet_path, "coldkey") - return bittensor.keyfile(path=coldkey_path) - - @property - def coldkeypub_file(self) -> "bittensor.keyfile": - """ - Property that returns the coldkeypub file. - - Returns: - bittensor.keyfile: The coldkeypub file. - """ - wallet_path = os.path.expanduser(os.path.join(self.path, self.name)) - coldkeypub_path = os.path.join(wallet_path, "coldkeypub.txt") - return bittensor.keyfile(path=coldkeypub_path) - - def set_hotkey( - self, - keypair: "bittensor.Keypair", - encrypt: bool = False, - overwrite: bool = False, - ) -> "bittensor.keyfile": - """ - Sets the hotkey for the wallet. - - Args: - keypair (bittensor.Keypair): The hotkey keypair. - encrypt (bool, optional): Whether to encrypt the hotkey. Defaults to ``False``. - overwrite (bool, optional): Whether to overwrite an existing hotkey. Defaults to ``False``. - - Returns: - bittensor.keyfile: The hotkey file. - """ - self._hotkey = keypair - self.hotkey_file.set_keypair(keypair, encrypt=encrypt, overwrite=overwrite) - - def set_coldkeypub( - self, - keypair: "bittensor.Keypair", - encrypt: bool = False, - overwrite: bool = False, - ) -> "bittensor.keyfile": - """ - Sets the coldkeypub for the wallet. - - Args: - keypair (bittensor.Keypair): The coldkeypub keypair. - encrypt (bool, optional): Whether to encrypt the coldkeypub. Defaults to ``False``. - overwrite (bool, optional): Whether to overwrite an existing coldkeypub. Defaults to ``False``. - - Returns: - bittensor.keyfile: The coldkeypub file. - """ - self._coldkeypub = bittensor.Keypair(ss58_address=keypair.ss58_address) - self.coldkeypub_file.set_keypair( - self._coldkeypub, encrypt=encrypt, overwrite=overwrite - ) - - def set_coldkey( - self, - keypair: "bittensor.Keypair", - encrypt: bool = True, - overwrite: bool = False, - ) -> "bittensor.keyfile": - """ - Sets the coldkey for the wallet. - - Args: - keypair (bittensor.Keypair): The coldkey keypair. - encrypt (bool, optional): Whether to encrypt the coldkey. Defaults to ``True``. - overwrite (bool, optional): Whether to overwrite an existing coldkey. Defaults to ``False``. - - Returns: - bittensor.keyfile: The coldkey file. - """ - self._coldkey = keypair - self.coldkey_file.set_keypair( - self._coldkey, encrypt=encrypt, overwrite=overwrite - ) - - def get_coldkey(self, password: str = None) -> "bittensor.Keypair": - """ - Gets the coldkey from the wallet. - - Args: - password (str, optional): The password to decrypt the coldkey. Defaults to ``None``. - - Returns: - bittensor.Keypair: The coldkey keypair. - """ - return self.coldkey_file.get_keypair(password=password) - - def get_hotkey(self, password: str = None) -> "bittensor.Keypair": - """ - Gets the hotkey from the wallet. - - Args: - password (str, optional): The password to decrypt the hotkey. Defaults to ``None``. - - Returns: - bittensor.Keypair: The hotkey keypair. - """ - return self.hotkey_file.get_keypair(password=password) - - def get_coldkeypub(self, password: str = None) -> "bittensor.Keypair": - """ - Gets the coldkeypub from the wallet. - - Args: - password (str, optional): The password to decrypt the coldkeypub. Defaults to ``None``. - - Returns: - bittensor.Keypair: The coldkeypub keypair. - """ - return self.coldkeypub_file.get_keypair(password=password) - - @property - def hotkey(self) -> "bittensor.Keypair": - r"""Loads the hotkey from wallet.path/wallet.name/hotkeys/wallet.hotkey or raises an error. - - Returns: - hotkey (Keypair): - hotkey loaded from config arguments. - Raises: - KeyFileError: Raised if the file is corrupt of non-existent. - CryptoKeyError: Raised if the user enters an incorrec password for an encrypted keyfile. - """ - if self._hotkey == None: - self._hotkey = self.hotkey_file.keypair - return self._hotkey - - @property - def coldkey(self) -> "bittensor.Keypair": - r"""Loads the hotkey from wallet.path/wallet.name/coldkey or raises an error. - - Returns: - coldkey (Keypair): coldkey loaded from config arguments. - Raises: - KeyFileError: Raised if the file is corrupt of non-existent. - CryptoKeyError: Raised if the user enters an incorrec password for an encrypted keyfile. - """ - if self._coldkey == None: - self._coldkey = self.coldkey_file.keypair - return self._coldkey - - @property - def coldkeypub(self) -> "bittensor.Keypair": - r"""Loads the coldkeypub from wallet.path/wallet.name/coldkeypub.txt or raises an error. - - Returns: - coldkeypub (Keypair): coldkeypub loaded from config arguments. - Raises: - KeyFileError: Raised if the file is corrupt of non-existent. - CryptoKeyError: Raised if the user enters an incorrect password for an encrypted keyfile. - """ - if self._coldkeypub == None: - self._coldkeypub = self.coldkeypub_file.keypair - return self._coldkeypub - - def create_coldkey_from_uri( - self, - uri: str, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates coldkey from suri string, optionally encrypts it with the user-provided password. - - Args: - uri: (str, required): - URI string to use i.e., ``/Alice`` or ``/Bob``. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the coldkey under the same path ``//coldkey``. - Returns: - wallet (bittensor.wallet): - This object with newly created coldkey. - """ - keypair = Keypair.create_from_uri(uri) - if not suppress: - display_mnemonic_msg(keypair, "coldkey") - self.set_coldkey(keypair, encrypt=use_password, overwrite=overwrite) - self.set_coldkeypub(keypair, overwrite=overwrite) - return self - - def create_hotkey_from_uri( - self, - uri: str, - use_password: bool = False, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates hotkey from suri string, optionally encrypts it with the user-provided password. - - Args: - uri: (str, required): - URI string to use i.e., ``/Alice`` or ``/Bob`` - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the hotkey under the same path ``//hotkeys/``. - Returns: - wallet (bittensor.wallet): - This object with newly created hotkey. - """ - keypair = Keypair.create_from_uri(uri) - if not suppress: - display_mnemonic_msg(keypair, "hotkey") - self.set_hotkey(keypair, encrypt=use_password, overwrite=overwrite) - return self - - def new_coldkey( - self, - n_words: int = 12, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates a new coldkey, optionally encrypts it with the user-provided password and saves to disk. - - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the coldkey under the same path ``//coldkey``. - Returns: - wallet (bittensor.wallet): - This object with newly created coldkey. - """ - self.create_new_coldkey(n_words, use_password, overwrite, suppress) - - def create_new_coldkey( - self, - n_words: int = 12, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates a new coldkey, optionally encrypts it with the user-provided password and saves to disk. - - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the coldkey under the same path ``//coldkey``. - Returns: - wallet (bittensor.wallet): - This object with newly created coldkey. - """ - mnemonic = Keypair.generate_mnemonic(n_words) - keypair = Keypair.create_from_mnemonic(mnemonic) - if not suppress: - display_mnemonic_msg(keypair, "coldkey") - self.set_coldkey(keypair, encrypt=use_password, overwrite=overwrite) - self.set_coldkeypub(keypair, overwrite=overwrite) - return self - - def new_hotkey( - self, - n_words: int = 12, - use_password: bool = False, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates a new hotkey, optionally encrypts it with the user-provided password and saves to disk. - - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the hotkey under the same path ``//hotkeys/``. - Returns: - wallet (bittensor.wallet): - This object with newly created hotkey. - """ - self.create_new_hotkey(n_words, use_password, overwrite, suppress) - - def create_new_hotkey( - self, - n_words: int = 12, - use_password: bool = False, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Creates a new hotkey, optionally encrypts it with the user-provided password and saves to disk. - - Args: - n_words: (int, optional): - Number of mnemonic words to use. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Will this operation overwrite the hotkey under the same path //hotkeys/ - Returns: - wallet (bittensor.wallet): - This object with newly created hotkey. - """ - mnemonic = Keypair.generate_mnemonic(n_words) - keypair = Keypair.create_from_mnemonic(mnemonic) - if not suppress: - display_mnemonic_msg(keypair, "hotkey") - self.set_hotkey(keypair, encrypt=use_password, overwrite=overwrite) - return self - - def regenerate_coldkeypub( - self, - ss58_address: Optional[str] = None, - public_key: Optional[Union[str, bytes]] = None, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": - """Regenerates the coldkeypub from the passed ``ss58_address`` or public_key and saves the file. Requires either ``ss58_address`` or public_key to be passed. - - Args: - ss58_address: (str, optional): - Address as ``ss58`` string. - public_key: (str | bytes, optional): - Public key as hex string or bytes. - overwrite (bool, optional) (default: False): - Determins if this operation overwrites the coldkeypub (if exists) under the same path ``//coldkeypub``. - Returns: - wallet (bittensor.wallet): - Newly re-generated wallet with coldkeypub. - - """ - if ss58_address is None and public_key is None: - raise ValueError("Either ss58_address or public_key must be passed") - - if not is_valid_bittensor_address_or_public_key( - ss58_address if ss58_address is not None else public_key - ): - raise ValueError( - f"Invalid {'ss58_address' if ss58_address is not None else 'public_key'}" - ) - - if ss58_address is not None: - ss58_format = bittensor.utils.get_ss58_format(ss58_address) - keypair = Keypair( - ss58_address=ss58_address, - public_key=public_key, - ss58_format=ss58_format, - ) - else: - keypair = Keypair( - ss58_address=ss58_address, - public_key=public_key, - ss58_format=bittensor.__ss58_format__, - ) - - # No need to encrypt the public key - self.set_coldkeypub(keypair, overwrite=overwrite) - - return self - - # Short name for regenerate_coldkeypub - regen_coldkeypub = regenerate_coldkeypub - - @overload - def regenerate_coldkey( - self, - mnemonic: Optional[Union[list, str]] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - @overload - def regenerate_coldkey( - self, - seed: Optional[str] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - @overload - def regenerate_coldkey( - self, - json: Optional[Tuple[Union[str, Dict], str]] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - def regenerate_coldkey( - self, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - **kwargs, - ) -> "wallet": - """Regenerates the coldkey from the passed mnemonic or seed, or JSON encrypts it with the user's password and saves the file. - - Args: - mnemonic: (Union[list, str], optional): - Key mnemonic as list of words or string space separated words. - seed: (str, optional): - Seed as hex string. - json: (Tuple[Union[str, Dict], str], optional): - Restore from encrypted JSON backup as ``(json_data: Union[str, Dict], passphrase: str)`` - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determines if this operation overwrites the coldkey under the same path ``//coldkey``. - Returns: - wallet (bittensor.wallet): - This object with newly created coldkey. - - Note: - Uses priority order: ``mnemonic > seed > json``. - - """ - if len(kwargs) == 0: - raise ValueError("Must pass either mnemonic, seed, or json") - - # Get from kwargs - mnemonic = kwargs.get("mnemonic", None) - seed = kwargs.get("seed", None) - json = kwargs.get("json", None) - - if mnemonic is None and seed is None and json is None: - raise ValueError("Must pass either mnemonic, seed, or json") - if mnemonic is not None: - if isinstance(mnemonic, str): - mnemonic = mnemonic.split() - elif isinstance(mnemonic, list) and len(mnemonic) == 1: - mnemonic = mnemonic[0].split() - if len(mnemonic) not in [12, 15, 18, 21, 24]: - raise ValueError( - "Mnemonic has invalid size. This should be 12,15,18,21 or 24 words" - ) - keypair = Keypair.create_from_mnemonic( - " ".join(mnemonic), ss58_format=bittensor.__ss58_format__ - ) - if not suppress: - display_mnemonic_msg(keypair, "coldkey") - elif seed is not None: - keypair = Keypair.create_from_seed( - seed, ss58_format=bittensor.__ss58_format__ - ) - else: - # json is not None - if ( - not isinstance(json, tuple) - or len(json) != 2 - or not isinstance(json[0], (str, dict)) - or not isinstance(json[1], str) - ): - raise ValueError( - "json must be a tuple of (json_data: str | Dict, passphrase: str)" - ) - - json_data, passphrase = json - keypair = Keypair.create_from_encrypted_json( - json_data, passphrase, ss58_format=bittensor.__ss58_format__ - ) - - self.set_coldkey(keypair, encrypt=use_password, overwrite=overwrite) - self.set_coldkeypub(keypair, overwrite=overwrite) - return self - - # Short name for regenerate_coldkey - regen_coldkey = regenerate_coldkey - - @overload - def regenerate_hotkey( - self, - mnemonic: Optional[Union[list, str]] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - @overload - def regenerate_hotkey( - self, - seed: Optional[str] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - @overload - def regenerate_hotkey( - self, - json: Optional[Tuple[Union[str, Dict], str]] = None, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - ) -> "wallet": ... - - def regenerate_hotkey( - self, - use_password: bool = True, - overwrite: bool = False, - suppress: bool = False, - **kwargs, - ) -> "wallet": - """Regenerates the hotkey from passed mnemonic or seed, encrypts it with the user's password and saves the file. - - Args: - mnemonic: (Union[list, str], optional): - Key mnemonic as list of words or string space separated words. - seed: (str, optional): - Seed as hex string. - json: (Tuple[Union[str, Dict], str], optional): - Restore from encrypted JSON backup as ``(json_data: Union[str, Dict], passphrase: str)``. - use_password (bool, optional): - Is the created key password protected. - overwrite (bool, optional): - Determies if this operation overwrites the hotkey under the same path ``//hotkeys/``. - Returns: - wallet (bittensor.wallet): - This object with newly created hotkey. - """ - if len(kwargs) == 0: - raise ValueError("Must pass either mnemonic, seed, or json") - - # Get from kwargs - mnemonic = kwargs.get("mnemonic", None) - seed = kwargs.get("seed", None) - json = kwargs.get("json", None) - - if mnemonic is None and seed is None and json is None: - raise ValueError("Must pass either mnemonic, seed, or json") - if mnemonic is not None: - if isinstance(mnemonic, str): - mnemonic = mnemonic.split() - elif isinstance(mnemonic, list) and len(mnemonic) == 1: - mnemonic = mnemonic[0].split() - if len(mnemonic) not in [12, 15, 18, 21, 24]: - raise ValueError( - "Mnemonic has invalid size. This should be 12,15,18,21 or 24 words" - ) - keypair = Keypair.create_from_mnemonic( - " ".join(mnemonic), ss58_format=bittensor.__ss58_format__ - ) - if not suppress: - display_mnemonic_msg(keypair, "hotkey") - elif seed is not None: - keypair = Keypair.create_from_seed( - seed, ss58_format=bittensor.__ss58_format__ - ) - else: - # json is not None - if ( - not isinstance(json, tuple) - or len(json) != 2 - or not isinstance(json[0], (str, dict)) - or not isinstance(json[1], str) - ): - raise ValueError( - "json must be a tuple of (json_data: str | Dict, passphrase: str)" - ) - - json_data, passphrase = json - keypair = Keypair.create_from_encrypted_json( - json_data, passphrase, ss58_format=bittensor.__ss58_format__ - ) - - self.set_hotkey(keypair, encrypt=use_password, overwrite=overwrite) - return self - - # Short name for regenerate_hotkey - regen_hotkey = regenerate_hotkey diff --git a/requirements/btcli.txt b/requirements/btcli.txt new file mode 100644 index 000000000..6a86c87bc --- /dev/null +++ b/requirements/btcli.txt @@ -0,0 +1 @@ +bittensor-cli \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index a9e1a1bc4..14d616b48 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,4 +1,3 @@ -black==24.3.0 pytest==7.2.0 pytest-asyncio==0.23.7 pytest-mock==3.12.0 diff --git a/requirements/prod.txt b/requirements/prod.txt index 8bb6acd0f..631f949be 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,35 +1,23 @@ +wheel +setuptools~=70.0.0 aiohttp~=3.9 -ansible~=8.5.0 -ansible_vault~=2.1 -backoff -certifi~=2024.7.4 +bt-decode colorama~=0.4.6 -cryptography~=42.0.5 -ddt~=1.6.0 -eth-utils<2.3.0 -fuzzywuzzy>=0.18.0 fastapi~=0.110.1 munch~=2.5.0 -netaddr -numpy~=1.26 +numpy~=2.0.1 msgpack-numpy-opentensor~=0.5.0 nest_asyncio +netaddr packaging -pycryptodome>=3.18.0,<4.0.0 -pyyaml -password_strength -pydantic>=2.3, <3 -PyNaCl~=1.3 -python-Levenshtein python-statemachine~=2.1 +pyyaml retry requests rich +pydantic>=2.3, <3 +python-Levenshtein scalecodec==1.2.11 -setuptools~=70.0.0 -shtab~=1.6.5 substrate-interface~=1.7.9 -termcolor -tqdm uvicorn -wheel +bittensor-wallet==1.0.0 \ No newline at end of file diff --git a/scripts/run.sh b/scripts/run.sh deleted file mode 100755 index b6502b5d3..000000000 --- a/scripts/run.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -cd ~/.bittensor/bittensor - -UPSTREAM=${1:-'@{u}'} -LOCAL=$(git rev-parse @) -REMOTE=$(git rev-parse "$UPSTREAM") -BASE=$(git merge-base @ "$UPSTREAM") - -if [ $LOCAL = $REMOTE ]; then - echo "Up-to-date" -elif [ $LOCAL = $BASE ]; then - git pull origin master - pip install -e . -else - echo "Diverged" -fi - diff --git a/setup.py b/setup.py index 49c419724..290274bd8 100644 --- a/setup.py +++ b/setup.py @@ -1,32 +1,32 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao - +# 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. -from setuptools import setup, find_packages -from os import path -from io import open import codecs -import re import os import pathlib +import re +from io import open +from os import path + +from setuptools import setup, find_packages def read_requirements(path): requirements = [] - git_requirements = [] with pathlib.Path(path).open() as requirements_txt: for line in requirements_txt: @@ -40,6 +40,7 @@ def read_requirements(path): requirements = read_requirements("requirements/prod.txt") +extra_requirements_btcli = read_requirements("requirements/btcli.txt") extra_requirements_dev = read_requirements("requirements/dev.txt") extra_requirements_cubit = read_requirements("requirements/cubit.txt") extra_requirements_torch = read_requirements("requirements/torch.txt") @@ -52,7 +53,7 @@ def read_requirements(path): # loading version from setup.py with codecs.open( - os.path.join(here, "bittensor/__init__.py"), encoding="utf-8" + os.path.join(here, "bittensor/core/settings.py"), encoding="utf-8" ) as init_file: version_match = re.search( r"^__version__ = ['\"]([^'\"]*)['\"]", init_file.read(), re.M @@ -74,15 +75,14 @@ def read_requirements(path): python_requires=">=3.9", install_requires=requirements, extras_require={ + "btcli": extra_requirements_btcli, "dev": extra_requirements_dev, "torch": extra_requirements_torch, }, - scripts=["bin/btcli"], classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Topic :: Software Development :: Build Tools", - # Pick your license as you wish "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 6f648be13..d82944afd 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -1,104 +1,84 @@ -import logging import os import re import shlex import signal -import socket import subprocess import time import pytest from substrateinterface import SubstrateInterface -from tests.e2e_tests.utils import ( +from bittensor import logging +from tests.e2e_tests.utils.e2e_test_utils import ( clone_or_update_templates, install_templates, + template_path, uninstall_templates, ) -logging.basicConfig(level=logging.INFO) - - -# Function to check if the process is running by port -def is_chain_running(port): - """Check if a node is running on the given port.""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - try: - # Attempt to connect to the given port on localhost - s.connect(("127.0.0.1", port)) - return True - except (ConnectionRefusedError, OSError): - # If the connection is refused or there's an OS error, the node is not running - return False - # Fixture for setting up and tearing down a localnet.sh chain between tests @pytest.fixture(scope="function") def local_chain(request): param = request.param if hasattr(request, "param") else None + # Get the environment variable for the script path script_path = os.getenv("LOCALNET_SH_PATH") if not script_path: + # Skip the test if the localhost.sh path is not set logging.warning("LOCALNET_SH_PATH env variable is not set, e2e test skipped.") pytest.skip("LOCALNET_SH_PATH environment variable is not set.") - # Determine the port to check based on `param` - port = 9945 # Default port if `param` is None + # Check if param is None, and handle it accordingly + args = "" if param is None else f"{param}" - # Always perform template installation - logging.info("Downloading and installing neuron templates from GitHub") - templates_dir = clone_or_update_templates() - install_templates(templates_dir) + # Compile commands to send to process + cmds = shlex.split(f"{script_path} {args}") - already_running = False - if is_chain_running(port): - already_running = True - logging.info(f"Chain already running on port {port}, skipping start.") - else: - logging.info(f"Starting new chain on port {port}...") - # compile commands to send to process - cmds = shlex.split(f"{script_path} {param}") - # Start new node process - process = subprocess.Popen( - cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid - ) - - # Wait for the node to start using the existing pattern match - pattern = re.compile(r"Imported #1") - timestamp = int(time.time()) - - def wait_for_node_start(process, pattern): - for line in process.stdout: - print(line.strip()) - if int(time.time()) - timestamp > 20 * 60: - pytest.fail("Subtensor not started in time") - if pattern.search(line): - print("Node started!") - break - - wait_for_node_start(process, pattern) - - # Continue with installing templates - logging.info("Downloading and installing neuron templates from GitHub") + # Start new node process + process = subprocess.Popen( + cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid + ) + + # Pattern match indicates node is compiled and ready + pattern = re.compile(r"Imported #1") + + # install neuron templates + logging.info("downloading and installing neuron templates from github") templates_dir = clone_or_update_templates() install_templates(templates_dir) - # Run the test, passing in the substrate interface - yield SubstrateInterface(url=f"ws://127.0.0.1:{port}") + timestamp = int(time.time()) + + def wait_for_node_start(process, pattern): + for line in process.stdout: + print(line.strip()) + # 10 min as timeout + if int(time.time()) - timestamp > 10 * 60: + print("Subtensor not started in time") + break + if pattern.search(line): + print("Node started!") + break + + wait_for_node_start(process, pattern) + + # Run the test, passing in substrate interface + yield SubstrateInterface(url="ws://127.0.0.1:9945") - if not already_running: - # Terminate the process group (includes all child processes) - os.killpg(os.getpgid(process.pid), signal.SIGTERM) + # Terminate the process group (includes all child processes) + os.killpg(os.getpgid(process.pid), signal.SIGTERM) - # Give some time for the process to terminate - time.sleep(1) + # Give some time for the process to terminate + time.sleep(1) - # If the process is not terminated, send SIGKILL - if process.poll() is None: - os.killpg(os.getpgid(process.pid), signal.SIGKILL) + # If the process is not terminated, send SIGKILL + if process.poll() is None: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) - # Ensure the process has terminated - process.wait() + # Ensure the process has terminated + process.wait() - logging.info("Uninstalling neuron templates") - uninstall_templates(templates_dir) + # uninstall templates + logging.info("uninstalling neuron templates") + uninstall_templates(template_path) diff --git a/tests/e2e_tests/multistep/test_axon.py b/tests/e2e_tests/multistep/test_axon.py deleted file mode 100644 index ebe95587e..000000000 --- a/tests/e2e_tests/multistep/test_axon.py +++ /dev/null @@ -1,112 +0,0 @@ -import asyncio -import sys - -import pytest - -import bittensor -from bittensor import logging -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, -) -from bittensor.utils import networking -from tests.e2e_tests.utils import ( - setup_wallet, - template_path, - templates_repo, -) - -""" -Test the axon mechanism. - -Verify that: -* axon is registered on network as a miner -* ip -* type -* port - -are set correctly, and that the miner is currently running - -""" - - -@pytest.mark.asyncio -async def test_axon(local_chain): - logging.info("Testing test_axon") - netuid = 1 - # Register root as Alice - alice_keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # Verify subnet created successfully - assert local_chain.query( - "SubtensorModule", "NetworksAdded", [netuid] - ).serialize(), "Subnet wasn't created successfully" - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) - - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - - # validate one miner with ip of none - old_axon = metagraph.axons[0] - - assert len(metagraph.axons) == 1, "Expected 1 axon, but got len(metagraph.axons)" - assert old_axon.hotkey == alice_keypair.ss58_address - assert old_axon.coldkey == alice_keypair.ss58_address - assert old_axon.ip == "0.0.0.0" - assert old_axon.port == 0 - assert old_axon.ip_type == 0 - - # register miner - # "python neurons/miner.py --netuid 1 --subtensor.chain_endpoint ws://localhost:9945 --wallet.name wallet.name --wallet.hotkey wallet.hotkey.ss58_address" - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/miner.py"', - "--no_prompt", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - wallet.path, - "--wallet.name", - wallet.name, - "--wallet.hotkey", - "default", - ] - ) - - axon_process = await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - logging.info("Neuron Alice is now mining") - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph to refresh with latest data - - # refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - updated_axon = metagraph.axons[0] - external_ip = networking.get_external_ip() - - assert len(metagraph.axons) == 1 - assert updated_axon.ip == external_ip - assert updated_axon.ip_type == networking.ip_version(external_ip) - assert updated_axon.port == 8091 - assert updated_axon.hotkey == alice_keypair.ss58_address - assert updated_axon.coldkey == alice_keypair.ss58_address - logging.info("Passed test_axon") diff --git a/tests/e2e_tests/multistep/test_dendrite.py b/tests/e2e_tests/multistep/test_dendrite.py deleted file mode 100644 index c68ccda81..000000000 --- a/tests/e2e_tests/multistep/test_dendrite.py +++ /dev/null @@ -1,164 +0,0 @@ -import asyncio -import sys - -import pytest - -import bittensor -from bittensor import logging -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, - RootRegisterCommand, - RootSetBoostCommand, - StakeCommand, -) -from tests.e2e_tests.utils import ( - setup_wallet, - template_path, - templates_repo, - wait_epoch, -) - -""" -Test the dendrites mechanism. - -Verify that: -* dendrite is registered on network as a validator -* stake successfully -* validator permit is set - -""" - - -@pytest.mark.asyncio -async def test_dendrite(local_chain): - logging.info("Testing test_dendrite") - netuid = 1 - # Register root as Alice - the subnet owner - alice_keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # Verify subnet created successfully - assert local_chain.query( - "SubtensorModule", "NetworksAdded", [netuid] - ).serialize(), "Subnet wasn't created successfully" - - bob_keypair, exec_command, wallet_path = setup_wallet("//Bob") - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) - - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # assert one neuron is Bob - assert len(subtensor.neurons(netuid=netuid)) == 1 - neuron = metagraph.neurons[0] - assert neuron.hotkey == bob_keypair.ss58_address - assert neuron.coldkey == bob_keypair.ss58_address - - # assert stake is 0 - assert neuron.stake.tao == 0 - - # Stake to become to top neuron after the first epoch - exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - ], - ) - - # refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - neuron = metagraph.neurons[0] - # assert stake is 10000 - assert ( - neuron.stake.tao == 10_000.0 - ), f"Expected 10_000.0 staked TAO, but got {neuron.stake.tao}" - - # assert neuron is not validator - assert neuron.active is True - assert neuron.validator_permit is False - assert neuron.validator_trust == 0.0 - assert neuron.pruning_score == 0 - - # register validator from template - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/validator.py"', - "--no_prompt", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - wallet.path, - "--wallet.name", - wallet.name, - "--wallet.hotkey", - "default", - ] - ) - - # run validator in the background - dendrite_process = await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - logging.info("Neuron Alice is now validating") - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - - # register validator with root network - exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--netuid", - str(netuid), - ], - ) - - exec_command( - RootSetBoostCommand, - [ - "root", - "boost", - "--netuid", - str(netuid), - "--increase", - "1", - ], - ) - # get current block, wait until next epoch - await wait_epoch(subtensor, netuid=netuid) - - # refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - - # refresh validator neuron - neuron = metagraph.neurons[0] - - assert len(metagraph.neurons) == 1 - assert neuron.active is True - assert neuron.validator_permit is True - assert neuron.hotkey == bob_keypair.ss58_address - assert neuron.coldkey == bob_keypair.ss58_address - logging.info("Passed test_dendrite") diff --git a/tests/e2e_tests/multistep/test_emissions.py b/tests/e2e_tests/multistep/test_emissions.py deleted file mode 100644 index a05ff478a..000000000 --- a/tests/e2e_tests/multistep/test_emissions.py +++ /dev/null @@ -1,283 +0,0 @@ -import asyncio -import sys - -import pytest - -import bittensor -from bittensor import logging -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, - RootRegisterCommand, - RootSetBoostCommand, - RootSetWeightsCommand, - SetTakeCommand, - StakeCommand, - SubnetSudoCommand, -) -from tests.e2e_tests.utils import ( - setup_wallet, - template_path, - templates_repo, - wait_epoch, -) - -""" -Test the emissions mechanism. - -Verify that for the miner: -* trust -* rank -* consensus -* incentive -* emission -are updated with proper values after an epoch has passed. - -For the validator verify that: -* validator_permit -* validator_trust -* dividends -* stake -are updated with proper values after an epoch has passed. - -""" - - -@pytest.mark.asyncio -@pytest.mark.skip -async def test_emissions(local_chain): - logging.info("Testing test_emissions") - netuid = 1 - # Register root as Alice - the subnet owner and validator - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - # Verify subnet created successfully - assert local_chain.query( - "SubtensorModule", "NetworksAdded", [netuid] - ).serialize(), "Subnet wasn't created successfully" - - # Register Bob as miner - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - - # Register Alice as neuron to the subnet - alice_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) - - # Register Bob as neuron to the subnet - bob_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - # assert two neurons are in network - assert len(subtensor.neurons(netuid=netuid)) == 2 - - # Alice to stake to become to top neuron after the first epoch - alice_exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # register Alice as validator - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/validator.py"', - "--no_prompt", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - alice_wallet.path, - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - # run validator in the background - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - logging.info("Neuron Alice is now validating") - await asyncio.sleep(5) - - # register validator with root network - alice_exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--netuid", - str(netuid), - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - await wait_epoch(subtensor, netuid=netuid) - - alice_exec_command( - RootSetBoostCommand, - [ - "root", - "boost", - "--netuid", - str(netuid), - "--increase", - "1000", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # register Bob as miner - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/miner.py"', - "--no_prompt", - "--netuid", - str(netuid), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - bob_wallet.path, - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - logging.info("Neuron Bob is now mining") - await wait_epoch(subtensor) - - logging.warning("Setting root set weights") - alice_exec_command( - RootSetWeightsCommand, - [ - "root", - "weights", - "--netuid", - str(netuid), - "--weights", - "0.3", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # Set delegate take for Alice - alice_exec_command(SetTakeCommand, ["r", "set_take", "--take", "0.15"]) - - # Lower the rate limit - alice_exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - str(netuid), - "--wallet.name", - alice_wallet.name, - "--param", - "weights_rate_limit", - "--value", - "1", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # wait epoch until for emissions to get distributed - await wait_epoch(subtensor) - - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - - # refresh metagraph - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # get current emissions and validate that Alice has gotten tao - weights = [(0, [(0, 65535), (1, 65535)])] - assert ( - subtensor.weights(netuid=netuid) == weights - ), "Weights set vs weights in subtensor don't match" - - neurons = subtensor.neurons(netuid=netuid) - bob = neurons[1] - alice = neurons[0] - - assert bob.emission > 0 - assert bob.consensus == 1 - assert bob.incentive == 1 - assert bob.rank == 1 - assert bob.trust == 1 - - assert alice.emission > 0 - assert alice.bonds == [(1, 65535)] - assert alice.dividends == 1 - assert alice.stake.tao > 10000 # assert an increase in stake - assert alice.validator_permit is True - assert alice.validator_trust == 1 - - assert alice.weights == [(0, 65535), (1, 65535)] - - assert ( - subtensor.get_emission_value_by_subnet(netuid=netuid) > 0 - ), ( - "Emissions are not greated than 0" - ) # emission on this subnet is strictly greater than 0 - logging.info("Passed test_emissions") diff --git a/tests/e2e_tests/subcommands/__init__.py b/tests/e2e_tests/subcommands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/delegation/__init__.py b/tests/e2e_tests/subcommands/delegation/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py b/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py deleted file mode 100644 index ddad1dfcc..000000000 --- a/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py +++ /dev/null @@ -1,61 +0,0 @@ -from bittensor import logging -from bittensor.commands.delegates import NominateCommand, SetTakeCommand -from bittensor.commands.network import RegisterSubnetworkCommand -from bittensor.commands.register import RegisterCommand -from bittensor.commands.root import RootRegisterCommand -from tests.e2e_tests.utils import setup_wallet - - -def test_set_delegate_increase_take(local_chain): - logging.info("Testing test_set_delegate_increase_take") - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RootRegisterCommand, ["root", "register"]) - - # Create subnet 1 and verify created successfully - assert not ( - local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - ), "Subnet is already registered" - - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - assert local_chain.query( - "SubtensorModule", "NetworksAdded", [1] - ).serialize(), "Subnet wasn't registered" - - # Register and nominate Bob - keypair, exec_command, wallet = setup_wallet("//Bob") - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlock", [keypair.ss58_address] - ).serialize() - == 0 - ) - - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address] - ).serialize() - == 0 - ) - exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) - exec_command(NominateCommand, ["root", "nominate"]) - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlock", [keypair.ss58_address] - ).serialize() - > 0 - ) - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address] - ).serialize() - > 0 - ) - - # Set delegate take for Bob - exec_command(SetTakeCommand, ["r", "set_take", "--take", "0.15"]) - assert local_chain.query( - "SubtensorModule", "Delegates", [keypair.ss58_address] - ).value == int(0.15 * 65535), "Take value set incorrectly" - logging.info("Passed test_set_delegate_increase_take") diff --git a/tests/e2e_tests/subcommands/hyperparams/__init__.py b/tests/e2e_tests/subcommands/hyperparams/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py b/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py deleted file mode 100644 index 434184cb8..000000000 --- a/tests/e2e_tests/subcommands/hyperparams/test_liquid_alpha.py +++ /dev/null @@ -1,278 +0,0 @@ -import bittensor -from bittensor import logging -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, - StakeCommand, - SubnetSudoCommand, -) -from tests.e2e_tests.utils import setup_wallet - -""" -Test the liquid alpha weights mechanism. - -Verify that: -* it can get enabled -* liquid alpha values cannot be set before the feature flag is set -* after feature flag, you can set alpha_high -* after feature flag, you can set alpha_low -""" - - -def test_liquid_alpha_enabled(local_chain, capsys): - logging.info("Testing test_liquid_alpha_enabled") - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # hyperparameter values - alpha_values = "6553, 53083" - - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - # Stake to become to top neuron after the first epoch - exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "100000", - ], - ) - - # Assert liquid alpha disabled - subtensor = bittensor.subtensor(network="ws://localhost:9945") - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False - ), "Liquid alpha is enabled by default" - - # Attempt to set alpha high/low while disabled (should fail) - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=list(map(int, alpha_values.split(","))), - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None - output = capsys.readouterr().out - assert ( - "❌ Failed: Subtensor returned `LiquidAlphaDisabled (Module)` error. This means: \n`Attempting to set alpha high/low while disabled`" - in output - ) - - # Enable Liquid Alpha - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "liquid_alpha_enabled", - "--value", - "True", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - assert subtensor.get_subnet_hyperparameters( - netuid=1 - ).liquid_alpha_enabled, "Failed to enable liquid alpha" - - output = capsys.readouterr().out - assert "✅ Hyper parameter liquid_alpha_enabled changed to True" in output - - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "alpha_values", - "--value", - "87, 54099", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 54099 - ), "Failed to set alpha high" - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 87 - ), "Failed to set alpha low" - - u16_max = 65535 - # Set alpha high too low - alpha_high_too_low = ( - u16_max * 4 // 5 - ) - 1 # One less than the minimum acceptable value - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=[6553, alpha_high_too_low], - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None - output = capsys.readouterr().out - assert ( - "❌ Failed: Subtensor returned `AlphaHighTooLow (Module)` error. This means: \n`Alpha high is too low: alpha_high > 0.8`" - in output - ) - - alpha_high_too_high = u16_max + 1 # One more than the max acceptable value - try: - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=[6553, alpha_high_too_high], - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None, "Expected not to be able to set alpha value above u16" - except Exception as e: - assert str(e) == "65536 out of range for u16", f"Unexpected error: {e}" - - # Set alpha low too low - alpha_low_too_low = 0 - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=[alpha_low_too_low, 53083], - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None - output = capsys.readouterr().out - assert ( - "❌ Failed: Subtensor returned `AlphaLowOutOfRange (Module)` error. This means: \n`Alpha low is out of range: alpha_low > 0 && alpha_low < 0.8`" - in output - ) - - # Set alpha low too high - alpha_low_too_high = ( - u16_max * 4 // 5 - ) + 1 # One more than the maximum acceptable value - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=[alpha_low_too_high, 53083], - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None - output = capsys.readouterr().out - assert ( - "❌ Failed: Subtensor returned `AlphaLowOutOfRange (Module)` error. This means: \n`Alpha low is out of range: alpha_low > 0 && alpha_low < 0.8`" - in output - ) - - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "alpha_values", - "--value", - alpha_values, - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 53083 - ), "Failed to set alpha high" - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 6553 - ), "Failed to set alpha low" - - output = capsys.readouterr().out - assert "✅ Hyper parameter alpha_values changed to [6553.0, 53083.0]" in output - - # Disable Liquid Alpha - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "liquid_alpha_enabled", - "--value", - "False", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False - ), "Failed to disable liquid alpha" - - output = capsys.readouterr().out - assert "✅ Hyper parameter liquid_alpha_enabled changed to False" in output - - result = subtensor.set_hyperparameter( - wallet=wallet, - netuid=1, - parameter="alpha_values", - value=list(map(int, alpha_values.split(","))), - wait_for_inclusion=True, - wait_for_finalization=True, - ) - assert result is None - output = capsys.readouterr().out - assert ( - "❌ Failed: Subtensor returned `LiquidAlphaDisabled (Module)` error. This means: \n`Attempting to set alpha high/low while disabled`" - in output - ) - logging.info("Passed test_liquid_alpha_enabled") diff --git a/tests/e2e_tests/subcommands/register/__init__.py b/tests/e2e_tests/subcommands/register/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/register/test_swap_hotkey.py b/tests/e2e_tests/subcommands/register/test_swap_hotkey.py deleted file mode 100644 index 798aafc3f..000000000 --- a/tests/e2e_tests/subcommands/register/test_swap_hotkey.py +++ /dev/null @@ -1,566 +0,0 @@ -import asyncio -import sys -import uuid - -import pytest - -import bittensor -from bittensor import logging -from bittensor.commands import ( - ListCommand, - NewHotkeyCommand, - RegisterCommand, - RegisterSubnetworkCommand, - RootRegisterCommand, - StakeCommand, - SwapHotkeyCommand, -) -from tests.e2e_tests.utils import ( - setup_wallet, - template_path, - templates_repo, - wait_interval, -) - -""" -Test the swap_hotkey mechanism. - -Verify that: -* Alice - neuron is registered on network as a validator -* Bob - neuron is registered on network as a miner -* Swap hotkey of Alice via BTCLI -* verify that the hotkey is swapped -* verify that stake hotkey, delegates hotkey, UIDS and prometheus hotkey is swapped -""" - - -@pytest.mark.asyncio -async def test_swap_hotkey_validator_owner(local_chain): - logging.info("Testing swap hotkey of validator_owner") - # Register root as Alice - the subnet owner and validator - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register Bob as miner - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - - alice_old_hotkey_address = alice_wallet.hotkey.ss58_address - - # Register Alice as neuron to the subnet - alice_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - # Register Bob as neuron to the subnet - bob_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - logging.info("Alice and bob registered to the subnet") - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - # assert two neurons are in network - assert ( - len(subtensor.neurons(netuid=1)) == 2 - ), "Alice and Bob neurons not found in the network" - - # register Bob as miner - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/miner.py"', - "--no_prompt", - "--netuid", - "1", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - bob_wallet.path, - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - logging.info("Bob neuron is now mining") - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph to refresh with latest data - - # register Alice as validator - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/validator.py"', - "--no_prompt", - "--netuid", - "1", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - alice_wallet.path, - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - # run validator in the background - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - logging.info("Alice neuron is now validating") - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - - # register validator with root network - alice_exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--netuid", - "1", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - ], - ) - - # Alice to stake to become to top neuron after the first epoch - alice_exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - ], - ) - - # get latest metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # assert alice has old hotkey - alice_neuron = metagraph.neurons[0] - - # get current number of hotkeys - wallet_tree = alice_exec_command(ListCommand, ["w", "list"], "get_tree") - num_hotkeys = len(wallet_tree.children[0].children) - - assert ( - alice_neuron.coldkey == "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" - ), "Alice coldkey not as expected" - assert ( - alice_neuron.hotkey == alice_old_hotkey_address - ), "Alice hotkey not as expected" - assert ( - alice_neuron.stake_dict["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"].tao - == 10000.0 - ), "Alice tao not as expected" - assert alice_neuron.hotkey == alice_neuron.coldkey, "Coldkey and hotkey don't match" - assert ( - alice_neuron.hotkey == subtensor.get_all_subnets_info()[1].owner_ss58 - ), "Hotkey doesn't match owner address" - assert alice_neuron.coldkey == subtensor.get_hotkey_owner( - alice_old_hotkey_address - ), "Coldkey doesn't match hotkey owner" - assert ( - subtensor.is_hotkey_delegate(alice_neuron.hotkey) is True - ), "Alice is not a delegate" - assert ( - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=alice_neuron.hotkey, netuid=1 - ) - is True - ), "Alice hotkey not registered on subnet" - assert ( - subtensor.get_uid_for_hotkey_on_subnet( - hotkey_ss58=alice_neuron.hotkey, netuid=1 - ) - == alice_neuron.uid - ), "Alice hotkey not regisred on netuid" - if num_hotkeys > 1: - logging.info(f"You have {num_hotkeys} hotkeys for Alice.") - - # generate new guid name for hotkey - new_hotkey_name = str(uuid.uuid4()) - - # create a new hotkey - alice_exec_command( - NewHotkeyCommand, - [ - "w", - "new_hotkey", - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - new_hotkey_name, - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - logging.info("New hotkey is created") - # wait rate limit, until we are allowed to change hotkeys - rate_limit = subtensor.tx_rate_limit() - curr_block = subtensor.get_current_block() - await wait_interval(rate_limit + curr_block + 1, subtensor) - - # swap hotkey - alice_exec_command( - SwapHotkeyCommand, - [ - "w", - "swap_hotkey", - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - alice_wallet.hotkey_str, - "--wallet.hotkey_b", - new_hotkey_name, - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # get latest metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # assert Alice has new hotkey - alice_neuron = metagraph.neurons[0] - wallet_tree = alice_exec_command(ListCommand, ["w", "list"], "get_tree") - new_num_hotkeys = len(wallet_tree.children[0].children) - - assert ( - alice_neuron.coldkey == "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" - ), "Coldkey was changed" # cold key didnt change - assert ( - alice_neuron.hotkey != alice_old_hotkey_address - ), "Hotkey is not updated w.r.t old_hotkey_address" - assert ( - alice_neuron.hotkey != alice_neuron.coldkey - ), "Hotkey is not updated w.r.t coldkey" - assert ( - alice_neuron.coldkey == subtensor.get_all_subnets_info()[1].owner_ss58 - ) # new hotkey address is subnet owner - assert alice_neuron.coldkey != subtensor.get_hotkey_owner( - alice_old_hotkey_address - ) # old key is NOT owner - assert alice_neuron.coldkey == subtensor.get_hotkey_owner( - alice_neuron.hotkey - ) # new key is owner - assert ( - subtensor.is_hotkey_delegate(alice_neuron.hotkey) is True - ) # new key is delegate - assert ( # new key is registered on subnet - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=alice_neuron.hotkey, netuid=1 - ) - is True - ) - assert ( # old key is NOT registered on subnet - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=alice_old_hotkey_address, netuid=1 - ) - is False - ) - assert ( # uid is unchanged - subtensor.get_uid_for_hotkey_on_subnet( - hotkey_ss58=alice_neuron.hotkey, netuid=1 - ) - == alice_neuron.uid - ) - assert new_num_hotkeys == num_hotkeys + 1 - logging.info("Finished test_swap_hotkey_validator_owner") - - -""" -Test the swap_hotkey mechanism. - -Verify that: -* Alice - neuron is registered on network as a validator -* Bob - neuron is registered on network as a miner -* Swap hotkey of Bob via BTCLI -* verify that the hotkey is swapped -* verify that stake hotkey, delegates hotkey, UIDS and prometheus hotkey is swapped -""" - - -@pytest.mark.asyncio -async def test_swap_hotkey_miner(local_chain): - logging.info("Testing test_swap_hotkey_miner") - # Register root as Alice - the subnet owner and validator - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - # Register Bob as miner - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - - bob_old_hotkey_address = bob_wallet.hotkey.ss58_address - - # Register Alice as neuron to the subnet - alice_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - # Register Bob as neuron to the subnet - bob_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - # assert two neurons are in network - total_neurons = len(subtensor.neurons(netuid=1)) - assert total_neurons == 2, f"Expected 2 neurons, found {total_neurons}" - - # register Bob as miner - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/miner.py"', - "--no_prompt", - "--netuid", - "1", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - bob_wallet.path, - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - logging.info("Bob neuron is now mining") - # register Alice as validator - cmd = " ".join( - [ - f"{sys.executable}", - f'"{template_path}{templates_repo}/neurons/validator.py"', - "--no_prompt", - "--netuid", - "1", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - alice_wallet.path, - "--wallet.name", - alice_wallet.name, - "--wallet.hotkey", - "default", - "--logging.trace", - ] - ) - # run validator in the background - - await asyncio.create_subprocess_shell( - cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - logging.info("Alice neuron is now validating") - await asyncio.sleep( - 5 - ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - - # register validator with root network - alice_exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--netuid", - "1", - ], - ) - - # Alice to stake to become to top neuron after the first epoch - alice_exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - ], - ) - - # get latest metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # assert bob has old hotkey - bob_neuron = metagraph.neurons[1] - - # get current number of hotkeys - wallet_tree = bob_exec_command(ListCommand, ["w", "list"], "get_tree") - num_hotkeys = len(wallet_tree.children[0].children) - - assert bob_neuron.coldkey == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - assert bob_neuron.hotkey == bob_old_hotkey_address - assert bob_neuron.hotkey == bob_neuron.coldkey - assert bob_neuron.coldkey == subtensor.get_hotkey_owner(bob_old_hotkey_address) - assert subtensor.is_hotkey_delegate(bob_neuron.hotkey) is False - assert ( - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=bob_neuron.hotkey, netuid=1 - ) - is True - ) - assert ( - subtensor.get_uid_for_hotkey_on_subnet(hotkey_ss58=bob_neuron.hotkey, netuid=1) - == bob_neuron.uid - ) - if num_hotkeys > 1: - logging.info(f"You have {num_hotkeys} hotkeys for Bob.") - - # generate new guid name for hotkey - new_hotkey_name = str(uuid.uuid4()) - - # create a new hotkey - bob_exec_command( - NewHotkeyCommand, - [ - "w", - "new_hotkey", - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - new_hotkey_name, - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # wait rate limit, until we are allowed to change hotkeys - rate_limit = subtensor.tx_rate_limit() - curr_block = subtensor.get_current_block() - await wait_interval(rate_limit + curr_block + 1, subtensor) - - # swap hotkey - bob_exec_command( - SwapHotkeyCommand, - [ - "w", - "swap_hotkey", - "--wallet.name", - bob_wallet.name, - "--wallet.hotkey", - bob_wallet.hotkey_str, - "--wallet.hotkey_b", - new_hotkey_name, - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - # get latest metagraph - metagraph = bittensor.metagraph(netuid=1, network="ws://localhost:9945") - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # assert bob has new hotkey - bob_neuron = metagraph.neurons[1] - wallet_tree = bob_exec_command(ListCommand, ["w", "list"], "get_tree") - new_num_hotkeys = len(wallet_tree.children[0].children) - - assert ( - bob_neuron.coldkey == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - ) # cold key didn't change - assert ( - bob_neuron.hotkey != bob_old_hotkey_address - ), "Old and New hotkeys are the same" - assert ( - bob_neuron.hotkey != bob_neuron.coldkey - ), "Hotkey is still the same as coldkey" - assert bob_neuron.coldkey == subtensor.get_hotkey_owner( - bob_neuron.hotkey - ), "Coldkey is not the owner of the new hotkey" # new key is owner - assert ( - subtensor.is_hotkey_delegate(bob_neuron.hotkey) is False - ) # new key is delegate ?? - assert ( # new key is registered on subnet - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=bob_neuron.hotkey, netuid=1 - ) - is True - ) - assert ( # old key is NOT registered on subnet - subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=bob_old_hotkey_address, netuid=1 - ) - is False - ) - assert ( # uid is unchanged - subtensor.get_uid_for_hotkey_on_subnet(hotkey_ss58=bob_neuron.hotkey, netuid=1) - == bob_neuron.uid - ), "UID for Bob changed on the subnet" - assert new_num_hotkeys == num_hotkeys + 1, "Total hotkeys are not as expected" - logging.info("Passed test_swap_hotkey_miner") diff --git a/tests/e2e_tests/subcommands/root/__init__.py b/tests/e2e_tests/subcommands/root/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/root/test_root_delegate_list.py b/tests/e2e_tests/subcommands/root/test_root_delegate_list.py deleted file mode 100644 index 998bc9057..000000000 --- a/tests/e2e_tests/subcommands/root/test_root_delegate_list.py +++ /dev/null @@ -1,24 +0,0 @@ -from bittensor import logging -from bittensor.commands.delegates import ListDelegatesCommand - -from ...utils import setup_wallet - - -# delegate seems hard code the network config -def test_root_delegate_list(local_chain, capsys): - logging.info("Testing test_root_delegate_list") - alice_keypair, exec_command, wallet = setup_wallet("//Alice") - - # 1200 hardcoded block gap - exec_command( - ListDelegatesCommand, - ["root", "list_delegates"], - ) - - captured = capsys.readouterr() - lines = captured.out.splitlines() - - # the command print too many lines - # To:do - Find a better to validate list delegates - assert len(lines) > 200 - logging.info("Passed test_root_delegate_list") diff --git a/tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py b/tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py deleted file mode 100644 index 7d45e5abc..000000000 --- a/tests/e2e_tests/subcommands/root/test_root_register_add_member_senate.py +++ /dev/null @@ -1,120 +0,0 @@ -import bittensor -from bittensor import logging -from bittensor.commands import ( - NominateCommand, - RegisterCommand, - RegisterSubnetworkCommand, - RootRegisterCommand, - SetTakeCommand, - StakeCommand, -) -from bittensor.commands.senate import SenateCommand - -from ...utils import setup_wallet - - -def test_root_register_add_member_senate(local_chain, capsys): - logging.info("Testing test_root_register_add_member_senate") - # Register root as Alice - the subnet owner - alice_keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - # Stake to become to top neuron after the first epoch - exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - ], - ) - - exec_command(NominateCommand, ["root", "nominate"]) - - exec_command(SetTakeCommand, ["r", "set_take", "--take", "0.8"]) - - captured = capsys.readouterr() - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - # Query local chain for senate members - members = local_chain.query("SenateMembers", "Members").serialize() - assert len(members) == 3, f"Expected 3 senate members, found {len(members)}" - - # Assert subtensor has 3 senate members - subtensor = bittensor.subtensor(network="ws://localhost:9945") - sub_senate = len(subtensor.get_senate_members()) - assert ( - sub_senate == 3 - ), f"Root senate expected 3 members but found {sub_senate} instead." - - # Execute command and capture output - exec_command( - SenateCommand, - ["root", "senate"], - ) - - captured = capsys.readouterr() - - # assert output is graph Titling "Senate" with names and addresses - assert "Senate" in captured.out - assert "NAME" in captured.out - assert "ADDRESS" in captured.out - assert "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL" in captured.out - assert "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" in captured.out - assert "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" in captured.out - - exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--wallet.hotkey", - "default", - "--wallet.name", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - # sudo_call_add_senate_member(local_chain, wallet) - - members = local_chain.query("SenateMembers", "Members").serialize() - assert len(members) == 4, f"Expected 4 senate members, found {len(members)}" - - # Assert subtensor has 4 senate members - subtensor = bittensor.subtensor(network="ws://localhost:9945") - sub_senate = len(subtensor.get_senate_members()) - assert ( - sub_senate == 4 - ), f"Root senate expected 3 members but found {sub_senate} instead." - - exec_command( - SenateCommand, - ["root", "senate"], - ) - - captured = capsys.readouterr() - - # assert output is graph Titling "Senate" with names and addresses - - assert "Senate" in captured.out - assert "NAME" in captured.out - assert "ADDRESS" in captured.out - assert "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL" in captured.out - assert "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" in captured.out - assert "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" in captured.out - assert "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" in captured.out diff --git a/tests/e2e_tests/subcommands/root/test_root_senate_vote.py b/tests/e2e_tests/subcommands/root/test_root_senate_vote.py deleted file mode 100644 index e08df9407..000000000 --- a/tests/e2e_tests/subcommands/root/test_root_senate_vote.py +++ /dev/null @@ -1,49 +0,0 @@ -from bittensor import logging -from bittensor.commands.root import RootRegisterCommand -from bittensor.commands.senate import VoteCommand - -from ...utils import ( - call_add_proposal, - setup_wallet, -) - - -def test_root_senate_vote(local_chain, capsys, monkeypatch): - logging.info("Testing test_root_senate_vote") - keypair, exec_command, wallet = setup_wallet("//Alice") - monkeypatch.setattr("rich.prompt.Confirm.ask", lambda self: True) - - exec_command( - RootRegisterCommand, - ["root", "register"], - ) - - members = local_chain.query("Triumvirate", "Members") - proposals = local_chain.query("Triumvirate", "Proposals").serialize() - - assert len(members) == 3, f"Expected 3 Triumvirate members, found {len(members)}" - assert ( - len(proposals) == 0 - ), f"Expected 0 initial Triumvirate proposals, found {len(proposals)}" - - call_add_proposal(local_chain, wallet) - - proposals = local_chain.query("Triumvirate", "Proposals").serialize() - - assert ( - len(proposals) == 1 - ), f"Expected 1 proposal in the Triumvirate after addition, found {len(proposals)}" - proposal_hash = proposals[0] - - exec_command( - VoteCommand, - ["root", "senate_vote", "--proposal", proposal_hash], - ) - - voting = local_chain.query("Triumvirate", "Voting", [proposal_hash]).serialize() - - assert len(voting["ayes"]) == 1, f"Expected 1 ayes, found {len(voting['ayes'])}" - assert ( - voting["ayes"][0] == wallet.hotkey.ss58_address - ), "wallet hotkey address doesn't match 'ayes' address" - logging.info("Passed test_root_senate_vote") diff --git a/tests/e2e_tests/subcommands/root/test_root_view_proposal.py b/tests/e2e_tests/subcommands/root/test_root_view_proposal.py deleted file mode 100644 index 9de8296e5..000000000 --- a/tests/e2e_tests/subcommands/root/test_root_view_proposal.py +++ /dev/null @@ -1,45 +0,0 @@ -import bittensor -from bittensor import logging -from bittensor.commands.senate import ProposalsCommand - -from ...utils import ( - call_add_proposal, - setup_wallet, -) - - -def test_root_view_proposal(local_chain, capsys): - logging.info("Testing test_root_view_proposal") - keypair, exec_command, wallet = setup_wallet("//Alice") - - proposals = local_chain.query("Triumvirate", "Proposals").serialize() - - assert len(proposals) == 0, "Proposals are not 0" - - call_add_proposal(local_chain, wallet) - - proposals = local_chain.query("Triumvirate", "Proposals").serialize() - - assert len(proposals) == 1, "Added proposal not found" - - exec_command( - ProposalsCommand, - ["root", "proposals"], - ) - - simulated_output = [ - "📡 Syncing with chain: local ...", - " Proposals Active Proposals: 1 Senate Size: 3 ", - "HASH C…", - "0x78b8a348690f565efe3730cd8189f7388c0a896b6fd090276639c9130c0eba47 r…", - " \x00) ", - " ", - ] - - captured = capsys.readouterr() - output = captured.out - - for expected_line in simulated_output: - assert ( - expected_line in output - ), f"Expected '{expected_line}' to be in the output" diff --git a/tests/e2e_tests/subcommands/stake/__init__.py b/tests/e2e_tests/subcommands/stake/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/stake/test_childkeys.py b/tests/e2e_tests/subcommands/stake/test_childkeys.py deleted file mode 100644 index 080d01263..000000000 --- a/tests/e2e_tests/subcommands/stake/test_childkeys.py +++ /dev/null @@ -1,472 +0,0 @@ -import bittensor -import pytest -from bittensor.commands import ( - RegisterCommand, - StakeCommand, - RegisterSubnetworkCommand, - SetChildrenCommand, - RevokeChildrenCommand, - GetChildrenCommand, -) -from bittensor.commands.stake import SetChildKeyTakeCommand, GetChildKeyTakeCommand -from bittensor.extrinsics.staking import prepare_child_proportions -from tests.e2e_tests.utils import setup_wallet, wait_interval - - -@pytest.mark.asyncio -async def test_set_revoke_children_multiple(local_chain, capsys): - """ - Test the setting and revoking of children hotkeys for staking. - - This test case covers the following scenarios: - 1. Setting multiple children hotkeys with specified proportions - 2. Retrieving children information - 3. Revoking all children hotkeys - 4. Verifying the absence of children after revocation - - The test uses three wallets (Alice, Bob, and Eve) and performs operations - on a local blockchain. - - Args: - local_chain: A fixture providing access to the local blockchain - capsys: A pytest fixture for capturing stdout and stderr - - The test performs the following steps: - - Set up wallets for Alice, Bob, and Eve - - Create a subnet and register wallets - - Add stake to Alice's wallet - - Set Bob and Eve as children of Alice with specific proportions - - Verify the children are set correctly - - Get and verify children information - - Revoke all children - - Verify children are revoked - - Check that no children exist after revocation - - This test ensures the proper functioning of setting children hotkeys, - retrieving children information, and revoking children in the staking system. - """ - # Setup - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - eve_keypair, eve_exec_command, eve_wallet = setup_wallet("//Eve") - - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - for exec_command in [alice_exec_command, bob_exec_command, eve_exec_command]: - exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) - - alice_exec_command(StakeCommand, ["stake", "add", "--amount", "100000"]) - - async def wait(): - # wait rate limit, until we are allowed to get children - - rate_limit = ( - subtensor.query_constant( - module_name="SubtensorModule", constant_name="InitialTempo" - ).value - * 2 - ) - curr_block = subtensor.get_current_block() - await wait_interval(rate_limit + curr_block + 1, subtensor) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - await wait() - - children_with_proportions = [ - [0.4, bob_keypair.ss58_address], - [0.2, eve_keypair.ss58_address], - ] - - # Test 1: Set multiple children - alice_exec_command( - SetChildrenCommand, - [ - "stake", - "set_children", - "--netuid", - "1", - "--children", - f"{children_with_proportions[0][1]},{children_with_proportions[1][1]}", - "--hotkey", - str(alice_keypair.ss58_address), - "--proportions", - f"{children_with_proportions[0][0]},{children_with_proportions[1][0]}", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - await wait() - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) - - assert len(children_info) == 2, "Failed to set children hotkeys" - - normalized_proportions = prepare_child_proportions(children_with_proportions) - assert ( - children_info[0][0] == normalized_proportions[0][0] - and children_info[1][0] == normalized_proportions[1][0] - ), "Incorrect proportions set" - - # Test 2: Get children information - alice_exec_command( - GetChildrenCommand, - [ - "stake", - "get_children", - "--netuid", - "1", - "--hotkey", - str(alice_keypair.ss58_address), - ], - ) - output = capsys.readouterr().out - assert "5FHne… │ 40.000%" in output - assert "5HGjW… │ 20.000%" in output - assert "Total │ 60.000%" in output - - await wait() - - # Test 3: Revoke all children - alice_exec_command( - RevokeChildrenCommand, - [ - "stake", - "revoke_children", - "--netuid", - "1", - "--hotkey", - str(alice_keypair.ss58_address), - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - await wait() - - assert ( - subtensor.get_children(netuid=1, hotkey=alice_keypair.ss58_address) == [] - ), "Failed to revoke children hotkeys" - - await wait() - # Test 4: Get children after revocation - alice_exec_command( - GetChildrenCommand, - [ - "stake", - "get_children", - "--netuid", - "1", - "--hotkey", - str(alice_keypair.ss58_address), - ], - ) - output = capsys.readouterr().out - assert "There are currently no child hotkeys on subnet" in output - - -@pytest.mark.asyncio -async def test_set_revoke_childkey_take(local_chain, capsys): - """ - Test the setting and retrieving of childkey take amounts for staking. - - This test case covers the following scenarios: - 1. Setting a childkey take amount for a specific hotkey - 2. Retrieving the childkey take amount - 3. Verifying the retrieved childkey take amount - - The test uses one wallet (Alice) and performs operations - on a local blockchain. - - Args: - local_chain: A fixture providing access to the local blockchain - capsys: A pytest fixture for capturing stdout and stderr - - The test performs the following steps: - - Set up wallets for Alice, Bob, and Eve - - Create a subnet and register wallets - - Set a childkey take amount for Alice - - Verify the setting operation was successful - - Retrieve the set childkey take amount - - Verify the retrieved amount is correct - - This test ensures the proper functioning of setting and retrieving - childkey take amounts in the staking system. - """ - # Setup - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - for exec_command in [alice_exec_command]: - exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) - - # Test 1: Set multiple children - alice_exec_command( - SetChildKeyTakeCommand, - [ - "stake", - "set_childkey_take", - "--netuid", - "1", - "--hotkey", - str(alice_keypair.ss58_address), - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--take", - "0.12", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - output = capsys.readouterr().out - assert ( - "The childkey take for 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY is now \nset to 12.000%." - in output - ) - - # Test 1: Set multiple children - alice_exec_command( - GetChildKeyTakeCommand, - [ - "stake", - "get_childkey_take", - "--netuid", - "1", - "--hotkey", - str(alice_keypair.ss58_address), - ], - ) - - output = capsys.readouterr().out - assert ( - "The childkey take for 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY is \n12.000%." - in output - ) - - -@pytest.mark.asyncio -async def test_set_revoke_children_singular(local_chain, capsys): - """ - Test the setting and revoking of children hotkeys for staking. - - This test case covers the following scenarios: - 1. Setting multiple children hotkeys with specified proportions (set one at a time) - 2. Retrieving children information - 3. Revoking children hotkeys (one at a time) - 4. Verifying the absence of children after revocation - - The test uses three wallets (Alice, Bob, and Eve) and performs operations - on a local blockchain. - - Args: - local_chain: A fixture providing access to the local blockchain - capsys: A pytest fixture for capturing stdout and stderr - - The test performs the following steps: - - Set up wallets for Alice, Bob, and Eve - - Create a subnet and register wallets - - Add stake to Alice's wallet - - Set Bob and Eve as children of Alice with specific proportions - - Verify the children are set correctly - - Get and verify children information - - Revoke all children - - Verify children are revoked - - Check that no children exist after revocation - - This test ensures the proper functioning of setting children hotkeys, - retrieving children information, and revoking children in the staking system. - """ - # Setup - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - eve_keypair, eve_exec_command, eve_wallet = setup_wallet("//Eve") - - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - - for exec_command in [alice_exec_command, bob_exec_command, eve_exec_command]: - exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) - - alice_exec_command(StakeCommand, ["stake", "add", "--amount", "100000"]) - - async def wait(): - # wait rate limit, until we are allowed to get children - - rate_limit = ( - subtensor.query_constant( - module_name="SubtensorModule", constant_name="InitialTempo" - ).value - * 2 - ) - curr_block = subtensor.get_current_block() - await wait_interval(rate_limit + curr_block + 1, subtensor) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - await wait() - - children_with_proportions = [ - [0.6, bob_keypair.ss58_address], - [0.4, eve_keypair.ss58_address], - ] - - # Test 1: Set first children - alice_exec_command( - SetChildrenCommand, - [ - "stake", - "set_children", - "--netuid", - "1", - "--children", - f"{children_with_proportions[0][1]}", - "--hotkey", - str(alice_keypair.ss58_address), - "--proportions", - f"{children_with_proportions[0][0]}", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - output = capsys.readouterr().out - assert "5FHne… │ 60.000%" in output - - await wait() - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) - - assert len(children_info) == 1, "Failed to set child hotkeys" - - # Test 2: Set second child - alice_exec_command( - SetChildrenCommand, - [ - "stake", - "set_children", - "--netuid", - "1", - "--children", - f"{children_with_proportions[1][1]}", - "--hotkey", - str(alice_keypair.ss58_address), - "--proportions", - f"{children_with_proportions[1][0]}", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - await wait() - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) - - assert len(children_info) == 1, "Failed to set child hotkey" - - # Test 2: Get children information - alice_exec_command( - GetChildrenCommand, - [ - "stake", - "get_children", - "--netuid", - "1", - "--hotkey", - str(alice_keypair.ss58_address), - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - ], - ) - output = capsys.readouterr().out - assert "5HGjW… │ 40.000%" in output - - await wait() - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) - assert len(children_info) == 1, "Failed to revoke child hotkey" - - # Test 4: Revoke second child - alice_exec_command( - RevokeChildrenCommand, - [ - "stake", - "revoke_children", - "--netuid", - "1", - "--hotkey", - str(alice_keypair.ss58_address), - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - await wait() - subtensor = bittensor.subtensor(network="ws://localhost:9945") - children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) - assert len(children_info) == 0, "Failed to revoke child hotkey" - - # Test 4: Get children after revocation - alice_exec_command( - GetChildrenCommand, - [ - "stake", - "get_children", - "--netuid", - "1", - "--hotkey", - str(alice_keypair.ss58_address), - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - ], - ) - output = capsys.readouterr().out - assert ( - "There are currently no child hotkeys on subnet 1 with Parent HotKey \n5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY." - in output - ) diff --git a/tests/e2e_tests/subcommands/stake/test_stake_add_remove.py b/tests/e2e_tests/subcommands/stake/test_stake_add_remove.py deleted file mode 100644 index 2598f1fea..000000000 --- a/tests/e2e_tests/subcommands/stake/test_stake_add_remove.py +++ /dev/null @@ -1,81 +0,0 @@ -from bittensor import logging -from bittensor.commands.network import RegisterSubnetworkCommand -from bittensor.commands.register import RegisterCommand -from bittensor.commands.stake import StakeCommand -from bittensor.commands.unstake import UnStakeCommand - -from ...utils import ( - setup_wallet, - sudo_call_set_network_limit, - sudo_call_set_target_stakes_per_interval, -) - - -def test_stake_add(local_chain): - logging.info("Testing test_stake_add") - alice_keypair, exec_command, wallet = setup_wallet("//Alice") - assert sudo_call_set_network_limit( - local_chain, wallet - ), "Unable to set network limit" - assert sudo_call_set_target_stakes_per_interval( - local_chain, wallet - ), "Unable to set target stakes per interval" - - assert not ( - local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() - ), "Subnet was found in netuid 1" - - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - assert local_chain.query( - "SubtensorModule", "NetworksAdded", [1] - ).serialize(), "Subnet 1 was successfully added" - - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlock", [wallet.hotkey.ss58_address] - ).serialize() - == 0 - ), "LastTxBlock is not 0" - - assert ( - local_chain.query( - "SubtensorModule", "LastTxBlockDelegateTake", [wallet.hotkey.ss58_address] - ).serialize() - == 0 - ), "LastTxBlockDelegateTake is not 0" - - exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) - - assert ( - local_chain.query( - "SubtensorModule", "TotalHotkeyStake", [wallet.hotkey.ss58_address] - ).serialize() - == 0 - ), "TotalHotkeyStake is not 0" - - stake_amount = 2 - exec_command(StakeCommand, ["stake", "add", "--amount", str(stake_amount)]) - exact_stake = local_chain.query( - "SubtensorModule", "TotalHotkeyStake", [wallet.hotkey.ss58_address] - ).serialize() - withdraw_loss = 1_000_000 - stake_amount_in_rao = stake_amount * 1_000_000_000 - - assert ( - stake_amount_in_rao - withdraw_loss < exact_stake <= stake_amount_in_rao - ), f"Stake amount mismatch: expected {exact_stake} to be between {stake_amount_in_rao - withdraw_loss} and {stake_amount_in_rao}" - - # we can test remove after set the stake rate limit larger than 1 - remove_amount = 1 - - exec_command(UnStakeCommand, ["stake", "remove", "--amount", str(remove_amount)]) - total_hotkey_stake = local_chain.query( - "SubtensorModule", "TotalHotkeyStake", [wallet.hotkey.ss58_address] - ).serialize() - expected_stake = exact_stake - remove_amount * 1_000_000_000 - assert ( - total_hotkey_stake == expected_stake - ), f"Unstake amount mismatch: expected {expected_stake}, but got {total_hotkey_stake}" - - logging.info("Passed test_stake_add") diff --git a/tests/e2e_tests/subcommands/stake/test_stake_show.py b/tests/e2e_tests/subcommands/stake/test_stake_show.py deleted file mode 100644 index af155ffc2..000000000 --- a/tests/e2e_tests/subcommands/stake/test_stake_show.py +++ /dev/null @@ -1,37 +0,0 @@ -from bittensor import logging -from bittensor.commands.stake import StakeShow - -from ...utils import setup_wallet - - -def test_stake_show(local_chain, capsys): - logging.info("Testing test_stake_show") - keypair, exec_command, wallet = setup_wallet("//Alice") - - # Execute the command - exec_command(StakeShow, ["stake", "show"]) - captured = capsys.readouterr() - output = captured.out - - # Check the header line - assert "Coldkey" in output, "Output missing 'Coldkey'." - assert "Balance" in output, "Output missing 'Balance'." - assert "Account" in output, "Output missing 'Account'." - assert "Stake" in output, "Output missing 'Stake'." - assert "Rate" in output, "Output missing 'Rate'." - - # Check the first line of data - assert "default" in output, "Output missing 'default'." - assert "1000000.000000" in output.replace( - "τ", "" - ), "Output missing '1000000.000000'." - - # Check the second line of data - assert "0.000000" in output.replace("τ", ""), "Output missing '0.000000'." - assert "0/d" in output, "Output missing '0/d'." - - # Check the third line of data - - assert "1000000.00000" in output.replace("τ", ""), "Output missing '1000000.00000'." - assert "0.00000" in output.replace("τ", ""), "Output missing '0.00000'." - assert "0.00000/d" in output.replace("τ", ""), "Output missing '0.00000/d'." diff --git a/tests/e2e_tests/subcommands/subnet/__init__.py b/tests/e2e_tests/subcommands/subnet/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/subnet/test_list.py b/tests/e2e_tests/subcommands/subnet/test_list.py deleted file mode 100644 index 74b79a2df..000000000 --- a/tests/e2e_tests/subcommands/subnet/test_list.py +++ /dev/null @@ -1,29 +0,0 @@ -import bittensor -from bittensor.commands import RegisterSubnetworkCommand -from tests.e2e_tests.utils import setup_wallet - -""" -Test the list command before and after registering subnets. - -Verify that: -* list of subnets gets displayed -------------------------- -* Register a subnets -* Ensure is visible in list cmd -""" - - -def test_list_command(local_chain, capsys): - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - - netuid = 0 - - assert local_chain.query("SubtensorModule", "NetworksAdded", [netuid]).serialize() - - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - netuid - 1 - - # Verify subnet 1 created successfully - assert local_chain.query("SubtensorModule", "NetworksAdded", [netuid]).serialize() diff --git a/tests/e2e_tests/subcommands/subnet/test_metagraph.py b/tests/e2e_tests/subcommands/subnet/test_metagraph.py deleted file mode 100644 index a77876c2b..000000000 --- a/tests/e2e_tests/subcommands/subnet/test_metagraph.py +++ /dev/null @@ -1,122 +0,0 @@ -import bittensor -from bittensor import logging -from bittensor.commands import ( - MetagraphCommand, - RegisterCommand, - RegisterSubnetworkCommand, -) -from tests.e2e_tests.utils import setup_wallet - -""" -Test the metagraph command before and after registering neurons. - -Verify that: -* Metagraph gets displayed -* Initially empty -------------------------- -* Register 2 neurons one by one -* Ensure both are visible in metagraph -""" - - -def test_metagraph_command(local_chain, capsys): - logging.info("Testing test_metagraph_command") - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # Verify subnet 1 created successfully - assert local_chain.query( - "SubtensorModule", "NetworksAdded", [1] - ).serialize(), "Subnet wasn't created successfully" - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - metagraph = subtensor.metagraph(netuid=1) - - # Assert metagraph is empty - assert len(metagraph.uids) == 0, "Metagraph is not empty" - - # Execute btcli metagraph command - exec_command(MetagraphCommand, ["subnet", "metagraph", "--netuid", "1"]) - - captured = capsys.readouterr() - - # Assert metagraph is printed for netuid 1 - - assert ( - "Metagraph: net: local:1" in captured.out - ), "Netuid 1 was not displayed in metagraph" - - # Register Bob as neuron to the subnet - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") - bob_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - captured = capsys.readouterr() - - # Assert neuron was registered - - assert "✅ Registered" in captured.out, "Neuron was not registered" - - # Refresh the metagraph - metagraph = subtensor.metagraph(netuid=1) - - # Assert metagraph has registered neuron - assert len(metagraph.uids) == 1, "Metagraph doesn't have exactly 1 neuron" - assert ( - metagraph.hotkeys[0] == "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - ), "Neuron's hotkey in metagraph doesn't match" - # Execute btcli metagraph command - exec_command(MetagraphCommand, ["subnet", "metagraph", "--netuid", "1"]) - - captured = capsys.readouterr() - - # Assert the neuron is registered and displayed - assert "Metagraph: net: local:1" in captured.out - assert "N: 1/1" in captured.out, "Neuron isn't displayed in metagraph" - - # Register Dave as neuron to the subnet - dave_keypair, dave_exec_command, dave_wallet = setup_wallet("//Dave") - dave_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - captured = capsys.readouterr() - - # Assert neuron was registered - - assert "✅ Registered" in captured.out, "Neuron was not registered" - - # Refresh the metagraph - metagraph = subtensor.metagraph(netuid=1) - - # Assert metagraph has registered neuron - assert len(metagraph.uids) == 2 - assert ( - metagraph.hotkeys[1] == "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" - ), "Neuron's hotkey in metagraph doesn't match" - - # Execute btcli metagraph command - exec_command(MetagraphCommand, ["subnet", "metagraph", "--netuid", "1"]) - - captured = capsys.readouterr() - - # Assert the neuron is registered and displayed - assert "Metagraph: net: local:1" in captured.out - assert "N: 2/2" in captured.out - - logging.info("Passed test_metagraph_command") diff --git a/tests/e2e_tests/subcommands/wallet/__init__.py b/tests/e2e_tests/subcommands/wallet/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/wallet/test_faucet.py b/tests/e2e_tests/subcommands/wallet/test_faucet.py deleted file mode 100644 index 64ae2b7f8..000000000 --- a/tests/e2e_tests/subcommands/wallet/test_faucet.py +++ /dev/null @@ -1,92 +0,0 @@ -import pytest - -import bittensor -from bittensor import logging -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, - RunFaucetCommand, -) -from tests.e2e_tests.utils import ( - setup_wallet, -) - - -@pytest.mark.skip -@pytest.mark.parametrize("local_chain", [False], indirect=True) -def test_faucet(local_chain): - logging.info("Testing test_faucet") - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # Verify subnet 1 created successfully - assert local_chain.query( - "SubtensorModule", "NetworksAdded", [1] - ).serialize(), "Subnet wasn't created successfully" - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--no_prompt", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # verify current balance - wallet_balance = subtensor.get_balance(keypair.ss58_address) - assert wallet_balance.tao == 998999.0, "Balance wasn't as expected" - - # run faucet 3 times - for i in range(3): - logging.info(f"faucet run #:{i + 1}") - try: - exec_command( - RunFaucetCommand, - [ - "wallet", - "faucet", - "--wallet.name", - wallet.name, - "--wallet.hotkey", - "default", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - logging.info( - f"wallet balance is {subtensor.get_balance(keypair.ss58_address).tao} tao" - ) - except SystemExit as e: - logging.warning( - "Block not generated fast enough to be within 3 block seconds window." - ) - # Handle the SystemExit exception - assert e.code == 1 # Assert that the exit code is 1 - except Exception as e: - logging.warning(f"Unexpected exception occurred on faucet: {e}") - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - new_wallet_balance = subtensor.get_balance(keypair.ss58_address) - # verify balance increase - assert ( - wallet_balance.tao < new_wallet_balance.tao - ), "Old wallet balance is not less than the new wallet" - logging.info("Passed test_faucet") diff --git a/tests/e2e_tests/subcommands/wallet/test_list.py b/tests/e2e_tests/subcommands/wallet/test_list.py deleted file mode 100644 index 15f34514b..000000000 --- a/tests/e2e_tests/subcommands/wallet/test_list.py +++ /dev/null @@ -1,72 +0,0 @@ -from bittensor.commands.list import ListCommand -from bittensor.commands.wallets import WalletCreateCommand -from bittensor.subtensor import subtensor - -from ...utils import setup_wallet - - -def test_wallet_list(capsys): - """ - Test the listing of wallets in the Bittensor network. - - Steps: - 1. Set up a default wallet - 2. List existing wallets and verify the default setup - 3. Create a new wallet - 4. List wallets again and verify the new wallet is present - - Raises: - AssertionError: If any of the checks or verifications fail - """ - - wallet_path_name = "//Alice" - base_path = f"/tmp/btcli-e2e-wallet-list-{wallet_path_name.strip('/')}" - keypair, exec_command, wallet = setup_wallet(wallet_path_name) - - # List initial wallets - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - - captured = capsys.readouterr() - # Assert the default wallet is present in the display - assert "default" in captured.out - assert "└── default" in captured.out - - # Create a new wallet - exec_command( - WalletCreateCommand, - [ - "wallet", - "create", - "--wallet.name", - "new_wallet", - "--wallet.hotkey", - "new_hotkey", - "--no_password", - "--overwrite_coldkey", - "--overwrite_hotkey", - "--no_prompt", - "--wallet.path", - base_path, - ], - ) - - # List wallets again - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - - captured = capsys.readouterr() - - # Verify the new wallet is displayed - assert "new_wallet" in captured.out - assert "new_hotkey" in captured.out diff --git a/tests/e2e_tests/subcommands/wallet/test_transfer.py b/tests/e2e_tests/subcommands/wallet/test_transfer.py deleted file mode 100644 index 9d1bd2692..000000000 --- a/tests/e2e_tests/subcommands/wallet/test_transfer.py +++ /dev/null @@ -1,35 +0,0 @@ -from bittensor import logging -from bittensor.commands.transfer import TransferCommand - -from ...utils import setup_wallet - - -# Example test using the local_chain fixture -def test_transfer(local_chain): - logging.info("Testing test_transfer") - keypair, exec_command, wallet = setup_wallet("//Alice") - - acc_before = local_chain.query("System", "Account", [keypair.ss58_address]) - exec_command( - TransferCommand, - [ - "wallet", - "transfer", - "--amount", - "2", - "--dest", - "5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx", - ], - ) - acc_after = local_chain.query("System", "Account", [keypair.ss58_address]) - - expected_transfer = 2_000_000_000 - tolerance = 200_000 # Tx fee tolerance - - actual_difference = ( - acc_before.value["data"]["free"] - acc_after.value["data"]["free"] - ) - assert ( - expected_transfer <= actual_difference <= expected_transfer + tolerance - ), f"Expected transfer with tolerance: {expected_transfer} <= {actual_difference} <= {expected_transfer + tolerance}" - logging.info("Passed test_transfer") diff --git a/tests/e2e_tests/subcommands/wallet/test_wallet_creations.py b/tests/e2e_tests/subcommands/wallet/test_wallet_creations.py deleted file mode 100644 index 78a235ad2..000000000 --- a/tests/e2e_tests/subcommands/wallet/test_wallet_creations.py +++ /dev/null @@ -1,505 +0,0 @@ -import os -import re -import time -from typing import Dict, Optional, Tuple - -from bittensor import logging -from bittensor.commands.list import ListCommand -from bittensor.commands.wallets import ( - NewColdkeyCommand, - NewHotkeyCommand, - RegenColdkeyCommand, - RegenColdkeypubCommand, - RegenHotkeyCommand, - WalletCreateCommand, -) -from bittensor.subtensor import subtensor - -from ...utils import setup_wallet - -""" -Verify commands: - -* btcli w list -* btcli w create -* btcli w new_coldkey -* btcli w new_hotkey -* btcli w regen_coldkey -* btcli w regen_coldkeypub -* btcli w regen_hotkey -""" - - -def verify_wallet_dir( - base_path: str, - wallet_name: str, - hotkey_name: Optional[str] = None, - coldkeypub_name: Optional[str] = None, -) -> Tuple[bool, str]: - """ - Verifies the existence of wallet directory, coldkey, and optionally the hotkey. - - Args: - base_path (str): The base directory path where wallets are stored. - wallet_name (str): The name of the wallet directory to verify. - hotkey_name (str, optional): The name of the hotkey file to verify. If None, - only the wallet and coldkey file are checked. - coldkeypub_name (str, optional): The name of the coldkeypub file to verify. If None - only the wallet and coldkey is checked - - Returns: - tuple: Returns a tuple containing a boolean and a message. The boolean is True if - all checks pass, otherwise False. - """ - wallet_path = os.path.join(base_path, wallet_name) - - # Check if wallet directory exists - if not os.path.isdir(wallet_path): - return False, f"Wallet directory {wallet_name} not found in {base_path}" - - # Check if coldkey file exists - coldkey_path = os.path.join(wallet_path, "coldkey") - if not os.path.isfile(coldkey_path): - return False, f"Coldkey file not found in {wallet_name}" - - # Check if coldkeypub exists - if coldkeypub_name: - coldkeypub_path = os.path.join(wallet_path, coldkeypub_name) - if not os.path.isfile(coldkeypub_path): - return False, f"Coldkeypub file not found in {wallet_name}" - - # Check if hotkey directory and file exists - if hotkey_name: - hotkeys_path = os.path.join(wallet_path, "hotkeys") - if not os.path.isdir(hotkeys_path): - return False, f"Hotkeys directory not found in {wallet_name}" - - hotkey_file_path = os.path.join(hotkeys_path, hotkey_name) - if not os.path.isfile(hotkey_file_path): - return ( - False, - f"Hotkey file {hotkey_name} not found in {wallet_name}/hotkeys", - ) - - return True, f"Wallet {wallet_name} verified successfully" - - -def verify_key_pattern(output: str, wallet_name: str) -> Optional[str]: - """ - Verifies that a specific wallet key pattern exists in the output text. - - Args: - output (str): The string output where the wallet key should be verified. - wallet_name (str): The name of the wallet to search for in the output. - - Raises: - AssertionError: If the wallet key pattern is not found, or if the key does not - start with '5', or if the key is not exactly 48 characters long. - """ - split_output = output.splitlines() - pattern = rf"{wallet_name}\s*\((5[A-Za-z0-9]{{47}})\)" - found = False - - # Traverse each line to find instance of the pattern - for line in split_output: - match = re.search(pattern, line) - if match: - # Assert key starts with '5' - assert match.group(1).startswith( - "5" - ), f"{wallet_name} should start with '5'" - # Assert length of key is 48 characters - assert ( - len(match.group(1)) == 48 - ), f"Key for {wallet_name} should be 48 characters long" - found = True - return match.group(1) - - # If no match is found in any line, raise an assertion error - assert found, f"{wallet_name} not found in wallet list" - return None - - -def extract_ss58_address(output: str, wallet_name: str) -> str: - """ - Extracts the ss58 address from the given output for a specified wallet. - - Args: - output (str): The captured output. - wallet_name (str): The name of the wallet. - - Returns: - str: ss58 address. - """ - pattern = rf"{wallet_name}\s*\((5[A-Za-z0-9]{{47}})\)" - lines = output.splitlines() - for line in lines: - match = re.search(pattern, line) - if match: - return match.group(1) # Return the ss58 address - - raise ValueError(f"ss58 address not found for wallet {wallet_name}") - - -def extract_mnemonics_from_commands(output: str) -> Dict[str, Optional[str]]: - """ - Extracts mnemonics of coldkeys & hotkeys from the given output for a specified wallet. - - Args: - output (str): The captured output. - - Returns: - dict: A dictionary keys 'coldkey' and 'hotkey', each containing their mnemonics. - """ - mnemonics: Dict[str, Optional[str]] = {"coldkey": None, "hotkey": None} - lines = output.splitlines() - - # Regex pattern to capture the mnemonic - pattern = re.compile(r"btcli w regen_(coldkey|hotkey) --mnemonic ([a-z ]+)") - - for line in lines: - line = line.strip().lower() - match = pattern.search(line) - if match: - key_type = match.group(1) # 'coldkey' or 'hotkey' - mnemonic_phrase = match.group(2).strip() - mnemonics[key_type] = mnemonic_phrase - - return mnemonics - - -def test_wallet_creations(local_chain: subtensor, capsys): - """ - Test the creation and verification of wallet keys and directories in the Bittensor network. - - Steps: - 1. List existing wallets and verify the default setup. - 2. Create a new wallet with both coldkey and hotkey, verify their presence in the output, - and check their physical existence. - 3. Create a new coldkey and verify both its display in the command line output and its physical file. - 4. Create a new hotkey for an existing coldkey, verify its display in the command line output, - and check for both coldkey and hotkey files. - - Raises: - AssertionError: If any of the checks or verifications fail - """ - - logging.info("Testing test_wallet_creations (create, new_hotkey, new_coldkey)") - wallet_path_name = "//Alice" - base_path = f"/tmp/btcli-e2e-wallet-{wallet_path_name.strip('/')}" - keypair, exec_command, wallet = setup_wallet(wallet_path_name) - - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - - captured = capsys.readouterr() - # Assert the coldkey and hotkey are present in the display with keys - assert ( - "default" and "└── default" in captured.out - ), "Default wallet not found in wallet list" - wallet_status, message = verify_wallet_dir( - base_path, "default", hotkey_name="default" - ) - assert wallet_status, message - - # ----------------------------- - # Command 1: - # ----------------------------- - # Create a new wallet (coldkey + hotkey) - logging.info("Testing wallet create command") - exec_command( - WalletCreateCommand, - [ - "wallet", - "create", - "--wallet.name", - "new_wallet", - "--wallet.hotkey", - "new_hotkey", - "--no_password", - "--overwrite_coldkey", - "--overwrite_hotkey", - "--no_prompt", - "--wallet.path", - base_path, - ], - ) - - captured = capsys.readouterr() - - # List the wallets - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - - captured = capsys.readouterr() - - # Verify coldkey "new_wallet" is displayed with key - verify_key_pattern(captured.out, "new_wallet") - - # Verify hotkey "new_hotkey" is displayed with key - verify_key_pattern(captured.out, "new_hotkey") - - # Physically verify "new_wallet" and "new_hotkey" are present - wallet_status, message = verify_wallet_dir( - base_path, "new_wallet", hotkey_name="new_hotkey" - ) - assert wallet_status, message - - # ----------------------------- - # Command 2: - # ----------------------------- - # Create a new wallet (coldkey) - logging.info("Testing wallet new_coldkey command") - exec_command( - NewColdkeyCommand, - [ - "wallet", - "new_coldkey", - "--wallet.name", - "new_coldkey", - "--no_password", - "--no_prompt", - "--overwrite_coldkey", - "--wallet.path", - base_path, - ], - ) - - captured = capsys.readouterr() - - # List the wallets - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - - captured = capsys.readouterr() - - # Verify coldkey "new_coldkey" is displayed with key - verify_key_pattern(captured.out, "new_coldkey") - - # Physically verify "new_coldkey" is present - wallet_status, message = verify_wallet_dir(base_path, "new_coldkey") - assert wallet_status, message - - # ----------------------------- - # Command 3: - # ----------------------------- - # Create a new hotkey for alice_new_coldkey wallet - logging.info("Testing wallet new_hotkey command") - exec_command( - NewHotkeyCommand, - [ - "wallet", - "new_hotkey", - "--wallet.name", - "new_coldkey", - "--wallet.hotkey", - "new_hotkey", - "--no_prompt", - "--overwrite_hotkey", - "--wallet.path", - base_path, - ], - ) - - captured = capsys.readouterr() - - # List the wallets - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - captured = capsys.readouterr() - - # Verify hotkey "alice_new_hotkey" is displyed with key - verify_key_pattern(captured.out, "new_hotkey") - - # Physically verify "alice_new_coldkey" and "alice_new_hotkey" are present - wallet_status, message = verify_wallet_dir( - base_path, "new_coldkey", hotkey_name="new_hotkey" - ) - assert wallet_status, message - logging.info("Passed test_wallet_creations") - - -def test_wallet_regen(local_chain: subtensor, capsys): - """ - Test the regeneration of coldkeys, hotkeys, and coldkeypub files using mnemonics or ss58 address. - - Steps: - 1. List existing wallets and verify the default setup. - 2. Regenerate the coldkey using the mnemonics and verify using mod time. - 3. Regenerate the coldkeypub using ss58 address and verify using mod time - 4. Regenerate the hotkey using mnemonics and verify using mod time. - - Raises: - AssertionError: If any of the checks or verifications fail - """ - logging.info( - "Testing test_wallet_regen (regen_coldkey, regen_hotkey, regen_coldkeypub)" - ) - wallet_path_name = "//Bob" - base_path = f"/tmp/btcli-e2e-wallet-{wallet_path_name.strip('/')}" - keypair, exec_command, wallet = setup_wallet(wallet_path_name) - - # Create a new wallet (coldkey + hotkey) - exec_command( - WalletCreateCommand, - [ - "wallet", - "create", - "--wallet.name", - "new_wallet", - "--wallet.hotkey", - "new_hotkey", - "--no_password", - "--overwrite_coldkey", - "--overwrite_hotkey", - "--no_prompt", - "--wallet.path", - base_path, - ], - ) - - captured = capsys.readouterr() - mnemonics = extract_mnemonics_from_commands(captured.out) - - wallet_status, message = verify_wallet_dir( - base_path, - "new_wallet", - hotkey_name="new_hotkey", - coldkeypub_name="coldkeypub.txt", - ) - assert wallet_status, message # Ensure wallet exists - - # ----------------------------- - # Command 1: - # ----------------------------- - - logging.info("Testing w regen_coldkey") - coldkey_path = os.path.join(base_path, "new_wallet", "coldkey") - initial_coldkey_mod_time = os.path.getmtime(coldkey_path) - - exec_command( - RegenColdkeyCommand, - [ - "wallet", - "regen_coldkey", - "--wallet.name", - "new_wallet", - "--wallet.path", - base_path, - "--no_prompt", - "--overwrite_coldkey", - "--mnemonic", - mnemonics["coldkey"], - "--no_password", - ], - ) - - # Wait a bit to ensure file system updates modification time - time.sleep(1) - - new_coldkey_mod_time = os.path.getmtime(coldkey_path) - - assert ( - initial_coldkey_mod_time != new_coldkey_mod_time - ), "Coldkey file was not regenerated as expected" - - # ----------------------------- - # Command 2: - # ----------------------------- - - logging.info("Testing w regen_coldkeypub") - coldkeypub_path = os.path.join(base_path, "new_wallet", "coldkeypub.txt") - initial_coldkeypub_mod_time = os.path.getmtime(coldkeypub_path) - - # List the wallets - exec_command( - ListCommand, - [ - "wallet", - "list", - ], - ) - captured = capsys.readouterr() - ss58_address = extract_ss58_address(captured.out, "new_wallet") - - exec_command( - RegenColdkeypubCommand, - [ - "wallet", - "regen_coldkeypub", - "--wallet.name", - "new_wallet", - "--wallet.path", - base_path, - "--no_prompt", - "--overwrite_coldkeypub", - "--ss58_address", - ss58_address, - ], - ) - - # Wait a bit to ensure file system updates modification time - time.sleep(1) - - new_coldkeypub_mod_time = os.path.getmtime(coldkeypub_path) - - assert ( - initial_coldkeypub_mod_time != new_coldkeypub_mod_time - ), "Coldkeypub file was not regenerated as expected" - - # ----------------------------- - # Command 3: - # ----------------------------- - - logging.info("Testing w regen_hotkey") - hotkey_path = os.path.join(base_path, "new_wallet", "hotkeys", "new_hotkey") - initial_hotkey_mod_time = os.path.getmtime(hotkey_path) - - exec_command( - RegenHotkeyCommand, - [ - "wallet", - "regen_hotkey", - "--no_prompt", - "--overwrite_hotkey", - "--wallet.name", - "new_wallet", - "--wallet.hotkey", - "new_hotkey", - "--wallet.path", - base_path, - "--mnemonic", - mnemonics["hotkey"], - ], - ) - - # Wait a bit to ensure file system updates modification time - time.sleep(1) - - new_hotkey_mod_time = os.path.getmtime(hotkey_path) - - assert ( - initial_hotkey_mod_time != new_hotkey_mod_time - ), "Hotkey file was not regenerated as expected" - - logging.info("Passed test_wallet_regen") diff --git a/tests/e2e_tests/subcommands/weights/__init__.py b/tests/e2e_tests/subcommands/weights/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/e2e_tests/subcommands/weights/test_commit_weights.py b/tests/e2e_tests/subcommands/weights/test_commit_weights.py deleted file mode 100644 index c53746be8..000000000 --- a/tests/e2e_tests/subcommands/weights/test_commit_weights.py +++ /dev/null @@ -1,246 +0,0 @@ -import asyncio -import re - -import numpy as np -import pytest - -import bittensor -import bittensor.utils.weight_utils as weight_utils -from bittensor import logging -from bittensor.commands import ( - CommitWeightCommand, - RegisterCommand, - RegisterSubnetworkCommand, - RevealWeightCommand, - StakeCommand, - SubnetSudoCommand, -) -from tests.e2e_tests.utils import setup_wallet, wait_interval - -""" -Test the Commit/Reveal weights mechanism. - -Verify that: -* Weights are commited -* weights are hashed with salt ---- after an epoch --- -* weights are un-hashed with salt -* weights are properly revealed - -""" - - -@pytest.mark.asyncio -async def test_commit_and_reveal_weights(local_chain): - logging.info("Testing test_commit_and_reveal_weights") - # Register root as Alice - keypair, exec_command, wallet = setup_wallet("//Alice") - - exec_command(RegisterSubnetworkCommand, ["s", "create"]) - - # define values - weights = 0.1 - uid = 0 - salt = "18, 179, 107, 0, 165, 211, 141, 197" - - # Verify subnet 1 created successfully - assert local_chain.query( - "SubtensorModule", "NetworksAdded", [1] - ).serialize(), "Subnet wasn't created successfully" - - # Register a neuron to the subnet - exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - "1", - ], - ) - - # Stake to become to top neuron after the first epoch - exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "100000", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - - # Enable Commit Reveal - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "commit_reveal_weights_enabled", - "--value", - "True", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - assert subtensor.get_subnet_hyperparameters( - netuid=1 - ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" - - # Lower the interval - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "commit_reveal_weights_interval", - "--value", - "370", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).commit_reveal_weights_interval - == 370 - ), "Failed to set commit/reveal interval" - - # Lower the rate limit - exec_command( - SubnetSudoCommand, - [ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--wallet.name", - wallet.name, - "--param", - "weights_rate_limit", - "--value", - "0", - "--wait_for_inclusion", - "True", - "--wait_for_finalization", - "True", - ], - ) - - subtensor = bittensor.subtensor(network="ws://localhost:9945") - assert ( - subtensor.get_subnet_hyperparameters(netuid=1).weights_rate_limit == 0 - ), "Failed to set commit/reveal rate limit" - - # Configure the CLI arguments for the CommitWeightCommand - exec_command( - CommitWeightCommand, - [ - "wt", - "commit", - "--no_prompt", - "--netuid", - "1", - "--uids", - str(uid), - "--weights", - str(weights), - "--salt", - str(salt), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - "/tmp/btcli-wallet", - ], - ) - - weight_commits = subtensor.query_module( - module="SubtensorModule", - name="WeightCommits", - params=[1, wallet.hotkey.ss58_address], - ) - - # Assert that the committed weights are set correctly - assert weight_commits.value is not None, "Weight commit not found in storage" - commit_hash, commit_block = weight_commits.value - assert commit_block > 0, f"Invalid block number: {commit_block}" - - # Query the WeightCommitRevealInterval storage map - weight_commit_reveal_interval = subtensor.query_module( - module="SubtensorModule", name="WeightCommitRevealInterval", params=[1] - ) - interval = weight_commit_reveal_interval.value - assert interval > 0, "Invalid WeightCommitRevealInterval" - - # Wait until the reveal block range - await wait_interval(interval, subtensor) - - # Configure the CLI arguments for the RevealWeightCommand - exec_command( - RevealWeightCommand, - [ - "wt", - "reveal", - "--no_prompt", - "--netuid", - "1", - "--uids", - str(uid), - "--weights", - str(weights), - "--salt", - str(salt), - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - "/tmp/btcli-wallet", - ], - ) - - # Query the Weights storage map - revealed_weights = subtensor.query_module( - module="SubtensorModule", - name="Weights", - params=[1, uid], # netuid and uid - ) - - # Assert that the revealed weights are set correctly - assert revealed_weights.value is not None, "Weight reveal not found in storage" - - uid_list = [int(x) for x in re.split(r"[ ,]+", str(uid))] - uids = np.array(uid_list, dtype=np.int64) - weight_list = [float(x) for x in re.split(r"[ ,]+", str(weights))] - weights_array = np.array(weight_list, dtype=np.float32) - weight_uids, expected_weights = weight_utils.convert_weights_and_uids_for_emit( - uids, weights_array - ) - assert ( - expected_weights[0] == revealed_weights.value[0][1] - ), f"Incorrect revealed weights. Expected: {expected_weights[0]}, Actual: {revealed_weights.value[0][1]}" - logging.info("Passed test_commit_and_reveal_weights") diff --git a/tests/e2e_tests/test_axon.py b/tests/e2e_tests/test_axon.py new file mode 100644 index 000000000..853719f85 --- /dev/null +++ b/tests/e2e_tests/test_axon.py @@ -0,0 +1,128 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import logging +from bittensor.utils import networking +from tests.e2e_tests.utils.chain_interactions import register_neuron, register_subnet +from tests.e2e_tests.utils.e2e_test_utils import ( + setup_wallet, + template_path, + templates_repo, +) + + +@pytest.mark.asyncio +async def test_axon(local_chain): + """ + Test the Axon mechanism and successful registration on the network. + + 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 + 4. Check the metagraph again after running the miner and verify all attributes + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_axon") + + netuid = 1 + # Register root as Alice - the subnet owner + alice_keypair, wallet = setup_wallet("//Alice") + + # Register a subnet, netuid 1 + assert register_subnet(local_chain, wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" + + # Register Alice to the network + assert register_neuron( + local_chain, wallet, netuid + ), f"Neuron wasn't registered to subnet {netuid}" + + metagraph = bittensor.Metagraph(netuid=netuid, network="ws://localhost:9945") + + # Validate current metagraph stats + old_axon = metagraph.axons[0] + assert len(metagraph.axons) == 1, f"Expected 1 axon, but got {len(metagraph.axons)}" + assert old_axon.hotkey == alice_keypair.ss58_address, "Hotkey mismatch for the axon" + assert ( + old_axon.coldkey == alice_keypair.ss58_address + ), "Coldkey mismatch for the axon" + assert old_axon.ip == "0.0.0.0", f"Expected IP 0.0.0.0, but got {old_axon.ip}" + assert old_axon.port == 0, f"Expected port 0, but got {old_axon.port}" + assert old_axon.ip_type == 0, f"Expected IP type 0, but got {old_axon.ip_type}" + + # Prepare to run the miner + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/miner.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + wallet.path, + "--wallet.name", + wallet.name, + "--wallet.hotkey", + "default", + ] + ) + + # Run the miner in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + logging.info("Neuron Alice is now mining") + + # Waiting for 5 seconds for metagraph to be updated + await asyncio.sleep(5) + + # Refresh the metagraph + metagraph = bittensor.Metagraph(netuid=netuid, network="ws://localhost:9945") + updated_axon = metagraph.axons[0] + external_ip = networking.get_external_ip() + + # Assert updated attributes + assert ( + len(metagraph.axons) == 1 + ), f"Expected 1 axon, but got {len(metagraph.axons)} after mining" + + assert ( + len(metagraph.neurons) == 1 + ), f"Expected 1 neuron, but got {len(metagraph.neurons)}" + + assert ( + updated_axon.ip == external_ip + ), f"Expected IP {external_ip}, but got {updated_axon.ip}" + + assert ( + updated_axon.ip_type == networking.ip_version(external_ip) + ), f"Expected IP type {networking.ip_version(external_ip)}, but got {updated_axon.ip_type}" + + assert updated_axon.port == 8091, f"Expected port 8091, but got {updated_axon.port}" + + assert ( + updated_axon.hotkey == alice_keypair.ss58_address + ), "Hotkey mismatch after mining" + + assert ( + updated_axon.coldkey == alice_keypair.ss58_address + ), "Coldkey mismatch after mining" + + logging.info("✅ Passed test_axon") diff --git a/tests/e2e_tests/test_commit_weights.py b/tests/e2e_tests/test_commit_weights.py new file mode 100644 index 000000000..ca9b0a0a2 --- /dev/null +++ b/tests/e2e_tests/test_commit_weights.py @@ -0,0 +1,165 @@ +import time + +import numpy as np +import pytest + +import bittensor +from bittensor import logging +from bittensor.utils.weight_utils import convert_weights_and_uids_for_emit +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, + sudo_set_hyperparameter_bool, + sudo_set_hyperparameter_values, + wait_interval, +) +from tests.e2e_tests.utils.e2e_test_utils import setup_wallet + + +@pytest.mark.asyncio +async def test_commit_and_reveal_weights(local_chain): + """ + Tests the commit/reveal weights mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Enable commit-reveal mechanism on the subnet + 4. Lower the commit_reveal interval and rate limit + 5. Commit weights and verify + 6. Wait interval & reveal weights and verify + Raises: + AssertionError: If any of the checks or verifications fail + """ + netuid = 1 + logging.info("Testing test_commit_and_reveal_weights") + # Register root as Alice + keypair, alice_wallet = setup_wallet("//Alice") + assert register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Verify subnet 1 created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet wasn't created successfully" + + assert register_neuron( + local_chain, alice_wallet, netuid + ), "Unable to register Alice as a neuron" + + # Stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(100_000)) + + # Enable commit_reveal on the subnet + assert sudo_set_hyperparameter_bool( + local_chain, + alice_wallet, + "sudo_set_commit_reveal_weights_enabled", + True, + netuid, + ), "Unable to enable commit reveal on the subnet" + + subtensor = bittensor.Subtensor(network="ws://localhost:9945") + assert subtensor.get_subnet_hyperparameters( + netuid=netuid + ).commit_reveal_weights_enabled, "Failed to enable commit/reveal" + + # Lower the commit_reveal interval + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_commit_reveal_weights_interval", + call_params={"netuid": netuid, "interval": "370"}, + return_error_message=True, + ) + + subtensor = bittensor.Subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters( + netuid=netuid + ).commit_reveal_weights_interval + == 370 + ), "Failed to set commit/reveal interval" + + assert ( + subtensor.weights_rate_limit(netuid=netuid) > 0 + ), "Weights rate limit is below 0" + # Lower the rate limit + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_weights_set_rate_limit", + call_params={"netuid": netuid, "weights_set_rate_limit": "0"}, + return_error_message=True, + ) + subtensor = bittensor.Subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters(netuid=netuid).weights_rate_limit == 0 + ), "Failed to set weights_rate_limit" + assert subtensor.weights_rate_limit(netuid=netuid) == 0 + + # Commit-reveal values + uids = np.array([0], dtype=np.int64) + weights = np.array([0.1], dtype=np.float32) + salt = [18, 179, 107, 0, 165, 211, 141, 197] + weight_uids, weight_vals = convert_weights_and_uids_for_emit( + uids=uids, weights=weights + ) + + # Commit weights + success, message = subtensor.commit_weights( + alice_wallet, + netuid, + salt=salt, + uids=weight_uids, + weights=weight_vals, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + weight_commits = subtensor.query_module( + module="SubtensorModule", + name="WeightCommits", + params=[netuid, alice_wallet.hotkey.ss58_address], + ) + # Assert that the committed weights are set correctly + assert weight_commits.value is not None, "Weight commit not found in storage" + commit_hash, commit_block = weight_commits.value + assert commit_block > 0, f"Invalid block number: {commit_block}" + + # Query the WeightCommitRevealInterval storage map + weight_commit_reveal_interval = subtensor.query_module( + module="SubtensorModule", name="WeightCommitRevealInterval", params=[netuid] + ) + interval = weight_commit_reveal_interval.value + assert interval > 0, "Invalid WeightCommitRevealInterval" + + # Wait until the reveal block range + await wait_interval(interval, subtensor) + + # Reveal weights + success, message = subtensor.reveal_weights( + alice_wallet, + netuid, + uids=weight_uids, + weights=weight_vals, + salt=salt, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + time.sleep(10) + + # Query the Weights storage map + revealed_weights = subtensor.query_module( + module="SubtensorModule", + name="Weights", + params=[netuid, 0], # netuid and uid + ) + + # Assert that the revealed weights are set correctly + assert revealed_weights.value is not None, "Weight reveal not found in storage" + + assert ( + weight_vals[0] == revealed_weights.value[0][1] + ), f"Incorrect revealed weights. Expected: {weights[0]}, Actual: {revealed_weights.value[0][1]}" + logging.info("✅ Passed test_commit_and_reveal_weights") diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py new file mode 100644 index 000000000..e075326ca --- /dev/null +++ b/tests/e2e_tests/test_dendrite.py @@ -0,0 +1,136 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import logging, Subtensor + +from tests.e2e_tests.utils.e2e_test_utils import ( + setup_wallet, + template_path, + templates_repo, +) +from tests.e2e_tests.utils.chain_interactions import ( + register_neuron, + register_subnet, + add_stake, + wait_epoch, +) + + +@pytest.mark.asyncio +async def test_dendrite(local_chain): + """ + Test the Dendrite mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Bob as a validator + 3. Add stake to Bob and ensure neuron is not a validator yet + 4. Run Bob as a validator and wait epoch + 5. Ensure Bob's neuron has all correct attributes of a validator + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_dendrite") + netuid = 1 + + # Register root as Alice - the subnet owner + alice_keypair, alice_wallet = setup_wallet("//Alice") + + # Register a subnet, netuid 1 + assert register_subnet(local_chain, alice_wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" + + # Register Bob + bob_keypair, bob_wallet = setup_wallet("//Bob") + + # Register Bob to the network + assert register_neuron( + local_chain, bob_wallet, netuid + ), f"Neuron wasn't registered to subnet {netuid}" + + metagraph = bittensor.Metagraph(netuid=netuid, network="ws://localhost:9945") + subtensor = Subtensor(network="ws://localhost:9945") + + # Assert one neuron is Bob + assert len(subtensor.neurons(netuid=netuid)) == 1 + neuron = metagraph.neurons[0] + assert neuron.hotkey == bob_keypair.ss58_address + assert neuron.coldkey == bob_keypair.ss58_address + + # Assert stake is 0 + assert neuron.stake.tao == 0 + + # Stake to become to top neuron after the first epoch + assert add_stake(local_chain, bob_wallet, bittensor.Balance.from_tao(10_000)) + + # Refresh metagraph + metagraph = bittensor.Metagraph(netuid=netuid, network="ws://localhost:9945") + old_neuron = metagraph.neurons[0] + + # Assert stake is 10000 + assert ( + old_neuron.stake.tao == 10_000.0 + ), f"Expected 10_000.0 staked TAO, but got {neuron.stake.tao}" + + # Assert neuron is not a validator yet + assert old_neuron.active is True + assert old_neuron.validator_permit is False + assert old_neuron.validator_trust == 0.0 + assert old_neuron.pruning_score == 0 + + # Prepare to run the validator + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/validator.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + bob_wallet.path, + "--wallet.name", + bob_wallet.name, + "--wallet.hotkey", + "default", + ] + ) + + # Run the validator in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Alice is now validating") + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + + await wait_epoch(subtensor, netuid=netuid) + + # Refresh metagraph + metagraph = bittensor.Metagraph(netuid=netuid, network="ws://localhost:9945") + + # Refresh validator neuron + updated_neuron = metagraph.neurons[0] + + assert len(metagraph.neurons) == 1 + assert updated_neuron.active is True + assert updated_neuron.validator_permit is True + assert updated_neuron.hotkey == bob_keypair.ss58_address + assert updated_neuron.coldkey == bob_keypair.ss58_address + assert updated_neuron.pruning_score != 0 + + logging.info("✅ Passed test_dendrite") diff --git a/tests/e2e_tests/multistep/test_incentive.py b/tests/e2e_tests/test_incentive.py similarity index 52% rename from tests/e2e_tests/multistep/test_incentive.py rename to tests/e2e_tests/test_incentive.py index c2f6baa66..3e309f4f6 100644 --- a/tests/e2e_tests/multistep/test_incentive.py +++ b/tests/e2e_tests/test_incentive.py @@ -3,97 +3,69 @@ import pytest -import bittensor -from bittensor import logging -from bittensor.commands import ( - RegisterCommand, - RegisterSubnetworkCommand, - RootRegisterCommand, - RootSetBoostCommand, - StakeCommand, +from bittensor import Subtensor, logging +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, + wait_epoch, ) -from tests.e2e_tests.utils import ( +from tests.e2e_tests.utils.e2e_test_utils import ( setup_wallet, template_path, templates_repo, - wait_epoch, ) - -""" -Test the incentive mechanism. - -Verify that for the miner: -* trust -* rank -* consensus -* incentive -are updated with proper values after an epoch has passed. - -For the validator verify that: -* validator_permit -* validator_trust -* dividends -* stake -are updated with proper values after an epoch has passed. - -""" +from bittensor.utils.balance import Balance +from bittensor.core.extrinsics.set_weights import do_set_weights +from bittensor.core.metagraph import Metagraph @pytest.mark.asyncio async def test_incentive(local_chain): + """ + Test the incentive mechanism and interaction of miners/validators + + Steps: + 1. Register a subnet and register Alice & Bob + 2. Add Stake by Alice + 3. Run Alice as validator & Bob as miner. Wait Epoch + 4. Verify miner has correct: trust, rank, consensus, incentive + 5. Verify validator has correct: validator_permit, validator_trust, dividends, stake + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.info("Testing test_incentive") netuid = 1 + # Register root as Alice - the subnet owner and validator - alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) + alice_keypair, alice_wallet = setup_wallet("//Alice") + register_subnet(local_chain, alice_wallet) + # Verify subnet created successfully assert local_chain.query( "SubtensorModule", "NetworksAdded", [netuid] ).serialize(), "Subnet wasn't created successfully" # Register Bob as miner - bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") + bob_keypair, bob_wallet = setup_wallet("//Bob") - # Register Alice as neuron to the subnet - alice_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) + # Register Alice as a neuron on the subnet + register_neuron(local_chain, alice_wallet, netuid) - # Register Bob as neuron to the subnet - bob_exec_command( - RegisterCommand, - [ - "s", - "register", - "--netuid", - str(netuid), - ], - ) + # Register Bob as a neuron on the subnet + register_neuron(local_chain, bob_wallet, netuid) - subtensor = bittensor.subtensor(network="ws://localhost:9945") - # assert two neurons are in network + subtensor = Subtensor(network="ws://localhost:9945") + # Assert two neurons are in network assert ( len(subtensor.neurons(netuid=netuid)) == 2 ), "Alice & Bob not registered in the subnet" # Alice to stake to become to top neuron after the first epoch - alice_exec_command( - StakeCommand, - [ - "stake", - "add", - "--amount", - "10000", - ], - ) + add_stake(local_chain, alice_wallet, Balance.from_tao(10_000)) - # register Bob as miner + # Prepare to run Bob as miner cmd = " ".join( [ f"{sys.executable}", @@ -115,7 +87,8 @@ async def test_incentive(local_chain): ] ) - miner_process = await asyncio.create_subprocess_shell( + # Run Bob as miner in the background + await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, @@ -125,7 +98,7 @@ async def test_incentive(local_chain): 5 ) # wait for 5 seconds for the metagraph to refresh with latest data - # register Alice as validator + # Prepare to run Alice as validator cmd = " ".join( [ f"{sys.executable}", @@ -146,9 +119,9 @@ async def test_incentive(local_chain): "--logging.trace", ] ) - # run validator in the background - validator_process = await asyncio.create_subprocess_shell( + # Run Alice as validator in the background + await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, @@ -158,45 +131,10 @@ async def test_incentive(local_chain): 5 ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data - # register validator with root network - alice_exec_command( - RootRegisterCommand, - [ - "root", - "register", - "--netuid", - str(netuid), - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - ], - ) - - alice_exec_command( - RootSetBoostCommand, - [ - "root", - "boost", - "--netuid", - str(netuid), - "--increase", - "100", - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - ], - ) + # Get latest metagraph + metagraph = Metagraph(netuid=netuid, network="ws://localhost:9945") - # get latest metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") - - # get current emissions + # Get current miner/validator stats bob_neuron = metagraph.neurons[1] assert bob_neuron.incentive == 0 assert bob_neuron.consensus == 0 @@ -209,13 +147,12 @@ async def test_incentive(local_chain): assert alice_neuron.stake.tao == 10_000.0 assert alice_neuron.validator_trust == 0 - # wait until next epoch + # Wait until next epoch await wait_epoch(subtensor) - # for some reason the weights do not get set through the template. Set weight manually. - alice_wallet = bittensor.wallet() - alice_wallet._hotkey = alice_keypair - subtensor._do_set_weights( + # Set weights by Alice on the subnet + do_set_weights( + self=subtensor, wallet=alice_wallet, uids=[1], vals=[65535], @@ -226,13 +163,12 @@ async def test_incentive(local_chain): ) logging.info("Alice neuron set weights successfully") - # wait epoch until weight go into effect await wait_epoch(subtensor) - # refresh metagraph - metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + # Refresh metagraph + metagraph = Metagraph(netuid=netuid, network="ws://localhost:9945") - # get current emissions and validate that Alice has gotten tao + # Get current emissions and validate that Alice has gotten tao bob_neuron = metagraph.neurons[1] assert bob_neuron.incentive == 1 assert bob_neuron.consensus == 1 @@ -244,4 +180,5 @@ async def test_incentive(local_chain): assert alice_neuron.dividends == 1 assert alice_neuron.stake.tao == 10_000.0 assert alice_neuron.validator_trust == 1 - logging.info("Passed test_incentive") + + logging.info("✅ Passed test_incentive") diff --git a/tests/e2e_tests/test_liquid_alpha.py b/tests/e2e_tests/test_liquid_alpha.py new file mode 100644 index 000000000..d73162fbb --- /dev/null +++ b/tests/e2e_tests/test_liquid_alpha.py @@ -0,0 +1,186 @@ +import bittensor +from bittensor import logging +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, + sudo_set_hyperparameter_bool, + sudo_set_hyperparameter_values, +) +from tests.e2e_tests.utils.e2e_test_utils import setup_wallet + + +def liquid_alpha_call_params(netuid: int, alpha_values: str): + alpha_low, alpha_high = [v.strip() for v in alpha_values.split(",")] + return { + "netuid": netuid, + "alpha_low": alpha_low, + "alpha_high": alpha_high, + } + + +def test_liquid_alpha(local_chain): + """ + Test the liquid alpha mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Alice's neuron and add stake + 3. Verify we can't set alpha values without enabling liquid_alpha + 4. Test setting alpha values after enabling liquid_alpha + 5. Verify failures when setting incorrect values (upper and lower bounds) + Raises: + AssertionError: If any of the checks or verifications fail + """ + u16_max = 65535 + netuid = 1 + logging.info("Testing test_liquid_alpha_enabled") + + # Register root as Alice + keypair, alice_wallet = setup_wallet("//Alice") + register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + # Register a neuron to the subnet + assert register_neuron( + local_chain, alice_wallet, netuid + ), "Unable to register Alice as a neuron" + + # Stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(100_000)) + + # Assert liquid alpha is disabled + subtensor = bittensor.Subtensor(network="ws://localhost:9945") + assert ( + subtensor.get_subnet_hyperparameters(netuid=netuid).liquid_alpha_enabled + is False + ), "Liquid alpha is enabled by default" + + # Attempt to set alpha high/low while disabled (should fail) + alpha_values = "6553, 53083" + call_params = liquid_alpha_call_params(netuid, alpha_values) + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Alpha values set while being disabled" + assert error_message["name"] == "LiquidAlphaDisabled" + + # Enabled liquid alpha on the subnet + assert sudo_set_hyperparameter_bool( + local_chain, alice_wallet, "sudo_set_liquid_alpha_enabled", True, netuid + ), "Unable to enable liquid alpha" + + assert subtensor.get_subnet_hyperparameters( + netuid=1 + ).liquid_alpha_enabled, "Failed to enable liquid alpha" + + # Attempt to set alpha high & low after enabling the hyperparameter + alpha_values = "87, 54099" + call_params = liquid_alpha_call_params(netuid, alpha_values) + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + ), "Unable to set alpha_values" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 54099 + ), "Failed to set alpha high" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 87 + ), "Failed to set alpha low" + + # Testing alpha high upper and lower bounds + + # 1. Test setting Alpha_high too low + alpha_high_too_low = ( + u16_max * 4 // 5 + ) - 1 # One less than the minimum acceptable value + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_low}") + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + + assert result is False, "Able to set incorrect alpha_high value" + assert error_message["name"] == "AlphaHighTooLow" + + # 2. Test setting Alpha_high too high + alpha_high_too_high = u16_max + 1 # One more than the max acceptable value + call_params = liquid_alpha_call_params(netuid, f"6553, {alpha_high_too_high}") + try: + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + except Exception as e: + assert str(e) == "65536 out of range for u16", f"Unexpected error: {e}" + + # Testing alpha low upper and lower bounds + + # 1. Test setting Alpha_low too low + alpha_low_too_low = 0 + call_params = liquid_alpha_call_params(netuid, f"{alpha_low_too_low}, 53083") + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Able to set incorrect alpha_low value" + assert error_message["name"] == "AlphaLowOutOfRange" + + # 2. Test setting Alpha_low too high + alpha_low_too_high = ( + u16_max * 4 // 5 + ) + 1 # One more than the maximum acceptable value + call_params = liquid_alpha_call_params(netuid, f"{alpha_low_too_high}, 53083") + result, error_message = sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + return_error_message=True, + ) + assert result is False, "Able to set incorrect alpha_low value" + assert error_message["name"] == "AlphaLowOutOfRange" + + # Setting normal alpha values + alpha_values = "6553, 53083" + call_params = liquid_alpha_call_params(netuid, alpha_values) + assert sudo_set_hyperparameter_values( + local_chain, + alice_wallet, + call_function="sudo_set_alpha_values", + call_params=call_params, + ), "Unable to set liquid alpha values" + + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_high == 53083 + ), "Failed to set alpha high" + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).alpha_low == 6553 + ), "Failed to set alpha low" + + # Disable Liquid Alpha + assert sudo_set_hyperparameter_bool( + local_chain, alice_wallet, "sudo_set_liquid_alpha_enabled", False, netuid + ), "Unable to disable liquid alpha" + + assert ( + subtensor.get_subnet_hyperparameters(netuid=1).liquid_alpha_enabled is False + ), "Failed to disable liquid alpha" + logging.info("✅ Passed test_liquid_alpha") diff --git a/tests/e2e_tests/test_metagraph.py b/tests/e2e_tests/test_metagraph.py new file mode 100644 index 000000000..ff16dde36 --- /dev/null +++ b/tests/e2e_tests/test_metagraph.py @@ -0,0 +1,177 @@ +import time + +import bittensor +from bittensor import logging +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + setup_wallet, +) + + +def neuron_to_dict(neuron): + """ + Convert a neuron object to a dictionary, excluding private attributes, methods, and specific fields. + Returns: + dict: A dictionary of the neuron's public attributes. + + Note: + Excludes 'weights' and 'bonds' fields. These are present in subtensor + but not in metagraph + """ + excluded_fields = {"weights", "bonds"} + return { + attr: getattr(neuron, attr) + for attr in dir(neuron) + if not attr.startswith("_") + and not callable(getattr(neuron, attr)) + and attr not in excluded_fields + } + + +def test_metagraph(local_chain): + """ + Tests the metagraph + + Steps: + 1. Register a subnet through Alice + 2. Assert metagraph's initial state + 3. Register Bob and validate info in metagraph + 4. Fetch neuron info of Bob through subtensor & metagraph and verify + 5. Register Dave and validate info in metagraph + 6. Verify low balance stake fails & add stake thru Bob and verify + 7. Load pre_dave metagraph from latest save and verify both instances + Raises: + AssertionError: If any of the checks or verifications fail + """ + logging.info("Testing test_metagraph_command") + netuid = 1 + + # Register Alice, Bob, and Dave + alice_keypair, alice_wallet = setup_wallet("//Alice") + bob_keypair, bob_wallet = setup_wallet("//Bob") + dave_keypair, dave_wallet = setup_wallet("//Dave") + + # Register the subnet through Alice + register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Verify subnet was created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [1] + ).serialize(), "Subnet wasn't created successfully" + + # Initialize metagraph + subtensor = bittensor.Subtensor(network="ws://localhost:9945") + metagraph = subtensor.metagraph(netuid=1) + + # Assert metagraph is empty + assert len(metagraph.uids) == 0, "Metagraph is not empty" + + # Register Bob to the subnet + assert register_neuron( + local_chain, bob_wallet, netuid + ), "Unable to register Bob as a neuron" + + # Refresh the metagraph + metagraph.sync(subtensor=subtensor) + + # Assert metagraph has Bob neuron + assert len(metagraph.uids) == 1, "Metagraph doesn't have exactly 1 neuron" + assert ( + metagraph.hotkeys[0] == bob_keypair.ss58_address + ), "Bob's hotkey doesn't match in metagraph" + assert len(metagraph.coldkeys) == 1, "Metagraph doesn't have exactly 1 coldkey" + assert metagraph.n.max() == 1, "Metagraph's max n is not 1" + assert metagraph.n.min() == 1, "Metagraph's min n is not 1" + assert len(metagraph.addresses) == 1, "Metagraph doesn't have exactly 1 address" + + # Fetch UID of Bob + uid = subtensor.get_uid_for_hotkey_on_subnet( + bob_keypair.ss58_address, netuid=netuid + ) + + # Fetch neuron info of Bob through subtensor and metagraph + neuron_info_bob = subtensor.neuron_for_uid(uid, netuid=netuid) + metagraph_dict = neuron_to_dict(metagraph.neurons[uid]) + subtensor_dict = neuron_to_dict(neuron_info_bob) + + # Verify neuron info is the same in both objects + assert ( + metagraph_dict == subtensor_dict + ), "Neuron info of Bob doesn't match b/w metagraph & subtensor" + + # Create pre_dave metagraph for future verifications + metagraph_pre_dave = subtensor.metagraph(netuid=1) + + # Register Dave as a neuron + assert register_neuron( + local_chain, dave_wallet, netuid + ), "Unable to register Dave as a neuron" + + metagraph.sync(subtensor=subtensor) + + # Assert metagraph now includes Dave's neuron + assert ( + len(metagraph.uids) == 2 + ), "Metagraph doesn't have exactly 2 neurons post Dave" + assert ( + metagraph.hotkeys[1] == dave_keypair.ss58_address + ), "Neuron's hotkey in metagraph doesn't match" + assert ( + len(metagraph.coldkeys) == 2 + ), "Metagraph doesn't have exactly 2 coldkeys post Dave" + assert metagraph.n.max() == 2, "Metagraph's max n is not 2 post Dave" + assert metagraph.n.min() == 2, "Metagraph's min n is not 2 post Dave" + assert len(metagraph.addresses) == 2, "Metagraph doesn't have 2 addresses post Dave" + + # Test staking with low balance + assert not add_stake( + local_chain, dave_wallet, bittensor.Balance.from_tao(10_000) + ), "Low balance stake should fail" + + # Add stake by Bob + assert add_stake( + local_chain, bob_wallet, bittensor.Balance.from_tao(10_000) + ), "Failed to add stake for Bob" + + # Assert stake is added after updating metagraph + metagraph.sync(subtensor=subtensor) + assert metagraph.neurons[0].stake == bittensor.Balance.from_tao( + 10_000 + ), "Bob's stake not updated in metagraph" + + # Test the save() and load() mechanism + # We save the metagraph and pre_dave loads it + metagraph.save() + time.sleep(3) + metagraph_pre_dave.load() + + # Ensure data is synced between two metagraphs + assert len(metagraph.uids) == len( + metagraph_pre_dave.uids + ), "UID count mismatch after save and load" + assert ( + metagraph.uids == metagraph_pre_dave.uids + ).all(), "UIDs don't match after save and load" + + assert len(metagraph.axons) == len( + metagraph_pre_dave.axons + ), "Axon count mismatch after save and load" + assert ( + metagraph.axons[1].hotkey == metagraph_pre_dave.axons[1].hotkey + ), "Axon hotkey mismatch after save and load" + assert ( + metagraph.axons == metagraph_pre_dave.axons + ), "Axons don't match after save and load" + + assert len(metagraph.neurons) == len( + metagraph_pre_dave.neurons + ), "Neuron count mismatch after save and load" + assert ( + metagraph.neurons == metagraph_pre_dave.neurons + ), "Neurons don't match after save and load" + + logging.info("✅ Passed test_metagraph") diff --git a/tests/e2e_tests/test_subtensor_functions.py b/tests/e2e_tests/test_subtensor_functions.py new file mode 100644 index 000000000..32d0f6e14 --- /dev/null +++ b/tests/e2e_tests/test_subtensor_functions.py @@ -0,0 +1,152 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import logging +from tests.e2e_tests.utils.chain_interactions import ( + register_neuron, + register_subnet, +) +from tests.e2e_tests.utils.e2e_test_utils import ( + setup_wallet, + template_path, + templates_repo, +) + + +@pytest.mark.asyncio +async def test_subtensor_extrinsics(local_chain): + """ + Tests subtensor extrinsics + + Steps: + 1. Validate subnets in the chain before/after registering netuid = 1 + 2. Register Alice's neuron + 3. Verify Alice and Bob's participation in subnets (individually and global) + 4. Verify uids of Alice and Bob gets populated correctly + 5. Start Alice as a validator and verify neuroninfo before/after is different + Raises: + AssertionError: If any of the checks or verifications fail + """ + netuid = 1 + subtensor = bittensor.Subtensor(network="ws://localhost:9945") + + # Subnets 0 and 3 are bootstrapped from the start + assert subtensor.get_subnets() == [0, 3] + assert subtensor.get_total_subnets() == 2 + + # Add wallets for Alice and Bob + alice_keypair, alice_wallet = setup_wallet("//Alice") + bob_keypair, bob_wallet = setup_wallet("//Bob") + + # Register subnet + register_subnet(local_chain, alice_wallet), "Unable to register the subnet" + + # Subnet 1 is added after registration + assert subtensor.get_subnets() == [0, 1, 3] + assert subtensor.get_total_subnets() == 3 + + # Verify subnet 1 created successfully + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + assert subtensor.subnet_exists(netuid) + + # Register Alice to the subnet + assert register_neuron( + local_chain, alice_wallet, netuid + ), "Unable to register Alice as a neuron" + + # Verify Alice is registered to netuid 1 and Bob isn't registered to any + assert subtensor.get_netuids_for_hotkey(hotkey_ss58=alice_keypair.ss58_address) == [ + 1 + ], "Alice is not registered to netuid 1 as expected" + assert ( + subtensor.get_netuids_for_hotkey(hotkey_ss58=bob_keypair.ss58_address) == [] + ), "Bob is unexpectedly registered to some netuid" + + # Verify Alice's hotkey is registered to any subnet (currently netuid = 1) + assert subtensor.is_hotkey_registered_any( + hotkey_ss58=alice_keypair.ss58_address + ), "Alice's hotkey is not registered to any subnet" + assert not subtensor.is_hotkey_registered_any( + hotkey_ss58=bob_keypair.ss58_address + ), "Bob's hotkey is unexpectedly registered to a subnet" + + # Verify netuid = 1 only has Alice registered and not Bob + assert subtensor.is_hotkey_registered_on_subnet( + netuid=netuid, hotkey_ss58=alice_keypair.ss58_address + ), "Alice's hotkey is not registered on netuid 1" + assert not subtensor.is_hotkey_registered_on_subnet( + netuid=netuid, hotkey_ss58=bob_keypair.ss58_address + ), "Bob's hotkey is unexpectedly registered on netuid 1" + + # Verify Alice's UID on netuid 1 is 0 + assert ( + subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=alice_keypair.ss58_address, netuid=netuid + ) + == 0 + ), "UID for Alice's hotkey on netuid 1 is not 0 as expected" + + # Register Bob to the subnet + assert register_neuron( + local_chain, bob_wallet, netuid + ), "Unable to register Alice as a neuron" + + # Verify Bob's UID on netuid 1 is 1 + assert ( + subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=bob_keypair.ss58_address, netuid=netuid + ) + == 1 + ), "UID for Bob's hotkey on netuid 1 is not 1 as expected" + + neuron_info_old = subtensor.get_neuron_for_pubkey_and_subnet( + alice_keypair.ss58_address, netuid=netuid + ) + + # Prepare to run Alice as validator + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/validator.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + alice_wallet.path, + "--wallet.name", + alice_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + + # Run Alice as validator in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Alice is now validating") + + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + subtensor = bittensor.Subtensor(network="ws://localhost:9945") + + # Verify neuron info is updated after running as a validator + neuron_info = subtensor.get_neuron_for_pubkey_and_subnet( + alice_keypair.ss58_address, netuid=netuid + ) + assert ( + neuron_info_old.axon_info != neuron_info.axon_info + ), "Neuron info not updated after running validator" + + logging.info("✅ Passed test_subtensor_extrinsics") diff --git a/tests/e2e_tests/test_transfer.py b/tests/e2e_tests/test_transfer.py new file mode 100644 index 000000000..b6be1cd6a --- /dev/null +++ b/tests/e2e_tests/test_transfer.py @@ -0,0 +1,52 @@ +from bittensor import Subtensor, logging +from bittensor.core.subtensor import transfer_extrinsic +from tests.e2e_tests.utils.e2e_test_utils import setup_wallet + + +def test_transfer(local_chain): + """ + Test the transfer mechanism on the chain + + Steps: + 1. Create a wallet for Alice + 2. Calculate existing balance and transfer 2 Tao + 3. Calculate balance after extrinsic call and verify calculations + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_transfer") + + # Set up Alice wallet + keypair, wallet = setup_wallet("//Alice") + + # Account details before transfer + acc_before = local_chain.query("System", "Account", [keypair.ss58_address]) + + # Transfer Tao using extrinsic + subtensor = Subtensor(network="ws://localhost:9945") + transfer_extrinsic( + subtensor=subtensor, + wallet=wallet, + dest="5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx", + amount=2, + wait_for_finalization=True, + wait_for_inclusion=True, + prompt=False, + ) + + # Account details after transfer + acc_after = local_chain.query("System", "Account", [keypair.ss58_address]) + + # Transfer calculation assertions + expected_transfer = 2_000_000_000 + tolerance = 200_000 # Tx fee tolerance + + actual_difference = ( + acc_before.value["data"]["free"] - acc_after.value["data"]["free"] + ) + assert ( + expected_transfer <= actual_difference <= expected_transfer + tolerance + ), f"Expected transfer with tolerance: {expected_transfer} <= {actual_difference} <= {expected_transfer + tolerance}" + + logging.info("✅ Passed test_transfer") diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py deleted file mode 100644 index 5a4adc6c9..000000000 --- a/tests/e2e_tests/utils.py +++ /dev/null @@ -1,214 +0,0 @@ -import logging -import asyncio -import os -import shutil -import subprocess -import sys -import time -from typing import List - -from substrateinterface import SubstrateInterface - -import bittensor -from bittensor import Keypair - -template_path = os.getcwd() + "/neurons/" -templates_repo = "templates repository" - - -def setup_wallet(uri: str): - keypair = Keypair.create_from_uri(uri) - wallet_path = "/tmp/btcli-e2e-wallet-{}".format(uri.strip("/")) - wallet = bittensor.wallet(path=wallet_path) - wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True) - wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) - wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True) - - def exec_command(command, extra_args: List[str], function: str = "run"): - parser = bittensor.cli.__create_parser__() - args = extra_args + [ - "--no_prompt", - "--subtensor.network", - "local", - "--subtensor.chain_endpoint", - "ws://localhost:9945", - "--wallet.path", - wallet_path, - ] - logging.info(f'executing command: {command} {" ".join(args)}') - config = bittensor.config( - parser=parser, - args=args, - ) - cli_instance = bittensor.cli(config) - # Dynamically call the specified function on the command - result = getattr(command, function)(cli_instance) - return result - - return keypair, exec_command, wallet - - -def sudo_call_set_network_limit( - substrate: SubstrateInterface, wallet: bittensor.wallet -) -> bool: - inner_call = substrate.compose_call( - call_module="AdminUtils", - call_function="sudo_set_network_rate_limit", - call_params={"rate_limit": 1}, - ) - call = substrate.compose_call( - call_module="Sudo", - call_function="sudo", - call_params={"call": inner_call}, - ) - - extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - response.process_events() - return response.is_success - - -def sudo_call_set_target_stakes_per_interval( - substrate: SubstrateInterface, wallet: bittensor.wallet -) -> bool: - inner_call = substrate.compose_call( - call_module="AdminUtils", - call_function="sudo_set_target_stakes_per_interval", - call_params={"target_stakes_per_interval": 100}, - ) - call = substrate.compose_call( - call_module="Sudo", - call_function="sudo", - call_params={"call": inner_call}, - ) - - extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - response.process_events() - return response.is_success - - -def call_add_proposal(substrate: SubstrateInterface, wallet: bittensor.wallet) -> bool: - proposal_call = substrate.compose_call( - call_module="System", - call_function="remark", - call_params={"remark": [0]}, - ) - call = substrate.compose_call( - call_module="Triumvirate", - call_function="propose", - call_params={ - "proposal": proposal_call, - "length_bound": 100_000, - "duration": 100_000_000, - }, - ) - - extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) - response = substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=True, - wait_for_finalization=True, - ) - - response.process_events() - return response.is_success - - -async def wait_epoch(subtensor, netuid=1): - q_tempo = [ - v.value - for [k, v] in subtensor.query_map_subtensor("Tempo") - if k.value == netuid - ] - if len(q_tempo) == 0: - raise Exception("could not determine tempo") - tempo = q_tempo[0] - logging.info(f"tempo = {tempo}") - await wait_interval(tempo, subtensor, netuid) - - -async def wait_interval(tempo, subtensor, netuid=1): - interval = tempo + 1 - current_block = subtensor.get_current_block() - last_epoch = current_block - 1 - (current_block + netuid + 1) % interval - next_tempo_block_start = last_epoch + interval - last_reported = None - while current_block < next_tempo_block_start: - await asyncio.sleep( - 1 - ) # Wait for 1 second before checking the block number again - current_block = subtensor.get_current_block() - if last_reported is None or current_block - last_reported >= 10: - last_reported = current_block - print( - f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" - ) - logging.info( - f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" - ) - - -def clone_or_update_templates(): - specific_commit = None - install_dir = template_path - repo_mapping = { - templates_repo: "https://github.com/opentensor/bittensor-subnet-template.git", - } - os.makedirs(install_dir, exist_ok=True) - os.chdir(install_dir) - - for repo, git_link in repo_mapping.items(): - if not os.path.exists(repo): - print(f"\033[94mCloning {repo}...\033[0m") - subprocess.run(["git", "clone", git_link, repo], check=True) - else: - print(f"\033[94mUpdating {repo}...\033[0m") - os.chdir(repo) - subprocess.run(["git", "pull"], check=True) - os.chdir("..") - - # here for pulling specific commit versions of repo - if specific_commit: - os.chdir(templates_repo) - print( - f"\033[94mChecking out commit {specific_commit} in {templates_repo}...\033[0m" - ) - subprocess.run(["git", "checkout", specific_commit], check=True) - os.chdir("..") - - return install_dir + templates_repo + "/" - - -def install_templates(install_dir): - subprocess.check_call([sys.executable, "-m", "pip", "install", install_dir]) - - -def uninstall_templates(install_dir): - # uninstall templates - subprocess.check_call( - [sys.executable, "-m", "pip", "uninstall", "bittensor_subnet_template", "-y"] - ) - # delete everything in directory - shutil.rmtree(install_dir) - - -async def write_output_log_to_file(name, stream): - log_file = f"{name}.log" - with open(log_file, "a") as f: - while True: - line = await stream.readline() - if not line: - break - f.write(line.decode()) - f.flush() diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py new file mode 100644 index 000000000..aad53812c --- /dev/null +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -0,0 +1,186 @@ +""" +This module provides functions interacting with the chain for end-to-end testing; +these are not present in btsdk but are required for e2e tests +""" + +import asyncio +from typing import Union, Optional, TYPE_CHECKING + +from bittensor import logging + +# for typing purposes +if TYPE_CHECKING: + from bittensor import Wallet + from bittensor.core.subtensor import Subtensor + from bittensor.utils.balance import Balance + from substrateinterface import SubstrateInterface + + +def sudo_set_hyperparameter_bool( + substrate: "SubstrateInterface", + wallet: "Wallet", + call_function: str, + value: bool, + netuid: int, +) -> bool: + """ + Sets boolean hyperparameter value through AdminUtils. Mimics setting hyperparams + """ + call = substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params={"netuid": netuid, "enabled": value}, + ) + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + response.process_events() + return response.is_success + + +def sudo_set_hyperparameter_values( + substrate: "SubstrateInterface", + wallet: "Wallet", + call_function: str, + call_params: dict, + return_error_message: bool = False, +) -> Union[bool, tuple[bool, Optional[str]]]: + """ + Sets liquid alpha values using AdminUtils. Mimics setting hyperparams + """ + call = substrate.compose_call( + call_module="AdminUtils", + call_function=call_function, + call_params=call_params, + ) + extrinsic = substrate.create_signed_extrinsic(call=call, keypair=wallet.coldkey) + response = substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + response.process_events() + + if return_error_message: + return response.is_success, response.error_message + + return response.is_success + + +def add_stake( + substrate: "SubstrateInterface", wallet: "Wallet", amount: "Balance" +) -> bool: + """ + Adds stake to a hotkey using SubtensorModule. Mimics command of adding stake + """ + stake_call = substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={"hotkey": wallet.hotkey.ss58_address, "amount_staked": amount.rao}, + ) + extrinsic = substrate.create_signed_extrinsic( + call=stake_call, keypair=wallet.coldkey + ) + response = substrate.submit_extrinsic( + extrinsic, wait_for_finalization=True, wait_for_inclusion=True + ) + response.process_events() + return response.is_success + + +def register_subnet(substrate: "SubstrateInterface", wallet: "Wallet") -> bool: + """ + Registers a subnet on the chain using wallet. Mimics register subnet command. + """ + register_call = substrate.compose_call( + call_module="SubtensorModule", + call_function="register_network", + call_params={"immunity_period": 0, "reg_allowed": True}, + ) + extrinsic = substrate.create_signed_extrinsic( + call=register_call, keypair=wallet.coldkey + ) + response = substrate.submit_extrinsic( + extrinsic, wait_for_finalization=True, wait_for_inclusion=True + ) + response.process_events() + return response.is_success + + +def register_neuron( + substrate: "SubstrateInterface", wallet: "Wallet", netuid: int +) -> bool: + """ + Registers a neuron on a subnet. Mimics subnet register command. + """ + neuron_register_call = substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + }, + ) + extrinsic = substrate.create_signed_extrinsic( + call=neuron_register_call, keypair=wallet.coldkey + ) + response = substrate.submit_extrinsic( + extrinsic, wait_for_finalization=True, wait_for_inclusion=True + ) + response.process_events() + return response.is_success + + +async def wait_epoch(subtensor: "Subtensor", netuid: int = 1): + """ + Waits for the next epoch to start on a specific subnet. + + Queries the tempo value from the Subtensor module and calculates the + interval based on the tempo. Then waits for the next epoch to start + by monitoring the current block number. + + Raises: + Exception: If the tempo cannot be determined from the chain. + """ + q_tempo = [ + v.value + for [k, v] in subtensor.query_map_subtensor("Tempo") + if k.value == netuid + ] + if len(q_tempo) == 0: + raise Exception("could not determine tempo") + tempo = q_tempo[0] + logging.info(f"tempo = {tempo}") + await wait_interval(tempo, subtensor, netuid) + + +async def wait_interval(tempo: int, subtensor: "Subtensor", netuid: int = 1): + """ + Waits until the next tempo interval starts for a specific subnet. + + Calculates the next tempo block start based on the current block number + and the provided tempo, then enters a loop where it periodically checks + the current block number until the next tempo interval starts. + """ + interval = tempo + 1 + current_block = subtensor.get_current_block() + last_epoch = current_block - 1 - (current_block + netuid + 1) % interval + next_tempo_block_start = last_epoch + interval + last_reported = None + + while current_block < next_tempo_block_start: + await asyncio.sleep( + 1 + ) # Wait for 1 second before checking the block number again + current_block = subtensor.get_current_block() + if last_reported is None or current_block - last_reported >= 10: + last_reported = current_block + print( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + logging.info( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) diff --git a/tests/e2e_tests/utils/e2e_test_utils.py b/tests/e2e_tests/utils/e2e_test_utils.py new file mode 100644 index 000000000..ba662647a --- /dev/null +++ b/tests/e2e_tests/utils/e2e_test_utils.py @@ -0,0 +1,83 @@ +import os +import shutil +import subprocess +import sys + +from substrateinterface import Keypair + +import bittensor + +template_path = os.getcwd() + "/neurons/" +templates_repo = "templates repository" + + +def setup_wallet(uri: str) -> tuple[Keypair, bittensor.Wallet]: + """ + Sets up a wallet using the provided URI. + + This function creates a keypair from the given URI and initializes a wallet + at a temporary path. It sets the coldkey, coldkeypub, and hotkey for the wallet + using the generated keypair. + + Side Effects: + - Creates a wallet in a temporary directory. + - Sets keys in the wallet without encryption and with overwriting enabled. + """ + keypair = Keypair.create_from_uri(uri) + wallet_path = f"/tmp/btcli-e2e-wallet-{uri.strip('/')}" + wallet = bittensor.Wallet(path=wallet_path) + wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True) + wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) + wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True) + return keypair, wallet + + +def clone_or_update_templates(specific_commit=None): + """ + Clones or updates the Bittensor subnet template repository. + + This function clones the Bittensor subnet template repository if it does not + already exist in the specified installation directory. If the repository already + exists, it updates it by pulling the latest changes. Optionally, it can check out + a specific commit if the `specific_commit` variable is set. + """ + install_dir = template_path + repo_mapping = { + templates_repo: "https://github.com/opentensor/bittensor-subnet-template.git", + } + + os.makedirs(install_dir, exist_ok=True) + os.chdir(install_dir) + + for repo, git_link in repo_mapping.items(): + if not os.path.exists(repo): + print(f"\033[94mCloning {repo}...\033[0m") + subprocess.run(["git", "clone", git_link, repo], check=True) + else: + print(f"\033[94mUpdating {repo}...\033[0m") + os.chdir(repo) + subprocess.run(["git", "pull"], check=True) + os.chdir("..") + + # For pulling specific commit versions of repo + if specific_commit: + os.chdir(templates_repo) + print( + f"\033[94mChecking out commit {specific_commit} in {templates_repo}...\033[0m" + ) + subprocess.run(["git", "checkout", specific_commit], check=True) + os.chdir("..") + + return install_dir + templates_repo + "/" + + +def install_templates(install_dir): + subprocess.check_call([sys.executable, "-m", "pip", "install", install_dir]) + + +def uninstall_templates(install_dir): + subprocess.check_call( + [sys.executable, "-m", "pip", "uninstall", "bittensor_subnet_template", "-y"] + ) + # Delete everything in directory + shutil.rmtree(install_dir) diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index fc9e8ad9d..f876d249b 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -16,15 +16,17 @@ # DEALINGS IN THE SOFTWARE. import os -from .helpers import ( - _get_mock_coldkey, - _get_mock_hotkey, - _get_mock_keypair, - _get_mock_wallet, +from .helpers import ( # noqa: F401 CLOSE_IN_VALUE, MockConsole, __mock_wallet_factory__, ) +from bittensor_wallet.mock.wallet_mock import ( # noqa: F401 + get_mock_coldkey, + get_mock_hotkey, + get_mock_keypair, + get_mock_wallet, +) def is_running_in_circleci(): diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index 482f59ce2..417bd643b 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -1,14 +1,14 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -16,21 +16,23 @@ # DEALINGS IN THE SOFTWARE. from typing import Union -from bittensor import Balance, NeuronInfo, AxonInfo, PrometheusInfo, __ss58_format__ -from bittensor.mock.wallet_mock import MockWallet as _MockWallet -from bittensor.mock.wallet_mock import get_mock_coldkey as _get_mock_coldkey -from bittensor.mock.wallet_mock import get_mock_hotkey as _get_mock_hotkey -from bittensor.mock.wallet_mock import get_mock_keypair as _get_mock_keypair -from bittensor.mock.wallet_mock import get_mock_wallet as _get_mock_wallet + +from bittensor_wallet.mock.wallet_mock import MockWallet as _MockWallet +from bittensor_wallet.mock.wallet_mock import get_mock_coldkey +from bittensor_wallet.mock.wallet_mock import get_mock_hotkey +from bittensor_wallet.mock.wallet_mock import get_mock_wallet from rich.console import Console from rich.text import Text +from bittensor.utils.balance import Balance +from bittensor.core.chain_data import AxonInfo, NeuronInfo, PrometheusInfo + def __mock_wallet_factory__(*args, **kwargs) -> _MockWallet: """Returns a mock wallet object.""" - mock_wallet = _get_mock_wallet() + mock_wallet = get_mock_wallet() return mock_wallet @@ -51,12 +53,8 @@ def __eq__(self, __o: Union[float, int, Balance]) -> bool: # True if __o \in [value - tolerance, value + tolerance] # or if value \in [__o - tolerance, __o + tolerance] return ( - (self.value - self.tolerance) <= __o - and __o <= (self.value + self.tolerance) - ) or ( - (__o - self.tolerance) <= self.value - and self.value <= (__o + self.tolerance) - ) + (self.value - self.tolerance) <= __o <= (self.value + self.tolerance) + ) or ((__o - self.tolerance) <= self.value <= (__o + self.tolerance)) def get_mock_neuron(**kwargs) -> NeuronInfo: @@ -118,7 +116,7 @@ def get_mock_neuron(**kwargs) -> NeuronInfo: def get_mock_neuron_by_uid(uid: int, **kwargs) -> NeuronInfo: return get_mock_neuron( - uid=uid, hotkey=_get_mock_hotkey(uid), coldkey=_get_mock_coldkey(uid), **kwargs + uid=uid, hotkey=get_mock_hotkey(uid), coldkey=get_mock_coldkey(uid), **kwargs ) diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py index e69de29bb..640a13250 100644 --- a/tests/integration_tests/__init__.py +++ b/tests/integration_tests/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py deleted file mode 100644 index 6fe1acf3b..000000000 --- a/tests/integration_tests/test_cli.py +++ /dev/null @@ -1,2752 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2022 Yuma Rao -# Copyright © 2022-2023 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - -# 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 contextlib -from copy import deepcopy -import os -import random -import shutil -from types import SimpleNamespace -from typing import Dict -import unittest -from unittest.mock import MagicMock, patch - -import pytest - -import bittensor -from bittensor import Balance -from bittensor.commands.delegates import _get_coldkey_wallets_for_path -from bittensor.commands.identity import SetIdentityCommand -from bittensor.commands.wallets import _get_coldkey_ss58_addresses_for_path -from bittensor.mock import MockSubtensor -from bittensor.wallet import wallet as Wallet -from tests.helpers import ( - is_running_in_circleci, - MockConsole, - _get_mock_keypair, - _get_mock_wallet as generate_wallet, -) - - -_subtensor_mock: MockSubtensor = MockSubtensor() - - -def setUpModule(): - _subtensor_mock.reset() - - _subtensor_mock.create_subnet(netuid=1) - - _subtensor_mock.create_subnet(netuid=2) - - _subtensor_mock.create_subnet(netuid=3) - - # Set diff 0 - _subtensor_mock.set_difficulty(netuid=1, difficulty=0) - - _subtensor_mock.set_difficulty(netuid=2, difficulty=0) - - _subtensor_mock.set_difficulty(netuid=3, difficulty=0) - - -def return_mock_sub(*args, **kwargs): - return MockSubtensor - - -@patch("bittensor.subtensor", new_callable=return_mock_sub) -class TestCLIWithNetworkAndConfig(unittest.TestCase): - def setUp(self): - self._config = TestCLIWithNetworkAndConfig.construct_config() - - @property - def config(self): - copy_ = deepcopy(self._config) - return copy_ - - @staticmethod - def construct_config(): - parser = bittensor.cli.__create_parser__() - defaults = bittensor.config(parser=parser, args=[]) - # Parse commands and subcommands - for command in bittensor.ALL_COMMANDS: - if ( - command in bittensor.ALL_COMMANDS - and "commands" in bittensor.ALL_COMMANDS[command] - ): - for subcommand in bittensor.ALL_COMMANDS[command]["commands"]: - defaults.merge( - bittensor.config(parser=parser, args=[command, subcommand]) - ) - else: - defaults.merge(bittensor.config(parser=parser, args=[command])) - - defaults.netuid = 1 - # Always use mock subtensor. - defaults.subtensor.network = "finney" - # Skip version checking. - defaults.no_version_checking = True - - return defaults - - def test_overview(self, _): - if is_running_in_circleci(): - config = self.config - config.wallet.path = "/tmp/test_cli_test_overview" - config.wallet.name = "mock_wallet" - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - - mock_hotkeys = ["hk0", "hk1", "hk2", "hk3", "hk4"] - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - coldkeypub_file=MagicMock( - exists_on_device=MagicMock(return_value=True) # Wallet exists - ), - ) - for idx, hk in enumerate(mock_hotkeys) - ] - - mock_registrations = [ - (1, mock_wallets[0]), - (1, mock_wallets[1]), - # (1, mock_wallets[2]), Not registered on netuid 1 - (2, mock_wallets[0]), - # (2, mock_wallets[1]), Not registered on netuid 2 - (2, mock_wallets[2]), - (3, mock_wallets[0]), - (3, mock_wallets[1]), - (3, mock_wallets[2]), # All registered on netuid 3 (but hk3) - (3, mock_wallets[4]), # hk4 is only on netuid 3 - ] # hk3 is not registered on any network - - # Register each wallet to it's subnet. - print("Registering wallets to mock subtensor...") - - for netuid, wallet in mock_registrations: - _ = _subtensor_mock.force_register_neuron( - netuid=netuid, - coldkey=wallet.coldkey.ss58_address, - hotkey=wallet.hotkey.ss58_address, - ) - - def mock_get_wallet(*args, **kwargs): - hk = kwargs.get("hotkey") - name_ = kwargs.get("name") - - if not hk and kwargs.get("config"): - hk = kwargs.get("config").wallet.hotkey - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_ and wallet.hotkey_str == hk: - return wallet - else: - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - return mock_wallets[0] - - mock_console = MockConsole() - with patch( - "bittensor.commands.overview.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - with patch("bittensor.__console__", mock_console): - cli.run() - - # Check that the overview was printed. - self.assertIsNotNone(mock_console.captured_print) - - output_no_syntax = mock_console.remove_rich_syntax( - mock_console.captured_print - ) - - # Check that each subnet was printed. - self.assertIn("Subnet: 1", output_no_syntax) - self.assertIn("Subnet: 2", output_no_syntax) - self.assertIn("Subnet: 3", output_no_syntax) - - # Check that only registered hotkeys are printed once for each subnet. - for wallet in mock_wallets: - expected = [ - wallet.hotkey_str for _, wallet in mock_registrations - ].count(wallet.hotkey_str) - occurrences = output_no_syntax.count(wallet.hotkey_str) - self.assertEqual(occurrences, expected) - - # Check that unregistered hotkeys are not printed. - for wallet in mock_wallets: - if wallet not in [w for _, w in mock_registrations]: - self.assertNotIn(wallet.hotkey_str, output_no_syntax) - - def test_overview_not_in_first_subnet(self, _): - if is_running_in_circleci(): - config = self.config - config.wallet.path = "/tmp/test_cli_test_overview" - config.wallet.name = "mock_wallet" - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - - mock_hotkeys = ["hk0", "hk1", "hk2", "hk3", "hk4"] - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - coldkeypub_file=MagicMock( - exists_on_device=MagicMock(return_value=True) # Wallet exists - ), - ) - for idx, hk in enumerate(mock_hotkeys) - ] - - mock_registrations = [ - # No registrations in subnet 1 or 2 - (3, mock_wallets[4]) # hk4 is on netuid 3 - ] - - # Register each wallet to it's subnet - print("Registering mock wallets to subnets...") - - for netuid, wallet in mock_registrations: - print( - "Registering wallet {} to subnet {}".format( - wallet.hotkey_str, netuid - ) - ) - _ = _subtensor_mock.force_register_neuron( - netuid=netuid, - coldkey=wallet.coldkey.ss58_address, - hotkey=wallet.hotkey.ss58_address, - ) - - def mock_get_wallet(*args, **kwargs): - hk = kwargs.get("hotkey") - name_ = kwargs.get("name") - - if not hk and kwargs.get("config"): - hk = kwargs.get("config").wallet.hotkey - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_ and wallet.hotkey_str == hk: - return wallet - else: - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - return mock_wallets[0] - - mock_console = MockConsole() - with patch( - "bittensor.commands.overview.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - with patch("bittensor.__console__", mock_console): - cli.run() - - # Check that the overview was printed. - self.assertIsNotNone(mock_console.captured_print) - - output_no_syntax = mock_console.remove_rich_syntax( - mock_console.captured_print - ) - - # Check that each subnet was printed except subnet 1 and 2. - # Subnet 1 and 2 are not printed because no wallet is registered to them. - self.assertNotIn("Subnet: 1", output_no_syntax) - self.assertNotIn("Subnet: 2", output_no_syntax) - self.assertIn("Subnet: 3", output_no_syntax) - - # Check that only registered hotkeys are printed once for each subnet. - for wallet in mock_wallets: - expected = [ - wallet.hotkey_str for _, wallet in mock_registrations - ].count(wallet.hotkey_str) - occurrences = output_no_syntax.count(wallet.hotkey_str) - self.assertEqual(occurrences, expected) - - # Check that unregistered hotkeys are not printed. - for wallet in mock_wallets: - if wallet not in [w for _, w in mock_registrations]: - self.assertNotIn(wallet.hotkey_str, output_no_syntax) - - def test_overview_with_hotkeys_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.hotkeys = ["some_hotkey"] - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - cli.run() - - def test_overview_without_hotkeys_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - cli.run() - - def test_overview_with_sort_by_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.wallet.sort_by = "rank" - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - cli.run() - - def test_overview_with_sort_by_bad_column_name(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.wallet.sort_by = "totallynotmatchingcolumnname" - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - cli.run() - - def test_overview_without_sort_by_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - cli.run() - - def test_overview_with_sort_order_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.wallet.sort_order = "desc" # Set descending sort order - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - cli.run() - - def test_overview_with_sort_order_config_bad_sort_type(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.wallet.sort_order = "nowaythisshouldmatchanyorderingchoice" - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - cli.run() - - def test_overview_without_sort_order_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - # Don't specify sort_order in config - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - cli.run() - - def test_overview_with_width_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.width = 100 - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - cli.run() - - def test_overview_without_width_config(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - # Don't specify width in config - config.no_prompt = True - config.all = False - config.netuid = [] # Don't set, so it tries all networks. - - cli = bittensor.cli(config) - cli.run() - - def test_overview_all(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "overview" - config.no_prompt = True - config.netuid = [] # Don't set, so it tries all networks. - - config.all = True - cli = bittensor.cli(config) - cli.run() - - def test_unstake_with_specific_hotkeys(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0", "hk1", "hk2"] - config.all_hotkeys = False - # Notice no max_stake specified - - mock_stakes: Dict[str, Balance] = { - # All have more than 5.0 stake - "hk0": Balance.from_float(10.0), - "hk1": Balance.from_float(11.1), - "hk2": Balance.from_float(12.2), - } - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them stakes - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) - - cli.run() - - # Check stakes after unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertAlmostEqual( - stake.tao, - mock_stakes[wallet.hotkey_str].tao - config.amount, - places=4, - ) - - def test_unstake_with_all_hotkeys(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - # Notice wallet.hotkeys not specified - config.all_hotkeys = True - # Notice no max_stake specified - - mock_stakes: Dict[str, Balance] = { - # All have more than 5.0 stake - "hk0": Balance.from_float(10.0), - "hk1": Balance.from_float(11.1), - "hk2": Balance.from_float(12.2), - } - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(list(mock_stakes.keys())) - ] - - # Register mock wallets and give them stakes - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch( - "bittensor.commands.unstake.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) - - cli.run() - - # Check stakes after unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertAlmostEqual( - stake.tao, - mock_stakes[wallet.hotkey_str].tao - config.amount, - places=4, - ) - - def test_unstake_with_exclude_hotkeys_from_all(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk1"] # Exclude hk1 - config.all_hotkeys = True - - mock_stakes: Dict[str, Balance] = { - # All have more than 5.0 stake - "hk0": Balance.from_float(10.0), - "hk1": Balance.from_float(11.1), - "hk2": Balance.from_float(12.2), - } - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(list(mock_stakes.keys())) - ] - - # Register mock wallets and give them stakes - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch( - "bittensor.commands.unstake.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) - - cli.run() - - # Check stakes after unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - if wallet.hotkey_str == "hk1": - # hk1 should not have been unstaked - self.assertAlmostEqual( - stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 - ) - else: - self.assertAlmostEqual( - stake.tao, - mock_stakes[wallet.hotkey_str].tao - config.amount, - places=4, - ) - - def test_unstake_with_multiple_hotkeys_max_stake(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 5.0 # The keys should have at most 5.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0", "hk1", "hk2"] - config.all_hotkeys = False - - mock_stakes: Dict[str, Balance] = { - # All have more than 5.0 stake - "hk0": Balance.from_float(10.0), - "hk1": Balance.from_float(4.9), - "hk2": Balance.from_float(12.2), - } - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(list(mock_stakes.keys())) - ] - - # Register mock wallets and give them stakes - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch( - "bittensor.commands.unstake.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) - - cli.run() - - # Check stakes after unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # All should have been unstaked below or equal to max_stake - self.assertLessEqual( - stake.tao, config.max_stake + 0.0001 - ) # Add a small buffer for fp errors - - if wallet.hotkey_str == "hk1": - # hk1 should not have been unstaked because it was already below max_stake - self.assertAlmostEqual( - stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 - ) - - def test_unstake_with_thresholds(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - # as the minimum required stake may change, this method allows us to dynamically - # update the amount in the mock without updating the tests - min_stake: Balance = _subtensor_mock.get_minimum_required_stake() - # Must be a float - config.amount = min_stake.tao # Unstake below the minimum required stake - wallet_names = ["w0", "w1", "w2"] - config.all_hotkeys = False - # Notice no max_stake specified - - mock_stakes: Dict[str, Balance] = { - "w0": 2 * min_stake - 1, # remaining stake will be below the threshold - "w1": 2 * min_stake - 2, - "w2": 2 * min_stake - 5, - } - - mock_wallets = [ - SimpleNamespace( - name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), - hotkey_str="hk{}".format(idx), # doesn't matter - hotkey=_get_mock_keypair(idx + 100, self.id()), # doesn't matter - ) - for idx, wallet_name in enumerate(wallet_names) - ] - - delegate_hotkey = mock_wallets[0].hotkey.ss58_address - - # Register mock neuron, only for w0 - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=delegate_hotkey, - coldkey=mock_wallets[0].coldkey.ss58_address, - stake=mock_stakes["w0"], - ) - - # Become a delegate - _ = _subtensor_mock.nominate( - wallet=mock_wallets[0], - ) - - # Stake to the delegate with the other coldkeys - for wallet in mock_wallets[1:]: - # Give balance - _ = _subtensor_mock.force_set_balance( - ss58_address=wallet.coldkeypub.ss58_address, - balance=( - mock_stakes[wallet.name] + _subtensor_mock.get_existential_deposit() - ).tao - + 1.0, - ) - _ = _subtensor_mock.add_stake( - wallet=wallet, - hotkey_ss58=delegate_hotkey, - amount=mock_stakes[wallet.name], - ) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("config") and kwargs["config"].get("wallet"): - for wallet in mock_wallets: - if wallet.name == kwargs["config"].wallet.name: - return wallet - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - for wallet in mock_wallets: - # Check stakes before unstaking - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=delegate_hotkey, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.name].rao) - - config.wallet.name = wallet.name - config.hotkey_ss58address = delegate_hotkey # Single unstake - - cli = bittensor.cli(config) - with patch.object(_subtensor_mock, "_do_unstake") as mock_unstake: - with patch( - "bittensor.__console__.print" - ) as mock_print: # Catch console print - cli.run() - - # Filter for console print calls - console_prints = [ - call[0][0] for call in mock_print.call_args_list - ] - minimum_print = filter( - lambda x: "less than minimum of" in x, console_prints - ) - - unstake_calls = mock_unstake.call_args_list - self.assertEqual(len(unstake_calls), 1) # Only one unstake call - - _, kwargs = unstake_calls[0] - # Verify delegate was unstaked - self.assertEqual(kwargs["hotkey_ss58"], delegate_hotkey) - self.assertEqual(kwargs["wallet"].name, wallet.name) - - if wallet.name == "w0": - # This wallet owns the delegate - # Should unstake specified amount - self.assertEqual( - kwargs["amount"], bittensor.Balance(config.amount) - ) - # No warning for w0 - self.assertRaises( - StopIteration, next, minimum_print - ) # No warning for w0 - else: - # Should unstake *all* the stake - staked = mock_stakes[wallet.name] - self.assertEqual(kwargs["amount"], staked) - - # Check warning was printed - _ = next( - minimum_print - ) # Doesn't raise, so the warning was printed - - def test_unstake_all(self, _): - config = self.config - config.command = "stake" - config.subcommand = "remove" - config.no_prompt = True - config.amount = 0.0 # 0 implies full unstake - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0"] - config.all_hotkeys = False - - mock_stakes: Dict[str, Balance] = {"hk0": Balance.from_float(10.0)} - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them stakes - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao) - - cli.run() - - # Check stakes after unstaking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # because the amount is less than the threshold, none of these should unstake - self.assertEqual(stake.tao, Balance.from_tao(0)) - - def test_stake_with_specific_hotkeys(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0", "hk1", "hk2"] - config.all_hotkeys = False - # Notice no max_stake specified - - mock_balance = Balance.from_float(22.2) - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkey.ss58_address, - ) - - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, 0) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertAlmostEqual(stake.tao, config.amount, places=4) - - def test_stake_with_all_hotkeys(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - # Notice wallet.hotkeys is not specified - config.all_hotkeys = True - # Notice no max_stake specified - - mock_hotkeys = ["hk0", "hk1", "hk2"] - - mock_balance = Balance.from_float(22.0) - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(mock_hotkeys) - ] - - # Register mock wallets and give them no stake - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - # Set the coldkey balance - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - with patch( - "bittensor.commands.stake.get_hotkey_wallets_for_wallet" - ) as mock_get_hotkey_wallets_for_wallet: - mock_get_hotkey_wallets_for_wallet.return_value = mock_wallets - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 0 - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 5.0 - self.assertAlmostEqual(stake.tao, config.amount, places=4) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertAlmostEqual( - balance.tao, - mock_balance.tao - (config.amount * len(mock_wallets)), - places=4, - ) - - def test_stake_with_exclude_hotkeys_from_all(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk1"] # exclude hk1 - config.all_hotkeys = True - # Notice no max_stake specified - - mock_hotkeys = ["hk0", "hk1", "hk2"] - - mock_balance = Balance.from_float(25.0) - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(mock_hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - # Set the coldkey balance - _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch( - "bittensor.commands.stake.get_hotkey_wallets_for_wallet" - ) as mock_get_all_wallets: - mock_get_all_wallets.return_value = mock_wallets - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 0 - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - if wallet.hotkey_str == "hk1": - # Check that hk1 stake is 0 - # We excluded it from staking - self.assertEqual(stake.tao, 0) - else: - # Check that all stakes are 5.0 - self.assertAlmostEqual(stake.tao, config.amount, places=4) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertAlmostEqual( - balance.tao, mock_balance.tao - (config.amount * 2), places=4 - ) - - def test_stake_with_multiple_hotkeys_max_stake(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0", "hk1", "hk2"] - config.all_hotkeys = False - - mock_balance = Balance.from_float(config.max_stake * 3) - - mock_stakes: Dict[str, Balance] = { - "hk0": Balance.from_float(0.0), - "hk1": Balance.from_float(config.max_stake * 2), - "hk2": Balance.from_float(0.0), - } - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - if wallet.hotkey_str == "hk1": - # Set the stake for hk1 - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, - ) - else: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are correct - if wallet.hotkey_str == "hk1": - self.assertAlmostEqual(stake.tao, config.max_stake * 2, places=4) - else: - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - # Check that all stakes at least 15.0 - self.assertGreaterEqual(stake.tao + 0.1, config.max_stake) - - if wallet.hotkey_str == "hk1": - # Check that hk1 stake was not changed - # It had more than max_stake already - self.assertAlmostEqual( - stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 - ) - - # Check that the balance decreased - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertLessEqual(balance.tao, mock_balance.tao) - - def test_stake_with_multiple_hotkeys_max_stake_not_enough_balance(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0", "hk1", "hk2"] - config.all_hotkeys = False - - mock_balance = Balance.from_float(15.0 * 2) # Not enough for all hotkeys - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 0 - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - if wallet.hotkey_str == "hk2": - # Check that the stake is still 0 - self.assertEqual(stake.tao, 0) - - else: - # Check that all stakes are maximum of 15.0 - self.assertLessEqual(stake.tao, config.max_stake) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertLessEqual(balance.tao, mock_balance.tao) - - def test_stake_with_single_hotkey_max_stake(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0"] - config.all_hotkeys = False - - mock_balance = Balance.from_float(15.0 * 3) - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - print("Registering mock wallet {}".format(wallet.hotkey_str)) - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 0 - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - # Check stakes after staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - # Check that all stakes are maximum of 15.0 - self.assertLessEqual(stake.tao, config.max_stake) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertLessEqual(balance.tao, mock_balance.tao) - - def test_stake_with_single_hotkey_max_stake_not_enough_balance(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0"] - config.all_hotkeys = False - - mock_balance = Balance.from_float(1.0) # Not enough balance to do max - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - ) - - _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - for wallet in mock_wallets: - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that all stakes are 0 - self.assertEqual(stake.rao, 0) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - wallet = mock_wallets[0] - - # Check did not stake - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - # Check that stake is less than max_stake - 1.0 - self.assertLessEqual(stake.tao, config.max_stake - 1.0) - - # Check that the balance decreased by less than max_stake - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertGreaterEqual(balance.tao, mock_balance.tao - config.max_stake) - - def test_stake_with_single_hotkey_max_stake_enough_stake(self, _): - # tests max stake when stake >= max_stake already - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - # Notie amount is not specified - config.max_stake = 15.0 # The keys should have at most 15.0 tao staked after - config.wallet.name = "fake_wallet" - config.hotkeys = ["hk0"] - config.all_hotkeys = False - - mock_balance = Balance.from_float(config.max_stake * 3) - - mock_stakes: Dict[str, Balance] = { # has enough stake, more than max_stake - "hk0": Balance.from_float(config.max_stake * 2) - } - - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - - mock_wallets = [ - SimpleNamespace( - name=config.wallet.name, - coldkey=mock_coldkey_kp, - coldkeypub=mock_coldkey_kp, - hotkey_str=hk, - hotkey=_get_mock_keypair(idx + 100, self.id()), - ) - for idx, hk in enumerate(config.hotkeys) - ] - - # Register mock wallets and give them balances - print("Registering mock wallets...") - - for wallet in mock_wallets: - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=wallet.hotkey.ss58_address, - coldkey=wallet.coldkeypub.ss58_address, - stake=mock_stakes[wallet.hotkey_str].rao, # More than max_stake - ) - - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_coldkey_kp.ss58_address, balance=mock_balance.rao - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("hotkey"): - for wallet in mock_wallets: - if wallet.hotkey_str == kwargs.get("hotkey"): - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - # Check stakes before staking - wallet = mock_wallets[0] - - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - # Check that stake is correct - self.assertAlmostEqual( - stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 - ) - # Check that the stake is greater than or equal to max_stake - self.assertGreaterEqual(stake.tao, config.max_stake) - - # Check that the balance is correct - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - cli.run() - - wallet = mock_wallets[0] - - # Check did not stake, since stake >= max_stake - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=wallet.hotkey.ss58_address, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - - # Check that all stake is unchanged - self.assertAlmostEqual( - stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4 - ) - - # Check that the balance is the same - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertAlmostEqual(balance.tao, mock_balance.tao, places=4) - - def test_stake_with_thresholds(self, _): - config = self.config - config.command = "stake" - config.subcommand = "add" - config.no_prompt = True - - min_stake: Balance = _subtensor_mock.get_minimum_required_stake() - # Must be a float - wallet_names = ["w0", "w1", "w2"] - config.all_hotkeys = False - # Notice no max_stake specified - - mock_stakes: Dict[str, Balance] = { - "w0": min_stake - 1, # new stake will be below the threshold - "w1": min_stake - 2, - "w2": min_stake - 5, - } - - mock_wallets = [ - SimpleNamespace( - name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), - hotkey_str="hk{}".format(idx), # doesn't matter - hotkey=_get_mock_keypair(idx + 100, self.id()), # doesn't matter - ) - for idx, wallet_name in enumerate(wallet_names) - ] - - delegate_hotkey = mock_wallets[0].hotkey.ss58_address - - # Register mock neuron, only for w0 - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=delegate_hotkey, - coldkey=mock_wallets[0].coldkey.ss58_address, - balance=(mock_stakes["w0"] + _subtensor_mock.get_existential_deposit()).tao - + 1.0, - ) # No stake, but enough balance - - # Become a delegate - _ = _subtensor_mock.nominate( - wallet=mock_wallets[0], - ) - - # Give enough balance - for wallet in mock_wallets[1:]: - # Give balance - _ = _subtensor_mock.force_set_balance( - ss58_address=wallet.coldkeypub.ss58_address, - balance=( - mock_stakes[wallet.name] + _subtensor_mock.get_existential_deposit() - ).tao - + 1.0, - ) - - def mock_get_wallet(*args, **kwargs): - if kwargs.get("config") and kwargs["config"].get("wallet"): - for wallet in mock_wallets: - if wallet.name == kwargs["config"].wallet.name: - return wallet - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - for wallet in mock_wallets: - # Check balances and stakes before staking - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=delegate_hotkey, - coldkey_ss58=wallet.coldkey.ss58_address, - ) - self.assertEqual(stake.rao, 0) # No stake - - balance = _subtensor_mock.get_balance( - address=wallet.coldkeypub.ss58_address - ) - self.assertGreaterEqual( - balance, mock_stakes[wallet.name] - ) # Enough balance - - config.wallet.name = wallet.name - config.wallet.hotkey = delegate_hotkey # Single stake - config.amount = mock_stakes[ - wallet.name - ].tao # Stake an amount below the threshold - - cli = bittensor.cli(config) - with patch.object(_subtensor_mock, "_do_stake") as mock_stake: - with patch( - "bittensor.__console__.print" - ) as mock_print: # Catch console print - cli.run() - - # Filter for console print calls - console_prints = [ - call[0][0] for call in mock_print.call_args_list - ] - minimum_print = filter( - lambda x: "below the minimum required" in x, console_prints - ) - - if wallet.name == "w0": - # This wallet owns the delegate - stake_calls = mock_stake.call_args_list - # Can stake below the threshold - self.assertEqual(len(stake_calls), 1) - - _, kwargs = stake_calls[0] - - # Should stake specified amount - self.assertEqual( - kwargs["amount"], bittensor.Balance(config.amount) - ) - # No error for w0 - self.assertRaises( - StopIteration, next, minimum_print - ) # No warning for w0 - else: - # Should not call stake - self.assertEqual(len(mock_stake.call_args_list), 0) - # Should print error - self.assertIsNotNone(next(minimum_print)) - - def test_nominate(self, _): - config = self.config - config.command = "root" - config.subcommand = "nominate" - config.no_prompt = True - config.wallet.name = "w0" - config.hotkey = "hk0" - - mock_balance = Balance.from_float(100.0) - - mock_wallet = SimpleNamespace( - name="w0", - coldkey=_get_mock_keypair(0, self.id()), - coldkeypub=_get_mock_keypair(0, self.id()), - hotkey_str="hk0", - hotkey=_get_mock_keypair(0 + 100, self.id()), - ) - - # Register mock wallet and give it a balance - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=mock_wallet.hotkey.ss58_address, - coldkey=mock_wallet.coldkey.ss58_address, - balance=mock_balance.rao, - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - hk = kwargs.get("hotkey") - name_ = kwargs.get("name") - - if not hk and kwargs.get("config"): - hk = kwargs.get("config").wallet.hotkey - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - if mock_wallet.name == name_: - return mock_wallet - else: - raise ValueError("Mock wallet not found") - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - cli.run() - - # Check the nomination - is_delegate = _subtensor_mock.is_hotkey_delegate( - hotkey_ss58=mock_wallet.hotkey.ss58_address - ) - self.assertTrue(is_delegate) - - def test_delegate_stake(self, _): - config = self.config - config.command = "root" - config.subcommand = "delegate" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "w1" - - mock_balances: Dict[str, Balance] = { - # All have more than 5.0 stake - "w0": { - "hk0": Balance.from_float(10.0), - }, - "w1": {"hk1": Balance.from_float(11.1)}, - } - - mock_stake = Balance.from_float(5.0) - - mock_wallets = [] - for idx, wallet_name in enumerate(list(mock_balances.keys())): - for idx_hk, hk in enumerate(list(mock_balances[wallet_name].keys())): - wallet = SimpleNamespace( - name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), - hotkey_str=hk, - hotkey=_get_mock_keypair(idx * 100 + idx_hk, self.id()), - ) - mock_wallets.append(wallet) - - # Set hotkey to be the hotkey from the other wallet - config.delegate_ss58key: str = mock_wallets[0].hotkey.ss58_address - - # Register mock wallets and give them balance - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=mock_wallets[0].hotkey.ss58_address, - coldkey=mock_wallets[0].coldkey.ss58_address, - balance=mock_balances["w0"]["hk0"].rao, - stake=mock_stake.rao, # Needs set stake to be a validator - ) - - # Give w1 some balance - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_wallets[1].coldkey.ss58_address, - balance=mock_balances["w1"]["hk1"].rao, - ) - - # Make the first wallet a delegate - success = _subtensor_mock.nominate(wallet=mock_wallets[0]) - self.assertTrue(success) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - hk = kwargs.get("hotkey") - name_ = kwargs.get("name") - - if not hk and kwargs.get("config"): - hk = kwargs.get("config").wallet.hotkey - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_ and wallet.hotkey_str == hk: - return wallet - else: - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - cli.run() - - # Check the stake - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=mock_wallets[0].hotkey.ss58_address, - coldkey_ss58=mock_wallets[1].coldkey.ss58_address, - ) - self.assertAlmostEqual(stake.tao, config.amount, places=4) - - def test_undelegate_stake(self, _): - config = self.config - config.command = "root" - config.subcommand = "undelegate" - config.no_prompt = True - config.amount = 5.0 - config.wallet.name = "w1" - - mock_balances: Dict[str, Balance] = { - # All have more than 5.0 stake - "w0": { - "hk0": Balance.from_float(10.0), - }, - "w1": {"hk1": Balance.from_float(11.1)}, - } - - mock_stake = Balance.from_float(5.0) - mock_delegated = Balance.from_float(6.0) - - mock_wallets = [] - for idx, wallet_name in enumerate(list(mock_balances.keys())): - for idx_hk, hk in enumerate(list(mock_balances[wallet_name].keys())): - wallet = SimpleNamespace( - name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), - hotkey_str=hk, - hotkey=_get_mock_keypair(idx * 100 + idx_hk, self.id()), - ) - mock_wallets.append(wallet) - - # Set hotkey to be the hotkey from the other wallet - config.delegate_ss58key: str = mock_wallets[0].hotkey.ss58_address - - # Register mock wallets and give them balance - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=mock_wallets[0].hotkey.ss58_address, - coldkey=mock_wallets[0].coldkey.ss58_address, - balance=mock_balances["w0"]["hk0"].rao, - stake=mock_stake.rao, # Needs set stake to be a validator - ) - - # Give w1 some balance - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_wallets[1].coldkey.ss58_address, - balance=mock_balances["w1"]["hk1"].rao, - ) - - # Make the first wallet a delegate - success = _subtensor_mock.nominate(wallet=mock_wallets[0]) - self.assertTrue(success) - - # Stake to the delegate - success = _subtensor_mock.delegate( - wallet=mock_wallets[1], - delegate_ss58=mock_wallets[0].hotkey.ss58_address, - amount=mock_delegated, - prompt=False, - ) - self.assertTrue(success) - - # Verify the stake - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=mock_wallets[0].hotkey.ss58_address, - coldkey_ss58=mock_wallets[1].coldkey.ss58_address, - ) - self.assertAlmostEqual(stake.tao, mock_delegated.tao, places=4) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - hk = kwargs.get("hotkey") - name_ = kwargs.get("name") - - if not hk and kwargs.get("config"): - hk = kwargs.get("config").wallet.hotkey - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_ and wallet.hotkey_str == hk: - return wallet - else: - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - return mock_wallets[0] - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - cli.run() - - # Check the stake - stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=mock_wallets[0].hotkey.ss58_address, - coldkey_ss58=mock_wallets[1].coldkey.ss58_address, - ) - self.assertAlmostEqual( - stake.tao, mock_delegated.tao - config.amount, places=4 - ) - - def test_transfer(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "transfer" - config.no_prompt = True - config.amount = 3.2 - config.wallet.name = "w1" - - mock_balances: Dict[str, Balance] = { - "w0": Balance.from_float(10.0), - "w1": Balance.from_float(config.amount + 0.001), - } - - mock_wallets = [] - for idx, wallet_name in enumerate(list(mock_balances.keys())): - wallet = SimpleNamespace( - name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), - ) - mock_wallets.append(wallet) - - # Set dest to w0 - config.dest = mock_wallets[0].coldkey.ss58_address - - # Give w0 and w1 balance - - for wallet in mock_wallets: - success, err = _subtensor_mock.force_set_balance( - ss58_address=wallet.coldkey.ss58_address, - balance=mock_balances[wallet.name].rao, - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - name_ = kwargs.get("name") - - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - raise ValueError(f"No mock wallet found with name: {name_}") - - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - cli.run() - - # Check the balance of w0 - balance = _subtensor_mock.get_balance( - address=mock_wallets[0].coldkey.ss58_address - ) - self.assertAlmostEqual( - balance.tao, mock_balances["w0"].tao + config.amount, places=4 - ) - - # Check the balance of w1 - balance = _subtensor_mock.get_balance( - address=mock_wallets[1].coldkey.ss58_address - ) - self.assertAlmostEqual( - balance.tao, mock_balances["w1"].tao - config.amount, places=4 - ) # no fees - - def test_transfer_not_enough_balance(self, _): - config = self.config - config.command = "wallet" - config.subcommand = "transfer" - config.no_prompt = True - config.amount = 3.2 - config.wallet.name = "w1" - - mock_balances: Dict[str, Balance] = { - "w0": Balance.from_float(10.0), - "w1": Balance.from_float(config.amount - 0.1), # not enough balance - } - - mock_wallets = [] - for idx, wallet_name in enumerate(list(mock_balances.keys())): - wallet = SimpleNamespace( - name=wallet_name, - coldkey=_get_mock_keypair(idx, self.id()), - coldkeypub=_get_mock_keypair(idx, self.id()), - ) - mock_wallets.append(wallet) - - # Set dest to w0 - config.dest = mock_wallets[0].coldkey.ss58_address - - # Give w0 and w1 balance - - for wallet in mock_wallets: - success, err = _subtensor_mock.force_set_balance( - ss58_address=wallet.coldkey.ss58_address, - balance=mock_balances[wallet.name].rao, - ) - - cli = bittensor.cli(config) - - def mock_get_wallet(*args, **kwargs): - name_ = kwargs.get("name") - - if not name_ and kwargs.get("config"): - name_ = kwargs.get("config").wallet.name - - for wallet in mock_wallets: - if wallet.name == name_: - return wallet - else: - raise ValueError(f"No mock wallet found with name: {name_}") - - mock_console = MockConsole() - with patch("bittensor.wallet") as mock_create_wallet: - mock_create_wallet.side_effect = mock_get_wallet - - with patch("bittensor.__console__", mock_console): - cli.run() - - # Check that the overview was printed. - self.assertIsNotNone(mock_console.captured_print) - - output_no_syntax = mock_console.remove_rich_syntax( - mock_console.captured_print - ) - - self.assertIn("Not enough balance", output_no_syntax) - - # Check the balance of w0 - balance = _subtensor_mock.get_balance( - address=mock_wallets[0].coldkey.ss58_address - ) - self.assertAlmostEqual( - balance.tao, mock_balances["w0"].tao, places=4 - ) # did not transfer - - # Check the balance of w1 - balance = _subtensor_mock.get_balance( - address=mock_wallets[1].coldkey.ss58_address - ) - self.assertAlmostEqual( - balance.tao, mock_balances["w1"].tao, places=4 - ) # did not transfer - - def test_register(self, _): - config = self.config - config.command = "subnets" - config.subcommand = "register" - config.no_prompt = True - - mock_wallet = generate_wallet(hotkey=_get_mock_keypair(100, self.id())) - - # Give the wallet some balance for burning - success, err = _subtensor_mock.force_set_balance( - ss58_address=mock_wallet.coldkeypub.ss58_address, - balance=Balance.from_float(200.0), - ) - - with patch("bittensor.wallet", return_value=mock_wallet) as mock_create_wallet: - cli = bittensor.cli(config) - cli.run() - mock_create_wallet.assert_called_once() - - # Verify that the wallet was registered - subtensor = bittensor.subtensor(config) - registered = subtensor.is_hotkey_registered_on_subnet( - hotkey_ss58=mock_wallet.hotkey.ss58_address, netuid=1 - ) - - self.assertTrue(registered) - - def test_pow_register(self, _): - # Not the best way to do this, but I need to finish these tests, and unittest doesn't make this - # as simple as pytest - config = self.config - config.command = "subnets" - config.subcommand = "pow_register" - config.pow_register.num_processes = 1 - config.pow_register.update_interval = 50_000 - config.no_prompt = True - - mock_wallet = generate_wallet(hotkey=_get_mock_keypair(100, self.id())) - - class MockException(Exception): - pass - - with patch("bittensor.wallet", return_value=mock_wallet) as mock_create_wallet: - with patch( - "bittensor.extrinsics.registration.POWSolution.is_stale", - side_effect=MockException, - ) as mock_is_stale: - with pytest.raises(MockException): - cli = bittensor.cli(config) - cli.run() - mock_create_wallet.assert_called_once() - - self.assertEqual(mock_is_stale.call_count, 1) - - def test_stake(self, _): - amount_to_stake: Balance = Balance.from_tao(0.5) - config = self.config - config.no_prompt = True - config.command = "stake" - config.subcommand = "add" - config.amount = amount_to_stake.tao - config.stake_all = False - config.use_password = False - config.model = "core_server" - config.hotkey = "hk0" - - subtensor = bittensor.subtensor(config) - - mock_wallet = generate_wallet(hotkey=_get_mock_keypair(100, self.id())) - - # Register the hotkey and give it some balance - _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=mock_wallet.hotkey.ss58_address, - coldkey=mock_wallet.coldkey.ss58_address, - balance=( - amount_to_stake + Balance.from_tao(1.0) - ).rao, # 1.0 tao extra for fees, etc - ) - - with patch("bittensor.wallet", return_value=mock_wallet) as mock_create_wallet: - old_stake = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=mock_wallet.hotkey.ss58_address, - coldkey_ss58=mock_wallet.coldkey.ss58_address, - ) - - cli = bittensor.cli(config) - cli.run() - mock_create_wallet.assert_called() - self.assertEqual(mock_create_wallet.call_count, 2) - - new_stake = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=mock_wallet.hotkey.ss58_address, - coldkey_ss58=mock_wallet.coldkey.ss58_address, - ) - - self.assertGreater(new_stake, old_stake) - - def test_metagraph(self, _): - config = self.config - config.wallet.name = "metagraph_testwallet" - config.command = "subnets" - config.subcommand = "metagraph" - config.no_prompt = True - - # Add some neurons to the metagraph - mock_nn = [] - - def register_mock_neuron(i: int) -> int: - mock_nn.append( - SimpleNamespace( - hotkey=_get_mock_keypair(i + 100, self.id()).ss58_address, - coldkey=_get_mock_keypair(i, self.id()).ss58_address, - balance=Balance.from_rao(random.randint(0, 2**45)).rao, - stake=Balance.from_rao(random.randint(0, 2**45)).rao, - ) - ) - uid = _subtensor_mock.force_register_neuron( - netuid=config.netuid, - hotkey=mock_nn[i].hotkey, - coldkey=mock_nn[i].coldkey, - balance=mock_nn[i].balance, - stake=mock_nn[i].stake, - ) - return uid - - for i in range(5): - _ = register_mock_neuron(i) - - _subtensor_mock.neurons_lite(netuid=config.netuid) - - cli = bittensor.cli(config) - - mock_console = MockConsole() - with patch("bittensor.__console__", mock_console): - cli.run() - - # Check that the overview was printed. - self.assertIsNotNone(mock_console.captured_print) - - output_no_syntax = mock_console.remove_rich_syntax(mock_console.captured_print) - - self.assertIn("Metagraph", output_no_syntax) - nn = _subtensor_mock.neurons_lite(netuid=config.netuid) - self.assertIn( - str(len(nn) - 1), output_no_syntax - ) # Check that the number of neurons is output - # Check each uid is in the output - for neuron in nn: - self.assertIn(str(neuron.uid), output_no_syntax) - - def test_inspect(self, _): - config = self.config - config.wallet.name = "inspect_testwallet" - config.no_prompt = True - config.n_words = 12 - config.use_password = False - config.overwrite_coldkey = True - config.overwrite_hotkey = True - - # First create a new coldkey - config.command = "wallet" - config.subcommand = "new_coldkey" - cli = bittensor.cli(config) - cli.run() - - # Now let's give it a hotkey - config.command = "wallet" - config.subcommand = "new_hotkey" - cli.config = config - cli.run() - - # Now inspect it - config.command = "wallet" - cli.config.subcommand = "inspect" - cli.config = config - cli.run() - - config.command = "wallet" - cli.config.subcommand = "list" - cli.config = config - cli.run() - - # Run History Command to get list of transfers - config.command = "wallet" - cli.config.subcommand = "history" - cli.config = config - cli.run() - - -@patch("bittensor.subtensor", new_callable=return_mock_sub) -class TestCLIWithNetworkUsingArgs(unittest.TestCase): - """ - Test the CLI by passing args directly to the bittensor.cli factory - """ - - @unittest.mock.patch.object(MockSubtensor, "get_delegates") - def test_list_delegates(self, mocked_get_delegates, _): - # Call - cli = bittensor.cli(args=["root", "list_delegates"]) - cli.run() - - # Assertions - # make sure get_delegates called once without previous state (current only) - self.assertEqual(mocked_get_delegates.call_count, 2) - - def test_list_subnets(self, _): - cli = bittensor.cli( - args=[ - "subnets", - "list", - ] - ) - cli.run() - - def test_delegate(self, _): - """ - Test delegate add command - """ - mock_wallet = generate_wallet(hotkey=_get_mock_keypair(100, self.id())) - delegate_wallet = generate_wallet(hotkey=_get_mock_keypair(100 + 1, self.id())) - - # register the wallet - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=mock_wallet.hotkey.ss58_address, - coldkey=mock_wallet.coldkey.ss58_address, - ) - - # register the delegate - _ = _subtensor_mock.force_register_neuron( - netuid=1, - hotkey=delegate_wallet.hotkey.ss58_address, - coldkey=delegate_wallet.coldkey.ss58_address, - ) - - # make the delegate a delegate - _subtensor_mock.nominate(delegate_wallet, wait_for_finalization=True) - self.assertTrue( - _subtensor_mock.is_hotkey_delegate(delegate_wallet.hotkey.ss58_address) - ) - - # Give the wallet some TAO - _, err = _subtensor_mock.force_set_balance( - ss58_address=mock_wallet.coldkey.ss58_address, - balance=Balance.from_tao(20.0), - ) - self.assertEqual(err, None) - - # Check balance - old_balance = _subtensor_mock.get_balance(mock_wallet.coldkey.ss58_address) - self.assertEqual(old_balance.tao, 20.0) - - # Check delegate stake - old_delegate_stake = _subtensor_mock.get_total_stake_for_hotkey( - delegate_wallet.hotkey.ss58_address - ) - - # Check wallet stake - old_wallet_stake = _subtensor_mock.get_total_stake_for_coldkey( - mock_wallet.coldkey.ss58_address - ) - - with patch( - "bittensor.wallet", return_value=mock_wallet - ): # Mock wallet creation. SHOULD NOT BE REGISTERED - cli = bittensor.cli( - args=[ - "root", - "delegate", - "--subtensor.network", - "mock", # Mock network - "--wallet.name", - "mock", - "--delegate_ss58key", - delegate_wallet.hotkey.ss58_address, - "--amount", - "10.0", # Delegate 10 TAO - "--no_prompt", - ] - ) - cli.run() - - # Check delegate stake - new_delegate_stake = _subtensor_mock.get_total_stake_for_hotkey( - delegate_wallet.hotkey.ss58_address - ) - - # Check wallet stake - new_wallet_stake = _subtensor_mock.get_total_stake_for_coldkey( - mock_wallet.coldkey.ss58_address - ) - - # Check that the delegate stake increased by 10 TAO - self.assertAlmostEqual( - new_delegate_stake.tao, old_delegate_stake.tao + 10.0, delta=1e-6 - ) - - # Check that the wallet stake increased by 10 TAO - self.assertAlmostEqual( - new_wallet_stake.tao, old_wallet_stake.tao + 10.0, delta=1e-6 - ) - - new_balance = _subtensor_mock.get_balance(mock_wallet.coldkey.ss58_address) - self.assertAlmostEqual(new_balance.tao, old_balance.tao - 10.0, delta=1e-6) - - -@pytest.fixture(scope="function") -def wallets_dir_path(tmp_path): - wallets_dir = tmp_path / "wallets" - wallets_dir.mkdir() - yield wallets_dir - - -@pytest.mark.parametrize( - "test_id, wallet_names, expected_wallet_count", - [ - ("happy_path_single_wallet", ["wallet1"], 1), # Single wallet - ( - "happy_path_multiple_wallets", - ["wallet1", "wallet2", "wallet3"], - 3, - ), # Multiple wallets - ("happy_path_no_wallets", [], 0), # No wallets - ], -) -def test_get_coldkey_wallets_for_path( - test_id, wallet_names, expected_wallet_count, wallets_dir_path -): - # Arrange: Create mock wallet directories - for name in wallet_names: - (wallets_dir_path / name).mkdir() - - # Act: Call the function with the test directory - wallets = _get_coldkey_wallets_for_path(str(wallets_dir_path)) - - # Assert: Check if the correct number of wallet objects are returned - assert len(wallets) == expected_wallet_count - for wallet in wallets: - assert isinstance( - wallet, Wallet - ), "The returned object should be an instance of bittensor.wallet" - - -@pytest.mark.parametrize( - "test_id, exception, mock_path, expected_result", - [ - ( - "error_case_invalid_path", - StopIteration, - "/invalid/path", - [], - ), # Invalid path causing StopIteration - ], -) -def test_get_coldkey_wallets_for_path_errors( - test_id, exception, mock_path, expected_result -): - # Arrange: Patch os.walk to raise an exception - with patch("os.walk", side_effect=exception): - # Act: Call the function with an invalid path - wallets = _get_coldkey_wallets_for_path(mock_path) - - # Assert: Check if an empty list is returned - assert ( - wallets == expected_result - ), "Function should return an empty list on error" - - -@pytest.mark.parametrize( - "test_id, display, legal, web, riot, email, pgp_fingerprint, image, info, twitter, expected_exception, expected_message", - [ - ( - "test-run-happy-path-1", - "Alice", - "Alice Doe", - "https://alice.example.com", - "@alice:matrix", - "alice@example.com", - "ABCD1234ABCD1234ABCD", - "https://alice.image", - "Alice in Wonderland", - "@liceTwitter", - None, - "", - ), - # Edge cases - ( - "test_run_edge_case_002", - "", - "", - "", - "", - "", - "", - "", - "", - "", - None, - "", - ), # Empty strings as input - # Error cases - # Each field has a maximum size of 64 bytes, PGP fingerprint has a maximum size of 20 bytes - ( - "test_run_error_case_003", - "A" * 65, - "B" * 65, - "C" * 65, - "D" * 65, - "E" * 65, - "F" * 21, - "G" * 65, - "H" * 65, - "I" * 65, - ValueError, - "Identity value `display` must be <= 64 raw bytes", - ), - ], -) -def test_set_identity_command( - test_id, - display, - legal, - web, - riot, - email, - pgp_fingerprint, - image, - info, - twitter, - expected_exception, - expected_message, -): - # Arrange - mock_cli = MagicMock() - mock_cli.config = MagicMock( - display=display, - legal=legal, - web=web, - riot=riot, - email=email, - pgp_fingerprint=pgp_fingerprint, - image=image, - info=info, - twitter=twitter, - ) - mock_subtensor = MagicMock() - mock_subtensor.update_identity = MagicMock() - mock_subtensor.query_identity = MagicMock(return_value={}) - mock_subtensor.close = MagicMock() - mock_wallet = MagicMock() - mock_wallet.hotkey.ss58_address = "fake_ss58_address" - mock_wallet.coldkey.ss58_address = "fake_coldkey_ss58_address" - mock_wallet.coldkey = MagicMock() - - with patch("bittensor.subtensor", return_value=mock_subtensor), patch( - "bittensor.wallet", return_value=mock_wallet - ), patch("bittensor.__console__", MagicMock()), patch( - "rich.prompt.Prompt.ask", side_effect=["y", "y"] - ), patch("sys.exit") as mock_exit: - # Act - if expected_exception: - with pytest.raises(expected_exception) as exc_info: - SetIdentityCommand._run(mock_cli, mock_subtensor) - # Assert - assert str(exc_info.value) == expected_message - else: - SetIdentityCommand._run(mock_cli, mock_subtensor) - # Assert - mock_subtensor.update_identity.assert_called_once() - assert mock_exit.call_count == 0 - - -@pytest.fixture -def setup_files(tmp_path): - def _setup_files(files): - for file_path, content in files.items(): - full_path = tmp_path / file_path - os.makedirs(full_path.parent, exist_ok=True) - with open(full_path, "w") as f: - f.write(content) - return tmp_path - - return _setup_files - - -@pytest.mark.parametrize( - "test_id, setup_data, expected", - [ - # Error cases - ( - "error_case_nonexistent_dir", - {"just_a_file.txt": ""}, - ([], []), - ), # Nonexistent dir - ], -) -def test_get_coldkey_ss58_addresses_for_path( - setup_files, test_id, setup_data, expected -): - path = setup_files(setup_data) - - # Arrange - # Setup done in setup_files fixture and parametrize - - # Act - result = _get_coldkey_ss58_addresses_for_path(str(path)) - - # Assert - assert ( - result == expected - ), f"Test ID: {test_id} failed. Expected {expected}, got {result}" - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/integration_tests/test_cli_no_network.py b/tests/integration_tests/test_cli_no_network.py deleted file mode 100644 index e3a3d6a49..000000000 --- a/tests/integration_tests/test_cli_no_network.py +++ /dev/null @@ -1,1533 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2022 Yuma Rao -# Copyright © 2022-2023 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 unittest -from unittest.mock import MagicMock, patch -from typing import Any, Optional -import pytest -from copy import deepcopy -import re - -from tests.helpers import _get_mock_coldkey, __mock_wallet_factory__, MockConsole - -import bittensor -from bittensor import Balance -from rich.table import Table - - -class MockException(Exception): - pass - - -mock_delegate_info = { - "hotkey_ss58": "", - "total_stake": bittensor.Balance.from_rao(0), - "nominators": [], - "owner_ss58": "", - "take": 0.18, - "validator_permits": [], - "registrations": [], - "return_per_1000": bittensor.Balance.from_rao(0), - "total_daily_return": bittensor.Balance.from_rao(0), -} - - -def return_mock_sub_1(*args, **kwargs): - return MagicMock( - return_value=MagicMock( - get_subnets=MagicMock(return_value=[1]), # Mock subnet 1 ONLY. - block=10_000, - get_delegates=MagicMock( - return_value=[bittensor.DelegateInfo(**mock_delegate_info)] - ), - ) - ) - - -def return_mock_wallet_factory(*args, **kwargs): - return MagicMock( - return_value=__mock_wallet_factory__(*args, **kwargs), - add_args=bittensor.wallet.add_args, - ) - - -@patch( - "bittensor.subtensor", - new_callable=return_mock_sub_1, -) -@patch("bittensor.wallet", new_callable=return_mock_wallet_factory) -class TestCLINoNetwork(unittest.TestCase): - def setUp(self): - self._config = TestCLINoNetwork.construct_config() - - def config(self): - copy_ = deepcopy(self._config) - return copy_ - - @staticmethod - def construct_config(): - parser = bittensor.cli.__create_parser__() - defaults = bittensor.config(parser=parser, args=["subnets", "metagraph"]) - - # Parse commands and subcommands - for command in bittensor.ALL_COMMANDS: - if ( - command in bittensor.ALL_COMMANDS - and "commands" in bittensor.ALL_COMMANDS[command] - ): - for subcommand in bittensor.ALL_COMMANDS[command]["commands"]: - defaults.merge( - bittensor.config(parser=parser, args=[command, subcommand]) - ) - else: - defaults.merge(bittensor.config(parser=parser, args=[command])) - - defaults.netuid = 1 - defaults.subtensor.network = "mock" - defaults.no_version_checking = True - - return defaults - - def test_check_configs(self, _, __): - config = self.config() - config.no_prompt = True - config.model = "core_server" - config.dest = "no_prompt" - config.amount = 1 - config.mnemonic = "this is a mnemonic" - config.seed = None - config.uids = [1, 2, 3] - config.weights = [0.25, 0.25, 0.25, 0.25] - config.no_version_checking = True - config.ss58_address = bittensor.Keypair.create_from_seed(b"0" * 32).ss58_address - config.public_key_hex = None - config.proposal_hash = "" - - cli_instance = bittensor.cli - - # Define the response function for rich.prompt.Prompt.ask - def ask_response(prompt: str) -> Any: - if "delegate index" in prompt: - return 0 - elif "wallet name" in prompt: - return "mock" - elif "hotkey" in prompt: - return "mock" - - # Patch the ask response - with patch("rich.prompt.Prompt.ask", ask_response): - # Loop through all commands and their subcommands - for command, command_data in bittensor.ALL_COMMANDS.items(): - config.command = command - if isinstance(command_data, dict): - for subcommand in command_data["commands"].keys(): - config.subcommand = subcommand - cli_instance.check_config(config) - else: - config.subcommand = None - cli_instance.check_config(config) - - def test_new_coldkey(self, _, __): - config = self.config() - config.wallet.name = "new_coldkey_testwallet" - - config.command = "wallet" - config.subcommand = "new_coldkey" - config.amount = 1 - config.dest = "no_prompt" - config.model = "core_server" - config.n_words = 12 - config.use_password = False - config.no_prompt = True - config.overwrite_coldkey = True - - cli = bittensor.cli(config) - cli.run() - - def test_new_hotkey(self, _, __): - config = self.config() - config.wallet.name = "new_hotkey_testwallet" - config.command = "wallet" - config.subcommand = "new_hotkey" - config.amount = 1 - config.dest = "no_prompt" - config.model = "core_server" - config.n_words = 12 - config.use_password = False - config.no_prompt = True - config.overwrite_hotkey = True - - cli = bittensor.cli(config) - cli.run() - - def test_regen_coldkey(self, _, __): - config = self.config() - config.wallet.name = "regen_coldkey_testwallet" - config.command = "wallet" - config.subcommand = "regen_coldkey" - config.amount = 1 - config.dest = "no_prompt" - config.model = "core_server" - config.mnemonic = "faculty decade seven jelly gospel axis next radio grain radio remain gentle" - config.seed = None - config.n_words = 12 - config.use_password = False - config.no_prompt = True - config.overwrite_coldkey = True - - cli = bittensor.cli(config) - cli.run() - - def test_regen_coldkeypub(self, _, __): - config = self.config() - config.wallet.name = "regen_coldkeypub_testwallet" - config.command = "wallet" - config.subcommand = "regen_coldkeypub" - config.ss58_address = "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - config.public_key = None - config.use_password = False - config.no_prompt = True - config.overwrite_coldkeypub = True - - cli = bittensor.cli(config) - cli.run() - - def test_regen_hotkey(self, _, __): - config = self.config() - config.wallet.name = "regen_hotkey_testwallet" - config.command = "wallet" - config.subcommand = "regen_hotkey" - config.amount = 1 - config.model = "core_server" - config.mnemonic = "faculty decade seven jelly gospel axis next radio grain radio remain gentle" - config.seed = None - config.n_words = 12 - config.use_password = False - config.no_prompt = True - config.overwrite_hotkey = True - - cli = bittensor.cli(config) - cli.run() - - def test_list(self, _, __): - # Mock IO for wallet - with patch( - "bittensor.wallet", - side_effect=[ - MagicMock( - coldkeypub_file=MagicMock( - exists_on_device=MagicMock(return_value=True), # Wallet exists - is_encrypted=MagicMock( - return_value=False # Wallet is not encrypted - ), - ), - coldkeypub=MagicMock( - ss58_address=bittensor.Keypair.create_from_mnemonic( - bittensor.Keypair.generate_mnemonic() - ).ss58_address - ), - ), - MagicMock( - hotkey_file=MagicMock( - exists_on_device=MagicMock(return_value=True), # Wallet exists - is_encrypted=MagicMock( - return_value=False # Wallet is not encrypted - ), - ), - hotkey=MagicMock( - ss58_address=bittensor.Keypair.create_from_mnemonic( - bittensor.Keypair.generate_mnemonic() - ).ss58_address - ), - ), - ], - ): - config = self.config() - config.wallet.path = "tmp/walletpath" - config.wallet.name = "mock_wallet" - config.no_prompt = True - config.command = "wallet" - config.subcommand = "list" - - cli = bittensor.cli(config) - with patch( - "os.walk", - side_effect=[ - iter([("/tmp/walletpath", ["mock_wallet"], [])]), # 1 wallet dir - iter( - [ - ("/tmp/walletpath/mock_wallet/hotkeys", [], ["hk0"]) - ] # 1 hotkey file - ), - ], - ): - cli.run() - - def test_list_no_wallet(self, _, __): - with patch( - "bittensor.wallet", - side_effect=[ - MagicMock( - coldkeypub_file=MagicMock( - exists_on_device=MagicMock(return_value=True) - ) - ) - ], - ): - config = self.config() - config.wallet.path = "/tmp/test_cli_test_list_no_wallet" - config.no_prompt = True - config.command = "wallet" - config.subcommand = "list" - - cli = bittensor.cli(config) - # This shouldn't raise an error anymore - cli.run() - - def test_btcli_help(self, _, __): - with pytest.raises(SystemExit) as pytest_wrapped_e: - with patch( - "argparse.ArgumentParser._print_message", return_value=None - ) as mock_print_message: - args = ["--help"] - bittensor.cli(args=args).run() - - mock_print_message.assert_called_once() - - call_args = mock_print_message.call_args - help_out = call_args[0][0] - - # Extract commands from the help text. - commands_section = re.search( - r"positional arguments:.*?{(.+?)}", help_out, re.DOTALL - ).group(1) - extracted_commands = [cmd.strip() for cmd in commands_section.split(",")] - - # Get expected commands - parser = bittensor.cli.__create_parser__() - expected_commands = [command for command in parser._actions[-1].choices] - - # Validate each expected command is in extracted commands - for command in expected_commands: - assert ( - command in extracted_commands - ), f"Command {command} not found in help output" - - # Check for duplicates - assert len(extracted_commands) == len( - set(extracted_commands) - ), "Duplicate commands found in help output" - - @patch("torch.cuda.is_available", return_value=True) - def test_register_cuda_use_cuda_flag(self, _, __, patched_sub): - base_args = [ - "subnets", - "pow_register", - "--wallet.path", - "tmp/walletpath", - "--wallet.name", - "mock", - "--wallet.hotkey", - "hk0", - "--no_prompt", - "--cuda.dev_id", - "0", - ] - - patched_sub.return_value = MagicMock( - get_subnets=MagicMock(return_value=[1]), - subnet_exists=MagicMock(return_value=True), - register=MagicMock(side_effect=MockException), - ) - - # Should be able to set true without argument - args = base_args + [ - "--pow_register.cuda.use_cuda", # should be True without any arugment - ] - with pytest.raises(MockException): - cli = bittensor.cli(args=args) - cli.run() - - self.assertEqual(cli.config.pow_register.cuda.get("use_cuda"), True) - - # Should be able to set to false with no argument - - args = base_args + [ - "--pow_register.cuda.no_cuda", - ] - with pytest.raises(MockException): - cli = bittensor.cli(args=args) - cli.run() - - self.assertEqual(cli.config.pow_register.cuda.get("use_cuda"), False) - - -def return_mock_sub_2(*args, **kwargs): - return MagicMock( - return_value=MagicMock( - get_subnet_burn_cost=MagicMock(return_value=0.1), - get_subnets=MagicMock(return_value=[1]), # Need to pass check config - get_delegates=MagicMock( - return_value=[ - bittensor.DelegateInfo( - hotkey_ss58="", - total_stake=Balance.from_rao(0), - nominators=[], - owner_ss58="", - take=0.18, - validator_permits=[], - registrations=[], - return_per_1000=Balance(0.0), - total_daily_return=Balance(0.0), - ) - ] - ), - block=10_000, - ), - add_args=bittensor.subtensor.add_args, - ) - - -@patch("bittensor.wallet", new_callable=return_mock_wallet_factory) -@patch("bittensor.subtensor", new_callable=return_mock_sub_2) -class TestEmptyArgs(unittest.TestCase): - """ - Test that the CLI doesn't crash when no args are passed - """ - - @patch("rich.prompt.PromptBase.ask", side_effect=MockException) - def test_command_no_args(self, _, __, patched_prompt_ask): - # Get argparser - parser = bittensor.cli.__create_parser__() - # Get all commands from argparser - commands = [ - command - for command in parser._actions[-1].choices # extract correct subparser keys - if len(command) > 1 # Skip singleton aliases - and command - not in [ - "subnet", - "sudos", - "stakes", - "roots", - "wallets", - "weight", - "st", - "wt", - "su", - ] # Skip duplicate aliases - ] - # Test that each command and its subcommands can be run with no args - for command in commands: - command_data = bittensor.ALL_COMMANDS.get(command) - - # If command is dictionary, it means it has subcommands - if isinstance(command_data, dict): - for subcommand in command_data["commands"].keys(): - try: - # Run each subcommand - bittensor.cli(args=[command, subcommand]).run() - except MockException: - pass # Expected exception - else: - try: - # If no subcommands, just run the command - bittensor.cli(args=[command]).run() - except MockException: - pass # Expected exception - - # Should not raise any other exceptions - - -mock_delegate_info = { - "hotkey_ss58": "", - "total_stake": bittensor.Balance.from_rao(0), - "nominators": [], - "owner_ss58": "", - "take": 0.18, - "validator_permits": [], - "registrations": [], - "return_per_1000": bittensor.Balance.from_rao(0), - "total_daily_return": bittensor.Balance.from_rao(0), -} - - -def return_mock_sub_3(*args, **kwargs): - return MagicMock( - return_value=MagicMock( - get_subnets=MagicMock(return_value=[1]), # Mock subnet 1 ONLY. - block=10_000, - get_delegates=MagicMock( - return_value=[bittensor.DelegateInfo(**mock_delegate_info)] - ), - ), - block=10_000, - ) - - -@patch("bittensor.subtensor", new_callable=return_mock_sub_3) -class TestCLIDefaultsNoNetwork(unittest.TestCase): - def test_inspect_prompt_wallet_name(self, _): - # Patch command to exit early - with patch("bittensor.commands.inspect.InspectCommand.run", return_value=None): - # Test prompt happens when no wallet name is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "inspect", - # '--wallet.name', 'mock', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called_once() - - # Test NO prompt happens when wallet name is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "inspect", - "--wallet.name", - "coolwalletname", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - # Test NO prompt happens when wallet name 'default' is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "inspect", - "--wallet.name", - "default", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_overview_prompt_wallet_name(self, _): - # Patch command to exit early - with patch( - "bittensor.commands.overview.OverviewCommand.run", return_value=None - ): - # Test prompt happens when no wallet name is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "overview", - # '--wallet.name', 'mock', - "--netuid", - "1", - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called_once() - - # Test NO prompt happens when wallet name is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "overview", - "--wallet.name", - "coolwalletname", - "--netuid", - "1", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - # Test NO prompt happens when wallet name 'default' is passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=[ - "wallet", - "overview", - "--wallet.name", - "default", - "--netuid", - "1", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_stake_prompt_wallet_name_and_hotkey_name(self, _): - base_args = [ - "stake", - "add", - "--all", - ] - # Patch command to exit early - with patch("bittensor.commands.stake.StakeCommand.run", return_value=None): - # Test prompt happens when - # - wallet name IS NOT passed, AND - # - hotkey name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - # '--wallet.name', 'mock', - #'--wallet.hotkey', 'mock_hotkey', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 2, - msg="Prompt should have been called twice", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in [val for val in kwargs0.values()] - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - args1, kwargs1 = mock_ask_prompt.call_args_list[1] - combined_args_kwargs1 = [arg for arg in args1] + [ - val for val in kwargs1.values() - ] - # check that prompt was called for hotkey - - self.assertTrue( - any(filter(lambda x: "hotkey" in x.lower(), combined_args_kwargs1)), - msg=f"Prompt should have been called for hotkey: {combined_args_kwargs1}", - ) - - # Test prompt happens when - # - wallet name IS NOT passed, AND - # - hotkey name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - #'--wallet.name', 'mock', - "--wallet.hotkey", - "mock_hotkey", - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - # Test prompt happens when - # - wallet name IS passed, AND - # - hotkey name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "mock", - #'--wallet.hotkey', 'mock_hotkey', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for hotkey - self.assertTrue( - any(filter(lambda x: "hotkey" in x.lower(), combined_args_kwargs0)), - msg=f"Prompt should have been called for hotkey {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - wallet name IS passed, AND - # - hotkey name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "coolwalletname", - "--wallet.hotkey", - "coolwalletname_hotkey", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - # Test NO prompt happens when - # - wallet name 'default' IS passed, AND - # - hotkey name 'default' IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_unstake_prompt_wallet_name_and_hotkey_name(self, _): - base_args = [ - "stake", - "remove", - "--all", - ] - # Patch command to exit early - with patch("bittensor.commands.unstake.UnStakeCommand.run", return_value=None): - # Test prompt happens when - # - wallet name IS NOT passed, AND - # - hotkey name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - # '--wallet.name', 'mock', - #'--wallet.hotkey', 'mock_hotkey', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 2, - msg="Prompt should have been called twice", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - args1, kwargs1 = mock_ask_prompt.call_args_list[1] - combined_args_kwargs1 = [arg for arg in args1] + [ - val for val in kwargs1.values() - ] - # check that prompt was called for hotkey - self.assertTrue( - any(filter(lambda x: "hotkey" in x.lower(), combined_args_kwargs1)), - msg=f"Prompt should have been called for hotkey {combined_args_kwargs1}", - ) - - # Test prompt happens when - # - wallet name IS NOT passed, AND - # - hotkey name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - #'--wallet.name', 'mock', - "--wallet.hotkey", - "mock_hotkey", - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - # Test prompt happens when - # - wallet name IS passed, AND - # - hotkey name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock", "mock_hotkey"] - - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "mock", - #'--wallet.hotkey', 'mock_hotkey', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for hotkey - self.assertTrue( - any(filter(lambda x: "hotkey" in x.lower(), combined_args_kwargs0)), - msg=f"Prompt should have been called for hotkey {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - wallet name IS passed, AND - # - hotkey name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "coolwalletname", - "--wallet.hotkey", - "coolwalletname_hotkey", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - # Test NO prompt happens when - # - wallet name 'default' IS passed, AND - # - hotkey name 'default' IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "default", - "--wallet.hotkey", - "default", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_delegate_prompt_wallet_name(self, _): - base_args = [ - "root", - "delegate", - "--all", - "--delegate_ss58key", - _get_mock_coldkey(0), - ] - # Patch command to exit early - with patch( - "bittensor.commands.delegates.DelegateStakeCommand.run", return_value=None - ): - # Test prompt happens when - # - wallet name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock"] - - cli = bittensor.cli( - args=base_args - + [ - # '--wallet.name', 'mock', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - wallet name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "coolwalletname", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_undelegate_prompt_wallet_name(self, _): - base_args = [ - "root", - "undelegate", - "--all", - "--delegate_ss58key", - _get_mock_coldkey(0), - ] - # Patch command to exit early - with patch( - "bittensor.commands.delegates.DelegateUnstakeCommand.run", return_value=None - ): - # Test prompt happens when - # - wallet name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock"] - - cli = bittensor.cli( - args=base_args - + [ - # '--wallet.name', 'mock', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - wallet name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "coolwalletname", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_history_prompt_wallet_name(self, _): - base_args = [ - "wallet", - "history", - ] - # Patch command to exit early - with patch( - "bittensor.commands.wallets.GetWalletHistoryCommand.run", return_value=None - ): - # Test prompt happens when - # - wallet name IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = ["mock"] - - cli = bittensor.cli( - args=base_args - + [ - # '--wallet.name', 'mock', - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for wallet name - self.assertTrue( - any( - filter( - lambda x: "wallet name" in x.lower(), combined_args_kwargs0 - ) - ), - msg=f"Prompt should have been called for wallet name: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - wallet name IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--wallet.name", - "coolwalletname", - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_delegate_prompt_hotkey(self, _): - # Tests when - # - wallet name IS passed, AND - # - delegate hotkey IS NOT passed - base_args = [ - "root", - "delegate", - "--all", - "--wallet.name", - "mock", - ] - - delegate_ss58 = _get_mock_coldkey(0) - with patch("bittensor.commands.delegates.show_delegates"): - with patch( - "bittensor.subtensor.Subtensor.get_delegates", - return_value=[ - bittensor.DelegateInfo( - hotkey_ss58=delegate_ss58, # return delegate with mock coldkey - total_stake=bittensor.Balance.from_float(0.1), - nominators=[], - owner_ss58="", - take=0.18, - validator_permits=[], - registrations=[], - return_per_1000=bittensor.Balance.from_float(0.1), - total_daily_return=bittensor.Balance.from_float(0.1), - ) - ], - ): - # Patch command to exit early - with patch( - "bittensor.commands.delegates.DelegateStakeCommand.run", - return_value=None, - ): - # Test prompt happens when - # - delegate hotkey IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = [ - "0" - ] # select delegate with mock coldkey - - cli = bittensor.cli( - args=base_args - + [ - # '--delegate_ss58key', delegate_ss58, - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for delegate hotkey - self.assertTrue( - any( - filter( - lambda x: "delegate" in x.lower(), - combined_args_kwargs0, - ) - ), - msg=f"Prompt should have been called for delegate: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - delegate hotkey IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--delegate_ss58key", - delegate_ss58, - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_undelegate_prompt_hotkey(self, _): - # Tests when - # - wallet name IS passed, AND - # - delegate hotkey IS NOT passed - base_args = [ - "root", - "undelegate", - "--all", - "--wallet.name", - "mock", - ] - - delegate_ss58 = _get_mock_coldkey(0) - with patch("bittensor.commands.delegates.show_delegates"): - with patch( - "bittensor.subtensor.Subtensor.get_delegates", - return_value=[ - bittensor.DelegateInfo( - hotkey_ss58=delegate_ss58, # return delegate with mock coldkey - total_stake=bittensor.Balance.from_float(0.1), - nominators=[], - owner_ss58="", - take=0.18, - validator_permits=[], - registrations=[], - return_per_1000=bittensor.Balance.from_float(0.1), - total_daily_return=bittensor.Balance.from_float(0.1), - ) - ], - ): - # Patch command to exit early - with patch( - "bittensor.commands.delegates.DelegateUnstakeCommand.run", - return_value=None, - ): - # Test prompt happens when - # - delegate hotkey IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = [ - "0" - ] # select delegate with mock coldkey - - cli = bittensor.cli( - args=base_args - + [ - # '--delegate_ss58key', delegate_ss58, - ] - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called ONCE", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for delegate hotkey - self.assertTrue( - any( - filter( - lambda x: "delegate" in x.lower(), - combined_args_kwargs0, - ) - ), - msg=f"Prompt should have been called for delegate: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - delegate hotkey IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--delegate_ss58key", - delegate_ss58, - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - def test_vote_command_prompt_proposal_hash(self, _): - """Test that the vote command prompts for proposal_hash when it is not passed""" - base_args = [ - "root", - "senate_vote", - "--wallet.name", - "mock", - "--wallet.hotkey", - "mock_hotkey", - ] - - mock_proposal_hash = "mock_proposal_hash" - - with patch("bittensor.subtensor.Subtensor.is_senate_member", return_value=True): - with patch( - "bittensor.subtensor.Subtensor.get_vote_data", - return_value={"index": 1}, - ): - # Patch command to exit early - with patch( - "bittensor.commands.senate.VoteCommand.run", - return_value=None, - ): - # Test prompt happens when - # - proposal_hash IS NOT passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - mock_ask_prompt.side_effect = [ - mock_proposal_hash # Proposal hash - ] - - cli = bittensor.cli( - args=base_args - # proposal_hash not added - ) - cli.run() - - # Prompt happened - mock_ask_prompt.assert_called() - self.assertEqual( - mock_ask_prompt.call_count, - 1, - msg="Prompt should have been called once", - ) - args0, kwargs0 = mock_ask_prompt.call_args_list[0] - combined_args_kwargs0 = [arg for arg in args0] + [ - val for val in kwargs0.values() - ] - # check that prompt was called for proposal_hash - self.assertTrue( - any( - filter( - lambda x: "proposal" in x.lower(), - combined_args_kwargs0, - ) - ), - msg=f"Prompt should have been called for proposal: {combined_args_kwargs0}", - ) - - # Test NO prompt happens when - # - proposal_hash IS passed - with patch("rich.prompt.Prompt.ask") as mock_ask_prompt: - cli = bittensor.cli( - args=base_args - + [ - "--proposal_hash", - mock_proposal_hash, - ] - ) - cli.run() - - # NO prompt happened - mock_ask_prompt.assert_not_called() - - @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) - def test_commit_reveal_weights_enabled_parse_boolean_argument(self, mock_sub, __): - param = "commit_reveal_weights_enabled" - - def _test_value_parsing(parsed_value: bool, modified: str): - cli = bittensor.cli( - args=[ - "sudo", - "set", - "--netuid", - "1", - "--param", - param, - "--value", - modified, - "--wallet.name", - "mock", - ] - ) - cli.run() - - _, kwargs = mock_sub.call_args - passed_config = kwargs["config"] - self.assertEqual(passed_config.param, param, msg="Incorrect param") - self.assertEqual( - passed_config.value, - parsed_value, - msg=f"Boolean argument not correctly for {modified}", - ) - - for boolean_value in [True, False, 1, 0]: - as_str = str(boolean_value) - - _test_value_parsing(boolean_value, as_str) - _test_value_parsing(boolean_value, as_str.capitalize()) - _test_value_parsing(boolean_value, as_str.upper()) - _test_value_parsing(boolean_value, as_str.lower()) - - @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) - def test_hyperparameter_allowed_values( - self, - mock_sub, - __, - ): - params = ["alpha_values"] - - def _test_value_parsing(param: str, value: str): - cli = bittensor.cli( - args=[ - "sudo", - "set", - "hyperparameters", - "--netuid", - "1", - "--param", - param, - "--value", - value, - "--wallet.name", - "mock", - ] - ) - should_raise_error = False - error_message = "" - - try: - alpha_low_str, alpha_high_str = value.strip("[]").split(",") - alpha_high = float(alpha_high_str) - alpha_low = float(alpha_low_str) - if alpha_high <= 52428 or alpha_high >= 65535: - should_raise_error = True - error_message = "between 52428 and 65535" - elif alpha_low < 0 or alpha_low > 52428: - should_raise_error = True - error_message = "between 0 and 52428" - except ValueError: - should_raise_error = True - error_message = "a number or a boolean" - except TypeError: - should_raise_error = True - error_message = "a number or a boolean" - - if isinstance(value, bool): - should_raise_error = True - error_message = "a number or a boolean" - - if should_raise_error: - with pytest.raises(ValueError) as exc_info: - cli.run() - assert ( - f"Hyperparameter {param} value is not within bounds. Value is {value} but must be {error_message}" - in str(exc_info.value) - ) - else: - cli.run() - _, kwargs = mock_sub.call_args - passed_config = kwargs["config"] - self.assertEqual(passed_config.param, param, msg="Incorrect param") - self.assertEqual( - passed_config.value, - value, - msg=f"Value argument not set correctly for {param}", - ) - - for param in params: - for value in [ - [0.8, 11], - [52429, 52428], - [52427, 53083], - [6553, 53083], - [-123, None], - [1, 0], - [True, "Some string"], - ]: - as_str = str(value).strip("[]") - _test_value_parsing(param, as_str) - - @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) - def test_network_registration_allowed_parse_boolean_argument(self, mock_sub, __): - param = "network_registration_allowed" - - def _test_value_parsing(parsed_value: bool, modified: str): - cli = bittensor.cli( - args=[ - "sudo", - "set", - "--netuid", - "1", - "--param", - param, - "--value", - modified, - "--wallet.name", - "mock", - ] - ) - cli.run() - - _, kwargs = mock_sub.call_args - passed_config = kwargs["config"] - self.assertEqual(passed_config.param, param, msg="Incorrect param") - self.assertEqual( - passed_config.value, - parsed_value, - msg=f"Boolean argument not correctly for {modified}", - ) - - for boolean_value in [True, False, 1, 0]: - as_str = str(boolean_value) - - _test_value_parsing(boolean_value, as_str) - _test_value_parsing(boolean_value, as_str.capitalize()) - _test_value_parsing(boolean_value, as_str.upper()) - _test_value_parsing(boolean_value, as_str.lower()) - - @patch("bittensor.wallet", new_callable=return_mock_wallet_factory) - def test_network_pow_registration_allowed_parse_boolean_argument( - self, mock_sub, __ - ): - param = "network_pow_registration_allowed" - - def _test_value_parsing(parsed_value: bool, modified: str): - cli = bittensor.cli( - args=[ - "sudo", - "set", - "--netuid", - "1", - "--param", - param, - "--value", - modified, - "--wallet.name", - "mock", - ] - ) - cli.run() - - _, kwargs = mock_sub.call_args - passed_config = kwargs["config"] - self.assertEqual(passed_config.param, param, msg="Incorrect param") - self.assertEqual( - passed_config.value, - parsed_value, - msg=f"Boolean argument not correctly for {modified}", - ) - - for boolean_value in [True, False, 1, 0]: - as_str = str(boolean_value) - - _test_value_parsing(boolean_value, as_str) - _test_value_parsing(boolean_value, as_str.capitalize()) - _test_value_parsing(boolean_value, as_str.upper()) - _test_value_parsing(boolean_value, as_str.lower()) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/integration_tests/test_metagraph_integration.py b/tests/integration_tests/test_metagraph_integration.py index 5dbb9ddfc..34bf4f590 100644 --- a/tests/integration_tests/test_metagraph_integration.py +++ b/tests/integration_tests/test_metagraph_integration.py @@ -1,15 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Foundation - +# 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 @@ -19,25 +18,22 @@ import bittensor import torch import os -from bittensor.mock import MockSubtensor -from bittensor.metagraph import METAGRAPH_STATE_DICT_NDARRAY_KEYS, get_save_dir +from bittensor.utils.mock import MockSubtensor +from bittensor.core.metagraph import METAGRAPH_STATE_DICT_NDARRAY_KEYS, get_save_dir _subtensor_mock: MockSubtensor = MockSubtensor() def setUpModule(): _subtensor_mock.reset() - _subtensor_mock.create_subnet(netuid=3) - - # Set diff 0 - _subtensor_mock.set_difficulty(netuid=3, difficulty=0) + _subtensor_mock.set_difficulty(netuid=3, difficulty=0) # Set diff 0 class TestMetagraph: def setup_method(self): self.sub = MockSubtensor() - self.metagraph = bittensor.metagraph(netuid=3, network="mock", sync=False) + self.metagraph = bittensor.Metagraph(netuid=3, network="mock", sync=False) def test_print_empty(self): print(self.metagraph) diff --git a/tests/integration_tests/test_subtensor_integration.py b/tests/integration_tests/test_subtensor_integration.py index 407dee848..8539839cc 100644 --- a/tests/integration_tests/test_subtensor_integration.py +++ b/tests/integration_tests/test_subtensor_integration.py @@ -1,40 +1,36 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2023 Opentensor Technologies Inc - +# 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 random import unittest -from queue import Empty as QueueEmpty from unittest.mock import MagicMock, patch -import numpy as np -import pytest from substrateinterface import Keypair import bittensor -from bittensor.mock import MockSubtensor -from bittensor.utils import weight_utils +from bittensor.core import settings from bittensor.utils.balance import Balance +from bittensor.utils.mock import MockSubtensor from tests.helpers import ( - _get_mock_coldkey, + get_mock_coldkey, MockConsole, - _get_mock_keypair, - _get_mock_wallet, + get_mock_keypair, + get_mock_wallet, ) +from bittensor.core.extrinsics import transfer class TestSubtensor(unittest.TestCase): @@ -43,9 +39,9 @@ class TestSubtensor(unittest.TestCase): subtensor: MockSubtensor def setUp(self): - self.wallet = _get_mock_wallet( - hotkey=_get_mock_keypair(0, self.id()), - coldkey=_get_mock_keypair(1, self.id()), + self.wallet = get_mock_wallet( + hotkey=get_mock_keypair(0, self.id()), + coldkey=get_mock_keypair(1, self.id()), ) self.balance = Balance.from_tao(1000) self.mock_neuron = MagicMock() # NOTE: this might need more sophistication @@ -55,12 +51,12 @@ def setUp(self): def setUpClass(cls) -> None: # mock rich console status mock_console = MockConsole() - cls._mock_console_patcher = patch("bittensor.__console__", mock_console) + cls._mock_console_patcher = patch( + "bittensor.core.settings.bt_console", mock_console + ) cls._mock_console_patcher.start() - # Keeps the same mock network for all tests. This stops the network from being re-setup for each test. cls._mock_subtensor = MockSubtensor() - cls._do_setup_subnet() @classmethod @@ -77,183 +73,58 @@ def tearDownClass(cls) -> None: def test_network_overrides(self): """Tests that the network overrides the chain_endpoint.""" # Argument importance: chain_endpoint (arg) > network (arg) > config.subtensor.chain_endpoint > config.subtensor.network - config0 = bittensor.subtensor.config() + config0 = bittensor.Subtensor.config() config0.subtensor.network = "finney" - config0.subtensor.chain_endpoint = "wss://finney.subtensor.io" # Should not match bittensor.__finney_entrypoint__ - assert config0.subtensor.chain_endpoint != bittensor.__finney_entrypoint__ + config0.subtensor.chain_endpoint = "wss://finney.subtensor.io" # Should not match bittensor.core.settings.FINNEY_ENTRYPOINT + assert config0.subtensor.chain_endpoint != settings.FINNEY_ENTRYPOINT - config1 = bittensor.subtensor.config() + config1 = bittensor.Subtensor.config() config1.subtensor.network = "local" config1.subtensor.chain_endpoint = None # Mock network calls with patch("substrateinterface.SubstrateInterface.connect_websocket"): with patch("substrateinterface.SubstrateInterface.reload_type_registry"): - print(bittensor.subtensor, type(bittensor.subtensor)) + print(bittensor.Subtensor, type(bittensor.Subtensor)) # Choose network arg over config - sub1 = bittensor.subtensor(config=config1, network="local") + sub1 = bittensor.Subtensor(config=config1, network="local") self.assertEqual( sub1.chain_endpoint, - bittensor.__local_entrypoint__, + settings.LOCAL_ENTRYPOINT, msg="Explicit network arg should override config.network", ) # Choose network config over chain_endpoint config - sub2 = bittensor.subtensor(config=config0) + sub2 = bittensor.Subtensor(config=config0) self.assertNotEqual( sub2.chain_endpoint, - bittensor.__finney_entrypoint__, # Here we expect the endpoint corresponding to the network "finney" + settings.FINNEY_ENTRYPOINT, # Here we expect the endpoint corresponding to the network "finney" msg="config.network should override config.chain_endpoint", ) - sub3 = bittensor.subtensor(config=config1) + sub3 = bittensor.Subtensor(config=config1) # Should pick local instead of finney (default) assert sub3.network == "local" - assert sub3.chain_endpoint == bittensor.__local_entrypoint__ + assert sub3.chain_endpoint == settings.LOCAL_ENTRYPOINT def test_get_current_block(self): block = self.subtensor.get_current_block() - assert type(block) == int + assert type(block) is int def test_do_block_step(self): self.subtensor.do_block_step() block = self.subtensor.get_current_block() - assert type(block) == int + assert type(block) is int def test_do_block_step_query_previous_block(self): self.subtensor.do_block_step() block = self.subtensor.get_current_block() self.subtensor.query_subtensor("NetworksAdded", block) - def test_unstake(self): - self.subtensor._do_unstake = MagicMock(return_value=True) - - self.subtensor.substrate.get_payment_info = MagicMock( - return_value={"partialFee": 100} - ) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - success = self.subtensor.unstake(self.wallet, amount=200) - self.assertTrue(success, msg="Unstake should succeed") - - def test_unstake_inclusion(self): - self.subtensor._do_unstake = MagicMock(return_value=True) - - self.subtensor.substrate.get_payment_info = MagicMock( - return_value={"partialFee": 100} - ) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - success = self.subtensor.unstake( - self.wallet, amount=200, wait_for_inclusion=True - ) - self.assertTrue(success, msg="Unstake should succeed") - - def test_unstake_failed(self): - self.subtensor._do_unstake = MagicMock(return_value=False) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - fail = self.subtensor.unstake(self.wallet, amount=200, wait_for_inclusion=True) - self.assertFalse(fail, msg="Unstake should fail") - - def test_stake(self): - self.subtensor._do_stake = MagicMock(return_value=True) - - self.subtensor.substrate.get_payment_info = MagicMock( - return_value={"partialFee": 100} - ) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - self.subtensor.get_hotkey_owner = MagicMock( - return_value=self.wallet.coldkeypub.ss58_address - ) - success = self.subtensor.add_stake(self.wallet, amount=200) - self.assertTrue(success, msg="Stake should succeed") - - def test_stake_inclusion(self): - self.subtensor._do_stake = MagicMock(return_value=True) - - self.subtensor.substrate.get_payment_info = MagicMock( - return_value={"partialFee": 100} - ) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - self.subtensor.get_hotkey_owner = MagicMock( - return_value=self.wallet.coldkeypub.ss58_address - ) - success = self.subtensor.add_stake( - self.wallet, amount=200, wait_for_inclusion=True - ) - self.assertTrue(success, msg="Stake should succeed") - - def test_stake_failed(self): - self.subtensor._do_stake = MagicMock(return_value=False) - - self.subtensor.substrate.get_payment_info = MagicMock( - return_value={"partialFee": 100} - ) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_balance = MagicMock(return_value=Balance.from_rao(0)) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_stake_for_coldkey_and_hotkey = MagicMock( - return_value=Balance.from_tao(500) - ) - self.subtensor.get_hotkey_owner = MagicMock( - return_value=self.wallet.coldkeypub.ss58_address - ) - fail = self.subtensor.add_stake( - self.wallet, amount=200, wait_for_inclusion=True - ) - self.assertFalse(fail, msg="Stake should fail") - def test_transfer(self): - fake_coldkey = _get_mock_coldkey(1) + fake_coldkey = get_mock_coldkey(1) - self.subtensor._do_transfer = MagicMock(return_value=(True, "0x", None)) - self.subtensor.register = MagicMock(return_value=True) + transfer.do_transfer = MagicMock(return_value=(True, "0x", None)) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( return_value=self.mock_neuron ) @@ -266,9 +137,8 @@ def test_transfer(self): self.assertTrue(success, msg="Transfer should succeed") def test_transfer_inclusion(self): - fake_coldkey = _get_mock_coldkey(1) - self.subtensor._do_transfer = MagicMock(return_value=(True, "0x", None)) - self.subtensor.register = MagicMock(return_value=True) + fake_coldkey = get_mock_coldkey(1) + transfer.do_transfer = MagicMock(return_value=(True, "0x", None)) self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( return_value=self.mock_neuron ) @@ -280,8 +150,8 @@ def test_transfer_inclusion(self): self.assertTrue(success, msg="Transfer should succeed") def test_transfer_failed(self): - fake_coldkey = _get_mock_coldkey(1) - self.subtensor._do_transfer = MagicMock( + fake_coldkey = get_mock_coldkey(1) + transfer.do_transfer = MagicMock( return_value=(False, None, "Mock failure message") ) @@ -291,7 +161,7 @@ def test_transfer_failed(self): self.assertFalse(fail, msg="Transfer should fail") def test_transfer_invalid_dest(self): - fake_coldkey = _get_mock_coldkey(1) + fake_coldkey = get_mock_coldkey(1) fail = self.subtensor.transfer( self.wallet, @@ -302,36 +172,30 @@ def test_transfer_invalid_dest(self): self.assertFalse(fail, msg="Transfer should fail because of invalid dest") def test_transfer_dest_as_bytes(self): - fake_coldkey = _get_mock_coldkey(1) - self.subtensor._do_transfer = MagicMock(return_value=(True, "0x", None)) - - self.subtensor.register = MagicMock(return_value=True) - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=self.mock_neuron - ) - self.subtensor.get_balance = MagicMock(return_value=self.balance) - - dest_as_bytes: bytes = Keypair(fake_coldkey).public_key - success = self.subtensor.transfer( - self.wallet, - dest_as_bytes, # invalid dest - amount=200, - wait_for_inclusion=True, - ) - self.assertTrue(success, msg="Transfer should succeed") + fake_coldkey = get_mock_coldkey(1) + with patch( + "bittensor.core.extrinsics.transfer.do_transfer", + return_value=(True, "0x", None), + ): + self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( + return_value=self.mock_neuron + ) + self.subtensor.get_balance = MagicMock(return_value=self.balance) + + dest_as_bytes: bytes = Keypair(fake_coldkey).public_key + success = self.subtensor.transfer( + self.wallet, + dest_as_bytes, # invalid dest + amount=200, + wait_for_inclusion=True, + ) + self.assertTrue(success, msg="Transfer should succeed") def test_set_weights(self): chain_weights = [0] - class success: - def __init__(self): - self.is_success = True - - def process_events(self): - return True - self.subtensor.set_weights = MagicMock(return_value=True) - self.subtensor._do_set_weights = MagicMock(return_value=(True, None)) + self.subtensor.do_set_weights = MagicMock(return_value=(True, None)) success = self.subtensor.set_weights( wallet=self.wallet, @@ -339,11 +203,11 @@ def process_events(self): uids=[1], weights=chain_weights, ) - assert success == True + assert success is True def test_set_weights_inclusion(self): chain_weights = [0] - self.subtensor._do_set_weights = MagicMock(return_value=(True, None)) + self.subtensor.do_set_weights = MagicMock(return_value=(True, None)) self.subtensor.set_weights = MagicMock(return_value=True) success = self.subtensor.set_weights( @@ -353,11 +217,11 @@ def test_set_weights_inclusion(self): weights=chain_weights, wait_for_inclusion=True, ) - assert success == True + assert success is True def test_set_weights_failed(self): chain_weights = [0] - self.subtensor._do_set_weights = MagicMock( + self.subtensor.do_set_weights = MagicMock( return_value=(False, "Mock failure message") ) self.subtensor.set_weights = MagicMock(return_value=False) @@ -369,481 +233,17 @@ def test_set_weights_failed(self): weights=chain_weights, wait_for_inclusion=True, ) - assert fail == False - - def test_commit_weights(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - commit_hash = bittensor.utils.weight_utils.generate_weight_hash( - address=self.wallet.hotkey.ss58_address, - netuid=3, - uids=weight_uids, - values=weight_vals, - salt=salt.tolist(), - version_key=0, - ) - - self.subtensor.commit_weights = MagicMock( - return_value=(True, "Successfully committed weights.") - ) - self.subtensor._do_commit_weights = MagicMock(return_value=(True, None)) - - success, message = self.subtensor.commit_weights( - wallet=self.wallet, netuid=3, uids=uids, weights=weights, salt=salt - ) - assert success is True - assert message == "Successfully committed weights." - - def test_commit_weights_inclusion(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - commit_hash = bittensor.utils.weight_utils.generate_weight_hash( - address=self.wallet.hotkey.ss58_address, - netuid=1, - uids=weight_uids, - values=weight_vals, - salt=salt.tolist(), - version_key=0, - ) - - self.subtensor._do_commit_weights = MagicMock(return_value=(True, None)) - self.subtensor.commit_weights = MagicMock( - return_value=(True, "Successfully committed weights.") - ) - - success, message = self.subtensor.commit_weights( - wallet=self.wallet, - netuid=1, - uids=uids, - weights=weights, - salt=salt, - wait_for_inclusion=True, - ) - assert success is True - assert message == "Successfully committed weights." - - def test_commit_weights_failed(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - - weight_uids, weight_vals = weight_utils.convert_weights_and_uids_for_emit( - uids=uids, weights=weights - ) - - commit_hash = bittensor.utils.weight_utils.generate_weight_hash( - address=self.wallet.hotkey.ss58_address, - netuid=3, - uids=weight_uids, - values=weight_vals, - salt=salt.tolist(), - version_key=0, - ) - - self.subtensor._do_commit_weights = MagicMock( - return_value=(False, "Mock failure message") - ) - self.subtensor.commit_weights = MagicMock( - return_value=(False, "Mock failure message") - ) - - success, message = self.subtensor.commit_weights( - wallet=self.wallet, - netuid=3, - uids=uids, - weights=weights, - salt=salt, - wait_for_inclusion=True, - ) - assert success is False - assert message == "Mock failure message" - - def test_reveal_weights(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - - self.subtensor.reveal_weights = MagicMock( - return_value=(True, "Successfully revealed weights.") - ) - self.subtensor._do_reveal_weights = MagicMock(return_value=(True, None)) - - success, message = self.subtensor.reveal_weights( - wallet=self.wallet, - netuid=3, - uids=uids, - weights=weights, - salt=salt, - version_key=0, - ) - assert success is True - assert message == "Successfully revealed weights." - - def test_reveal_weights_inclusion(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - - self.subtensor._do_reveal_weights = MagicMock(return_value=(True, None)) - self.subtensor.reveal_weights = MagicMock( - return_value=(True, "Successfully revealed weights.") - ) - - success, message = self.subtensor.reveal_weights( - wallet=self.wallet, - netuid=1, - uids=uids, - weights=weights, - salt=salt, - version_key=0, - wait_for_inclusion=True, - ) - assert success is True - assert message == "Successfully revealed weights." - - def test_reveal_weights_failed(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - - self.subtensor._do_reveal_weights = MagicMock( - return_value=(False, "Mock failure message") - ) - self.subtensor.reveal_weights = MagicMock( - return_value=(False, "Mock failure message") - ) - - success, message = self.subtensor.reveal_weights( - wallet=self.wallet, - netuid=3, - uids=uids, - weights=weights, - salt=salt, - version_key=0, - wait_for_inclusion=True, - ) - assert success is False - assert message == "Mock failure message" - - def test_commit_and_reveal_weights(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - version_key = 0 - - # Mock the commit_weights and reveal_weights functions - self.subtensor.commit_weights = MagicMock( - return_value=(True, "Successfully committed weights.") - ) - self.subtensor._do_commit_weights = MagicMock(return_value=(True, None)) - self.subtensor.reveal_weights = MagicMock( - return_value=(True, "Successfully revealed weights.") - ) - self.subtensor._do_reveal_weights = MagicMock(return_value=(True, None)) - - # Commit weights - commit_success, commit_message = self.subtensor.commit_weights( - wallet=self.wallet, - netuid=3, - uids=uids, - weights=weights, - salt=salt, - ) - assert commit_success is True - assert commit_message == "Successfully committed weights." - - # Reveal weights - reveal_success, reveal_message = self.subtensor.reveal_weights( - wallet=self.wallet, - netuid=3, - uids=uids, - weights=weights, - salt=salt, - version_key=version_key, - ) - assert reveal_success is True - assert reveal_message == "Successfully revealed weights." - - def test_commit_and_reveal_weights_inclusion(self): - weights = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32) - uids = np.array([1, 2, 3, 4], dtype=np.int64) - salt = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=np.int64) - version_key = 0 - - # Mock the commit_weights and reveal_weights functions - self.subtensor.commit_weights = MagicMock( - return_value=(True, "Successfully committed weights.") - ) - self.subtensor._do_commit_weights = MagicMock(return_value=(True, None)) - self.subtensor.reveal_weights = MagicMock( - return_value=(True, "Successfully revealed weights.") - ) - self.subtensor._do_reveal_weights = MagicMock(return_value=(True, None)) - - # Commit weights with wait_for_inclusion - commit_success, commit_message = self.subtensor.commit_weights( - wallet=self.wallet, - netuid=1, - uids=uids, - weights=weights, - salt=salt, - wait_for_inclusion=True, - ) - assert commit_success is True - assert commit_message == "Successfully committed weights." - - # Reveal weights with wait_for_inclusion - reveal_success, reveal_message = self.subtensor.reveal_weights( - wallet=self.wallet, - netuid=1, - uids=uids, - weights=weights, - salt=salt, - version_key=version_key, - wait_for_inclusion=True, - ) - assert reveal_success is True - assert reveal_message == "Successfully revealed weights." + assert fail is False def test_get_balance(self): - fake_coldkey = _get_mock_coldkey(0) + fake_coldkey = get_mock_coldkey(0) balance = self.subtensor.get_balance(address=fake_coldkey) - assert type(balance) == bittensor.utils.balance.Balance - - def test_get_balances(self): - balances = self.subtensor.get_balances() - assert type(balances) == dict - for i in balances: - assert type(balances[i]) == bittensor.utils.balance.Balance - - def test_get_uid_by_hotkey_on_subnet(self): - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - mock_hotkey_kp = _get_mock_keypair(100, self.id()) - - # Register on subnet 3 - mock_uid = self.subtensor.force_register_neuron( - netuid=3, - hotkey=mock_hotkey_kp.ss58_address, - coldkey=mock_coldkey_kp.ss58_address, - ) - - uid = self.subtensor.get_uid_for_hotkey_on_subnet( - mock_hotkey_kp.ss58_address, netuid=3 - ) - self.assertIsInstance( - uid, int, msg="get_uid_for_hotkey_on_subnet should return an int" - ) - self.assertEqual( - uid, - mock_uid, - msg="get_uid_for_hotkey_on_subnet should return the correct uid", - ) - - def test_is_hotkey_registered(self): - mock_coldkey_kp = _get_mock_keypair(0, self.id()) - mock_hotkey_kp = _get_mock_keypair(100, self.id()) - - # Register on subnet 3 - _ = self.subtensor.force_register_neuron( - netuid=3, - hotkey=mock_hotkey_kp.ss58_address, - coldkey=mock_coldkey_kp.ss58_address, - ) - - registered = self.subtensor.is_hotkey_registered( - mock_hotkey_kp.ss58_address, netuid=3 - ) - self.assertTrue(registered, msg="Hotkey should be registered") - - def test_is_hotkey_registered_not_registered(self): - mock_hotkey_kp = _get_mock_keypair(100, self.id()) - - # Do not register on subnet 3 - - registered = self.subtensor.is_hotkey_registered( - mock_hotkey_kp.ss58_address, netuid=3 - ) - self.assertFalse(registered, msg="Hotkey should not be registered") - - def test_registration_multiprocessed_already_registered(self): - workblocks_before_is_registered = random.randint(5, 10) - # return False each work block but return True after a random number of blocks - is_registered_return_values = ( - [False for _ in range(workblocks_before_is_registered)] - + [True] - + [True, False] - ) - # this should pass the initial False check in the subtensor class and then return True because the neuron is already registered - - mock_neuron = MagicMock() - mock_neuron.is_null = True - - # patch solution queue to return None - with patch( - "multiprocessing.queues.Queue.get", return_value=None - ) as mock_queue_get: - # patch time queue get to raise Empty exception - with patch( - "multiprocessing.queues.Queue.get_nowait", side_effect=QueueEmpty - ) as mock_queue_get_nowait: - wallet = _get_mock_wallet( - hotkey=_get_mock_keypair(0, self.id()), - coldkey=_get_mock_keypair(1, self.id()), - ) - self.subtensor.is_hotkey_registered = MagicMock( - side_effect=is_registered_return_values - ) - - self.subtensor.difficulty = MagicMock(return_value=1) - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - side_effect=mock_neuron - ) - self.subtensor._do_pow_register = MagicMock(return_value=(True, None)) - - with patch("bittensor.__console__.status") as mock_set_status: - # Need to patch the console status to avoid opening a parallel live display - mock_set_status.__enter__ = MagicMock(return_value=True) - mock_set_status.__exit__ = MagicMock(return_value=True) - - # should return True - assert self.subtensor.register( - wallet=wallet, netuid=3, num_processes=3, update_interval=5 - ) - - # calls until True and once again before exiting subtensor class - # This assertion is currently broken when difficulty is too low - assert ( - self.subtensor.is_hotkey_registered.call_count - == workblocks_before_is_registered + 2 - ) - - def test_registration_partly_failed(self): - do_pow_register_mock = MagicMock( - side_effect=[(False, "Failed"), (False, "Failed"), (True, None)] - ) - - def is_registered_side_effect(*args, **kwargs): - nonlocal do_pow_register_mock - return do_pow_register_mock.call_count < 3 - - current_block = [i for i in range(0, 100)] - - wallet = _get_mock_wallet( - hotkey=_get_mock_keypair(0, self.id()), - coldkey=_get_mock_keypair(1, self.id()), - ) - - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=bittensor.NeuronInfo.get_null_neuron() - ) - self.subtensor.is_hotkey_registered = MagicMock( - side_effect=is_registered_side_effect - ) - - self.subtensor.difficulty = MagicMock(return_value=1) - self.subtensor.get_current_block = MagicMock(side_effect=current_block) - self.subtensor._do_pow_register = do_pow_register_mock - - # should return True - self.assertTrue( - self.subtensor.register( - wallet=wallet, netuid=3, num_processes=3, update_interval=5 - ), - msg="Registration should succeed", - ) - - def test_registration_failed(self): - is_registered_return_values = [False for _ in range(100)] - current_block = [i for i in range(0, 100)] - mock_neuron = MagicMock() - mock_neuron.is_null = True - - with patch( - "bittensor.extrinsics.registration.create_pow", return_value=None - ) as mock_create_pow: - wallet = _get_mock_wallet( - hotkey=_get_mock_keypair(0, self.id()), - coldkey=_get_mock_keypair(1, self.id()), - ) - - self.subtensor.is_hotkey_registered = MagicMock( - side_effect=is_registered_return_values - ) - - self.subtensor.get_current_block = MagicMock(side_effect=current_block) - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=mock_neuron - ) - self.subtensor.substrate.get_block_hash = MagicMock( - return_value="0x" + "0" * 64 - ) - self.subtensor._do_pow_register = MagicMock(return_value=(False, "Failed")) - - # should return True - self.assertIsNot( - self.subtensor.register(wallet=wallet, netuid=3), - True, - msg="Registration should fail", - ) - self.assertEqual(mock_create_pow.call_count, 3) - - def test_registration_stale_then_continue(self): - # verify that after a stale solution, the solve will continue without exiting - - class ExitEarly(Exception): - pass - - mock_is_stale = MagicMock(side_effect=[True, False]) - - mock_do_pow_register = MagicMock(side_effect=ExitEarly()) - - mock_subtensor_self = MagicMock( - neuron_for_pubkey=MagicMock( - return_value=MagicMock(is_null=True) - ), # not registered - _do_pow_register=mock_do_pow_register, - substrate=MagicMock( - get_block_hash=MagicMock(return_value="0x" + "0" * 64), - ), - ) - - mock_wallet = MagicMock() - - mock_create_pow = MagicMock(return_value=MagicMock(is_stale=mock_is_stale)) - - with patch("bittensor.extrinsics.registration.create_pow", mock_create_pow): - # should create a pow and check if it is stale - # then should create a new pow and check if it is stale - # then should enter substrate and exit early because of test - self.subtensor.get_neuron_for_pubkey_and_subnet = MagicMock( - return_value=bittensor.NeuronInfo.get_null_neuron() - ) - with pytest.raises(ExitEarly): - bittensor.subtensor.register(mock_subtensor_self, mock_wallet, netuid=3) - self.assertEqual( - mock_create_pow.call_count, 2, msg="must try another pow after stale" - ) - self.assertEqual(mock_is_stale.call_count, 2) - self.assertEqual( - mock_do_pow_register.call_count, - 1, - msg="only tries to submit once, then exits", - ) + assert type(balance) is bittensor.utils.balance.Balance def test_defaults_to_finney(self): - sub = bittensor.subtensor() + sub = bittensor.Subtensor() assert sub.network == "finney" - assert sub.chain_endpoint == bittensor.__finney_entrypoint__ + assert sub.chain_endpoint == settings.FINNEY_ENTRYPOINT if __name__ == "__main__": diff --git a/tests/unit_tests/commands/test_utils.py b/tests/unit_tests/commands/test_utils.py deleted file mode 100644 index 7edd1c663..000000000 --- a/tests/unit_tests/commands/test_utils.py +++ /dev/null @@ -1,60 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc -# -# 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 pytest -from bittensor.commands.utils import get_delegates_details -import bittensor - - -def test_get_delegates_details_success(mocker): - """Tests the get_delegates_details function to ensure it successfully retrieves delegate details.""" - # Prep - mocked_sub = mocker.MagicMock() - mocker.patch("bittensor.subtensor", return_value=mocked_sub) - - # Call - - fake_subtensor = bittensor.subtensor() - result = get_delegates_details(fake_subtensor) - - # Assertions - mocked_sub.get_delegate_identities.assert_called_once() - assert result == mocked_sub.get_delegate_identities.return_value - - -def test_get_delegates_details_error(mocker): - """Tests the get_delegates_details function to ensure it handles errors correctly.""" - # Prep - test_error_message = "Test exception" - mocked_sub = mocker.MagicMock() - mocked_sub.get_delegate_identities.side_effect = Exception(test_error_message) - - mocker.patch("bittensor.subtensor", return_value=mocked_sub) - mocker.patch("bittensor.logging.exception") - - # Call - fake_subtensor = bittensor.subtensor() - result = get_delegates_details(fake_subtensor) - - # Assertions - mocked_sub.get_delegate_identities.assert_called_once() - bittensor.logging.exception.assert_called_once_with( - f"Unable to get Delegates Identities. Error: {test_error_message}" - ) - assert result is None diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 90b6f2574..a5503f896 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -3,11 +3,11 @@ @pytest.fixture -def force_legacy_torch_compat_api(monkeypatch): +def force_legacy_torch_compatible_api(monkeypatch): monkeypatch.setenv("USE_TORCH", "1") @pytest.fixture -def mock_aioresponse(): +def mock_aio_response(): with aioresponses() as m: yield m diff --git a/tests/unit_tests/extrinsics/test_commit_weights.py b/tests/unit_tests/extrinsics/test_commit_weights.py new file mode 100644 index 000000000..35a1d4d42 --- /dev/null +++ b/tests/unit_tests/extrinsics/test_commit_weights.py @@ -0,0 +1,133 @@ +import pytest + +from bittensor.core import subtensor as subtensor_module +from bittensor.core.settings import version_as_int +from bittensor.core.subtensor import Subtensor +from bittensor.core.extrinsics.commit_weights import ( + do_commit_weights, + do_reveal_weights, +) + + +@pytest.fixture +def subtensor(mocker): + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + return Subtensor() + + +def test_do_commit_weights(subtensor, mocker): + """Successful _do_commit_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + netuid = 1 + commit_hash = "fake_commit_hash" + wait_for_inclusion = True + wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = None + + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = do_commit_weights( + self=subtensor, + wallet=fake_wallet, + netuid=netuid, + commit_hash=commit_hash, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Assertions + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="commit_weights", + call_params={ + "netuid": netuid, + "commit_hash": commit_hash, + }, + ) + + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey + ) + + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + + assert result == ( + False, + subtensor.substrate.submit_extrinsic.return_value.error_message, + ) + + +def test_do_reveal_weights(subtensor, mocker): + """Verifies that the `_do_reveal_weights` method interacts with the right substrate methods.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_wallet.hotkey = "hotkey" + + netuid = 1 + uids = [1, 2, 3, 4] + values = [1, 2, 3, 4] + salt = [4, 2, 2, 1] + wait_for_inclusion = True + wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = None + + mocked_format_error_message = mocker.MagicMock() + subtensor_module.format_error_message = mocked_format_error_message + + # Call + result = do_reveal_weights( + self=subtensor, + wallet=fake_wallet, + netuid=netuid, + uids=uids, + values=values, + salt=salt, + version_key=version_as_int, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="reveal_weights", + call_params={ + "netuid": netuid, + "uids": uids, + "values": values, + "salt": salt, + "version_key": version_as_int, + }, + ) + + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, keypair=fake_wallet.hotkey + ) + + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + + assert result == ( + False, + subtensor.substrate.submit_extrinsic.return_value.error_message, + ) diff --git a/tests/unit_tests/extrinsics/test_delegation.py b/tests/unit_tests/extrinsics/test_delegation.py deleted file mode 100644 index 42dcf4e70..000000000 --- a/tests/unit_tests/extrinsics/test_delegation.py +++ /dev/null @@ -1,459 +0,0 @@ -import pytest -from unittest.mock import MagicMock, patch -from bittensor.subtensor import Subtensor -from bittensor.wallet import wallet as Wallet -from bittensor.utils.balance import Balance -from bittensor.extrinsics.delegation import ( - nominate_extrinsic, - delegate_extrinsic, - undelegate_extrinsic, -) -from bittensor.errors import ( - NominationError, - NotDelegateError, - NotRegisteredError, - StakeError, -) - - -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=Subtensor) - mock.network = "magic_mock" - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=Wallet) - mock.hotkey.ss58_address = "fake_hotkey_address" - mock.coldkey.ss58_address = "fake_coldkey_address" - mock.coldkey = MagicMock() - mock.hotkey = MagicMock() - mock.name = "fake_wallet_name" - mock.hotkey_str = "fake_hotkey_str" - return mock - - -@pytest.mark.parametrize( - "already_delegate, nomination_success, raises_exception, expected_result", - [ - (False, True, None, True), # Successful nomination - (True, None, None, False), # Already a delegate - (False, None, NominationError, False), # Failure - Nomination error - (False, None, ValueError, False), # Failure - ValueError - ], - ids=[ - "success-nomination-done", - "failure-already-delegate", - "failure-nomination-error", - "failure-value-error", - ], -) -def test_nominate_extrinsic( - mock_subtensor, - mock_wallet, - already_delegate, - nomination_success, - raises_exception, - expected_result, -): - # Arrange - with patch.object( - mock_subtensor, "is_hotkey_delegate", return_value=already_delegate - ), patch.object( - mock_subtensor, "_do_nominate", return_value=nomination_success - ) as mock_nominate: - if raises_exception: - mock_subtensor._do_nominate.side_effect = raises_exception - - # Act - result = nominate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - wait_for_finalization=False, - wait_for_inclusion=True, - ) - # Assert - assert result == expected_result - - if not already_delegate and nomination_success is not None: - mock_nominate.assert_called_once_with( - wallet=mock_wallet, wait_for_inclusion=True, wait_for_finalization=False - ) - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, is_delegate, prompt_response, stake_amount, balance_sufficient, transaction_success, raises_error, expected_result, delegate_called", - [ - (True, False, True, True, 100, True, True, None, True, True), # Success case - ( - False, - False, - True, - True, - 100, - True, - True, - None, - True, - True, - ), # Success case - no wait - ( - True, - False, - True, - True, - None, - True, - True, - None, - True, - True, - ), # Success case - all stake - ( - True, - False, - True, - True, - 0.000000100, - True, - True, - None, - True, - True, - ), # Success case - below cutoff threshold - ( - True, - False, - True, - True, - Balance.from_tao(1), - True, - True, - None, - True, - True, - ), # Success case - from Tao - ( - True, - False, - False, - None, - 100, - True, - False, - NotDelegateError, - False, - False, - ), # Not a delegate error - ( - True, - False, - True, - True, - 200, - False, - False, - None, - False, - False, - ), # Insufficient balance - ( - True, - False, - True, - False, - 100, - True, - True, - None, - False, - False, - ), # User declines prompt - ( - True, - False, - True, - True, - 100, - True, - False, - None, - False, - True, - ), # Transaction fails - ( - True, - False, - True, - True, - 100, - True, - False, - NotRegisteredError, - False, - True, - ), # Raises a NotRegisteredError - ( - True, - False, - True, - True, - 100, - True, - False, - StakeError, - False, - True, - ), # Raises a StakeError - ], - ids=[ - "success-delegate", - "success-no-wait", - "success-all-stake", - "success-below-existential-threshold", - "success-from-tao", - "failure-not-delegate", - "failure-low-balance", - "failure-prompt-declined", - "failure-transaction-failed", - "failure-NotRegisteredError", - "failure-StakeError", - ], -) -def test_delegate_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - is_delegate, - prompt_response, - stake_amount, - balance_sufficient, - transaction_success, - raises_error, - expected_result, - delegate_called, -): - # Arrange - wallet_balance = Balance.from_tao(500) - wallet_insufficient_balance = Balance.from_tao(0.002) - - with patch("rich.prompt.Confirm.ask", return_value=prompt_response), patch.object( - mock_subtensor, - "get_balance", - return_value=wallet_balance - if balance_sufficient - else wallet_insufficient_balance, - ), patch.object( - mock_subtensor, "is_hotkey_delegate", return_value=is_delegate - ), patch.object( - mock_subtensor, "_do_delegation", return_value=transaction_success - ) as mock_delegate: - if raises_error: - mock_delegate.side_effect = raises_error - - # Act - if raises_error == NotDelegateError: - with pytest.raises(raises_error): - result = delegate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=stake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=True, - ) - else: - result = delegate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=stake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=True, - ) - # Assert - assert result == expected_result - - if delegate_called: - if stake_amount is None: - called_stake_amount = wallet_balance - elif isinstance(stake_amount, Balance): - called_stake_amount = stake_amount - else: - called_stake_amount = Balance.from_tao(stake_amount) - - if called_stake_amount > Balance.from_rao(1000): - called_stake_amount -= Balance.from_rao(1000) - - mock_delegate.assert_called_once_with( - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=called_stake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, is_delegate, prompt_response, unstake_amount, current_stake, transaction_success, raises_error, expected_result", - [ - (True, False, True, True, 50, 100, True, None, True), # Success case - (False, False, True, True, 50, 100, True, None, True), # Success case - no wait - ( - False, - False, - True, - True, - Balance.from_tao(1), - 100, - True, - None, - True, - ), # Success case - from tao - (True, False, True, True, None, 100, True, None, True), # Success - unstake all - ( - True, - False, - True, - True, - 1000, - 1000, - False, - None, - False, - ), # Failure - transaction fails - ( - True, - False, - False, - None, - 100, - 120, - True, - NotDelegateError, - False, - ), # Not a delegate - (True, False, True, False, 100, 111, True, None, False), # User declines prompt - ( - True, - False, - True, - True, - 100, - 90, - True, - None, - False, - ), # Insufficient stake to unstake - ( - True, - False, - True, - True, - 100, - 100, - False, - StakeError, - False, - ), # StakeError raised - ( - True, - False, - True, - True, - 100, - 100, - False, - NotRegisteredError, - False, - ), # NotRegisteredError raised - ], - ids=[ - "success-undelegate", - "success-undelegate-no-wait", - "success-from-tao", - "success-undelegate-all", - "failure-transaction-failed", - "failure-NotDelegateError", - "failure-prompt-declined", - "failure-insufficient-stake", - "failure--StakeError", - "failure-NotRegisteredError", - ], -) -def test_undelegate_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - is_delegate, - prompt_response, - unstake_amount, - current_stake, - transaction_success, - raises_error, - expected_result, -): - # Arrange - wallet_balance = Balance.from_tao(500) - - with patch("rich.prompt.Confirm.ask", return_value=prompt_response), patch.object( - mock_subtensor, "is_hotkey_delegate", return_value=is_delegate - ), patch.object( - mock_subtensor, "get_balance", return_value=wallet_balance - ), patch.object( - mock_subtensor, - "get_stake_for_coldkey_and_hotkey", - return_value=Balance.from_tao(current_stake), - ), patch.object( - mock_subtensor, "_do_undelegation", return_value=transaction_success - ) as mock_undelegate: - if raises_error: - mock_undelegate.side_effect = raises_error - - # Act - if raises_error == NotDelegateError: - with pytest.raises(raises_error): - result = undelegate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=unstake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=True, - ) - else: - result = undelegate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=unstake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=True, - ) - - # Assert - assert result == expected_result - - if expected_result and prompt_response: - if unstake_amount is None: - called_unstake_amount = Balance.from_tao(current_stake) - elif isinstance(unstake_amount, Balance): - called_unstake_amount = unstake_amount - else: - called_unstake_amount = Balance.from_tao(unstake_amount) - - mock_undelegate.assert_called_once_with( - wallet=mock_wallet, - delegate_ss58=mock_wallet.hotkey.ss58_address, - amount=called_unstake_amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) diff --git a/tests/unit_tests/extrinsics/test_init.py b/tests/unit_tests/extrinsics/test_init.py index 8e3caaf90..8a2480a9b 100644 --- a/tests/unit_tests/extrinsics/test_init.py +++ b/tests/unit_tests/extrinsics/test_init.py @@ -4,6 +4,7 @@ def test_format_error_message_with_right_error_message(): + """Verify that error message from extrinsic response parses correctly.""" # Prep fake_error_message = { "type": "SomeType", @@ -22,6 +23,7 @@ def test_format_error_message_with_right_error_message(): def test_format_error_message_with_empty_error_message(): + """Verify that empty error message from extrinsic response parses correctly.""" # Prep fake_error_message = {} @@ -36,6 +38,7 @@ def test_format_error_message_with_empty_error_message(): def test_format_error_message_with_wrong_type_error_message(): + """Verify that error message from extrinsic response with wrong type parses correctly.""" # Prep fake_error_message = None @@ -47,3 +50,65 @@ def test_format_error_message_with_wrong_type_error_message(): assert "UnknownType" in result assert "UnknownError" in result assert "Unknown Description" in result + + +def test_format_error_message_with_custom_error_message_with_index(mocker): + """Tests error formatter if subtensor error is custom error with index.""" + # Preps + fake_custom_error = { + "code": 1010, + "message": "SomeErrorName", + "data": "Custom error: 1", + } + fake_subtensor_error = { + "docs": ["Some description"], + "fields": [], + "index": 1, + "name": "SomeErrorName", + } + + fake_substrate = mocker.MagicMock() + fake_substrate.metadata.get_metadata_pallet().errors.__getitem__().value.get = ( + mocker.Mock( + side_effect=[fake_custom_error["message"], fake_subtensor_error["docs"]] + ) + ) + + mocker.patch( + "substrateinterface.base.SubstrateInterface", return_value=fake_substrate + ) + + # Call + result = format_error_message(fake_custom_error, fake_substrate) + + # Assertions + assert ( + result + == f"Subtensor returned `SubstrateRequestException({fake_subtensor_error['name']})` error. This means: `Some description`." + ) + + +def test_format_error_message_with_custom_error_message_without_index(mocker): + """Tests error formatter if subtensor error is custom error without index.""" + # Preps + fake_custom_error = { + "code": 1010, + "message": "SomeErrorType", + "data": "Custom error description", + } + fake_substrate = mocker.MagicMock() + fake_substrate.metadata.get_metadata_pallet().errors.__getitem__().value.get.return_value = fake_custom_error[ + "message" + ] + mocker.patch( + "substrateinterface.base.SubstrateInterface", return_value=fake_substrate + ) + + # Call + result = format_error_message(fake_custom_error, fake_substrate) + + # Assertions + assert ( + result + == f"Subtensor returned `SubstrateRequestException({fake_custom_error['message']})` error. This means: `{fake_custom_error['data']}`." + ) diff --git a/tests/unit_tests/extrinsics/test_network.py b/tests/unit_tests/extrinsics/test_network.py deleted file mode 100644 index 67df030ff..000000000 --- a/tests/unit_tests/extrinsics/test_network.py +++ /dev/null @@ -1,157 +0,0 @@ -import pytest -from unittest.mock import MagicMock, patch -from bittensor.subtensor import Subtensor -from bittensor.wallet import wallet as Wallet -from bittensor.extrinsics.network import ( - set_hyperparameter_extrinsic, - register_subnetwork_extrinsic, -) - - -# Mock the bittensor and related modules to avoid real network calls and wallet operations -@pytest.fixture -def mock_subtensor(): - subtensor = MagicMock(spec=Subtensor) - subtensor.get_balance.return_value = 100 - subtensor.get_subnet_burn_cost.return_value = 10 - subtensor.substrate = MagicMock() - subtensor.substrate.get_block_hash = MagicMock(return_value="0x" + "0" * 64) - return subtensor - - -@pytest.fixture -def mock_wallet(): - wallet = MagicMock(spec=Wallet) - wallet.coldkeypub.ss58_address = "fake_address" - wallet.coldkey = MagicMock() - return wallet - - -@pytest.fixture -def mock_other_owner_wallet(): - wallet = MagicMock(spec=Wallet) - wallet.coldkeypub.ss58_address = "fake_other_owner" - return wallet - - -@pytest.mark.parametrize( - "test_id, wait_for_inclusion, wait_for_finalization, prompt, expected", - [ - ("happy-path-01", False, True, False, True), - ("happy-path-02", True, False, False, True), - ("happy-path-03", False, False, False, True), - ("happy-path-04", True, True, False, True), - ], -) -def test_register_subnetwork_extrinsic_happy_path( - mock_subtensor, - mock_wallet, - test_id, - wait_for_inclusion, - wait_for_finalization, - prompt, - expected, -): - # Arrange - mock_subtensor.substrate.submit_extrinsic.return_value.is_success = True - - # Act - result = register_subnetwork_extrinsic( - mock_subtensor, mock_wallet, wait_for_inclusion, wait_for_finalization, prompt - ) - - # Assert - assert result == expected - - -# Edge cases -@pytest.mark.parametrize( - "test_id, balance, burn_cost, prompt_input, expected", - [ - ("edge-case-01", 0, 10, False, False), # Balance is zero - ("edge-case-02", 10, 10, False, False), # Balance equals burn cost - ("edge-case-03", 9, 10, False, False), # Balance less than burn cost - ("edge-case-04", 100, 10, True, True), # User declines prompt - ], -) -def test_register_subnetwork_extrinsic_edge_cases( - mock_subtensor, - mock_wallet, - test_id, - balance, - burn_cost, - prompt_input, - expected, - monkeypatch, -): - # Arrange - mock_subtensor.get_balance.return_value = balance - mock_subtensor.get_subnet_burn_cost.return_value = burn_cost - monkeypatch.setattr("rich.prompt.Confirm.ask", lambda x: prompt_input) - - # Act - result = register_subnetwork_extrinsic(mock_subtensor, mock_wallet, prompt=True) - - # Assert - assert result == expected - - -@pytest.mark.parametrize( - "netuid, parameter, value, is_owner, wait_for_inclusion, wait_for_finalization, prompt, extrinsic_success, expected_result", - [ - # Success - no wait - (1, "serving_rate_limit", 49, True, False, False, False, True, True), - # Success - with wait - (1, "serving_rate_limit", 50, True, True, True, False, True, True), - # Failure - wallet doesn't own subnet - (1, "serving_rate_limit", 50, False, True, True, False, True, False), - # Failure - invalid hyperparameter - (1, None, 50, True, True, False, False, False, False), - ], - ids=[ - "success-no-wait", - "success-with-wait", - "failure-not-owner", - "failure-invalid-hyperparameter", - ], -) -def test_set_hyperparameter_extrinsic( - mock_subtensor, - mock_wallet, - mock_other_owner_wallet, - netuid, - parameter, - value, - is_owner, - wait_for_inclusion, - wait_for_finalization, - prompt, - extrinsic_success, - expected_result, -): - # Arrange - with patch.object( - mock_subtensor, - "get_subnet_owner", - return_value=mock_wallet.coldkeypub.ss58_address - if is_owner - else mock_other_owner_wallet.coldkeypub.ss58_address, - ), patch.object( - mock_subtensor.substrate, - "submit_extrinsic", - return_value=MagicMock(is_success=extrinsic_success), - ): - # Act - result = set_hyperparameter_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=netuid, - parameter=parameter, - value=value, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_result diff --git a/tests/unit_tests/extrinsics/test_prometheus.py b/tests/unit_tests/extrinsics/test_prometheus.py index 7d9c975fb..dbcfed1e4 100644 --- a/tests/unit_tests/extrinsics/test_prometheus.py +++ b/tests/unit_tests/extrinsics/test_prometheus.py @@ -1,21 +1,42 @@ -import pytest +# 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. + from unittest.mock import MagicMock, patch -import bittensor -from bittensor.subtensor import Subtensor -from bittensor.wallet import wallet as Wallet -from bittensor.extrinsics.prometheus import prometheus_extrinsic + +import pytest +from bittensor_wallet import Wallet + +from bittensor.core.extrinsics.prometheus import ( + prometheus_extrinsic, +) +from bittensor.core.subtensor import Subtensor +from bittensor.core.settings import version_as_int # Mocking the bittensor and networking modules @pytest.fixture def mock_bittensor(): - with patch("bittensor.subtensor") as mock: + with patch("bittensor.core.subtensor.Subtensor") as mock: yield mock @pytest.fixture def mock_wallet(): - with patch("bittensor.wallet") as mock: + with patch("bittensor_wallet.Wallet") as mock: yield mock @@ -49,13 +70,14 @@ def test_prometheus_extrinsic_happy_path( # Arrange subtensor = MagicMock(spec=Subtensor) subtensor.network = "test_network" + subtensor.substrate = MagicMock() wallet = MagicMock(spec=Wallet) mock_net.get_external_ip.return_value = "192.168.0.1" mock_net.ip_to_int.return_value = 3232235521 # IP in integer form mock_net.ip_version.return_value = 4 neuron = MagicMock() neuron.is_null = False - neuron.prometheus_info.version = bittensor.__version_as_int__ + neuron.prometheus_info.version = version_as_int neuron.prometheus_info.ip = 3232235521 neuron.prometheus_info.port = port neuron.prometheus_info.ip_type = 4 @@ -91,6 +113,7 @@ def test_prometheus_extrinsic_edge_cases( # Arrange subtensor = MagicMock(spec=Subtensor) subtensor.network = "test_network" + subtensor.substrate = MagicMock() wallet = MagicMock(spec=Wallet) mock_net.get_external_ip.return_value = ip mock_net.ip_to_int.return_value = 3232235521 # IP in integer form @@ -112,43 +135,33 @@ def test_prometheus_extrinsic_edge_cases( ) # Assert - assert result == True, f"Test ID: {test_id}" + assert result is True, f"Test ID: {test_id}" # Error cases -@pytest.mark.parametrize( - "ip, port, netuid, exception, test_id", - [ - ( - None, - 9221, - 0, - RuntimeError("Unable to attain your external ip."), - "error-case-no-external-ip", - ), - ], -) -def test_prometheus_extrinsic_error_cases( - mock_bittensor, mock_wallet, mock_net, ip, port, netuid, exception, test_id -): +def test_prometheus_extrinsic_error_cases(mock_bittensor, mock_wallet, mocker): # Arrange subtensor = MagicMock(spec=Subtensor) subtensor.network = "test_network" + subtensor.substrate = MagicMock() + subtensor.substrate.websocket.sock.getsockopt.return_value = 0 wallet = MagicMock(spec=Wallet) - mock_net.get_external_ip.side_effect = exception neuron = MagicMock() neuron.is_null = True subtensor.get_neuron_for_pubkey_and_subnet.return_value = neuron subtensor._do_serve_prometheus.return_value = (True,) - # Act & Assert - with pytest.raises(ValueError): - prometheus_extrinsic( - subtensor=subtensor, - wallet=wallet, - ip=ip, - port=port, - netuid=netuid, - wait_for_inclusion=False, - wait_for_finalization=True, - ) + with mocker.patch( + "bittensor.utils.networking.get_external_ip", side_effect=RuntimeError + ): + # Act & Assert + with pytest.raises(RuntimeError): + prometheus_extrinsic( + subtensor=subtensor, + wallet=wallet, + ip=None, + port=9221, + netuid=1, + wait_for_inclusion=False, + wait_for_finalization=True, + ) diff --git a/tests/unit_tests/extrinsics/test_registration.py b/tests/unit_tests/extrinsics/test_registration.py deleted file mode 100644 index 49805f0cf..000000000 --- a/tests/unit_tests/extrinsics/test_registration.py +++ /dev/null @@ -1,401 +0,0 @@ -import pytest -from unittest.mock import MagicMock, patch -from bittensor.subtensor import Subtensor -from bittensor.wallet import wallet as Wallet -from bittensor.utils.registration import POWSolution -from bittensor.extrinsics.registration import ( - MaxSuccessException, - MaxAttemptsException, - swap_hotkey_extrinsic, - burned_register_extrinsic, - register_extrinsic, -) - - -# Mocking external dependencies -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=Subtensor) - mock.network = "mock_network" - mock.substrate = MagicMock() - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=Wallet) - mock.coldkeypub.ss58_address = "mock_address" - mock.coldkey = MagicMock() - mock.hotkey = MagicMock() - mock.hotkey.ss58_address = "fake_ss58_address" - return mock - - -@pytest.fixture -def mock_pow_solution(): - mock = MagicMock(spec=POWSolution) - mock.block_number = 123 - mock.nonce = 456 - mock.seal = [0, 1, 2, 3] - mock.is_stale.return_value = False - return mock - - -@pytest.fixture -def mock_new_wallet(): - mock = MagicMock(spec=Wallet) - mock.coldkeypub.ss58_address = "mock_address" - mock.coldkey = MagicMock() - mock.hotkey = MagicMock() - return mock - - -@pytest.mark.parametrize( - "wait_for_inclusion,wait_for_finalization,prompt,cuda,dev_id,tpb,num_processes,update_interval,log_verbose,expected", - [ - (False, True, False, False, 0, 256, None, None, False, True), - (True, False, False, True, [0], 256, 1, 100, True, False), - (False, False, False, True, 1, 512, 2, 200, False, False), - ], - ids=["happy-path-1", "happy-path-2", "happy-path-3"], -) -def test_run_faucet_extrinsic_happy_path( - mock_subtensor, - mock_wallet, - mock_pow_solution, - wait_for_inclusion, - wait_for_finalization, - prompt, - cuda, - dev_id, - tpb, - num_processes, - update_interval, - log_verbose, - expected, -): - with patch( - "bittensor.utils.registration._solve_for_difficulty_fast", - return_value=mock_pow_solution, - ) as mock_create_pow, patch("rich.prompt.Confirm.ask", return_value=True): - from bittensor.extrinsics.registration import run_faucet_extrinsic - - # Arrange - mock_subtensor.get_balance.return_value = 100 - mock_subtensor.substrate.submit_extrinsic.return_value.is_success = True - - # Act - result = run_faucet_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # Assert - if isinstance(result, tuple): - assert result[0] == expected - if result[0] is True: - # Checks only if successful - mock_subtensor.substrate.submit_extrinsic.assert_called() - else: - assert result == expected - mock_subtensor.get_balance.assert_called_with("mock_address") - - -@pytest.mark.parametrize( - "cuda,torch_cuda_available,prompt_response,expected", - [ - ( - True, - False, - False, - False, - ), # ID: edge-case-1: CUDA required but not available, user declines prompt - ( - True, - False, - True, - False, - ), # ID: edge-case-2: CUDA required but not available, user accepts prompt but fails due to CUDA unavailability - ], - ids=["edge-case-1", "edge-case-2"], -) -def test_run_faucet_extrinsic_edge_cases( - mock_subtensor, mock_wallet, cuda, torch_cuda_available, prompt_response, expected -): - with patch("torch.cuda.is_available", return_value=torch_cuda_available), patch( - "rich.prompt.Confirm.ask", return_value=prompt_response - ): - from bittensor.extrinsics.registration import run_faucet_extrinsic - - # Act - result = run_faucet_extrinsic( - subtensor=mock_subtensor, wallet=mock_wallet, cuda=cuda - ) - - # Assert - assert result[0] == expected - - -@pytest.mark.parametrize( - "exception,expected", - [ - (KeyboardInterrupt, (True, "Done")), # ID: error-1: User interrupts the process - ( - MaxSuccessException, - (True, "Max successes reached: 3"), - ), # ID: error-2: Maximum successes reached - ( - MaxAttemptsException, - (False, "Max attempts reached: 3"), - ), # ID: error-3: Maximum attempts reached - ], - ids=["error-1", "error-2", "error-3"], -) -@pytest.mark.skip(reason="Waiting for fix to MaxAttemptedException") -def test_run_faucet_extrinsic_error_cases( - mock_subtensor, mock_wallet, mock_pow_solution, exception, expected -): - with patch( - "bittensor.utils.registration.create_pow", - side_effect=[mock_pow_solution, exception], - ): - from bittensor.extrinsics.registration import run_faucet_extrinsic - - # Act - result = run_faucet_extrinsic( - subtensor=mock_subtensor, wallet=mock_wallet, max_allowed_attempts=3 - ) - - # Assert - assert result == expected - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, prompt, swap_success, prompt_response, expected_result, test_id", - [ - # Happy paths - (False, True, False, True, None, True, "happy-path-finalization-true"), - (True, False, False, True, None, True, "happy-path-inclusion-true"), - (True, True, False, True, None, True, "edge-both-waits-true"), - # Error paths - (False, True, False, False, None, False, "swap_failed"), - (False, True, True, True, False, False, "error-prompt-declined"), - ], -) -def test_swap_hotkey_extrinsic( - mock_subtensor, - mock_wallet, - mock_new_wallet, - wait_for_inclusion, - wait_for_finalization, - prompt, - swap_success, - prompt_response, - expected_result, - test_id, -): - # Arrange - with patch.object( - mock_subtensor, - "_do_swap_hotkey", - return_value=(swap_success, "Mock error message"), - ): - with patch( - "rich.prompt.Confirm.ask", return_value=prompt_response - ) as mock_confirm: - # Act - result = swap_hotkey_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - new_wallet=mock_new_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_result, f"Test failed for test_id: {test_id}" - - if prompt: - mock_confirm.assert_called_once() - else: - mock_confirm.assert_not_called() - - -@pytest.mark.parametrize( - "subnet_exists, neuron_is_null, recycle_success, prompt, prompt_response, is_registered, expected_result, test_id", - [ - # Happy paths - (True, False, None, False, None, None, True, "neuron-not-null"), - (True, True, True, True, True, True, True, "happy-path-wallet-registered"), - # Error paths - (False, True, False, False, None, None, False, "subnet-non-existence"), - (True, True, True, True, False, None, False, "prompt-declined"), - (True, True, False, True, True, False, False, "error-path-recycling-failed"), - (True, True, True, True, True, False, False, "error-path-not-registered"), - ], -) -def test_burned_register_extrinsic( - mock_subtensor, - mock_wallet, - subnet_exists, - neuron_is_null, - recycle_success, - prompt, - prompt_response, - is_registered, - expected_result, - test_id, -): - # Arrange - with patch.object( - mock_subtensor, "subnet_exists", return_value=subnet_exists - ), patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=MagicMock(is_null=neuron_is_null), - ), patch.object( - mock_subtensor, - "_do_burned_register", - return_value=(recycle_success, "Mock error message"), - ), patch.object( - mock_subtensor, "is_hotkey_registered", return_value=is_registered - ), patch("rich.prompt.Confirm.ask", return_value=prompt_response) as mock_confirm: - # Act - result = burned_register_extrinsic( - subtensor=mock_subtensor, wallet=mock_wallet, netuid=123, prompt=True - ) - - # Assert - assert result == expected_result, f"Test failed for test_id: {test_id}" - - if prompt: - mock_confirm.assert_called_once() - else: - mock_confirm.assert_not_called() - - -@pytest.mark.parametrize( - "subnet_exists, neuron_is_null, prompt, prompt_response, cuda_available, expected_result, test_id", - [ - (False, True, True, True, True, False, "subnet-does-not-exist"), - (True, False, True, True, True, True, "neuron-already-registered"), - (True, True, True, False, True, False, "user-declines-prompt"), - (True, True, False, None, False, False, "cuda-unavailable"), - ], -) -def test_register_extrinsic_without_pow( - mock_subtensor, - mock_wallet, - subnet_exists, - neuron_is_null, - prompt, - prompt_response, - cuda_available, - expected_result, - test_id, -): - # Arrange - with patch.object( - mock_subtensor, "subnet_exists", return_value=subnet_exists - ), patch.object( - mock_subtensor, - "get_neuron_for_pubkey_and_subnet", - return_value=MagicMock(is_null=neuron_is_null), - ), patch("rich.prompt.Confirm.ask", return_value=prompt_response), patch( - "torch.cuda.is_available", return_value=cuda_available - ): - # Act - result = register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=prompt, - 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( - "pow_success, pow_stale, registration_success, cuda, hotkey_registered, expected_result, test_id", - [ - (True, False, True, False, False, True, "successful-with-valid-pow"), - (True, False, True, True, False, True, "successful-with-valid-cuda-pow"), - # Pow failed but key was registered already - (False, False, False, False, True, True, "hotkey-registered"), - # Pow was a success but registration failed with error 'key already registered' - (True, False, False, False, False, True, "registration-fail-key-registered"), - ], -) -def test_register_extrinsic_with_pow( - mock_subtensor, - mock_wallet, - mock_pow_solution, - pow_success, - pow_stale, - registration_success, - cuda, - hotkey_registered, - expected_result, - test_id, -): - # Arrange - with patch( - "bittensor.utils.registration._solve_for_difficulty_fast", - return_value=mock_pow_solution if pow_success else None, - ), patch( - "bittensor.utils.registration._solve_for_difficulty_fast_cuda", - return_value=mock_pow_solution if pow_success else None, - ), patch.object( - mock_subtensor, - "_do_pow_register", - return_value=(registration_success, "HotKeyAlreadyRegisteredInSubNet"), - ), 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 = MagicMock( - return_value=hotkey_registered - ) - - result = register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuid=123, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=False, - 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}" diff --git a/tests/unit_tests/extrinsics/test_root.py b/tests/unit_tests/extrinsics/test_root.py deleted file mode 100644 index b801f7b4e..000000000 --- a/tests/unit_tests/extrinsics/test_root.py +++ /dev/null @@ -1,308 +0,0 @@ -import pytest -from unittest.mock import MagicMock, patch -from bittensor.subtensor import Subtensor -from bittensor.extrinsics.root import ( - root_register_extrinsic, - set_root_weights_extrinsic, -) - - -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=Subtensor) - mock.network = "magic_mock" - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock() - mock.hotkey.ss58_address = "fake_hotkey_address" - return mock - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, hotkey_registered, registration_success, prompt, user_response, expected_result", - [ - ( - False, - True, - [True, None], - True, - True, - True, - True, - ), # Already registered after attempt - ( - False, - True, - [False, True], - True, - True, - True, - True, - ), # Registration succeeds with user confirmation - (False, True, [False, False], False, False, None, None), # Registration fails - ( - False, - True, - [False, False], - True, - False, - None, - None, - ), # Registration succeeds but neuron not found - ( - False, - True, - [False, False], - True, - True, - False, - False, - ), # User declines registration - ], - ids=[ - "success-already-registered", - "success-registration-succeeds", - "failure-registration-failed", - "failure-neuron-not-found", - "failure-prompt-declined", - ], -) -def test_root_register_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - hotkey_registered, - registration_success, - prompt, - user_response, - expected_result, -): - # Arrange - mock_subtensor.is_hotkey_registered.side_effect = hotkey_registered - - with patch.object( - mock_subtensor, - "_do_root_register", - return_value=(registration_success, "Error registering"), - ) as mock_register, patch("rich.prompt.Confirm.ask", return_value=user_response): - # Act - result = root_register_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - # Assert - assert result == expected_result - - if not hotkey_registered[0] and user_response: - mock_register.assert_called_once() - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, netuids, weights, prompt, user_response, expected_success", - [ - (True, False, [1, 2], [0.5, 0.5], True, True, True), # Success - weights set - ( - False, - False, - [1, 2], - [0.5, 0.5], - False, - None, - True, - ), # Success - weights set no wait - ( - True, - False, - [1, 2], - [2000, 20], - True, - True, - True, - ), # Success - large value to be normalized - ( - True, - False, - [1, 2], - [2000, 0], - True, - True, - True, - ), # Success - single large value - ( - True, - False, - [1, 2], - [0.5, 0.5], - True, - False, - False, - ), # Failure - prompt declined - ( - True, - False, - [1, 2], - [0.5, 0.5], - False, - None, - False, - ), # Failure - setting weights failed - ( - True, - False, - [], - [], - None, - False, - False, - ), # Exception catched - ValueError 'min() arg is an empty sequence' - ], - ids=[ - "success-weights-set", - "success-not-wait", - "success-large-value", - "success-single-value", - "failure-user-declines", - "failure-setting-weights", - "failure-value-error-exception", - ], -) -def test_set_root_weights_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - netuids, - weights, - prompt, - user_response, - expected_success, -): - # Arrange - with patch.object( - mock_subtensor, - "_do_set_root_weights", - return_value=(expected_success, "Mock error"), - ), patch.object( - mock_subtensor, "min_allowed_weights", return_value=0 - ), patch.object(mock_subtensor, "max_weight_limit", return_value=1), patch( - "rich.prompt.Confirm.ask", return_value=user_response - ) as mock_confirm: - # Act - result = set_root_weights_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - netuids=netuids, - weights=weights, - version_key=0, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_success - if prompt: - mock_confirm.assert_called_once() - else: - mock_confirm.assert_not_called() - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, netuids, weights, prompt, user_response, expected_success", - [ - (True, False, [1, 2], [0.5, 0.5], True, True, True), # Success - weights set - ( - False, - False, - [1, 2], - [0.5, 0.5], - False, - None, - True, - ), # Success - weights set no wait - ( - True, - False, - [1, 2], - [2000, 20], - True, - True, - True, - ), # Success - large value to be normalized - ( - True, - False, - [1, 2], - [2000, 0], - True, - True, - True, - ), # Success - single large value - ( - True, - False, - [1, 2], - [0.5, 0.5], - True, - False, - False, - ), # Failure - prompt declined - ( - True, - False, - [1, 2], - [0.5, 0.5], - False, - None, - False, - ), # Failure - setting weights failed - ( - True, - False, - [], - [], - None, - False, - False, - ), # Exception catched - ValueError 'min() arg is an empty sequence' - ], - ids=[ - "success-weights-set", - "success-not-wait", - "success-large-value", - "success-single-value", - "failure-user-declines", - "failure-setting-weights", - "failure-value-error-exception", - ], -) -def test_set_root_weights_extrinsic_torch( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - netuids, - weights, - prompt, - user_response, - expected_success, - force_legacy_torch_compat_api, -): - test_set_root_weights_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - netuids, - weights, - prompt, - user_response, - expected_success, - ) diff --git a/tests/unit_tests/extrinsics/test_senate.py b/tests/unit_tests/extrinsics/test_senate.py deleted file mode 100644 index 66849efc5..000000000 --- a/tests/unit_tests/extrinsics/test_senate.py +++ /dev/null @@ -1,237 +0,0 @@ -import pytest -from unittest.mock import MagicMock, patch -from bittensor import subtensor, wallet -from bittensor.extrinsics.senate import ( - leave_senate_extrinsic, - register_senate_extrinsic, - vote_senate_extrinsic, -) - - -# Mocking external dependencies -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=subtensor) - mock.substrate = MagicMock() - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=wallet) - mock.coldkey = MagicMock() - mock.hotkey = MagicMock() - mock.hotkey.ss58_address = "fake_hotkey_address" - mock.is_senate_member = None - return mock - - -# Parametrized test cases -@pytest.mark.parametrize( - "wait_for_inclusion,wait_for_finalization,prompt,response_success,is_registered,expected_result, test_id", - [ - # Happy path tests - (False, True, False, True, True, True, "happy-path-finalization-true"), - (True, False, False, True, True, True, "happy-path-inclusion-true"), - (False, False, False, True, True, True, "happy-path-no_wait"), - # Edge cases - (True, True, False, True, True, True, "edge-both-waits-true"), - # Error cases - (False, True, False, False, False, None, "error-finalization-failed"), - (True, False, False, False, False, None, "error-inclusion-failed"), - (False, True, True, True, False, False, "error-prompt-declined"), - ], -) -def test_register_senate_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - prompt, - response_success, - is_registered, - expected_result, - test_id, -): - # Arrange - with patch( - "bittensor.extrinsics.senate.Confirm.ask", return_value=not prompt - ), patch("bittensor.extrinsics.senate.time.sleep"), 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", - ), - ) as mock_submit_extrinsic, patch.object( - mock_wallet, "is_senate_member", return_value=is_registered - ): - # Act - result = register_senate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_result, f"Test ID: {test_id}" - - -@pytest.mark.parametrize( - "wait_for_inclusion, wait_for_finalization, prompt, response_success, \ - vote, vote_in_ayes, vote_in_nays, expected_result, test_id", - [ - # Happy path tests - (False, True, False, True, True, True, False, True, "happy-finalization-aye"), - (True, False, False, True, False, False, True, True, "happy-inclusion-nay"), - (False, False, False, True, True, True, False, True, "happy-no-wait-aye"), - # Edge cases - (True, True, False, True, True, True, False, True, "edge-both-waits-true-aye"), - # Error cases - (True, False, False, False, True, False, False, None, "error-inclusion-failed"), - (True, False, True, True, True, True, False, False, "error-prompt-declined"), - ( - True, - False, - False, - True, - True, - False, - False, - None, - "error-no-vote-registered-aye", - ), - ( - False, - True, - False, - True, - False, - False, - False, - None, - "error-no-vote-registered-nay", - ), - ( - False, - True, - False, - False, - True, - False, - False, - None, - "error-finalization-failed", - ), - ], -) -def test_vote_senate_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - prompt, - vote, - response_success, - vote_in_ayes, - vote_in_nays, - expected_result, - test_id, -): - # Arrange - proposal_hash = "mock_hash" - proposal_idx = 123 - - with patch( - "bittensor.extrinsics.senate.Confirm.ask", return_value=not prompt - ), patch("bittensor.extrinsics.senate.time.sleep"), 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", - ), - ), patch.object( - mock_subtensor, - "get_vote_data", - return_value={ - "ayes": [mock_wallet.hotkey.ss58_address] if vote_in_ayes else [], - "nays": [mock_wallet.hotkey.ss58_address] if vote_in_nays else [], - }, - ): - # Act - result = vote_senate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - proposal_hash=proposal_hash, - proposal_idx=proposal_idx, - vote=vote, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_result, f"Test ID: {test_id}" - - -# Parametrized test cases -@pytest.mark.parametrize( - "wait_for_inclusion,wait_for_finalization,prompt,response_success,is_registered,expected_result, test_id", - [ - # Happy path tests - (False, True, False, True, False, True, "happy-path-finalization-true"), - (True, False, False, True, False, True, "happy-path-inclusion-true"), - (False, False, False, True, False, True, "happy-path-no_wait"), - # Edge cases - (True, True, False, True, False, True, "edge-both-waits-true"), - # Error cases - (False, True, False, False, True, None, "error-finalization-failed"), - (True, False, False, False, True, None, "error-inclusion-failed"), - (False, True, True, True, False, False, "error-prompt-declined"), - ], -) -def test_leave_senate_extrinsic( - mock_subtensor, - mock_wallet, - wait_for_inclusion, - wait_for_finalization, - prompt, - response_success, - is_registered, - expected_result, - test_id, -): - # Arrange - with patch( - "bittensor.extrinsics.senate.Confirm.ask", return_value=not prompt - ), patch("bittensor.extrinsics.senate.time.sleep"), 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", - ), - ), patch.object(mock_wallet, "is_senate_member", return_value=is_registered): - # Act - result = leave_senate_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert result == expected_result, f"Test ID: {test_id}" diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index 7aa3ebf5b..a57e32d01 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -1,35 +1,49 @@ -import pytest +# 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. from unittest.mock import MagicMock, patch -from bittensor.subtensor import Subtensor -from bittensor.wallet import wallet as Wallet -from bittensor.axon import axon as Axon -from bittensor.extrinsics.serving import ( - serve_extrinsic, - publish_metadata, - serve_axon_extrinsic, -) + +import pytest +from bittensor_wallet import Wallet + +from bittensor.core.axon import Axon +from bittensor.core.subtensor import Subtensor +from bittensor.core.extrinsics import serving @pytest.fixture -def mock_subtensor(): - mock_subtensor = MagicMock(spec=Subtensor) +def mock_subtensor(mocker): + mock_subtensor = mocker.MagicMock(spec=Subtensor) mock_subtensor.network = "test_network" - mock_subtensor.substrate = MagicMock() + mock_subtensor.substrate = mocker.MagicMock() return mock_subtensor @pytest.fixture -def mock_wallet(): - wallet = MagicMock(spec=Wallet) +def mock_wallet(mocker): + wallet = mocker.MagicMock(spec=Wallet) wallet.hotkey.ss58_address = "hotkey_address" wallet.coldkeypub.ss58_address = "coldkey_address" return wallet @pytest.fixture -def mock_axon(mock_wallet): - axon = MagicMock(spec=Axon) +def mock_axon(mock_wallet, mocker): + axon = mocker.MagicMock(spec=Axon) axon.wallet = mock_wallet() axon.external_port = 9221 return axon @@ -98,12 +112,16 @@ def test_serve_extrinsic_happy_path( prompt, expected, test_id, + mocker, ): # Arrange - mock_subtensor._do_serve_axon.return_value = (True, "") - with patch("bittensor.extrinsics.serving.Confirm.ask", return_value=True): + serving.do_serve_axon = mocker.MagicMock(return_value=(True, "")) + with patch( + "bittensor.core.extrinsics.serving.Confirm.ask", + return_value=True, + ): # Act - result = serve_extrinsic( + result = serving.serve_extrinsic( mock_subtensor, mock_wallet, ip, @@ -155,12 +173,16 @@ def test_serve_extrinsic_edge_cases( prompt, expected, test_id, + mocker, ): # Arrange - mock_subtensor._do_serve_axon.return_value = (True, "") - with patch("bittensor.extrinsics.serving.Confirm.ask", return_value=True): + serving.do_serve_axon = mocker.MagicMock(return_value=(True, "")) + with patch( + "bittensor.core.extrinsics.serving.Confirm.ask", + return_value=True, + ): # Act - result = serve_extrinsic( + result = serving.serve_extrinsic( mock_subtensor, mock_wallet, ip, @@ -212,12 +234,16 @@ def test_serve_extrinsic_error_cases( prompt, expected_error_message, test_id, + mocker, ): # Arrange - mock_subtensor._do_serve_axon.return_value = (False, "Error serving axon") - with patch("bittensor.extrinsics.serving.Confirm.ask", return_value=True): + serving.do_serve_axon = mocker.MagicMock(return_value=(False, "Error serving axon")) + with patch( + "bittensor.core.extrinsics.serving.Confirm.ask", + return_value=True, + ): # Act - result = serve_extrinsic( + result = serving.serve_extrinsic( mock_subtensor, mock_wallet, ip, @@ -277,6 +303,7 @@ def test_serve_axon_extrinsic( serve_success, expected_result, test_id, + mocker, ): mock_axon.external_ip = external_ip # Arrange @@ -285,27 +312,27 @@ def test_serve_axon_extrinsic( side_effect=Exception("Failed to fetch IP") if not external_ip_success else MagicMock(return_value="192.168.1.1"), - ), patch.object(mock_subtensor, "serve", return_value=serve_success): + ): + serving.do_serve_axon = mocker.MagicMock(return_value=(serve_success, "")) # Act if not external_ip_success: with pytest.raises(RuntimeError): - result = serve_axon_extrinsic( + serving.serve_axon_extrinsic( mock_subtensor, netuid, mock_axon, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - prompt=prompt, ) else: - result = serve_axon_extrinsic( + result = serving.serve_axon_extrinsic( mock_subtensor, netuid, mock_axon, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - prompt=prompt, ) + # Assert assert result == expected_result, f"Test ID: {test_id}" @@ -361,8 +388,8 @@ def test_publish_metadata( ), ): # Act - result = publish_metadata( - subtensor=mock_subtensor, + result = serving.publish_metadata( + self=mock_subtensor, wallet=mock_wallet, netuid=net_uid, data_type=type_u, diff --git a/tests/unit_tests/extrinsics/test_set_weights.py b/tests/unit_tests/extrinsics/test_set_weights.py index 68ce7acae..9c32fc9bd 100644 --- a/tests/unit_tests/extrinsics/test_set_weights.py +++ b/tests/unit_tests/extrinsics/test_set_weights.py @@ -1,20 +1,29 @@ -import torch -import pytest from unittest.mock import MagicMock, patch -from bittensor import subtensor, wallet -from bittensor.extrinsics.set_weights import set_weights_extrinsic + +import pytest +import torch +from bittensor_wallet import Wallet + +from bittensor.core import subtensor as subtensor_module +from bittensor.core.extrinsics.set_weights import ( + do_set_weights, + set_weights_extrinsic, +) +from bittensor.core.settings import version_as_int +from bittensor.core.subtensor import Subtensor @pytest.fixture def mock_subtensor(): - mock = MagicMock(spec=subtensor) + mock = MagicMock(spec=Subtensor) mock.network = "mock_network" + mock.substrate = MagicMock() return mock @pytest.fixture def mock_wallet(): - mock = MagicMock(spec=wallet) + mock = MagicMock(spec=Wallet) return mock @@ -43,7 +52,17 @@ def mock_wallet(): True, "Not waiting for finalization or inclusion.", ), - ([1, 2], [0.5, 0.5], 0, True, False, True, True, False, "Mock error message"), + ( + [1, 2], + [0.5, 0.5], + 0, + True, + False, + True, + True, + False, + "Subtensor returned `UnknownError(UnknownType)` error. This means: `Unknown Description`.", + ), ([1, 2], [0.5, 0.5], 0, True, True, True, False, False, "Prompt refused."), ], ids=[ @@ -71,9 +90,8 @@ def test_set_weights_extrinsic( with patch( "bittensor.utils.weight_utils.convert_weights_and_uids_for_emit", return_value=(uids_tensor, weights_tensor), - ), patch("rich.prompt.Confirm.ask", return_value=user_accepts), patch.object( - mock_subtensor, - "_do_set_weights", + ), patch("rich.prompt.Confirm.ask", return_value=user_accepts), patch( + "bittensor.core.extrinsics.set_weights.do_set_weights", return_value=(expected_success, "Mock error message"), ) as mock_do_set_weights: result, message = set_weights_extrinsic( @@ -92,6 +110,7 @@ def test_set_weights_extrinsic( assert message == expected_message, f"Test {expected_message} failed." if user_accepts is not False: mock_do_set_weights.assert_called_once_with( + self=mock_subtensor, wallet=mock_wallet, netuid=123, uids=uids_tensor, @@ -100,3 +119,160 @@ def test_set_weights_extrinsic( wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, ) + + +def test_do_set_weights_is_success(mock_subtensor, mocker): + """Successful _do_set_weights call.""" + # Prep + fake_wallet = mocker.MagicMock() + 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 = True + + # Call + result = do_set_weights( + self=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, + }, + ) + + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=mock_subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + era={"period": 5}, + ) + + mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( + mock_subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + mock_subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == (True, "Successfully set weights.") + + +def test_do_set_weights_is_not_success(mock_subtensor, mocker): + """Unsuccessful _do_set_weights call.""" + # Prep + fake_wallet = mocker.MagicMock() + 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( + self=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, + }, + ) + + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=mock_subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + era={"period": 5}, + ) + + mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( + mock_subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + mock_subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == ( + False, + mock_subtensor.substrate.submit_extrinsic.return_value.error_message, + ) + + +def test_do_set_weights_no_waits(mock_subtensor, mocker): + """Successful _do_set_weights call without wait flags for fake_wait_for_inclusion and fake_wait_for_finalization.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_uids = [1, 2, 3] + fake_vals = [4, 5, 6] + fake_netuid = 1 + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + + # Call + result = do_set_weights( + self=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, + }, + ) + + mock_subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=mock_subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + era={"period": 5}, + ) + + mock_subtensor.substrate.submit_extrinsic.assert_called_once_with( + mock_subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + 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 deleted file mode 100644 index c3b888520..000000000 --- a/tests/unit_tests/extrinsics/test_staking.py +++ /dev/null @@ -1,551 +0,0 @@ -import pytest -from unittest.mock import patch, MagicMock -import bittensor -from bittensor.utils.balance import Balance -from bittensor.extrinsics.staking import ( - add_stake_extrinsic, - add_stake_multiple_extrinsic, -) -from bittensor.errors import NotDelegateError - - -# Mocking external dependencies -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=bittensor.subtensor) - mock.network = "mock_network" - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=bittensor.wallet) - mock.hotkey.ss58_address = "5FHneW46..." - mock.coldkeypub.ss58_address = "5Gv8YYFu8..." - mock.hotkey_str = "mock_hotkey_str" - mock.name = "mock_wallet" - return mock - - -@pytest.fixture -def mock_other_owner_wallet(): - mock = MagicMock(spec=bittensor.wallet) - mock.hotkey.ss58_address = "11HneC46..." - mock.coldkeypub.ss58_address = "6Gv9ZZFu8..." - mock.hotkey_str = "mock_hotkey_str_other_owner" - mock.name = "mock_wallet_other_owner" - return mock - - -# Parametrized test cases -@pytest.mark.parametrize( - "hotkey_ss58, hotkey_owner, hotkey_delegate, amount, wait_for_inclusion, wait_for_finalization, prompt, user_accepts, expected_success, exception", - [ - # Simple staking to own hotkey, float - (None, True, None, 10.0, True, False, False, None, True, None), - # Simple staking to own hotkey, int - (None, True, None, 10, True, False, False, None, True, None), - # Not waiting for inclusion & finalization, own hotkey - ("5FHneW46...", True, None, 10.0, False, False, False, None, True, None), - # Prompt refused - (None, True, None, 10.0, True, False, True, False, False, None), - # Stake all - (None, True, None, None, True, False, False, None, True, None), - # Insufficient balance - (None, True, None, 110, True, False, False, None, False, None), - # No deduction scenario - (None, True, None, 0.000000100, True, False, False, None, True, None), - # Not owner but Delegate - ("5FHneW46...", False, True, 10.0, True, False, False, None, True, None), - # Not owner but Delegate and prompt refused - ("5FHneW46...", False, True, 10.0, True, False, True, False, False, None), - # Not owner and not delegate - ( - "5FHneW46...", - False, - False, - 10.0, - True, - False, - False, - None, - False, - NotDelegateError, - ), - # Staking failed - (None, True, None, 10.0, True, False, False, None, False, None), - ], - ids=[ - "success-own-hotkey-float", - "success-own-hotkey-int", - "success-own-hotkey-no-wait", - "prompt-refused", - "success-staking-all", - "failure-insufficient-balance", - "success-no-deduction", - "success-delegate", - "failure-delegate-prompt-refused", - "failure-not-delegate", - "failure-staking", - ], -) -def test_add_stake_extrinsic( - mock_subtensor, - mock_wallet, - mock_other_owner_wallet, - hotkey_ss58, - hotkey_owner, - hotkey_delegate, - amount, - wait_for_inclusion, - wait_for_finalization, - prompt, - user_accepts, - expected_success, - exception, -): - # Arrange - if not amount: - staking_balance = amount if amount else Balance.from_tao(100) - else: - staking_balance = ( - Balance.from_tao(amount) - if not isinstance(amount, bittensor.Balance) - else amount - ) - - with patch.object( - mock_subtensor, "_do_stake", return_value=expected_success - ) as mock_add_stake, patch.object( - mock_subtensor, "get_balance", return_value=Balance.from_tao(100) - ), patch.object( - mock_subtensor, - "get_stake_for_coldkey_and_hotkey", - return_value=Balance.from_tao(50), - ), patch.object( - mock_subtensor, - "get_hotkey_owner", - return_value=mock_wallet.coldkeypub.ss58_address - if hotkey_owner - else mock_other_owner_wallet.coldkeypub.ss58_address, - ), patch.object( - mock_subtensor, "is_hotkey_delegate", return_value=hotkey_delegate - ), patch.object(mock_subtensor, "get_delegate_take", return_value=0.01), patch( - "rich.prompt.Confirm.ask", return_value=user_accepts - ) as mock_confirm, patch.object( - mock_subtensor, - "get_minimum_required_stake", - return_value=bittensor.Balance.from_tao(0.01), - ), patch.object( - mock_subtensor, - "get_existential_deposit", - return_value=bittensor.Balance.from_rao(100_000), - ): - mock_balance = mock_subtensor.get_balance() - existential_deposit = mock_subtensor.get_existential_deposit() - if staking_balance > mock_balance - existential_deposit: - staking_balance = mock_balance - existential_deposit - - # Act - if not hotkey_owner and not hotkey_delegate: - with pytest.raises(exception): - result = add_stake_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - else: - result = add_stake_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert ( - result == expected_success - ), f"Expected {expected_success}, but got {result}" - - if prompt: - mock_confirm.assert_called_once() - - if expected_success: - if not hotkey_ss58: - hotkey_ss58 = mock_wallet.hotkey.ss58_address - - mock_add_stake.assert_called_once_with( - wallet=mock_wallet, - hotkey_ss58=hotkey_ss58, - amount=staking_balance, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -# Parametrized test cases -@pytest.mark.parametrize( - "hotkey_ss58s, amounts, hotkey_owner, hotkey_delegates ,wallet_balance, wait_for_inclusion, wait_for_finalization, prompt, prompt_response, stake_responses, expected_success, stake_attempted, exception, exception_msg", - [ - # Successful stake - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, True], - [None, None], - 100.0, - True, - False, - False, - None, - [True, True], - True, - 2, - None, - None, - ), - # Successful stake with prompt - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, True], - [None, None], - 100.0, - True, - True, - True, - True, - [True, True], - True, - 2, - None, - None, - ), - # Successful stake, no deduction scenario - ( - ["5FHneW46...", "11HneC46..."], - [0.000000100, 0.000000100], - [True, True], - [None, None], - 100.0, - True, - False, - False, - None, - [True, True], - True, - 2, - None, - None, - ), - # Successful stake, not waiting for finalization & inclusion - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, True], - [None, None], - 100.0, - False, - False, - False, - None, - [True, True], - True, - 2, - None, - None, - ), - # Successful stake, one key is a delegate - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, False], - [True, True], - 100.0, - True, - False, - False, - None, - [True, True], - True, - 2, - None, - None, - ), - # Partial successful stake, one key is not a delegate - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, False], - [True, False], - 100.0, - True, - False, - False, - None, - [True, False], - True, - 1, - None, - None, - ), - # Successful, staking all tao to first wallet, not waiting for finalization + inclusion - ( - ["5FHneW46...", "11HneC46..."], - None, - [True, True], - [None, None], - 100.0, - False, - False, - False, - None, - [True, False], - True, - 1, - None, - None, - ), - # Successful, staking all tao to first wallet - ( - ["5FHneW46...", "11HneC46..."], - None, - [True, True], - [None, None], - 100.0, - True, - False, - False, - None, - [True, False], - True, - 1, - None, - None, - ), - # Success, staking 0 tao - ( - ["5FHneW46...", "11HneC46..."], - [0.0, 0.0], - [True, True], - [None, None], - 100.0, - True, - False, - False, - None, - [None, None], - True, - 0, - None, - None, - ), - # Complete failure to stake for both keys - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, True], - [None, None], - 100.0, - True, - False, - False, - None, - [False, False], - False, - 2, - None, - None, - ), - # Complete failure, both keys are not delegates - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [False, False], - [False, False], - 100.0, - True, - False, - False, - None, - [False, False], - False, - 0, - None, - None, - ), - # Unsuccessful stake with prompt declined both times - ( - ["5FHneW46...", "11HneC46..."], - [10.0, 20.0], - [True, True], - [None, None], - 100.0, - True, - True, - True, - False, - [None, None], - False, - 0, - None, - None, - ), - # Exception, TypeError for incorrect hotkey_ss58s - ( - [123, "11HneC46..."], - [10.0, 20.0], - [False, False], - [False, False], - 100.0, - True, - False, - False, - None, - [None, None], - None, - 0, - TypeError, - "hotkey_ss58s must be a list of str", - ), - # Exception, ValueError for mismatch between hotkeys and amounts - ( - ["5FHneW46...", "11HneC46..."], - [10.0], - [False, False], - [False, False], - 100.0, - True, - False, - False, - None, - [None, None], - None, - 0, - ValueError, - "amounts must be a list of the same length as hotkey_ss58s", - ), - # Exception, TypeError for incorrect amounts - ( - ["5FHneW46...", "11HneC46..."], - ["abc", 12], - [False, False], - [False, False], - 100.0, - True, - False, - False, - None, - [None, None], - None, - 0, - TypeError, - "amounts must be a [list of bittensor.Balance or float] or None", - ), - ], - ids=[ - "success-basic-path", - "success-with-prompt", - "success-no-deduction", - "success-no-wait", - "success-one-delegate", - "partial-success-one-not-delegate", - "success-all-tao-no-wait", - "success-all-tao", - "success-0-tao", - "failure-both-keys", - "failure-both-not-delegates", - "failure-prompt-declined", - "failure-type-error-hotkeys", - "failure-value-error-amount", - "failure-type-error-amount", - ], -) -def test_add_stake_multiple_extrinsic( - mock_subtensor, - mock_wallet, - mock_other_owner_wallet, - hotkey_ss58s, - amounts, - hotkey_owner, - hotkey_delegates, - wallet_balance, - wait_for_inclusion, - wait_for_finalization, - prompt, - prompt_response, - stake_responses, - expected_success, - stake_attempted, - exception, - exception_msg, -): - # Arrange - def hotkey_delegate_side_effect(hotkey_ss58): - index = hotkey_ss58s.index(hotkey_ss58) - return hotkey_delegates[index] - - def owner_side_effect(hotkey_ss58): - index = hotkey_ss58s.index(hotkey_ss58) - return ( - mock_wallet.coldkeypub.ss58_address - if hotkey_owner[index] - else mock_other_owner_wallet.coldkeypub.ss58_address - ) - - def stake_side_effect(hotkey_ss58, *args, **kwargs): - index = hotkey_ss58s.index(hotkey_ss58) - return stake_responses[index] - - with patch.object( - mock_subtensor, "get_balance", return_value=Balance.from_tao(wallet_balance) - ), patch.object( - mock_subtensor, "is_hotkey_delegate", side_effect=hotkey_delegate_side_effect - ), patch.object( - mock_subtensor, "get_hotkey_owner", side_effect=owner_side_effect - ), patch.object( - mock_subtensor, "_do_stake", side_effect=stake_side_effect - ) as mock_do_stake, patch.object( - mock_subtensor, "tx_rate_limit", return_value=0 - ), patch("rich.prompt.Confirm.ask", return_value=prompt_response) as mock_confirm: - # Act - if exception: - with pytest.raises(exception) as exc_info: - result = add_stake_multiple_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - # Assert - assert str(exc_info.value) == exception_msg - - # Act - else: - result = add_stake_multiple_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert ( - result == expected_success - ), f"Expected {expected_success}, but got {result}" - if prompt: - assert mock_confirm.called - assert mock_do_stake.call_count == stake_attempted diff --git a/tests/unit_tests/extrinsics/test_transfer.py b/tests/unit_tests/extrinsics/test_transfer.py new file mode 100644 index 000000000..af59d5769 --- /dev/null +++ b/tests/unit_tests/extrinsics/test_transfer.py @@ -0,0 +1,142 @@ +import pytest + +from bittensor.core import subtensor as subtensor_module +from bittensor.core.extrinsics.transfer import do_transfer +from bittensor.core.subtensor import Subtensor +from bittensor.utils.balance import Balance + + +@pytest.fixture +def subtensor(mocker): + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) + return Subtensor() + + +def test_do_transfer_is_success_true(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = True + + # Call + result = do_transfer( + subtensor, + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization, + ) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + 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( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == ( + True, + subtensor.substrate.submit_extrinsic.return_value.block_hash, + None, + ) + + +def test_do_transfer_is_success_false(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + 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_transfer( + subtensor, + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization, + ) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + 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( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + + assert result == ( + False, + None, + subtensor.substrate.submit_extrinsic.return_value.error_message, + ) + + +def test_do_transfer_no_waits(subtensor, mocker): + """Successful do_transfer call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_transfer_balance = Balance(1) + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + + # Call + result = do_transfer( + subtensor, + fake_wallet, + fake_dest, + fake_transfer_balance, + fake_wait_for_inclusion, + fake_wait_for_finalization, + ) + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + 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( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == (True, None, None) diff --git a/tests/unit_tests/extrinsics/test_unstaking.py b/tests/unit_tests/extrinsics/test_unstaking.py deleted file mode 100644 index 0fa6ba84c..000000000 --- a/tests/unit_tests/extrinsics/test_unstaking.py +++ /dev/null @@ -1,332 +0,0 @@ -import bittensor -import pytest - -from unittest.mock import patch, MagicMock - -from bittensor.utils.balance import Balance -from bittensor.extrinsics.unstaking import unstake_extrinsic, unstake_multiple_extrinsic - - -@pytest.fixture -def mock_subtensor(): - mock = MagicMock(spec=bittensor.subtensor) - mock.network = "mock_network" - return mock - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock(spec=bittensor.wallet) - mock.hotkey.ss58_address = "5FHneW46..." - mock.coldkeypub.ss58_address = "5Gv8YYFu8..." - mock.hotkey_str = "mock_hotkey_str" - return mock - - -def mock_get_minimum_required_stake(): - # Valid minimum threshold as of 2024/05/01 - return Balance.from_rao(100_000_000) - - -@pytest.mark.parametrize( - "hotkey_ss58, amount, wait_for_inclusion, wait_for_finalization, prompt, user_accepts, expected_success, unstake_attempted", - [ - # Successful unstake without waiting for inclusion or finalization - (None, 10.0, False, False, False, None, True, True), - # Successful unstake with prompt accepted - ("5FHneW46...", 10.0, True, True, True, True, True, True), - # Prompt declined - ("5FHneW46...", 10.0, True, True, True, False, False, False), - # Not enough stake to unstake - ("5FHneW46...", 1000.0, True, True, False, None, False, False), - # Successful - unstake threshold not reached - (None, 0.01, True, True, False, None, True, True), - # Successful unstaking all - (None, None, False, False, False, None, True, True), - # Failure - unstaking failed - (None, 10.0, False, False, False, None, False, True), - ], - ids=[ - "successful-no-wait", - "successful-with-prompt", - "failure-prompt-declined", - "failure-not-enough-stake", - "success-threshold-not-reached", - "success-unstake-all", - "failure-unstake-failed", - ], -) -def test_unstake_extrinsic( - mock_subtensor, - mock_wallet, - hotkey_ss58, - amount, - wait_for_inclusion, - wait_for_finalization, - prompt, - user_accepts, - expected_success, - unstake_attempted, -): - mock_current_stake = Balance.from_tao(50) - mock_current_balance = Balance.from_tao(100) - - with patch.object( - mock_subtensor, "_do_unstake", return_value=(expected_success) - ), patch.object( - mock_subtensor, "get_balance", return_value=mock_current_balance - ), patch.object( - mock_subtensor, - "get_minimum_required_stake", - side_effect=mock_get_minimum_required_stake, - ), patch.object( - mock_subtensor, - "get_stake_for_coldkey_and_hotkey", - return_value=mock_current_stake, - ), patch("rich.prompt.Confirm.ask", return_value=user_accepts) as mock_confirm: - result = unstake_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58=hotkey_ss58, - amount=amount, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - assert ( - result == expected_success - ), f"Expected result {expected_success}, but got {result}" - - if prompt: - mock_confirm.assert_called_once() - - if unstake_attempted: - mock_subtensor._do_unstake.assert_called_once_with( - wallet=mock_wallet, - hotkey_ss58=hotkey_ss58 or mock_wallet.hotkey.ss58_address, - amount=bittensor.Balance.from_tao(amount) - if amount - else mock_current_stake, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - else: - mock_subtensor._do_unstake.assert_not_called() - - -@pytest.mark.parametrize( - # TODO: Write dynamic test to test for amount = None with multiple hotkeys - "hotkey_ss58s, amounts, wallet_balance, wait_for_inclusion, wait_for_finalization, prompt, prompt_response, unstake_responses, expected_success, unstake_attempted, exception, exception_msg", - [ - # Successful unstake - no wait - ( - ["5FHneW46...", "5FHneW47..."], - [10.0, 20.0], - 100, - False, - False, - True, - True, - [True, True], - True, - 2, - None, - None, - ), - # Partial-success unstake - one unstake fails - ( - ["5FHneW46...", "5FHneW47..."], - [10.0, 20.0], - 100, - True, - False, - True, - True, - [True, False], - True, - 2, - None, - None, - ), - # Success, based on no hotkeys - func to be confirmed - ([], [], 100, True, True, False, None, [None], True, 0, None, None), - # Unsuccessful unstake - not enough stake - ( - ["5FHneW46..."], - [1000.0], - 100, - True, - True, - False, - True, - [None], - False, - 0, - None, - None, - ), - # Successful unstake - new stake below threshold - ( - ["5FHneW46..."], - [ - 100 - mock_get_minimum_required_stake() + 0.01 - ], # New stake just below threshold - 100, - True, - True, - False, - True, - [True], - True, # Sucessful unstake - 1, - None, - None, - ), - # Unsuccessful unstake with prompt declined both times - ( - ["5FHneW46...", "5FHneW48..."], - [10.0, 10.0], - 100, - True, - True, - True, - False, - [None, None], - False, - 0, - None, - None, - ), - # Exception, TypeError for incorrect hotkey_ss58s - ( - ["5FHneW46...", 123], - [10.0, 20.0], - 100, - True, - False, - False, - None, - [None, None], - None, - 0, - TypeError, - "hotkey_ss58s must be a list of str", - ), - # Exception, ValueError for mismatch between hotkeys and amounts - ( - ["5FHneW46...", "5FHneW48..."], - [10.0], - 100, - True, - False, - False, - None, - [None, None], - None, - 0, - ValueError, - "amounts must be a list of the same length as hotkey_ss58s", - ), - # Exception, TypeError for incorrect amounts - ( - ["5FHneW46...", "5FHneW48..."], - [10.0, "tao"], - 100, - True, - False, - False, - None, - [None, None], - None, - 0, - TypeError, - "amounts must be a [list of bittensor.Balance or float] or None", - ), - ], - ids=[ - "success-no-wait", - "partial-success-one-fail", - "success-no-hotkey", - "failure-not-enough-stake", - "success-threshold-not-reached", - "failure-prompt-declined", - "failure-type-error-hotkeys", - "failure-value-error-amounts", - "failure-type-error-amounts", - ], -) -def test_unstake_multiple_extrinsic( - mock_subtensor, - mock_wallet, - hotkey_ss58s, - amounts, - wallet_balance, - wait_for_inclusion, - wait_for_finalization, - prompt, - prompt_response, - unstake_responses, - expected_success, - unstake_attempted, - exception, - exception_msg, -): - # Arrange - mock_current_stake = Balance.from_tao(100) - amounts_in_balances = [ - Balance.from_tao(amount) if isinstance(amount, float) else amount - for amount in amounts - ] - - def unstake_side_effect(hotkey_ss58, *args, **kwargs): - index = hotkey_ss58s.index(hotkey_ss58) - return unstake_responses[index] - - with patch.object( - mock_subtensor, "_do_unstake", side_effect=unstake_side_effect - ) as mock_unstake, patch.object( - mock_subtensor, - "get_minimum_required_stake", - side_effect=mock_get_minimum_required_stake, - ), patch.object( - mock_subtensor, "get_balance", return_value=Balance.from_tao(wallet_balance) - ), patch.object(mock_subtensor, "tx_rate_limit", return_value=0), patch.object( - mock_subtensor, - "get_stake_for_coldkey_and_hotkey", - return_value=mock_current_stake, - ), patch("rich.prompt.Confirm.ask", return_value=prompt_response) as mock_confirm: - # Act - if exception: - with pytest.raises(exception) as exc_info: - result = unstake_multiple_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - # Assert - assert str(exc_info.value) == exception_msg - - # Act - else: - result = unstake_multiple_extrinsic( - subtensor=mock_subtensor, - wallet=mock_wallet, - hotkey_ss58s=hotkey_ss58s, - amounts=amounts_in_balances, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - prompt=prompt, - ) - - # Assert - assert ( - result == expected_success - ), f"Expected {expected_success}, but got {result}" - if prompt: - assert mock_confirm.called - assert mock_unstake.call_count == unstake_attempted diff --git a/tests/unit_tests/factories/neuron_factory.py b/tests/unit_tests/factories/neuron_factory.py index 4ad70c5dc..f99a084ac 100644 --- a/tests/unit_tests/factories/neuron_factory.py +++ b/tests/unit_tests/factories/neuron_factory.py @@ -1,6 +1,6 @@ import factory -from bittensor.chain_data import AxonInfo, NeuronInfoLite, PrometheusInfo +from bittensor.core.chain_data import AxonInfo, NeuronInfoLite, PrometheusInfo from bittensor.utils.balance import Balance diff --git a/tests/unit_tests/test_axon.py b/tests/unit_tests/test_axon.py index 7ba433a15..689d21737 100644 --- a/tests/unit_tests/test_axon.py +++ b/tests/unit_tests/test_axon.py @@ -1,16 +1,14 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 @@ -18,64 +16,66 @@ # DEALINGS IN THE SOFTWARE. -# Standard Lib import re import time from dataclasses import dataclass - -from typing import Any, Optional +from typing import Any, Optional, Tuple from unittest import IsolatedAsyncioTestCase from unittest.mock import AsyncMock, MagicMock, patch -# Third Party import fastapi import netaddr import pydantic import pytest -from starlette.requests import Request from fastapi.testclient import TestClient +from starlette.requests import Request -# Bittensor -import bittensor -from bittensor import Synapse, RunException, StreamingSynapse -from bittensor.axon import AxonMiddleware -from bittensor.axon import axon as Axon -from bittensor.utils.axon_utils import allowed_nonce_window_ns, calculate_diff_seconds -from bittensor.constants import ALLOWED_DELTA, NANOSECONDS_IN_SECOND +from bittensor.core.axon import AxonMiddleware, Axon +from bittensor.core.errors import RunException +from bittensor.core.settings import version_as_int +from bittensor.core.stream import StreamingSynapse +from bittensor.core.synapse import Synapse +from bittensor.core.threadpool import PriorityThreadPoolExecutor +from bittensor.utils.axon_utils import ( + allowed_nonce_window_ns, + calculate_diff_seconds, + ALLOWED_DELTA, + NANOSECONDS_IN_SECOND, +) -def test_attach(): +def test_attach_initial(): # Create a mock AxonServer instance - server = bittensor.axon() + server = Axon() # Define the Synapse type - class Synapse(bittensor.Synapse): + class TestSynapse(Synapse): pass # Define the functions with the correct signatures - def forward_fn(synapse: Synapse) -> Any: + def forward_fn(synapse: TestSynapse) -> Any: pass - def blacklist_fn(synapse: Synapse) -> bool: - return True + def blacklist_fn(synapse: TestSynapse) -> Tuple[bool, str]: + return True, "" - def priority_fn(synapse: Synapse) -> float: + def priority_fn(synapse: TestSynapse) -> float: return 1.0 - def verify_fn(synapse: Synapse) -> None: + def verify_fn(synapse: TestSynapse) -> None: pass # Test attaching with correct signatures server.attach(forward_fn, blacklist_fn, priority_fn, verify_fn) # Define functions with incorrect signatures - def wrong_blacklist_fn(synapse: Synapse) -> int: + def wrong_blacklist_fn(synapse: TestSynapse) -> int: return 1 - def wrong_priority_fn(synapse: Synapse) -> int: + def wrong_priority_fn(synapse: TestSynapse) -> int: return 1 - def wrong_verify_fn(synapse: Synapse) -> bool: + def wrong_verify_fn(synapse: TestSynapse) -> bool: return True # Test attaching with incorrect signatures @@ -91,14 +91,14 @@ def wrong_verify_fn(synapse: Synapse) -> bool: def test_attach(): # Create a mock AxonServer instance - server = bittensor.axon() + server = Axon() # Define the Synapse type - class Synapse: + class FakeSynapse: pass # Define a class that inherits from Synapse - class InheritedSynapse(bittensor.Synapse): + class InheritedSynapse(Synapse): pass # Define a function with the correct signature @@ -122,7 +122,7 @@ def wrong_forward_fn(synapse: NonInheritedSynapse) -> Any: def test_log_and_handle_error(): - from bittensor.axon import log_and_handle_error + from bittensor.core.axon import log_and_handle_error synapse = SynapseMock() @@ -133,7 +133,7 @@ def test_log_and_handle_error(): def test_create_error_response(): - from bittensor.axon import create_error_response + from bittensor.core.axon import create_error_response synapse = SynapseMock() synapse.axon.status_code = 500 @@ -200,10 +200,10 @@ def __init__(self): self.priority_fns = {} self.forward_fns = {} self.verify_fns = {} - self.thread_pool = bittensor.PriorityThreadPoolExecutor(max_workers=1) + self.thread_pool = PriorityThreadPoolExecutor(max_workers=1) -class SynapseMock(bittensor.Synapse): +class SynapseMock(Synapse): pass @@ -474,7 +474,7 @@ def setUp(self): self.mock_axon = MagicMock() self.mock_axon.uuid = "1234" self.mock_axon.forward_class_types = { - "request_name": bittensor.Synapse, + "request_name": Synapse, } self.mock_axon.wallet.hotkey.sign.return_value = bytes.fromhex("aabbccdd") # Create an instance of AxonMiddleware @@ -493,7 +493,7 @@ async def test_preprocess(self): synapse = await self.axon_middleware.preprocess(request) # Check if the preprocess function fills the axon information into the synapse - assert synapse.axon.version == str(bittensor.__version_as_int__) + assert synapse.axon.version == str(version_as_int) assert synapse.axon.uuid == "1234" assert synapse.axon.nonce is not None assert synapse.axon.status_message is None @@ -582,7 +582,7 @@ async def test_unknown_path(self, http_client): ) async def test_ping__no_dendrite(self, http_client): - response = http_client.post_synapse(bittensor.Synapse()) + response = http_client.post_synapse(Synapse()) assert (response.status_code, response.json()) == ( 401, { @@ -737,7 +737,7 @@ def test_nonce_within_allowed_window(nonce_offset_seconds, expected_result): [ None, fastapi.Response, - bittensor.StreamingSynapse, + StreamingSynapse, ], ) async def test_streaming_synapse( diff --git a/tests/unit_tests/test_chain_data.py b/tests/unit_tests/test_chain_data.py index a6474bbee..353f697d4 100644 --- a/tests/unit_tests/test_chain_data.py +++ b/tests/unit_tests/test_chain_data.py @@ -1,9 +1,26 @@ +# 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 pytest -import bittensor import torch -from bittensor.chain_data import AxonInfo, ChainDataType, DelegateInfo, NeuronInfo -SS58_FORMAT = bittensor.__ss58_format__ +from bittensor.core.chain_data import AxonInfo, DelegateInfo +from bittensor.core.chain_data.utils import ChainDataType + RAOPERTAO = 10**18 @@ -224,7 +241,7 @@ def test_to_parameter_dict(axon_info, test_case): def test_to_parameter_dict_torch( axon_info, test_case, - force_legacy_torch_compat_api, + force_legacy_torch_compatible_api, ): result = axon_info.to_parameter_dict() @@ -294,7 +311,7 @@ def test_from_parameter_dict(parameter_dict, expected, test_case): ], ) def test_from_parameter_dict_torch( - parameter_dict, expected, test_case, force_legacy_torch_compat_api + parameter_dict, expected, test_case, force_legacy_torch_compatible_api ): # Act result = AxonInfo.from_parameter_dict(parameter_dict) @@ -349,175 +366,16 @@ def create_neuron_info_decoded( } -@pytest.mark.parametrize( - "test_id, neuron_info_decoded,", - [ - ( - "happy-path-1", - create_neuron_info_decoded( - hotkey=b"\x01" * 32, - coldkey=b"\x02" * 32, - stake=[(b"\x02" * 32, 1000)], - weights=[(1, 2)], - bonds=[(3, 4)], - rank=100, - emission=1000, - incentive=200, - consensus=300, - trust=400, - validator_trust=500, - dividends=600, - uid=1, - netuid=2, - active=True, - last_update=1000, - validator_permit=100, - pruning_score=1000, - prometheus_info={ - "version": 1, - "ip": 2130706433, - "port": 8080, - "ip_type": 4, - "block": 100, - }, - axon_info={ - "version": 1, - "ip": 2130706433, - "port": 8080, - "ip_type": 4, - }, - ), - ), - ], -) -def test_fix_decoded_values_happy_path(test_id, neuron_info_decoded): - # Act - result = NeuronInfo.fix_decoded_values(neuron_info_decoded) - - # Assert - assert result.hotkey == neuron_info_decoded["hotkey"], f"Test case: {test_id}" - assert result.coldkey == neuron_info_decoded["coldkey"], f"Test case: {test_id}" - assert result.stake == neuron_info_decoded["stake"], f"Test case: {test_id}" - assert result.weights == neuron_info_decoded["weights"], f"Test case: {test_id}" - assert result.bonds == neuron_info_decoded["bonds"], f"Test case: {test_id}" - assert result.rank == neuron_info_decoded["rank"], f"Test case: {test_id}" - assert result.emission == neuron_info_decoded["emission"], f"Test case: {test_id}" - assert result.incentive == neuron_info_decoded["incentive"], f"Test case: {test_id}" - assert result.consensus == neuron_info_decoded["consensus"], f"Test case: {test_id}" - assert result.trust == neuron_info_decoded["trust"], f"Test case: {test_id}" - assert ( - result.validator_trust == neuron_info_decoded["validator_trust"] - ), f"Test case: {test_id}" - assert result.dividends == neuron_info_decoded["dividends"], f"Test case: {test_id}" - assert result.uid == neuron_info_decoded["uid"], f"Test case: {test_id}" - assert result.netuid == neuron_info_decoded["netuid"], f"Test case: {test_id}" - assert result.active == neuron_info_decoded["active"], f"Test case: {test_id}" - assert ( - result.last_update == neuron_info_decoded["last_update"] - ), f"Test case: {test_id}" - - -@pytest.mark.parametrize( - "test_id, neuron_info_decoded", - [ - ( - "edge-1", - create_neuron_info_decoded( - hotkey=b"\x01" * 32, - coldkey=b"\x02" * 32, - stake=[], - weights=[(1, 2)], - bonds=[(3, 4)], - rank=100, - emission=1000, - incentive=200, - consensus=300, - trust=400, - validator_trust=500, - dividends=600, - uid=1, - netuid=2, - active=True, - last_update=1000, - validator_permit=100, - pruning_score=1000, - prometheus_info={ - "version": 1, - "ip": 2130706433, - "port": 8080, - "ip_type": 4, - "block": 100, - }, - axon_info={ - "version": 1, - "ip": 2130706433, - "port": 8080, - "ip_type": 4, - }, - ), - ), - ], -) -def test_fix_decoded_values_edge_cases(test_id, neuron_info_decoded): - # Act - result = NeuronInfo.fix_decoded_values(neuron_info_decoded) - - # Assert - assert result.stake == 0, f"Test case: {test_id}" - assert result.weights == neuron_info_decoded["weights"], f"Test case: {test_id}" - - -@pytest.mark.parametrize( - "test_id, neuron_info_decoded, expected_exception", - [ - ( - "error-1", - create_neuron_info_decoded( - hotkey="not_bytes", - coldkey=b"\x02" * 32, - stake=[(b"\x02" * 32, 1000)], - weights=[(1, 2)], - bonds=[(3, 4)], - rank=100, - emission=1000, - incentive=200, - consensus=300, - trust=400, - validator_trust=500, - dividends=600, - uid=1, - netuid=2, - active=True, - last_update=1000, - validator_permit=100, - pruning_score=1000, - prometheus_info={}, - axon_info={}, - ), - ValueError, - ), - ], -) -def test_fix_decoded_values_error_cases( - test_id, neuron_info_decoded, expected_exception -): - # Arrange - # (Omitted since all input values are provided via test parameters) - - # Act / Assert - with pytest.raises(expected_exception): - NeuronInfo.fix_decoded_values(neuron_info_decoded), f"Test case: {test_id}" - - @pytest.fixture def mock_from_scale_encoding(mocker): - return mocker.patch("bittensor.chain_data.from_scale_encoding") + return mocker.patch("bittensor.core.chain_data.delegate_info.from_scale_encoding") @pytest.fixture def mock_fix_decoded_values(mocker): return mocker.patch( - "bittensor.DelegateInfo.fix_decoded_values", side_effect=lambda x: x + "bittensor.core.chain_data.DelegateInfo.fix_decoded_values", + side_effect=lambda x: x, ) diff --git a/tests/unit_tests/test_dendrite.py b/tests/unit_tests/test_dendrite.py index 0146bb778..3150aaf64 100644 --- a/tests/unit_tests/test_dendrite.py +++ b/tests/unit_tests/test_dendrite.py @@ -17,24 +17,26 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -# Standard Lib import asyncio import typing from unittest.mock import MagicMock, Mock -# Third Party import aiohttp import pytest -# Application -import bittensor -from bittensor.constants import DENDRITE_ERROR_MAPPING, DENDRITE_DEFAULT_ERROR -from bittensor.dendrite import dendrite as Dendrite -from bittensor.synapse import TerminalInfo -from tests.helpers import _get_mock_wallet +from bittensor.core.axon import Axon +from bittensor.core.dendrite import ( + DENDRITE_ERROR_MAPPING, + DENDRITE_DEFAULT_ERROR, + Dendrite, +) +from bittensor.core.synapse import TerminalInfo +from tests.helpers import get_mock_wallet +from bittensor.core.synapse import Synapse +from bittensor.core.chain_data import AxonInfo -class SynapseDummy(bittensor.Synapse): +class SynapseDummy(Synapse): input: int output: typing.Optional[int] = None @@ -46,10 +48,9 @@ def dummy(synapse: SynapseDummy) -> SynapseDummy: @pytest.fixture def setup_dendrite(): - user_wallet = ( - _get_mock_wallet() - ) # assuming bittensor.wallet() returns a wallet object - dendrite_obj = bittensor.dendrite(user_wallet) + # Assuming bittensor.Wallet() returns a wallet object + user_wallet = get_mock_wallet() + dendrite_obj = Dendrite(user_wallet) return dendrite_obj @@ -60,7 +61,7 @@ def dendrite_obj(setup_dendrite): @pytest.fixture def axon_info(): - return bittensor.AxonInfo( + return AxonInfo( version=1, ip="127.0.0.1", port=666, @@ -72,7 +73,7 @@ def axon_info(): @pytest.fixture(scope="session") def setup_axon(): - axon = bittensor.axon() + axon = Axon() axon.attach(forward_fn=dummy) axon.start() yield axon @@ -81,17 +82,17 @@ def setup_axon(): def test_init(setup_dendrite): dendrite_obj = setup_dendrite - assert isinstance(dendrite_obj, bittensor.dendrite) + assert isinstance(dendrite_obj, Dendrite) assert dendrite_obj.keypair == setup_dendrite.keypair def test_str(dendrite_obj): - expected_string = "dendrite({})".format(dendrite_obj.keypair.ss58_address) + expected_string = f"dendrite({dendrite_obj.keypair.ss58_address})" assert str(dendrite_obj) == expected_string def test_repr(dendrite_obj): - expected_string = "dendrite({})".format(dendrite_obj.keypair.ss58_address) + expected_string = f"dendrite({dendrite_obj.keypair.ss58_address})" assert repr(dendrite_obj) == expected_string @@ -108,7 +109,7 @@ async def test_aclose(dendrite_obj, setup_axon): axon = setup_axon # Use context manager to open an async session async with dendrite_obj: - resp = await dendrite_obj([axon], SynapseDummy(input=1), deserialize=False) + await dendrite_obj([axon], SynapseDummy(input=1), deserialize=False) # Close should automatically be called on the session after context manager scope assert dendrite_obj._session is None @@ -127,16 +128,16 @@ def __await__(self): def test_dendrite_create_wallet(): - d = bittensor.dendrite(_get_mock_wallet()) - d = bittensor.dendrite(_get_mock_wallet().hotkey) - d = bittensor.dendrite(_get_mock_wallet().coldkeypub) + d = Dendrite(get_mock_wallet()) + d = Dendrite(get_mock_wallet().hotkey) + d = Dendrite(get_mock_wallet().coldkeypub) assert d.__str__() == d.__repr__() @pytest.mark.asyncio async def test_forward_many(): n = 10 - d = bittensor.dendrite(wallet=_get_mock_wallet()) + d = Dendrite(wallet=get_mock_wallet()) d.call = AsyncMock() axons = [MagicMock() for _ in range(n)] @@ -152,10 +153,10 @@ async def test_forward_many(): def test_pre_process_synapse(): - d = bittensor.dendrite(wallet=_get_mock_wallet()) - s = bittensor.Synapse() + d = Dendrite(wallet=get_mock_wallet()) + s = Synapse() synapse = d.preprocess_synapse_for_request( - target_axon_info=bittensor.axon(wallet=_get_mock_wallet()).info(), + target_axon_info=Axon(wallet=get_mock_wallet()).info(), synapse=s, timeout=12, ) @@ -296,7 +297,7 @@ def test_terminal_info_error_cases( @pytest.mark.asyncio async def test_dendrite__call__success_response( - axon_info, dendrite_obj, mock_aioresponse + axon_info, dendrite_obj, mock_aio_response ): input_synapse = SynapseDummy(input=1) expected_synapse = SynapseDummy( @@ -312,7 +313,7 @@ async def test_dendrite__call__success_response( ) ) ) - mock_aioresponse.post( + mock_aio_response.post( f"http://127.0.0.1:666/SynapseDummy", body=expected_synapse.json(), ) @@ -327,13 +328,13 @@ async def test_dendrite__call__success_response( @pytest.mark.asyncio async def test_dendrite__call__handles_http_error_response( - axon_info, dendrite_obj, mock_aioresponse + axon_info, dendrite_obj, mock_aio_response ): status_code = 414 message = "Custom Error" - mock_aioresponse.post( - f"http://127.0.0.1:666/SynapseDummy", + mock_aio_response.post( + "http://127.0.0.1:666/SynapseDummy", status=status_code, payload={"message": message}, ) diff --git a/tests/unit_tests/test_deprecated.py b/tests/unit_tests/test_deprecated.py new file mode 100644 index 000000000..c4b906a0c --- /dev/null +++ b/tests/unit_tests/test_deprecated.py @@ -0,0 +1,51 @@ +# 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 sys + + +def test_mock_import(): + """ + Tests that `bittensor.mock` can be imported and is the same as `bittensor.utils.mock`. + """ + import bittensor.mock as redirected_mock + import bittensor.utils.mock as real_mock + + assert "bittensor.mock" in sys.modules + assert redirected_mock is real_mock + + +def test_extrinsics_import(): + """Tests that `bittensor.extrinsics` can be imported and is the same as `bittensor.utils.deprecated.extrinsics`.""" + import bittensor.extrinsics as redirected_extrinsics + import bittensor.core.extrinsics as real_extrinsics + + assert "bittensor.extrinsics" in sys.modules + assert redirected_extrinsics is real_extrinsics + + +def test_object_aliases_are_correctly_mapped(): + """Ensures all object aliases correctly map to their respective classes in Bittensor package.""" + import bittensor + + assert issubclass(bittensor.axon, bittensor.Axon) + assert issubclass(bittensor.config, bittensor.Config) + assert issubclass(bittensor.dendrite, bittensor.Dendrite) + assert issubclass(bittensor.keyfile, bittensor.Keyfile) + assert issubclass(bittensor.metagraph, bittensor.Metagraph) + assert issubclass(bittensor.wallet, bittensor.Wallet) + assert issubclass(bittensor.synapse, bittensor.Synapse) diff --git a/tests/unit_tests/test_keyfile.py b/tests/unit_tests/test_keyfile.py deleted file mode 100644 index 0f3b69cac..000000000 --- a/tests/unit_tests/test_keyfile.py +++ /dev/null @@ -1,643 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2022 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 os -import json -import time -import pytest -import shutil -import bittensor -import unittest.mock as mock -from scalecodec import ScaleBytes -from substrateinterface import Keypair, KeypairType -from substrateinterface.constants import DEV_PHRASE -from substrateinterface.exceptions import ConfigurationError -from bip39 import bip39_validate - -from bittensor import get_coldkey_password_from_environment - - -def test_generate_mnemonic(): - """ - Test the generation of a mnemonic and its validation. - """ - mnemonic = Keypair.generate_mnemonic() - assert bip39_validate(mnemonic) == True - - -def test_invalid_mnemonic(): - """ - Test the validation of an invalid mnemonic. - """ - mnemonic = "This is an invalid mnemonic" - assert bip39_validate(mnemonic) == False - - -def test_create_sr25519_keypair(): - """ - Test the creation of a sr25519 keypair from a mnemonic and verify the SS58 address. - """ - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - keypair = Keypair.create_from_mnemonic(mnemonic, ss58_format=0) - assert keypair.ss58_address == "16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2" - - -def test_only_provide_ss58_address(): - """ - Test the creation of a keypair with only the SS58 address provided. - """ - keypair = Keypair(ss58_address="16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2") - - assert ( - f"0x{keypair.public_key.hex()}" - == "0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446" - ) - - -def test_only_provide_public_key(): - """ - Test the creation of a keypair with only the public key provided. - """ - keypair = Keypair( - public_key="0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446", - ss58_format=0, - ) - - assert keypair.ss58_address == "16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2" - - -def test_provide_no_ss58_address_and_public_key(): - """ - Test the creation of a keypair without providing SS58 address and public key. - """ - with pytest.raises(ValueError): - Keypair() - - -def test_incorrect_private_key_length_sr25519(): - """ - Test the creation of a keypair with an incorrect private key length for sr25519. - """ - with pytest.raises(ValueError): - Keypair( - private_key="0x23", - ss58_address="16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2", - ) - - -def test_incorrect_public_key(): - """ - Test the creation of a keypair with an incorrect public key. - """ - with pytest.raises(ValueError): - Keypair(public_key="0x23") - - -def test_sign_and_verify(): - """ - Test the signing and verification of a message using a keypair. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("Test1231223123123") - assert keypair.verify("Test1231223123123", signature) == True - - -def test_sign_and_verify_hex_data(): - """ - Test the signing and verification of hex data using a keypair. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("0x1234") - assert keypair.verify("0x1234", signature) == True - - -def test_sign_and_verify_scale_bytes(): - """ - Test the signing and verification of ScaleBytes data using a keypair. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - data = ScaleBytes("0x1234") - signature = keypair.sign(data) - assert keypair.verify(data, signature) == True - - -def test_sign_missing_private_key(): - """ - Test signing a message with a keypair that is missing the private key. - """ - keypair = Keypair(ss58_address="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY") - with pytest.raises(ConfigurationError): - keypair.sign("0x1234") - - -def test_sign_unsupported_crypto_type(): - """ - Test signing a message with an unsupported crypto type. - """ - keypair = Keypair.create_from_private_key( - ss58_address="16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2", - private_key="0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e", - crypto_type=3, - ) - with pytest.raises(ConfigurationError): - keypair.sign("0x1234") - - -def test_verify_unsupported_crypto_type(): - """ - Test verifying a signature with an unsupported crypto type. - """ - keypair = Keypair.create_from_private_key( - ss58_address="16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2", - private_key="0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e", - crypto_type=3, - ) - with pytest.raises(ConfigurationError): - keypair.verify("0x1234", "0x1234") - - -def test_sign_and_verify_incorrect_signature(): - """ - Test verifying an incorrect signature for a signed message. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = "0x4c291bfb0bb9c1274e86d4b666d13b2ac99a0bacc04a4846fb8ea50bda114677f83c1f164af58fc184451e5140cc8160c4de626163b11451d3bbb208a1889f8a" - assert keypair.verify("Test1231223123123", signature) == False - - -def test_sign_and_verify_invalid_signature(): - """ - Test verifying an invalid signature format for a signed message. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = "Test" - with pytest.raises(TypeError): - keypair.verify("Test1231223123123", signature) - - -def test_sign_and_verify_invalid_message(): - """ - Test verifying a signature against an incorrect message. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic) - signature = keypair.sign("Test1231223123123") - assert keypair.verify("OtherMessage", signature) == False - - -def test_create_ed25519_keypair(): - """ - Test the creation of an ed25519 keypair from a mnemonic and verify the SS58 address. - """ - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - keypair = Keypair.create_from_mnemonic( - mnemonic, ss58_format=0, crypto_type=KeypairType.ED25519 - ) - assert keypair.ss58_address == "16dYRUXznyhvWHS1ktUENGfNAEjCawyDzHRtN9AdFnJRc38h" - - -def test_sign_and_verify_ed25519(): - """ - Test the signing and verification of a message using an ed25519 keypair. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=KeypairType.ED25519) - signature = keypair.sign("Test1231223123123") - assert keypair.verify("Test1231223123123", signature) == True - - -def test_sign_and_verify_invalid_signature_ed25519(): - """ - Test verifying an incorrect signature for a message signed with an ed25519 keypair. - """ - mnemonic = Keypair.generate_mnemonic() - keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=KeypairType.ED25519) - signature = "0x4c291bfb0bb9c1274e86d4b666d13b2ac99a0bacc04a4846fb8ea50bda114677f83c1f164af58fc184451e5140cc8160c4de626163b11451d3bbb208a1889f8a" - assert keypair.verify("Test1231223123123", signature) == False - - -def test_unsupport_crypto_type(): - """ - Test creating a keypair with an unsupported crypto type. - """ - with pytest.raises(ValueError): - Keypair.create_from_seed( - seed_hex="0xda3cf5b1e9144931?a0f0db65664aab662673b099415a7f8121b7245fb0be4143", - crypto_type=2, - ) - - -def test_create_keypair_from_private_key(): - """ - Test creating a keypair from a private key and verify the public key. - """ - keypair = Keypair.create_from_private_key( - ss58_address="16ADqpMa4yzfmWs3nuTSMhfZ2ckeGtvqhPWCNqECEGDcGgU2", - private_key="0x1f1995bdf3a17b60626a26cfe6f564b337d46056b7a1281b64c649d592ccda0a9cffd34d9fb01cae1fba61aeed184c817442a2186d5172416729a4b54dd4b84e", - ) - assert ( - f"0x{keypair.public_key.hex()}" - == "0xe4359ad3e2716c539a1d663ebd0a51bdc5c98a12e663bb4c4402db47828c9446" - ) - - -def test_hdkd_hard_path(): - """ - Test hierarchical deterministic key derivation with a hard derivation path. - """ - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - derivation_address = "5FEiH8iuDUw271xbqWTWuB6WrDjv5dnCeDX1CyHubAniXDNN" - derivation_path = "//Alice" - derived_keypair = Keypair.create_from_uri(mnemonic + derivation_path) - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_soft_path(): - """ - Test hierarchical deterministic key derivation with a soft derivation path. - """ - derivation_address = "5GNXbA46ma5dg19GXdiKi5JH3mnkZ8Yea3bBtZAvj7t99P9i" - mnemonic = "old leopard transfer rib spatial phone calm indicate online fire caution review" - derived_keypair = Keypair.create_from_uri(f"{mnemonic}/Alice") - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_default_to_dev_mnemonic(): - """ - Test hierarchical deterministic key derivation with a default development mnemonic. - """ - derivation_address = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" - derivation_path = "//Alice" - derived_keypair = Keypair.create_from_uri(derivation_path) - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_nested_hard_soft_path(): - """ - Test hierarchical deterministic key derivation with a nested hard and soft derivation path. - """ - derivation_address = "5CJGwWiKXSE16WJaxBdPZhWqUYkotgenLUALv7ZvqQ4TXeqf" - derivation_path = "//Bob/test" - derived_keypair = Keypair.create_from_uri(derivation_path) - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_nested_soft_hard_path(): - """ - Test hierarchical deterministic key derivation with a nested soft and hard derivation path. - """ - derivation_address = "5Cwc8tShrshDJUp1P1M21dKUTcYQpV9GcfSa4hUBNmMdV3Cx" - derivation_path = "/Bob//test" - derived_keypair = Keypair.create_from_uri(derivation_path) - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_path_gt_32_bytes(): - """ - Test hierarchical deterministic key derivation with a derivation path longer than 32 bytes. - """ - derivation_address = "5GR5pfZeNs1uQiSWVxZaQiZou3wdZiX894eqgvfNfHbEh7W2" - derivation_path = "//PathNameLongerThan32BytesWhichShouldBeHashed" - derived_keypair = Keypair.create_from_uri(derivation_path) - assert derivation_address == derived_keypair.ss58_address - - -def test_hdkd_unsupported_password(): - """ - Test hierarchical deterministic key derivation with an unsupported password. - """ - - with pytest.raises(NotImplementedError): - Keypair.create_from_uri(f"{DEV_PHRASE}///test") - - -def create_keyfile(root_path): - """ - Creates a keyfile object with two keypairs: alice and bob. - - Args: - root_path (str): The root path for the keyfile. - - Returns: - bittensor.keyfile: The created keyfile object. - """ - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - - mnemonic = bittensor.Keypair.generate_mnemonic(12) - alice = bittensor.Keypair.create_from_mnemonic(mnemonic) - keyfile.set_keypair( - alice, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - - bob = bittensor.Keypair.create_from_uri("/Bob") - keyfile.set_keypair( - bob, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - - return keyfile - - -@pytest.fixture(scope="session") -def keyfile_setup_teardown(): - root_path = f"/tmp/pytest{time.time()}" - os.makedirs(root_path, exist_ok=True) - - create_keyfile(root_path) - - yield root_path - - shutil.rmtree(root_path) - - -def test_create(keyfile_setup_teardown): - """ - Test case for creating a keyfile and performing various operations on it. - """ - root_path = keyfile_setup_teardown - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - - mnemonic = bittensor.Keypair.generate_mnemonic(12) - alice = bittensor.Keypair.create_from_mnemonic(mnemonic) - keyfile.set_keypair( - alice, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - assert keyfile.is_readable() - assert keyfile.is_writable() - assert keyfile.is_encrypted() - keyfile.decrypt(password="thisisafakepassword") - assert not keyfile.is_encrypted() - keyfile.encrypt(password="thisisafakepassword") - assert keyfile.is_encrypted() - str(keyfile) - keyfile.decrypt(password="thisisafakepassword") - assert not keyfile.is_encrypted() - str(keyfile) - - assert ( - keyfile.get_keypair(password="thisisafakepassword").ss58_address - == alice.ss58_address - ) - assert ( - keyfile.get_keypair(password="thisisafakepassword").private_key - == alice.private_key - ) - assert ( - keyfile.get_keypair(password="thisisafakepassword").public_key - == alice.public_key - ) - - bob = bittensor.Keypair.create_from_uri("/Bob") - keyfile.set_keypair( - bob, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - assert ( - keyfile.get_keypair(password="thisisafakepassword").ss58_address - == bob.ss58_address - ) - assert ( - keyfile.get_keypair(password="thisisafakepassword").public_key == bob.public_key - ) - - repr(keyfile) - - -def test_legacy_coldkey(keyfile_setup_teardown): - """ - Test case for legacy cold keyfile. - """ - root_path = keyfile_setup_teardown - legacy_filename = os.path.join(root_path, "coldlegacy_keyfile") - keyfile = bittensor.keyfile(path=legacy_filename) - keyfile.make_dirs() - keyfile_data = b"0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - with open(legacy_filename, "wb") as keyfile_obj: - keyfile_obj.write(keyfile_data) - assert keyfile.keyfile_data == keyfile_data - keyfile.encrypt(password="this is the fake password") - keyfile.decrypt(password="this is the fake password") - expected_decryption = { - "accountId": "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f", - "publicKey": "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f", - "privateKey": None, - "secretPhrase": None, - "secretSeed": None, - "ss58Address": "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm", - } - for key, value in expected_decryption.items(): - value_str = f'"{value}"' if value is not None else "null" - assert f'"{key}": {value_str}'.encode() in keyfile.keyfile_data - - assert ( - keyfile.get_keypair().ss58_address - == "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - ) - assert ( - f"0x{keyfile.get_keypair().public_key.hex()}" - == "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - ) - - -def test_validate_password(): - """ - Test case for the validate_password function. - - This function tests the behavior of the validate_password function from the bittensor.keyfile module. - It checks various scenarios to ensure that the function correctly validates passwords. - """ - from bittensor.keyfile import validate_password - - assert validate_password(None) == False - assert validate_password("passw0rd") == False - assert validate_password("123456789") == False - with mock.patch("getpass.getpass", return_value="biTTensor"): - assert validate_password("biTTensor") == True - with mock.patch("getpass.getpass", return_value="biTTenso"): - assert validate_password("biTTensor") == False - - -def test_decrypt_keyfile_data_legacy(): - """ - Test case for decrypting legacy keyfile data. - - This test case verifies that the `decrypt_keyfile_data` function correctly decrypts - encrypted data using a legacy encryption scheme. - - The test generates a key using a password and encrypts a sample data. Then, it decrypts - the encrypted data using the same password and asserts that the decrypted data matches - the original data. - """ - import base64 - - from cryptography.fernet import Fernet - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC - - from bittensor.keyfile import decrypt_keyfile_data - - __SALT = b"Iguesscyborgslikemyselfhaveatendencytobeparanoidaboutourorigins" - - def __generate_key(password): - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - salt=__SALT, - length=32, - iterations=10000000, - backend=default_backend(), - ) - key = base64.urlsafe_b64encode(kdf.derive(password.encode())) - return key - - pw = "fakepasssword238947239" - data = b"encrypt me!" - key = __generate_key(pw) - cipher_suite = Fernet(key) - encrypted_data = cipher_suite.encrypt(data) - - decrypted_data = decrypt_keyfile_data(encrypted_data, pw) - assert decrypted_data == data - - -def test_user_interface(): - """ - Test the user interface for asking password to encrypt. - - This test case uses the `ask_password_to_encrypt` function from the `bittensor.keyfile` module. - It mocks the `getpass.getpass` function to simulate user input of passwords. - The expected result is that the `ask_password_to_encrypt` function returns the correct password. - """ - from bittensor.keyfile import ask_password_to_encrypt - - with mock.patch( - "getpass.getpass", - side_effect=["pass", "password", "asdury3294y", "asdury3294y"], - ): - assert ask_password_to_encrypt() == "asdury3294y" - - -def test_overwriting(keyfile_setup_teardown): - """ - Test case for overwriting a keypair in the keyfile. - """ - root_path = keyfile_setup_teardown - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - alice = bittensor.Keypair.create_from_uri("/Alice") - keyfile.set_keypair( - alice, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - bob = bittensor.Keypair.create_from_uri("/Bob") - - with pytest.raises(bittensor.KeyFileError) as pytest_wrapped_e: - with mock.patch("builtins.input", return_value="n"): - keyfile.set_keypair( - bob, encrypt=True, overwrite=False, password="thisisafakepassword" - ) - - -def test_serialized_keypair_to_keyfile_data(keyfile_setup_teardown): - """ - Test case for serializing a keypair to keyfile data. - - This test case verifies that the `serialized_keypair_to_keyfile_data` function correctly - serializes a keypair to keyfile data. It then deserializes the keyfile data and asserts - that the deserialized keypair matches the original keypair. - """ - from bittensor.keyfile import serialized_keypair_to_keyfile_data - - root_path = keyfile_setup_teardown - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - - mnemonic = bittensor.Keypair.generate_mnemonic(12) - keypair = bittensor.Keypair.create_from_mnemonic(mnemonic) - - keyfile.set_keypair( - keypair, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - keypair_data = serialized_keypair_to_keyfile_data(keypair) - decoded_keypair_data = json.loads(keypair_data.decode()) - - assert decoded_keypair_data["secretPhrase"] == keypair.mnemonic - assert decoded_keypair_data["ss58Address"] == keypair.ss58_address - assert decoded_keypair_data["publicKey"] == f"0x{keypair.public_key.hex()}" - assert decoded_keypair_data["accountId"] == f"0x{keypair.public_key.hex()}" - - -def test_deserialize_keypair_from_keyfile_data(keyfile_setup_teardown): - """ - Test case for deserializing a keypair from keyfile data. - - This test case verifies that the `deserialize_keypair_from_keyfile_data` function correctly - deserializes keyfile data to a keypair. It first serializes a keypair to keyfile data and - then deserializes the keyfile data to a keypair. It then asserts that the deserialized keypair - matches the original keypair. - """ - from bittensor.keyfile import serialized_keypair_to_keyfile_data - from bittensor.keyfile import deserialize_keypair_from_keyfile_data - - root_path = keyfile_setup_teardown - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - - mnemonic = bittensor.Keypair.generate_mnemonic(12) - keypair = bittensor.Keypair.create_from_mnemonic(mnemonic) - - keyfile.set_keypair( - keypair, encrypt=True, overwrite=True, password="thisisafakepassword" - ) - keypair_data = serialized_keypair_to_keyfile_data(keypair) - deserialized_keypair = deserialize_keypair_from_keyfile_data(keypair_data) - - assert deserialized_keypair.ss58_address == keypair.ss58_address - assert deserialized_keypair.public_key == keypair.public_key - assert deserialized_keypair.private_key == keypair.private_key - - -def test_get_coldkey_password_from_environment(monkeypatch): - password_by_wallet = { - "WALLET": "password", - "my_wallet": "password2", - "my-wallet": "password2", - } - - monkeypatch.setenv("bt_cold_pw_wallet", password_by_wallet["WALLET"]) - monkeypatch.setenv("BT_COLD_PW_My_Wallet", password_by_wallet["my_wallet"]) - - for wallet, password in password_by_wallet.items(): - assert get_coldkey_password_from_environment(wallet) == password - - assert get_coldkey_password_from_environment("non_existent_wallet") is None - - -def test_keyfile_error_incorrect_password(keyfile_setup_teardown): - """ - Test case for attempting to decrypt a keyfile with an incorrect password. - """ - root_path = keyfile_setup_teardown - keyfile = bittensor.keyfile(path=os.path.join(root_path, "keyfile")) - - # Ensure the keyfile is encrypted - assert keyfile.is_encrypted() - - # Attempt to decrypt with an incorrect password - with pytest.raises(bittensor.KeyFileError) as excinfo: - keyfile.get_keypair(password="incorrect_password") - - assert "Invalid password" in str(excinfo.value) diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 1822fc86e..2aa43d1e7 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -1,10 +1,15 @@ -import pytest -import multiprocessing import logging as stdlogging +import multiprocessing from unittest.mock import MagicMock, patch -from bittensor.btlogging import LoggingMachine -from bittensor.btlogging.defines import DEFAULT_LOG_FILE_NAME, BITTENSOR_LOGGER_NAME -from bittensor.btlogging.loggingmachine import LoggingConfig + +import pytest + +from bittensor.utils.btlogging import LoggingMachine +from bittensor.utils.btlogging.defines import ( + DEFAULT_LOG_FILE_NAME, + BITTENSOR_LOGGER_NAME, +) +from bittensor.utils.btlogging.loggingmachine import LoggingConfig, _concat_message @pytest.fixture(autouse=True, scope="session") @@ -44,7 +49,7 @@ def mock_config(tmp_path): def logging_machine(mock_config): config, _ = mock_config logging_machine = LoggingMachine(config=config) - yield logging_machine + return logging_machine def test_initialization(logging_machine, mock_config): @@ -75,7 +80,9 @@ def test_state_transitions(logging_machine, mock_config): Test state transitions and the associated logging level changes. """ config, log_file_path = mock_config - with patch("bittensor.btlogging.loggingmachine.all_loggers") as mocked_all_loggers: + with patch( + "bittensor.utils.btlogging.loggingmachine.all_loggers" + ) as mocked_all_loggers: # mock the main bittensor logger, identified by its `name` field mocked_bt_logger = MagicMock() mocked_bt_logger.name = BITTENSOR_LOGGER_NAME @@ -112,7 +119,7 @@ def test_state_transitions(logging_machine, mock_config): logging_machine.enable_default() assert logging_machine.current_state_value == "Default" # main logger set to INFO - mocked_bt_logger.setLevel.assert_called_with(stdlogging.INFO) + mocked_bt_logger.setLevel.assert_called_with(stdlogging.WARNING) # 3rd party loggers should be disabled by setting to CRITICAL mocked_third_party_logger.setLevel.assert_called_with(stdlogging.CRITICAL) @@ -168,3 +175,25 @@ def test_all_log_levels_output(logging_machine, caplog): assert "Test warning" in caplog.text assert "Test error" in caplog.text assert "Test critical" in caplog.text + + +@pytest.mark.parametrize( + "msg, prefix, suffix, expected_result", + [ + ("msg", "", "", "msg"), + ("msg", None, None, "msg"), + ("msg", "prefix", None, "prefix - msg"), + ("msg", None, "suffix", "msg - suffix"), + ("msg", "prefix", "suffix", "prefix - msg - suffix"), + ], + ids=[ + "message, no prefix (str), no suffix (str)", + "message, no prefix (None), no suffix (None)", + "message and prefix only", + "message and suffix only", + "message, prefix, and suffix", + ], +) +def test_concat(msg, prefix, suffix, expected_result): + """Test different options of message concatenation with prefix and suffix.""" + assert _concat_message(msg, prefix, suffix) == expected_result diff --git a/tests/unit_tests/test_metagraph.py b/tests/unit_tests/test_metagraph.py index 40303297a..e4dca70a1 100644 --- a/tests/unit_tests/test_metagraph.py +++ b/tests/unit_tests/test_metagraph.py @@ -1,27 +1,28 @@ # The MIT License (MIT) -# Copyright © 2023 Opentensor Technologies Inc - +# 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. +from unittest.mock import MagicMock from unittest.mock import Mock -import pytest + import numpy as np -import bittensor +import pytest -from bittensor.metagraph import metagraph as Metagraph -from unittest.mock import MagicMock +from bittensor.core import settings +from bittensor.core.metagraph import Metagraph @pytest.fixture @@ -57,7 +58,7 @@ def mock_environment(): def test_set_metagraph_attributes(mock_environment): subtensor, neurons = mock_environment - metagraph = bittensor.metagraph(1, sync=False) + metagraph = Metagraph(1, sync=False) metagraph.neurons = neurons metagraph._set_metagraph_attributes(block=5, subtensor=subtensor) @@ -95,7 +96,7 @@ def test_set_metagraph_attributes(mock_environment): def test_process_weights_or_bonds(mock_environment): _, neurons = mock_environment - metagraph = bittensor.metagraph(1, sync=False) + metagraph = Metagraph(1, sync=False) metagraph.neurons = neurons # Test weights processing @@ -124,42 +125,11 @@ def test_process_weights_or_bonds(mock_environment): # TODO: Add more checks to ensure the bonds have been processed correctly -def test_process_weights_or_bonds_torch( - mock_environment, force_legacy_torch_compat_api -): - _, neurons = mock_environment - metagraph = bittensor.metagraph(1, sync=False) - metagraph.neurons = neurons - - # Test weights processing - weights = metagraph._process_weights_or_bonds( - data=[neuron.weights for neuron in neurons], attribute="weights" - ) - assert weights.shape[0] == len( - neurons - ) # Number of rows should be equal to number of neurons - assert weights.shape[1] == len( - neurons - ) # Number of columns should be equal to number of neurons - # TODO: Add more checks to ensure the weights have been processed correctly - - # Test bonds processing - bonds = metagraph._process_weights_or_bonds( - data=[neuron.bonds for neuron in neurons], attribute="bonds" - ) - assert bonds.shape[0] == len( - neurons - ) # Number of rows should be equal to number of neurons - assert bonds.shape[1] == len( - neurons - ) # Number of columns should be equal to number of neurons - - -# Mocking the bittensor.subtensor class for testing purposes +# Mocking the bittensor.Subtensor class for testing purposes @pytest.fixture def mock_subtensor(): subtensor = MagicMock() - subtensor.chain_endpoint = bittensor.__finney_entrypoint__ + subtensor.chain_endpoint = settings.FINNEY_ENTRYPOINT subtensor.network = "finney" subtensor.get_current_block.return_value = 601 return subtensor diff --git a/tests/unit_tests/test_overview.py b/tests/unit_tests/test_overview.py deleted file mode 100644 index 638ab4df4..000000000 --- a/tests/unit_tests/test_overview.py +++ /dev/null @@ -1,266 +0,0 @@ -# Standard Lib -from copy import deepcopy -from unittest.mock import MagicMock, patch - -# Pytest -import pytest - -# Bittensor -import bittensor -from bittensor.commands.overview import OverviewCommand -from tests.unit_tests.factories.neuron_factory import NeuronInfoLiteFactory - - -@pytest.fixture -def mock_subtensor(): - mock = MagicMock() - mock.get_balance = MagicMock(return_value=100) - return mock - - -def fake_config(**kwargs): - config = deepcopy(construct_config()) - for key, value in kwargs.items(): - setattr(config, key, value) - return config - - -def construct_config(): - parser = bittensor.cli.__create_parser__() - defaults = bittensor.config(parser=parser, args=[]) - # Parse commands and subcommands - for command in bittensor.ALL_COMMANDS: - if ( - command in bittensor.ALL_COMMANDS - and "commands" in bittensor.ALL_COMMANDS[command] - ): - for subcommand in bittensor.ALL_COMMANDS[command]["commands"]: - defaults.merge( - bittensor.config(parser=parser, args=[command, subcommand]) - ) - else: - defaults.merge(bittensor.config(parser=parser, args=[command])) - - defaults.netuid = 1 - # Always use mock subtensor. - defaults.subtensor.network = "finney" - # Skip version checking. - defaults.no_version_checking = True - - return defaults - - -@pytest.fixture -def mock_wallet(): - mock = MagicMock() - mock.coldkeypub_file.exists_on_device = MagicMock(return_value=True) - mock.coldkeypub_file.is_encrypted = MagicMock(return_value=False) - mock.coldkeypub.ss58_address = "fake_address" - return mock - - -class MockHotkey: - def __init__(self, hotkey_str): - self.hotkey_str = hotkey_str - - -class MockCli: - def __init__(self, config): - self.config = config - - -@pytest.mark.parametrize( - "config_all, exists_on_device, is_encrypted, expected_balance, test_id", - [ - (True, True, False, 100, "happy_path_all_wallets"), - (False, True, False, 100, "happy_path_single_wallet"), - (True, False, False, 0, "edge_case_no_wallets_found"), - (True, True, True, 0, "edge_case_encrypted_wallet"), - ], -) -def test_get_total_balance( - mock_subtensor, - mock_wallet, - config_all, - exists_on_device, - is_encrypted, - expected_balance, - test_id, -): - # Arrange - cli = MockCli(fake_config(all=config_all)) - mock_wallet.coldkeypub_file.exists_on_device.return_value = exists_on_device - mock_wallet.coldkeypub_file.is_encrypted.return_value = is_encrypted - - with patch( - "bittensor.wallet", return_value=mock_wallet - ) as mock_wallet_constructor, patch( - "bittensor.commands.overview.get_coldkey_wallets_for_path", - return_value=[mock_wallet] if config_all else [], - ), patch( - "bittensor.commands.overview.get_all_wallets_for_path", - return_value=[mock_wallet], - ), patch( - "bittensor.commands.overview.get_hotkey_wallets_for_wallet", - return_value=[mock_wallet], - ): - # Act - result_hotkeys, result_balance = OverviewCommand._get_total_balance( - 0, mock_subtensor, cli - ) - - # Assert - assert result_balance == expected_balance, f"Test ID: {test_id}" - assert all( - isinstance(hotkey, MagicMock) for hotkey in result_hotkeys - ), f"Test ID: {test_id}" - - -@pytest.mark.parametrize( - "config, all_hotkeys, expected_result, test_id", - [ - # Happy path tests - ( - {"all_hotkeys": False, "hotkeys": ["abc123", "xyz456"]}, - [MockHotkey("abc123"), MockHotkey("xyz456"), MockHotkey("mno567")], - ["abc123", "xyz456"], - "test_happy_path_included", - ), - ( - {"all_hotkeys": True, "hotkeys": ["abc123", "xyz456"]}, - [MockHotkey("abc123"), MockHotkey("xyz456"), MockHotkey("mno567")], - ["mno567"], - "test_happy_path_excluded", - ), - # Edge cases - ( - {"all_hotkeys": False, "hotkeys": []}, - [MockHotkey("abc123"), MockHotkey("xyz456")], - [], - "test_edge_no_hotkeys_specified", - ), - ( - {"all_hotkeys": True, "hotkeys": []}, - [MockHotkey("abc123"), MockHotkey("xyz456")], - ["abc123", "xyz456"], - "test_edge_all_hotkeys_excluded", - ), - ( - {"all_hotkeys": False, "hotkeys": ["abc123", "xyz456"]}, - [], - [], - "test_edge_no_hotkeys_available", - ), - ( - {"all_hotkeys": True, "hotkeys": ["abc123", "xyz456"]}, - [], - [], - "test_edge_no_hotkeys_available_excluded", - ), - ], -) -def test_get_hotkeys(config, all_hotkeys, expected_result, test_id): - # Arrange - cli = MockCli( - fake_config( - hotkeys=config.get("hotkeys"), all_hotkeys=config.get("all_hotkeys") - ) - ) - - # Act - result = OverviewCommand._get_hotkeys(cli, all_hotkeys) - - # Assert - assert [ - hotkey.hotkey_str for hotkey in result - ] == expected_result, f"Failed {test_id}" - - -def test_get_hotkeys_error(): - # Arrange - cli = MockCli(fake_config(hotkeys=["abc123", "xyz456"], all_hotkeys=False)) - all_hotkeys = None - - # Act - with pytest.raises(TypeError): - OverviewCommand._get_hotkeys(cli, all_hotkeys) - - -@pytest.fixture -def neuron_info(): - return [ - (1, [NeuronInfoLiteFactory(netuid=1)], None), - (2, [NeuronInfoLiteFactory(netuid=2)], None), - ] - - -@pytest.fixture -def neurons_dict(): - return { - "1": [NeuronInfoLiteFactory(netuid=1)], - "2": [NeuronInfoLiteFactory(netuid=2)], - } - - -@pytest.fixture -def netuids_list(): - return [1, 2] - - -# Test cases -@pytest.mark.parametrize( - "test_id, results, expected_neurons, expected_netuids", - [ - # Test ID: 01 - Happy path, all neurons processed correctly - ( - "01", - [ - (1, [NeuronInfoLiteFactory(netuid=1)], None), - (2, [NeuronInfoLiteFactory(netuid=2)], None), - ], - { - "1": [NeuronInfoLiteFactory(netuid=1)], - "2": [NeuronInfoLiteFactory(netuid=2)], - }, - [1, 2], - ), - # Test ID: 02 - Error message present, should skip processing for that netuid - ( - "02", - [ - (1, [NeuronInfoLiteFactory(netuid=1)], None), - (2, [], "Error fetching data"), - ], - {"1": [NeuronInfoLiteFactory()]}, - [1], - ), - # Test ID: 03 - No neurons found for a netuid, should remove the netuid - ( - "03", - [(1, [NeuronInfoLiteFactory()], None), (2, [], None)], - {"1": [NeuronInfoLiteFactory()]}, - [1], - ), - # Test ID: 04 - Mixed conditions - ( - "04", - [ - (1, [NeuronInfoLiteFactory(netuid=1)], None), - (2, [], None), - ], - {"1": [NeuronInfoLiteFactory()]}, - [1], - ), - ], -) -def test_process_neuron_results( - test_id, results, expected_neurons, expected_netuids, neurons_dict, netuids_list -): - # Act - actual_neurons = OverviewCommand._process_neuron_results( - results, neurons_dict, netuids_list - ) - - # Assert - assert actual_neurons.keys() == expected_neurons.keys(), f"Failed test {test_id}" - assert netuids_list == expected_netuids, f"Failed test {test_id}" diff --git a/tests/unit_tests/test_subnets.py b/tests/unit_tests/test_subnets.py new file mode 100644 index 000000000..9cec02e93 --- /dev/null +++ b/tests/unit_tests/test_subnets.py @@ -0,0 +1,82 @@ +import pytest +from mpmath.ctx_mp_python import return_mpc + +from bittensor.utils import subnets + + +class MySubnetsAPI(subnets.SubnetsAPI): + """Example of user class inherited from SubnetsAPI.""" + + def prepare_synapse(self, *args, **kwargs): + """Prepare the synapse-specific payload.""" + + def process_responses(self, responses): + """Process the responses from the network.""" + return responses + + +def test_instance_creation(mocker): + """Test the creation of a MySubnetsAPI instance.""" + # Prep + mocked_dendrite = mocker.patch.object(subnets, "Dendrite") + fake_wallet = mocker.MagicMock() + + # Call + instance = MySubnetsAPI(fake_wallet) + + # Asserts + assert isinstance(instance, subnets.SubnetsAPI) + mocked_dendrite.assert_called_once_with(wallet=fake_wallet) + assert instance.dendrite == mocked_dendrite.return_value + assert instance.wallet == fake_wallet + + +@pytest.mark.asyncio +async def test_query_api(mocker): + """Test querying the MySubnetsAPI instance asynchronously.""" + # Prep + mocked_async_dendrite = mocker.AsyncMock() + mocked_dendrite = mocker.patch.object( + subnets, "Dendrite", return_value=mocked_async_dendrite + ) + + fake_wallet = mocker.MagicMock() + fake_axon = mocker.MagicMock() + + mocked_synapse = mocker.MagicMock() + mocked_synapse.return_value.name = "test synapse" + mocked_prepare_synapse = mocker.patch.object( + MySubnetsAPI, "prepare_synapse", return_value=mocked_synapse + ) + + # Call + instance = MySubnetsAPI(fake_wallet) + result = await instance.query_api(fake_axon, **{"key": "val"}) + + # Asserts + mocked_prepare_synapse.assert_called_once_with(key="val") + mocked_dendrite.assert_called_once_with(wallet=fake_wallet) + assert result == mocked_async_dendrite.return_value + + +@pytest.mark.asyncio +async def test_test_instance_call(mocker): + """Test the MySubnetsAPI instance call with asynchronous handling.""" + # Prep + mocked_async_dendrite = mocker.AsyncMock() + mocked_dendrite = mocker.patch.object( + subnets, "Dendrite", return_value=mocked_async_dendrite + ) + mocked_query_api = mocker.patch.object( + MySubnetsAPI, "query_api", new=mocker.AsyncMock() + ) + fake_wallet = mocker.MagicMock() + fake_axon = mocker.MagicMock() + + # Call + instance = MySubnetsAPI(fake_wallet) + await instance(fake_axon) + + # Asserts + mocked_dendrite.assert_called_once_with(wallet=fake_wallet) + mocked_query_api.assert_called_once_with(fake_axon) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 89060734b..d0783d20f 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -1,41 +1,33 @@ # The MIT License (MIT) -# Copyright © 2022 Opentensor Foundation - +# 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. -# Standard Lib import argparse -import dataclasses import unittest.mock as mock -from typing import Any from unittest.mock import MagicMock -# 3rd Party import pytest - -# Application -import bittensor -from bittensor import subtensor_module -from bittensor.chain_data import SubnetHyperparameters -from bittensor.commands import utils -from bittensor.commands.utils import normalize_hyperparameters -from bittensor.subtensor import ( - Subtensor, - _logger, -) -from bittensor.utils import wallet_utils +from bittensor_wallet import Wallet + +from bittensor.core import subtensor as subtensor_module, settings +from bittensor.core.axon import Axon +from bittensor.core.chain_data import SubnetHyperparameters +from bittensor.core.settings import version_as_int +from bittensor.core.subtensor import Subtensor, logging +from bittensor.utils import u16_normalized_float, u64_normalized_float from bittensor.utils.balance import Balance U16_MAX = 65535 @@ -48,11 +40,10 @@ def test_serve_axon_with_external_ip_set(): mock_serve_axon = MagicMock(return_value=True) - mock_subtensor = MagicMock(spec=bittensor.subtensor, serve_axon=mock_serve_axon) + mock_subtensor = MagicMock(spec=Subtensor, serve_axon=mock_serve_axon) - mock_add_insecure_port = mock.MagicMock(return_value=None) mock_wallet = MagicMock( - spec=bittensor.wallet, + spec=Wallet, coldkey=MagicMock(), coldkeypub=MagicMock( # mock ss58 address @@ -63,8 +54,8 @@ def test_serve_axon_with_external_ip_set(): ), ) - mock_config = bittensor.axon.config() - mock_axon_with_external_ip_set = bittensor.axon( + mock_config = Axon.config() + mock_axon_with_external_ip_set = Axon( wallet=mock_wallet, ip=internal_ip, external_ip=external_ip, @@ -95,13 +86,13 @@ def test_serve_axon_with_external_port_set(): mock_serve_axon = MagicMock(return_value=True) mock_subtensor = MagicMock( - spec=bittensor.subtensor, + spec=Subtensor, serve=mock_serve, serve_axon=mock_serve_axon, ) mock_wallet = MagicMock( - spec=bittensor.wallet, + spec=Wallet, coldkey=MagicMock(), coldkeypub=MagicMock( # mock ss58 address @@ -112,9 +103,9 @@ def test_serve_axon_with_external_port_set(): ), ) - mock_config = bittensor.axon.config() + mock_config = Axon.config() - mock_axon_with_external_port_set = bittensor.axon( + mock_axon_with_external_port_set = Axon( wallet=mock_wallet, port=internal_port, external_port=external_port, @@ -143,58 +134,6 @@ class ExitEarly(Exception): pass -def test_stake_multiple(): - mock_amount: bittensor.Balance = bittensor.Balance.from_tao(1.0) - - mock_wallet = MagicMock( - spec=bittensor.wallet, - coldkey=MagicMock(), - coldkeypub=MagicMock( - # mock ss58 address - ss58_address="5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - ), - hotkey=MagicMock( - ss58_address="5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg" - ), - ) - - mock_hotkey_ss58s = ["5CtstubuSoVLJGCXkiWRNKrrGg2DVBZ9qMs2qYTLsZR4q1Wg"] - - mock_amounts = [mock_amount] # more than 1000 RAO - - mock_neuron = MagicMock( - is_null=False, - ) - - mock_do_stake = MagicMock(side_effect=ExitEarly) - - mock_subtensor = MagicMock( - spec=bittensor.subtensor, - network="mock_net", - get_balance=MagicMock( - return_value=bittensor.Balance.from_tao(mock_amount.tao + 20.0) - ), # enough balance to stake - get_neuron_for_pubkey_and_subnet=MagicMock(return_value=mock_neuron), - _do_stake=mock_do_stake, - ) - - with pytest.raises(ExitEarly): - bittensor.subtensor.add_stake_multiple( - mock_subtensor, - wallet=mock_wallet, - hotkey_ss58s=mock_hotkey_ss58s, - amounts=mock_amounts, - ) - - mock_do_stake.assert_called_once() - # args, kwargs - _, kwargs = mock_do_stake.call_args - - assert kwargs["amount"] == pytest.approx( - mock_amount.rao, rel=1e9 - ) # delta of 1.0 TAO - - @pytest.mark.parametrize( "test_id, expected_output", [ @@ -233,46 +172,46 @@ def mock_add_argument(*args, **kwargs): "network, expected_network, expected_endpoint", [ # Happy path tests - ("finney", "finney", bittensor.__finney_entrypoint__), - ("local", "local", bittensor.__local_entrypoint__), - ("test", "test", bittensor.__finney_test_entrypoint__), - ("archive", "archive", bittensor.__archive_entrypoint__), + ("finney", "finney", settings.FINNEY_ENTRYPOINT), + ("local", "local", settings.LOCAL_ENTRYPOINT), + ("test", "test", settings.FINNEY_TEST_ENTRYPOINT), + ("archive", "archive", settings.ARCHIVE_ENTRYPOINT), # Endpoint override tests ( - bittensor.__finney_entrypoint__, + settings.FINNEY_ENTRYPOINT, "finney", - bittensor.__finney_entrypoint__, + settings.FINNEY_ENTRYPOINT, ), ( "entrypoint-finney.opentensor.ai", "finney", - bittensor.__finney_entrypoint__, + settings.FINNEY_ENTRYPOINT, ), ( - bittensor.__finney_test_entrypoint__, + settings.FINNEY_TEST_ENTRYPOINT, "test", - bittensor.__finney_test_entrypoint__, + settings.FINNEY_TEST_ENTRYPOINT, ), ( "test.finney.opentensor.ai", "test", - bittensor.__finney_test_entrypoint__, + settings.FINNEY_TEST_ENTRYPOINT, ), ( - bittensor.__archive_entrypoint__, + settings.ARCHIVE_ENTRYPOINT, "archive", - bittensor.__archive_entrypoint__, + settings.ARCHIVE_ENTRYPOINT, ), ( "archive.chain.opentensor.ai", "archive", - bittensor.__archive_entrypoint__, + settings.ARCHIVE_ENTRYPOINT, ), ("127.0.0.1", "local", "127.0.0.1"), ("localhost", "local", "localhost"), # Edge cases (None, None, None), - ("unknown", "unknown network", "unknown"), + ("unknown", "unknown", "unknown"), ], ) def test_determine_chain_endpoint_and_network( @@ -288,51 +227,22 @@ def test_determine_chain_endpoint_and_network( assert result_endpoint == expected_endpoint -# Subtensor().get_error_info_by_index tests -@pytest.fixture -def substrate(): - class MockSubstrate: - pass - - return MockSubstrate() - - @pytest.fixture -def subtensor(substrate): - mock.patch.object( - subtensor_module, - "get_subtensor_errors", - return_value={ - "1": ("ErrorOne", "Description one"), - "2": ("ErrorTwo", "Description two"), - }, - ).start() +def subtensor(mocker): + fake_substrate = mocker.MagicMock() + fake_substrate.websocket.sock.getsockopt.return_value = 0 + mocker.patch.object( + subtensor_module, "SubstrateInterface", return_value=fake_substrate + ) return Subtensor() -def test_get_error_info_by_index_known_error(subtensor): - name, description = subtensor.get_error_info_by_index(1) - assert name == "ErrorOne" - assert description == "Description one" - - @pytest.fixture def mock_logger(): - with mock.patch.object(_logger, "warning") as mock_warning: + with mock.patch.object(logging, "warning") as mock_warning: yield mock_warning -def test_get_error_info_by_index_unknown_error(subtensor, mock_logger): - fake_index = 999 - name, description = subtensor.get_error_info_by_index(fake_index) - assert name == "Unknown Error" - assert description == "" - mock_logger.assert_called_once_with( - f"Subtensor returned an error with an unknown index: {fake_index}" - ) - - -# Subtensor()._get_hyperparameter tests def test_hyperparameter_subnet_does_not_exist(subtensor, mocker): """Tests when the subnet does not exist.""" subtensor.subnet_exists = mocker.MagicMock(return_value=False) @@ -351,7 +261,6 @@ def test_hyperparameter_result_is_none(subtensor, mocker): def test_hyperparameter_result_has_no_value(subtensor, mocker): """Test when the result has no 'value' attribute.""" - subtensor.subnet_exists = mocker.MagicMock(return_value=True) subtensor.query_subtensor = mocker.MagicMock(return_value=None) assert subtensor._get_hyperparameter("Difficulty", 1, None) is None @@ -381,76 +290,6 @@ def test_hyperparameter_success_float(subtensor, mocker): subtensor.query_subtensor.assert_called_once_with("Difficulty", None, [1]) -# Tests Hyper parameter calls -@pytest.mark.parametrize( - "method, param_name, value, expected_result_type", - [ - ("rho", "Rho", 1, int), - ("kappa", "Kappa", 1.0, float), - ("difficulty", "Difficulty", 1, int), - ("recycle", "Burn", 1, Balance), - ("immunity_period", "ImmunityPeriod", 1, int), - ("validator_batch_size", "ValidatorBatchSize", 1, int), - ("validator_prune_len", "ValidatorPruneLen", 1, int), - ("validator_logits_divergence", "ValidatorLogitsDivergence", 1.0, float), - ("validator_sequence_length", "ValidatorSequenceLength", 1, int), - ("validator_epochs_per_reset", "ValidatorEpochsPerReset", 1, int), - ("validator_epoch_length", "ValidatorEpochLen", 1, int), - ("validator_exclude_quantile", "ValidatorExcludeQuantile", 1.0, float), - ("max_allowed_validators", "MaxAllowedValidators", 1, int), - ("min_allowed_weights", "MinAllowedWeights", 1, int), - ("max_weight_limit", "MaxWeightsLimit", 1, float), - ("adjustment_alpha", "AdjustmentAlpha", 1, float), - ("bonds_moving_avg", "BondsMovingAverage", 1, float), - ("scaling_law_power", "ScalingLawPower", 1, float), - ("synergy_scaling_law_power", "SynergyScalingLawPower", 1, float), - ("subnetwork_n", "SubnetworkN", 1, int), - ("max_n", "MaxAllowedUids", 1, int), - ("blocks_since_epoch", "BlocksSinceEpoch", 1, int), - ("tempo", "Tempo", 1, int), - ], -) -def test_hyper_parameter_success_calls( - subtensor, mocker, method, param_name, value, expected_result_type -): - """ - Tests various hyperparameter methods to ensure they correctly fetch their respective hyperparameters and return the - expected values. - """ - # Prep - subtensor._get_hyperparameter = mocker.MagicMock(return_value=value) - - spy_u16_normalized_float = mocker.spy(subtensor_module, "U16_NORMALIZED_FLOAT") - spy_u64_normalized_float = mocker.spy(subtensor_module, "U64_NORMALIZED_FLOAT") - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - subtensor_method = getattr(subtensor, method) - result = subtensor_method(netuid=7, block=707) - - # Assertions - subtensor._get_hyperparameter.assert_called_once_with( - block=707, netuid=7, param_name=param_name - ) - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, expected_result_type) - - # Special cases - if method in [ - "kappa", - "validator_logits_divergence", - "validator_exclude_quantile", - "max_weight_limit", - ]: - spy_u16_normalized_float.assert_called_once() - - if method in ["adjustment_alpha", "bonds_moving_avg"]: - spy_u64_normalized_float.assert_called_once() - - if method in ["recycle"]: - spy_balance_from_rao.assert_called_once() - - def test_blocks_since_last_update_success_calls(subtensor, mocker): """Tests the weights_rate_limit method to ensure it correctly fetches the LastUpdate hyperparameter.""" # Prep @@ -494,6 +333,52 @@ def sample_hyperparameters(): return MagicMock(spec=SubnetHyperparameters) +def normalize_hyperparameters( + subnet: "SubnetHyperparameters", +) -> list[tuple[str, str, str]]: + """ + Normalizes the hyperparameters of a subnet. + + Args: + subnet: The subnet hyperparameters object. + + Returns: + A list of tuples containing the parameter name, value, and normalized value. + """ + param_mappings = { + "adjustment_alpha": u64_normalized_float, + "min_difficulty": u64_normalized_float, + "max_difficulty": u64_normalized_float, + "difficulty": u64_normalized_float, + "bonds_moving_avg": u64_normalized_float, + "max_weight_limit": u16_normalized_float, + "kappa": u16_normalized_float, + "alpha_high": u16_normalized_float, + "alpha_low": u16_normalized_float, + "min_burn": Balance.from_rao, + "max_burn": Balance.from_rao, + } + + normalized_values: list[tuple[str, str, str]] = [] + subnet_dict = subnet.__dict__ + + for param, value in subnet_dict.items(): + try: + if param in param_mappings: + norm_value = param_mappings[param](value) + if isinstance(norm_value, float): + norm_value = f"{norm_value:.{10}g}" + else: + norm_value = value + except Exception as e: + logging.warning(f"Error normalizing parameter '{param}': {e}") + norm_value = "-" + + normalized_values.append((param, str(value), str(norm_value))) + + return normalized_values + + def get_normalized_value(normalized_data, param_name): return next( ( @@ -537,7 +422,7 @@ def test_hyperparameter_normalization( # Mid-value test if is_balance: - numeric_value = float(str(norm_value).lstrip(bittensor.__tao_symbol__)) + numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) expected_tao = mid_value / 1e9 assert ( numeric_value == expected_tao @@ -551,7 +436,7 @@ def test_hyperparameter_normalization( norm_value = get_normalized_value(normalized, param_name) if is_balance: - numeric_value = float(str(norm_value).lstrip(bittensor.__tao_symbol__)) + numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) expected_tao = max_value / 1e9 assert ( numeric_value == expected_tao @@ -565,7 +450,7 @@ def test_hyperparameter_normalization( norm_value = get_normalized_value(normalized, param_name) if is_balance: - numeric_value = float(str(norm_value).lstrip(bittensor.__tao_symbol__)) + numeric_value = float(str(norm_value).lstrip(settings.TAO_SYMBOL)) expected_tao = zero_value / 1e9 assert ( numeric_value == expected_tao @@ -579,1803 +464,1557 @@ def test_hyperparameter_normalization( ########################### -# `get_total_stake_for_hotkey` tests -def test_get_total_stake_for_hotkey_success(subtensor, mocker): - """Tests successful retrieval of total stake for hotkey.""" +# get_prometheus_info tests +def test_get_prometheus_info_success(mocker, subtensor): + """Test get_prometheus_info returns correct data when information is found.""" # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=mocker.MagicMock(value=1)) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + netuid = 1 + hotkey_ss58 = "test_hotkey" + block = 123 + mock_result = mocker.MagicMock( + value={ + "ip": 3232235777, # 192.168.1.1 + "ip_type": 4, + "port": 9090, + "version": "1.0", + "block": 1000, + } + ) + mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.get_total_stake_for_hotkey(ss58_address=fake_ss58_address) + result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - # Assertions + # Asserts + assert result is not None + assert result.ip == "192.168.1.1" + assert result.ip_type == 4 + assert result.port == 9090 + assert result.version == "1.0" + assert result.block == 1000 subtensor.query_subtensor.assert_called_once_with( - "TotalHotkeyStake", None, [fake_ss58_address] + "Prometheus", block, [netuid, hotkey_ss58] ) - spy_balance_from_rao.assert_called_once() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, Balance) -def test_get_total_stake_for_hotkey_not_result(subtensor, mocker): - """Tests retrieval of total stake for hotkey when no result is returned.""" +def test_get_prometheus_info_no_data(mocker, subtensor): + """Test get_prometheus_info returns None when no information is found.""" # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=None) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + netuid = 1 + hotkey_ss58 = "test_hotkey" + block = 123 + mocker.patch.object(subtensor, "query_subtensor", return_value=None) # Call - result = subtensor.get_total_stake_for_hotkey(ss58_address=fake_ss58_address) + result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - # Assertions + # Asserts + assert result is None subtensor.query_subtensor.assert_called_once_with( - "TotalHotkeyStake", None, [fake_ss58_address] + "Prometheus", block, [netuid, hotkey_ss58] ) - spy_balance_from_rao.assert_not_called() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, type(None)) -def test_get_total_stake_for_hotkey_not_value(subtensor, mocker): - """Tests retrieval of total stake for hotkey when no value attribute is present.""" +def test_get_prometheus_info_no_value_attribute(mocker, subtensor): + """Test get_prometheus_info returns None when result has no value attribute.""" # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=object) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + netuid = 1 + hotkey_ss58 = "test_hotkey" + block = 123 + mock_result = mocker.MagicMock() + del mock_result.value + mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.get_total_stake_for_hotkey(ss58_address=fake_ss58_address) + result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - # Assertions + # Asserts + assert result is None subtensor.query_subtensor.assert_called_once_with( - "TotalHotkeyStake", None, [fake_ss58_address] + "Prometheus", block, [netuid, hotkey_ss58] ) - spy_balance_from_rao.assert_not_called() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(subtensor.query_subtensor.return_value, object) - assert not hasattr(result, "value") -# `get_total_stake_for_coldkey` tests -def test_get_total_stake_for_coldkey_success(subtensor, mocker): - """Tests successful retrieval of total stake for coldkey.""" +def test_get_prometheus_info_no_block(mocker, subtensor): + """Test get_prometheus_info with no block specified.""" # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=mocker.MagicMock(value=1)) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + netuid = 1 + hotkey_ss58 = "test_hotkey" + mock_result = MagicMock( + value={ + "ip": "192.168.1.1", + "ip_type": 4, + "port": 9090, + "version": "1.0", + "block": 1000, + } + ) + mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.get_total_stake_for_coldkey(ss58_address=fake_ss58_address) + result = subtensor.get_prometheus_info(netuid, hotkey_ss58) - # Assertions + # Asserts + assert result is not None + assert result.ip == "192.168.1.1" + assert result.ip_type == 4 + assert result.port == 9090 + assert result.version == "1.0" + assert result.block == 1000 subtensor.query_subtensor.assert_called_once_with( - "TotalColdkeyStake", None, [fake_ss58_address] + "Prometheus", None, [netuid, hotkey_ss58] ) - spy_balance_from_rao.assert_called_once() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, Balance) - - -def test_get_total_stake_for_coldkey_not_result(subtensor, mocker): - """Tests retrieval of total stake for coldkey when no result is returned.""" - # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=None) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - # Call - result = subtensor.get_total_stake_for_coldkey(ss58_address=fake_ss58_address) - # Assertions - subtensor.query_subtensor.assert_called_once_with( - "TotalColdkeyStake", None, [fake_ss58_address] - ) - spy_balance_from_rao.assert_not_called() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(result, type(None)) +########################### +# Global Parameters tests # +########################### -def test_get_total_stake_for_coldkey_not_value(subtensor, mocker): - """Tests retrieval of total stake for coldkey when no value attribute is present.""" - # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=object) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" - spy_balance_from_rao = mocker.spy(Balance, "from_rao") +# `block` property test +def test_block_property(mocker, subtensor): + """Test block property returns the correct block number.""" + expected_block = 123 + mocker.patch.object(subtensor, "get_current_block", return_value=expected_block) - # Call - result = subtensor.get_total_stake_for_coldkey(ss58_address=fake_ss58_address) + result = subtensor.block - # Assertions - subtensor.query_subtensor.assert_called_once_with( - "TotalColdkeyStake", None, [fake_ss58_address] - ) - spy_balance_from_rao.assert_not_called() - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert isinstance(subtensor.query_subtensor.return_value, object) - assert not hasattr(result, "value") + assert result == expected_block + subtensor.get_current_block.assert_called_once() -# `get_stake` tests -def test_get_stake_returns_correct_data(mocker, subtensor): - """Tests that get_stake returns correct data.""" +# `subnet_exists` tests +def test_subnet_exists_success(mocker, subtensor): + """Test subnet_exists returns True when subnet exists.""" # Prep - hotkey_ss58 = "test_hotkey" + netuid = 1 block = 123 - expected_query_result = [ - (mocker.MagicMock(value="coldkey1"), mocker.MagicMock(value=100)), - (mocker.MagicMock(value="coldkey2"), mocker.MagicMock(value=200)), - ] - mocker.patch.object( - subtensor, "query_map_subtensor", return_value=expected_query_result - ) + mock_result = mocker.MagicMock(value=True) + mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.get_stake(hotkey_ss58, block) + result = subtensor.subnet_exists(netuid, block) - # Assertion - assert result == [ - ("coldkey1", Balance.from_rao(100)), - ("coldkey2", Balance.from_rao(200)), - ] - subtensor.query_map_subtensor.assert_called_once_with("Stake", block, [hotkey_ss58]) + # Asserts + assert result is True + subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) -def test_get_stake_no_block(mocker, subtensor): - """Tests get_stake with no block specified.""" +def test_subnet_exists_no_data(mocker, subtensor): + """Test subnet_exists returns False when no subnet information is found.""" # Prep - hotkey_ss58 = "test_hotkey" - expected_query_result = [ - (MagicMock(value="coldkey1"), MagicMock(value=100)), - ] - mocker.patch.object( - subtensor, "query_map_subtensor", return_value=expected_query_result - ) + netuid = 1 + block = 123 + mocker.patch.object(subtensor, "query_subtensor", return_value=None) # Call - result = subtensor.get_stake(hotkey_ss58) + result = subtensor.subnet_exists(netuid, block) - # Assertion - assert result == [("coldkey1", Balance.from_rao(100))] - subtensor.query_map_subtensor.assert_called_once_with("Stake", None, [hotkey_ss58]) + # Asserts + assert result is False + subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) -def test_get_stake_empty_result(mocker, subtensor): - """Tests get_stake with an empty result.""" +def test_subnet_exists_no_value_attribute(mocker, subtensor): + """Test subnet_exists returns False when result has no value attribute.""" # Prep - hotkey_ss58 = "test_hotkey" + netuid = 1 block = 123 - expected_query_result = [] - mocker.patch.object( - subtensor, "query_map_subtensor", return_value=expected_query_result - ) + mock_result = mocker.MagicMock() + del mock_result.value + mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.get_stake(hotkey_ss58, block) + result = subtensor.subnet_exists(netuid, block) - # Assertion - assert result == [] - subtensor.query_map_subtensor.assert_called_once_with("Stake", block, [hotkey_ss58]) + # Asserts + assert result is False + subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) -# `does_hotkey_exist` tests -def test_does_hotkey_exist_true(mocker, subtensor): - """Test does_hotkey_exist returns True when hotkey exists and is valid.""" +def test_subnet_exists_no_block(mocker, subtensor): + """Test subnet_exists with no block specified.""" # Prep - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock(value="valid_coldkey") + netuid = 1 + mock_result = mocker.MagicMock(value=True) mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.does_hotkey_exist(hotkey_ss58, block) + result = subtensor.subnet_exists(netuid) - # Assertions + # Asserts assert result is True - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) + subtensor.query_subtensor.assert_called_once_with("NetworksAdded", None, [netuid]) -def test_does_hotkey_exist_false_special_value(mocker, subtensor): - """Test does_hotkey_exist returns False when result value is the special value.""" +# `get_total_subnets` tests +def test_get_total_subnets_success(mocker, subtensor): + """Test get_total_subnets returns correct data when total subnet information is found.""" # Prep - hotkey_ss58 = "test_hotkey" block = 123 - special_value = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - mock_result = MagicMock(value=special_value) + total_subnets_value = 10 + mock_result = mocker.MagicMock(value=total_subnets_value) mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.does_hotkey_exist(hotkey_ss58, block) + result = subtensor.get_total_subnets(block) - # Assertions - assert result is False - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) + # Asserts + assert result is not None + assert result == total_subnets_value + subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) -def test_does_hotkey_exist_false_no_value(mocker, subtensor): - """Test does_hotkey_exist returns False when result has no value attribute.""" - # Prep - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.does_hotkey_exist(hotkey_ss58, block) - - # Assertions - assert result is False - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) - - -def test_does_hotkey_exist_false_no_result(mocker, subtensor): - """Test does_hotkey_exist returns False when query_subtensor returns None.""" - # Prep - hotkey_ss58 = "test_hotkey" - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.does_hotkey_exist(hotkey_ss58, block) - - # Assertions - assert result is False - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) - - -def test_does_hotkey_exist_no_block(mocker, subtensor): - """Test does_hotkey_exist with no block specified.""" - # Prep - hotkey_ss58 = "test_hotkey" - mock_result = mocker.MagicMock(value="valid_coldkey") - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.does_hotkey_exist(hotkey_ss58) - - # Assertions - assert result is True - subtensor.query_subtensor.assert_called_once_with("Owner", None, [hotkey_ss58]) - - -# `get_hotkey_owner` tests -def test_get_hotkey_owner_exists(mocker, subtensor): - """Test get_hotkey_owner when the hotkey exists.""" - # Prep - hotkey_ss58 = "test_hotkey" - block = 123 - expected_owner = "coldkey_owner" - mock_result = mocker.MagicMock(value=expected_owner) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - mocker.patch.object(subtensor, "does_hotkey_exist", return_value=True) - - # Call - result = subtensor.get_hotkey_owner(hotkey_ss58, block) - - # Assertions - assert result == expected_owner - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) - subtensor.does_hotkey_exist.assert_called_once_with(hotkey_ss58, block) - - -def test_get_hotkey_owner_does_not_exist(mocker, subtensor): - """Test get_hotkey_owner when the hotkey does not exist.""" - # Prep - hotkey_ss58 = "test_hotkey" - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - mocker.patch.object(subtensor, "does_hotkey_exist", return_value=False) - - # Call - result = subtensor.get_hotkey_owner(hotkey_ss58, block) - - # Assertions - assert result is None - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) - subtensor.does_hotkey_exist.assert_not_called() - - -def test_get_hotkey_owner_no_block(mocker, subtensor): - """Test get_hotkey_owner with no block specified.""" - # Prep - hotkey_ss58 = "test_hotkey" - expected_owner = "coldkey_owner" - mock_result = mocker.MagicMock(value=expected_owner) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - mocker.patch.object(subtensor, "does_hotkey_exist", return_value=True) - - # Call - result = subtensor.get_hotkey_owner(hotkey_ss58) - - # Assertions - assert result == expected_owner - subtensor.query_subtensor.assert_called_once_with("Owner", None, [hotkey_ss58]) - subtensor.does_hotkey_exist.assert_called_once_with(hotkey_ss58, None) - - -def test_get_hotkey_owner_no_value_attribute(mocker, subtensor): - """Test get_hotkey_owner when the result has no value attribute.""" - # Prep - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - mocker.patch.object(subtensor, "does_hotkey_exist", return_value=True) - - # Call - result = subtensor.get_hotkey_owner(hotkey_ss58, block) - - # Assertions - assert result is None - subtensor.query_subtensor.assert_called_once_with("Owner", block, [hotkey_ss58]) - subtensor.does_hotkey_exist.assert_not_called() - - -# `get_axon_info` tests -def test_get_axon_info_success(mocker, subtensor): - """Test get_axon_info returns correct data when axon information is found.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock( - value={ - "ip": "192.168.1.1", - "ip_type": 4, - "port": 8080, - "protocol": "tcp", - "version": "1.0", - "placeholder1": "data1", - "placeholder2": "data2", - } - ) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_axon_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is not None - assert result.ip == "192.168.1.1" - assert result.ip_type == 4 - assert result.port == 8080 - assert result.protocol == "tcp" - assert result.version == "1.0" - assert result.placeholder1 == "data1" - assert result.placeholder2 == "data2" - assert result.hotkey == hotkey_ss58 - assert result.coldkey == "" - subtensor.query_subtensor.assert_called_once_with( - "Axons", block, [netuid, hotkey_ss58] - ) - - -def test_get_axon_info_no_data(mocker, subtensor): - """Test get_axon_info returns None when no axon information is found.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.get_axon_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "Axons", block, [netuid, hotkey_ss58] - ) - - -def test_get_axon_info_no_value_attribute(mocker, subtensor): - """Test get_axon_info returns None when result has no value attribute.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_axon_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "Axons", block, [netuid, hotkey_ss58] - ) - - -def test_get_axon_info_no_block(mocker, subtensor): - """Test get_axon_info with no block specified.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - mock_result = mocker.MagicMock( - value={ - "ip": 3232235777, # 192.168.1.1 - "ip_type": 4, - "port": 8080, - "protocol": "tcp", - "version": "1.0", - "placeholder1": "data1", - "placeholder2": "data2", - } - ) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_axon_info(netuid, hotkey_ss58) - - # Asserts - assert result is not None - assert result.ip == "192.168.1.1" - assert result.ip_type == 4 - assert result.port == 8080 - assert result.protocol == "tcp" - assert result.version == "1.0" - assert result.placeholder1 == "data1" - assert result.placeholder2 == "data2" - assert result.hotkey == hotkey_ss58 - assert result.coldkey == "" - subtensor.query_subtensor.assert_called_once_with( - "Axons", None, [netuid, hotkey_ss58] - ) - - -# get_prometheus_info tests -def test_get_prometheus_info_success(mocker, subtensor): - """Test get_prometheus_info returns correct data when information is found.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock( - value={ - "ip": 3232235777, # 192.168.1.1 - "ip_type": 4, - "port": 9090, - "version": "1.0", - "block": 1000, - } - ) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is not None - assert result.ip == "192.168.1.1" - assert result.ip_type == 4 - assert result.port == 9090 - assert result.version == "1.0" - assert result.block == 1000 - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", block, [netuid, hotkey_ss58] - ) - - -def test_get_prometheus_info_no_data(mocker, subtensor): - """Test get_prometheus_info returns None when no information is found.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", block, [netuid, hotkey_ss58] - ) - - -def test_get_prometheus_info_no_value_attribute(mocker, subtensor): - """Test get_prometheus_info returns None when result has no value attribute.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58, block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", block, [netuid, hotkey_ss58] - ) - - -def test_get_prometheus_info_no_block(mocker, subtensor): - """Test get_prometheus_info with no block specified.""" - # Prep - netuid = 1 - hotkey_ss58 = "test_hotkey" - mock_result = MagicMock( - value={ - "ip": "192.168.1.1", - "ip_type": 4, - "port": 9090, - "version": "1.0", - "block": 1000, - } - ) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.get_prometheus_info(netuid, hotkey_ss58) - - # Asserts - assert result is not None - assert result.ip == "192.168.1.1" - assert result.ip_type == 4 - assert result.port == 9090 - assert result.version == "1.0" - assert result.block == 1000 - subtensor.query_subtensor.assert_called_once_with( - "Prometheus", None, [netuid, hotkey_ss58] - ) - - -########################### -# Global Parameters tests # -########################### - - -# `block` property test -def test_block_property(mocker, subtensor): - """Test block property returns the correct block number.""" - expected_block = 123 - mocker.patch.object(subtensor, "get_current_block", return_value=expected_block) - - result = subtensor.block - - assert result == expected_block - subtensor.get_current_block.assert_called_once() - - -# `total_issuance` tests -def test_total_issuance_success(mocker, subtensor): - """Test total_issuance returns correct data when issuance information is found.""" - # Prep - block = 123 - issuance_value = 1000 - mock_result = mocker.MagicMock(value=issuance_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_issuance(block) - - # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("TotalIssuance", block) - spy_balance_from_rao.assert_called_once_with( - subtensor.query_subtensor.return_value.value - ) - - -def test_total_issuance_no_data(mocker, subtensor): - """Test total_issuance returns None when no issuance information is found.""" - # Prep - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_issuance(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalIssuance", block) - spy_balance_from_rao.assert_not_called() - - -def test_total_issuance_no_value_attribute(mocker, subtensor): - """Test total_issuance returns None when result has no value attribute.""" - # Prep - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_issuance(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalIssuance", block) - spy_balance_from_rao.assert_not_called() - - -def test_total_issuance_no_block(mocker, subtensor): - """Test total_issuance with no block specified.""" - # Prep - issuance_value = 1000 - mock_result = mocker.MagicMock(value=issuance_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_issuance() - - # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("TotalIssuance", None) - spy_balance_from_rao.assert_called_once_with( - subtensor.query_subtensor.return_value.value - ) - - -# `total_stake` method tests -def test_total_stake_success(mocker, subtensor): - """Test total_stake returns correct data when stake information is found.""" - # Prep - block = 123 - stake_value = 5000 - mock_result = mocker.MagicMock(value=stake_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_stake(block) - - # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("TotalStake", block) - spy_balance_from_rao.assert_called_once_with( - subtensor.query_subtensor.return_value.value - ) - - -def test_total_stake_no_data(mocker, subtensor): - """Test total_stake returns None when no stake information is found.""" - # Prep - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_stake(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalStake", block) - spy_balance_from_rao.assert_not_called() - - -def test_total_stake_no_value_attribute(mocker, subtensor): - """Test total_stake returns None when result has no value attribute.""" - # Prep - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_stake(block) - - # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalStake", block) - spy_balance_from_rao.assert_not_called() - - -def test_total_stake_no_block(mocker, subtensor): - """Test total_stake with no block specified.""" - # Prep - stake_value = 5000 - mock_result = mocker.MagicMock(value=stake_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") - - # Call - result = subtensor.total_stake() - - # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("TotalStake", None) - ( - spy_balance_from_rao.assert_called_once_with( - subtensor.query_subtensor.return_value.value - ), - ) - - -# `serving_rate_limit` method tests -def test_serving_rate_limit_success(mocker, subtensor): - """Test serving_rate_limit returns correct data when rate limit information is found.""" - # Prep - netuid = 1 - block = 123 - rate_limit_value = "10" - mocker.patch.object(subtensor, "_get_hyperparameter", return_value=rate_limit_value) - - # Call - result = subtensor.serving_rate_limit(netuid, block) - - # Asserts - assert result is not None - assert result == int(rate_limit_value) - subtensor._get_hyperparameter.assert_called_once_with( - param_name="ServingRateLimit", netuid=netuid, block=block - ) - - -def test_serving_rate_limit_no_data(mocker, subtensor): - """Test serving_rate_limit returns None when no rate limit information is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "_get_hyperparameter", return_value=None) - - # Call - result = subtensor.serving_rate_limit(netuid, block) - - # Asserts - assert result is None - subtensor._get_hyperparameter.assert_called_once_with( - param_name="ServingRateLimit", netuid=netuid, block=block - ) - - -def test_serving_rate_limit_no_block(mocker, subtensor): - """Test serving_rate_limit with no block specified.""" - # Prep - netuid = 1 - rate_limit_value = "10" - mocker.patch.object(subtensor, "_get_hyperparameter", return_value=rate_limit_value) - - # Call - result = subtensor.serving_rate_limit(netuid) - - # Asserts - assert result is not None - assert result == int(rate_limit_value) - subtensor._get_hyperparameter.assert_called_once_with( - param_name="ServingRateLimit", netuid=netuid, block=None - ) - - -# `tx_rate_limit` tests -def test_tx_rate_limit_success(mocker, subtensor): - """Test tx_rate_limit returns correct data when rate limit information is found.""" - # Prep - block = 123 - rate_limit_value = 100 - mock_result = mocker.MagicMock(value=rate_limit_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.tx_rate_limit(block) - - # Asserts - assert result is not None - assert result == rate_limit_value - subtensor.query_subtensor.assert_called_once_with("TxRateLimit", block) - - -def test_tx_rate_limit_no_data(mocker, subtensor): - """Test tx_rate_limit returns None when no rate limit information is found.""" +def test_get_total_subnets_no_data(mocker, subtensor): + """Test get_total_subnets returns None when no total subnet information is found.""" # Prep block = 123 mocker.patch.object(subtensor, "query_subtensor", return_value=None) # Call - result = subtensor.tx_rate_limit(block) + result = subtensor.get_total_subnets(block) # Asserts assert result is None - subtensor.query_subtensor.assert_called_once_with("TxRateLimit", block) + subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) -def test_tx_rate_limit_no_value_attribute(mocker, subtensor): - """Test tx_rate_limit returns None when result has no value attribute.""" +def test_get_total_subnets_no_value_attribute(mocker, subtensor): + """Test get_total_subnets returns None when result has no value attribute.""" # Prep block = 123 mock_result = mocker.MagicMock() - del mock_result.value + del mock_result.value # Simulating a missing value attribute mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.tx_rate_limit(block) + result = subtensor.get_total_subnets(block) # Asserts assert result is None - subtensor.query_subtensor.assert_called_once_with("TxRateLimit", block) + subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) -def test_tx_rate_limit_no_block(mocker, subtensor): - """Test tx_rate_limit with no block specified.""" +def test_get_total_subnets_no_block(mocker, subtensor): + """Test get_total_subnets with no block specified.""" # Prep - rate_limit_value = 100 - mock_result = mocker.MagicMock(value=rate_limit_value) + total_subnets_value = 10 + mock_result = mocker.MagicMock(value=total_subnets_value) mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) # Call - result = subtensor.tx_rate_limit() + result = subtensor.get_total_subnets() # Asserts assert result is not None - assert result == rate_limit_value - subtensor.query_subtensor.assert_called_once_with("TxRateLimit", None) - - -############################ -# Network Parameters tests # -############################ - - -# `subnet_exists` tests -def test_subnet_exists_success(mocker, subtensor): - """Test subnet_exists returns True when subnet exists.""" - # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock(value=True) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.subnet_exists(netuid, block) - - # Asserts - assert result is True - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) - - -def test_subnet_exists_no_data(mocker, subtensor): - """Test subnet_exists returns False when no subnet information is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - - # Call - result = subtensor.subnet_exists(netuid, block) - - # Asserts - assert result is False - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) - - -def test_subnet_exists_no_value_attribute(mocker, subtensor): - """Test subnet_exists returns False when result has no value attribute.""" - # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.subnet_exists(netuid, block) - - # Asserts - assert result is False - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", block, [netuid]) - - -def test_subnet_exists_no_block(mocker, subtensor): - """Test subnet_exists with no block specified.""" - # Prep - netuid = 1 - mock_result = mocker.MagicMock(value=True) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - - # Call - result = subtensor.subnet_exists(netuid) - - # Asserts - assert result is True - subtensor.query_subtensor.assert_called_once_with("NetworksAdded", None, [netuid]) + assert result == total_subnets_value + subtensor.query_subtensor.assert_called_once_with("TotalNetworks", None) -# `get_all_subnet_netuids` tests -def test_get_all_subnet_netuids_success(mocker, subtensor): - """Test get_all_subnet_netuids returns correct list when netuid information is found.""" +# `get_subnets` tests +def test_get_subnets_success(mocker, subtensor): + """Test get_subnets returns correct list when subnet information is found.""" # Prep block = 123 mock_netuid1 = mocker.MagicMock(value=1) mock_netuid2 = mocker.MagicMock(value=2) mock_result = mocker.MagicMock() - mock_result.records = True - mock_result.__iter__.return_value = [(mock_netuid1, True), (mock_netuid2, True)] + mock_result.records = [(mock_netuid1, True), (mock_netuid2, True)] mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) # Call - result = subtensor.get_all_subnet_netuids(block) + result = subtensor.get_subnets(block) # Asserts assert result == [1, 2] subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) -def test_get_all_subnet_netuids_no_data(mocker, subtensor): - """Test get_all_subnet_netuids returns empty list when no netuid information is found.""" +def test_get_subnets_no_data(mocker, subtensor): + """Test get_subnets returns empty list when no subnet information is found.""" # Prep block = 123 - mocker.patch.object(subtensor, "query_map_subtensor", return_value=None) + mock_result = mocker.MagicMock() + mock_result.records = [] + mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) # Call - result = subtensor.get_all_subnet_netuids(block) + result = subtensor.get_subnets(block) # Asserts assert result == [] subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) -def test_get_all_subnet_netuids_no_records_attribute(mocker, subtensor): - """Test get_all_subnet_netuids returns empty list when result has no records attribute.""" +def test_get_subnets_no_records_attribute(mocker, subtensor): + """Test get_subnets returns empty list when result has no records attribute.""" # Prep block = 123 mock_result = mocker.MagicMock() - del mock_result.records - mock_result.__iter__.return_value = [] + del mock_result.records # Simulating a missing records attribute mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) # Call - result = subtensor.get_all_subnet_netuids(block) + result = subtensor.get_subnets(block) # Asserts assert result == [] subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) -def test_get_all_subnet_netuids_no_block(mocker, subtensor): - """Test get_all_subnet_netuids with no block specified.""" +def test_get_subnets_no_block_specified(mocker, subtensor): + """Test get_subnets with no block specified.""" # Prep mock_netuid1 = mocker.MagicMock(value=1) mock_netuid2 = mocker.MagicMock(value=2) mock_result = mocker.MagicMock() - mock_result.records = True - mock_result.__iter__.return_value = [(mock_netuid1, True), (mock_netuid2, True)] + mock_result.records = [(mock_netuid1, True), (mock_netuid2, True)] mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) # Call - result = subtensor.get_all_subnet_netuids() + result = subtensor.get_subnets() # Asserts assert result == [1, 2] subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", None) -# `get_total_subnets` tests -def test_get_total_subnets_success(mocker, subtensor): - """Test get_total_subnets returns correct data when total subnet information is found.""" +# `get_subnet_hyperparameters` tests +def test_get_subnet_hyperparameters_success(mocker, subtensor): + """Test get_subnet_hyperparameters returns correct data when hyperparameters are found.""" # Prep + netuid = 1 block = 123 - total_subnets_value = 10 - mock_result = mocker.MagicMock(value=total_subnets_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) + hex_bytes_result = "0x010203" + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) + mocker.patch.object( + subtensor_module.SubnetHyperparameters, + "from_vec_u8", + return_value=["from_vec_u8"], + ) # Call - result = subtensor.get_total_subnets(block) + result = subtensor.get_subnet_hyperparameters(netuid, block) # Asserts - assert result is not None - assert result == total_subnets_value - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], + block=block, + ) + subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( + bytes_result + ) -def test_get_total_subnets_no_data(mocker, subtensor): - """Test get_total_subnets returns None when no total subnet information is found.""" +def test_get_subnet_hyperparameters_hex_without_prefix(subtensor, mocker): + """Test get_subnet_hyperparameters correctly processes hex string without '0x' prefix.""" # Prep + netuid = 1 block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) + hex_bytes_result = "010203" + bytes_result = bytes.fromhex(hex_bytes_result) + mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) + mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") # Call - result = subtensor.get_total_subnets(block) + result = subtensor.get_subnet_hyperparameters(netuid, block) # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], + block=block, + ) + subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( + bytes_result + ) -def test_get_total_subnets_no_value_attribute(mocker, subtensor): - """Test get_total_subnets returns None when result has no value attribute.""" +def test_get_subnet_hyperparameters_no_data(mocker, subtensor): + """Test get_subnet_hyperparameters returns empty list when no data is found.""" # Prep + netuid = 1 block = 123 - mock_result = mocker.MagicMock() - del mock_result.value # Simulating a missing value attribute - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) + mocker.patch.object(subtensor, "query_runtime_api", return_value=None) + mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") # Call - result = subtensor.get_total_subnets(block) + result = subtensor.get_subnet_hyperparameters(netuid, block) # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", block) + assert result == [] + subtensor.query_runtime_api.assert_called_once_with( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams", + params=[netuid], + block=block, + ) + subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() -def test_get_total_subnets_no_block(mocker, subtensor): - """Test get_total_subnets with no block specified.""" +def test_query_subtensor(subtensor, mocker): + """Tests query_subtensor call.""" # Prep - total_subnets_value = 10 - mock_result = mocker.MagicMock(value=total_subnets_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) + fake_name = "module_name" # Call - result = subtensor.get_total_subnets() + result = subtensor.query_subtensor(fake_name) # Asserts - assert result is not None - assert result == total_subnets_value - subtensor.query_subtensor.assert_called_once_with("TotalNetworks", None) + subtensor.substrate.query.assert_called_once_with( + module="SubtensorModule", + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == subtensor.substrate.query.return_value -# `get_subnet_modality` tests -def test_get_subnet_modality_success(mocker, subtensor): - """Test get_subnet_modality returns correct data when modality information is found.""" +def test_query_runtime_api(subtensor, mocker): + """Tests query_runtime_api call.""" # Prep - netuid = 1 - block = 123 - modality_value = 42 - mock_result = mocker.MagicMock(value=modality_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) + fake_runtime_api = "NeuronInfoRuntimeApi" + fake_method = "get_neuron_lite" + + mocked_state_call = mocker.MagicMock() + subtensor.state_call = mocked_state_call + + mocked_runtime_configuration = mocker.patch.object( + subtensor_module, "RuntimeConfiguration" + ) + mocked_scalecodec = mocker.patch.object(subtensor_module.scalecodec, "ScaleBytes") # Call - result = subtensor.get_subnet_modality(netuid, block) + result = subtensor.query_runtime_api(fake_runtime_api, fake_method, None) # Asserts - assert result is not None - assert result == modality_value - subtensor.query_subtensor.assert_called_once_with( - "NetworkModality", block, [netuid] + subtensor.state_call.assert_called_once_with( + method=f"{fake_runtime_api}_{fake_method}", data="0x", block=None + ) + mocked_scalecodec.assert_called_once_with( + subtensor.state_call.return_value.__getitem__.return_value + ) + mocked_runtime_configuration.assert_called_once() + mocked_runtime_configuration.return_value.update_type_registry.assert_called() + mocked_runtime_configuration.return_value.create_scale_object.assert_called() + assert ( + result + == mocked_runtime_configuration.return_value.create_scale_object.return_value.decode.return_value ) -def test_get_subnet_modality_no_data(mocker, subtensor): - """Test get_subnet_modality returns None when no modality information is found.""" +def test_query_map_subtensor(subtensor, mocker): + """Tests query_map_subtensor call.""" # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) + fake_name = "module_name" # Call - result = subtensor.get_subnet_modality(netuid, block) + result = subtensor.query_map_subtensor(fake_name) # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "NetworkModality", block, [netuid] + subtensor.substrate.query_map.assert_called_once_with( + module="SubtensorModule", + storage_function=fake_name, + params=None, + block_hash=None, ) + assert result == subtensor.substrate.query_map.return_value -def test_get_subnet_modality_no_value_attribute(mocker, subtensor): - """Test get_subnet_modality returns None when result has no value attribute.""" +def test_state_call(subtensor, mocker): + """Tests state_call call.""" # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value # Simulating a missing value attribute - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) + fake_method = "method" + fake_data = "data" # Call - result = subtensor.get_subnet_modality(netuid, block) + result = subtensor.state_call(fake_method, fake_data) # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with( - "NetworkModality", block, [netuid] + subtensor.substrate.rpc_request.assert_called_once_with( + method="state_call", + params=[fake_method, fake_data], ) + assert result == subtensor.substrate.rpc_request.return_value -def test_get_subnet_modality_no_block_specified(mocker, subtensor): - """Test get_subnet_modality with no block specified.""" +def test_query_map(subtensor, mocker): + """Tests query_map call.""" # Prep - netuid = 1 - modality_value = 42 - mock_result = mocker.MagicMock(value=modality_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) + fake_module_name = "module_name" + fake_name = "constant_name" # Call - result = subtensor.get_subnet_modality(netuid) + result = subtensor.query_map(fake_module_name, fake_name) # Asserts - assert result is not None - assert result == modality_value - subtensor.query_subtensor.assert_called_once_with("NetworkModality", None, [netuid]) + subtensor.substrate.query_map.assert_called_once_with( + module=fake_module_name, + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == subtensor.substrate.query_map.return_value -# `get_emission_value_by_subnet` tests -def test_get_emission_value_by_subnet_success(mocker, subtensor): - """Test get_emission_value_by_subnet returns correct data when emission value is found.""" +def test_query_constant(subtensor, mocker): + """Tests query_constant call.""" # Prep - netuid = 1 - block = 123 - emission_value = 1000 - mock_result = mocker.MagicMock(value=emission_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + fake_module_name = "module_name" + fake_constant_name = "constant_name" # Call - result = subtensor.get_emission_value_by_subnet(netuid, block) + result = subtensor.query_constant(fake_module_name, fake_constant_name) # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("EmissionValues", block, [netuid]) - spy_balance_from_rao.assert_called_once_with(emission_value) - assert result == Balance.from_rao(emission_value) + subtensor.substrate.get_constant.assert_called_once_with( + module_name=fake_module_name, + constant_name=fake_constant_name, + block_hash=None, + ) + assert result == subtensor.substrate.get_constant.return_value -def test_get_emission_value_by_subnet_no_data(mocker, subtensor): - """Test get_emission_value_by_subnet returns None when no emission value is found.""" +def test_query_module(subtensor): # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + fake_module = "module" + fake_name = "function_name" # Call - result = subtensor.get_emission_value_by_subnet(netuid, block) + result = subtensor.query_module(fake_module, fake_name) # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("EmissionValues", block, [netuid]) - spy_balance_from_rao.assert_not_called() + subtensor.substrate.query.assert_called_once_with( + module=fake_module, + storage_function=fake_name, + params=None, + block_hash=None, + ) + assert result == subtensor.substrate.query.return_value -def test_get_emission_value_by_subnet_no_value_attribute(mocker, subtensor): - """Test get_emission_value_by_subnet returns None when result has no value attribute.""" +def test_metagraph(subtensor, mocker): + """Tests subtensor.metagraph call.""" # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value # Simulating a missing value attribute - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + fake_netuid = 1 + fake_lite = True + mocked_metagraph = mocker.patch.object(subtensor_module, "Metagraph") # Call - result = subtensor.get_emission_value_by_subnet(netuid, block) + result = subtensor.metagraph(fake_netuid, fake_lite) # Asserts - assert result is None - subtensor.query_subtensor.assert_called_once_with("EmissionValues", block, [netuid]) - spy_balance_from_rao.assert_not_called() + mocked_metagraph.assert_called_once_with( + network=subtensor.network, netuid=fake_netuid, lite=fake_lite, sync=False + ) + mocked_metagraph.return_value.sync.assert_called_once_with( + block=None, lite=fake_lite, subtensor=subtensor + ) + assert result == mocked_metagraph.return_value -def test_get_emission_value_by_subnet_no_block_specified(mocker, subtensor): - """Test get_emission_value_by_subnet with no block specified.""" +def test_get_netuids_for_hotkey(subtensor, mocker): + """Tests get_netuids_for_hotkey call.""" # Prep - netuid = 1 - emission_value = 1000 - mock_result = mocker.MagicMock(value=emission_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_balance_from_rao = mocker.spy(Balance, "from_rao") + fake_hotkey_ss58 = "hotkey_ss58" + fake_block = 123 + + mocked_query_map_subtensor = mocker.MagicMock() + subtensor.query_map_subtensor = mocked_query_map_subtensor # Call - result = subtensor.get_emission_value_by_subnet(netuid) + result = subtensor.get_netuids_for_hotkey(fake_hotkey_ss58, fake_block) # Asserts - assert result is not None - subtensor.query_subtensor.assert_called_once_with("EmissionValues", None, [netuid]) - spy_balance_from_rao.assert_called_once_with(emission_value) - assert result == Balance.from_rao(emission_value) + mocked_query_map_subtensor.assert_called_once_with( + "IsNetworkMember", fake_block, [fake_hotkey_ss58] + ) + assert result == [] + + +def test_get_current_block(subtensor): + """Tests get_current_block call.""" + # Call + result = subtensor.get_current_block() + + # Asserts + subtensor.substrate.get_block_number.assert_called_once_with(None) + assert result == subtensor.substrate.get_block_number.return_value -# `get_subnet_connection_requirements` tests -def test_get_subnet_connection_requirements_success(mocker, subtensor): - """Test get_subnet_connection_requirements returns correct data when requirements are found.""" +def test_is_hotkey_registered_any(subtensor, mocker): + """Tests is_hotkey_registered_any call""" # Prep - netuid = 1 - block = 123 - mock_tuple1 = (mocker.MagicMock(value="requirement1"), mocker.MagicMock(value=10)) - mock_tuple2 = (mocker.MagicMock(value="requirement2"), mocker.MagicMock(value=20)) - mock_result = mocker.MagicMock() - mock_result.records = [mock_tuple1, mock_tuple2] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) + fake_hotkey_ss58 = "hotkey_ss58" + fake_block = 123 + return_value = [1, 2] + + mocked_get_netuids_for_hotkey = mocker.MagicMock(return_value=return_value) + subtensor.get_netuids_for_hotkey = mocked_get_netuids_for_hotkey + + # Call + result = subtensor.is_hotkey_registered_any(fake_hotkey_ss58, fake_block) + + # Asserts + mocked_get_netuids_for_hotkey.assert_called_once_with(fake_hotkey_ss58, fake_block) + assert result is (len(return_value) > 0) + + +def test_is_hotkey_registered_on_subnet(subtensor, mocker): + """Tests is_hotkey_registered_on_subnet call.""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + fake_netuid = 1 + fake_block = 123 + + mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet # Call - result = subtensor.get_subnet_connection_requirements(netuid, block) + result = subtensor.is_hotkey_registered_on_subnet( + fake_hotkey_ss58, fake_netuid, fake_block + ) # Asserts - assert result == {"requirement1": 10, "requirement2": 20} - subtensor.query_map_subtensor.assert_called_once_with( - "NetworkConnect", block, [netuid] + mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with( + fake_hotkey_ss58, fake_netuid, fake_block ) + assert result is (mocked_get_uid_for_hotkey_on_subnet.return_value is not None) -def test_get_subnet_connection_requirements_no_data(mocker, subtensor): - """Test get_subnet_connection_requirements returns empty dict when no data is found.""" +def test_is_hotkey_registered_without_netuid(subtensor, mocker): + """Tests is_hotkey_registered call with no netuid specified.""" # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - mock_result.records = [] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) + fake_hotkey_ss58 = "hotkey_ss58" + + mocked_is_hotkey_registered_any = mocker.MagicMock() + subtensor.is_hotkey_registered_any = mocked_is_hotkey_registered_any + + # Call + + result = subtensor.is_hotkey_registered(fake_hotkey_ss58) + + # Asserts + mocked_is_hotkey_registered_any.assert_called_once_with(fake_hotkey_ss58, None) + assert result == mocked_is_hotkey_registered_any.return_value + + +def test_is_hotkey_registered_with_netuid(subtensor, mocker): + """Tests is_hotkey_registered call with netuid specified.""" + # Prep + fake_hotkey_ss58 = "hotkey_ss58" + fake_netuid = 123 + + mocked_is_hotkey_registered_on_subnet = mocker.MagicMock() + subtensor.is_hotkey_registered_on_subnet = mocked_is_hotkey_registered_on_subnet # Call - result = subtensor.get_subnet_connection_requirements(netuid, block) + + result = subtensor.is_hotkey_registered(fake_hotkey_ss58, fake_netuid) + + # Asserts + mocked_is_hotkey_registered_on_subnet.assert_called_once_with( + fake_hotkey_ss58, fake_netuid, None + ) + assert result == mocked_is_hotkey_registered_on_subnet.return_value + + +def test_set_weights(subtensor, mocker): + """Successful set_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_netuid = 1 + fake_uids = [2, 4] + fake_weights = [0.4, 0.6] + fake_wait_for_inclusion = False + fake_wait_for_finalization = False + fake_prompt = False + fake_max_retries = 5 + + expected_result = (True, None) + + mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet + + mocked_blocks_since_last_update = mocker.MagicMock(return_value=2) + subtensor.blocks_since_last_update = mocked_blocks_since_last_update + + mocked_weights_rate_limit = mocker.MagicMock(return_value=1) + subtensor.weights_rate_limit = mocked_weights_rate_limit + + mocked_set_weights_extrinsic = mocker.patch.object( + subtensor_module, "set_weights_extrinsic", return_value=expected_result + ) + + # Call + result = subtensor.set_weights( + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + version_key=settings.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + prompt=fake_prompt, + max_retries=fake_max_retries, + ) # Asserts - assert result == {} - subtensor.query_map_subtensor.assert_called_once_with( - "NetworkConnect", block, [netuid] + mocked_get_uid_for_hotkey_on_subnet.assert_called_once_with( + fake_wallet.hotkey.ss58_address, fake_netuid ) + mocked_blocks_since_last_update.assert_called_with( + fake_netuid, mocked_get_uid_for_hotkey_on_subnet.return_value + ) + mocked_weights_rate_limit.assert_called_with(fake_netuid) + mocked_set_weights_extrinsic.assert_called_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=fake_netuid, + uids=fake_uids, + weights=fake_weights, + version_key=settings.version_as_int, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + prompt=fake_prompt, + ) + assert result == expected_result -def test_get_subnet_connection_requirements_no_records_attribute(mocker, subtensor): - """Test get_subnet_connection_requirements returns empty dict when result has no records attribute.""" +def test_serve_axon(subtensor, mocker): + """Tests successful serve_axon call.""" # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - del mock_result.records # Simulating a missing records attribute + fake_netuid = 123 + fake_axon = mocker.MagicMock() + fake_wait_for_inclusion = False + fake_wait_for_finalization = True - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) + mocked_serve_axon_extrinsic = mocker.patch.object( + subtensor_module, "serve_axon_extrinsic" + ) # Call - result = subtensor.get_subnet_connection_requirements(netuid, block) + result = subtensor.serve_axon( + fake_netuid, fake_axon, fake_wait_for_inclusion, fake_wait_for_finalization + ) # Asserts - assert result == {} - subtensor.query_map_subtensor.assert_called_once_with( - "NetworkConnect", block, [netuid] + mocked_serve_axon_extrinsic.assert_called_once_with( + subtensor, + fake_netuid, + fake_axon, + fake_wait_for_inclusion, + fake_wait_for_finalization, ) + assert result == mocked_serve_axon_extrinsic.return_value -def test_get_subnet_connection_requirements_no_block_specified(mocker, subtensor): - """Test get_subnet_connection_requirements with no block specified.""" +def test_get_block_hash(subtensor, mocker): + """Tests successful get_block_hash call.""" # Prep - netuid = 1 - mock_tuple1 = (mocker.MagicMock(value="requirement1"), mocker.MagicMock(value=10)) - mock_tuple2 = (mocker.MagicMock(value="requirement2"), mocker.MagicMock(value=20)) - mock_result = mocker.MagicMock() - mock_result.records = [mock_tuple1, mock_tuple2] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) + fake_block_id = 123 + + # Call + result = subtensor.get_block_hash(fake_block_id) + + # Asserts + subtensor.substrate.get_block_hash.assert_called_once_with(block_id=fake_block_id) + assert result == subtensor.substrate.get_block_hash.return_value + + +def test_commit(subtensor, mocker): + """Test successful commit call.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_netuid = 1 + fake_data = "some data to network" + mocked_publish_metadata = mocker.patch.object(subtensor_module, "publish_metadata") # Call - result = subtensor.get_subnet_connection_requirements(netuid) + result = subtensor.commit(fake_wallet, fake_netuid, fake_data) # Asserts - assert result == {"requirement1": 10, "requirement2": 20} - subtensor.query_map_subtensor.assert_called_once_with( - "NetworkConnect", None, [netuid] + mocked_publish_metadata.assert_called_once_with( + subtensor, fake_wallet, fake_netuid, f"Raw{len(fake_data)}", fake_data.encode() ) + assert result is None -# `get_subnets` tests -def test_get_subnets_success(mocker, subtensor): - """Test get_subnets returns correct list when subnet information is found.""" +def test_subnetwork_n(subtensor, mocker): + """Test successful subnetwork_n call.""" # Prep - block = 123 - mock_netuid1 = mocker.MagicMock(value=1) - mock_netuid2 = mocker.MagicMock(value=2) - mock_result = mocker.MagicMock() - mock_result.records = [(mock_netuid1, True), (mock_netuid2, True)] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) + fake_netuid = 1 + fake_block = 123 + fake_result = 2 + + mocked_get_hyperparameter = mocker.MagicMock() + mocked_get_hyperparameter.return_value = fake_result + subtensor._get_hyperparameter = mocked_get_hyperparameter # Call - result = subtensor.get_subnets(block) + result = subtensor.subnetwork_n(fake_netuid, fake_block) # Asserts - assert result == [1, 2] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) + mocked_get_hyperparameter.assert_called_once_with( + param_name="SubnetworkN", netuid=fake_netuid, block=fake_block + ) + assert result == mocked_get_hyperparameter.return_value -def test_get_subnets_no_data(mocker, subtensor): - """Test get_subnets returns empty list when no subnet information is found.""" +def test_transfer(subtensor, mocker): + """Tests successful transfer call.""" # Prep - block = 123 - mock_result = mocker.MagicMock() - mock_result.records = [] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) + fake_wallet = mocker.MagicMock() + fake_dest = "SS58PUBLICKEY" + fake_amount = 1.1 + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + fake_prompt = False + mocked_transfer_extrinsic = mocker.patch.object( + subtensor_module, "transfer_extrinsic" + ) # Call - result = subtensor.get_subnets(block) + result = subtensor.transfer( + fake_wallet, + fake_dest, + fake_amount, + fake_wait_for_inclusion, + fake_wait_for_finalization, + fake_prompt, + ) # Asserts - assert result == [] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) + mocked_transfer_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + dest=fake_dest, + amount=fake_amount, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + prompt=fake_prompt, + ) + assert result == mocked_transfer_extrinsic.return_value -def test_get_subnets_no_records_attribute(mocker, subtensor): - """Test get_subnets returns empty list when result has no records attribute.""" +def test_get_neuron_for_pubkey_and_subnet(subtensor, mocker): + """Successful call to get_neuron_for_pubkey_and_subnet.""" # Prep - block = 123 - mock_result = mocker.MagicMock() - del mock_result.records # Simulating a missing records attribute - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) + fake_hotkey_ss58 = "fake_hotkey" + fake_netuid = 1 + fake_block = 123 + + mocked_neuron_for_uid = mocker.MagicMock() + subtensor.neuron_for_uid = mocked_neuron_for_uid + + mocked_get_uid_for_hotkey_on_subnet = mocker.MagicMock() + subtensor.get_uid_for_hotkey_on_subnet = mocked_get_uid_for_hotkey_on_subnet # Call - result = subtensor.get_subnets(block) + result = subtensor.get_neuron_for_pubkey_and_subnet( + hotkey_ss58=fake_hotkey_ss58, + netuid=fake_netuid, + block=fake_block, + ) # Asserts - assert result == [] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", block) + mocked_neuron_for_uid.assert_called_once_with( + mocked_get_uid_for_hotkey_on_subnet.return_value, + fake_netuid, + block=fake_block, + ) + assert result == mocked_neuron_for_uid.return_value -def test_get_subnets_no_block_specified(mocker, subtensor): - """Test get_subnets with no block specified.""" +def test_neuron_for_uid_none(subtensor, mocker): + """Test neuron_for_uid successful call.""" # Prep - mock_netuid1 = mocker.MagicMock(value=1) - mock_netuid2 = mocker.MagicMock(value=2) - mock_result = mocker.MagicMock() - mock_result.records = [(mock_netuid1, True), (mock_netuid2, True)] - mocker.patch.object(subtensor, "query_map_subtensor", return_value=mock_result) + fake_uid = None + fake_netuid = 2 + fake_block = 123 + mocked_neuron_info = mocker.patch.object( + subtensor_module.NeuronInfo, "get_null_neuron" + ) # Call - result = subtensor.get_subnets() + result = subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block=fake_block + ) # Asserts - assert result == [1, 2] - subtensor.query_map_subtensor.assert_called_once_with("NetworksAdded", None) + mocked_neuron_info.assert_called_once() + assert result == mocked_neuron_info.return_value -# `get_all_subnets_info` tests -def test_get_all_subnets_info_success(mocker, subtensor): - """Test get_all_subnets_info returns correct data when subnet information is found.""" +def test_neuron_for_uid_response_none(subtensor, mocker): + """Test neuron_for_uid successful call.""" # Prep - block = 123 - subnet_data = [1, 2, 3] # Mocked response data - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" - ) - mock_response = {"result": subnet_data} - mocker.patch.object(subtensor.substrate, "rpc_request", return_value=mock_response) - mocker.patch.object( - subtensor_module.SubnetInfo, - "list_from_vec_u8", - return_value="list_from_vec_u80", + fake_uid = 1 + fake_netuid = 2 + fake_block = 123 + mocked_neuron_info = mocker.patch.object( + subtensor_module.NeuronInfo, "get_null_neuron" ) + subtensor.substrate.rpc_request.return_value.get.return_value = None + # Call - result = subtensor.get_all_subnets_info(block) + result = subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block=fake_block + ) # Asserts - subtensor.substrate.get_block_hash.assert_called_once_with(block) + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) subtensor.substrate.rpc_request.assert_called_once_with( - method="subnetInfo_getSubnetsInfo", params=["mock_block_hash"] + method="neuronInfo_getNeuron", + params=[fake_netuid, fake_uid, subtensor.substrate.get_block_hash.return_value], ) - subtensor_module.SubnetInfo.list_from_vec_u8.assert_called_once_with(subnet_data) + + mocked_neuron_info.assert_called_once() + assert result == mocked_neuron_info.return_value -@pytest.mark.parametrize("result_", [[], None]) -def test_get_all_subnets_info_no_data(mocker, subtensor, result_): - """Test get_all_subnets_info returns empty list when no subnet information is found.""" +def test_neuron_for_uid_success(subtensor, mocker): + """Test neuron_for_uid successful call.""" # Prep - block = 123 - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" + fake_uid = 1 + fake_netuid = 2 + fake_block = 123 + mocked_neuron_from_vec_u8 = mocker.patch.object( + subtensor_module.NeuronInfo, "from_vec_u8" ) - mock_response = {"result": result_} - mocker.patch.object(subtensor.substrate, "rpc_request", return_value=mock_response) - mocker.patch.object(subtensor_module.SubnetInfo, "list_from_vec_u8") # Call - result = subtensor.get_all_subnets_info(block) + result = subtensor.neuron_for_uid( + uid=fake_uid, netuid=fake_netuid, block=fake_block + ) # Asserts - assert result == [] - subtensor.substrate.get_block_hash.assert_called_once_with(block) + subtensor.substrate.get_block_hash.assert_called_once_with(fake_block) subtensor.substrate.rpc_request.assert_called_once_with( - method="subnetInfo_getSubnetsInfo", params=["mock_block_hash"] + method="neuronInfo_getNeuron", + params=[fake_netuid, fake_uid, subtensor.substrate.get_block_hash.return_value], + ) + + mocked_neuron_from_vec_u8.assert_called_once_with( + subtensor.substrate.rpc_request.return_value.get.return_value ) - subtensor_module.SubnetInfo.list_from_vec_u8.assert_not_called() + assert result == mocked_neuron_from_vec_u8.return_value -def test_get_all_subnets_info_retry(mocker, subtensor): - """Test get_all_subnets_info retries on failure.""" +def test_do_serve_prometheus_is_success(subtensor, mocker): + """Successful do_serve_prometheus call.""" # Prep - block = 123 - subnet_data = [1, 2, 3] - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = True + + # Call + result = subtensor._do_serve_prometheus( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, ) - mock_response = {"result": subnet_data} - mock_rpc_request = mocker.patch.object( - subtensor.substrate, - "rpc_request", - side_effect=[Exception, Exception, mock_response], + + # Asserts + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_prometheus", + call_params=fake_call_params, ) - mocker.patch.object( - subtensor_module.SubnetInfo, "list_from_vec_u8", return_value=["some_data"] + + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, ) - # Call - result = subtensor.get_all_subnets_info(block) + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) - # Asserts - subtensor.substrate.get_block_hash.assert_called_with(block) - assert mock_rpc_request.call_count == 3 - subtensor_module.SubnetInfo.list_from_vec_u8.assert_called_once_with(subnet_data) - assert result == ["some_data"] + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == (True, None) -# `get_subnet_info` tests -def test_get_subnet_info_success(mocker, subtensor): - """Test get_subnet_info returns correct data when subnet information is found.""" +def test_do_serve_prometheus_is_not_success(subtensor, mocker): + """Unsuccessful do_serve_axon call.""" # Prep - netuid = 1 - block = 123 - subnet_data = [1, 2, 3] - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" - ) - mock_response = {"result": subnet_data} - mocker.patch.object(subtensor.substrate, "rpc_request", return_value=mock_response) - mocker.patch.object( - subtensor_module.SubnetInfo, "from_vec_u8", return_value=["from_vec_u8"] - ) + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = None # Call - result = subtensor.get_subnet_info(netuid, block) + result = subtensor._do_serve_prometheus( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) # Asserts - subtensor.substrate.get_block_hash.assert_called_once_with(block) - subtensor.substrate.rpc_request.assert_called_once_with( - method="subnetInfo_getSubnetInfo", params=[netuid, "mock_block_hash"] + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_prometheus", + call_params=fake_call_params, ) - subtensor_module.SubnetInfo.from_vec_u8.assert_called_once_with(subnet_data) + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, + ) -@pytest.mark.parametrize("result_", [None, {}]) -def test_get_subnet_info_no_data(mocker, subtensor, result_): - """Test get_subnet_info returns None when no subnet information is found.""" - # Prep - netuid = 1 - block = 123 - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == ( + False, + subtensor.substrate.submit_extrinsic.return_value.error_message, ) - mock_response = {"result": result_} - mocker.patch.object(subtensor.substrate, "rpc_request", return_value=mock_response) - mocker.patch.object(subtensor_module.SubnetInfo, "from_vec_u8") + + +def test_do_serve_prometheus_no_waits(subtensor, mocker): + """Unsuccessful do_serve_axon call.""" + # Prep + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = False + fake_wait_for_finalization = False # Call - result = subtensor.get_subnet_info(netuid, block) + result = subtensor._do_serve_prometheus( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) # Asserts - assert result is None - subtensor.substrate.get_block_hash.assert_called_once_with(block) - subtensor.substrate.rpc_request.assert_called_once_with( - method="subnetInfo_getSubnetInfo", params=[netuid, "mock_block_hash"] + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_prometheus", + call_params=fake_call_params, ) - subtensor_module.SubnetInfo.from_vec_u8.assert_not_called() - -def test_get_subnet_info_retry(mocker, subtensor): - """Test get_subnet_info retries on failure.""" - # Prep - netuid = 1 - block = 123 - subnet_data = [1, 2, 3] - mocker.patch.object( - subtensor.substrate, "get_block_hash", return_value="mock_block_hash" + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, ) - mock_response = {"result": subnet_data} - mock_rpc_request = mocker.patch.object( - subtensor.substrate, - "rpc_request", - side_effect=[Exception, Exception, mock_response], + + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, ) - mocker.patch.object( - subtensor_module.SubnetInfo, "from_vec_u8", return_value=["from_vec_u8"] + assert result == (True, None) + + +def test_serve_prometheus(subtensor, mocker): + """Test serve_prometheus function successful call.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_port = 1234 + fake_netuid = 1 + wait_for_inclusion = True + wait_for_finalization = False + + mocked_prometheus_extrinsic = mocker.patch.object( + subtensor_module, "prometheus_extrinsic" ) # Call - result = subtensor.get_subnet_info(netuid, block) + result = subtensor.serve_prometheus( + fake_wallet, + fake_port, + fake_netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) # Asserts - subtensor.substrate.get_block_hash.assert_called_with(block) - assert mock_rpc_request.call_count == 3 - subtensor_module.SubnetInfo.from_vec_u8.assert_called_once_with(subnet_data) + mocked_prometheus_extrinsic.assert_called_once_with( + subtensor, + wallet=fake_wallet, + port=fake_port, + netuid=fake_netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + assert result == mocked_prometheus_extrinsic.return_value -# `get_subnet_hyperparameters` tests -def test_get_subnet_hyperparameters_success(mocker, subtensor): - """Test get_subnet_hyperparameters returns correct data when hyperparameters are found.""" + +def test_do_serve_axon_is_success(subtensor, mocker): + """Successful do_serve_axon call.""" # Prep - netuid = 1 - block = 123 - hex_bytes_result = "0x010203" - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) - mocker.patch.object( - subtensor_module.SubnetHyperparameters, - "from_vec_u8", - return_value=["from_vec_u8"], - ) + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = True # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) + result = subtensor._do_serve_axon( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) # Asserts - subtensor.query_runtime_api.assert_called_once_with( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_axon", + call_params=fake_call_params, ) - subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( - bytes_result + + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, ) + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) -def test_get_subnet_hyperparameters_no_data(mocker, subtensor): - """Test get_subnet_hyperparameters returns empty list when no data is found.""" + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == (True, None) + + +def test_do_serve_axon_is_not_success(subtensor, mocker): + """Unsuccessful do_serve_axon call.""" # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_runtime_api", return_value=None) - mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = True + fake_wait_for_finalization = True + + subtensor.substrate.submit_extrinsic.return_value.is_success = None # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) + result = subtensor._do_serve_axon( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) # Asserts - assert result == [] - subtensor.query_runtime_api.assert_called_once_with( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_axon", + call_params=fake_call_params, + ) + + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, ) - subtensor_module.SubnetHyperparameters.from_vec_u8.assert_not_called() + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) -def test_get_subnet_hyperparameters_hex_without_prefix(mocker, subtensor): - """Test get_subnet_hyperparameters correctly processes hex string without '0x' prefix.""" + subtensor.substrate.submit_extrinsic.return_value.process_events.assert_called_once() + assert result == ( + False, + subtensor.substrate.submit_extrinsic.return_value.error_message, + ) + + +def test_do_serve_axon_no_waits(subtensor, mocker): + """Unsuccessful do_serve_axon call.""" # Prep - netuid = 1 - block = 123 - hex_bytes_result = "010203" - bytes_result = bytes.fromhex(hex_bytes_result) - mocker.patch.object(subtensor, "query_runtime_api", return_value=hex_bytes_result) - mocker.patch.object(subtensor_module.SubnetHyperparameters, "from_vec_u8") + fake_wallet = mocker.MagicMock() + fake_call_params = mocker.MagicMock() + fake_wait_for_inclusion = False + fake_wait_for_finalization = False # Call - result = subtensor.get_subnet_hyperparameters(netuid, block) + result = subtensor._do_serve_axon( + wallet=fake_wallet, + call_params=fake_call_params, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) # Asserts - subtensor.query_runtime_api.assert_called_once_with( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block=block, + subtensor.substrate.compose_call.assert_called_once_with( + call_module="SubtensorModule", + call_function="serve_axon", + call_params=fake_call_params, ) - subtensor_module.SubnetHyperparameters.from_vec_u8.assert_called_once_with( - bytes_result + + subtensor.substrate.create_signed_extrinsic.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.hotkey, ) + subtensor.substrate.submit_extrinsic.assert_called_once_with( + subtensor.substrate.create_signed_extrinsic.return_value, + wait_for_inclusion=fake_wait_for_inclusion, + wait_for_finalization=fake_wait_for_finalization, + ) + assert result == (True, None) -# `get_subnet_owner` tests -def test_get_subnet_owner_success(mocker, subtensor): - """Test get_subnet_owner returns correct data when owner information is found.""" - # Prep - netuid = 1 - block = 123 - owner_address = "5F3sa2TJAWMqDhXG6jhV4N8ko9rXPM6twz9mG9m3rrgq3xiJ" - mock_result = mocker.MagicMock(value=owner_address) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) + +def test_immunity_period(subtensor, mocker): + """Successful immunity_period call.""" + # Preps + fake_netuid = 1 + fake_block = 123 + fare_result = 101 + + mocked_get_hyperparameter = mocker.MagicMock() + mocked_get_hyperparameter.return_value = fare_result + subtensor._get_hyperparameter = mocked_get_hyperparameter # Call - result = subtensor.get_subnet_owner(netuid, block) + result = subtensor.immunity_period(netuid=fake_netuid, block=fake_block) - # Asserts - subtensor.query_subtensor.assert_called_once_with("SubnetOwner", block, [netuid]) - assert result == owner_address + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="ImmunityPeriod", + netuid=fake_netuid, + block=fake_block, + ) + assert result == mocked_get_hyperparameter.return_value -def test_get_subnet_owner_no_data(mocker, subtensor): - """Test get_subnet_owner returns None when no owner information is found.""" +def test_get_uid_for_hotkey_on_subnet(subtensor, mocker): + """Successful get_uid_for_hotkey_on_subnet call.""" # Prep - netuid = 1 - block = 123 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) + fake_hotkey_ss58 = "fake_hotkey_ss58" + fake_netuid = 1 + fake_block = 123 + mocked_query_subtensor = mocker.MagicMock() + subtensor.query_subtensor = mocked_query_subtensor # Call - result = subtensor.get_subnet_owner(netuid, block) + result = subtensor.get_uid_for_hotkey_on_subnet( + hotkey_ss58=fake_hotkey_ss58, netuid=fake_netuid, block=fake_block + ) - # Asserts - subtensor.query_subtensor.assert_called_once_with("SubnetOwner", block, [netuid]) - assert result is None + # Assertions + mocked_query_subtensor.assert_called_once_with( + "Uids", fake_block, [fake_netuid, fake_hotkey_ss58] + ) + assert result == mocked_query_subtensor.return_value.value -def test_get_subnet_owner_no_value_attribute(mocker, subtensor): - """Test get_subnet_owner returns None when result has no value attribute.""" - # Prep - netuid = 1 - block = 123 - mock_result = mocker.MagicMock() - del mock_result.value # Simulating a missing value attribute - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) + +def test_tempo(subtensor, mocker): + """Successful tempo call.""" + # Preps + fake_netuid = 1 + fake_block = 123 + fare_result = 101 + + mocked_get_hyperparameter = mocker.MagicMock() + mocked_get_hyperparameter.return_value = fare_result + subtensor._get_hyperparameter = mocked_get_hyperparameter # Call - result = subtensor.get_subnet_owner(netuid, block) + result = subtensor.tempo(netuid=fake_netuid, block=fake_block) - # Asserts - subtensor.query_subtensor.assert_called_once_with("SubnetOwner", block, [netuid]) - assert result is None + # Assertions + mocked_get_hyperparameter.assert_called_once_with( + param_name="Tempo", + netuid=fake_netuid, + block=fake_block, + ) + assert result == mocked_get_hyperparameter.return_value + + +def test_get_commitment(subtensor, mocker): + """Successful get_commitment call.""" + # Preps + fake_netuid = 1 + fake_uid = 2 + fake_block = 3 + fake_hotkey = "hotkey" + fake_hex_data = "0x010203" + expected_result = bytes.fromhex(fake_hex_data[2:]).decode() + + mocked_metagraph = mocker.MagicMock() + subtensor.metagraph = mocked_metagraph + mocked_metagraph.return_value.hotkeys = {fake_uid: fake_hotkey} + + mocked_get_metadata = mocker.patch.object(subtensor_module, "get_metadata") + mocked_get_metadata.return_value = { + "info": {"fields": [{fake_hex_data: fake_hex_data}]} + } + + # Call + result = subtensor.get_commitment( + netuid=fake_netuid, uid=fake_uid, block=fake_block + ) + # Assertions + mocked_metagraph.assert_called_once_with(fake_netuid) + assert result == expected_result -#################### -# Nomination tests # -#################### +def test_min_allowed_weights(subtensor, mocker): + """Successful min_allowed_weights call.""" + fake_netuid = 1 + fake_block = 123 + return_value = 10 -# `is_hotkey_delegate` tests -def test_is_hotkey_delegate_success(mocker, subtensor): - """Test is_hotkey_delegate returns True when hotkey is a delegate.""" - # Prep - hotkey_ss58 = "hotkey_ss58" - block = 123 - mock_delegates = [ - mocker.MagicMock(hotkey_ss58=hotkey_ss58), - mocker.MagicMock(hotkey_ss58="hotkey_ss583"), - ] - mocker.patch.object(subtensor, "get_delegates", return_value=mock_delegates) + mocked_get_hyperparameter = mocker.MagicMock(return_value=return_value) + subtensor._get_hyperparameter = mocked_get_hyperparameter # Call - result = subtensor.is_hotkey_delegate(hotkey_ss58, block) + result = subtensor.min_allowed_weights(netuid=fake_netuid, block=fake_block) - # Asserts - subtensor.get_delegates.assert_called_once_with(block=block) - assert result is True + # Assertion + mocked_get_hyperparameter.assert_called_once_with( + param_name="MinAllowedWeights", block=fake_block, netuid=fake_netuid + ) + assert result == return_value -def test_is_hotkey_delegate_not_found(mocker, subtensor): - """Test is_hotkey_delegate returns False when hotkey is not a delegate.""" - # Prep - hotkey_ss58 = "hotkey_ss58" - block = 123 - mock_delegates = [mocker.MagicMock(hotkey_ss58="hotkey_ss583")] - mocker.patch.object(subtensor, "get_delegates", return_value=mock_delegates) +def test_max_weight_limit(subtensor, mocker): + """Successful max_weight_limit call.""" + fake_netuid = 1 + fake_block = 123 + return_value = 100 + + mocked_get_hyperparameter = mocker.MagicMock(return_value=return_value) + subtensor._get_hyperparameter = mocked_get_hyperparameter + + mocked_u16_normalized_float = mocker.MagicMock() + subtensor_module.u16_normalized_float = mocked_u16_normalized_float # Call - result = subtensor.is_hotkey_delegate(hotkey_ss58, block) + result = subtensor.max_weight_limit(netuid=fake_netuid, block=fake_block) - # Asserts - subtensor.get_delegates.assert_called_once_with(block=block) - assert result is False + # Assertion + mocked_get_hyperparameter.assert_called_once_with( + param_name="MaxWeightsLimit", block=fake_block, netuid=fake_netuid + ) + assert result == mocked_u16_normalized_float.return_value -# `get_delegate_take` tests -def test_get_delegate_take_success(mocker, subtensor): - """Test get_delegate_take returns correct data when delegate take is found.""" - # Prep - hotkey_ss58 = "hotkey_ss58" - block = 123 - delegate_take_value = 32768 - mock_result = mocker.MagicMock(value=delegate_take_value) - mocker.patch.object(subtensor, "query_subtensor", return_value=mock_result) - spy_u16_normalized_float = mocker.spy(subtensor_module, "U16_NORMALIZED_FLOAT") +def test_get_transfer_fee(subtensor, mocker): + """Successful get_transfer_fee call.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_dest = "SS58ADDRESS" + value = 1 + + fake_payment_info = {"partialFee": int(2e10)} + subtensor.substrate.get_payment_info.return_value = fake_payment_info # Call - subtensor.get_delegate_take(hotkey_ss58, block) + result = subtensor.get_transfer_fee(wallet=fake_wallet, dest=fake_dest, value=value) # Asserts - subtensor.query_subtensor.assert_called_once_with("Delegates", block, [hotkey_ss58]) - spy_u16_normalized_float.assert_called_once_with(delegate_take_value) + subtensor.substrate.compose_call.assert_called_once_with( + call_module="Balances", + call_function="transfer_allow_death", + call_params={"dest": fake_dest, "value": value}, + ) + subtensor.substrate.get_payment_info.assert_called_once_with( + call=subtensor.substrate.compose_call.return_value, + keypair=fake_wallet.coldkeypub, + ) -def test_get_delegate_take_no_data(mocker, subtensor): - """Test get_delegate_take returns None when no delegate take is found.""" - # Prep - hotkey_ss58 = "hotkey_ss58" - block = 123 - delegate_take_value = 32768 - mocker.patch.object(subtensor, "query_subtensor", return_value=None) - spy_u16_normalized_float = mocker.spy(subtensor_module, "U16_NORMALIZED_FLOAT") + assert result == 2e10 + + +def test_get_transfer_fee_incorrect_value(subtensor, mocker): + """Successful get_transfer_fee call.""" + # Preps + fake_wallet = mocker.MagicMock() + fake_dest = mocker.MagicMock() + value = "no_int_no_float_no_Balance" + + mocked_substrate = mocker.MagicMock() + subtensor.substrate = mocked_substrate + spy_balance_from_rao = mocker.spy(Balance, "from_rao") # Call - result = subtensor.get_delegate_take(hotkey_ss58, block) + result = subtensor.get_transfer_fee(wallet=fake_wallet, dest=fake_dest, value=value) # Asserts - subtensor.query_subtensor.assert_called_once_with("Delegates", block, [hotkey_ss58]) - spy_u16_normalized_float.assert_not_called() - assert result is None + spy_balance_from_rao.assert_called_once_with(2e7) + assert result == Balance.from_rao(int(2e7)) -def test_get_remaining_arbitration_period(subtensor, mocker): - """Tests successful retrieval of total stake for hotkey.""" + +def test_get_existential_deposit(subtensor, mocker): + """Successful get_existential_deposit call.""" # Prep - subtensor.query_subtensor = mocker.MagicMock(return_value=mocker.MagicMock(value=0)) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" + block = 123 + + mocked_query_constant = mocker.MagicMock() + value = 10 + mocked_query_constant.return_value.value = value + subtensor.query_constant = mocked_query_constant # Call - result = subtensor.get_remaining_arbitration_period(coldkey_ss58=fake_ss58_address) + result = subtensor.get_existential_deposit(block=block) # Assertions - subtensor.query_subtensor.assert_called_once_with( - name="ColdkeyArbitrationBlock", block=None, params=[fake_ss58_address] + mocked_query_constant.assert_called_once_with( + module_name="Balances", constant_name="ExistentialDeposit", block=block ) - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert result == 0 + assert isinstance(result, Balance) + assert result == Balance.from_rao(value) -def test_get_remaining_arbitration_period_happy(subtensor, mocker): - """Tests successful retrieval of total stake for hotkey.""" - # Prep - subtensor.query_subtensor = mocker.MagicMock( - return_value=mocker.MagicMock(value=2000) + +def test_commit_weights(subtensor, mocker): + """Successful commit_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + netuid = 1 + salt = [1, 3] + uids = [2, 4] + weights = [0.4, 0.6] + wait_for_inclusion = False + wait_for_finalization = False + prompt = False + max_retries = 5 + + expected_result = (True, None) + mocked_generate_weight_hash = mocker.patch.object( + subtensor_module, "generate_weight_hash", return_value=expected_result + ) + mocked_commit_weights_extrinsic = mocker.patch.object( + subtensor_module, "commit_weights_extrinsic", return_value=expected_result ) - fake_ss58_address = "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" # Call - result = subtensor.get_remaining_arbitration_period( - coldkey_ss58=fake_ss58_address, block=200 + result = subtensor.commit_weights( + wallet=fake_wallet, + netuid=netuid, + salt=salt, + uids=uids, + weights=weights, + version_key=settings.version_as_int, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + max_retries=max_retries, ) - # Assertions - subtensor.query_subtensor.assert_called_once_with( - name="ColdkeyArbitrationBlock", block=200, params=[fake_ss58_address] + # Asserts + mocked_generate_weight_hash.assert_called_once_with( + address=fake_wallet.hotkey.ss58_address, + netuid=netuid, + uids=list(uids), + values=list(weights), + salt=list(salt), + version_key=settings.version_as_int, ) - # if we change the methods logic in the future we have to be make sure the returned type is correct - assert result == 1800 # 2000 - 200 - -def test_get_delegate_identities(subtensor, mocker): - """Tests the get_delegate_identities method to ensure it correctly retrieves and decodes delegate identities.""" - # Preps + mocked_commit_weights_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + commit_hash=mocked_generate_weight_hash.return_value, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + assert result == expected_result - subtensor.substrate = mocker.MagicMock() - fake_info_dictionary = { - "additional": [ - ( - {"Raw11": "0x6465736372697074696f6e"}, - {"Raw64": "0x46616b65206465736372697074696f6e"}, - ) - ], - "display": {"Raw17": "0x46616b654e616d65"}, - "legal": "None", - "web": {"Raw22": "0x687474703a2f2f7777772e626c61626c612d746573742e636f6d"}, - "riot": "None", - "email": "None", - "pgp_fingerprint": None, - "image": "None", - "twitter": "None", - } - expected_return = { - "additional": [("description", "Fake description")], - "display": "FakeName", - "legal": "None", - "web": "http://www.blabla-test.com", - "riot": "None", - "email": "None", - "pgp_fingerprint": None, - "image": "None", - "twitter": "None", - } +def test_reveal_weights(subtensor, mocker): + """Successful test_reveal_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + netuid = 1 + uids = [1, 2, 3, 4] + weights = [0.1, 0.2, 0.3, 0.4] + salt = [4, 2, 2, 1] + expected_result = (True, None) + mocked_extrinsic = mocker.patch.object( + subtensor_module, "reveal_weights_extrinsic", return_value=expected_result + ) - @dataclasses.dataclass - class FakeQueryReturn: - value: Any + # Call + result = subtensor.reveal_weights( + wallet=fake_wallet, + netuid=netuid, + uids=uids, + weights=weights, + salt=salt, + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=False, + ) - fake_value = {"info": fake_info_dictionary} - mocked_query_map = [ - [FakeQueryReturn("fake_address"), FakeQueryReturn(fake_value)], - ] + # Assertions + assert result == (True, None) + mocked_extrinsic.assert_called_once_with( + subtensor=subtensor, + wallet=fake_wallet, + netuid=netuid, + uids=uids, + version_key=version_as_int, + weights=weights, + salt=salt, + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=False, + ) + + +def test_reveal_weights_false(subtensor, mocker): + """Failed test_reveal_weights call.""" + # Preps + fake_wallet = mocker.MagicMock() + netuid = 1 + uids = [1, 2, 3, 4] + weights = [0.1, 0.2, 0.3, 0.4] + salt = [4, 2, 2, 1] - mocker.patch.object(subtensor.substrate, "query_map", return_value=mocked_query_map) - spy_from_chain_data = mocker.spy(utils.DelegatesDetails, "from_chain_data") - spy_decode_hex_identity_dict = mocker.spy(wallet_utils, "decode_hex_identity_dict") + expected_result = ( + False, + "No attempt made. Perhaps it is too soon to reveal weights!", + ) + mocked_extrinsic = mocker.patch.object(subtensor_module, "reveal_weights_extrinsic") - # Calls - result = subtensor.get_delegate_identities() + # Call + result = subtensor.reveal_weights( + wallet=fake_wallet, + netuid=netuid, + uids=uids, + weights=weights, + salt=salt, + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=False, + ) - # Assertions - spy_decode_hex_identity_dict.assert_called_once_with(fake_info_dictionary) - spy_from_chain_data.assert_called_once_with(expected_return) - assert result == { - "fake_address": utils.DelegatesDetails.from_chain_data(expected_return) - } + # Assertion + assert result == expected_result + assert mocked_extrinsic.call_count == 5 def test_connect_without_substrate(mocker): diff --git a/tests/unit_tests/test_synapse.py b/tests/unit_tests/test_synapse.py index b0ce4f132..80c127c58 100644 --- a/tests/unit_tests/test_synapse.py +++ b/tests/unit_tests/test_synapse.py @@ -1,28 +1,31 @@ # The MIT License (MIT) -# Copyright © 2022 Opentensor Foundation - +# 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 json + import base64 -import pytest -import bittensor +import json from typing import Optional, ClassVar +import pytest + +from bittensor.core.synapse import Synapse + def test_parse_headers_to_inputs(): - class Test(bittensor.Synapse): + class Test(Synapse): key1: list[int] # Define a mock headers dictionary to use for testing @@ -57,7 +60,7 @@ class Test(bittensor.Synapse): def test_from_headers(): - class Test(bittensor.Synapse): + class Test(Synapse): key1: list[int] # Define a mock headers dictionary to use for testing @@ -93,10 +96,10 @@ class Test(bittensor.Synapse): def test_synapse_create(): # Create an instance of Synapse - synapse = bittensor.Synapse() + synapse = Synapse() # Ensure the instance created is of type Synapse - assert isinstance(synapse, bittensor.Synapse) + assert isinstance(synapse, Synapse) # Check default properties of a newly created Synapse assert synapse.name == "Synapse" @@ -125,7 +128,7 @@ def test_synapse_create(): def test_custom_synapse(): # Define a custom Synapse subclass - class Test(bittensor.Synapse): + class Test(Synapse): a: int # Carried through because required. b: int = None # Not carried through headers c: Optional[int] # Required, carried through headers, cannot be None @@ -177,7 +180,7 @@ class Test(bittensor.Synapse): def test_body_hash_override(): # Create a Synapse instance - synapse_instance = bittensor.Synapse() + synapse_instance = Synapse() # Try to set the body_hash property and expect an AttributeError with pytest.raises( @@ -188,7 +191,7 @@ def test_body_hash_override(): def test_default_instance_fields_dict_consistency(): - synapse_instance = bittensor.Synapse() + synapse_instance = Synapse() assert synapse_instance.model_dump() == { "name": "Synapse", "timeout": 12.0, @@ -222,7 +225,7 @@ def test_default_instance_fields_dict_consistency(): } -class LegacyHashedSynapse(bittensor.Synapse): +class LegacyHashedSynapse(Synapse): """Legacy Synapse subclass that serialized `required_hash_fields`.""" a: int @@ -232,7 +235,7 @@ class LegacyHashedSynapse(bittensor.Synapse): required_hash_fields: Optional[list[str]] = ["b", "a", "d"] -class HashedSynapse(bittensor.Synapse): +class HashedSynapse(Synapse): a: int b: int c: Optional[int] = None diff --git a/tests/unit_tests/test_tensor.py b/tests/unit_tests/test_tensor.py index 9939b397e..8bf7bf06a 100644 --- a/tests/unit_tests/test_tensor.py +++ b/tests/unit_tests/test_tensor.py @@ -1,25 +1,27 @@ # The MIT License (MIT) -# Copyright © 2022 Opentensor Foundation - +# 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 pytest -import numpy as np -import bittensor + import numpy +import numpy as np +import pytest import torch +from bittensor.core.tensor import Tensor + # This is a fixture that creates an example tensor for testing @pytest.fixture @@ -28,16 +30,16 @@ def example_tensor(): data = np.array([1, 2, 3, 4]) # Serialize the tensor into a Tensor instance and return it - return bittensor.tensor(data) + return Tensor.serialize(data) @pytest.fixture -def example_tensor_torch(force_legacy_torch_compat_api): +def example_tensor_torch(force_legacy_torch_compatible_api): # Create a tensor from a list using PyTorch data = torch.tensor([1, 2, 3, 4]) # Serialize the tensor into a Tensor instance and return it - return bittensor.tensor(data) + return Tensor.serialize(data) def test_deserialize(example_tensor): @@ -49,7 +51,7 @@ def test_deserialize(example_tensor): assert tensor.tolist() == [1, 2, 3, 4] -def test_deserialize_torch(example_tensor_torch, force_legacy_torch_compat_api): +def test_deserialize_torch(example_tensor_torch, force_legacy_torch_compatible_api): tensor = example_tensor_torch.deserialize() # Check that the result is a PyTorch tensor with the correct values assert isinstance(tensor, torch.Tensor) @@ -58,7 +60,7 @@ def test_deserialize_torch(example_tensor_torch, force_legacy_torch_compat_api): def test_serialize(example_tensor): # Check that the serialized tensor is an instance of Tensor - assert isinstance(example_tensor, bittensor.Tensor) + assert isinstance(example_tensor, Tensor) # Check that the Tensor instance has the correct buffer, dtype, and shape assert example_tensor.buffer == example_tensor.buffer @@ -87,9 +89,9 @@ def test_serialize(example_tensor): assert example_tensor.shape == example_tensor.shape -def test_serialize_torch(example_tensor_torch, force_legacy_torch_compat_api): +def test_serialize_torch(example_tensor_torch, force_legacy_torch_compatible_api): # Check that the serialized tensor is an instance of Tensor - assert isinstance(example_tensor_torch, bittensor.Tensor) + assert isinstance(example_tensor_torch, Tensor) # Check that the Tensor instance has the correct buffer, dtype, and shape assert example_tensor_torch.buffer == example_tensor_torch.buffer @@ -120,7 +122,7 @@ def test_serialize_torch(example_tensor_torch, force_legacy_torch_compat_api): def test_buffer_field(): # Create a Tensor instance with a specified buffer, dtype, and shape - tensor = bittensor.Tensor( + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="float32", shape=[3, 3] ) @@ -128,9 +130,9 @@ def test_buffer_field(): assert tensor.buffer == "0x321e13edqwds231231231232131" -def test_buffer_field_torch(force_legacy_torch_compat_api): +def test_buffer_field_torch(force_legacy_torch_compatible_api): # Create a Tensor instance with a specified buffer, dtype, and shape - tensor = bittensor.Tensor( + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="torch.float32", shape=[3, 3] ) @@ -140,7 +142,7 @@ def test_buffer_field_torch(force_legacy_torch_compat_api): def test_dtype_field(): # Create a Tensor instance with a specified buffer, dtype, and shape - tensor = bittensor.Tensor( + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="float32", shape=[3, 3] ) @@ -148,8 +150,8 @@ def test_dtype_field(): assert tensor.dtype == "float32" -def test_dtype_field_torch(force_legacy_torch_compat_api): - tensor = bittensor.Tensor( +def test_dtype_field_torch(force_legacy_torch_compatible_api): + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="torch.float32", shape=[3, 3] ) assert tensor.dtype == "torch.float32" @@ -157,7 +159,7 @@ def test_dtype_field_torch(force_legacy_torch_compat_api): def test_shape_field(): # Create a Tensor instance with a specified buffer, dtype, and shape - tensor = bittensor.Tensor( + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="float32", shape=[3, 3] ) @@ -165,79 +167,79 @@ def test_shape_field(): assert tensor.shape == [3, 3] -def test_shape_field_torch(force_legacy_torch_compat_api): - tensor = bittensor.Tensor( +def test_shape_field_torch(force_legacy_torch_compatible_api): + tensor = Tensor( buffer="0x321e13edqwds231231231232131", dtype="torch.float32", shape=[3, 3] ) assert tensor.shape == [3, 3] def test_serialize_all_types(): - bittensor.tensor(np.array([1], dtype=np.float16)) - bittensor.tensor(np.array([1], dtype=np.float32)) - bittensor.tensor(np.array([1], dtype=np.float64)) - bittensor.tensor(np.array([1], dtype=np.uint8)) - bittensor.tensor(np.array([1], dtype=np.int32)) - bittensor.tensor(np.array([1], dtype=np.int64)) - bittensor.tensor(np.array([1], dtype=bool)) + Tensor.serialize(np.array([1], dtype=np.float16)) + Tensor.serialize(np.array([1], dtype=np.float32)) + Tensor.serialize(np.array([1], dtype=np.float64)) + Tensor.serialize(np.array([1], dtype=np.uint8)) + Tensor.serialize(np.array([1], dtype=np.int32)) + Tensor.serialize(np.array([1], dtype=np.int64)) + Tensor.serialize(np.array([1], dtype=bool)) -def test_serialize_all_types_torch(force_legacy_torch_compat_api): - bittensor.tensor(torch.tensor([1], dtype=torch.float16)) - bittensor.tensor(torch.tensor([1], dtype=torch.float32)) - bittensor.tensor(torch.tensor([1], dtype=torch.float64)) - bittensor.tensor(torch.tensor([1], dtype=torch.uint8)) - bittensor.tensor(torch.tensor([1], dtype=torch.int32)) - bittensor.tensor(torch.tensor([1], dtype=torch.int64)) - bittensor.tensor(torch.tensor([1], dtype=torch.bool)) +def test_serialize_all_types_torch(force_legacy_torch_compatible_api): + Tensor.serialize(torch.tensor([1], dtype=torch.float16)) + Tensor.serialize(torch.tensor([1], dtype=torch.float32)) + Tensor.serialize(torch.tensor([1], dtype=torch.float64)) + Tensor.serialize(torch.tensor([1], dtype=torch.uint8)) + Tensor.serialize(torch.tensor([1], dtype=torch.int32)) + Tensor.serialize(torch.tensor([1], dtype=torch.int64)) + Tensor.serialize(torch.tensor([1], dtype=torch.bool)) def test_serialize_all_types_equality(): rng = np.random.default_rng() tensor = rng.standard_normal((100,), dtype=np.float32) - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) tensor = rng.standard_normal((100,), dtype=np.float64) - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) tensor = np.random.randint(255, 256, (1000,), dtype=np.uint8) - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) tensor = np.random.randint(2_147_483_646, 2_147_483_647, (1000,), dtype=np.int32) - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) tensor = np.random.randint( 9_223_372_036_854_775_806, 9_223_372_036_854_775_807, (1000,), dtype=np.int64 ) - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) tensor = rng.standard_normal((100,), dtype=np.float32) < 0.5 - assert np.all(bittensor.tensor(tensor).tensor() == tensor) + assert np.all(Tensor.serialize(tensor).tensor() == tensor) -def test_serialize_all_types_equality_torch(force_legacy_torch_compat_api): +def test_serialize_all_types_equality_torch(force_legacy_torch_compatible_api): torchtensor = torch.randn([100], dtype=torch.float16) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) torchtensor = torch.randn([100], dtype=torch.float32) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) torchtensor = torch.randn([100], dtype=torch.float64) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) torchtensor = torch.randint(255, 256, (1000,), dtype=torch.uint8) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) torchtensor = torch.randint( 2_147_483_646, 2_147_483_647, (1000,), dtype=torch.int32 ) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) torchtensor = torch.randint( 9_223_372_036_854_775_806, 9_223_372_036_854_775_807, (1000,), dtype=torch.int64 ) - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) torchtensor = torch.randn([100], dtype=torch.float32) < 0.5 - assert torch.all(bittensor.tensor(torchtensor).tensor() == torchtensor) + assert torch.all(Tensor.serialize(torchtensor).tensor() == torchtensor) diff --git a/tests/unit_tests/test_wallet.py b/tests/unit_tests/test_wallet.py deleted file mode 100644 index 0d0466e34..000000000 --- a/tests/unit_tests/test_wallet.py +++ /dev/null @@ -1,517 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2022 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 json -import time -import pytest -import random -import re -import bittensor -from bittensor.errors import KeyFileError -from rich.prompt import Confirm -from ansible_vault import Vault -from unittest.mock import patch - - -def legacy_encrypt_keyfile_data(keyfile_data: bytes, password: str = None) -> bytes: - console = bittensor.__console__ - with console.status(":locked_with_key: Encrypting key..."): - vault = Vault(password) - return vault.vault.encrypt(keyfile_data) - - -def create_wallet(default_updated_password): - # create an nacl wallet - wallet = bittensor.wallet( - name=f"mock-{str(time.time())}", - path="/tmp/tests_wallets/do_not_use", - ) - with patch.object( - bittensor, - "ask_password_to_encrypt", - return_value=default_updated_password, - ): - wallet.create() - assert "NaCl" in str(wallet.coldkey_file) - - return wallet - - -def create_legacy_wallet(default_legacy_password=None, legacy_password=None): - def _legacy_encrypt_keyfile_data(*args, **kwargs): - args = { - k: v - for k, v in zip( - legacy_encrypt_keyfile_data.__code__.co_varnames[: len(args)], - args, - ) - } - kwargs = {**args, **kwargs} - kwargs["password"] = legacy_password - return legacy_encrypt_keyfile_data(**kwargs) - - legacy_wallet = bittensor.wallet( - name=f"mock-legacy-{str(time.time())}", - path="/tmp/tests_wallets/do_not_use", - ) - legacy_password = ( - default_legacy_password if legacy_password == None else legacy_password - ) - - # create a legacy ansible wallet - with patch.object( - bittensor, - "encrypt_keyfile_data", - new=_legacy_encrypt_keyfile_data, - # new = TestWalletUpdate.legacy_encrypt_keyfile_data, - ): - legacy_wallet.create() - assert "Ansible" in str(legacy_wallet.coldkey_file) - - return legacy_wallet - - -@pytest.fixture -def wallet_update_setup(): - # Setup the default passwords and wallets - default_updated_password = "nacl_password" - default_legacy_password = "ansible_password" - empty_wallet = bittensor.wallet( - name=f"mock-empty-{str(time.time())}", - path="/tmp/tests_wallets/do_not_use", - ) - legacy_wallet = create_legacy_wallet( - default_legacy_password=default_legacy_password - ) - wallet = create_wallet(default_updated_password) - - return { - "default_updated_password": default_updated_password, - "default_legacy_password": default_legacy_password, - "empty_wallet": empty_wallet, - "legacy_wallet": legacy_wallet, - "wallet": wallet, - } - - -def test_encrypt_and_decrypt(): - """Test message can be encrypted and decrypted successfully with ansible/nacl.""" - json_data = { - "address": "This is the address.", - "id": "This is the id.", - "key": "This is the key.", - } - message = json.dumps(json_data).encode() - - # encrypt and decrypt with nacl - encrypted_message = bittensor.encrypt_keyfile_data(message, "password") - decrypted_message = bittensor.decrypt_keyfile_data(encrypted_message, "password") - assert decrypted_message == message - assert bittensor.keyfile_data_is_encrypted(encrypted_message) - assert not bittensor.keyfile_data_is_encrypted(decrypted_message) - assert not bittensor.keyfile_data_is_encrypted_ansible(decrypted_message) - assert bittensor.keyfile_data_is_encrypted_nacl(encrypted_message) - - # encrypt and decrypt with legacy ansible - encrypted_message = legacy_encrypt_keyfile_data(message, "password") - decrypted_message = bittensor.decrypt_keyfile_data(encrypted_message, "password") - assert decrypted_message == message - assert bittensor.keyfile_data_is_encrypted(encrypted_message) - assert not bittensor.keyfile_data_is_encrypted(decrypted_message) - assert not bittensor.keyfile_data_is_encrypted_nacl(decrypted_message) - assert bittensor.keyfile_data_is_encrypted_ansible(encrypted_message) - - -def test_check_and_update_encryption_not_updated(wallet_update_setup): - """Test for a few cases where wallet should not be updated. - 1. When the wallet is already updated. - 2. When it is the hotkey. - 3. When the wallet is empty. - 4. When the wallet is legacy but no prompt to ask for password. - 5. When the password is wrong. - """ - wallet = wallet_update_setup["wallet"] - empty_wallet = wallet_update_setup["empty_wallet"] - legacy_wallet = wallet_update_setup["legacy_wallet"] - default_legacy_password = wallet_update_setup["default_legacy_password"] - # test the checking with no rewriting needs to be done. - with patch("bittensor.encrypt_keyfile_data") as encrypt: - # self.wallet is already the most updated with nacl encryption. - assert wallet.coldkey_file.check_and_update_encryption() - - # hotkey_file is not encrypted, thus do not need to be updated. - assert not wallet.hotkey_file.check_and_update_encryption() - - # empty_wallet has not been created, thus do not need to be updated. - assert not empty_wallet.coldkey_file.check_and_update_encryption() - - # legacy wallet cannot be updated without asking for password form prompt. - assert not legacy_wallet.coldkey_file.check_and_update_encryption( - no_prompt=True - ) - - # Wrong password - legacy_wallet = create_legacy_wallet( - default_legacy_password=default_legacy_password - ) - with patch("getpass.getpass", return_value="wrong_password"), patch.object( - Confirm, "ask", return_value=False - ): - assert not legacy_wallet.coldkey_file.check_and_update_encryption() - - # no renewal has been done in this test. - assert not encrypt.called - - -def test_check_and_update_excryption(wallet_update_setup, legacy_wallet=None): - """Test for the alignment of the updated VS old wallet. - 1. Same coldkey_file data. - 2. Same coldkey path. - 3. Same hotkey_file data. - 4. Same hotkey path. - 5. same password. - - Read the updated wallet in 2 ways. - 1. Directly as the output of check_and_update_encryption() - 2. Read from file using the same coldkey and hotkey name - """ - default_legacy_password = wallet_update_setup["default_legacy_password"] - - def check_new_coldkey_file(keyfile): - new_keyfile_data = keyfile._read_keyfile_data_from_file() - new_decrypted_keyfile_data = bittensor.decrypt_keyfile_data( - new_keyfile_data, legacy_password - ) - new_path = legacy_wallet.coldkey_file.path - - assert old_coldkey_file_data != None - assert new_keyfile_data != None - assert not old_coldkey_file_data == new_keyfile_data - assert bittensor.keyfile_data_is_encrypted_ansible(old_coldkey_file_data) - assert bittensor.keyfile_data_is_encrypted_nacl(new_keyfile_data) - assert not bittensor.keyfile_data_is_encrypted_nacl(old_coldkey_file_data) - assert not bittensor.keyfile_data_is_encrypted_ansible(new_keyfile_data) - assert old_decrypted_coldkey_file_data == new_decrypted_keyfile_data - assert new_path == old_coldkey_path - - def check_new_hotkey_file(keyfile): - new_keyfile_data = keyfile._read_keyfile_data_from_file() - new_path = legacy_wallet.hotkey_file.path - - assert old_hotkey_file_data == new_keyfile_data - assert new_path == old_hotkey_path - assert not bittensor.keyfile_data_is_encrypted(new_keyfile_data) - - if legacy_wallet == None: - legacy_password = f"PASSword-{random.randint(0, 10000)}" - legacy_wallet = create_legacy_wallet(legacy_password=legacy_password) - - else: - legacy_password = default_legacy_password - - # get old cold keyfile data - old_coldkey_file_data = legacy_wallet.coldkey_file._read_keyfile_data_from_file() - old_decrypted_coldkey_file_data = bittensor.decrypt_keyfile_data( - old_coldkey_file_data, legacy_password - ) - old_coldkey_path = legacy_wallet.coldkey_file.path - - # get old hot keyfile data - old_hotkey_file_data = legacy_wallet.hotkey_file._read_keyfile_data_from_file() - old_hotkey_path = legacy_wallet.hotkey_file.path - - # update legacy_wallet from ansible to nacl - with patch("getpass.getpass", return_value=legacy_password), patch.object( - Confirm, "ask", return_value=True - ): - legacy_wallet.coldkey_file.check_and_update_encryption() - - # get new keyfile data from the same legacy wallet - check_new_coldkey_file(legacy_wallet.coldkey_file) - check_new_hotkey_file(legacy_wallet.hotkey_file) - - # get new keyfile data from wallet name - updated_legacy_wallet = bittensor.wallet( - name=legacy_wallet.name, - hotkey=legacy_wallet.hotkey_str, - path="/tmp/tests_wallets/do_not_use", - ) - check_new_coldkey_file(updated_legacy_wallet.coldkey_file) - check_new_hotkey_file(updated_legacy_wallet.hotkey_file) - - # def test_password_retain(self): - # [tick] test the same password works - # [tick] try to read using the same hotkey/coldkey name - # [tick] test the same keyfile data could be retained - # [tick] test what if a wrong password was inserted - # [no need] try to read from the new file path - # [tick] test the old and new encrypted is not the same - # [tick] test that the hotkeys are not affected - - -@pytest.fixture -def mock_wallet(): - wallet = bittensor.wallet( - name=f"mock-{str(time.time())}", - hotkey=f"mock-{str(time.time())}", - path="/tmp/tests_wallets/do_not_use", - ) - wallet.create_new_coldkey(use_password=False, overwrite=True, suppress=True) - wallet.create_new_hotkey(use_password=False, overwrite=True, suppress=True) - - return wallet - - -def test_regen_coldkeypub_from_ss58_addr(mock_wallet): - """Test the `regenerate_coldkeypub` method of the wallet class, which regenerates the cold key pair from an SS58 address. - It checks whether the `set_coldkeypub` method is called with the expected arguments, and verifies that the generated key pair's SS58 address matches the input SS58 address. - It also tests the behavior when an invalid SS58 address is provided, raising a `ValueError` as expected. - """ - ss58_address = "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" - with patch.object(mock_wallet, "set_coldkeypub") as mock_set_coldkeypub: - mock_wallet.regenerate_coldkeypub( - ss58_address=ss58_address, overwrite=True, suppress=True - ) - - mock_set_coldkeypub.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkeypub.call_args_list[0][0][0] - assert keypair.ss58_address == ss58_address - - ss58_address_bad = ( - "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zx" # 1 character short - ) - with pytest.raises(ValueError): - mock_wallet.regenerate_coldkeypub( - ss58_address=ss58_address_bad, overwrite=True, suppress=True - ) - - -def test_regen_coldkeypub_from_hex_pubkey_str(mock_wallet): - """Test the `regenerate_coldkeypub` method of the wallet class, which regenerates the cold key pair from a hex public key string. - It checks whether the `set_coldkeypub` method is called with the expected arguments, and verifies that the generated key pair's public key matches the input public key. - It also tests the behavior when an invalid public key string is provided, raising a `ValueError` as expected. - """ - pubkey_str = "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - with patch.object(mock_wallet, "set_coldkeypub") as mock_set_coldkeypub: - mock_wallet.regenerate_coldkeypub( - public_key=pubkey_str, overwrite=True, suppress=True - ) - - mock_set_coldkeypub.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkeypub.call_args_list[0][0][0] - assert "0x" + keypair.public_key.hex() == pubkey_str - - pubkey_str_bad = "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512" # 1 character short - with pytest.raises(ValueError): - mock_wallet.regenerate_coldkeypub( - ss58_address=pubkey_str_bad, overwrite=True, suppress=True - ) - - -def test_regen_coldkeypub_from_hex_pubkey_bytes(mock_wallet): - """Test the `regenerate_coldkeypub` method of the wallet class, which regenerates the cold key pair from a hex public key byte string. - It checks whether the `set_coldkeypub` method is called with the expected arguments, and verifies that the generated key pair's public key matches the input public key. - """ - pubkey_str = "0x32939b6abc4d81f02dff04d2b8d1d01cc8e71c5e4c7492e4fa6a238cdca3512f" - pubkey_bytes = bytes.fromhex(pubkey_str[2:]) # Remove 0x from beginning - with patch.object(mock_wallet, "set_coldkeypub") as mock_set_coldkeypub: - mock_wallet.regenerate_coldkeypub( - public_key=pubkey_bytes, overwrite=True, suppress=True - ) - - mock_set_coldkeypub.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkeypub.call_args_list[0][0][0] - assert keypair.public_key == pubkey_bytes - - -def test_regen_coldkeypub_no_pubkey(mock_wallet): - """Test the `regenerate_coldkeypub` method of the wallet class when no public key is provided. - It verifies that a `ValueError` is raised when neither a public key nor an SS58 address is provided. - """ - with pytest.raises(ValueError): - # Must provide either public_key or ss58_address - mock_wallet.regenerate_coldkeypub( - ss58_address=None, public_key=None, overwrite=True, suppress=True - ) - - -def test_regen_coldkey_from_hex_seed_str(mock_wallet): - """Test the `regenerate_coldkey` method of the wallet class, which regenerates the cold key pair from a hex seed string. - It checks whether the `set_coldkey` method is called with the expected arguments, and verifies that the generated key pair's seed and SS58 address match the input seed and the expected SS58 address. - It also tests the behavior when an invalid seed string is provided, raising a `ValueError` as expected. - """ - ss58_addr = "5D5cwd8DX6ij7nouVcoxDuWtJfiR1BnzCkiBVTt7DU8ft5Ta" - seed_str = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f7636" - with patch.object(mock_wallet, "set_coldkey") as mock_set_coldkey: - mock_wallet.regenerate_coldkey(seed=seed_str, overwrite=True, suppress=True) - - mock_set_coldkey.assert_called_once() - keypair: bittensor.Keypair = mock_set_coldkey.call_args_list[0][0][0] - seed_hex = ( - keypair.seed_hex - if isinstance(keypair.seed_hex, str) - else keypair.seed_hex.hex() - ) - - assert re.match( - rf"(0x|){seed_str[2:]}", seed_hex - ), "The seed_hex does not match the expected pattern" - assert ( - keypair.ss58_address == ss58_addr - ) # Check that the ss58 address is correct - - seed_str_bad = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f763" # 1 character short - with pytest.raises(ValueError): - mock_wallet.regenerate_coldkey(seed=seed_str_bad, overwrite=True, suppress=True) - - -def test_regen_hotkey_from_hex_seed_str(mock_wallet): - """Test the `regenerate_coldkey` method of the wallet class, which regenerates the cold key pair from a hex seed string. - It checks whether the `set_coldkey` method is called with the expected arguments, and verifies that the generated key pair's seed and SS58 address match the input seed and the expected SS58 address. - It also tests the behavior when an invalid seed string is provided, raising a `ValueError` as expected. - """ - ss58_addr = "5D5cwd8DX6ij7nouVcoxDuWtJfiR1BnzCkiBVTt7DU8ft5Ta" - seed_str = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f7636" - with patch.object(mock_wallet, "set_hotkey") as mock_set_hotkey: - mock_wallet.regenerate_hotkey(seed=seed_str, overwrite=True, suppress=True) - - mock_set_hotkey.assert_called_once() - keypair: bittensor.Keypair = mock_set_hotkey.call_args_list[0][0][0] - - seed_hex = ( - keypair.seed_hex - if isinstance(keypair.seed_hex, str) - else keypair.seed_hex.hex() - ) - - pattern = rf"(0x|){seed_str[2:]}" - assert re.match( - pattern, seed_hex - ), f"The seed_hex '{seed_hex}' does not match the expected pattern '{pattern}'" - assert ( - keypair.ss58_address == ss58_addr - ) # Check that the ss58 address is correct - - seed_str_bad = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f763" # 1 character short - with pytest.raises(ValueError): - mock_wallet.regenerate_hotkey(seed=seed_str_bad, overwrite=True, suppress=True) - - -@pytest.mark.parametrize( - "mnemonic, expected_exception", - [ - # Input is in a string format - ( - "fiscal prevent noise record smile believe quote front weasel book axis legal", - None, - ), - # Input is in a list format (acquired by encapsulating mnemonic arg in a string "" in the cli) - ( - [ - "fiscal prevent noise record smile believe quote front weasel book axis legal" - ], - None, - ), - # Input is in a full list format (aquired by pasting mnemonic arg simply w/o quotes in cli) - ( - [ - "fiscal", - "prevent", - "noise", - "record", - "smile", - "believe", - "quote", - "front", - "weasel", - "book", - "axis", - "legal", - ], - None, - ), - # Incomplete mnemonic - ("word1 word2 word3", ValueError), - # No mnemonic added - (None, ValueError), - ], - ids=[ - "string-format", - "list-format-thru-string", - "list-format", - "incomplete-mnemonic", - "no-mnemonic", - ], -) -def test_regen_coldkey_mnemonic(mock_wallet, mnemonic, expected_exception): - """Test the `regenerate_coldkey` method of the wallet class, which regenerates the cold key pair from a mnemonic. - We test different input formats of mnemonics and check if the function works as expected. - """ - with patch.object(mock_wallet, "set_coldkey") as mock_set_coldkey, patch.object( - mock_wallet, "set_coldkeypub" - ) as mock_set_coldkeypub: - if expected_exception: - with pytest.raises(expected_exception): - mock_wallet.regenerate_coldkey( - mnemonic=mnemonic, overwrite=True, suppress=True - ) - else: - mock_wallet.regenerate_coldkey(mnemonic=mnemonic) - mock_set_coldkey.assert_called_once() - mock_set_coldkeypub.assert_called_once() - - -@pytest.mark.parametrize( - "overwrite, user_input, expected_exception", - [ - (True, None, None), # Test with overwrite=True, no user input needed - (False, "n", True), # Test with overwrite=False and user says no, KeyFileError - (False, "y", None), # Test with overwrite=False and user says yes - ], -) -def test_regen_coldkey_overwrite_functionality( - mock_wallet, overwrite, user_input, expected_exception -): - """Test the `regenerate_coldkey` method of the wallet class, emphasizing on the overwrite functionality""" - ss58_addr = "5D5cwd8DX6ij7nouVcoxDuWtJfiR1BnzCkiBVTt7DU8ft5Ta" - seed_str = "0x659c024d5be809000d0d93fe378cfde020846150b01c49a201fc2a02041f7636" - - with patch.object(mock_wallet, "set_coldkey") as mock_set_coldkey, patch( - "builtins.input", return_value=user_input - ): - if expected_exception: - with pytest.raises(KeyFileError): - mock_wallet.regenerate_coldkey( - seed=seed_str, overwrite=overwrite, suppress=True - ) - else: - mock_wallet.regenerate_coldkey( - seed=seed_str, overwrite=overwrite, suppress=True - ) - mock_set_coldkey.assert_called_once() - keypair = mock_set_coldkey.call_args_list[0][0][0] - seed_hex = ( - keypair.seed_hex - if isinstance(keypair.seed_hex, str) - else keypair.seed_hex.hex() - ) - assert re.match( - rf"(0x|){seed_str[2:]}", seed_hex - ), "The seed_hex does not match the expected pattern" - assert ( - keypair.ss58_address == ss58_addr - ), "The SS58 address does not match the expected address" diff --git a/tests/unit_tests/utils/test_balance.py b/tests/unit_tests/utils/test_balance.py index b99bc111f..66aa550f2 100644 --- a/tests/unit_tests/utils/test_balance.py +++ b/tests/unit_tests/utils/test_balance.py @@ -1,14 +1,15 @@ +"""Test the Balance class.""" + +from typing import Union + import pytest from hypothesis import given from hypothesis import strategies as st -from typing import Union -from bittensor import Balance +from bittensor.utils.balance import Balance from tests.helpers import CLOSE_IN_VALUE -""" -Test the Balance class -""" + valid_tao_numbers_strategy = st.one_of( st.integers(max_value=21_000_000, min_value=-21_000_000), st.floats( @@ -439,7 +440,7 @@ def test_balance_not_eq_none(balance: Union[int, float]): Test the inequality (!=) of a Balance object and None. """ balance_ = Balance(balance) - assert not balance_ == None + assert balance_ is not None @given(balance=valid_tao_numbers_strategy) @@ -448,7 +449,7 @@ def test_balance_neq_none(balance: Union[int, float]): Test the inequality (!=) of a Balance object and None. """ balance_ = Balance(balance) - assert balance_ != None + assert balance_ is not None def test_balance_init_from_invalid_value(): @@ -507,3 +508,13 @@ def test_balance_eq_invalid_type(balance: Union[int, float]): balance_ = Balance(balance) with pytest.raises(NotImplementedError): balance_ == "" + + +def test_from_float(): + """Tests from_float method call.""" + assert Balance.from_tao(1.0) == Balance(1000000000) + + +def test_from_rao(): + """Tests from_rao method call.""" + assert Balance.from_tao(1) == Balance(1000000000) diff --git a/tests/unit_tests/utils/test_init.py b/tests/unit_tests/utils/test_init.py new file mode 100644 index 000000000..fbbc8d5bc --- /dev/null +++ b/tests/unit_tests/utils/test_init.py @@ -0,0 +1,27 @@ +import pytest + +from bittensor import warnings, __getattr__, version_split, logging, trace, debug + + +def test_getattr_version_split(): + """Test that __getattr__ for 'version_split' issues a deprecation warning and returns the correct value.""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert __getattr__("version_split") == version_split + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "version_split is deprecated" in str(w[-1].message) + + +@pytest.mark.parametrize("test_input, expected", [(True, "Trace"), (False, "Default")]) +def test_trace(test_input, expected): + """Test the trace function turns tracing on|off.""" + trace(test_input) + assert logging.current_state_value == expected + + +@pytest.mark.parametrize("test_input, expected", [(True, "Debug"), (False, "Default")]) +def test_debug(test_input, expected): + """Test the debug function turns tracing on|off.""" + debug(test_input) + assert logging.current_state_value == expected diff --git a/tests/unit_tests/utils/test_registration.py b/tests/unit_tests/utils/test_registration.py index d0c4fc743..c85608b5f 100644 --- a/tests/unit_tests/utils/test_registration.py +++ b/tests/unit_tests/utils/test_registration.py @@ -1,3 +1,20 @@ +# 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 pytest from bittensor.utils.registration import LazyLoadedTorch @@ -14,7 +31,7 @@ def error(self, message): @pytest.fixture def mock_bittensor_logging(monkeypatch): mock_logger = MockBittensorLogging() - monkeypatch.setattr("bittensor.logging", mock_logger) + monkeypatch.setattr("bittensor.utils.registration.logging", mock_logger) return mock_logger diff --git a/tests/unit_tests/utils/test_subtensor.py b/tests/unit_tests/utils/test_subtensor.py deleted file mode 100644 index 1c1220bce..000000000 --- a/tests/unit_tests/utils/test_subtensor.py +++ /dev/null @@ -1,99 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2022 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 json - -import pytest - -import bittensor.utils.subtensor as st_utils - - -class MockPallet: - def __init__(self, errors): - self.errors = errors - - -@pytest.fixture -def pallet_with_errors(): - """Provide a mock pallet with sample errors.""" - return MockPallet( - [ - {"index": 1, "name": "ErrorOne", "docs": ["Description one."]}, - { - "index": 2, - "name": "ErrorTwo", - "docs": ["Description two.", "Continued."], - }, - ] - ) - - -@pytest.fixture -def empty_pallet(): - """Provide a mock pallet with no errors.""" - return MockPallet([]) - - -def test_get_errors_from_pallet_with_errors(pallet_with_errors): - """Ensure errors are correctly parsed from pallet.""" - expected = { - "1": {"name": "ErrorOne", "description": "Description one."}, - "2": {"name": "ErrorTwo", "description": "Description two. Continued."}, - } - assert st_utils._get_errors_from_pallet(pallet_with_errors) == expected - - -def test_get_errors_from_pallet_empty(empty_pallet): - """Test behavior with an empty list of errors.""" - assert st_utils._get_errors_from_pallet(empty_pallet) is None - - -def test_save_errors_to_cache(tmp_path): - """Ensure that errors are correctly saved to a file.""" - test_file = tmp_path / "subtensor_errors_map.json" - errors = {"1": {"name": "ErrorOne", "description": "Description one."}} - st_utils._ERRORS_FILE_PATH = test_file - st_utils._save_errors_to_cache("0x123", errors) - - with open(test_file, "r") as file: - data = json.load(file) - assert data["subtensor_build_id"] == "0x123" - assert data["errors"] == errors - - -def test_get_errors_from_cache(tmp_path): - """Test retrieval of errors from cache.""" - test_file = tmp_path / "subtensor_errors_map.json" - errors = {"1": {"name": "ErrorOne", "description": "Description one."}} - - st_utils._ERRORS_FILE_PATH = test_file - with open(test_file, "w") as file: - json.dump({"subtensor_build_id": "0x123", "errors": errors}, file) - assert st_utils._get_errors_from_cache() == { - "subtensor_build_id": "0x123", - "errors": errors, - } - - -def test_get_errors_no_cache(mocker, empty_pallet): - """Test get_errors function when no cache is available.""" - mocker.patch("bittensor.utils.subtensor._get_errors_from_cache", return_value=None) - mocker.patch("bittensor.utils.subtensor.SubstrateInterface") - substrate_mock = mocker.MagicMock() - substrate_mock.metadata.get_metadata_pallet.return_value = empty_pallet - substrate_mock.metadata[0].value = "0x123" - assert st_utils.get_subtensor_errors(substrate_mock) == {} diff --git a/tests/unit_tests/utils/test_utils.py b/tests/unit_tests/utils/test_utils.py index 3c077aba7..8ed28c067 100644 --- a/tests/unit_tests/utils/test_utils.py +++ b/tests/unit_tests/utils/test_utils.py @@ -1,328 +1,169 @@ # The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc - +# 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 logging - -import numpy as np -import bittensor.utils.weight_utils as weight_utils +from bittensor import utils +from bittensor.core.settings import SS58_FORMAT import pytest -def test_convert_weight_and_uids(): - uids = np.arange(10) - weights = np.random.rand(10) - weight_utils.convert_weights_and_uids_for_emit(uids, weights) - - # min weight < 0 - weights[5] = -1 - with pytest.raises(ValueError) as pytest_wrapped_e: - weight_utils.convert_weights_and_uids_for_emit(uids, weights) - - # min uid < 0 - weights[5] = 0 - uids[3] = -1 - with pytest.raises(ValueError) as pytest_wrapped_e: - weight_utils.convert_weights_and_uids_for_emit(uids, weights) - - # len(uids) != len(weights) - uids[3] = 3 - with pytest.raises(ValueError) as pytest_wrapped_e: - weight_utils.convert_weights_and_uids_for_emit(uids, weights[1:]) - - # sum(weights) == 0 - weights = np.zeros(10) - weight_utils.convert_weights_and_uids_for_emit(uids, weights) - - # test for overflow and underflow - for _ in range(5): - uids = np.arange(10) - weights = np.random.rand(10) - weight_utils.convert_weights_and_uids_for_emit(uids, weights) - - -def test_normalize_with_max_weight(): - weights = np.random.rand(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.01) - assert wn.max() <= 0.01 - - weights = np.zeros(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.01) - assert wn.max() <= 0.01 - - weights = np.random.rand(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.02) - assert wn.max() <= 0.02 - - weights = np.zeros(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.02) - assert wn.max() <= 0.02 - - weights = np.random.rand(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.03) - assert wn.max() <= 0.03 - - weights = np.zeros(1000) - wn = weight_utils.normalize_max_weight(weights, limit=0.03) - assert wn.max() <= 0.03 - - # Check for Limit - limit = 0.001 - weights = np.random.rand(2000) - w = weights / weights.sum() - wn = weight_utils.normalize_max_weight(weights, limit=limit) - assert (w.max() >= limit and np.abs(limit - wn.max()) < 0.001) or ( - w.max() < limit and wn.max() < limit +def test_ss58_to_vec_u8(mocker): + """Tests `utils.ss58_to_vec_u8` function.""" + # Prep + test_ss58_address = "5DD26kC2kxajmwfbbZmVmxhrY9VeeyR1Gpzy9i8wxLUg6zxm" + fake_return = b"2\xa6?" + mocked_ss58_address_to_bytes = mocker.patch.object( + utils, "ss58_address_to_bytes", return_value=fake_return ) - # Check for Zeros - limit = 0.01 - weights = np.zeros(2000) - wn = weight_utils.normalize_max_weight(weights, limit=limit) - assert wn.max() == 1 / 2000 - - # Check for Ordering after normalization - weights = np.random.rand(100) - wn = weight_utils.normalize_max_weight(weights, limit=1) - assert np.array_equal(wn, weights / weights.sum()) - - # Check for epsilon changes - epsilon = 0.01 - weights = np.sort(np.random.rand(100)) - x = weights / weights.sum() - limit = x[-10] - change = epsilon * limit - y = weight_utils.normalize_max_weight(x, limit=limit - change) - z = weight_utils.normalize_max_weight(x, limit=limit + change) - assert np.abs(y - z).sum() < epsilon - - -@pytest.mark.parametrize( - "test_id, n, uids, weights, expected", - [ - ("happy-path-1", 3, [0, 1, 2], [15, 5, 80], np.array([0.15, 0.05, 0.8])), - ("happy-path-2", 4, [1, 3], [50, 50], np.array([0.0, 0.5, 0.0, 0.5])), - ], -) -def test_convert_weight_uids_and_vals_to_tensor_happy_path( - test_id, n, uids, weights, expected -): - # Act - result = weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) - - # Assert - assert np.allclose(result, expected), f"Failed {test_id}" - - -@pytest.mark.parametrize( - "test_id, n, uids, weights, expected", - [ - ("edge_case_empty", 5, [], [], np.zeros(5)), - ("edge_case_single", 1, [0], [100], np.array([1.0])), - ("edge_case_all_zeros", 4, [0, 1, 2, 3], [0, 0, 0, 0], np.zeros(4)), - ], -) -def test_convert_weight_uids_and_vals_to_tensor_edge_cases( - test_id, n, uids, weights, expected -): - # Act - result = weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) + # Call + result = utils.ss58_to_vec_u8(test_ss58_address) - # Assert - assert np.allclose(result, expected), f"Failed {test_id}" + # Asserts + mocked_ss58_address_to_bytes.assert_called_once_with(test_ss58_address) + assert result == [int(byte) for byte in fake_return] @pytest.mark.parametrize( - "test_id, n, uids, weights, exception", + "test_input,expected", [ - ("error-case-mismatched-lengths", 3, [0, 1, 3, 4, 5], [10, 20, 30], IndexError), - ("error-case-negative-n", -1, [0, 1], [10, 20], ValueError), - ("error-case-invalid-uids", 3, [0, 3], [10, 20], IndexError), + ("y", True), + ("yes", True), + ("t", True), + ("true", True), + ("on", True), + ("1", True), + ("n", False), + ("no", False), + ("f", False), + ("false", False), + ("off", False), + ("0", False), ], ) -def test_convert_weight_uids_and_vals_to_tensor_error_cases( - test_id, n, uids, weights, exception -): - # Act / Assert - with pytest.raises(exception): - weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) +def test_strtobool(test_input, expected): + """Test truthy values.""" + assert utils.strtobool(test_input) is expected @pytest.mark.parametrize( - "test_id, n, uids, weights, subnets, expected", + "test_input", [ - ( - "happy-path-1", - 3, - [0, 1, 2], - [15, 5, 80], - [0, 1, 2], - np.array([0.15, 0.05, 0.8]), - ), - ( - "happy-path-2", - 3, - [0, 2], - [300, 300], - [0, 1, 2], - np.array([0.5, 0.0, 0.5]), - ), + "maybe", + "2", + "onoff", ], ) -def test_convert_root_weight_uids_and_vals_to_tensor_happy_paths( - test_id, n, uids, weights, subnets, expected -): - # Act - result = weight_utils.convert_root_weight_uids_and_vals_to_tensor( - n, uids, weights, subnets +def test_strtobool_raise_error(test_input): + """Tests invalid values.""" + with pytest.raises(ValueError): + utils.strtobool(test_input) + + +def test_get_explorer_root_url_by_network_from_map(): + """Tests private utils._get_explorer_root_url_by_network_from_map function.""" + # Prep + # Test with a known network + network_map = { + "entity1": {"network1": "url1", "network2": "url2"}, + "entity2": {"network1": "url3", "network3": "url4"}, + } + # Test with no matching network in the map + network_map_empty = { + "entity1": {}, + "entity2": {}, + } + + # Assertions + assert utils._get_explorer_root_url_by_network_from_map( + "network1", network_map + ) == { + "entity1": "url1", + "entity2": "url3", + } + # Test with an unknown network + assert ( + utils._get_explorer_root_url_by_network_from_map("unknown_network", network_map) + == {} ) - - # Assert - assert np.allclose(result, expected, atol=1e-4), f"Failed {test_id}" - - -@pytest.mark.parametrize( - "test_id, n, uids, weights, subnets, expected", - [ - ( - "edge-1", - 1, - [0], - [0], - [0], - np.array([0.0]), - ), # Single neuron with zero weight - ( - "edge-2", - 2, - [0, 1], - [0, 0], - [0, 1], - np.array([0.0, 0.0]), - ), # All zero weights - ], -) -def test_convert_root_weight_uids_and_vals_to_tensor_edge_cases( - test_id, n, uids, weights, subnets, expected -): - # Act - result = weight_utils.convert_root_weight_uids_and_vals_to_tensor( - n, uids, weights, subnets + assert ( + utils._get_explorer_root_url_by_network_from_map("network1", network_map_empty) + == {} ) - # Assert - assert np.allclose(result, expected, atol=1e-4), f"Failed {test_id}" - -@pytest.mark.parametrize( - "test_id, n, uids, weights, subnets, exception", - [ - # uid not in subnets - ( - "error-1", - 3, - [1, 3], - [100, 200], - [1, 2], - "The subnet is unavailable at the moment.", - ), - # More uids than subnets - ( - "error-2", - 3, - [1, 2, 3], - [100, 200], - [1], - "The subnet is unavailable at the moment.", - ), - ], -) -def test_convert_root_weight_uids_and_vals_to_tensor_error_cases( - test_id, n, uids, weights, subnets, exception, caplog -): - with caplog.at_level(logging.WARNING): - weight_utils.convert_root_weight_uids_and_vals_to_tensor( - n, uids, weights, subnets - ) +def test_get_explorer_url_for_network(): + """Tests `utils.get_explorer_url_for_network` function.""" + # Prep + fake_block_hash = "0x1234567890abcdef" + fake_map = {"opentensor": {"network": "url"}, "taostats": {"network": "url2"}} - assert any( - exception in record.message and record.levelname == "WARNING" - for record in caplog.records - ) + # Call + result = utils.get_explorer_url_for_network("network", fake_block_hash, fake_map) + # Assert + assert result == { + "opentensor": f"url/query/{fake_block_hash}", + "taostats": f"url2/extrinsic/{fake_block_hash}", + } + + +def test_ss58_address_to_bytes(mocker): + """Tests utils.ss58_address_to_bytes function.""" + # Prep + fake_ss58_address = "ss58_address" + mocked_scalecodec_ss58_decode = mocker.patch.object( + utils.scalecodec, "ss58_decode", return_value="" + ) -@pytest.mark.parametrize( - "test_id, n, uids, bonds, expected_output", - [ - ( - "happy-path-1", - 5, - [1, 3, 4], - [10, 20, 30], - np.array([0, 10, 0, 20, 30], dtype=np.int64), - ), - ( - "happy-path-2", - 3, - [0, 1, 2], - [7, 8, 9], - np.array([7, 8, 9], dtype=np.int64), - ), - ("happy-path-3", 4, [2], [15], np.array([0, 0, 15, 0], dtype=np.int64)), - ], -) -def test_happy_path(test_id, n, uids, bonds, expected_output): - # Act - result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) + # Call + result = utils.ss58_address_to_bytes(fake_ss58_address) - # Assert - assert np.array_equal(result, expected_output), f"Failed {test_id}" + # Asserts + mocked_scalecodec_ss58_decode.assert_called_once_with( + fake_ss58_address, SS58_FORMAT + ) + assert result == bytes.fromhex(mocked_scalecodec_ss58_decode.return_value) @pytest.mark.parametrize( - "test_id, n, uids, bonds, expected_output", + "test_input, expected_result", [ - ("edge-1", 1, [0], [0], np.array([0], dtype=np.int64)), # Single element - ( - "edge-2", - 10, - [], - [], - np.zeros(10, dtype=np.int64), - ), # Empty uids and bonds + (123, False), + ("0x234SD", True), + ("5D34SD", True), + (b"0x234SD", True), ], ) -def test_edge_cases(test_id, n, uids, bonds, expected_output): - # Act - result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) - - # Assert - assert np.array_equal(result, expected_output), f"Failed {test_id}" +def test_is_valid_bittensor_address_or_public_key(mocker, test_input, expected_result): + """Tests utils.is_valid_bittensor_address_or_public_key function.""" + # Prep + mocked_is_valid_ed25519_pubkey = mocker.patch.object( + utils, "_is_valid_ed25519_pubkey", return_value=True + ) + mocked_ss58_is_valid_ss58_address = mocker.patch.object( + utils.ss58, "is_valid_ss58_address", side_effect=[False, True] + ) + # Call + result = utils.is_valid_bittensor_address_or_public_key(test_input) -@pytest.mark.parametrize( - "test_id, n, uids, bonds, exception", - [ - ("error-1", 5, [1, 3, 6], [10, 20, 30], IndexError), # uid out of bounds - ("error-2", -1, [0], [10], ValueError), # Negative number of neurons - ], -) -def test_error_cases(test_id, n, uids, bonds, exception): - # Act / Assert - with pytest.raises(exception): - weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) + # Asserts + if not isinstance(test_input, int) and isinstance(test_input, bytes): + mocked_is_valid_ed25519_pubkey.assert_called_with(test_input) + if isinstance(test_input, str) and not test_input.startswith("0x"): + assert mocked_ss58_is_valid_ss58_address.call_count == 2 + assert result == expected_result diff --git a/tests/unit_tests/utils/test_version.py b/tests/unit_tests/utils/test_version.py index f9760933f..fa96bddad 100644 --- a/tests/unit_tests/utils/test_version.py +++ b/tests/unit_tests/utils/test_version.py @@ -22,13 +22,16 @@ from freezegun import freeze_time from datetime import datetime, timedelta, timezone -from bittensor.utils.version import ( - VERSION_CHECK_THRESHOLD, - VersionCheckError, - get_and_save_latest_version, - check_version, - version_checking, -) +# from bittensor.utils.version import ( +# VERSION_CHECK_THRESHOLD, +# VersionCheckError, +# get_and_save_latest_version, +# check_version, +# version_checking, +# __version__ +# ) +from bittensor.utils import version + from unittest.mock import MagicMock from pytest_mock import MockerFixture @@ -62,14 +65,14 @@ def test_get_and_save_latest_version_no_file( ): assert not version_file_path.exists() - assert get_and_save_latest_version() == pypi_version + assert version.get_and_save_latest_version() == pypi_version mock_get_version_from_pypi.assert_called_once() assert version_file_path.exists() assert version_file_path.read_text() == pypi_version -@pytest.mark.parametrize("elapsed", [0, VERSION_CHECK_THRESHOLD - 5]) +@pytest.mark.parametrize("elapsed", [0, version.VERSION_CHECK_THRESHOLD - 5]) def test_get_and_save_latest_version_file_fresh_check( mock_get_version_from_pypi: MagicMock, version_file_path: Path, elapsed: int ): @@ -78,7 +81,7 @@ def test_get_and_save_latest_version_file_fresh_check( version_file_path.write_text("6.9.5") with freeze_time(now + timedelta(seconds=elapsed)): - assert get_and_save_latest_version() == "6.9.5" + assert version.get_and_save_latest_version() == "6.9.5" mock_get_version_from_pypi.assert_not_called() @@ -90,8 +93,8 @@ def test_get_and_save_latest_version_file_expired_check( version_file_path.write_text("6.9.5") - with freeze_time(now + timedelta(seconds=VERSION_CHECK_THRESHOLD + 1)): - assert get_and_save_latest_version() == pypi_version + with freeze_time(now + timedelta(seconds=version.VERSION_CHECK_THRESHOLD + 1)): + assert version.get_and_save_latest_version() == pypi_version mock_get_version_from_pypi.assert_called_once() assert version_file_path.read_text() == pypi_version @@ -111,13 +114,13 @@ def test_get_and_save_latest_version_file_expired_check( def test_check_version_newer_available( mocker: MockerFixture, current_version: str, latest_version: str, capsys ): - mocker.patch("bittensor.utils.version.bittensor.__version__", current_version) + version.__version__ = current_version mocker.patch( "bittensor.utils.version.get_and_save_latest_version", return_value=latest_version, ) - check_version() + version.check_version() captured = capsys.readouterr() @@ -137,13 +140,13 @@ def test_check_version_newer_available( def test_check_version_up_to_date( mocker: MockerFixture, current_version: str, latest_version: str, capsys ): - mocker.patch("bittensor.utils.version.bittensor.__version__", current_version) + version.__version__ = current_version mocker.patch( "bittensor.utils.version.get_and_save_latest_version", return_value=latest_version, ) - check_version() + version.check_version() captured = capsys.readouterr() @@ -153,16 +156,16 @@ def test_check_version_up_to_date( def test_version_checking(mocker: MockerFixture): mock = mocker.patch("bittensor.utils.version.check_version") - version_checking() + version.version_checking() mock.assert_called_once() def test_version_checking_exception(mocker: MockerFixture): mock = mocker.patch( - "bittensor.utils.version.check_version", side_effect=VersionCheckError + "bittensor.utils.version.check_version", side_effect=version.VersionCheckError ) - version_checking() + version.version_checking() mock.assert_called_once() diff --git a/tests/unit_tests/utils/test_wallet_utils.py b/tests/unit_tests/utils/test_wallet_utils.py deleted file mode 100644 index fccb15dbd..000000000 --- a/tests/unit_tests/utils/test_wallet_utils.py +++ /dev/null @@ -1,60 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2021 Yuma Rao -# Copyright © 2022 Opentensor Foundation -# Copyright © 2023 Opentensor Technologies Inc -# -# 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. - -from bittensor.utils.wallet_utils import decode_hex_identity_dict - - -def test_decode_hex_identity_dict(): - """Tests the decode_hex_identity_dict function to ensure it correctly decodes a hex-encoded identity dictionary.""" - # Prep - fake_info_dictionary = { - "additional": [ - ( - {"Raw11": "0x6465736372697074696f6e"}, - {"Raw64": "0x46616b65206465736372697074696f6e"}, - ) - ], - "display": {"Raw17": "0x46616b654e616d65"}, - "legal": "None", - "web": {"Raw22": "0x687474703a2f2f7777772e626c61626c612d746573742e636f6d"}, - "riot": "None", - "email": "None", - "pgp_fingerprint": None, - "image": "None", - "twitter": "None", - } - - expected_result = { - "additional": [("description", "Fake description")], - "display": "FakeName", - "legal": "None", - "web": "http://www.blabla-test.com", - "riot": "None", - "email": "None", - "pgp_fingerprint": None, - "image": "None", - "twitter": "None", - } - - # Call - - result = decode_hex_identity_dict(fake_info_dictionary) - - # Assertions - assert result == expected_result diff --git a/tests/unit_tests/utils/test_weight_utils.py b/tests/unit_tests/utils/test_weight_utils.py index 66f3c8127..74009434b 100644 --- a/tests/unit_tests/utils/test_weight_utils.py +++ b/tests/unit_tests/utils/test_weight_utils.py @@ -19,10 +19,13 @@ import logging import numpy as np +from hypothesis import settings + import bittensor.utils.weight_utils as weight_utils import pytest from bittensor.utils import torch +from bittensor.core.settings import version_as_int def test_convert_weight_and_uids(): @@ -57,7 +60,7 @@ def test_convert_weight_and_uids(): weight_utils.convert_weights_and_uids_for_emit(uids, weights) -def test_convert_weight_and_uids_torch(force_legacy_torch_compat_api): +def test_convert_weight_and_uids_torch(force_legacy_torch_compatible_api): uids = torch.tensor(list(range(10))) weights = torch.rand(10) weight_utils.convert_weights_and_uids_for_emit(uids, weights) @@ -144,7 +147,7 @@ def test_normalize_with_max_weight(): def test_normalize_with_max_weight__legacy_torch_api_compat( - force_legacy_torch_compat_api, + force_legacy_torch_compatible_api, ): weights = torch.rand(1000) wn = weight_utils.normalize_max_weight(weights, limit=0.01) @@ -240,7 +243,7 @@ def test_convert_weight_uids_and_vals_to_tensor_happy_path( ], ) def test_convert_weight_uids_and_vals_to_tensor_happy_path_torch( - test_id, n, uids, weights, subnets, expected, force_legacy_torch_compat_api + test_id, n, uids, weights, subnets, expected, force_legacy_torch_compatible_api ): # Act result = weight_utils.convert_weight_uids_and_vals_to_tensor(n, uids, weights) @@ -338,7 +341,7 @@ def test_convert_root_weight_uids_and_vals_to_tensor_happy_paths( ], ) def test_convert_root_weight_uids_and_vals_to_tensor_edge_cases( - test_id, n, uids, weights, subnets, expected, force_legacy_torch_compat_api + test_id, n, uids, weights, subnets, expected, force_legacy_torch_compatible_api ): # Act result = weight_utils.convert_root_weight_uids_and_vals_to_tensor( @@ -468,7 +471,7 @@ def test_happy_path(test_id, n, uids, bonds, expected_output): ], ) def test_happy_path_torch( - test_id, n, uids, bonds, expected_output, force_legacy_torch_compat_api + test_id, n, uids, bonds, expected_output, force_legacy_torch_compatible_api ): # Act result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) @@ -512,7 +515,7 @@ def test_edge_cases(test_id, n, uids, bonds, expected_output): ], ) def test_edge_cases_torch( - test_id, n, uids, bonds, expected_output, force_legacy_torch_compat_api + test_id, n, uids, bonds, expected_output, force_legacy_torch_compatible_api ): # Act result = weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) @@ -532,3 +535,147 @@ def test_error_cases(test_id, n, uids, bonds, exception): # Act / Assert with pytest.raises(exception): weight_utils.convert_bond_uids_and_vals_to_tensor(n, uids, bonds) + + +def test_process_weights_for_netuid(mocker): + """Test the process_weights_for_netuid function.""" + # Prep + fake_uids = np.array([1, 2, 3, 4, 5], dtype=np.int64) + fake_weights = np.array([1.0, 2.5, 3.3, 4.7, 5.9], dtype=np.float32) + fake_netuid = 1 + fake_subtensor = mocker.MagicMock() + fake_metagraph = mocker.MagicMock() + fake_exclude_quantile = 0 + + fake_subtensor.min_allowed_weights.return_value = 0.1 + fake_subtensor.max_weight_limit.return_value = 1.0 + fake_metagraph.n = 1 + mocked_normalize_max_weight = mocker.patch.object( + weight_utils, "normalize_max_weight" + ) + + # Call + result = weight_utils.process_weights_for_netuid( + uids=fake_uids, + weights=fake_weights, + netuid=fake_netuid, + subtensor=fake_subtensor, + metagraph=fake_metagraph, + exclude_quantile=fake_exclude_quantile, + ) + + # Asserts + fake_subtensor.min_allowed_weights.assert_called_once_with(netuid=fake_netuid) + fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) + + res1, res2 = result + assert np.array_equal(res1, fake_uids) + assert res2 == mocked_normalize_max_weight.return_value + + +def test_process_weights_with_all_zero_weights(mocker): + """Test the process_weights_for_netuid function with all zero weights.""" + # Prep + fake_uids = np.array([1, 2, 3, 4, 5], dtype=np.int64) + fake_weights = np.array([0.0, 0.0, 0.0, 0.0, 0.0], dtype=np.float32) + fake_netuid = 1 + fake_subtensor = mocker.MagicMock() + fake_metagraph = mocker.MagicMock() + fake_exclude_quantile = 0 + + fake_subtensor.min_allowed_weights.return_value = 0.1 + fake_subtensor.max_weight_limit.return_value = 1.0 + fake_metagraph.n = 1 + + # Call + result = weight_utils.process_weights_for_netuid( + uids=fake_uids, + weights=fake_weights, + netuid=fake_netuid, + subtensor=fake_subtensor, + metagraph=fake_metagraph, + exclude_quantile=fake_exclude_quantile, + ) + + # Asserts + fake_subtensor.min_allowed_weights.assert_called_once_with(netuid=fake_netuid) + fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) + + res1, res2 = result + assert np.array_equal(res1, np.array([0])) + assert np.array_equal(res2, np.array([1.0])) + + +def test_process_weights_for_netuid_with_nzs_less_min_allowed_weights(mocker): + """Tests process_weights_for_netuid method when non-zero weights are less than the min allowed weights.""" + # Prep + fake_uids = np.array([1, 2, 3, 4, 5], dtype=np.int64) + fake_weights = np.array([0.1, 0.2, 0.3, 0.0, 0.0], dtype=np.float32) + fake_netuid = 1 + fake_subtensor = mocker.MagicMock() + fake_metagraph = None + fake_exclude_quantile = 0 + + fake_subtensor.min_allowed_weights.return_value = 4 + fake_subtensor.max_weight_limit.return_value = 1.0 + fake_subtensor.metagraph.return_value.n = 5 + mocked_np_arange = mocker.patch.object(np, "arange") + mocked_normalize_max_weight = mocker.patch.object( + weight_utils, "normalize_max_weight" + ) + + # Call + result = weight_utils.process_weights_for_netuid( + uids=fake_uids, + weights=fake_weights, + netuid=fake_netuid, + subtensor=fake_subtensor, + metagraph=fake_metagraph, + exclude_quantile=fake_exclude_quantile, + ) + + # Asserts + fake_subtensor.metagraph.assert_called_once_with(fake_netuid) + fake_subtensor.min_allowed_weights.assert_called_once_with(netuid=fake_netuid) + fake_subtensor.max_weight_limit.assert_called_once_with(netuid=fake_netuid) + assert result == ( + mocked_np_arange.return_value, + mocked_normalize_max_weight.return_value, + ) + + +def test_generate_weight_hash(mocker): + """Tests weight_utils.generate_weight_hash function.""" + # Prep + fake_address = "5D1ABCD" + fake_netuid = 1 + fake_uids = [1, 2] + fake_values = [10, 20] + fake_version_key = 80000 + fake_salt = [1, 2] + + mocked_scale_bytes = mocker.patch.object(weight_utils, "ScaleBytes") + mocked_keypair = mocker.patch.object(weight_utils, "Keypair") + mocker_vec = mocker.patch.object(weight_utils, "Vec") + mocked_u16 = mocker.patch.object(weight_utils, "U16") + mocked_hasher = mocker.patch.object(weight_utils.hashlib, "blake2b") + + # Call + result = weight_utils.generate_weight_hash( + address=fake_address, + netuid=fake_netuid, + uids=fake_uids, + values=fake_values, + version_key=fake_version_key, + salt=fake_salt, + ) + + # Asserts + mocked_scale_bytes.assert_called() + mocked_keypair.assert_called() + mocker_vec.assert_called() + mocked_u16.assert_called() + assert ( + result + == mocked_hasher.return_value.hexdigest.return_value.__radd__.return_value + ) From 27bf3db6f1a41222eb4578575b72b18972ff6073 Mon Sep 17 00:00:00 2001 From: Roman <167799377+roman-opentensor@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:25:52 -0700 Subject: [PATCH 257/260] Implement warning logging level (#2323) * Make version.py bittensor import independent * Update setup.py, replace file with the __version__ * Update settings.py * Make axon independent of bittensor import. Fix axon tests. * Make chain_data.py independent of bittensor import. Fix chain_data tests. * Make dendrite.py independent of bittensor import. Fix dendrite tests. * Make staking.py independent of bittensor import. Fix staking tests. * Make subtensor.py independent of bittensor import. Fix subtensor tests. * Make prometheus.py independent of bittensor import. Fix prometheus tests. * Axon refactoring * Move subnets.py into bittensor/utils since this module is not used within the bittensor package, but is actively used by the community. * Make synapse.py independent of bittensor import. Fix synapse tests. * Make tensor.py independent of bittensor import. Fix tensor tests. * Change name `bittensor.utils.hash` -> `bittensor.utils.get_hash` * Moved from using `import bittensor` (withing bittensor codebase) to direct imports of modules and other objects. It helped to avoid namespace conflicts. * Add backwards compatibility for 'bittensor.api.extrinsics' as 'bittensor.extrinsics' * Move the contents of `wallet_utils.py` to `bittensor/utils/__init__.py`, since it is used directly from utils. * Delete `bccli` staff and related tests. Fix setup.py. * Fix tests. * Makes the `bittensor.utils.mock` subpackage available as `bittensor.mock` for backwards compatibility. * Check for participation before nomination call * Ruff formatting. Fix ruff's complaints. * Removing obsolete, unused (community and bittensor) code * Move backwards compatibility related content to `bittensor/utils/backwards_compatibility` subpackage * btlogging/loggingmachine.py improvement from https://github.com/opentensor/bittensor/pull/2155 * Fixes after `Merge streaming fix to staging` from https://github.com/opentensor/bittensor/pull/2183 * Review fix. * Remove black from dev requirements. * black back * Remove space from prod.txt. Remove black from dev.txt * Add dendrite reference to backwords compatibility * Fix the usage of env vars in default settings. * ruff * Change the subpackage name to `deprecated` * Update __version__ until 8.0.0 * Move `subnets.py` to `bittensor/utils` as it is considered more utility than deprecated. * Rename test module * Add aliases mapping test * ruff * Init: Adds initial e2e tests * Ruff * Fixes LOCALNET_SH_PATH env_var * Adds module description * Adds commit-reveal back and adds e2e for liquid alpha & commit reveal * Review suggestions implemented * Fixes logging string * Enhance: Switch from format() to f-strings * ruff * Optimize imports * Add the opportunity to run `python - m bittensor` * replace deps link from https to ssh * Fixes metagraph save/load for neurons, added e2e tests (metagraph + extrinsics) * Renamed test * Adds missing statements * increase subtensor.py test coverage until 80% * ruff * add tests * ruff * add tests _do_serve_prometheus * add tests neuron_for_uid * sort tests by group * sort tests by group * remove unused import * add test for `get_neuron_for_pubkey_and_subnet` * Add unit tests to subtensor.py * Fix ruff + `test_do_set_weights_no_waits` description * add tests * ruff + add tests * ruff * add test * fix unused imports * Config refactoring * Remove duplicated tests * add test * remove duplicated import * remove unused functions * remove unused functions + add tests * ruff * fix type annotation * add tests * Add tests and remove unused functions * fix test * ruff * Add balance tests, small refactoring * Add init test for bittensor * ruff * Add tests for utils.weight_utils.py * add tests for bittensor.utils.subnets * ruff * update test file * Decoupling chain_data.py to sub-package * Fix test * ruff * review fix * update `requirements/prod.txt` * Update localnet entrypoint port * Add substrate reconnection logic * Fix tests + add tests * Ruff formatter * add docstring to the decorator * update requirements * update requirements * update requirements * cleanup __init__.py * - move `extrinsics` from deprecated into `bittensor/core`; - update `deprecated` sub-package to `deprecated.py` module. * update ssh to https in requirements * add reconnection logic for correctly closed connection * remove unused import * fix pagination * Move do_* methods into the related extrinsic module (commit_weights.py). * Move do_* methods into the related extrinsic module (bittensor/core/extrinsics/set_weights.py). * Move do_* methods into the related extrinsic module (bittensor/core/extrinsics/transfer.py). + ruff * Move do_* methods into the related extrinsic module (bittensor/core/extrinsics/serving.py) * Move do_* methods into the related extrinsic module (bittensor/core/extrinsics/prometheus.py) * bug fixes * extrinsics refactoring * bug fix * ruff * fix * removed exit sys call for ConnectionRefusedError in _get_substrate * remove unused import * change the log message * Corrected arguments order in logging methods + test * fix integration test * ruff * comments fixes * fix `bittensor/core/subtensor.py:445: error: Argument "logger" to "retry" has incompatible type "LoggingMachine"; expected "Logger | None" [arg-type]` * integrate `bt_decode` into BTSDK * fix logger linter checker * Reverts logging enhancement * remove unused code and tests * remove unused imports * modification of error formatter, moved the submit_extrinsic call to utils.py, fixed the subtensor.py regarding extrinsics calls, fixed tests * ruff * Improved logic for concatenating message, prefix, and suffix in bittensor logging + test * fix + ruff * remove unused import * improve btlogging concat test * docs AxonInfo * docs DelegateInfo * DelegateInfoLite * IPInfo * NeuronInfo * NeuronInfo + NeuronInfoLite * PrometheusInfo * ProposalVoteData * ScheduledColdkeySwapInfo * ProposalVoteData * StakeInfo * StakeInfo + SubnetHyperparameters * bittensor/core/chain_data/utils.py * bittensor/core/axon.py * bittensor/core/axon.py * bittensor/core/config.py * bittensor/core/dendrite.py * bittensor/core/metagraph.py * bittensor/core/stream.py * bittensor/core/subtensor.py * bittensor/core/synapse.py * bittensor/core/tensor.py * replace Tuple, Dict, List, Set to python native types * bittensor/__init__.py * bittensor/utils/weight_utils.py * bittensor/utils/version.py * bittensor/utils/btlogging * fix * remove wrong `optional` from docstring's annotations * apply camel case rule * bittensor/core/extrinsics/commit_weights.py * bittensor/core/extrinsics/prometheus.py * bittensor/core/extrinsics/serving.py * axon improvement (add annotation) * README updates for SDK * bittensor/core/extrinsics/set_weights.py * bittensor/core/extrinsics/transfer.py * fix * weight_utils.py * check up and fix * fix Axon Signature * fix axon * delete unused import * update GPG * remove unused code, remove old tests, add new tests * fix * temp commented line * fix review comments * fix review comments * Adding TOC and working in review comments. * Adding installation options * Formatting adjustments for html display * Ruff formatted * Example reformatted * update metagraph * update dendrite.py * Update README.md Co-authored-by: garrett-opentensor <156717492+garrett-opentensor@users.noreply.github.com> * Update README.md Co-authored-by: garrett-opentensor <156717492+garrett-opentensor@users.noreply.github.com> * Update README.md * Update axon.py * fix tests/unit_tests/test_axon.py * Update README.md * set `WARNING` level ad default logging level * fix test * implement warning version, do info the same as default level * fix * ruff * cleanup * Change warning to debug for local connection information * update deps * remove unused file --------- Co-authored-by: ibraheem-opentensor <165814940+ibraheem-opentensor@users.noreply.github.com> Co-authored-by: ibraheem-opentensor Co-authored-by: Watchmaker Co-authored-by: garrett-opentensor <156717492+garrett-opentensor@users.noreply.github.com> --- bittensor/__init__.py | 21 ------- bittensor/core/subtensor.py | 4 +- bittensor/utils/btlogging/loggingmachine.py | 65 ++++++++++++++++++++- bittensor/utils/deprecated.py | 36 ++++++++++++ 4 files changed, 101 insertions(+), 25 deletions(-) diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 5ddba2abe..f4d8ee906 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -22,27 +22,6 @@ from .utils.deprecated import * -# Logging helpers. -def trace(on: bool = True): - """ - Enables or disables trace logging. - - Args: - on (bool): If True, enables trace logging. If False, disables trace logging. - """ - logging.set_trace(on) - - -def debug(on: bool = True): - """ - Enables or disables debug logging. - - Args: - on (bool): If True, enables debug logging. If False, disables debug logging. - """ - logging.set_debug(on) - - def __getattr__(name): if name == "version_split": warnings.warn( diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 533964595..5296ba419 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -175,11 +175,11 @@ def __init__( logging.info( f"You are connecting to {self.network} network with endpoint {self.chain_endpoint}." ) - logging.warning( + logging.debug( "We strongly encourage running a local subtensor node whenever possible. " "This increases decentralization and resilience of the network." ) - logging.warning( + logging.debug( "In a future release, local subtensor will become the default endpoint. " "To get ahead of this change, please run a local subtensor node and point to it." ) diff --git a/bittensor/utils/btlogging/loggingmachine.py b/bittensor/utils/btlogging/loggingmachine.py index b2cfb2918..5c1e5c329 100644 --- a/bittensor/utils/btlogging/loggingmachine.py +++ b/bittensor/utils/btlogging/loggingmachine.py @@ -69,26 +69,48 @@ class LoggingMachine(StateMachine, Logger): Debug = State() Trace = State() Disabled = State() + Warning = State() enable_default = ( Debug.to(Default) | Trace.to(Default) | Disabled.to(Default) | Default.to(Default) + | Warning.to(Default) ) + enable_info = enable_default + enable_trace = ( - Default.to(Trace) | Debug.to(Trace) | Disabled.to(Trace) | Trace.to(Trace) + Default.to(Trace) + | Debug.to(Trace) + | Disabled.to(Trace) + | Trace.to(Trace) + | Warning.to(Trace) ) enable_debug = ( - Default.to(Debug) | Trace.to(Debug) | Disabled.to(Debug) | Debug.to(Debug) + Default.to(Debug) + | Trace.to(Debug) + | Disabled.to(Debug) + | Debug.to(Debug) + | Warning.to(Debug) + ) + + enable_warning = ( + Default.to(Warning) + | Trace.to(Warning) + | Disabled.to(Warning) + | Debug.to(Warning) + | Warning.to(Warning) ) disable_trace = Trace.to(Default) disable_debug = Debug.to(Default) + disable_warning = Warning.to(Default) + disable_logging = ( Trace.to(Disabled) | Debug.to(Disabled) @@ -308,9 +330,29 @@ def before_enable_default(self): continue logger.setLevel(stdlogging.CRITICAL) + def before_enable_info(self): + """Logs status before enable Default.""" + self._logger.info(f"Enabling default logging.") + self._logger.setLevel(stdlogging.INFO) + for logger in all_loggers(): + if logger.name in self._primary_loggers: + continue + logger.setLevel(stdlogging.CRITICAL) + def after_enable_default(self): pass + def before_enable_warning(self): + """Logs status before enable Warning.""" + self._logger.info("Enabling warning.") + self._stream_formatter.set_trace(True) + for logger in all_loggers(): + logger.setLevel(stdlogging.WARNING) + + def after_enable_warning(self): + """Logs status after enable Warning.""" + self._logger.info("Warning enabled.") + # Trace def before_enable_trace(self): """Logs status before enable Trace.""" @@ -449,6 +491,25 @@ def set_trace(self, on: bool = True): if self.current_state_value == "Trace": self.disable_trace() + def set_warning(self, on: bool = True): + """Sets Warning state.""" + if on and not self.current_state_value == "Warning": + self.enable_warning() + elif not on: + if self.current_state_value == "Warning": + self.disable_warning() + + def set_default(self): + """Sets Default state.""" + if not self.current_state_value == "Default": + self.enable_default() + + # as an option to be more obvious. `bittensor.logging.set_info()` is the same `bittensor.logging.set_default()` + def set_info(self): + """Sets Default state.""" + if not self.current_state_value == "Default": + self.enable_info() + def get_level(self) -> int: """Returns Logging level.""" return self._logger.level diff --git a/bittensor/utils/deprecated.py b/bittensor/utils/deprecated.py index 6075a93d8..4f43aa371 100644 --- a/bittensor/utils/deprecated.py +++ b/bittensor/utils/deprecated.py @@ -111,6 +111,7 @@ get_hash, ) from bittensor.utils.balance import Balance as Balance # noqa: F401 +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 @@ -148,3 +149,38 @@ # Makes the `bittensor.core.extrinsics` subpackage available as `bittensor.extrinsics` for backwards compatibility. extrinsics_subpackage = importlib.import_module("bittensor.core.extrinsics") sys.modules["bittensor.extrinsics"] = extrinsics_subpackage + + +# Logging helpers. +def trace(on: bool = True): + """ + Enables or disables trace logging. + + Args: + on (bool): If True, enables trace logging. If False, disables trace logging. + """ + logging.set_trace(on) + + +def debug(on: bool = True): + """ + Enables or disables debug logging. + + Args: + on (bool): If True, enables debug logging. If False, disables debug logging. + """ + logging.set_debug(on) + + +def warning(on: bool = True): + """ + Enables or disables warning logging. + + Args: + on (bool): If True, enables warning logging. If False, disables warning logging and sets default (INFO) level. + """ + logging.set_warning(on) + + +# set Warning logging level for bittensor SDK +warning() From e0672f6f641870cfaba0df9ab84ee7720bf6bf02 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 1 Oct 2024 11:28:21 -0700 Subject: [PATCH 258/260] remove unused file --- tests/e2e_tests/utils/test_utils.py | 83 ----------------------------- 1 file changed, 83 deletions(-) delete mode 100644 tests/e2e_tests/utils/test_utils.py diff --git a/tests/e2e_tests/utils/test_utils.py b/tests/e2e_tests/utils/test_utils.py deleted file mode 100644 index ba662647a..000000000 --- a/tests/e2e_tests/utils/test_utils.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -import shutil -import subprocess -import sys - -from substrateinterface import Keypair - -import bittensor - -template_path = os.getcwd() + "/neurons/" -templates_repo = "templates repository" - - -def setup_wallet(uri: str) -> tuple[Keypair, bittensor.Wallet]: - """ - Sets up a wallet using the provided URI. - - This function creates a keypair from the given URI and initializes a wallet - at a temporary path. It sets the coldkey, coldkeypub, and hotkey for the wallet - using the generated keypair. - - Side Effects: - - Creates a wallet in a temporary directory. - - Sets keys in the wallet without encryption and with overwriting enabled. - """ - keypair = Keypair.create_from_uri(uri) - wallet_path = f"/tmp/btcli-e2e-wallet-{uri.strip('/')}" - wallet = bittensor.Wallet(path=wallet_path) - wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True) - wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) - wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True) - return keypair, wallet - - -def clone_or_update_templates(specific_commit=None): - """ - Clones or updates the Bittensor subnet template repository. - - This function clones the Bittensor subnet template repository if it does not - already exist in the specified installation directory. If the repository already - exists, it updates it by pulling the latest changes. Optionally, it can check out - a specific commit if the `specific_commit` variable is set. - """ - install_dir = template_path - repo_mapping = { - templates_repo: "https://github.com/opentensor/bittensor-subnet-template.git", - } - - os.makedirs(install_dir, exist_ok=True) - os.chdir(install_dir) - - for repo, git_link in repo_mapping.items(): - if not os.path.exists(repo): - print(f"\033[94mCloning {repo}...\033[0m") - subprocess.run(["git", "clone", git_link, repo], check=True) - else: - print(f"\033[94mUpdating {repo}...\033[0m") - os.chdir(repo) - subprocess.run(["git", "pull"], check=True) - os.chdir("..") - - # For pulling specific commit versions of repo - if specific_commit: - os.chdir(templates_repo) - print( - f"\033[94mChecking out commit {specific_commit} in {templates_repo}...\033[0m" - ) - subprocess.run(["git", "checkout", specific_commit], check=True) - os.chdir("..") - - return install_dir + templates_repo + "/" - - -def install_templates(install_dir): - subprocess.check_call([sys.executable, "-m", "pip", "install", install_dir]) - - -def uninstall_templates(install_dir): - subprocess.check_call( - [sys.executable, "-m", "pip", "uninstall", "bittensor_subnet_template", "-y"] - ) - # Delete everything in directory - shutil.rmtree(install_dir) From e6c96536d2b0fdba6ae11a84cb46e7498148532c Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 1 Oct 2024 17:30:20 -0700 Subject: [PATCH 259/260] Bumps version --- bittensor/core/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index cfccf362b..f1ab20c1e 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -15,7 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -__version__ = "8.0.0" +__version__ = "8.1.0" import os import re From 7ac6ae576232444afccd530bef18744d1f4f6a19 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 1 Oct 2024 17:34:22 -0700 Subject: [PATCH 260/260] Adds changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 011a1464d..dd10906f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 8.1.0 /2024-10-02 + +## What's Changed +* Implements new logging level 'warning' by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2323 +* Adds ConnectionRefusedError in-case of connection error by @roman-opentensor in https://github.com/opentensor/bittensor/pull/2326 + ## 8.0.0 /2024-09-25 ## What's Changed