diff --git a/CHANGELOG.md b/CHANGELOG.md index ef8621b85..0e540255c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 9.0.0rc1 /2025-02-04 + +## What's Changed +* Uses new Async Substrate Interface +* Updates how we use runtime calls +* Updates stake move to accept ss58 origin hotkeys +* Fixes slippage calculation in stake move +* Adds improved error handling through '--verbose' flag +* Improved docstrings + ## 8.2.0rc15 /2025-02-03 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 6274e84ca..32f176772 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0rc15" +__version__ = "9.0.0rc1" -__all__ = ["CLIManager", "__version__"] +__all__ = [CLIManager, __version__] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 194bc8fb4..bea9f0cc8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5,6 +5,7 @@ import re import ssl import sys +import traceback from pathlib import Path from typing import Coroutine, Optional from dataclasses import fields @@ -16,6 +17,7 @@ from rich import box from rich.prompt import Confirm, FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table +from rich.tree import Tree from bittensor_cli.src import ( defaults, HELP_PANELS, @@ -26,9 +28,7 @@ ) from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.async_substrate_interface import ( - SubstrateRequestException, -) +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.commands import sudo, wallets from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.subnets import price, subnets @@ -53,18 +53,19 @@ ) from typing_extensions import Annotated from textwrap import dedent -from websockets import ConnectionClosed +from websockets import ConnectionClosed, InvalidHandshake from yaml import safe_dump, safe_load try: from git import Repo, GitError except ImportError: + Repo = None class GitError(Exception): pass -__version__ = "8.2.0rc15" +__version__ = "9.0.0rc1" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) @@ -279,7 +280,8 @@ def parse_to_list( def verbosity_console_handler(verbosity_level: int = 1) -> None: """ Sets verbosity level of console output - :param verbosity_level: int corresponding to verbosity level of console output (0 is quiet, 1 is normal, 2 is verbose) + :param verbosity_level: int corresponding to verbosity level of console output (0 is quiet, 1 is normal, 2 is + verbose) """ if verbosity_level not in range(3): raise ValueError( @@ -310,7 +312,8 @@ def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[in return None elif netuid is None and all_netuids is False: answer = Prompt.ask( - f"Enter the [{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}]netuid[/{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}] to use. Leave blank for all netuids", + f"Enter the [{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}]netuid" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}] to use. Leave blank for all netuids", default=None, show_default=False, ) @@ -473,6 +476,16 @@ def version_callback(value: bool): raise typer.Exit() +def commands_callback(value: bool): + """ + Prints a tree of commands for the app + """ + if value: + cli = CLIManager() + console.print(cli.generate_command_tree()) + raise typer.Exit() + + class CLIManager: """ :var app: the main CLI Typer app @@ -817,6 +830,40 @@ def __init__(self): self.sudo_app.command("get_take", hidden=True)(self.sudo_get_take) self.sudo_app.command("set_take", hidden=True)(self.sudo_set_take) + def generate_command_tree(self) -> Tree: + """ + Generates a rich.Tree of the commands, subcommands, and groups of this app + """ + + def build_rich_tree(data: dict, parent: Tree): + for group, content in data.get("groups", {}).items(): + group_node = parent.add( + f"[bold cyan]{group}[/]" + ) # Add group to the tree + for command in content.get("commands", []): + group_node.add(f"[green]{command}[/]") # Add commands to the group + build_rich_tree(content, group_node) # Recurse for subgroups + + def traverse_group(group: typer.Typer) -> dict: + tree = {} + if commands := [ + cmd.name for cmd in group.registered_commands if not cmd.hidden + ]: + tree["commands"] = commands + for group in group.registered_groups: + if "groups" not in tree: + tree["groups"] = {} + if not group.hidden: + if group_transversal := traverse_group(group.typer_instance): + tree["groups"][group.name] = group_transversal + + return tree + + groups_and_commands = traverse_group(self.app) + root = Tree("[bold magenta]BTCLI Commands[/]") # Root node + build_rich_tree(groups_and_commands, root) + return root + def initialize_chain( self, network: Optional[list[str]] = None, @@ -853,29 +900,45 @@ def initialize_chain( self.subtensor = SubtensorInterface(defaults.subtensor.network) return self.subtensor - def _run_command(self, cmd: Coroutine) -> None: + def _run_command(self, cmd: Coroutine, exit_early: bool = True): """ Runs the supplied coroutine with `asyncio.run` """ async def _run(): + initiated = False try: if self.subtensor: async with self.subtensor: + initiated = True result = await cmd else: + initiated = True result = await cmd return result - except (ConnectionRefusedError, ssl.SSLError): + except (ConnectionRefusedError, ssl.SSLError, InvalidHandshake): err_console.print(f"Unable to connect to the chain: {self.subtensor}") - asyncio.create_task(cmd).cancel() - raise typer.Exit() - except ConnectionClosed: - asyncio.create_task(cmd).cancel() - raise typer.Exit() - except SubstrateRequestException as e: - err_console.print(str(e)) - raise typer.Exit() + verbose_console.print(traceback.format_exc()) + except ( + ConnectionClosed, + SubstrateRequestException, + KeyboardInterrupt, + ) as e: + if isinstance(e, SubstrateRequestException): + err_console.print(str(e)) + verbose_console.print(traceback.format_exc()) + except Exception as e: + err_console.print(f"An unknown error has occurred: {e}") + verbose_console.print(traceback.format_exc()) + finally: + if initiated is False: + asyncio.create_task(cmd).cancel() + if exit_early is True: + try: + raise typer.Exit() + except Exception as e: # ensures we always exit cleanly + if not isinstance(e, typer.Exit): + err_console.print(f"An unknown error has occurred: {e}") if sys.version_info < (3, 10): # For Python 3.9 or lower @@ -887,13 +950,22 @@ async def _run(): def main_callback( self, version: Annotated[ - Optional[bool], typer.Option("--version", callback=version_callback) + Optional[bool], + typer.Option( + "--version", callback=version_callback, help="Show BTCLI version" + ), + ] = None, + commands: Annotated[ + Optional[bool], + typer.Option( + "--commands", callback=commands_callback, help="Show BTCLI commands" + ), ] = None, ): """ - Command line interface (CLI) for Bittensor. Uses the values in the configuration file. These values can be overriden by passing them explicitly in the command line. + Command line interface (CLI) for Bittensor. Uses the values in the configuration file. These values can be + overriden by passing them explicitly in the command line. """ - # Load or create the config file if os.path.exists(self.config_path): with open(self.config_path, "r") as f: @@ -1491,8 +1563,8 @@ def wallet_transfer( validate=WV.WALLET, ) - # For Rao games - temporarilyt commented out - effective_network = get_effective_network(self.config, network) + # For Rao games - temporarily commented out + # effective_network = get_effective_network(self.config, network) # if is_rao_network(effective_network): # print_error("This command is disabled on the 'rao' network.") # raise typer.Exit() @@ -2268,7 +2340,7 @@ def wallet_set_id( "--image", help="The image URL for the identity.", ), - discord_handle: str = typer.Option( + discord: str = typer.Option( "", "--discord", help="The Discord handle for the identity.", @@ -2278,11 +2350,16 @@ def wallet_set_id( "--description", help="The description for the identity.", ), - additional_info: str = typer.Option( + additional: str = typer.Option( "", "--additional", help="Additional details for the identity.", ), + github_repo: str = typer.Option( + "", + "--github", + help="The GitHub repository for the identity.", + ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -2318,7 +2395,8 @@ def wallet_set_id( self.initialize_chain(network), wallet.coldkeypub.ss58_address, "Current on-chain identity", - ) + ), + exit_early=False, ) if prompt: @@ -2334,9 +2412,10 @@ def wallet_set_id( name, web_url, image_url, - discord_handle, + discord, description, - additional_info, + additional, + github_repo, ) return self._run_command( @@ -2349,6 +2428,7 @@ def wallet_set_id( identity["discord"], identity["description"], identity["additional"], + identity["github_repo"], prompt, ) ) @@ -2507,7 +2587,12 @@ def stake_list( return self._run_command( stake.stake_list( - wallet, coldkey_ss58, self.initialize_chain(network), live, verbose, no_prompt + wallet, + coldkey_ss58, + self.initialize_chain(network), + live, + verbose, + no_prompt, ) ) @@ -2622,7 +2707,8 @@ def stake_add( max_rows=12, prompt=False, delegate_selection=True, - ) + ), + exit_early=False, ) if selected_hotkey is None: print_error("No delegate selected. Exiting.") @@ -2682,7 +2768,8 @@ def stake_add( free_balance, staked_balance = self._run_command( wallets.wallet_balance( wallet, self.initialize_chain(network), False, None - ) + ), + exit_early=False, ) if free_balance == Balance.from_tao(0): print_error("You dont have any balance to stake.") @@ -3048,22 +3135,25 @@ def stake_move( ) origin_hotkey = wallet.hotkey.ss58_address else: - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[], - validate=WV.WALLET_AND_HOTKEY, - ) - origin_hotkey = wallet.hotkey.ss58_address + if is_valid_ss58_address(wallet_hotkey): + origin_hotkey = wallet_hotkey + else: + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[], + validate=WV.WALLET_AND_HOTKEY, + ) + origin_hotkey = wallet.hotkey.ss58_address if not interactive_selection: - if not origin_netuid: + if origin_netuid is None: origin_netuid = IntPrompt.ask( "Enter the [blue]origin subnet[/blue] (netuid) to move stake from" ) - if not destination_netuid: + if destination_netuid is None: destination_netuid = IntPrompt.ask( "Enter the [blue]destination subnet[/blue] (netuid) to move stake to" ) @@ -3599,7 +3689,8 @@ def sudo_set( self.verbosity_handler(quiet, verbose) hyperparams = self._run_command( - sudo.get_hyperparameters(self.initialize_chain(network), netuid) + sudo.get_hyperparameters(self.initialize_chain(network), netuid), + exit_early=False, ) if not hyperparams: @@ -3770,11 +3861,9 @@ def sudo_set_take( validate=WV.WALLET_AND_HOTKEY, ) - current_take = self._run_command( - sudo.get_current_take(self.initialize_chain(network), wallet) - ) - console.print( - f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%" + self._run_command( + sudo.display_current_take(self.initialize_chain(network), wallet), + exit_early=False, ) if not take: @@ -3818,11 +3907,8 @@ def sudo_get_take( validate=WV.WALLET_AND_HOTKEY, ) - current_take = self._run_command( - sudo.get_current_take(self.initialize_chain(network), wallet) - ) - console.print( - f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%" + self._run_command( + sudo.display_current_take(self.initialize_chain(network), wallet) ) def subnets_list( @@ -4024,6 +4110,18 @@ def subnets_create( "--email", help="Contact email for subnet", ), + subnet_url: Optional[str] = typer.Option( + None, "--subnet-url", "--url", help="Subnet URL" + ), + discord: Optional[str] = typer.Option( + None, "--discord-handle", "--discord", help="Discord handle" + ), + description: Optional[str] = typer.Option( + None, "--description", help="Description" + ), + additional_info: Optional[str] = typer.Option( + None, "--additional-info", help="Additional information" + ), prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4051,9 +4149,14 @@ def subnets_create( subnet_name=subnet_name, github_repo=github_repo, subnet_contact=subnet_contact, + subnet_url=subnet_url, + discord=discord, + description=description, + additional=additional_info, ) success = self._run_command( - subnets.create(wallet, self.initialize_chain(network), identity, prompt) + subnets.create(wallet, self.initialize_chain(network), identity, prompt), + exit_early=False, ) if success and prompt: diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index ffd5ab26b..6d3b805c3 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -8,7 +8,7 @@ class Constants: 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" - rao_entrypoint = "wss://rao.chain.opentensor.ai:443/" + rao_entrypoint = "wss://rao.chain.opentensor.ai:443" dev_entrypoint = "wss://dev.chain.opentensor.ai:443 " local_entrypoint = "ws://127.0.0.1:9944" network_map = { @@ -143,203 +143,6 @@ class WalletValidationTypes(Enum): "types": { "Balance": "u64", # Need to override default u128 }, - "runtime_api": { - "DelegateInfoRuntimeApi": { - "methods": { - "get_delegated": { - "params": [ - { - "name": "coldkey", - "type": "Vec", - }, - ], - "type": "Vec", - }, - "get_delegates": { - "params": [], - "type": "Vec", - }, - } - }, - "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", - }, - "get_neuron": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - { - "name": "uid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_neurons": { - "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", - }, - "get_subnet_stake_info_for_coldkeys": { - "params": [ - {"name": "coldkey_account_vecs", "type": "Vec>"}, - {"name": "netuid", "type": "u16"}, - ], - "type": "Vec", - }, - "get_subnet_stake_info_for_coldkey": { - "params": [ - {"name": "coldkey_account_vec", "type": "Vec"}, - {"name": "netuid", "type": "u16"}, - ], - "type": "Vec", - }, - "get_total_subnet_stake": { - "params": [{"name": "netuid", "type": "u16"}], - "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", - }, - "get_subnet_info": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_subnets_info": { - "params": [], - "type": "Vec", - }, - "get_subnet_info_v2": { - "params": [ - { - "name": "netuid", - "type": "u16", - }, - ], - "type": "Vec", - }, - "get_subnets_info_v2": { - "params": [], - "type": "Vec", - }, - "get_all_dynamic_info": { - "params": [], - "type": "Vec", - }, - "get_dynamic_info": { - "params": [{"name": "netuid", "type": "u16"}], - "type": "Vec", - }, - "get_subnet_state": { - "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", - }, - } - }, - }, } UNITS = [ @@ -830,7 +633,7 @@ class WalletValidationTypes(Enum): "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", + "max_weights_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", @@ -843,7 +646,7 @@ class WalletValidationTypes(Enum): "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_period": "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_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py deleted file mode 100644 index 342c2c787..000000000 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ /dev/null @@ -1,2752 +0,0 @@ -import asyncio -import json -import random -from collections import defaultdict -from dataclasses import dataclass -from hashlib import blake2b -from typing import Optional, Any, Union, Callable, Awaitable, cast, TYPE_CHECKING - -from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15 -from async_property import async_property -from scalecodec import GenericExtrinsic -from scalecodec.base import ScaleBytes, ScaleType, RuntimeConfigurationObject -from scalecodec.type_registry import load_type_registry_preset -from scalecodec.types import GenericCall -from bittensor_wallet import Keypair -from substrateinterface.exceptions import ( - SubstrateRequestException, - ExtrinsicNotFound, - BlockNotFound, -) -from substrateinterface.storage import StorageKey -from websockets.asyncio.client import connect -from websockets.exceptions import ConnectionClosed - -if TYPE_CHECKING: - from websockets.asyncio.client import ClientConnection - -ResultHandler = Callable[[dict, Any], Awaitable[tuple[dict, bool]]] - - -class TimeoutException(Exception): - pass - - -def timeout_handler(signum, frame): - raise TimeoutException("Operation timed out") - - -class ExtrinsicReceipt: - """ - Object containing information of submitted extrinsic. Block hash where extrinsic is included is required - when retrieving triggered events or determine if extrinsic was successful - """ - - def __init__( - self, - substrate: "AsyncSubstrateInterface", - extrinsic_hash: Optional[str] = None, - block_hash: Optional[str] = None, - block_number: Optional[int] = None, - extrinsic_idx: Optional[int] = None, - finalized=None, - ): - """ - Object containing information of submitted extrinsic. Block hash where extrinsic is included is required - when retrieving triggered events or determine if extrinsic was successful - - Parameters - ---------- - substrate - extrinsic_hash - block_hash - finalized - """ - self.substrate = substrate - self.extrinsic_hash = extrinsic_hash - self.block_hash = block_hash - self.block_number = block_number - self.finalized = finalized - - self.__extrinsic_idx = extrinsic_idx - self.__extrinsic = None - - self.__triggered_events: Optional[list] = None - self.__is_success: Optional[bool] = None - self.__error_message = None - self.__weight = None - self.__total_fee_amount = None - - async def get_extrinsic_identifier(self) -> str: - """ - Returns the on-chain identifier for this extrinsic in format "[block_number]-[extrinsic_idx]" e.g. 134324-2 - Returns - ------- - str - """ - if self.block_number is None: - if self.block_hash is None: - raise ValueError( - "Cannot create extrinsic identifier: block_hash is not set" - ) - - self.block_number = await self.substrate.get_block_number(self.block_hash) - - if self.block_number is None: - raise ValueError( - "Cannot create extrinsic identifier: unknown block_hash" - ) - - return f"{self.block_number}-{await self.extrinsic_idx}" - - async def retrieve_extrinsic(self): - if not self.block_hash: - raise ValueError( - "ExtrinsicReceipt can't retrieve events because it's unknown which block_hash it is " - "included, manually set block_hash or use `wait_for_inclusion` when sending extrinsic" - ) - # Determine extrinsic idx - - block = await self.substrate.get_block(block_hash=self.block_hash) - - extrinsics = block["extrinsics"] - - if len(extrinsics) > 0: - if self.__extrinsic_idx is None: - self.__extrinsic_idx = self.__get_extrinsic_index( - block_extrinsics=extrinsics, extrinsic_hash=self.extrinsic_hash - ) - - if self.__extrinsic_idx >= len(extrinsics): - raise ExtrinsicNotFound() - - self.__extrinsic = extrinsics[self.__extrinsic_idx] - - @async_property - async def extrinsic_idx(self) -> int: - """ - Retrieves the index of this extrinsic in containing block - - Returns - ------- - int - """ - if self.__extrinsic_idx is None: - await self.retrieve_extrinsic() - return self.__extrinsic_idx - - @async_property - async def triggered_events(self) -> list: - """ - Gets triggered events for submitted extrinsic. block_hash where extrinsic is included is required, manually - set block_hash or use `wait_for_inclusion` when submitting extrinsic - - Returns - ------- - list - """ - if self.__triggered_events is None: - if not self.block_hash: - raise ValueError( - "ExtrinsicReceipt can't retrieve events because it's unknown which block_hash it is " - "included, manually set block_hash or use `wait_for_inclusion` when sending extrinsic" - ) - - if await self.extrinsic_idx is None: - await self.retrieve_extrinsic() - - self.__triggered_events = [] - - for event in await self.substrate.get_events(block_hash=self.block_hash): - if event["extrinsic_idx"] == await self.extrinsic_idx: - self.__triggered_events.append(event) - - return cast(list, self.__triggered_events) - - async def process_events(self): - if await self.triggered_events: - self.__total_fee_amount = 0 - - # Process fees - has_transaction_fee_paid_event = False - - for event in await self.triggered_events: - if ( - event["event"]["module_id"] == "TransactionPayment" - and event["event"]["event_id"] == "TransactionFeePaid" - ): - self.__total_fee_amount = event["event"]["attributes"]["actual_fee"] - has_transaction_fee_paid_event = True - - # Process other events - for event in await self.triggered_events: - # Check events - if ( - event["event"]["module_id"] == "System" - and event["event"]["event_id"] == "ExtrinsicSuccess" - ): - self.__is_success = True - self.__error_message = None - - if "dispatch_info" in event["event"]["attributes"]: - self.__weight = event["event"]["attributes"]["dispatch_info"][ - "weight" - ] - else: - # Backwards compatibility - self.__weight = event["event"]["attributes"]["weight"] - - elif ( - event["event"]["module_id"] == "System" - and event["event"]["event_id"] == "ExtrinsicFailed" - ): - self.__is_success = False - - dispatch_info = event["event"]["attributes"]["dispatch_info"] - dispatch_error = event["event"]["attributes"]["dispatch_error"] - - self.__weight = dispatch_info["weight"] - - if "Module" in dispatch_error: - module_index = dispatch_error["Module"][0]["index"] - error_index = int.from_bytes( - bytes(dispatch_error["Module"][0]["error"]), - byteorder="little", - signed=False, - ) - - if isinstance(error_index, str): - # Actual error index is first u8 in new [u8; 4] format - error_index = int(error_index[2:4], 16) - module_error = self.substrate.metadata.get_module_error( - module_index=module_index, error_index=error_index - ) - self.__error_message = { - "type": "Module", - "name": module_error.name, - "docs": module_error.docs, - } - elif "BadOrigin" in dispatch_error: - self.__error_message = { - "type": "System", - "name": "BadOrigin", - "docs": "Bad origin", - } - elif "CannotLookup" in dispatch_error: - self.__error_message = { - "type": "System", - "name": "CannotLookup", - "docs": "Cannot lookup", - } - elif "Other" in dispatch_error: - self.__error_message = { - "type": "System", - "name": "Other", - "docs": "Unspecified error occurred", - } - - elif not has_transaction_fee_paid_event: - if ( - event["event"]["module_id"] == "Treasury" - and event["event"]["event_id"] == "Deposit" - ): - self.__total_fee_amount += event["event"]["attributes"]["value"] - elif ( - event["event"]["module_id"] == "Balances" - and event["event"]["event_id"] == "Deposit" - ): - self.__total_fee_amount += event.value["attributes"]["amount"] - - @async_property - async def is_success(self) -> bool: - """ - Returns `True` if `ExtrinsicSuccess` event is triggered, `False` in case of `ExtrinsicFailed` - In case of False `error_message` will contain more details about the error - - - Returns - ------- - bool - """ - if self.__is_success is None: - await self.process_events() - - return cast(bool, self.__is_success) - - @async_property - async def error_message(self) -> Optional[dict]: - """ - Returns the error message if the extrinsic failed in format e.g.: - - `{'type': 'System', 'name': 'BadOrigin', 'docs': 'Bad origin'}` - - Returns - ------- - dict - """ - if self.__error_message is None: - if await self.is_success: - return None - await self.process_events() - return self.__error_message - - @async_property - async def weight(self) -> Union[int, dict]: - """ - Contains the actual weight when executing this extrinsic - - Returns - ------- - int (WeightV1) or dict (WeightV2) - """ - if self.__weight is None: - await self.process_events() - return self.__weight - - @async_property - async def total_fee_amount(self) -> int: - """ - Contains the total fee costs deducted when executing this extrinsic. This includes fee for the validator ( - (`Balances.Deposit` event) and the fee deposited for the treasury (`Treasury.Deposit` event) - - Returns - ------- - int - """ - if self.__total_fee_amount is None: - await self.process_events() - return cast(int, self.__total_fee_amount) - - # Helper functions - @staticmethod - def __get_extrinsic_index(block_extrinsics: list, extrinsic_hash: str) -> int: - """ - Returns the index of a provided extrinsic - """ - for idx, extrinsic in enumerate(block_extrinsics): - if ( - extrinsic.extrinsic_hash - and f"0x{extrinsic.extrinsic_hash.hex()}" == extrinsic_hash - ): - return idx - raise ExtrinsicNotFound() - - # Backwards compatibility methods - def __getitem__(self, item): - return getattr(self, item) - - def __iter__(self): - for item in self.__dict__.items(): - yield item - - def get(self, name): - return self[name] - - -class QueryMapResult: - def __init__( - self, - records: list, - page_size: int, - substrate: "AsyncSubstrateInterface", - module: Optional[str] = None, - storage_function: Optional[str] = None, - params: Optional[list] = None, - block_hash: Optional[str] = None, - last_key: Optional[str] = None, - max_results: Optional[int] = None, - ignore_decoding_errors: bool = False, - ): - self.records = records - self.page_size = page_size - self.module = module - self.storage_function = storage_function - self.block_hash = block_hash - self.substrate = substrate - self.last_key = last_key - self.max_results = max_results - self.params = params - self.ignore_decoding_errors = ignore_decoding_errors - self.loading_complete = False - self._buffer = iter(self.records) # Initialize the buffer with initial records - - async def retrieve_next_page(self, start_key) -> list: - result = await self.substrate.query_map( - module=self.module, - storage_function=self.storage_function, - params=self.params, - page_size=self.page_size, - block_hash=self.block_hash, - start_key=start_key, - max_results=self.max_results, - ignore_decoding_errors=self.ignore_decoding_errors, - ) - - # Update last key from new result set to use as offset for next page - self.last_key = result.last_key - return result.records - - def __aiter__(self): - return self - - async def __anext__(self): - try: - # Try to get the next record from the buffer - return next(self._buffer) - except StopIteration: - # If no more records in the buffer, try to fetch the next page - if self.loading_complete: - raise StopAsyncIteration - - next_page = await self.retrieve_next_page(self.last_key) - if not next_page: - self.loading_complete = True - raise StopAsyncIteration - - # Update the buffer with the newly fetched records - self._buffer = iter(next_page) - return next(self._buffer) - - def __getitem__(self, item): - return self.records[item] - - -@dataclass -class Preprocessed: - queryable: str - method: str - params: list - value_scale_type: str - storage_item: ScaleType - - -class RuntimeCache: - blocks: dict[int, "Runtime"] - block_hashes: dict[str, "Runtime"] - - def __init__(self): - self.blocks = {} - self.block_hashes = {} - - def add_item( - self, block: Optional[int], block_hash: Optional[str], runtime: "Runtime" - ): - if block is not None: - self.blocks[block] = runtime - if block_hash is not None: - self.block_hashes[block_hash] = runtime - - def retrieve( - self, block: Optional[int] = None, block_hash: Optional[str] = None - ) -> Optional["Runtime"]: - if block is not None: - return self.blocks.get(block) - elif block_hash is not None: - return self.block_hashes.get(block_hash) - else: - return None - - -class Runtime: - block_hash: str - block_id: int - runtime_version = None - transaction_version = None - cache_region = None - metadata = None - type_registry_preset = None - - def __init__(self, chain, runtime_config, metadata, type_registry): - self.runtime_config = RuntimeConfigurationObject() - self.config = {} - self.chain = chain - self.type_registry = type_registry - self.runtime_config = runtime_config - self.metadata = metadata - - def __str__(self): - return f"Runtime: {self.chain} | {self.config}" - - @property - def implements_scaleinfo(self) -> bool: - """ - Returns True if current runtime implementation a `PortableRegistry` (`MetadataV14` and higher) - """ - if self.metadata: - return self.metadata.portable_registry is not None - else: - return False - - def reload_type_registry( - self, use_remote_preset: bool = True, auto_discover: bool = True - ): - """ - Reload type registry and preset used to instantiate the SubstrateInterface object. Useful to periodically apply - changes in type definitions when a runtime upgrade occurred - - Parameters - ---------- - use_remote_preset: When True preset is downloaded from Github master, otherwise use files from local installed - scalecodec package - auto_discover - - Returns - ------- - - """ - self.runtime_config.clear_type_registry() - - self.runtime_config.implements_scale_info = self.implements_scaleinfo - - # Load metadata types in runtime configuration - self.runtime_config.update_type_registry(load_type_registry_preset(name="core")) - self.apply_type_registry_presets( - use_remote_preset=use_remote_preset, auto_discover=auto_discover - ) - - def apply_type_registry_presets( - self, - use_remote_preset: bool = True, - auto_discover: bool = True, - ): - """ - Applies type registry presets to the runtime - :param use_remote_preset: bool, whether to use presets from remote - :param auto_discover: bool, whether to use presets from local installed scalecodec package - """ - if self.type_registry_preset is not None: - # Load type registry according to preset - type_registry_preset_dict = load_type_registry_preset( - name=self.type_registry_preset, use_remote_preset=use_remote_preset - ) - - if not type_registry_preset_dict: - raise ValueError( - f"Type registry preset '{self.type_registry_preset}' not found" - ) - - elif auto_discover: - # Try to auto discover type registry preset by chain name - type_registry_name = self.chain.lower().replace(" ", "-") - try: - type_registry_preset_dict = load_type_registry_preset( - type_registry_name - ) - self.type_registry_preset = type_registry_name - except ValueError: - type_registry_preset_dict = None - - else: - type_registry_preset_dict = None - - if type_registry_preset_dict: - # Load type registries in runtime configuration - if self.implements_scaleinfo is False: - # Only runtime with no embedded types in metadata need the default set of explicit defined types - self.runtime_config.update_type_registry( - load_type_registry_preset( - "legacy", use_remote_preset=use_remote_preset - ) - ) - - if self.type_registry_preset != "legacy": - self.runtime_config.update_type_registry(type_registry_preset_dict) - - if self.type_registry: - # Load type registries in runtime configuration - self.runtime_config.update_type_registry(self.type_registry) - - -class RequestManager: - RequestResults = dict[Union[str, int], list[Union[ScaleType, dict]]] - - def __init__(self, payloads): - self.response_map = {} - self.responses = defaultdict(lambda: {"complete": False, "results": []}) - self.payloads_count = len(payloads) - - def add_request(self, item_id: int, request_id: Any): - """ - Adds an outgoing request to the responses map for later retrieval - """ - self.response_map[item_id] = request_id - - def overwrite_request(self, item_id: int, request_id: Any): - """ - Overwrites an existing request in the responses map with a new request_id. This is used - for multipart responses that generate a subscription id we need to watch, rather than the initial - request_id. - """ - self.response_map[request_id] = self.response_map.pop(item_id) - return request_id - - def add_response(self, item_id: int, response: dict, complete: bool): - """ - Maps a response to the request for later retrieval - """ - request_id = self.response_map[item_id] - self.responses[request_id]["results"].append(response) - self.responses[request_id]["complete"] = complete - - @property - def is_complete(self) -> bool: - """ - Returns whether all requests in the manager have completed - """ - return ( - all(info["complete"] for info in self.responses.values()) - and len(self.responses) == self.payloads_count - ) - - def get_results(self) -> RequestResults: - """ - Generates a dictionary mapping the requests initiated to the responses received. - """ - return { - request_id: info["results"] for request_id, info in self.responses.items() - } - - -class Websocket: - def __init__( - self, - ws_url: str, - max_subscriptions=1024, - max_connections=100, - shutdown_timer=5, - options: Optional[dict] = None, - ): - """ - Websocket manager object. Allows for the use of a single websocket connection by multiple - calls. - - :param ws_url: Websocket URL to connect to - :param max_subscriptions: Maximum number of subscriptions per websocket connection - :param max_connections: Maximum number of connections total - :param shutdown_timer: Number of seconds to shut down websocket connection after last use - """ - # TODO allow setting max concurrent connections and rpc subscriptions per connection - # TODO reconnection logic - self.ws_url = ws_url - self.ws: Optional["ClientConnection"] = None - self.id = 0 - self.max_subscriptions = max_subscriptions - self.max_connections = max_connections - self.shutdown_timer = shutdown_timer - self._received = {} - self._in_use = 0 - self._receiving_task = None - self._attempts = 0 - self._initialized = False - self._lock = asyncio.Lock() - self._exit_task = None - self._open_subscriptions = 0 - self._options = options if options else {} - - async def __aenter__(self): - async with self._lock: - self._in_use += 1 - if self._exit_task: - self._exit_task.cancel() - if not self._initialized: - self._initialized = True - self.ws = await asyncio.wait_for( - connect(self.ws_url, **self._options), timeout=10 - ) - self._receiving_task = asyncio.create_task(self._start_receiving()) - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - async with self._lock: - self._in_use -= 1 - if self._exit_task is not None: - self._exit_task.cancel() - try: - await self._exit_task - except asyncio.CancelledError: - pass - if self._in_use == 0 and self.ws is not None: - self.id = 0 - self._open_subscriptions = 0 - self._exit_task = asyncio.create_task(self._exit_with_timer()) - - async def _exit_with_timer(self): - """ - Allows for graceful shutdown of websocket connection after specified number of seconds, allowing - for reuse of the websocket connection. - """ - try: - await asyncio.sleep(self.shutdown_timer) - await self.shutdown() - except asyncio.CancelledError: - pass - - async def shutdown(self): - async with self._lock: - try: - self._receiving_task.cancel() - await self._receiving_task - await self.ws.close() - except (AttributeError, asyncio.CancelledError): - pass - self.ws = None - self._initialized = False - self._receiving_task = None - self.id = 0 - - async def _recv(self) -> None: - try: - response = json.loads(await self.ws.recv()) - async with self._lock: - self._open_subscriptions -= 1 - if "id" in response: - self._received[response["id"]] = response - elif "params" in response: - self._received[response["params"]["subscription"]] = response - else: - raise KeyError(response) - except ConnectionClosed: - raise - except KeyError as e: - raise e - - async def _start_receiving(self): - try: - while True: - await self._recv() - except asyncio.CancelledError: - pass - except ConnectionClosed: - # TODO try reconnect, but only if it's needed - raise - - async def send(self, payload: dict) -> int: - """ - Sends a payload to the websocket connection. - - :param payload: payload, generate a payload with the AsyncSubstrateInterface.make_payload method - """ - async with self._lock: - original_id = self.id - self.id += 1 - self._open_subscriptions += 1 - try: - await self.ws.send(json.dumps({**payload, **{"id": original_id}})) - return original_id - except ConnectionClosed: - raise - - async def retrieve(self, item_id: int) -> Optional[dict]: - """ - Retrieves a single item from received responses dict queue - - :param item_id: id of the item to retrieve - - :return: retrieved item - """ - while True: - async with self._lock: - if item_id in self._received: - return self._received.pop(item_id) - await asyncio.sleep(0.1) - - -class AsyncSubstrateInterface: - runtime = None - registry: Optional[PortableRegistry] = None - - def __init__( - self, - chain_endpoint: str, - use_remote_preset=False, - auto_discover=True, - auto_reconnect=True, - ss58_format=None, - type_registry=None, - chain_name=None, - ): - """ - The asyncio-compatible version of the subtensor interface commands we use in bittensor - """ - self.chain_endpoint = chain_endpoint - self.__chain = chain_name - self.ws = Websocket( - chain_endpoint, - options={ - "max_size": 2**32, - "write_limit": 2**16, - }, - ) - self._lock = asyncio.Lock() - self.last_block_hash: Optional[str] = None - self.config = { - "use_remote_preset": use_remote_preset, - "auto_discover": auto_discover, - "auto_reconnect": auto_reconnect, - "rpc_methods": None, - "strict_scale_decode": True, - } - self.initialized = False - self._forgettable_task = None - self.ss58_format = ss58_format - self.type_registry = type_registry - self.runtime_cache = RuntimeCache() - self.block_id: Optional[int] = None - self.runtime_version = None - self.runtime_config = RuntimeConfigurationObject() - self.__metadata_cache = {} - self.type_registry_preset = None - self.transaction_version = None - self.metadata = None - self.metadata_version_hex = "0x0f000000" # v15 - - async def __aenter__(self): - await self.initialize() - - async def initialize(self): - """ - Initialize the connection to the chain. - """ - async with self._lock: - if not self.initialized: - if not self.__chain: - chain = await self.rpc_request("system_chain", []) - self.__chain = chain.get("result") - self.reload_type_registry() - await asyncio.gather(self.load_registry(), self.init_runtime(None)) - self.initialized = True - - async def __aexit__(self, exc_type, exc_val, exc_tb): - pass - - @property - def chain(self): - """ - Returns the substrate chain currently associated with object - """ - return self.__chain - - async def get_storage_item(self, module: str, storage_function: str): - if not self.metadata: - await self.init_runtime() - metadata_pallet = self.metadata.get_metadata_pallet(module) - storage_item = metadata_pallet.get_storage_function(storage_function) - return storage_item - - async def _get_current_block_hash( - self, block_hash: Optional[str], reuse: bool - ) -> Optional[str]: - if block_hash: - self.last_block_hash = block_hash - return block_hash - elif reuse: - if self.last_block_hash: - return self.last_block_hash - return block_hash - - async def load_registry(self): - metadata_rpc_result = await self.rpc_request( - "state_call", - ["Metadata_metadata_at_version", self.metadata_version_hex], - ) - metadata_option_hex_str = metadata_rpc_result["result"] - metadata_option_bytes = bytes.fromhex(metadata_option_hex_str[2:]) - metadata_v15 = MetadataV15.decode_from_metadata_option(metadata_option_bytes) - self.registry = PortableRegistry.from_metadata_v15(metadata_v15) - - async def decode_scale( - self, type_string, scale_bytes: bytes, return_scale_obj=False - ): - """ - Helper function to decode arbitrary SCALE-bytes (e.g. 0x02000000) according to given RUST type_string - (e.g. BlockNumber). The relevant versioning information of the type (if defined) will be applied if block_hash - is set - - Parameters - ---------- - type_string - scale_bytes - block_hash - return_scale_obj: if True the SCALE object itself is returned, otherwise the serialized dict value of the object - - Returns - ------- - - """ - if scale_bytes == b"\x00": - obj = None - else: - obj = decode_by_type_string(type_string, self.registry, scale_bytes) - return obj - - async def init_runtime( - self, block_hash: Optional[str] = None, block_id: Optional[int] = None - ) -> Runtime: - """ - This method is used by all other methods that deals with metadata and types defined in the type registry. - It optionally retrieves the block_hash when block_id is given and sets the applicable metadata for that - block_hash. Also, it applies all the versioned types at the time of the block_hash. - - Because parsing of metadata and type registry is quite heavy, the result will be cached per runtime id. - In the future there could be support for caching backends like Redis to make this cache more persistent. - - :param block_hash: optional block hash, should not be specified if block_id is - :param block_id: optional block id, should not be specified if block_hash is - - :returns: Runtime object - """ - - async def get_runtime(block_hash, block_id) -> Runtime: - # Check if runtime state already set to current block - if ( - (block_hash and block_hash == self.last_block_hash) - or (block_id and block_id == self.block_id) - ) and self.metadata is not None: - return Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - - if block_id is not None: - block_hash = await self.get_block_hash(block_id) - - if not block_hash: - block_hash = await self.get_chain_head() - - self.last_block_hash = block_hash - self.block_id = block_id - - # In fact calls and storage functions are decoded against runtime of previous block, therefor retrieve - # metadata and apply type registry of runtime of parent block - block_header = await self.rpc_request( - "chain_getHeader", [self.last_block_hash] - ) - - if block_header["result"] is None: - raise SubstrateRequestException( - f'Block not found for "{self.last_block_hash}"' - ) - - parent_block_hash: str = block_header["result"]["parentHash"] - - if ( - parent_block_hash - == "0x0000000000000000000000000000000000000000000000000000000000000000" - ): - runtime_block_hash = self.last_block_hash - else: - runtime_block_hash = parent_block_hash - - runtime_info = await self.get_block_runtime_version( - block_hash=runtime_block_hash - ) - - if runtime_info is None: - raise SubstrateRequestException( - f"No runtime information for block '{block_hash}'" - ) - # Check if runtime state already set to current block - if ( - runtime_info.get("specVersion") == self.runtime_version - and self.metadata is not None - ): - return Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - - self.runtime_version = runtime_info.get("specVersion") - self.transaction_version = runtime_info.get("transactionVersion") - - if not self.metadata: - if self.runtime_version in self.__metadata_cache: - # Get metadata from cache - # self.debug_message('Retrieved metadata for {} from memory'.format(self.runtime_version)) - metadata = self.metadata = self.__metadata_cache[ - self.runtime_version - ] - else: - metadata = self.metadata = await self.get_block_metadata( - block_hash=runtime_block_hash, decode=True - ) - # self.debug_message('Retrieved metadata for {} from Substrate node'.format(self.runtime_version)) - - # Update metadata cache - self.__metadata_cache[self.runtime_version] = self.metadata - else: - metadata = self.metadata - # Update type registry - self.reload_type_registry(use_remote_preset=False, auto_discover=True) - - if self.implements_scaleinfo: - # self.debug_message('Add PortableRegistry from metadata to type registry') - self.runtime_config.add_portable_registry(self.metadata) - - # Set active runtime version - self.runtime_config.set_active_spec_version_id(self.runtime_version) - - # Check and apply runtime constants - ss58_prefix_constant = await self.get_constant( - "System", "SS58Prefix", block_hash=block_hash - ) - - if ss58_prefix_constant: - self.ss58_format = ss58_prefix_constant - - # Set runtime compatibility flags - try: - _ = self.runtime_config.create_scale_object( - "sp_weights::weight_v2::Weight" - ) - self.config["is_weight_v2"] = True - self.runtime_config.update_type_registry_types( - {"Weight": "sp_weights::weight_v2::Weight"} - ) - except NotImplementedError: - self.config["is_weight_v2"] = False - self.runtime_config.update_type_registry_types({"Weight": "WeightV1"}) - return Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - - if block_id and block_hash: - raise ValueError("Cannot provide block_hash and block_id at the same time") - - if ( - not (runtime := self.runtime_cache.retrieve(block_id, block_hash)) - or runtime.metadata is None - ): - runtime = await get_runtime(block_hash, block_id) - self.runtime_cache.add_item(block_id, block_hash, runtime) - return runtime - - def reload_type_registry( - self, use_remote_preset: bool = True, auto_discover: bool = True - ): - """ - Reload type registry and preset used to instantiate the SubtrateInterface object. Useful to periodically apply - changes in type definitions when a runtime upgrade occurred - - Parameters - ---------- - use_remote_preset: When True preset is downloaded from Github master, otherwise use files from local installed scalecodec package - auto_discover - - Returns - ------- - - """ - self.runtime_config.clear_type_registry() - - self.runtime_config.implements_scale_info = self.implements_scaleinfo - - # Load metadata types in runtime configuration - self.runtime_config.update_type_registry(load_type_registry_preset(name="core")) - self.apply_type_registry_presets( - use_remote_preset=use_remote_preset, auto_discover=auto_discover - ) - - def apply_type_registry_presets( - self, use_remote_preset: bool = True, auto_discover: bool = True - ): - if self.type_registry_preset is not None: - # Load type registry according to preset - type_registry_preset_dict = load_type_registry_preset( - name=self.type_registry_preset, use_remote_preset=use_remote_preset - ) - - if not type_registry_preset_dict: - raise ValueError( - f"Type registry preset '{self.type_registry_preset}' not found" - ) - - elif auto_discover: - # Try to auto discover type registry preset by chain name - type_registry_name = self.chain.lower().replace(" ", "-") - try: - type_registry_preset_dict = load_type_registry_preset( - type_registry_name - ) - # self.debug_message(f"Auto set type_registry_preset to {type_registry_name} ...") - self.type_registry_preset = type_registry_name - except ValueError: - type_registry_preset_dict = None - - else: - type_registry_preset_dict = None - - if type_registry_preset_dict: - # Load type registries in runtime configuration - if self.implements_scaleinfo is False: - # Only runtime with no embedded types in metadata need the default set of explicit defined types - self.runtime_config.update_type_registry( - load_type_registry_preset( - "legacy", use_remote_preset=use_remote_preset - ) - ) - - if self.type_registry_preset != "legacy": - self.runtime_config.update_type_registry(type_registry_preset_dict) - - if self.type_registry: - # Load type registries in runtime configuration - self.runtime_config.update_type_registry(self.type_registry) - - @property - def implements_scaleinfo(self) -> Optional[bool]: - """ - Returns True if current runtime implementation a `PortableRegistry` (`MetadataV14` and higher) - - Returns - ------- - bool - """ - if self.metadata: - return self.metadata.portable_registry is not None - else: - return None - - async def create_storage_key( - self, - pallet: str, - storage_function: str, - params: Optional[list] = None, - block_hash: str = None, - ) -> StorageKey: - """ - Create a `StorageKey` instance providing storage function details. See `subscribe_storage()`. - - Parameters - ---------- - pallet: name of pallet - storage_function: name of storage function - params: Optional list of parameters in case of a Mapped storage function - - Returns - ------- - StorageKey - """ - await self.init_runtime(block_hash=block_hash) - - return StorageKey.create_from_storage_function( - pallet, - storage_function, - params, - runtime_config=self.runtime_config, - metadata=self.metadata, - ) - - async def _get_block_handler( - self, - block_hash: str, - ignore_decoding_errors: bool = False, - include_author: bool = False, - header_only: bool = False, - finalized_only: bool = False, - subscription_handler: Optional[Callable] = None, - ): - try: - await self.init_runtime(block_hash=block_hash) - except BlockNotFound: - return None - - async def decode_block(block_data, block_data_hash=None): - if block_data: - if block_data_hash: - block_data["header"]["hash"] = block_data_hash - - if type(block_data["header"]["number"]) is str: - # Convert block number from hex (backwards compatibility) - block_data["header"]["number"] = int( - block_data["header"]["number"], 16 - ) - - extrinsic_cls = self.runtime_config.get_decoder_class("Extrinsic") - - if "extrinsics" in block_data: - for idx, extrinsic_data in enumerate(block_data["extrinsics"]): - extrinsic_decoder = extrinsic_cls( - data=ScaleBytes(extrinsic_data), - metadata=self.metadata, - runtime_config=self.runtime_config, - ) - try: - extrinsic_decoder.decode(check_remaining=True) - block_data["extrinsics"][idx] = extrinsic_decoder - - except Exception as e: - if not ignore_decoding_errors: - raise - block_data["extrinsics"][idx] = None - - for idx, log_data in enumerate(block_data["header"]["digest"]["logs"]): - if type(log_data) is str: - # Convert digest log from hex (backwards compatibility) - try: - log_digest_cls = self.runtime_config.get_decoder_class( - "sp_runtime::generic::digest::DigestItem" - ) - - if log_digest_cls is None: - raise NotImplementedError( - "No decoding class found for 'DigestItem'" - ) - - log_digest = log_digest_cls(data=ScaleBytes(log_data)) - log_digest.decode( - check_remaining=self.config.get("strict_scale_decode") - ) - - block_data["header"]["digest"]["logs"][idx] = log_digest - - if include_author and "PreRuntime" in log_digest.value: - if self.implements_scaleinfo: - engine = bytes(log_digest[1][0]) - # Retrieve validator set - parent_hash = block_data["header"]["parentHash"] - validator_set = await self.query( - "Session", "Validators", block_hash=parent_hash - ) - - if engine == b"BABE": - babe_predigest = ( - self.runtime_config.create_scale_object( - type_string="RawBabePreDigest", - data=ScaleBytes( - bytes(log_digest[1][1]) - ), - ) - ) - - babe_predigest.decode( - check_remaining=self.config.get( - "strict_scale_decode" - ) - ) - - rank_validator = babe_predigest[1].value[ - "authority_index" - ] - - block_author = validator_set[rank_validator] - block_data["author"] = block_author.value - - elif engine == b"aura": - aura_predigest = ( - self.runtime_config.create_scale_object( - type_string="RawAuraPreDigest", - data=ScaleBytes( - bytes(log_digest[1][1]) - ), - ) - ) - - aura_predigest.decode(check_remaining=True) - - rank_validator = aura_predigest.value[ - "slot_number" - ] % len(validator_set) - - block_author = validator_set[rank_validator] - block_data["author"] = block_author.value - else: - raise NotImplementedError( - f"Cannot extract author for engine {log_digest.value['PreRuntime'][0]}" - ) - else: - if ( - log_digest.value["PreRuntime"]["engine"] - == "BABE" - ): - validator_set = await self.query( - "Session", - "Validators", - block_hash=block_hash, - ) - rank_validator = log_digest.value["PreRuntime"][ - "data" - ]["authority_index"] - - block_author = validator_set.elements[ - rank_validator - ] - block_data["author"] = block_author.value - else: - raise NotImplementedError( - f"Cannot extract author for engine {log_digest.value['PreRuntime']['engine']}" - ) - - except Exception: - if not ignore_decoding_errors: - raise - block_data["header"]["digest"]["logs"][idx] = None - - return block_data - - if callable(subscription_handler): - rpc_method_prefix = "Finalized" if finalized_only else "New" - - async def result_handler(message, update_nr, subscription_id): - new_block = await decode_block({"header": message["params"]["result"]}) - - subscription_result = subscription_handler( - new_block, update_nr, subscription_id - ) - - if subscription_result is not None: - # Handler returned end result: unsubscribe from further updates - self._forgettable_task = asyncio.create_task( - self.rpc_request( - f"chain_unsubscribe{rpc_method_prefix}Heads", - [subscription_id], - ) - ) - - return subscription_result - - result = await self._make_rpc_request( - [ - self.make_payload( - "_get_block_handler", - f"chain_subscribe{rpc_method_prefix}Heads", - [], - ) - ], - result_handler=result_handler, - ) - - return result - - else: - if header_only: - response = await self.rpc_request("chain_getHeader", [block_hash]) - return await decode_block( - {"header": response["result"]}, block_data_hash=block_hash - ) - - else: - response = await self.rpc_request("chain_getBlock", [block_hash]) - return await decode_block( - response["result"]["block"], block_data_hash=block_hash - ) - - async def get_block( - self, - block_hash: Optional[str] = None, - block_number: Optional[int] = None, - ignore_decoding_errors: bool = False, - include_author: bool = False, - finalized_only: bool = False, - ) -> Optional[dict]: - """ - Retrieves a block and decodes its containing extrinsics and log digest items. If `block_hash` and `block_number` - is omitted the chain tip will be retrieve, or the finalized head if `finalized_only` is set to true. - - Either `block_hash` or `block_number` should be set, or both omitted. - - Parameters - ---------- - block_hash: the hash of the block to be retrieved - block_number: the block number to retrieved - ignore_decoding_errors: When set this will catch all decoding errors, set the item to None and continue decoding - include_author: This will retrieve the block author from the validator set and add to the result - finalized_only: when no `block_hash` or `block_number` is set, this will retrieve the finalized head - - Returns - ------- - A dict containing the extrinsic and digest logs data - """ - if block_hash and block_number: - raise ValueError("Either block_hash or block_number should be be set") - - if block_number is not None: - block_hash = await self.get_block_hash(block_number) - - if block_hash is None: - return - - if block_hash and finalized_only: - raise ValueError( - "finalized_only cannot be True when block_hash is provided" - ) - - if block_hash is None: - # Retrieve block hash - if finalized_only: - block_hash = await self.get_chain_finalised_head() - else: - block_hash = await self.get_chain_head() - - return await self._get_block_handler( - block_hash=block_hash, - ignore_decoding_errors=ignore_decoding_errors, - header_only=False, - include_author=include_author, - ) - - async def get_events(self, block_hash: Optional[str] = None) -> list: - """ - Convenience method to get events for a certain block (storage call for module 'System' and function 'Events') - - Parameters - ---------- - block_hash - - Returns - ------- - list - """ - - def convert_event_data(data): - # Extract phase information - phase_key, phase_value = next(iter(data["phase"].items())) - try: - extrinsic_idx = phase_value[0] - except IndexError: - extrinsic_idx = None - - # Extract event details - module_id, event_data = next(iter(data["event"].items())) - event_id, attributes_data = next(iter(event_data[0].items())) - - # Convert class and pays_fee dictionaries to their string equivalents if they exist - attributes = attributes_data - if isinstance(attributes, dict): - for key, value in attributes.items(): - if isinstance(value, dict): - # Convert nested single-key dictionaries to their keys as strings - sub_key = next(iter(value.keys())) - if value[sub_key] == (): - attributes[key] = sub_key - - # Create the converted dictionary - converted = { - "phase": phase_key, - "extrinsic_idx": extrinsic_idx, - "event": { - "module_id": module_id, - "event_id": event_id, - "attributes": attributes, - }, - "topics": list(data["topics"]), # Convert topics tuple to a list - } - - return converted - - events = [] - - if not block_hash: - block_hash = await self.get_chain_head() - - storage_obj = await self.query( - module="System", storage_function="Events", block_hash=block_hash - ) - if storage_obj: - for item in list(storage_obj): - # print("item!", item) - events.append(convert_event_data(item)) - # events += list(storage_obj) - return events - - async def get_block_runtime_version(self, block_hash: str) -> dict: - """ - Retrieve the runtime version id of given block_hash - """ - response = await self.rpc_request("state_getRuntimeVersion", [block_hash]) - return response.get("result") - - async def get_block_metadata( - self, block_hash: Optional[str] = None, decode: bool = True - ) -> Union[dict, ScaleType]: - """ - A pass-though to existing JSONRPC method `state_getMetadata`. - - Parameters - ---------- - block_hash - decode: True for decoded version - - Returns - ------- - - """ - params = None - if decode and not self.runtime_config: - raise ValueError( - "Cannot decode runtime configuration without a supplied runtime_config" - ) - - if block_hash: - params = [block_hash] - response = await self.rpc_request("state_getMetadata", params) - - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - if response.get("result") and decode: - metadata_decoder = self.runtime_config.create_scale_object( - "MetadataVersioned", data=ScaleBytes(response.get("result")) - ) - metadata_decoder.decode() - - return metadata_decoder - - return response - - async def _preprocess( - self, - query_for: Optional[list], - block_hash: Optional[str], - storage_function: str, - module: str, - ) -> Preprocessed: - """ - Creates a Preprocessed data object for passing to `_make_rpc_request` - """ - params = query_for if query_for else [] - # Search storage call in metadata - metadata_pallet = self.metadata.get_metadata_pallet(module) - - if not metadata_pallet: - raise SubstrateRequestException(f'Pallet "{module}" not found') - - storage_item = metadata_pallet.get_storage_function(storage_function) - - if not metadata_pallet or not storage_item: - raise SubstrateRequestException( - f'Storage function "{module}.{storage_function}" not found' - ) - - # SCALE type string of value - param_types = storage_item.get_params_type_string() - value_scale_type = storage_item.get_value_type_string() - - if len(params) != len(param_types): - raise ValueError( - f"Storage function requires {len(param_types)} parameters, {len(params)} given" - ) - - storage_key = StorageKey.create_from_storage_function( - module, - storage_item.value["name"], - params, - runtime_config=self.runtime_config, - metadata=self.metadata, - ) - method = "state_getStorageAt" - return Preprocessed( - str(query_for), - method, - [storage_key.to_hex(), block_hash], - value_scale_type, - storage_item, - ) - - async def _process_response( - self, - response: dict, - subscription_id: Union[int, str], - value_scale_type: Optional[str] = None, - storage_item: Optional[ScaleType] = None, - runtime: Optional[Runtime] = None, - result_handler: Optional[ResultHandler] = None, - ) -> tuple[Union[ScaleType, dict], bool]: - """ - Processes the RPC call response by decoding it, returning it as is, or setting a handler for subscriptions, - depending on the specific call. - - :param response: the RPC call response - :param subscription_id: the subscription id for subscriptions, used only for subscriptions with a result handler - :param value_scale_type: Scale Type string used for decoding ScaleBytes results - :param storage_item: The ScaleType object used for decoding ScaleBytes results - :param runtime: the runtime object, used for decoding ScaleBytes results - :param result_handler: the result handler coroutine used for handling longer-running subscriptions - - :return: (decoded response, completion) - """ - result: Union[dict, ScaleType] = response - if value_scale_type and isinstance(storage_item, ScaleType): - if not runtime: - async with self._lock: - runtime = Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - if response.get("result") is not None: - query_value = response.get("result") - elif storage_item.value["modifier"] == "Default": - # Fallback to default value of storage function if no result - query_value = storage_item.value_object["default"].value_object - else: - # No result is interpreted as an Option<...> result - value_scale_type = f"Option<{value_scale_type}>" - query_value = storage_item.value_object["default"].value_object - if isinstance(query_value, str): - q = bytes.fromhex(query_value[2:]) - elif isinstance(query_value, bytearray): - q = bytes(query_value) - else: - q = query_value - obj = await self.decode_scale(value_scale_type, q, True) - result = obj - if asyncio.iscoroutinefunction(result_handler): - # For multipart responses as a result of subscriptions. - message, bool_result = await result_handler(response, subscription_id) - return message, bool_result - return result, True - - async def _make_rpc_request( - self, - payloads: list[dict], - value_scale_type: Optional[str] = None, - storage_item: Optional[ScaleType] = None, - runtime: Optional[Runtime] = None, - result_handler: Optional[ResultHandler] = None, - ) -> RequestManager.RequestResults: - request_manager = RequestManager(payloads) - - subscription_added = False - - async with self.ws as ws: - for item in payloads: - item_id = await ws.send(item["payload"]) - request_manager.add_request(item_id, item["id"]) - - while True: - for item_id in request_manager.response_map.keys(): - if ( - item_id not in request_manager.responses - or asyncio.iscoroutinefunction(result_handler) - ): - if response := await ws.retrieve(item_id): - if ( - asyncio.iscoroutinefunction(result_handler) - and not subscription_added - ): - # handles subscriptions, overwrites the previous mapping of {item_id : payload_id} - # with {subscription_id : payload_id} - try: - item_id = request_manager.overwrite_request( - item_id, response["result"] - ) - except KeyError: - raise SubstrateRequestException(str(response)) - decoded_response, complete = await self._process_response( - response, - item_id, - value_scale_type, - storage_item, - runtime, - result_handler, - ) - request_manager.add_response( - item_id, decoded_response, complete - ) - if ( - asyncio.iscoroutinefunction(result_handler) - and not subscription_added - ): - subscription_added = True - break - - if request_manager.is_complete: - break - - return request_manager.get_results() - - @staticmethod - def make_payload(id_: str, method: str, params: list) -> dict: - """ - Creates a payload for making an rpc_request with _make_rpc_request - - :param id_: a unique name you would like to give to this request - :param method: the method in the RPC request - :param params: the params in the RPC request - - :return: the payload dict - """ - return { - "id": id_, - "payload": {"jsonrpc": "2.0", "method": method, "params": params}, - } - - async def rpc_request( - self, - method: str, - params: Optional[list], - block_hash: Optional[str] = None, - reuse_block_hash: bool = False, - ) -> Any: - """ - Makes an RPC request to the subtensor. Use this only if ``self.query`` and ``self.query_multiple`` and - ``self.query_map`` do not meet your needs. - - :param method: str the method in the RPC request - :param params: list of the params in the RPC request - :param block_hash: optional str, the hash of the block — only supply this if not supplying the block - hash in the params, and not reusing the block hash - :param reuse_block_hash: optional bool, whether to reuse the block hash in the params — only mark as True - if not supplying the block hash in the params, or via the `block_hash` parameter - - :return: the response from the RPC request - """ - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) - params = params or [] - payload_id = f"{method}{random.randint(0, 7000)}" - payloads = [ - self.make_payload( - payload_id, - method, - params + [block_hash] if block_hash else params, - ) - ] - runtime = Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - result = await self._make_rpc_request(payloads, runtime=runtime) - if "error" in result[payload_id][0]: - raise SubstrateRequestException(result[payload_id][0]["error"]["message"]) - if "result" in result[payload_id][0]: - return result[payload_id][0] - else: - raise SubstrateRequestException(result[payload_id][0]) - - async def get_block_hash(self, block_id: int) -> str: - return (await self.rpc_request("chain_getBlockHash", [block_id]))["result"] - - async def get_chain_head(self) -> str: - result = await self._make_rpc_request( - [ - self.make_payload( - "rpc_request", - "chain_getHead", - [], - ) - ], - runtime=Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ), - ) - self.last_block_hash = result["rpc_request"][0]["result"] - return result["rpc_request"][0]["result"] - - async def compose_call( - self, - call_module: str, - call_function: str, - call_params: Optional[dict] = None, - block_hash: Optional[str] = None, - ) -> GenericCall: - """ - Composes a call payload which can be used in an extrinsic. - - :param call_module: Name of the runtime module e.g. Balances - :param call_function: Name of the call function e.g. transfer - :param call_params: This is a dict containing the params of the call. e.g. - `{'dest': 'EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk', 'value': 1000000000000}` - :param block_hash: Use metadata at given block_hash to compose call - - :return: A composed call - """ - if call_params is None: - call_params = {} - - await self.init_runtime(block_hash=block_hash) - - call = self.runtime_config.create_scale_object( - type_string="Call", metadata=self.metadata - ) - - call.encode( - { - "call_module": call_module, - "call_function": call_function, - "call_args": call_params, - } - ) - - return call - - async def query_multiple( - self, - params: list, - storage_function: str, - module: str, - block_hash: Optional[str] = None, - reuse_block_hash: bool = False, - ) -> dict[str, ScaleType]: - """ - Queries the subtensor. Only use this when making multiple queries, else use ``self.query`` - """ - # By allowing for specifying the block hash, users, if they have multiple query types they want - # to do, can simply query the block hash first, and then pass multiple query_subtensor calls - # into an asyncio.gather, with the specified block hash - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) - if block_hash: - self.last_block_hash = block_hash - runtime = await self.init_runtime(block_hash=block_hash) - preprocessed: tuple[Preprocessed] = await asyncio.gather( - *[ - self._preprocess([x], block_hash, storage_function, module) - for x in params - ] - ) - all_info = [ - self.make_payload(item.queryable, item.method, item.params) - for item in preprocessed - ] - # These will always be the same throughout the preprocessed list, so we just grab the first one - value_scale_type = preprocessed[0].value_scale_type - storage_item = preprocessed[0].storage_item - - responses = await self._make_rpc_request( - all_info, value_scale_type, storage_item, runtime - ) - return { - param: responses[p.queryable][0] for (param, p) in zip(params, preprocessed) - } - - async def query_multi( - self, storage_keys: list[StorageKey], block_hash: Optional[str] = None - ) -> list: - """ - Query multiple storage keys in one request. - - Example: - - ``` - storage_keys = [ - substrate.create_storage_key( - "System", "Account", ["F4xQKRUagnSGjFqafyhajLs94e7Vvzvr8ebwYJceKpr8R7T"] - ), - substrate.create_storage_key( - "System", "Account", ["GSEX8kR4Kz5UZGhvRUCJG93D5hhTAoVZ5tAe6Zne7V42DSi"] - ) - ] - - result = substrate.query_multi(storage_keys) - ``` - - Parameters - ---------- - storage_keys: list of StorageKey objects - block_hash: Optional block_hash of state snapshot - - Returns - ------- - list of `(storage_key, scale_obj)` tuples - """ - - await self.init_runtime(block_hash=block_hash) - - # Retrieve corresponding value - response = await self.rpc_request( - "state_queryStorageAt", [[s.to_hex() for s in storage_keys], block_hash] - ) - - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - result = [] - - storage_key_map = {s.to_hex(): s for s in storage_keys} - - for result_group in response["result"]: - for change_storage_key, change_data in result_group["changes"]: - # Decode result for specified storage_key - storage_key = storage_key_map[change_storage_key] - if change_data is None: - change_data = b"\x00" - else: - change_data = bytes.fromhex(change_data[2:]) - result.append( - ( - storage_key, - await self.decode_scale( - storage_key.value_scale_type, change_data - ), - ) - ) - - return result - - async def create_scale_object( - self, - type_string: str, - data: Optional[ScaleBytes] = None, - block_hash: Optional[str] = None, - **kwargs, - ) -> "ScaleType": - """ - Convenience method to create a SCALE object of type `type_string`, this will initialize the runtime - automatically at moment of `block_hash`, or chain tip if omitted. - - :param type_string: str Name of SCALE type to create - :param data: ScaleBytes Optional ScaleBytes to decode - :param block_hash: Optional block hash for moment of decoding, when omitted the chain tip will be used - :param kwargs: keyword args for the Scale Type constructor - - :return: The created Scale Type object - """ - runtime = await self.init_runtime(block_hash=block_hash) - if "metadata" not in kwargs: - kwargs["metadata"] = runtime.metadata - - return runtime.runtime_config.create_scale_object( - type_string, data=data, **kwargs - ) - - async def generate_signature_payload( - self, - call: GenericCall, - era=None, - nonce: int = 0, - tip: int = 0, - tip_asset_id: Optional[int] = None, - include_call_length: bool = False, - ) -> ScaleBytes: - # Retrieve genesis hash - genesis_hash = await self.get_block_hash(0) - - if not era: - era = "00" - - if era == "00": - # Immortal extrinsic - block_hash = genesis_hash - else: - # Determine mortality of extrinsic - era_obj = self.runtime_config.create_scale_object("Era") - - if isinstance(era, dict) and "current" not in era and "phase" not in era: - raise ValueError( - 'The era dict must contain either "current" or "phase" element to encode a valid era' - ) - - era_obj.encode(era) - block_hash = await self.get_block_hash( - block_id=era_obj.birth(era.get("current")) - ) - - # Create signature payload - signature_payload = self.runtime_config.create_scale_object( - "ExtrinsicPayloadValue" - ) - - # Process signed extensions in metadata - if "signed_extensions" in self.metadata[1][1]["extrinsic"]: - # Base signature payload - signature_payload.type_mapping = [["call", "CallBytes"]] - - # Add signed extensions to payload - signed_extensions = self.metadata.get_signed_extensions() - - if "CheckMortality" in signed_extensions: - signature_payload.type_mapping.append( - ["era", signed_extensions["CheckMortality"]["extrinsic"]] - ) - - if "CheckEra" in signed_extensions: - signature_payload.type_mapping.append( - ["era", signed_extensions["CheckEra"]["extrinsic"]] - ) - - if "CheckNonce" in signed_extensions: - signature_payload.type_mapping.append( - ["nonce", signed_extensions["CheckNonce"]["extrinsic"]] - ) - - if "ChargeTransactionPayment" in signed_extensions: - signature_payload.type_mapping.append( - ["tip", signed_extensions["ChargeTransactionPayment"]["extrinsic"]] - ) - - if "ChargeAssetTxPayment" in signed_extensions: - signature_payload.type_mapping.append( - ["asset_id", signed_extensions["ChargeAssetTxPayment"]["extrinsic"]] - ) - - if "CheckMetadataHash" in signed_extensions: - signature_payload.type_mapping.append( - ["mode", signed_extensions["CheckMetadataHash"]["extrinsic"]] - ) - - if "CheckSpecVersion" in signed_extensions: - signature_payload.type_mapping.append( - [ - "spec_version", - signed_extensions["CheckSpecVersion"]["additional_signed"], - ] - ) - - if "CheckTxVersion" in signed_extensions: - signature_payload.type_mapping.append( - [ - "transaction_version", - signed_extensions["CheckTxVersion"]["additional_signed"], - ] - ) - - if "CheckGenesis" in signed_extensions: - signature_payload.type_mapping.append( - [ - "genesis_hash", - signed_extensions["CheckGenesis"]["additional_signed"], - ] - ) - - if "CheckMortality" in signed_extensions: - signature_payload.type_mapping.append( - [ - "block_hash", - signed_extensions["CheckMortality"]["additional_signed"], - ] - ) - - if "CheckEra" in signed_extensions: - signature_payload.type_mapping.append( - ["block_hash", signed_extensions["CheckEra"]["additional_signed"]] - ) - - if "CheckMetadataHash" in signed_extensions: - signature_payload.type_mapping.append( - [ - "metadata_hash", - signed_extensions["CheckMetadataHash"]["additional_signed"], - ] - ) - - if include_call_length: - length_obj = self.runtime_config.create_scale_object("Bytes") - call_data = str(length_obj.encode(str(call.data))) - - else: - call_data = str(call.data) - - payload_dict = { - "call": call_data, - "era": era, - "nonce": nonce, - "tip": tip, - "spec_version": self.runtime_version, - "genesis_hash": genesis_hash, - "block_hash": block_hash, - "transaction_version": self.transaction_version, - "asset_id": {"tip": tip, "asset_id": tip_asset_id}, - "metadata_hash": None, - "mode": "Disabled", - } - - signature_payload.encode(payload_dict) - - if signature_payload.data.length > 256: - return ScaleBytes( - data=blake2b(signature_payload.data.data, digest_size=32).digest() - ) - - return signature_payload.data - - async def create_signed_extrinsic( - self, - call: GenericCall, - keypair: Keypair, - era: Optional[dict] = None, - nonce: Optional[int] = None, - tip: int = 0, - tip_asset_id: Optional[int] = None, - signature: Optional[Union[bytes, str]] = None, - ) -> "GenericExtrinsic": - """ - Creates an extrinsic signed by given account details - - :param call: GenericCall to create extrinsic for - :param keypair: Keypair used to sign the extrinsic - :param era: Specify mortality in blocks in follow format: - {'period': [amount_blocks]} If omitted the extrinsic is immortal - :param nonce: nonce to include in extrinsics, if omitted the current nonce is retrieved on-chain - :param tip: The tip for the block author to gain priority during network congestion - :param tip_asset_id: Optional asset ID with which to pay the tip - :param signature: Optionally provide signature if externally signed - - :return: The signed Extrinsic - """ - await self.init_runtime() - - # Check requirements - if not isinstance(call, GenericCall): - raise TypeError("'call' must be of type Call") - - # Check if extrinsic version is supported - if self.metadata[1][1]["extrinsic"]["version"] != 4: # type: ignore - raise NotImplementedError( - f"Extrinsic version {self.metadata[1][1]['extrinsic']['version']} not supported" # type: ignore - ) - - # Retrieve nonce - if nonce is None: - nonce = await self.get_account_nonce(keypair.ss58_address) or 0 - - # Process era - if era is None: - era = "00" - else: - if isinstance(era, dict) and "current" not in era and "phase" not in era: - # Retrieve current block id - era["current"] = await self.get_block_number( - await self.get_chain_finalised_head() - ) - - if signature is not None: - if isinstance(signature, str) and signature[0:2] == "0x": - signature = bytes.fromhex(signature[2:]) - - # Check if signature is a MultiSignature and contains signature version - if len(signature) == 65: - signature_version = signature[0] - signature = signature[1:] - else: - signature_version = keypair.crypto_type - - else: - # Create signature payload - signature_payload = await self.generate_signature_payload( - call=call, era=era, nonce=nonce, tip=tip, tip_asset_id=tip_asset_id - ) - - # Set Signature version to crypto type of keypair - signature_version = keypair.crypto_type - - # Sign payload - signature = keypair.sign(signature_payload) - - # Create extrinsic - extrinsic = self.runtime_config.create_scale_object( - type_string="Extrinsic", metadata=self.metadata - ) - - value = { - "account_id": f"0x{keypair.public_key.hex()}", - "signature": f"0x{signature.hex()}", - "call_function": call.value["call_function"], - "call_module": call.value["call_module"], - "call_args": call.value["call_args"], - "nonce": nonce, - "era": era, - "tip": tip, - "asset_id": {"tip": tip, "asset_id": tip_asset_id}, - "mode": "Disabled", - } - - # Check if ExtrinsicSignature is MultiSignature, otherwise omit signature_version - signature_cls = self.runtime_config.get_decoder_class("ExtrinsicSignature") - if issubclass(signature_cls, self.runtime_config.get_decoder_class("Enum")): - value["signature_version"] = signature_version - - extrinsic.encode(value) - - return extrinsic - - async def get_chain_finalised_head(self): - """ - A pass-though to existing JSONRPC method `chain_getFinalizedHead` - - Returns - ------- - - """ - response = await self.rpc_request("chain_getFinalizedHead", []) - - if response is not None: - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - return response.get("result") - - async def runtime_call( - self, - api: str, - method: str, - params: Optional[Union[list, dict]] = None, - block_hash: Optional[str] = None, - ) -> ScaleType: - """ - Calls a runtime API method - - :param api: Name of the runtime API e.g. 'TransactionPaymentApi' - :param method: Name of the method e.g. 'query_fee_details' - :param params: List of parameters needed to call the runtime API - :param block_hash: Hash of the block at which to make the runtime API call - - :return: ScaleType from the runtime call - """ - await self.init_runtime() - - if params is None: - params = {} - - try: - runtime_call_def = self.runtime_config.type_registry["runtime_api"][api][ - "methods" - ][method] - runtime_api_types = self.runtime_config.type_registry["runtime_api"][ - api - ].get("types", {}) - except KeyError: - raise ValueError(f"Runtime API Call '{api}.{method}' not found in registry") - - if isinstance(params, list) and len(params) != len(runtime_call_def["params"]): - raise ValueError( - f"Number of parameter provided ({len(params)}) does not " - f"match definition {len(runtime_call_def['params'])}" - ) - - # Add runtime API types to registry - self.runtime_config.update_type_registry_types(runtime_api_types) - runtime = Runtime( - self.chain, - self.runtime_config, - self.metadata, - self.type_registry, - ) - - # Encode params - param_data = ScaleBytes(bytes()) - for idx, param in enumerate(runtime_call_def["params"]): - scale_obj = runtime.runtime_config.create_scale_object(param["type"]) - if isinstance(params, list): - param_data += scale_obj.encode(params[idx]) - else: - if param["name"] not in params: - raise ValueError(f"Runtime Call param '{param['name']}' is missing") - - param_data += scale_obj.encode(params[param["name"]]) - - # RPC request - result_data = await self.rpc_request( - "state_call", [f"{api}_{method}", str(param_data), block_hash] - ) - - # Decode result - # TODO update this to use bt-decode - result_obj = runtime.runtime_config.create_scale_object( - runtime_call_def["type"] - ) - result_obj.decode( - ScaleBytes(result_data["result"]), - check_remaining=self.config.get("strict_scale_decode"), - ) - - return result_obj - - async def get_account_nonce(self, account_address: str) -> int: - """ - Returns current nonce for given account address - - :param account_address: SS58 formatted address - - :return: Nonce for given account address - """ - nonce_obj = await self.runtime_call( - "AccountNonceApi", "account_nonce", [account_address] - ) - return nonce_obj.value - - async def get_metadata_constant(self, module_name, constant_name, block_hash=None): - """ - Retrieves the details of a constant for given module name, call function name and block_hash - (or chaintip if block_hash is omitted) - - Parameters - ---------- - module_name - constant_name - block_hash - - Returns - ------- - MetadataModuleConstants - """ - - await self.init_runtime(block_hash=block_hash) - - for module in self.metadata.pallets: - if module_name == module.name and module.constants: - for constant in module.constants: - if constant_name == constant.value["name"]: - return constant - - async def get_constant( - self, - module_name: str, - constant_name: str, - block_hash: Optional[str] = None, - reuse_block_hash: bool = False, - ) -> Optional["ScaleType"]: - """ - Returns the decoded `ScaleType` object of the constant for given module name, call function name and block_hash - (or chaintip if block_hash is omitted) - - Parameters - ---------- - :param module_name: Name of the module to query - :param constant_name: Name of the constant to query - :param block_hash: Hash of the block at which to make the runtime API call - :param reuse_block_hash: Reuse last-used block hash if set to true - - :return: ScaleType from the runtime call - """ - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) - constant = await self.get_metadata_constant( - module_name, constant_name, block_hash=block_hash - ) - if constant: - # Decode to ScaleType - return await self.decode_scale( - constant.type, - bytes(constant.constant_value), - return_scale_obj=True, - ) - else: - return None - - async def get_payment_info( - self, call: GenericCall, keypair: Keypair - ) -> dict[str, Any]: - """ - Retrieves fee estimation via RPC for given extrinsic - - Parameters - ---------- - call: Call object to estimate fees for - keypair: Keypair of the sender, does not have to include private key because no valid signature is required - - Returns - ------- - Dict with payment info - - E.g. `{'class': 'normal', 'partialFee': 151000000, 'weight': {'ref_time': 143322000}}` - - """ - - # Check requirements - if not isinstance(call, GenericCall): - raise TypeError("'call' must be of type Call") - - if not isinstance(keypair, Keypair): - raise TypeError("'keypair' must be of type Keypair") - - # No valid signature is required for fee estimation - signature = "0x" + "00" * 64 - - # Create extrinsic - extrinsic = await self.create_signed_extrinsic( - call=call, keypair=keypair, signature=signature - ) - extrinsic_len = self.runtime_config.create_scale_object("u32") - extrinsic_len.encode(len(extrinsic.data)) - - result = await self.runtime_call( - "TransactionPaymentApi", "query_info", [extrinsic, extrinsic_len] - ) - - return result.value - - async def query( - self, - module: str, - storage_function: str, - params: Optional[list] = None, - block_hash: Optional[str] = None, - raw_storage_key: Optional[bytes] = None, - subscription_handler=None, - reuse_block_hash: bool = False, - ) -> "ScaleType": - """ - Queries subtensor. This should only be used when making a single request. For multiple requests, - you should use ``self.query_multiple`` - """ - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) - if block_hash: - self.last_block_hash = block_hash - runtime = await self.init_runtime(block_hash=block_hash) - preprocessed: Preprocessed = await self._preprocess( - params, block_hash, storage_function, module - ) - payload = [ - self.make_payload( - preprocessed.queryable, preprocessed.method, preprocessed.params - ) - ] - value_scale_type = preprocessed.value_scale_type - storage_item = preprocessed.storage_item - - responses = await self._make_rpc_request( - payload, - value_scale_type, - storage_item, - runtime, - result_handler=subscription_handler, - ) - return responses[preprocessed.queryable][0] - - async def query_map( - self, - module: str, - storage_function: str, - params: Optional[list] = None, - block_hash: Optional[str] = None, - max_results: Optional[int] = None, - start_key: Optional[str] = None, - page_size: int = 100, - ignore_decoding_errors: bool = False, - reuse_block_hash: bool = False, - ) -> "QueryMapResult": - """ - Iterates over all key-pairs located at the given module and storage_function. The storage - item must be a map. - - Example: - - ``` - result = await substrate.query_map('System', 'Account', max_results=100) - - async for account, account_info in result: - print(f"Free balance of account '{account.value}': {account_info.value['data']['free']}") - ``` - - Note: it is important that you do not use `for x in result.records`, as this will sidestep possible - pagination. You must do `async for x in result`. - - :param module: The module name in the metadata, e.g. System or Balances. - :param storage_function: The storage function name, e.g. Account or Locks. - :param params: The input parameters in case of for example a `DoubleMap` storage function - :param block_hash: Optional block hash for result at given block, when left to None the chain tip will be used. - :param max_results: the maximum of results required, if set the query will stop fetching results when number is - reached - :param start_key: The storage key used as offset for the results, for pagination purposes - :param page_size: The results are fetched from the node RPC in chunks of this size - :param ignore_decoding_errors: When set this will catch all decoding errors, set the item to None and continue - decoding - :param reuse_block_hash: use True if you wish to make the query using the last-used block hash. Do not mark True - if supplying a block_hash - - :return: QueryMapResult object - """ - params = params or [] - block_hash = await self._get_current_block_hash(block_hash, reuse_block_hash) - if block_hash: - self.last_block_hash = block_hash - runtime = await self.init_runtime(block_hash=block_hash) - - metadata_pallet = runtime.metadata.get_metadata_pallet(module) - if not metadata_pallet: - raise ValueError(f'Pallet "{module}" not found') - storage_item = metadata_pallet.get_storage_function(storage_function) - - if not metadata_pallet or not storage_item: - raise ValueError( - f'Storage function "{module}.{storage_function}" not found' - ) - - value_type = storage_item.get_value_type_string() - param_types = storage_item.get_params_type_string() - key_hashers = storage_item.get_param_hashers() - - # Check MapType conditions - if len(param_types) == 0: - raise ValueError("Given storage function is not a map") - if len(params) > len(param_types) - 1: - raise ValueError( - f"Storage function map can accept max {len(param_types) - 1} parameters, {len(params)} given" - ) - - # Generate storage key prefix - storage_key = StorageKey.create_from_storage_function( - module, - storage_item.value["name"], - params, - runtime_config=runtime.runtime_config, - metadata=runtime.metadata, - ) - prefix = storage_key.to_hex() - - if not start_key: - start_key = prefix - - # Make sure if the max result is smaller than the page size, adjust the page size - if max_results is not None and max_results < page_size: - page_size = max_results - - # Retrieve storage keys - response = await self.rpc_request( - method="state_getKeysPaged", - params=[prefix, page_size, start_key, block_hash], - ) - - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - result_keys = response.get("result") - - result = [] - last_key = None - - def concat_hash_len(key_hasher: str) -> int: - """ - Helper function to avoid if statements - """ - if key_hasher == "Blake2_128Concat": - return 16 - elif key_hasher == "Twox64Concat": - return 8 - elif key_hasher == "Identity": - return 0 - else: - raise ValueError("Unsupported hash type") - - if len(result_keys) > 0: - last_key = result_keys[-1] - - # Retrieve corresponding value - response = await self.rpc_request( - method="state_queryStorageAt", params=[result_keys, block_hash] - ) - - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - for result_group in response["result"]: - for item in result_group["changes"]: - try: - # Determine type string - key_type_string = [] - for n in range(len(params), len(param_types)): - key_type_string.append( - f"[u8; {concat_hash_len(key_hashers[n])}]" - ) - key_type_string.append(param_types[n]) - - item_key_obj = await self.decode_scale( - type_string=f"({', '.join(key_type_string)})", - scale_bytes=bytes.fromhex(item[0][len(prefix) :]), - return_scale_obj=True, - ) - - # strip key_hashers to use as item key - if len(param_types) - len(params) == 1: - item_key = item_key_obj[1] - else: - item_key = tuple( - item_key_obj[key + 1] - for key in range(len(params), len(param_types) + 1, 2) - ) - - except Exception as _: - if not ignore_decoding_errors: - raise - item_key = None - - try: - try: - item_bytes = bytes.fromhex(item[1][2:]) - except ValueError: - item_bytes = bytes.fromhex(item[1]) - - item_value = await self.decode_scale( - type_string=value_type, - scale_bytes=item_bytes, - return_scale_obj=True, - ) - except Exception as _: - if not ignore_decoding_errors: - raise - item_value = None - - result.append([item_key, item_value]) - - return QueryMapResult( - records=result, - page_size=page_size, - module=module, - storage_function=storage_function, - params=params, - block_hash=block_hash, - substrate=self, - last_key=last_key, - max_results=max_results, - ignore_decoding_errors=ignore_decoding_errors, - ) - - async def submit_extrinsic( - self, - extrinsic: GenericExtrinsic, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = False, - ) -> "ExtrinsicReceipt": - """ - Submit an extrinsic to the connected node, with the possibility to wait until the extrinsic is included - in a block and/or the block is finalized. The receipt returned provided information about the block and - triggered events - - Parameters - ---------- - extrinsic: Extrinsic The extrinsic to be sent to the network - wait_for_inclusion: wait until extrinsic is included in a block (only works for websocket connections) - wait_for_finalization: wait until extrinsic is finalized (only works for websocket connections) - - Returns - ------- - ExtrinsicReceipt - - """ - - # Check requirements - if not isinstance(extrinsic, GenericExtrinsic): - raise TypeError("'extrinsic' must be of type Extrinsics") - - async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: - """ - Result handler function passed as an arg to _make_rpc_request as the result_handler - to handle the results of the extrinsic rpc call, which are multipart, and require - subscribing to the message - - :param message: message received from the rpc call - :param subscription_id: subscription id received from the initial rpc call for the subscription - - :returns: tuple containing the dict of the block info for the subscription, and bool for whether - the subscription is completed. - """ - # Check if extrinsic is included and finalized - if "params" in message and isinstance(message["params"]["result"], dict): - # Convert result enum to lower for backwards compatibility - message_result = { - k.lower(): v for k, v in message["params"]["result"].items() - } - - if "finalized" in message_result and wait_for_finalization: - # Created as a task because we don't actually care about the result - self._forgettable_task = asyncio.create_task( - self.rpc_request("author_unwatchExtrinsic", [subscription_id]) - ) - return { - "block_hash": message_result["finalized"], - "extrinsic_hash": "0x{}".format(extrinsic.extrinsic_hash.hex()), - "finalized": True, - }, True - elif ( - "inblock" in message_result - and wait_for_inclusion - and not wait_for_finalization - ): - # Created as a task because we don't actually care about the result - self._forgettable_task = asyncio.create_task( - self.rpc_request("author_unwatchExtrinsic", [subscription_id]) - ) - return { - "block_hash": message_result["inblock"], - "extrinsic_hash": "0x{}".format(extrinsic.extrinsic_hash.hex()), - "finalized": False, - }, True - return message, False - - if wait_for_inclusion or wait_for_finalization: - responses = ( - await self._make_rpc_request( - [ - self.make_payload( - "rpc_request", - "author_submitAndWatchExtrinsic", - [str(extrinsic.data)], - ) - ], - result_handler=result_handler, - ) - )["rpc_request"] - response = next( - (r for r in responses if "block_hash" in r and "extrinsic_hash" in r), - None, - ) - - if not response: - raise SubstrateRequestException(responses) - - # Also, this will be a multipart response, so maybe should change to everything after the first response? - # The following code implies this will be a single response after the initial subscription id. - result = ExtrinsicReceipt( - substrate=self, - extrinsic_hash=response["extrinsic_hash"], - block_hash=response["block_hash"], - finalized=response["finalized"], - ) - - else: - response = await self.rpc_request( - "author_submitExtrinsic", [str(extrinsic.data)] - ) - - if "result" not in response: - raise SubstrateRequestException(response.get("error")) - - result = ExtrinsicReceipt(substrate=self, extrinsic_hash=response["result"]) - - return result - - async def get_metadata_call_function( - self, - module_name: str, - call_function_name: str, - block_hash: Optional[str] = None, - ) -> Optional[list]: - """ - Retrieves a list of all call functions in metadata active for given block_hash (or chaintip if block_hash - is omitted) - - :param module_name: name of the module - :param call_function_name: name of the call function - :param block_hash: optional block hash - - :return: list of call functions - """ - runtime = await self.init_runtime(block_hash=block_hash) - - for pallet in runtime.metadata.pallets: - if pallet.name == module_name and pallet.calls: - for call in pallet.calls: - if call.name == call_function_name: - return call - return None - - async def get_block_number(self, block_hash: Optional[str]) -> int: - """Async version of `substrateinterface.base.get_block_number` method.""" - response = await self.rpc_request("chain_getHeader", [block_hash]) - - if "error" in response: - raise SubstrateRequestException(response["error"]["message"]) - - elif "result" in response: - if response["result"]: - return int(response["result"]["number"], 16) - - async def close(self): - """ - Closes the substrate connection, and the websocket connection. - """ - try: - await self.ws.shutdown() - except AttributeError: - pass diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index 70656e58c..3a9c2fc15 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.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, TypedDict +from typing import Union from bittensor_cli.src import UNITS @@ -229,12 +229,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) @@ -303,21 +297,10 @@ def set_unit(self, netuid: int): return self -class FixedPoint(TypedDict): - """ - Represents a fixed point ``U64F64`` number. - Where ``bits`` is a U128 representation of the fixed point number. - - This matches the type of the Alpha shares. - """ - - bits: int - - -def fixed_to_float(fixed: FixedPoint) -> float: +def fixed_to_float(fixed: dict) -> float: # Currently this is stored as a U64F64 # which is 64 bits of integer and 64 bits of fractional - uint_bits = 64 + # uint_bits = 64 frac_bits = 64 data: int = fixed["bits"] diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index f846ae0b2..3b5a4c89a 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1,82 +1,32 @@ +from abc import abstractmethod from dataclasses import dataclass from enum import Enum from typing import Optional, Any, Union -import bt_decode import netaddr -from scalecodec import ScaleBytes -from scalecodec.base import RuntimeConfiguration -from scalecodec.type_registry import load_type_registry_preset from scalecodec.utils.ss58 import ss58_encode from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.networking import int_to_ip -from bittensor_cli.src.bittensor.utils import SS58_FORMAT, u16_normalized_float +from bittensor_cli.src.bittensor.utils import ( + SS58_FORMAT, + u16_normalized_float, + decode_account_id, +) class ChainDataType(Enum): NeuronInfo = 1 - SubnetInfoV2 = 2 - DelegateInfo = 3 - NeuronInfoLite = 4 - DelegatedInfo = 5 - StakeInfo = 6 - IPInfo = 7 - SubnetHyperparameters = 8 - SubstakeElements = 9 - DynamicPoolInfoV2 = 10 - DelegateInfoLite = 11 - DynamicInfo = 12 - ScheduledColdkeySwapInfo = 13 - SubnetInfo = 14 - SubnetState = 15 - SubnetIdentity = 16 - - -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( - f"input must be a list[int], bytes, or ScaleBytes, not {type(input_)}" - ) - 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() - - -def from_scale_encoding( - input_: Union[list[int], bytes, ScaleBytes], - type_name: ChainDataType, - is_vec: bool = False, - is_option: bool = False, -) -> Optional[dict]: - 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 decode_account_id(account_id_bytes: tuple): - # Convert the AccountId bytes to a Base64 string - return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) + DelegateInfo = 2 + NeuronInfoLite = 3 + StakeInfo = 4 + SubnetHyperparameters = 5 + DelegateInfoLite = 6 + DynamicInfo = 7 + ScheduledColdkeySwapInfo = 8 + SubnetInfo = 9 + SubnetState = 10 + SubnetIdentity = 11 def decode_hex_identity(info_dictionary): @@ -146,14 +96,39 @@ def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": @dataclass -class SubnetHyperparameters: +class InfoBase: + """Base dataclass for info objects.""" + + @abstractmethod + def _fix_decoded(self, decoded: Any) -> "InfoBase": + raise NotImplementedError( + "This is an abstract method and must be implemented in a subclass." + ) + + @classmethod + def from_any(cls, data: Any) -> "InfoBase": + return cls._fix_decoded(data) + + @classmethod + def list_from_any(cls, data_list: list[Any]) -> list["InfoBase"]: + return [cls.from_any(data) for data in data_list] + + def __getitem__(self, item): + return getattr(self, item) + + def get(self, item, default=None): + return getattr(self, item, default) + + +@dataclass +class SubnetHyperparameters(InfoBase): """Dataclass for subnet hyperparameters.""" rho: int kappa: int immunity_period: int min_allowed_weights: int - max_weight_limit: float + max_weights_limit: float tempo: int min_difficulty: int max_difficulty: int @@ -171,48 +146,49 @@ class SubnetHyperparameters: max_validators: int adjustment_alpha: int difficulty: int - commit_reveal_weights_interval: int + commit_reveal_period: 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"]: - decoded = bt_decode.SubnetHyperparameters.decode(vec_u8) + def _fix_decoded( + cls, decoded: Union[dict, "SubnetHyperparameters"] + ) -> "SubnetHyperparameters": 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, + rho=decoded.get("rho"), + kappa=decoded.get("kappa"), + immunity_period=decoded.get("immunity_period"), + min_allowed_weights=decoded.get("min_allowed_weights"), + max_weights_limit=decoded.get("max_weights_limit"), + tempo=decoded.get("tempo"), + min_difficulty=decoded.get("min_difficulty"), + max_difficulty=decoded.get("max_difficulty"), + weights_version=decoded.get("weights_version"), + weights_rate_limit=decoded.get("weights_rate_limit"), + adjustment_interval=decoded.get("adjustment_interval"), + activity_cutoff=decoded.get("activity_cutoff"), + registration_allowed=decoded.get("registration_allowed"), + target_regs_per_interval=decoded.get("target_regs_per_interval"), + min_burn=decoded.get("min_burn"), + max_burn=decoded.get("max_burn"), + bonds_moving_avg=decoded.get("bonds_moving_avg"), + max_regs_per_block=decoded.get("max_regs_per_block"), + serving_rate_limit=decoded.get("serving_rate_limit"), + max_validators=decoded.get("max_validators"), + adjustment_alpha=decoded.get("adjustment_alpha"), + difficulty=decoded.get("difficulty"), + commit_reveal_period=decoded.get("commit_reveal_period"), + commit_reveal_weights_enabled=decoded.get("commit_reveal_weights_enabled"), + alpha_high=decoded.get("alpha_high"), + alpha_low=decoded.get("alpha_low"), + liquid_alpha_enabled=decoded.get("liquid_alpha_enabled"), ) @dataclass -class StakeInfo: +class StakeInfo(InfoBase): """Dataclass for stake info.""" hotkey_ss58: str # Hotkey address @@ -225,82 +201,23 @@ class StakeInfo: is_registered: bool @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), - netuid=int(decoded["netuid"]), - stake=Balance.from_rao(decoded["stake"]).set_unit(decoded["netuid"]), - locked=Balance.from_rao(decoded["locked"]).set_unit(decoded["netuid"]), - emission=Balance.from_rao(decoded["emission"]).set_unit(decoded["netuid"]), - drain=int(decoded["drain"]), - is_registered=bool(decoded["is_registered"]), - ) + def _fix_decoded(cls, decoded: Any) -> "StakeInfo": + hotkey = decode_account_id(decoded.get("hotkey")) + coldkey = decode_account_id(decoded.get("coldkey")) + netuid = int(decoded.get("netuid")) + stake = Balance.from_rao(decoded.get("stake")).set_unit(netuid) + locked = Balance.from_rao(decoded.get("locked")).set_unit(netuid) + emission = Balance.from_rao(decoded.get("emission")).set_unit(netuid) + drain = int(decoded.get("drain")) + is_registered = bool(decoded.get("is_registered")) - @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( - vec_u8, type_string="Vec<(AccountId, Vec)>" - ) + return StakeInfo( + hotkey, coldkey, netuid, stake, locked, emission, drain, is_registered ) - 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 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"] = int_to_ip(int(prometheus_info_decoded["ip"])) - - return cls(**prometheus_info_decoded) - - -@dataclass -class NeuronInfo: +class NeuronInfo(InfoBase): """Dataclass for neuron metadata.""" hotkey: str @@ -324,7 +241,6 @@ class NeuronInfo: 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 @@ -361,7 +277,6 @@ def get_null_neuron() -> "NeuronInfo": validator_permit=False, weights=[], bonds=[], - prometheus_info=None, axon_info=None, is_null=True, coldkey="000000000000000000000000000000000000000000000000", @@ -371,49 +286,42 @@ def get_null_neuron() -> "NeuronInfo": return neuron @classmethod - def from_vec_u8(cls, vec_u8: bytes) -> "NeuronInfo": - n = bt_decode.NeuronInfo.decode(vec_u8) - stake_dict = process_stake_data(n.stake, n.netuid) + def _fix_decoded(cls, decoded: Any) -> "NeuronInfo": + netuid = decoded.get("netuid") + stake_dict = process_stake_data(decoded.get("stake"), netuid=netuid) 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) + axon_info = decoded.get("axon_info", {}) + coldkey = decode_account_id(decoded.get("coldkey")) + hotkey = decode_account_id(decoded.get("hotkey")) return NeuronInfo( hotkey=hotkey, coldkey=coldkey, - uid=n.uid, - netuid=n.netuid, - active=n.active, + uid=decoded.get("uid"), + netuid=netuid, + active=decoded.get("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, - ), + rank=u16_normalized_float(decoded.get("rank")), + emission=decoded.get("emission") / 1e9, + incentive=u16_normalized_float(decoded.get("incentive")), + consensus=u16_normalized_float(decoded.get("consensus")), + trust=u16_normalized_float(decoded.get("trust")), + validator_trust=u16_normalized_float(decoded.get("validator_trust")), + dividends=u16_normalized_float(decoded.get("dividends")), + last_update=decoded.get("last_update"), + validator_permit=decoded.get("validator_permit"), + weights=[[e[0], e[1]] for e in decoded.get("weights")], + bonds=[[e[0], e[1]] for e in decoded.get("bonds")], + pruning_score=decoded.get("pruning_score"), 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, + version=axon_info.get("version"), + ip=str(netaddr.IPAddress(axon_info.get("ip"))), + port=axon_info.get("port"), + ip_type=axon_info.get("ip_type"), + placeholder1=axon_info.get("placeholder1"), + placeholder2=axon_info.get("placeholder2"), + protocol=axon_info.get("protocol"), hotkey=hotkey, coldkey=coldkey, ), @@ -422,7 +330,7 @@ def from_vec_u8(cls, vec_u8: bytes) -> "NeuronInfo": @dataclass -class NeuronInfoLite: +class NeuronInfoLite(InfoBase): """Dataclass for neuron metadata, but without the weights and bonds.""" hotkey: str @@ -443,7 +351,6 @@ class NeuronInfoLite: dividends: float last_update: int validator_permit: bool - prometheus_info: Optional["PrometheusInfo"] axon_info: AxonInfo pruning_score: int is_null: bool = False @@ -466,7 +373,6 @@ def get_null_neuron() -> "NeuronInfoLite": dividends=0, last_update=0, validator_permit=False, - prometheus_info=None, axon_info=None, is_null=True, coldkey="000000000000000000000000000000000000000000000000", @@ -476,78 +382,63 @@ def get_null_neuron() -> "NeuronInfoLite": 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, item.netuid) - stake = ( - sum(stake_dict.values()) - if stake_dict - else Balance(0).set_unit(item.netuid) - ) - 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 + def _fix_decoded(cls, decoded: Union[dict, "NeuronInfoLite"]) -> "NeuronInfoLite": + active = decoded.get("active") + axon_info = decoded.get("axon_info", {}) + coldkey = decode_account_id(decoded.get("coldkey")) + consensus = decoded.get("consensus") + dividends = decoded.get("dividends") + emission = decoded.get("emission") + hotkey = decode_account_id(decoded.get("hotkey")) + incentive = decoded.get("incentive") + last_update = decoded.get("last_update") + netuid = decoded.get("netuid") + pruning_score = decoded.get("pruning_score") + rank = decoded.get("rank") + stake_dict = process_stake_data(decoded.get("stake"), netuid) + stake = sum(stake_dict.values()) if stake_dict else Balance(0) + trust = decoded.get("trust") + uid = decoded.get("uid") + validator_permit = decoded.get("validator_permit") + validator_trust = decoded.get("validator_trust") + + neuron = cls( + active=active, + axon_info=AxonInfo( + version=axon_info.get("version"), + ip=str(netaddr.IPAddress(axon_info.get("ip"))), + port=axon_info.get("port"), + ip_type=axon_info.get("ip_type"), + placeholder1=axon_info.get("placeholder1"), + placeholder2=axon_info.get("placeholder2"), + protocol=axon_info.get("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, + 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 neuron @dataclass -class DelegateInfo: +class DelegateInfo(InfoBase): """ Dataclass for delegate information. For a lighter version of this class, see :func:`DelegateInfoLite`. @@ -578,80 +469,29 @@ class DelegateInfo: total_daily_return: Balance # Total daily return of the delegate @classmethod - def from_vec_u8(cls, vec_u8: bytes) -> Optional["DelegateInfo"]: - decoded = bt_decode.DelegateInfo.decode(vec_u8) - hotkey = decode_account_id(decoded.delegate_ss58) - owner = decode_account_id(decoded.owner_ss58) + def _fix_decoded(cls, decoded: "DelegateInfo") -> "DelegateInfo": + hotkey = decode_account_id(decoded.get("hotkey_ss58")) + owner = decode_account_id(decoded.get("owner_ss58")) nominators = [ - (decode_account_id(x), Balance.from_rao(y)) for x, y in decoded.nominators + (decode_account_id(x), Balance.from_rao(y)) + for x, y in decoded.get("nominators") ] total_stake = sum((x[1] for x in nominators)) if nominators else Balance(0) - return DelegateInfo( + return cls( hotkey_ss58=hotkey, total_stake=total_stake, nominators=nominators, owner_ss58=owner, - take=u16_normalized_float(decoded.take), - 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), + take=u16_normalized_float(decoded.get("take")), + validator_permits=decoded.get("validator_permits"), + registrations=decoded.get("registrations"), + return_per_1000=Balance.from_rao(decoded.get("return_per_1000")), + total_daily_return=Balance.from_rao(decoded.get("total_daily_return")), ) - @classmethod - def list_from_vec_u8(cls, vec_u8: bytes) -> list["DelegateInfo"]: - decoded = bt_decode.DelegateInfo.decode_vec(vec_u8) - results = [] - for d in decoded: - hotkey = decode_account_id(d.delegate_ss58) - owner = decode_account_id(d.owner_ss58) - nominators = [ - (decode_account_id(x), Balance.from_rao(y)) for x, y in d.nominators - ] - total_stake = sum((x[1] for x in nominators)) if nominators else Balance(0) - results.append( - DelegateInfo( - hotkey_ss58=hotkey, - total_stake=total_stake, - nominators=nominators, - owner_ss58=owner, - take=u16_normalized_float(d.take), - validator_permits=d.validator_permits, - registrations=d.registrations, - return_per_1000=Balance.from_rao(d.return_per_1000), - total_daily_return=Balance.from_rao(d.total_daily_return), - ) - ) - return results - - @classmethod - def delegated_list_from_vec_u8( - cls, vec_u8: bytes - ) -> list[tuple["DelegateInfo", Balance]]: - decoded = bt_decode.DelegateInfo.decode_delegated(vec_u8) - results = [] - for d, b in decoded: - nominators = [ - (decode_account_id(x), Balance.from_rao(y)) for x, y in d.nominators - ] - total_stake = sum((x[1] for x in nominators)) if nominators else Balance(0) - delegate = DelegateInfo( - hotkey_ss58=decode_account_id(d.delegate_ss58), - total_stake=total_stake, - nominators=nominators, - owner_ss58=decode_account_id(d.owner_ss58), - take=u16_normalized_float(d.take), - validator_permits=d.validator_permits, - registrations=d.registrations, - return_per_1000=Balance.from_rao(d.return_per_1000), - total_daily_return=Balance.from_rao(d.total_daily_return), - ) - results.append((delegate, Balance.from_rao(b))) - return results - @dataclass -class DelegateInfoLite: +class DelegateInfoLite(InfoBase): """ Dataclass for light delegate information. @@ -671,9 +511,9 @@ class DelegateInfoLite: owner_stake: Balance # Own stake of the delegate @classmethod - def fix_decoded_values(cls, decoded: Any) -> "DelegateInfoLite": + def _fix_decoded(cls, decoded: Any) -> "DelegateInfoLite": """Fixes the decoded values.""" - decoded_take = decoded["take"] + decoded_take = decoded.get("take") if decoded_take == 65535: fixed_take = None @@ -681,46 +521,17 @@ def fix_decoded_values(cls, decoded: Any) -> "DelegateInfoLite": fixed_take = u16_normalized_float(decoded_take) return cls( - hotkey_ss58=ss58_encode(decoded["delegate_ss58"], SS58_FORMAT), - owner_ss58=ss58_encode(decoded["owner_ss58"], SS58_FORMAT), + hotkey_ss58=ss58_encode(decoded.get("delegate_ss58"), SS58_FORMAT), + owner_ss58=ss58_encode(decoded.get("owner_ss58"), SS58_FORMAT), take=fixed_take, - total_stake=Balance.from_rao(decoded["total_stake"]), - owner_stake=Balance.from_rao(decoded["owner_stake"]), + total_stake=Balance.from_rao(decoded.get("total_stake")), + owner_stake=Balance.from_rao(decoded.get("owner_stake")), previous_total_stake=None, ) - @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DelegateInfoLite"]: - """Returns a DelegateInfoLite object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfoLite) - - if decoded is None: - return None - - decoded = DelegateInfoLite.fix_decoded_values(decoded) - - return decoded - - @classmethod - def list_from_vec_u8(cls, vec_u8: list[int]) -> list["DelegateInfoLite"]: - """Returns a list of DelegateInfoLite objects from a ``vec_u8``.""" - decoded = from_scale_encoding( - vec_u8, ChainDataType.DelegateInfoLite, is_vec=True - ) - - if decoded is None: - return [] - - decoded = [DelegateInfoLite.fix_decoded_values(d) for d in decoded] - - return decoded - @dataclass -class SubnetInfo: +class SubnetInfo(InfoBase): """Dataclass for subnet info.""" netuid: int @@ -730,7 +541,7 @@ class SubnetInfo: immunity_period: int max_allowed_validators: int min_allowed_weights: int - max_weight_limit: float + max_weights_limit: float scaling_law_power: float subnetwork_n: int max_n: int @@ -743,141 +554,59 @@ class SubnetInfo: owner_ss58: str @classmethod - def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: - decoded = bt_decode.SubnetInfo.decode_vec_option(vec_u8) - result = [] - for d in decoded: - result.append( - SubnetInfo( - netuid=d.netuid, - rho=d.rho, - kappa=d.kappa, - difficulty=d.difficulty, - immunity_period=d.immunity_period, - max_allowed_validators=d.max_allowed_validators, - min_allowed_weights=d.min_allowed_weights, - max_weight_limit=d.max_weights_limit, - scaling_law_power=d.scaling_law_power, - subnetwork_n=d.subnetwork_n, - max_n=d.max_allowed_uids, - blocks_since_epoch=d.blocks_since_last_step, - tempo=d.tempo, - modality=d.network_modality, - connection_requirements={ - str(int(netuid)): u16_normalized_float(int(req)) - for (netuid, req) in d.network_connect - }, - emission_value=d.emission_values, - burn=Balance.from_rao(d.burn), - owner_ss58=decode_account_id(d.owner), - ) - ) - return result - - -@dataclass -class SubnetInfoV2: - """Dataclass for subnet info.""" - - netuid: int - owner_ss58: str - max_allowed_validators: int - scaling_law_power: float - subnetwork_n: int - max_n: int - blocks_since_epoch: int - modality: int - emission_value: float - burn: Balance - tao_locked: Balance - hyperparameters: "SubnetHyperparameters" - dynamic_pool: "DynamicPool" - - @classmethod - def from_vec_u8(cls, vec_u8: bytes) -> Optional["SubnetInfoV2"]: - """Returns a SubnetInfoV2 object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - decoded = bt_decode.SubnetInfoV2.decode(vec_u8) # TODO fix values - - if decoded is None: - return None - - return cls.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfoV2"]: - """Returns a list of SubnetInfoV2 objects from a ``vec_u8``.""" - decoded = bt_decode.SubnetInfoV2.decode_vec(vec_u8) # TODO fix values - - if decoded is None: - return [] - - decoded = [cls.fix_decoded_values(d) for d in decoded] - - return decoded - - @classmethod - def fix_decoded_values(cls, decoded: dict) -> "SubnetInfoV2": - """Returns a SubnetInfoV2 object from a decoded SubnetInfoV2 dictionary.""" - # init dynamic pool object - pool_info = decoded["dynamic_pool"] - if pool_info: - pool = DynamicPool( - True, - pool_info["netuid"], - pool_info["alpha_issuance"], - pool_info["alpha_outstanding"], - pool_info["alpha_reserve"], - pool_info["tao_reserve"], - pool_info["k"], - ) - else: - pool = DynamicPool(False, decoded["netuid"], 0, 0, 0, 0, 0) - - return SubnetInfoV2( - netuid=decoded["netuid"], - owner_ss58=ss58_encode(decoded["owner"], SS58_FORMAT), - max_allowed_validators=decoded["max_allowed_validators"], - 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"], - modality=decoded["network_modality"], - emission_value=decoded["emission_values"], - burn=Balance.from_rao(decoded["burn"]), - tao_locked=Balance.from_rao(decoded["tao_locked"]), - hyperparameters=decoded["hyperparameters"], - dynamic_pool=pool, + def _fix_decoded(cls, decoded: "SubnetInfo") -> "SubnetInfo": + return SubnetInfo( + netuid=decoded.get("netuid"), + rho=decoded.get("rho"), + kappa=decoded.get("kappa"), + difficulty=decoded.get("difficulty"), + immunity_period=decoded.get("immunity_period"), + max_allowed_validators=decoded.get("max_allowed_validators"), + min_allowed_weights=decoded.get("min_allowed_weights"), + max_weights_limit=decoded.get("max_weights_limit"), + scaling_law_power=decoded.get("scaling_law_power"), + subnetwork_n=decoded.get("subnetwork_n"), + max_n=decoded.get("max_allowed_uids"), + blocks_since_epoch=decoded.get("blocks_since_last_step"), + tempo=decoded.get("tempo"), + modality=decoded.get("network_modality"), + connection_requirements={ + str(int(netuid)): u16_normalized_float(int(req)) + for (netuid, req) in decoded.get("network_connect") + }, + emission_value=decoded.get("emission_value"), + burn=Balance.from_rao(decoded.get("burn")), + owner_ss58=decode_account_id(decoded.get("owner")), ) @dataclass -class SubnetIdentity: +class SubnetIdentity(InfoBase): """Dataclass for subnet identity information.""" subnet_name: str github_repo: str subnet_contact: str + subnet_url: str + discord: str + description: str + additional: str @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetIdentity"]: - if len(vec_u8) == 0: - return None - - decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetIdentity) - if decoded is None: - return None - + def _fix_decoded(cls, decoded: dict) -> "SubnetIdentity": return SubnetIdentity( subnet_name=bytes(decoded["subnet_name"]).decode(), github_repo=bytes(decoded["github_repo"]).decode(), subnet_contact=bytes(decoded["subnet_contact"]).decode(), + subnet_url=bytes(decoded["subnet_url"]).decode(), + discord=bytes(decoded["discord"]).decode(), + description=bytes(decoded["description"]).decode(), + additional=bytes(decoded["additional"]).decode(), ) @dataclass -class DynamicInfo: +class DynamicInfo(InfoBase): netuid: int owner_hotkey: str owner_coldkey: str @@ -903,55 +632,34 @@ class DynamicInfo: subnet_volume: Balance @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicInfo"]: - if len(vec_u8) == 0: - return None - decoded = from_scale_encoding(vec_u8, ChainDataType.DynamicInfo) - if decoded is None: - return None - return DynamicInfo.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: Union[list[int], bytes]) -> list["DynamicInfo"]: - decoded = from_scale_encoding( - vec_u8, ChainDataType.DynamicInfo, is_vec=True, is_option=True - ) - if decoded is None: - return [] - decoded = [DynamicInfo.fix_decoded_values(d) for d in decoded] - return decoded - - @classmethod - def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": + def _fix_decoded(cls, decoded: Any) -> "DynamicInfo": """Returns a DynamicInfo object from a decoded DynamicInfo dictionary.""" - netuid = int(decoded["netuid"]) - symbol = bytes([int(b) for b in decoded["token_symbol"]]).decode() - subnet_name = bytes([int(b) for b in decoded["subnet_name"]]).decode() - is_dynamic = ( - True if int(decoded["netuid"]) > 0 else False - ) # TODO: Patching this temporarily for netuid 0 + netuid = int(decoded.get("netuid")) + symbol = bytes([int(b) for b in decoded.get("token_symbol")]).decode() + subnet_name = bytes([int(b) for b in decoded.get("subnet_name")]).decode() + is_dynamic = True if netuid > 0 else False # Patching for netuid 0 - owner_hotkey = ss58_encode(decoded["owner_hotkey"], SS58_FORMAT) - owner_coldkey = ss58_encode(decoded["owner_coldkey"], SS58_FORMAT) + owner_hotkey = decode_account_id(decoded.get("owner_hotkey")) + owner_coldkey = decode_account_id(decoded.get("owner_coldkey")) - emission = Balance.from_rao(decoded["emission"]).set_unit(0) - alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) - alpha_out = Balance.from_rao(decoded["alpha_out"]).set_unit(netuid) - tao_in = Balance.from_rao(decoded["tao_in"]).set_unit(0) - subnet_volume = Balance.from_rao(decoded["subnet_volume"]).set_unit(netuid) - alpha_out_emission = Balance.from_rao(decoded["alpha_out_emission"]).set_unit( - netuid - ) - alpha_in_emission = Balance.from_rao(decoded["alpha_in_emission"]).set_unit( + emission = Balance.from_rao(decoded.get("emission")).set_unit(0) + alpha_in = Balance.from_rao(decoded.get("alpha_in")).set_unit(netuid) + alpha_out = Balance.from_rao(decoded.get("alpha_out")).set_unit(netuid) + tao_in = Balance.from_rao(decoded.get("tao_in")).set_unit(0) + alpha_out_emission = Balance.from_rao( + decoded.get("alpha_out_emission") + ).set_unit(netuid) + alpha_in_emission = Balance.from_rao(decoded.get("alpha_in_emission")).set_unit( netuid ) - tao_in_emission = Balance.from_rao(decoded["tao_in_emission"]).set_unit(0) + subnet_volume = Balance.from_rao(decoded.get("subnet_volume")).set_unit(netuid) + tao_in_emission = Balance.from_rao(decoded.get("tao_in_emission")).set_unit(0) pending_alpha_emission = Balance.from_rao( - decoded["pending_alpha_emission"] + decoded.get("pending_alpha_emission") ).set_unit(netuid) pending_root_emission = Balance.from_rao( - decoded["pending_root_emission"] + decoded.get("pending_root_emission") ).set_unit(0) price = ( Balance.from_tao(1.0) @@ -962,11 +670,7 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": ) # TODO: Patching this temporarily for netuid 0 if decoded.get("subnet_identity"): - subnet_identity = SubnetIdentity( - subnet_name=decoded["subnet_identity"]["subnet_name"], - github_repo=decoded["subnet_identity"]["github_repo"], - subnet_contact=decoded["subnet_identity"]["subnet_contact"], - ) + subnet_identity = SubnetIdentity.from_any(decoded.get("subnet_identity")) else: subnet_identity = None @@ -976,9 +680,9 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": owner_coldkey=owner_coldkey, subnet_name=subnet_name, symbol=symbol, - tempo=int(decoded["tempo"]), - last_step=int(decoded["last_step"]), - blocks_since_last_step=int(decoded["blocks_since_last_step"]), + tempo=int(decoded.get("tempo")), + last_step=int(decoded.get("last_step")), + blocks_since_last_step=int(decoded.get("blocks_since_last_step")), emission=emission, alpha_in=alpha_in, alpha_out=alpha_out, @@ -991,7 +695,7 @@ def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": tao_in_emission=tao_in_emission, pending_alpha_emission=pending_alpha_emission, pending_root_emission=pending_root_emission, - network_registered_at=int(decoded["network_registered_at"]), + network_registered_at=int(decoded.get("network_registered_at")), subnet_identity=subnet_identity, subnet_volume=subnet_volume, ) @@ -1081,166 +785,7 @@ def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: @dataclass -class DynamicPoolInfoV2: - """Dataclass for dynamic pool info.""" - - netuid: int - alpha_issuance: int - alpha_outstanding: int - alpha_reserve: int - tao_reserve: int - k: int - - @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicPoolInfoV2"]: - """Returns a DynamicPoolInfoV2 object from a ``vec_u8``.""" - if len(vec_u8) == 0: - return None - return from_scale_encoding(vec_u8, ChainDataType.DynamicPoolInfoV2) - - -@dataclass -class DynamicPool: - is_dynamic: bool - alpha_issuance: Balance - alpha_outstanding: Balance - alpha_reserve: Balance - tao_reserve: Balance - k: int - price: Balance - netuid: int - - def __init__( - self, - is_dynamic: bool, - netuid: int, - alpha_issuance: Union[int, Balance], - alpha_outstanding: Union[int, Balance], - alpha_reserve: Union[int, Balance], - tao_reserve: Union[int, Balance], - k: int, - ): - self.is_dynamic = is_dynamic - self.netuid = netuid - self.alpha_issuance = ( - alpha_issuance - if isinstance(alpha_issuance, Balance) - else Balance.from_rao(alpha_issuance).set_unit(netuid) - ) - self.alpha_outstanding = ( - alpha_outstanding - if isinstance(alpha_outstanding, Balance) - else Balance.from_rao(alpha_outstanding).set_unit(netuid) - ) - self.alpha_reserve = ( - alpha_reserve - if isinstance(alpha_reserve, Balance) - else Balance.from_rao(alpha_reserve).set_unit(netuid) - ) - self.tao_reserve = ( - tao_reserve - if isinstance(tao_reserve, Balance) - else Balance.from_rao(tao_reserve).set_unit(0) - ) - self.k = k - if is_dynamic: - if self.alpha_reserve.tao > 0: - self.price = Balance.from_tao( - self.tao_reserve.tao / self.alpha_reserve.tao - ) - else: - self.price = Balance.from_tao(0.0) - else: - self.price = Balance.from_tao(1.0) - - def __str__(self) -> str: - return ( - f"DynamicPool( alpha_issuance={self.alpha_issuance}, " - f"alpha_outstanding={self.alpha_outstanding}, " - f"alpha_reserve={self.alpha_reserve}, " - f"tao_reserve={self.tao_reserve}, k={self.k}, price={self.price} )" - ) - - def __repr__(self) -> str: - return self.__str__() - - def tao_to_alpha(self, tao: Balance) -> Balance: - if self.price.tao != 0: - return Balance.from_tao(tao.tao / self.price.tao).set_unit(self.netuid) - else: - return Balance.from_tao(0) - - def alpha_to_tao(self, alpha: Balance) -> Balance: - return Balance.from_tao(alpha.tao * self.price.tao) - - def tao_to_alpha_with_slippage(self, tao: Balance) -> tuple[Balance, Balance]: - """ - Returns an estimate of how much Alpha would a staker receive if they stake their tao - using the current pool state - Args: - tao: Amount of TAO to stake. - Returns: - Tuple of balances where the first part is the amount of Alpha received, and the - second part (slippage) is the difference between the estimated amount and ideal - amount as if there was no slippage - """ - if self.is_dynamic: - new_tao_in = self.tao_reserve + tao - if new_tao_in == 0: - return tao, Balance.from_rao(0) - new_alpha_in = self.k / new_tao_in - - # Amount of alpha given to the staker - alpha_returned = Balance.from_rao( - self.alpha_reserve.rao - new_alpha_in.rao - ).set_unit(self.netuid) - - # Ideal conversion as if there is no slippage, just price - alpha_ideal = self.tao_to_alpha(tao) - - if alpha_ideal.tao > alpha_returned.tao: - slippage = Balance.from_tao( - alpha_ideal.tao - alpha_returned.tao - ).set_unit(self.netuid) - else: - slippage = Balance.from_tao(0) - else: - alpha_returned = tao.set_unit(self.netuid) - slippage = Balance.from_tao(0) - return alpha_returned, slippage - - def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: - """ - Returns an estimate of how much TAO would a staker receive if they unstake their - alpha using the current pool state - Args: - alpha: Amount of Alpha to stake. - Returns: - Tuple of balances where the first part is the amount of TAO received, and the - second part (slippage) is the difference between the estimated amount and ideal - amount as if there was no slippage - """ - if self.is_dynamic: - new_alpha_in = self.alpha_reserve + alpha - new_tao_reserve = self.k / new_alpha_in - # Amount of TAO given to the unstaker - tao_returned = Balance.from_rao(self.tao_reserve - new_tao_reserve) - - # Ideal conversion as if there is no slippage, just price - tao_ideal = self.alpha_to_tao(alpha) - - if tao_ideal > tao_returned: - slippage = Balance.from_tao(tao_ideal.tao - tao_returned.tao) - else: - slippage = Balance.from_tao(0) - else: - tao_returned = alpha.set_unit(0) - slippage = Balance.from_tao(0) - return tao_returned, slippage - - -@dataclass -class ScheduledColdkeySwapInfo: +class ScheduledColdkeySwapInfo(InfoBase): """Dataclass for scheduled coldkey swap information.""" old_coldkey: str @@ -1248,50 +793,17 @@ class ScheduledColdkeySwapInfo: arbitration_block: int @classmethod - def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": + def _fix_decoded(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 + old_coldkey=decode_account_id(decoded.get("old_coldkey")), + new_coldkey=decode_account_id(decoded.get("new_coldkey")), + arbitration_block=decoded.get("arbitration_block"), ) - if decoded is None: - return None - return [ss58_encode(account_id, SS58_FORMAT) for account_id in decoded] @dataclass -class SubnetState: +class SubnetState(InfoBase): netuid: int hotkeys: list[str] coldkeys: list[str] @@ -1312,350 +824,38 @@ class SubnetState: emission_history: list[list[int]] @classmethod - def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetState"]: - if len(vec_u8) == 0: - return None - decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetState) - if decoded is None: - return None - return SubnetState.fix_decoded_values(decoded) - - @classmethod - def list_from_vec_u8(cls, vec_u8: list[int]) -> list["SubnetState"]: - decoded = from_scale_encoding( - vec_u8, ChainDataType.SubnetState, is_vec=True, is_option=True - ) - if decoded is None: - return [] - decoded = [SubnetState.fix_decoded_values(d) for d in decoded] - return decoded - - @classmethod - def fix_decoded_values(cls, decoded: dict) -> "SubnetState": - netuid = decoded["netuid"] + def _fix_decoded(cls, decoded: Any) -> "SubnetState": + netuid = decoded.get("netuid") return SubnetState( netuid=netuid, - hotkeys=[ss58_encode(val, SS58_FORMAT) for val in decoded["hotkeys"]], - coldkeys=[ss58_encode(val, SS58_FORMAT) for val in decoded["coldkeys"]], - active=decoded["active"], - validator_permit=decoded["validator_permit"], + hotkeys=[decode_account_id(val) for val in decoded.get("hotkeys")], + coldkeys=[decode_account_id(val) for val in decoded.get("coldkeys")], + active=decoded.get("active"), + validator_permit=decoded.get("validator_permit"), pruning_score=[ - u16_normalized_float(val) for val in decoded["pruning_score"] + u16_normalized_float(val) for val in decoded.get("pruning_score") ], - last_update=decoded["last_update"], + last_update=decoded.get("last_update"), emission=[ - Balance.from_rao(val).set_unit(netuid) for val in decoded["emission"] + Balance.from_rao(val).set_unit(netuid) + for val in decoded.get("emission") ], - dividends=[u16_normalized_float(val) for val in decoded["dividends"]], - incentives=[u16_normalized_float(val) for val in decoded["incentives"]], - consensus=[u16_normalized_float(val) for val in decoded["consensus"]], - trust=[u16_normalized_float(val) for val in decoded["trust"]], - rank=[u16_normalized_float(val) for val in decoded["rank"]], - block_at_registration=decoded["block_at_registration"], + dividends=[u16_normalized_float(val) for val in decoded.get("dividends")], + incentives=[u16_normalized_float(val) for val in decoded.get("incentives")], + consensus=[u16_normalized_float(val) for val in decoded.get("consensus")], + trust=[u16_normalized_float(val) for val in decoded.get("trust")], + rank=[u16_normalized_float(val) for val in decoded.get("rank")], + block_at_registration=decoded.get("block_at_registration"), alpha_stake=[ - Balance.from_rao(val).set_unit(netuid) for val in decoded["alpha_stake"] + Balance.from_rao(val).set_unit(netuid) + for val in decoded.get("alpha_stake") ], tao_stake=[ - Balance.from_rao(val).set_unit(0) for val in decoded["tao_stake"] + Balance.from_rao(val).set_unit(0) for val in decoded.get("tao_stake") ], total_stake=[ - Balance.from_rao(val).set_unit(netuid) for val in decoded["total_stake"] + Balance.from_rao(val).set_unit(netuid) + for val in decoded.get("total_stake") ], - emission_history=decoded["emission_history"], + emission_history=decoded.get("emission_history"), ) - - -class SubstakeElements: - @staticmethod - def decode(result: list[int]) -> list[dict]: - descaled = from_scale_encoding( - input_=result, type_name=ChainDataType.SubstakeElements, is_vec=True - ) - result = [] - for item in descaled: - result.append( - { - "hotkey": ss58_encode(item["hotkey"], SS58_FORMAT), - "coldkey": ss58_encode(item["coldkey"], SS58_FORMAT), - "netuid": item["netuid"], - "stake": Balance.from_rao(item["stake"]), - } - ) - return result - - -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"], - ], - }, - "DynamicPoolInfoV2": { - "type": "struct", - "type_mapping": [ - ["netuid", "u16"], - ["alpha_issuance", "u64"], - ["alpha_outstanding", "u64"], - ["alpha_reserve", "u64"], - ["tao_reserve", "u64"], - ["k", "u128"], - ], - }, - "SubnetInfoV2": { - "type": "struct", - "type_mapping": [ - ["netuid", "u16"], - ["owner", "AccountId"], - ["max_allowed_validators", "u16"], - ["scaling_law_power", "u16"], - ["subnetwork_n", "u16"], - ["max_allowed_uids", "u16"], - ["blocks_since_last_step", "Compact"], - ["network_modality", "u16"], - ["emission_values", "Compact"], - ["burn", "Compact"], - ["tao_locked", "Compact"], - ["hyperparameters", "SubnetHyperparameters"], - ["dynamic_pool", "Option"], - ], - }, - "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"], - ], - }, - "DelegateInfoLite": { - "type": "struct", - "type_mapping": [ - ["delegate_ss58", "AccountId"], - ["owner_ss58", "AccountId"], - ["take", "u16"], - ["owner_stake", "Compact"], - ["total_stake", "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"], - ], - }, - "ScheduledColdkeySwapInfo": { - "type": "struct", - "type_mapping": [ - ["old_coldkey", "AccountId"], - ["new_coldkey", "AccountId"], - ["arbitration_block", "Compact"], - ], - }, - "SubnetState": { - "type": "struct", - "type_mapping": [ - ["netuid", "Compact"], - ["hotkeys", "Vec"], - ["coldkeys", "Vec"], - ["active", "Vec"], - ["validator_permit", "Vec"], - ["pruning_score", "Vec>"], - ["last_update", "Vec>"], - ["emission", "Vec>"], - ["dividends", "Vec>"], - ["incentives", "Vec>"], - ["consensus", "Vec>"], - ["trust", "Vec>"], - ["rank", "Vec>"], - ["block_at_registration", "Vec>"], - ["alpha_stake", "Vec>"], - ["tao_stake", "Vec>"], - ["total_stake", "Vec>"], - ["emission_history", "Vec>>"], - ], - }, - "StakeInfo": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["netuid", "Compact"], - ["stake", "Compact"], - ["locked", "Compact"], - ["emission", "Compact"], - ["drain", "Compact"], - ["is_registered", "bool"], - ], - }, - "DynamicInfo": { - "type": "struct", - "type_mapping": [ - ["netuid", "Compact"], - ["owner_hotkey", "AccountId"], - ["owner_coldkey", "AccountId"], - ["subnet_name", "Vec>"], - ["token_symbol", "Vec>"], - ["tempo", "Compact"], - ["last_step", "Compact"], - ["blocks_since_last_step", "Compact"], - ["emission", "Compact"], - ["alpha_in", "Compact"], - ["alpha_out", "Compact"], - ["tao_in", "Compact"], - ["alpha_out_emission", "Compact"], - ["alpha_in_emission", "Compact"], - ["tao_in_emission", "Compact"], - ["pending_alpha_emission", "Compact"], - ["pending_root_emission", "Compact"], - ["network_registered_at", "Compact"], - ["subnet_volume", "Compact"], - ["subnet_identity", "Option"], - ], - }, - "SubstakeElements": { - "type": "struct", - "type_mapping": [ - ["hotkey", "AccountId"], - ["coldkey", "AccountId"], - ["netuid", "Compact"], - ["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"], - ], - }, - "SubnetIdentity": { - "type": "struct", - "type_mapping": [ - ["subnet_name", "Vec"], - ["github_repo", "Vec"], - ["subnet_contact", "Vec"], - ], - }, - } -} diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index ae5db605d..dc4d00ca1 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -26,7 +26,7 @@ from rich.prompt import Confirm from rich.console import Console from rich.status import Status -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.chain_data import NeuronInfo @@ -438,7 +438,7 @@ async def is_hotkey_registered( subtensor: "SubtensorInterface", netuid: int, hotkey_ss58: str ) -> bool: """Checks to see if the hotkey is registered on a given netuid""" - _result = await subtensor.substrate.query( + _result = await subtensor.query( module="SubtensorModule", storage_function="Uids", params=[netuid, hotkey_ss58], @@ -489,27 +489,18 @@ async def register_extrinsic( """ async def get_neuron_for_pubkey_and_subnet(): - uid = await subtensor.substrate.query( + uid = await subtensor.query( "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] ) if uid is None: return NeuronInfo.get_null_neuron() - hex_bytes_result = await subtensor.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neuron", - params=[netuid, uid], + result = await subtensor.neuron_for_uid( + uid=uid, + netuid=netuid, + block_hash=subtensor.substrate.last_block_hash, ) - - if not (result := hex_bytes_result): - return NeuronInfo.get_null_neuron() - - if result.startswith("0x"): - bytes_result = bytes.fromhex(result[2:]) - else: - bytes_result = bytes.fromhex(result) - - return NeuronInfo.from_vec_u8(bytes_result) + return result print_verbose("Checking subnet status") if not await subtensor.subnet_exists(netuid): @@ -716,7 +707,7 @@ async def burned_register_extrinsic( f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", spinner="aesthetic", ) as status: - my_uid = await subtensor.substrate.query( + my_uid = await subtensor.query( "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] ) @@ -769,14 +760,14 @@ async def burned_register_extrinsic( subtensor.get_netuids_for_hotkey( wallet.hotkey.ss58_address, block_hash=block_hash ), - subtensor.substrate.query( + subtensor.query( "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] ), ) console.print( "Balance:\n" - f" [blue]{old_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance[wallet.coldkey.ss58_address]}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" [blue]{old_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" ) if len(netuids_for_hotkey) > 0: @@ -925,8 +916,8 @@ async def run_faucet_extrinsic( wallet.coldkeypub.ss58_address ) console.print( - f"Balance: [blue]{old_balance[wallet.coldkeypub.ss58_address]}[/blue] :arrow_right:" - f" [green]{new_balance[wallet.coldkeypub.ss58_address]}[/green]" + f"Balance: [blue]{old_balance}[/blue] :arrow_right:" + f" [green]{new_balance}[/green]" ) old_balance = new_balance @@ -1031,7 +1022,7 @@ async def _block_solver( limit = int(math.pow(2, 256)) - 1 # Establish communication queues - ## See the _Solver class for more information on the queues. + # See the _Solver class for more information on the queues. stop_event = Event() stop_event.clear() @@ -1044,7 +1035,7 @@ async def _block_solver( ) if cuda: - ## Create a worker per CUDA device + # Create a worker per CUDA device num_processes = len(dev_id) solvers = [ _CUDASolver( diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index 295ee640b..1c21f06cc 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -27,7 +27,7 @@ from rich.prompt import Confirm from rich.table import Table, Column from scalecodec import ScaleBytes, U16, Vec -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.extrinsics.registration import is_hotkey_registered @@ -342,7 +342,7 @@ async def root_register_extrinsic( # Successful registration, final check for neuron and pubkey else: - uid = await subtensor.substrate.query( + uid = await subtensor.query( module="SubtensorModule", storage_function="Uids", params=[0, wallet.hotkey.ss58_address], @@ -419,7 +419,7 @@ async def _do_set_weights(): else: return False, await response.error_message - my_uid = await subtensor.substrate.query( + my_uid = await subtensor.query( "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] ) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 9d0d5a722..f5a2b3dba 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -3,7 +3,7 @@ from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import NETWORK_EXPLORER_MAP from bittensor_cli.src.bittensor.balances import Balance @@ -61,14 +61,14 @@ async def get_transfer_fee() -> Balance: call=call, keypair=wallet.coldkeypub ) except SubstrateRequestException as e: - payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao + payment_info = {"partial_fee": int(2e7)} # assume 0.02 Tao err_console.print( - f":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n" - f" {format_error_message(e, subtensor.substrate)}[/bold white]\n" - f" Defaulting to default transfer fee: {payment_info['partialFee']}" + f":cross_mark: [red]Failed to get payment info[/red]:\n" + f" [bold white]{format_error_message(e)}[/bold white]\n" + f" Defaulting to default transfer fee: {payment_info['partial_fee']}" ) - return Balance.from_rao(payment_info["partialFee"]) + return Balance.from_rao(payment_info["partial_fee"]) async def do_transfer() -> tuple[bool, str, str]: """ @@ -98,7 +98,11 @@ async def do_transfer() -> tuple[bool, str, str]: block_hash_ = response.block_hash return True, block_hash_, "" else: - return False, "", format_error_message(await response.error_message, subtensor.substrate) + return ( + False, + "", + format_error_message(await response.error_message, subtensor.substrate), + ) # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): @@ -122,13 +126,12 @@ async def do_transfer() -> tuple[bool, str, str]: # check existential deposit and fee print_verbose("Fetching existential and fee", status) block_hash = await subtensor.substrate.get_chain_head() - account_balance_, existential_deposit = await asyncio.gather( + account_balance, existential_deposit = await asyncio.gather( subtensor.get_balance( wallet.coldkeypub.ss58_address, block_hash=block_hash ), subtensor.get_existential_deposit(block_hash=block_hash), ) - account_balance = account_balance_[wallet.coldkeypub.ss58_address] fee = await get_transfer_fee() if not keep_alive: @@ -184,7 +187,7 @@ async def do_transfer() -> tuple[bool, str, str]: ) console.print( f"Balance:\n" - f" [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance[wallet.coldkey.ss58_address]}[/green]" + f" [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" ) return True diff --git a/bittensor_cli/src/bittensor/minigraph.py b/bittensor_cli/src/bittensor/minigraph.py index 3d652d6d0..9e149c1bb 100644 --- a/bittensor_cli/src/bittensor/minigraph.py +++ b/bittensor_cli/src/bittensor/minigraph.py @@ -215,7 +215,7 @@ async def _process_root_weights(self, data, attribute: str) -> NDArray: """ async def get_total_subnets(): - _result = await self.subtensor.substrate.query( + _result = await self.subtensor.query( module="SubtensorModule", storage_function="TotalNetworks", params=[], @@ -224,7 +224,7 @@ async def get_total_subnets(): return _result async def get_subnets(): - _result = await self.subtensor.substrate.query( + _result = await self.subtensor.query( module="SubtensorModule", storage_function="TotalNetworks", ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a5f79776a..8c464062b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,46 +1,36 @@ import asyncio from typing import Optional, Any, Union, TypedDict, Iterable - import aiohttp from bittensor_wallet import Wallet from bittensor_wallet.utils import SS58_FORMAT -import scalecodec from scalecodec import GenericCall -from scalecodec.base import RuntimeConfiguration -from scalecodec.type_registry import load_type_registry_preset -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException import typer -from bittensor_cli.src.bittensor.async_substrate_interface import ( - AsyncSubstrateInterface, - TimeoutException, -) + +from async_substrate_interface.async_substrate import AsyncSubstrateInterface from bittensor_cli.src.bittensor.chain_data import ( DelegateInfo, - custom_rpc_type_registry, StakeInfo, NeuronInfoLite, NeuronInfo, SubnetHyperparameters, decode_account_id, decode_hex_identity, - DelegateInfoLite, DynamicInfo, SubnetState, ) from bittensor_cli.src import DelegatesDetails -from bittensor_cli.src.bittensor.balances import Balance, FixedPoint, fixed_to_float +from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float from bittensor_cli.src import Constants, defaults, TYPE_REGISTRY from bittensor_cli.src.bittensor.utils import ( - ss58_to_vec_u8, format_error_message, console, err_console, decode_hex_identity_dict, validate_chain_endpoint, u16_normalized_float, - u64_normalized_float, ) @@ -64,11 +54,11 @@ def __init__(self, proposal_dict: dict) -> None: self.end = proposal_dict["end"] @staticmethod - def decode_ss58_tuples(l: tuple): + def decode_ss58_tuples(data: tuple): """ Decodes a tuple of ss58 addresses formatted as bytes tuples """ - return [decode_account_id(l[x][0]) for x in range(len(l))] + return [decode_account_id(data[x][0]) for x in range(len(data))] class SubtensorInterface: @@ -101,13 +91,14 @@ def __init__(self, network): f"Network not specified or not valid. Using default chain endpoint: " f"{Constants.network_map[defaults.subtensor.network]}.\n" f"You can set this for commands with the `--network` flag, or by setting this" - f" in the config." + f" in the config. If you're sure you're using the correct URL, ensure it begins" + f" with 'ws://' or 'wss://'" ) self.chain_endpoint = Constants.network_map[defaults.subtensor.network] self.network = defaults.subtensor.network self.substrate = AsyncSubstrateInterface( - chain_endpoint=self.chain_endpoint, + url=self.chain_endpoint, ss58_format=SS58_FORMAT, type_registry=TYPE_REGISTRY, chain_name="Bittensor", @@ -123,7 +114,7 @@ async def __aenter__(self): try: async with self.substrate: return self - except TimeoutException: + except TimeoutError: # TODO verify err_console.print( "\n[red]Error[/red]: Timeout occurred connecting to substrate. " f"Verify your chain and network settings: {self}" @@ -133,25 +124,32 @@ async def __aenter__(self): async def __aexit__(self, exc_type, exc_val, exc_tb): await self.substrate.close() - async def encode_params( + async def query( 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 = await self.substrate.create_scale_object(param["type"]) - if isinstance(params, 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() + module: str, + storage_function: str, + params: Optional[list] = None, + block_hash: Optional[str] = None, + raw_storage_key: Optional[bytes] = None, + subscription_handler=None, + reuse_block_hash: bool = False, + ) -> Any: + """ + Pass-through to substrate.query which automatically returns the .value if it's a ScaleObj + """ + result = await self.substrate.query( + module, + storage_function, + params, + block_hash, + raw_storage_key, + subscription_handler, + reuse_block_hash, + ) + if hasattr(result, "value"): + return result.value + else: + return result async def get_all_subnet_netuids( self, block_hash: Optional[str] = None @@ -160,6 +158,7 @@ async def get_all_subnet_netuids( Retrieves the list of all subnet unique identifiers (netuids) currently present in the Bittensor network. :param block_hash: The hash of the block to retrieve the subnet unique identifiers from. + :return: A list of subnet netuids. This function provides a comprehensive view of the subnets within the Bittensor network, @@ -171,62 +170,11 @@ async def get_all_subnet_netuids( block_hash=block_hash, reuse_block_hash=True, ) - return ( - [] - if result is None or not hasattr(result, "records") - else [netuid async for netuid, exists in result if exists] - ) - - async def is_hotkey_delegate( - self, - hotkey_ss58: str, - block_hash: Optional[str] = None, - reuse_block: Optional[bool] = False, - ) -> 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. - - :param hotkey_ss58: The SS58 address of the neuron's hotkey. - :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used block hash. - - :return: `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. - """ - delegates = await self.get_delegates( - block_hash=block_hash, reuse_block=reuse_block - ) - return hotkey_ss58 in [info.hotkey_ss58 for info in delegates] - - async def get_delegates( - self, block_hash: Optional[str] = None, reuse_block: Optional[bool] = False - ) -> list[DelegateInfo]: - """ - Fetches all delegates on the chain - - :param block_hash: hash of the blockchain block number for the query. - :param reuse_block: whether to reuse the last-used block hash. - - :return: List of DelegateInfo objects, or an empty list if there are no delegates. - """ - hex_bytes_result = await self.query_runtime_api( - runtime_api="DelegateInfoRuntimeApi", - method="get_delegates", - params=[], - block_hash=block_hash, - ) - if hex_bytes_result is not None: - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return DelegateInfo.list_from_vec_u8(bytes_result) - else: - return [] + res = [] + async for netuid, exists in result: + if exists.value: + res.append(netuid) + return res async def get_stake_for_coldkey( self, @@ -247,25 +195,18 @@ async def get_stake_for_coldkey( 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 = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="StakeInfoRuntimeApi", method="get_stake_info_for_coldkey", - params=[encoded_coldkey], + params=[coldkey_ss58], block_hash=block_hash, reuse_block=reuse_block, ) - if hex_bytes_result is None: + if result is None: return [] - - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - stakes = StakeInfo.list_from_vec_u8(bytes_result) + stakes = StakeInfo.list_from_any(result) return [stake for stake in stakes if stake.stake > 0] async def get_stake_for_coldkey_and_hotkey( @@ -278,30 +219,29 @@ async def get_stake_for_coldkey_and_hotkey( """ 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. - netuid (Optional[int]): The subnet ID to filter by. If provided, only returns stake for this specific subnet. - block_hash (Optional[str]): The block hash at which to query the stake information. + :param hotkey_ss58: The SS58 address of the hotkey. + :param coldkey_ss58: The SS58 address of the coldkey. + :param netuid: The subnet ID to filter by. If provided, only returns stake for this specific + subnet. + :param block_hash: The block hash at which to query the stake information. - Returns: - Balance: The stake under the coldkey - hotkey pairing. + :return: Balance: The stake under the coldkey - hotkey pairing. """ - alpha_shares = await self.substrate.query( + alpha_shares = await self.query( module="SubtensorModule", storage_function="Alpha", params=[hotkey_ss58, coldkey_ss58, netuid], block_hash=block_hash, ) - hotkey_alpha = await self.substrate.query( + hotkey_alpha = await self.query( module="SubtensorModule", storage_function="TotalHotkeyAlpha", params=[hotkey_ss58, netuid], block_hash=block_hash, ) - hotkey_shares = await self.substrate.query( + hotkey_shares = await self.query( module="SubtensorModule", storage_function="TotalHotkeyShares", params=[hotkey_ss58, netuid], @@ -325,10 +265,10 @@ async def query_runtime_api( self, runtime_api: str, method: str, - params: Optional[Union[list[list[int]], dict[str, int]]], + params: Optional[Union[list, dict]] = None, block_hash: Optional[str] = None, reuse_block: Optional[bool] = False, - ) -> Optional[str]: + ) -> Optional[Any]: """ 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 @@ -340,45 +280,44 @@ async def query_runtime_api( :param block_hash: The hash of the blockchain block number at which to perform the query. :param reuse_block: Whether to reuse the last-used block hash. - :return: The Scale Bytes encoded result from the runtime API call, or ``None`` if the call fails. + :return: The decoded 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 = TYPE_REGISTRY["runtime_api"][runtime_api]["methods"][method] + if reuse_block: + block_hash = self.substrate.last_block_hash + result = ( + await self.substrate.runtime_call(runtime_api, method, params, block_hash) + ).value - data = ( - "0x" - if params is None - else await self.encode_params( - call_definition=call_definition, params=params - ) - ) - api_method = f"{runtime_api}_{method}" - - json_result = await self.substrate.rpc_request( - method="state_call", - params=[api_method, data, block_hash] if block_hash else [api_method, data], - ) - - 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) + return result - obj = rpc_runtime_config.create_scale_object(return_type, as_scale_bytes) - if obj.data.to_hex() == "0x0400": # RPC returned None result - return None + async def get_balance( + self, + address: str, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """ + Retrieves the balance for a single coldkey address - return obj.decode() + :param address: coldkey address + :param block_hash: the block hash, optional + :param reuse_block: Whether to reuse the last-used block hash when retrieving info. + :return: Balance object representing the address's balance + """ + result = await self.query( + module="System", + storage_function="Account", + params=[address], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + value = result or {"data": {"free": 0}} + return Balance(value["data"]["free"]) - async def get_balance( + async def get_balances( self, *addresses: str, block_hash: Optional[str] = None, @@ -391,6 +330,8 @@ async def get_balance( :param reuse_block: Whether to reuse the last-used block hash when retrieving info. :return: dict of {address: Balance objects} """ + if reuse_block: + block_hash = self.substrate.last_block_hash calls = [ ( await self.substrate.create_storage_key( @@ -422,7 +363,7 @@ async def get_total_stake_for_coldkey( :return: {address: Balance objects} """ sub_stakes = await self.get_stake_for_coldkeys( - ss58_addresses, block_hash=block_hash + list(ss58_addresses), block_hash=block_hash ) # Token pricing info dynamic_info = await self.all_subnets() @@ -485,14 +426,26 @@ async def get_total_stake_for_hotkey( ... } """ + if not block_hash: + if reuse_block: + block_hash = self.substrate.last_block_hash + else: + block_hash = await self.substrate.get_chain_head() + netuids = netuids or await self.get_all_subnet_netuids(block_hash=block_hash) - query: dict[tuple[str, int], int] = await self.substrate.query_multiple( - params=[(ss58, netuid) for ss58 in ss58_addresses for netuid in netuids], - module="SubtensorModule", - storage_function="TotalHotkeyAlpha", - block_hash=block_hash, - reuse_block_hash=reuse_block, - ) + calls = [ + ( + await self.substrate.create_storage_key( + "SubtensorModule", + "TotalHotkeyAlpha", + params=[ss58, netuid], + block_hash=block_hash, + ) + ) + for ss58 in ss58_addresses + for netuid in netuids + ] + query = await self.substrate.query_multi(calls, block_hash=block_hash) results: dict[str, dict[int, "Balance"]] = { hk_ss58: {} for hk_ss58 in ss58_addresses } @@ -510,29 +463,31 @@ async def current_take( hotkey_ss58: int, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> bool: + ) -> 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. + :param hotkey_ss58: The `SS58` address of the neuron's hotkey. + :param block_hash: The hash of the block number to retrieve the stake from. + :param reuse_block: Whether to reuse the last-used block hash when retrieving info. - Returns: - Optional[float]: The delegate take percentage, None if not available. + :return: 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 = await self.substrate.query( + result = await self.query( module="SubtensorModule", storage_function="Delegates", params=[hotkey_ss58], block_hash=block_hash, reuse_block_hash=reuse_block, ) - return u16_normalized_float(result) + if result is None: + return None + else: + return u16_normalized_float(result) async def get_netuids_for_hotkey( self, @@ -559,11 +514,11 @@ async def get_netuids_for_hotkey( block_hash=block_hash, reuse_block_hash=reuse_block, ) - return ( - [record[0] async for record in result if record[1]] - if result and hasattr(result, "records") - else [] - ) + res = [] + async for record in result: + if record[1].value: + res.append(record[0]) + return res async def subnet_exists( self, netuid: int, block_hash: Optional[str] = None, reuse_block: bool = False @@ -580,7 +535,7 @@ async def subnet_exists( 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 = await self.substrate.query( + result = await self.query( module="SubtensorModule", storage_function="NetworksAdded", params=[netuid], @@ -595,29 +550,22 @@ async def get_subnet_state( """ Retrieves the state of a specific subnet within the Bittensor network. - Args: - netuid: The network UID of the subnet to query. - block_hash: The hash of the blockchain block number for the query. + :param netuid: The network UID of the subnet to query. + :param block_hash: The hash of the blockchain block number for the query. - Returns: - SubnetState object containing the subnet's state information, or None if the subnet doesn't exist. + :return: SubnetState object containing the subnet's state information, or None if the subnet doesn't exist. """ - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_subnet_state", params=[netuid], block_hash=block_hash, ) - if hex_bytes_result is None: + if result is None: return None - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return SubnetState.from_vec_u8(bytes_result) + return SubnetState.from_any(result) async def get_hyperparameter( self, @@ -640,7 +588,7 @@ async def get_hyperparameter( print("subnet does not exist") return None - result = await self.substrate.query( + result = await self.query( module="SubtensorModule", storage_function=param_name, params=[netuid], @@ -722,11 +670,15 @@ async def get_existential_deposit( 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 = await self.substrate.get_constant( - module_name="Balances", - constant_name="ExistentialDeposit", - block_hash=block_hash, - reuse_block_hash=reuse_block, + result = getattr( + await self.substrate.get_constant( + module_name="Balances", + constant_name="ExistentialDeposit", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ), + "value", + None, ) if result is None: @@ -785,25 +737,18 @@ async def neurons_lite( 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 = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="NeuronInfoRuntimeApi", method="get_neurons_lite", - params=[ - netuid - ], # TODO check to see if this can accept more than one at a time + params=[netuid], block_hash=block_hash, reuse_block=reuse_block, ) - if hex_bytes_result is None: + if result is None: return [] - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return NeuronInfoLite.list_from_vec_u8(bytes_result) + return NeuronInfoLite.list_from_any(result) async def neuron_for_uid( self, uid: Optional[int], netuid: int, block_hash: Optional[str] = None @@ -826,22 +771,20 @@ async def neuron_for_uid( if uid is None: return NeuronInfo.get_null_neuron() - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="NeuronInfoRuntimeApi", method="get_neuron", - params=[netuid, uid], + params=[ + netuid, + uid, + ], # TODO check to see if this can accept more than one at a time block_hash=block_hash, ) - if not (result := hex_bytes_result): + if not result: return NeuronInfo.get_null_neuron() - if result.startswith("0x"): - bytes_result = bytes.fromhex(result[2:]) - else: - bytes_result = bytes.fromhex(result) - - return NeuronInfo.from_vec_u8(bytes_result) + return NeuronInfo.from_any(result) async def get_delegated( self, @@ -868,24 +811,17 @@ async def get_delegated( if block_hash else (self.substrate.last_block_hash if reuse_block else None) ) - encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="DelegateInfoRuntimeApi", method="get_delegated", - params=[encoded_coldkey], + params=[coldkey_ss58], block_hash=block_hash, ) - if not (result := hex_bytes_result): + if not result: return [] - if result.startswith("0x"): - bytes_result = bytes.fromhex(result[2:]) - else: - bytes_result = bytes.fromhex(result) - - return DelegateInfo.delegated_list_from_vec_u8(bytes_result) + return DelegateInfo.list_from_any(result) async def query_all_identities( self, @@ -903,18 +839,16 @@ async def query_all_identities( identities = await self.substrate.query_map( module="SubtensorModule", - storage_function="Identities", + storage_function="IdentitiesV2", block_hash=block_hash, reuse_block_hash=reuse_block, ) + all_identities = {} + async for ss58_address, identity in identities: + all_identities[decode_account_id(ss58_address[0])] = decode_hex_identity( + identity.value + ) - if identities is None: - return {} - - all_identities = { - decode_account_id(ss58_address[0]): decode_hex_identity(identity) - for ss58_address, identity in identities - } return all_identities async def query_identity( @@ -941,9 +875,9 @@ async def query_identity( 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. """ - identity_info = await self.substrate.query( + identity_info = await self.query( module="SubtensorModule", - storage_function="Identities", + storage_function="IdentitiesV2", params=[key], block_hash=block_hash, reuse_block_hash=reuse_block, @@ -971,8 +905,8 @@ async def fetch_coldkey_hotkey_identities( identities = {"coldkeys": {}, "hotkeys": {}} if not coldkey_identities: return identities - query = await self.substrate.query_multiple( - params=[(ss58) for ss58, _ in coldkey_identities.items()], + query = await self.substrate.query_multiple( # TODO probably more efficient to do this with query_multi + params=list(coldkey_identities.keys()), module="SubtensorModule", storage_function="OwnedHotkeys", block_hash=block_hash, @@ -1004,7 +938,6 @@ async def weights( 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: :param netuid: The network UID of the subnet to query. :param block_hash: The hash of the blockchain block for the query. @@ -1013,14 +946,15 @@ async def 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. """ - # TODO look into seeing if we can speed this up with storage query w_map_encoded = await self.substrate.query_map( module="SubtensorModule", storage_function="Weights", params=[netuid], block_hash=block_hash, ) - w_map = [(uid, w or []) async for uid, w in w_map_encoded] + w_map = [] + async for uid, w in w_map_encoded: + w_map.append((uid, w.value)) return w_map @@ -1048,7 +982,9 @@ async def bonds( params=[netuid], block_hash=block_hash, ) - b_map = [(uid, b) async for uid, b in b_map_encoded] + b_map = [] + async for uid, b in b_map_encoded: + b_map.append((uid, b)) return b_map @@ -1067,31 +1003,27 @@ async def does_hotkey_exist( :return: `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. """ - _result = await self.substrate.query( + result = await self.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, reuse_block_hash=reuse_block, ) - result = decode_account_id(_result[0]) - return_val = ( - False - if result is None - else result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ) + return_val = result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" return return_val async def get_hotkey_owner( - self, hotkey_ss58: str, block_hash: str + self, + hotkey_ss58: str, + block_hash: Optional[str] = None, ) -> Optional[str]: - hk_owner_query = await self.substrate.query( + val = await self.query( module="SubtensorModule", storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, ) - val = decode_account_id(hk_owner_query[0]) if val: exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash) else: @@ -1150,7 +1082,7 @@ async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: message (if applicable) """ try: - children = await self.substrate.query( + children = await self.query( module="SubtensorModule", storage_function="ChildKeys", params=[hotkey, netuid], @@ -1183,22 +1115,26 @@ async def get_subnet_hyperparameters( 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 = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_subnet_hyperparams", params=[netuid], block_hash=block_hash, ) - if hex_bytes_result is None: + if not result: 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_any(result) - return SubnetHyperparameters.from_vec_u8(bytes_result) + async def burn_cost(self, block_hash: Optional[str] = None) -> Optional[Balance]: + result = await self.query_runtime_api( + runtime_api="SubnetRegistrationRuntimeApi", + method="get_network_registration_cost", + params=[], + block_hash=block_hash, + ) + return Balance.from_rao(result) if result is not None else None async def get_vote_data( self, @@ -1219,7 +1155,7 @@ async def get_vote_data( 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 = await self.substrate.query( + vote_data = await self.query( module="Triumvirate", storage_function="Voting", params=[proposal_hash], @@ -1239,10 +1175,9 @@ async def get_delegate_identities( is filled-in by the info from GitHub. At some point, we want to totally move away from fetching this info from GitHub, but chain data is still limited in that regard. - Args: - block_hash: the hash of the blockchain block for the query + :param block_hash: the hash of the blockchain block for the query - Returns: {ss58: DelegatesDetails, ...} + :return: {ss58: DelegatesDetails, ...} """ timeout = aiohttp.ClientTimeout(10.0) @@ -1256,12 +1191,17 @@ async def get_delegate_identities( session.get(Constants.delegates_detail_url), ) - all_delegates_details = { - decode_account_id(ss58_address[0]): DelegatesDetails.from_chain_data( - decode_hex_identity_dict(identity["info"]) + all_delegates_details = {} + async for ss58_address, identity in identities_info: + all_delegates_details.update( + { + decode_account_id( + ss58_address[0] + ): DelegatesDetails.from_chain_data( + decode_hex_identity_dict(identity.value["info"]) + ) + } ) - for ss58_address, identity in identities_info - } if response.ok: all_delegates: dict[str, Any] = await response.json(content_type=None) @@ -1293,36 +1233,6 @@ async def get_delegate_identities( return all_delegates_details - async def get_delegates_by_netuid_light( - self, netuid: int, block_hash: Optional[str] = None - ) -> list[DelegateInfoLite]: - """ - 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: - netuid: the netuid to query - block_hash: The hash of the blockchain block number for the query. - - Returns: - A list of DelegateInfo objects detailing each delegate's characteristics. - - """ - # TODO (Ben): doesn't exist - params = [netuid] if not block_hash else [netuid, block_hash] - json_body = await self.substrate.rpc_request( - method="delegateInfo_getDelegatesLight", # custom rpc method - params=params, - ) - - result = json_body["result"] - - if result in (None, []): - return [] - - return DelegateInfoLite.list_from_vec_u8(result) # TODO this won't work yet - async def get_stake_for_coldkey_and_hotkey_on_netuid( self, hotkey_ss58: str, @@ -1331,8 +1241,11 @@ async def get_stake_for_coldkey_and_hotkey_on_netuid( block_hash: Optional[str] = None, ) -> "Balance": """Returns the stake under a coldkey - hotkey - netuid pairing""" - _result = await self.substrate.query( - "SubtensorModule", "Alpha", [hotkey_ss58, coldkey_ss58, netuid], block_hash + _result = await self.query( + "SubtensorModule", + "Alpha", + [hotkey_ss58, coldkey_ss58, netuid], + block_hash, ) if _result is None: return Balance(0).set_unit(netuid) @@ -1349,13 +1262,12 @@ async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( """ Queries the stake for multiple hotkey - coldkey - netuid pairings. - Args: - hotkey_ss58s: list of hotkey ss58 addresses - coldkey_ss58: a single coldkey ss58 address - netuids: list of netuids - block_hash: hash of the blockchain block, if any + :param hotkey_ss58s: list of hotkey ss58 addresses + :param coldkey_ss58: a single coldkey ss58 address + :param netuids: list of netuids + :param block_hash: hash of the blockchain block, if any - Returns: + :return: { hotkey_ss58_1: { netuid_1: netuid1_stake, @@ -1407,58 +1319,47 @@ async def get_stake_for_coldkeys( 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: A list of SS58 addresses of the accounts' coldkeys. - block_hash: The blockchain block number for the query. + :param coldkey_ss58_list: A list of SS58 addresses of the accounts' coldkeys. + :param block_hash: The blockchain block number for the query. - Returns: - A dictionary mapping each coldkey to a list of its StakeInfo objects. + :return: 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. """ - encoded_coldkeys = [ - ss58_to_vec_u8(coldkey_ss58) for coldkey_ss58 in coldkey_ss58_list - ] - - hex_bytes_result = await self.query_runtime_api( + result = await self.query_runtime_api( runtime_api="StakeInfoRuntimeApi", method="get_stake_info_for_coldkeys", - params=[encoded_coldkeys], + params=coldkey_ss58_list, block_hash=block_hash, ) - - if hex_bytes_result is None: + if 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 + stake_info_map = {} + for coldkey_bytes, stake_info_list in result: + coldkey_ss58 = decode_account_id(coldkey_bytes) + stake_info_map[coldkey_ss58] = StakeInfo.list_from_any(stake_info_list) + return stake_info_map - async def all_subnets( - self, block_hash: Optional[str] = None - ) -> list["DynamicInfo"]: - query = await self.substrate.runtime_call( + async def all_subnets(self, block_hash: Optional[str] = None) -> list[DynamicInfo]: + result = await self.query_runtime_api( "SubnetInfoRuntimeApi", "get_all_dynamic_info", block_hash=block_hash, ) - subnets = DynamicInfo.list_from_vec_u8(bytes.fromhex(query.decode()[2:])) - return subnets + return DynamicInfo.list_from_any(result) async def subnet( self, netuid: int, block_hash: Optional[str] = None ) -> "DynamicInfo": - query = await self.substrate.runtime_call( + result = await self.query_runtime_api( "SubnetInfoRuntimeApi", "get_dynamic_info", params=[netuid], block_hash=block_hash, ) - return DynamicInfo.from_vec_u8(bytes.fromhex(query.decode()[2:])) + return DynamicInfo.from_any(result) async def get_owned_hotkeys( self, @@ -1475,7 +1376,7 @@ async def get_owned_hotkeys( :return: A list of hotkey SS58 addresses owned by the coldkey. """ - owned_hotkeys = await self.substrate.query( + owned_hotkeys = await self.query( module="SubtensorModule", storage_function="OwnedHotkeys", params=[coldkey_ss58], diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 08cfd9e08..11dbdeab5 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -21,9 +21,7 @@ from numpy.typing import NDArray from rich.console import Console from rich.prompt import Prompt -import scalecodec -from scalecodec.base import RuntimeConfiguration -from scalecodec.type_registry import load_type_registry_preset +from scalecodec.utils.ss58 import ss58_encode, ss58_decode import typer @@ -33,9 +31,7 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters - from bittensor_cli.src.bittensor.async_substrate_interface import ( - AsyncSubstrateInterface, - ) + from async_substrate_interface.async_substrate import AsyncSubstrateInterface console = Console() err_console = Console(stderr=True) @@ -376,21 +372,15 @@ def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool return False -def decode_scale_bytes(return_type, scale_bytes, custom_rpc_type_registry): - """Decodes a ScaleBytes object using our type registry and return type""" - 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, scale_bytes) - if obj.data.to_hex() == "0x0400": # RPC returned None result - return None - return obj.decode() +def decode_account_id(account_id_bytes: Union[tuple[int], tuple[tuple[int]]]): + if isinstance(account_id_bytes, tuple) and isinstance(account_id_bytes[0], tuple): + account_id_bytes = account_id_bytes[0] + # Convert the AccountId bytes to a Base64 string + return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) -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) - return bytes.fromhex(account_id_hex) +def encode_account_id(ss58_address: str) -> bytes: + return bytes.fromhex(ss58_decode(ss58_address, SS58_FORMAT)) def ss58_to_vec_u8(ss58_address: str) -> list[int]: @@ -401,7 +391,7 @@ def ss58_to_vec_u8(ss58_address: str) -> list[int]: :return: A list of integers representing the byte values of the SS58 address. """ - ss58_bytes: bytes = ss58_address_to_bytes(ss58_address) + ss58_bytes: bytes = encode_account_id(ss58_address) encoded_address: list[int] = [int(byte) for byte in ss58_bytes] return encoded_address @@ -489,7 +479,7 @@ def format_error_message( elif all(x in d for x in ["code", "message", "data"]): new_error_message = d break - except ValueError: + except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError): pass if new_error_message is None: return_val = " ".join(error_message.args) @@ -1049,9 +1039,10 @@ def prompt_for_identity( name: Optional[str], web_url: Optional[str], image_url: Optional[str], - discord_handle: Optional[str], + discord: Optional[str], description: Optional[str], - additional_info: Optional[str], + additional: Optional[str], + github_repo: Optional[str], ): """ Prompts the user for identity fields with validation. @@ -1063,9 +1054,10 @@ def prompt_for_identity( ("name", "[blue]Display name[/blue]", name), ("url", "[blue]Web URL[/blue]", web_url), ("image", "[blue]Image URL[/blue]", image_url), - ("discord", "[blue]Discord handle[/blue]", discord_handle), + ("discord", "[blue]Discord handle[/blue]", discord), ("description", "[blue]Description[/blue]", description), - ("additional", "[blue]Additional information[/blue]", additional_info), + ("additional", "[blue]Additional information[/blue]", additional), + ("github_repo", "[blue]GitHub repository URL[/blue]", github_repo), ] text_rejection = partial( @@ -1079,9 +1071,10 @@ def prompt_for_identity( name, web_url, image_url, - discord_handle, + discord, description, - additional_info, + additional, + github_repo, ] ): console.print( @@ -1106,6 +1099,10 @@ def prompt_for_subnet_identity( subnet_name: Optional[str], github_repo: Optional[str], subnet_contact: Optional[str], + subnet_url: Optional[str], + discord: Optional[str], + description: Optional[str], + additional: Optional[str], ): """ Prompts the user for required subnet identity fields with validation. @@ -1143,6 +1140,34 @@ def prompt_for_subnet_identity( lambda x: x and not is_valid_contact(x), "[red]Error:[/red] Please enter a valid email address.", ), + ( + "subnet_url", + "[blue]Subnet URL [dim](optional)[/blue]", + subnet_url, + lambda x: x and sys.getsizeof(x) > 113, + "[red]Error:[/red] Please enter a valid URL.", + ), + ( + "discord", + "[blue]Discord handle [dim](optional)[/blue]", + discord, + lambda x: x and sys.getsizeof(x) > 113, + "[red]Error:[/red] Please enter a valid Discord handle.", + ), + ( + "description", + "[blue]Description [dim](optional)[/blue]", + description, + lambda x: x and sys.getsizeof(x) > 113, + "[red]Error:[/red] Description must be <= 64 raw bytes.", + ), + ( + "additional", + "[blue]Additional information [dim](optional)[/blue]", + additional, + lambda x: x and sys.getsizeof(x) > 113, + "[red]Error:[/red] Additional information must be <= 64 raw bytes.", + ), ] for key, prompt, value, rejection_func, rejection_msg in fields: @@ -1183,7 +1208,7 @@ def is_valid_github_url(url: str) -> bool: return False return True - except: + except Exception: # TODO figure out the exceptions that can be raised in here return False diff --git a/bittensor_cli/src/commands/stake/__init__.py b/bittensor_cli/src/commands/stake/__init__.py index 6b0b0fc91..d1dceeaba 100644 --- a/bittensor_cli/src/commands/stake/__init__.py +++ b/bittensor_cli/src/commands/stake/__init__.py @@ -3,9 +3,8 @@ import rich.prompt from rich.table import Table -from bittensor_cli.src import DelegatesDetails -from bittensor_cli.src.bittensor.chain_data import DelegateInfo, DelegateInfoLite -from bittensor_cli.src.bittensor.utils import console, err_console +from bittensor_cli.src.bittensor.chain_data import DelegateInfoLite +from bittensor_cli.src.bittensor.utils import console if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 52d30dd18..8678faab9 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -6,7 +6,7 @@ from rich.prompt import Confirm, Prompt, IntPrompt from rich.table import Table from rich.text import Text -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -22,6 +22,33 @@ ) +async def get_childkey_completion_block( + subtensor: SubtensorInterface, netuid: int +) -> tuple[int, int]: + """ + Calculates the block at which the childkey set request will complete + """ + blocks_since_last_step_query = subtensor.query( + "SubtensorModule", + "BlocksSinceLastStep", + params=[netuid], + ) + tempo_query = subtensor.get_hyperparameter( + param_name="Tempo", + netuid=netuid, + ) + block_number, blocks_since_last_step, tempo = await asyncio.gather( + subtensor.substrate.get_block_number(), + blocks_since_last_step_query, + tempo_query, + ) + cooldown = block_number + 7200 + blocks_left_in_tempo = tempo - blocks_since_last_step + next_tempo = block_number + blocks_left_in_tempo + next_epoch_after_cooldown = (cooldown - next_tempo) % tempo + cooldown + return block_number, next_epoch_after_cooldown + + async def set_children_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, @@ -227,7 +254,7 @@ async def get_childkey_take(subtensor, hotkey: str, netuid: int) -> Optional[int - Optional[float]: The value of the "ChildkeyTake" if found, or None if any error occurs. """ try: - childkey_take_ = await subtensor.substrate.query( + childkey_take_ = await subtensor.query( module="SubtensorModule", storage_function="ChildkeyTake", params=[hotkey, netuid], @@ -513,8 +540,14 @@ async def set_children( # Result if success: if wait_for_inclusion and wait_for_finalization: - console.print("New Status:") - await get_children(wallet, subtensor, netuid) + current_block, completion_block = await get_childkey_completion_block( + subtensor, netuid + ) + console.print( + f"Your childkey request has been submitted. It will be completed around block {completion_block}, " + f"assuming you have the required key swap cost (default: 0.1 Tao) in your coldkey at that time. " + f"The current block is {current_block}" + ) console.print( ":white_heavy_check_mark: [green]Set children hotkeys.[/green]" ) @@ -525,20 +558,28 @@ async def set_children( else: # set children on all subnets that parent is registered on netuids = await subtensor.get_all_subnet_netuids() - for netuid in netuids: - if netuid == 0: # dont include root network + for netuid_ in netuids: + if netuid_ == 0: # dont include root network continue - console.print(f"Setting children on netuid {netuid}.") + console.print(f"Setting children on netuid {netuid_}.") await set_children_extrinsic( subtensor=subtensor, wallet=wallet, - netuid=netuid, + netuid=netuid_, hotkey=hotkey, children_with_proportions=children_with_proportions, prompt=prompt, wait_for_inclusion=True, wait_for_finalization=False, ) + current_block, completion_block = await get_childkey_completion_block( + subtensor, netuid_ + ) + console.print( + f"Your childkey request for netuid {netuid_} has been submitted. It will be completed around " + f"block {completion_block}, assuming you have the required key swap cost (default: 0.1 Tao) in your " + f"coldkey at that time. The current block is {current_block}." + ) console.print( ":white_heavy_check_mark: [green]Sent set children request for all subnets.[/green]" ) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index dcf9e3a00..68211a5f5 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -60,17 +60,25 @@ async def display_stake_movement_cross_subnets( price = ( float(dynamic_origin.price) * 1 / (float(dynamic_destination.price) or 1) ) - received_amount_tao, _, slippage_pct_float = ( - dynamic_origin.alpha_to_tao_with_slippage(amount_to_move) + received_amount_tao, _, _ = dynamic_origin.alpha_to_tao_with_slippage( + amount_to_move ) received_amount_tao -= MIN_STAKE_FEE - received_amount, _, slippage_pct_float = ( - dynamic_destination.tao_to_alpha_with_slippage(received_amount_tao) + received_amount, _, _ = dynamic_destination.tao_to_alpha_with_slippage( + received_amount_tao ) received_amount.set_unit(destination_netuid) + + if received_amount < Balance.from_tao(0): + print_error("Not enough Alpha to pay the transaction fee.") + raise typer.Exit() + + ideal_amount = amount_to_move * price + total_slippage = ideal_amount - received_amount + slippage_pct_float = 100 * (total_slippage.tao / ideal_amount.tao) slippage_pct = f"{slippage_pct_float:.4f} %" price_str = ( - str(float(price)) + f"{price:.5f}" + f"{Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)}" ) @@ -648,7 +656,7 @@ async def move_stake( if not await response.is_success: err_console.print( f"\n:cross_mark: [red]Failed[/red] with error:" - f" {format_error_message( await response.error_message, subtensor.substrate)}" + f" {format_error_message(await response.error_message)}" ) return else: diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index b6fe73e43..2ea5dd8e5 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -12,7 +12,7 @@ from rich.progress import Progress, BarColumn, TextColumn from rich.console import Group from rich.live import Live -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance @@ -91,9 +91,7 @@ async def stake_add( current_wallet_balance_ = await subtensor.get_balance( wallet.coldkeypub.ss58_address ) - current_wallet_balance = current_wallet_balance_[ - wallet.coldkeypub.ss58_address - ].set_unit(0) + current_wallet_balance = current_wallet_balance_.set_unit(0) remaining_wallet_balance = current_wallet_balance max_slippage = 0.0 @@ -140,8 +138,8 @@ async def stake_add( starting_chain_head = await subtensor.substrate.get_chain_head() _all_dynamic_info, stake_info_dict = await asyncio.gather( subtensor.all_subnets(), - subtensor.get_stake_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=starting_chain_head, ), ) @@ -152,7 +150,7 @@ async def stake_add( for netuid in netuids: initial_stake_balances[hotkey_ss58][netuid] = Balance.from_rao(0) - for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: + for stake_info in stake_info_dict: if stake_info.hotkey_ss58 in initial_stake_balances: initial_stake_balances[stake_info.hotkey_ss58][stake_info.netuid] = ( stake_info.stake @@ -304,10 +302,6 @@ async def send_extrinsic( f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}" ) return - if not prompt: # TODO verbose? - console.print( - f":white_heavy_check_mark: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Submitted {amount_} to {netuid_i}[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - ) else: await response.process_events() if not await response.is_success: @@ -315,22 +309,21 @@ async def send_extrinsic( f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" ) else: - new_balance_, stake_info_dict = await asyncio.gather( + new_balance, stake_info_dict = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address), - subtensor.get_stake_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, ), ) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] new_stake = Balance.from_rao(0) - for stake_info in stake_info_dict[wallet.coldkeypub.ss58_address]: + for stake_info in stake_info_dict: if ( stake_info.hotkey_ss58 == staking_address_ss58 and stake_info.netuid == netuid_i ): new_stake = stake_info.stake.set_unit(netuid_i) break - + console.print(":white_heavy_check_mark: [green]Finalized[/green]") console.print( f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) @@ -356,7 +349,7 @@ async def send_extrinsic( await extrinsics_coroutines[0] else: with console.status(":satellite: Checking transaction rate limit ..."): - tx_rate_limit_blocks = await subtensor.substrate.query( + tx_rate_limit_blocks = await subtensor.query( module="SubtensorModule", storage_function="TxRateLimit" ) netuid_hk_pairs = [(ni, hk) for ni in netuids for hk in hotkeys_to_stake_to] @@ -759,10 +752,9 @@ async def _unstake_all( else ":white_heavy_check_mark: [green]Successfully unstaked all Alpha stakes[/green]" ) console.print(success_message) - new_balance_ = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] + new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) console.print( - f"Balance:\n [blue]{current_wallet_balance[wallet.coldkeypub.ss58_address]}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) return True else: @@ -875,28 +867,24 @@ async def unstake( # Prepare unstaking transactions unstake_operations = [] total_received_amount = Balance.from_tao(0) - current_wallet_balance: Balance = ( - await subtensor.get_balance(wallet.coldkeypub.ss58_address) - )[wallet.coldkeypub.ss58_address] + current_wallet_balance: Balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) max_float_slippage = 0 # Fetch stake balances chain_head = await subtensor.substrate.get_chain_head() - stake_info_dict = await subtensor.get_stake_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], + stake_info_list = await subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=chain_head, ) stake_in_netuids = {} - for _, stake_info_list in stake_info_dict.items(): - hotkey_stakes = {} - for stake_info in stake_info_list: - if stake_info.hotkey_ss58 not in hotkey_stakes: - hotkey_stakes[stake_info.hotkey_ss58] = {} - hotkey_stakes[stake_info.hotkey_ss58][stake_info.netuid] = ( - stake_info.stake - ) - - stake_in_netuids = hotkey_stakes + for stake_info in stake_info_list: + if stake_info.hotkey_ss58 not in stake_in_netuids: + stake_in_netuids[stake_info.hotkey_ss58] = {} + stake_in_netuids[stake_info.hotkey_ss58][stake_info.netuid] = ( + stake_info.stake + ) # Flag to check if user wants to quit skip_remaining_subnets = False @@ -1094,24 +1082,21 @@ async def unstake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") + await response.process_events() + if not await response.is_success: + print_error( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}", + status, + ) else: - await response.process_events() - if not await response.is_success: - print_error( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message, subtensor.substrate)}", - status, - ) - else: - new_balance_ = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) + new_balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) else: for op in unstake_operations: netuid_i = op["netuid"] @@ -1139,41 +1124,36 @@ async def unstake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") + await response.process_events() + if not await response.is_success: + print_error( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}", + status, + ) else: - await response.process_events() - if not await response.is_success: - print_error( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message, subtensor.substrate)}", - status, - ) - else: - new_balance_ = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - new_balance = new_balance_[wallet.coldkeypub.ss58_address] - new_stake_info = await subtensor.get_stake_for_coldkeys( - coldkey_ss58_list=[wallet.coldkeypub.ss58_address], - ) - new_stake = Balance.from_rao(0) - for stake_info in new_stake_info[ - wallet.coldkeypub.ss58_address - ]: - if ( - stake_info.hotkey_ss58 == staking_address_ss58 - and stake_info.netuid == netuid_i - ): - new_stake = stake_info.stake.set_unit(netuid_i) - break - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f" Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" - ) + new_balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + new_stake_info = await subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + new_stake = Balance.from_rao(0) + for stake_info in new_stake_info: + if ( + stake_info.hotkey_ss58 == staking_address_ss58 + and stake_info.netuid == netuid_i + ): + new_stake = stake_info.stake.set_unit(netuid_i) + break + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f" Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + ) console.print( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." ) @@ -1191,17 +1171,17 @@ async def stake_list( async def get_stake_data(block_hash: str = None): ( - substakes, + sub_stakes, registered_delegate_info, _dynamic_info, ) = await asyncio.gather( - subtensor.get_stake_for_coldkeys( - coldkey_ss58_list=[coldkey_address], block_hash=block_hash + subtensor.get_stake_for_coldkey( + coldkey_ss58=coldkey_address, block_hash=block_hash ), subtensor.get_delegate_identities(block_hash=block_hash), subtensor.all_subnets(), ) - sub_stakes = substakes[coldkey_address] + # sub_stakes = substakes[coldkey_address] dynamic_info = {info.netuid: info for info in _dynamic_info} return ( sub_stakes, @@ -1357,6 +1337,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): # Alpha ownership and TAO ownership cells if alpha_value.tao > 0.00009: if issuance.tao != 0: + # TODO figure out why this alpha_ownership does nothing alpha_ownership = "{:.4f}".format( (alpha_value.tao / issuance.tao) * 100 ) @@ -1750,7 +1731,7 @@ def format_cell( console.print( f"Wallet:\n" f" Coldkey SS58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{coldkey_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance[coldkey_address]}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" f" Total TAO ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_ownership}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" f" Total Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" ) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 041261bed..c7a8f7bcf 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -14,7 +14,6 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.chain_data import SubnetState from bittensor_cli.src.bittensor.extrinsics.registration import ( register_extrinsic, burned_register_extrinsic, @@ -76,7 +75,7 @@ async def _find_event_attributes_in_extrinsic_receipt( """ Searches for the attributes of a specified event within an extrinsic receipt. - :param response_: (substrateinterface.base.ExtrinsicReceipt): The receipt of the extrinsic to be searched. + :param response_: The receipt of the extrinsic to be searched. :param event_name: The name of the event to search for. :return: A list of attributes for the specified event. Returns [-1] if the event is not found. @@ -91,8 +90,7 @@ async def _find_event_attributes_in_extrinsic_receipt( return [-1] print_verbose("Fetching balance") - your_balance_ = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - your_balance = your_balance_[wallet.coldkeypub.ss58_address] + your_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) print_verbose("Fetching burn_cost") sn_burn_cost = await burn_cost(subtensor) @@ -124,6 +122,18 @@ async def _find_event_attributes_in_extrinsic_receipt( "subnet_contact": subnet_identity["subnet_contact"].encode() if subnet_identity.get("subnet_contact") else b"", + "subnet_url": subnet_identity["subnet_url"].encode() + if subnet_identity.get("subnet_url") + else b"", + "discord": subnet_identity["discord"].encode() + if subnet_identity.get("discord") + else b"", + "description": subnet_identity["description"].encode() + if subnet_identity.get("description") + else b"", + "additional": subnet_identity["additional"].encode() + if subnet_identity.get("additional") + else b"", } for field, value in identity_data.items(): max_size = 64 # bytes @@ -975,7 +985,7 @@ async def show_root(): The table displays the root subnet participants and their metrics. The columns are as follows: - Position: The sorted position of the hotkey by total TAO. - - TAO: The sum of all TAO balances for this hotkey accross all subnets. + - TAO: The sum of all TAO balances for this hotkey accross all subnets. - Stake: The stake balance of this hotkey on root (measured in TAO). - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. - Hotkey: The hotkey ss58 address. @@ -1348,20 +1358,17 @@ async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: f":satellite:Retrieving lock cost from {subtensor.network}...", spinner="aesthetic", ): - lc = await subtensor.query_runtime_api( - runtime_api="SubnetRegistrationRuntimeApi", - method="get_network_registration_cost", - params=[], - ) - if lc: - burn_cost_ = Balance(lc) - console.print( - f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{burn_cost_}" - ) - return burn_cost_ - else: - err_console.print("Subnet burn cost: [red]Failed to get subnet burn cost[/red]") - return None + burn_cost = await subtensor.burn_cost() + if burn_cost: + console.print( + f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{burn_cost}" + ) + return burn_cost + else: + err_console.print( + "Subnet burn cost: [red]Failed to get subnet burn cost[/red]" + ) + return None async def create( @@ -1396,9 +1403,10 @@ async def create( name=None, web_url=None, image_url=None, - discord_handle=None, + discord=None, description=None, - additional_info=None, + additional=None, + github_repo=None, ) await set_id( @@ -1410,6 +1418,7 @@ async def create( identity["discord"], identity["description"], identity["additional"], + identity["github_repo"], prompt, ) @@ -1457,7 +1466,7 @@ async def register( # Check current recycle amount print_verbose("Fetching recycle amount") - current_recycle_, balance_ = await asyncio.gather( + current_recycle_, balance = await asyncio.gather( subtensor.get_hyperparameter( param_name="Burn", netuid=netuid, block_hash=block_hash ), @@ -1466,7 +1475,6 @@ async def register( current_recycle = ( Balance.from_rao(int(current_recycle_)) if current_recycle_ else Balance(0) ) - balance = balance_[wallet.coldkeypub.ss58_address] # Check balance is sufficient if balance < current_recycle: @@ -1581,7 +1589,7 @@ async def metagraph_cmd( subtensor.get_hyperparameter( param_name="Difficulty", netuid=netuid, block_hash=block_hash ), - subtensor.substrate.query( + subtensor.query( module="SubtensorModule", storage_function="TotalIssuance", params=[], diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 19fb847d5..60a107a2e 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -95,12 +95,11 @@ async def set_hyperparameter_extrinsic( finalization/inclusion, the response is `True`. """ print_verbose("Confirming subnet owner") - subnet_owner_ = await subtensor.substrate.query( + subnet_owner = await subtensor.query( module="SubtensorModule", storage_function="SubnetOwner", params=[netuid], ) - subnet_owner = decode_account_id(subnet_owner_[0]) if subnet_owner != wallet.coldkeypub.ss58_address: err_console.print( ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" @@ -183,7 +182,7 @@ async def _get_senate_members( :return: list of the senate members' ss58 addresses """ - senate_members = await subtensor.substrate.query( + senate_members = await subtensor.query( module="SenateMembers", storage_function="Members", params=None, @@ -202,7 +201,7 @@ async def _get_proposals( subtensor: "SubtensorInterface", block_hash: str ) -> dict[str, tuple[dict, "ProposalVoteData"]]: async def get_proposal_call_data(p_hash: str) -> Optional[GenericCall]: - proposal_data = await subtensor.substrate.query( + proposal_data = await subtensor.query( module="Triumvirate", storage_function="ProposalOf", block_hash=block_hash, @@ -210,7 +209,7 @@ async def get_proposal_call_data(p_hash: str) -> Optional[GenericCall]: ) return proposal_data - ph = await subtensor.substrate.query( + ph = await subtensor.query( module="Triumvirate", storage_function="Proposals", params=None, @@ -685,6 +684,13 @@ async def get_current_take(subtensor: "SubtensorInterface", wallet: Wallet): return current_take +async def display_current_take(subtensor: "SubtensorInterface", wallet: Wallet) -> None: + current_take = await get_current_take(subtensor, wallet) + console.print( + f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%" + ) + + async def set_take( wallet: Wallet, subtensor: "SubtensorInterface", take: float ) -> bool: diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index c2b8e0217..4d0b339b5 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2,7 +2,7 @@ import itertools import os from collections import defaultdict -from typing import Any, Generator, Optional +from typing import Generator, Optional import aiohttp from bittensor_wallet import Wallet, Keypair @@ -14,7 +14,6 @@ from rich.table import Column, Table from rich.tree import Tree from rich.padding import Padding -from scalecodec import ScaleBytes import typer from bittensor_cli.src import COLOR_PALETTE @@ -288,7 +287,7 @@ async def wallet_balance( block_hash = await subtensor.substrate.get_chain_head() free_balances, staked_balances = await asyncio.gather( - subtensor.get_balance(*coldkeys, block_hash=block_hash), + subtensor.get_balances(*coldkeys, block_hash=block_hash), subtensor.get_total_stake_for_coldkey(*coldkeys, block_hash=block_hash), ) @@ -552,7 +551,7 @@ async def _get_total_balance( ] total_balance += sum( ( - await subtensor.get_balance( + await subtensor.get_balances( *(x.coldkeypub.ss58_address for x in _balance_cold_wallets), block_hash=block_hash, ) @@ -574,7 +573,7 @@ async def _get_total_balance( ): total_balance = sum( ( - await subtensor.get_balance( + await subtensor.get_balances( coldkey_wallet.coldkeypub.ss58_address, block_hash=block_hash ) ).values() @@ -1029,7 +1028,7 @@ def _map_hotkey_to_neurons( async def _fetch_neuron_for_netuid( netuid: int, subtensor: SubtensorInterface -) -> tuple[int, dict[str, list[ScaleBytes]]]: +) -> tuple[int, list[NeuronInfoLite]]: """ Retrieves all neurons for a specified netuid @@ -1038,25 +1037,13 @@ async def _fetch_neuron_for_netuid( :return: the original netuid, and a mapping of the neurons to their NeuronInfoLite objects """ - - async def neurons_lite_for_uid(uid: int) -> dict[Any, Any]: - block_hash = subtensor.substrate.last_block_hash - hex_bytes_result = await subtensor.query_runtime_api( - runtime_api="NeuronInfoRuntimeApi", - method="get_neurons_lite", - params=[uid], - block_hash=block_hash, - ) - - return hex_bytes_result - - neurons = await neurons_lite_for_uid(uid=netuid) + neurons = await subtensor.neurons_lite(netuid=netuid) return netuid, neurons async def _fetch_all_neurons( netuids: list[int], subtensor -) -> list[tuple[int, list[ScaleBytes]]]: +) -> list[tuple[int, list[NeuronInfoLite]]]: """Retrieves all neurons for each of the specified netuids""" return list( await asyncio.gather( @@ -1065,31 +1052,13 @@ async def _fetch_all_neurons( ) -def _process_neurons_for_netuids( - netuids_with_all_neurons_hex_bytes: list[tuple[int, list[ScaleBytes]]], -) -> list[tuple[int, list[NeuronInfoLite]]]: - """ - Using multiprocessing to decode a list of hex-bytes neurons with their respective netuid - - :param netuids_with_all_neurons_hex_bytes: netuids with hex-bytes neurons - :return: netuids mapped to decoded neurons - """ - all_results = [ - (netuid, NeuronInfoLite.list_from_vec_u8(bytes.fromhex(result[2:]))) - for netuid, result in netuids_with_all_neurons_hex_bytes - ] - return all_results - - async def _get_neurons_for_netuids( subtensor: SubtensorInterface, netuids: list[int], hot_wallets: list[str] ) -> list[tuple[int, list["NeuronInfoLite"], Optional[str]]]: - all_neurons_hex_bytes = await _fetch_all_neurons(netuids, subtensor) - - all_processed_neurons = _process_neurons_for_netuids(all_neurons_hex_bytes) + all_neurons = await _fetch_all_neurons(netuids, subtensor) return [ _map_hotkey_to_neurons(neurons, hot_wallets, netuid) - for netuid, neurons in all_processed_neurons + for netuid, neurons in all_neurons ] @@ -1211,7 +1180,7 @@ def neuron_row_maker( all_delegates: list[list[tuple[DelegateInfo, Balance]]] with console.status("Pulling balance data...", spinner="aesthetic"): balances, all_neurons, all_delegates = await asyncio.gather( - subtensor.get_balance( + subtensor.get_balances( *[w.coldkeypub.ss58_address for w in wallets_with_ckp_file], block_hash=block_hash, ), @@ -1327,9 +1296,10 @@ async def set_id( name: str, web_url: str, image_url: str, - discord_handle: str, + discord: str, description: str, - additional_info: str, + additional: str, + github_repo: str, prompt: bool, ): """Create a new or update existing identity on-chain.""" @@ -1338,9 +1308,10 @@ async def set_id( "name": name.encode(), "url": web_url.encode(), "image": image_url.encode(), - "discord": discord_handle.encode(), + "discord": discord.encode(), "description": description.encode(), - "additional": additional_info.encode(), + "additional": additional.encode(), + "github_repo": github_repo.encode(), } for field, value in identity_data.items(): @@ -1408,21 +1379,21 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): - arbitration_check = len( + arbitration_check = len( # TODO verify this works ( - await subtensor.substrate.query( + await subtensor.query( module="SubtensorModule", storage_function="ColdkeySwapDestinations", params=[wallet.coldkeypub.ss58_address], ) - ).decode() + ) ) if arbitration_check == 0: console.print( "[green]There has been no previous key swap initiated for your coldkey.[/green]" ) elif arbitration_check == 1: - arbitration_block = await subtensor.substrate.query( + arbitration_block = await subtensor.query( module="SubtensorModule", storage_function="ColdkeyArbitrationBlock", params=[wallet.coldkeypub.ss58_address], diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index cc5a7d379..df3815827 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -7,7 +7,7 @@ import numpy as np from numpy.typing import NDArray from rich.prompt import Confirm -from substrateinterface.exceptions import SubstrateRequestException +from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.bittensor.utils import err_console, console, format_error_message from bittensor_cli.src.bittensor.extrinsics.root import ( @@ -148,7 +148,7 @@ async def _commit_reveal( ) -> tuple[bool, str]: interval = int( await self.subtensor.get_hyperparameter( - param_name="get_commit_reveal_weights_interval", + param_name="get_commit_reveal_period", netuid=self.netuid, reuse_block=False, ) diff --git a/requirements.txt b/requirements.txt index c98676e6a..9d33a8679 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ wheel async-property==0.2.2 +async-substrate-interface==1.0.0rc10 aiohttp~=3.10.2 backoff~=2.2.1 GitPython>=3.0.0 @@ -13,11 +14,9 @@ pytest python-Levenshtein rich~=13.7 scalecodec==1.2.11 -substrate-interface~=1.7.9 typer~=0.12 websockets>=14.1 -bittensor-wallet>=2.0.2 -bt-decode==0.4.0 +bittensor-wallet>=3.0.2 plotille pywry -plotly +plotly \ No newline at end of file diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 5dcd9afb6..aaf8dde8b 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -8,9 +8,7 @@ import time import pytest -from bittensor_cli.src.bittensor.async_substrate_interface import ( - AsyncSubstrateInterface, -) +from async_substrate_interface.async_substrate import AsyncSubstrateInterface from .utils import setup_wallet @@ -20,7 +18,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 @@ -58,7 +56,7 @@ def wait_for_node_start(process, pattern): wait_for_node_start(process, pattern) # Run the test, passing in substrate interface - yield AsyncSubstrateInterface(chain_endpoint="ws://127.0.0.1:9945") + yield AsyncSubstrateInterface(url="ws://127.0.0.1:9945") # Terminate the process group (includes all child processes) os.killpg(os.getpgid(process.pid), signal.SIGTERM) diff --git a/tests/e2e_tests/test_root.py b/tests/e2e_tests/test_root.py index 1e87dbfa8..5a7674bb9 100644 --- a/tests/e2e_tests/test_root.py +++ b/tests/e2e_tests/test_root.py @@ -1,6 +1,7 @@ import time from bittensor_cli.src.bittensor.balances import Balance +import pytest from .utils import extract_coldkey_balance """ @@ -15,7 +16,7 @@ * btcli root undelegate-stake """ - +@pytest.mark.skip(reason="Root no longer applicable. We will update this.") def test_root_commands(local_chain, wallet_setup): """ Test the root commands and inspects their output diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index f335ada53..a0c53d759 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -44,7 +44,7 @@ def test_senate(local_chain, wallet_setup): # Fetch existing senate list root_senate = exec_command_bob( - command="root", + command="sudo", sub_command="senate", extra_args=[ "--network", @@ -58,9 +58,11 @@ def test_senate(local_chain, wallet_setup): # Register Bob to the root network (0) # Registering to root automatically makes you a senator if eligible root_register = exec_command_bob( - command="root", + command="subnets", sub_command="register", extra_args=[ + "--netuid", + "0", "--wallet-path", wallet_path_bob, "--network", @@ -76,7 +78,7 @@ def test_senate(local_chain, wallet_setup): # Fetch the senate members after registering to root root_senate_after_reg = exec_command_bob( - command="root", + command="sudo", sub_command="senate", extra_args=[ "--chain", @@ -93,7 +95,7 @@ def test_senate(local_chain, wallet_setup): # Fetch proposals after adding one proposals = exec_command_bob( - command="root", + command="sudo", sub_command="proposals", extra_args=[ "--chain", @@ -117,7 +119,7 @@ def test_senate(local_chain, wallet_setup): # Vote on the proposal by Bob (vote aye) vote_aye = exec_command_bob( - command="root", + command="sudo", sub_command="senate-vote", extra_args=[ "--wallet-path", @@ -138,7 +140,7 @@ def test_senate(local_chain, wallet_setup): # Fetch proposals after voting aye proposals_after_aye = exec_command_bob( - command="root", + command="sudo", sub_command="proposals", extra_args=[ "--chain", @@ -160,9 +162,11 @@ def test_senate(local_chain, wallet_setup): # Register Alice to the root network (0) # Registering to root automatically makes you a senator if eligible root_register = exec_command_alice( - command="root", + command="subnets", sub_command="register", extra_args=[ + "--netuid", + "0", "--wallet-path", wallet_path_alice, "--chain", @@ -178,7 +182,7 @@ def test_senate(local_chain, wallet_setup): # Vote on the proposal by Alice (vote nay) vote_nay = exec_command_alice( - command="root", + command="sudo", sub_command="senate-vote", extra_args=[ "--wallet-path", @@ -199,7 +203,7 @@ def test_senate(local_chain, wallet_setup): # Fetch proposals after voting proposals_after_nay = exec_command_bob( - command="root", + command="sudo", sub_command="proposals", extra_args=[ "--chain", diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 16276486a..abb3fe5d8 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -32,7 +32,7 @@ def test_staking(local_chain, wallet_setup): AssertionError: If any of the checks or verifications fail """ print("Testing staking and sudo commands🧪") - netuid = 1 + netuid = 2 wallet_path_alice = "//Alice" # Create wallet for Alice @@ -47,11 +47,19 @@ def test_staking(local_chain, wallet_setup): extra_args=[ "--wallet-path", wallet_path_alice, + "--wallet-hotkey", + wallet_alice.hotkey_str, "--chain", "ws://127.0.0.1:9945", "--wallet-name", wallet_alice.name, "--no-prompt", + "--subnet-name", + "test-subnet", + "--github-repo", + "https://github.com/bittensor/bittensor", + "--subnet-contact", + "test@test.com", ], ) assert f"✅ Registered subnetwork with netuid: {netuid}" in result.stdout @@ -74,13 +82,15 @@ def test_staking(local_chain, wallet_setup): "--no-prompt", ], ) - assert "✅ Registered" in register_subnet.stdout + assert "✅ Already Registered" in register_subnet.stdout # Add stake to Alice's hotkey add_stake = exec_command_alice( command="stake", sub_command="add", extra_args=[ + "--netuid", + netuid, "--wallet-path", wallet_path_alice, "--wallet-name", @@ -99,7 +109,7 @@ def test_staking(local_chain, wallet_setup): # Execute stake show for Alice's wallet show_stake = exec_command_alice( command="stake", - sub_command="show", + sub_command="list", extra_args=[ "--wallet-path", wallet_path_alice, @@ -113,19 +123,16 @@ def test_staking(local_chain, wallet_setup): cleaned_stake = [ re.sub(r"\s+", " ", line) for line in show_stake.stdout.splitlines() ] - stake_added = cleaned_stake[6].split()[6].strip("τ") - assert Balance.from_tao(100) == Balance.from_tao(float(stake_added)) - - # TODO: Ask nucleus the rate limit and wait epoch - # Sleep 120 seconds for rate limiting when unstaking - print("Waiting for interval for 2 minutes") - time.sleep(120) + stake_added = cleaned_stake[9].split()[8] + assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(100) # Execute remove_stake command and remove all 100 TAO from Alice remove_stake = exec_command_alice( command="stake", sub_command="remove", extra_args=[ + "--netuid", + netuid, "--wallet-path", wallet_path_alice, "--wallet-name", @@ -155,10 +162,10 @@ def test_staking(local_chain, wallet_setup): # Parse all hyperparameters and single out max_burn in TAO all_hyperparams = hyperparams.stdout.splitlines() - max_burn_tao = all_hyperparams[22].split()[2] + max_burn_tao = all_hyperparams[22].split()[3] # Assert max_burn is 100 TAO from default - assert Balance.from_tao(float(max_burn_tao.strip("τ"))) == Balance.from_tao(100) + assert Balance.from_tao(float(max_burn_tao)) == Balance.from_tao(100) # Change max_burn hyperparameter to 10 TAO change_hyperparams = exec_command_alice( @@ -199,10 +206,8 @@ def test_staking(local_chain, wallet_setup): # Parse updated hyperparameters all_updated_hyperparams = updated_hyperparams.stdout.splitlines() - updated_max_burn_tao = all_updated_hyperparams[22].split()[2] + updated_max_burn_tao = all_updated_hyperparams[22].split()[3] # Assert max_burn is now 10 TAO - assert Balance.from_tao(float(updated_max_burn_tao.strip("τ"))) == Balance.from_tao( - 10 - ) + assert Balance.from_tao(float(updated_max_burn_tao)) == Balance.from_tao(10) print("✅ Passed staking and sudo commands") diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 9a3399870..ebad8ff46 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -10,9 +10,7 @@ from typer.testing import CliRunner if TYPE_CHECKING: - from bittensor_cli.src.bittensor.async_substrate_interface import ( - AsyncSubstrateInterface, - ) + from async_substrate_interface.async_substrate import AsyncSubstrateInterface template_path = os.getcwd() + "/neurons/" templates_repo = "templates repository"