diff --git a/bittensor/__init__.py b/bittensor/__init__.py index 8ec8019728..693c3b41ec 100644 --- a/bittensor/__init__.py +++ b/bittensor/__init__.py @@ -22,9 +22,6 @@ # Install and apply nest asyncio to allow the async functions # to run in a .ipynb -import nest_asyncio - -nest_asyncio.apply() # Bittensor code and protocol version. __version__ = "7.0.0" diff --git a/bittensor/commander/data.py b/bittensor/commander/data.py new file mode 100644 index 0000000000..a596bb6b64 --- /dev/null +++ b/bittensor/commander/data.py @@ -0,0 +1,81 @@ +# The MIT License (MIT) +# Copyright © 2024 Yuma Rao + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from typing import Optional + +from pydantic import BaseModel + +import bittensor + + +class Config: + netuid = 0 + wallet = None + initialized = False + json_encrypted_path = None + json_encrypted_pw = None + netuids = [] # TODO implement + coldkey_unlocked = False + + def __bool__(self): + return self.initialized + + def setup(self, conf: "ConfigBody"): + self.initialized = True + self.netuid = conf.netuid + self.wallet = bittensor.wallet( + name=conf.wallet["name"], + hotkey=conf.wallet["hotkey"], + path=conf.wallet["path"], + ) # maybe config + self.json_encrypted_path = conf.json_encrypted_path + self.json_encrypted_pw = conf.json_encrypted_pw + + def as_dict(self): + return { + "initialized": self.initialized, + "netuid": self.netuid, + "wallet": { + "name": self.wallet.name, + "hotkey": self.wallet.hotkey.ss58_address, + "path": self.wallet.path, + }, + "json_encrypted_path": self.json_encrypted_path, + "json_encrypted_pw": self.json_encrypted_pw, + } + + +class ConfigBody(BaseModel): + netuid: int = 0 + wallet: dict + json_encrypted_path: Optional[str] = None + json_encrypted_pw: Optional[str] = None + + +class Password(BaseModel): + # TODO maybe encrypt this? + password: str + + +class StakeAdd(BaseModel): + all_tokens: bool = False + # uid: int + amount: float = None + max_stake: float = None + hotkeys_to_use: list[str] = None + all_hotkeys: bool = False + excluded_hotkeys: list[str] = None diff --git a/bittensor/commander/server.py b/bittensor/commander/server.py new file mode 100644 index 0000000000..789b0c0142 --- /dev/null +++ b/bittensor/commander/server.py @@ -0,0 +1,356 @@ +# The MIT License (MIT) +# Copyright © 2024 Yuma Rao + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import asyncio +import time +from typing import Union, Optional, List + +from fastapi import FastAPI, HTTPException, Depends +from fastapi.responses import JSONResponse + +import bittensor +from bittensor.commands import ( + network, + metagraph, + register, + overview, + transfer, + inspect, + wallets, + identity, + senate, + delegates, + unstake, +) +from bittensor.commands import list as list_commands +from bittensor.commands import root as root_commands +from bittensor.commands import stake as stake_commands +from bittensor.commander import data + +# TODO app-wide error-handling + +sub = bittensor.subtensor("test") +app = FastAPI(debug=True) +config = data.Config() +event_loop = asyncio.get_event_loop() + + +async def check_config(): + if not config: + raise HTTPException(status_code=401, detail="Config missing") + + +async def is_cold_key_unlocked(): + if not config.initialized and not config.wallet.cold_key_unlocked: + raise HTTPException( + status_code=401, + detail="Cold key needs to be unlocked before performing this operation.", + ) + + +@app.post("/setup") +async def setup(conf: data.ConfigBody): + config.setup(conf) + return JSONResponse(status_code=200, content={"success": True}) + + +@app.get("/setup", dependencies=[Depends(check_config)]) +async def get_setup(): + return JSONResponse(status_code=200, content=config.as_dict()) + + +@app.post("/unlock-cold-key", dependencies=[Depends(check_config)]) +async def unlock_cold_key(password: data.Password): + result = await event_loop.run_in_executor( + None, config.wallet.unlock_coldkey, password.password + ) + if result is True: + config.wallet.cold_key_unlocked = True + return JSONResponse({"success": result}) + + +async def run_fn(command_class, params=None): + start = time.time() + try: + if hasattr(command_class, "commander_run"): + response_content = await command_class.commander_run( + sub, config=config, params=params + ) + print(command_class, time.time() - start) + return JSONResponse(content=response_content) + else: + raise HTTPException( + status_code=501, detail="Command implementation missing" + ) + except Exception as e: + raise + # raise HTTPException(status_code=500, detail=str(e)) + + +# Subnets ####################### +@app.get("/subnets/create", dependencies=[Depends(check_config)]) +async def subnets_create(set_identity: bool): + return await run_fn( + network.RegisterSubnetworkCommand, params={"set_identity": set_identity} + ) + + +@app.get("/subnets/{sub_cmd}", dependencies=[Depends(check_config)]) +async def get_subnet(sub_cmd: str): + routing_list = { + "list": network.SubnetListCommand, + "metagraph": metagraph.MetagraphCommand, + "lock_cost": network.SubnetLockCostCommand, + # "pow_register": register.PowRegisterCommand, # Not yet working + "register": register.RegisterCommand, + "hyperparameters": network.SubnetHyperparamsCommand, + } + return await run_fn(routing_list[sub_cmd]) + + +# Wallet ####################### +@app.get("/wallet/new_key/{key_type}", dependencies=[Depends(check_config)]) +async def wallet_new_key( + key_type: str, n_words: int, use_password: bool, overwrite: bool +): + routing_list = { + "hotkey": wallets.NewHotkeyCommand, + "coldkey": wallets.NewColdkeyCommand, + } + try: + return await run_fn( + routing_list[key_type], + params={ + "n_words": n_words, + "use_password": use_password, + "overwrite": overwrite, + }, + ) + except KeyError: + raise HTTPException(status_code=404, detail="Key type not found") + + +@app.get("/wallet/{key_type}/regen_key", dependencies=[Depends(check_config)]) +async def wallet_regen_key( + key_type: str, + mnemonic: Union[str, None], + seed: Union[str, None], + use_password: bool = False, + overwrite: bool = False, +): + routing_list = { + "hotkey": wallets.RegenHotkeyCommand, + "coldkey": wallets.RegenColdkeyCommand, + } + try: + return await run_fn( + routing_list[key_type], + params={ + "mnemonic": mnemonic, + "seed": seed, + "use_password": use_password, + "overwrite": overwrite, + }, + ) + except KeyError: + raise HTTPException(status_code=404, detail="Key type not found") + + +@app.get("/wallet/hotkey/swap", dependencies=[Depends(check_config)]) +async def wallet_hotkey_swap(): + return await run_fn(register.SwapHotkeyCommand) # TODO + + +@app.get("/wallet/coldkey/regen_coldkey_pub", dependencies=[Depends(check_config)]) +async def wallet_coldkey( + ss58_address: Optional[str], + public_key: Optional[ + str + ], # This differs from the CLI implementation that also allows bytes + overwrite: Optional[bool] = False, +): + return await run_fn( + wallets.RegenColdkeypubCommand, + params={ + "ss58_address": ss58_address, + "public_key": public_key, + "overwrite": overwrite, + }, + ) + + +@app.get("/wallet/overview", dependencies=[Depends(check_config)]) +async def wallet_overview( + all_coldkeys: bool = True, hotkeys: List[str] = None, all_hotkeys: bool = True +): + # Hotkeys is List[str] I think + return await run_fn( + overview.OverviewCommand, + params={ + "all_coldkeys": all_coldkeys, + "hotkeys": hotkeys, + "all_hotkeys": all_hotkeys, + }, + ) + + +@app.get("/wallet/transfer", dependencies=[Depends(check_config)]) +async def wallet_transfer(dest: str, amount: float): + # Be sure to unlock the key first, if the key is encrypted + return await run_fn( + transfer.TransferCommand, params={"dest": dest, "amount": amount} + ) + + +@app.get("/wallet/identity/get", dependencies=[Depends(check_config)]) +async def wallet_identity_get(key: str): + return await run_fn(identity.GetIdentityCommand, params={"key": key}) + + +@app.get( + "/wallet/identity/set", + dependencies=[Depends(check_config), Depends(is_cold_key_unlocked)], +) +async def wallet_identity_set( + operating_hotkey_identity: bool, + display: str = "", + legal: str = "", + web: str = "", + pgp_fingerprint: str = "", + riot: str = "", + email: str = "", + image: str = "", + twitter: str = "", + info: str = "", +): + return await run_fn( + identity.SetIdentityCommand, + params={ + "display": display, + "legal": legal, + "web": web, + "pgp_fingerprint": pgp_fingerprint, + "riot": riot, + "email": email, + "image": image, + "twitter": twitter, + "info": info, + "operating_hotkey_identity": operating_hotkey_identity, + }, + ) + + +@app.get("/wallet/{sub_cmd}", dependencies=[Depends(check_config)]) +async def wallet( + sub_cmd: str, + n_words: int = 12, + use_password: bool = False, + overwrite: bool = False, + all_wallets: bool = False, +): + routing_list = { + "list": list_commands.ListCommand, + "balance": wallets.WalletBalanceCommand, + "inspect": inspect.InspectCommand, + "create": wallets.WalletCreateCommand, + "faucet": register.RunFaucetCommand, + "update": wallets.UpdateWalletCommand, + "history": wallets.GetWalletHistoryCommand, + } + return await run_fn( + routing_list[sub_cmd], + params={ + "n_words": n_words, + "use_password": use_password, + "overwrite": overwrite, + "all_wallets": all_wallets, + }, + ) + + +# Root +@app.get( + "/root/nominate", + dependencies=[Depends(check_config), Depends(is_cold_key_unlocked)], +) +async def root_nominate(): + return await run_fn(delegates.NominateCommand) + + +@app.get("/root/weights/set", dependencies=[Depends(check_config)]) +async def root_weights(netuids: list[int], weights: list[float]): + return await run_fn( + root_commands.RootSetWeightsCommand, + params={"netuids": netuids, "weights": weights}, + ) + + +@app.get("/root/delegates/{sub_cmd}", dependencies=[Depends(check_config)]) +async def root_delegates(sub_cmd: str): + routing_list = { + "delegate": delegates.DelegateStakeCommand, + "undelegate": delegates.DelegateUnstakeCommand, + "my": delegates.MyDelegatesCommand, + "list": delegates.ListDelegatesCommand, + "list_lite": delegates.ListDelegatesLiteCommand, + } + + +@app.get("/root/{sub_cmd}", dependencies=[Depends(check_config)]) +async def root(sub_cmd: str, amount: int = 0): + routing_list = { + "list": root_commands.RootList, + "boost": root_commands.RootSetBoostCommand, + "slash": root_commands.RootSetSlashCommand, + "register": root_commands.RootRegisterCommand, + "proposals": senate.ProposalsCommand, + "weights": root_commands.RootGetWeightsCommand, + } + return await run_fn( + routing_list[sub_cmd], + params={ + "amount": amount, + }, + ) + + +# Sudo +@app.get("/sudo/get", dependencies=[Depends(check_config)]) +async def sudo_get(): + return await run_fn(network.SubnetGetHyperparamsCommand) + + +@app.get("/sudo/set", dependencies=[Depends(check_config)]) +async def sudo_set(param: str, value: Union[str, int, bool, float]): + return await run_fn( + network.SubnetSudoCommand, params={"param": param, "value": value} + ) + + +# Stake +@app.post("/stake/add", dependencies=[Depends(check_config)]) +async def stake_add(params: data.StakeAdd): + return await run_fn(stake_commands.StakeCommand, params=params.model_dump()) + + +@app.get("/stake/{sub_cmd}", dependencies=[Depends(check_config)]) +async def stake(sub_cmd: str, all_wallets: bool = False): + routing_list = { + "show": stake_commands.StakeShow, + "remove": unstake.UnStakeCommand, + } + return await run_fn(routing_list[sub_cmd], params={"all_wallets": all_wallets}) diff --git a/bittensor/commands/delegates.py b/bittensor/commands/delegates.py index 1fd475785c..69f39e98ee 100644 --- a/bittensor/commands/delegates.py +++ b/bittensor/commands/delegates.py @@ -18,7 +18,7 @@ import argparse import os import sys -from typing import List, Dict, Optional +from typing import List, Dict, Optional, Union from rich.console import Text from rich.prompt import Prompt, FloatPrompt, Confirm @@ -820,6 +820,31 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params + ) -> dict[str, Union[bool, str]]: + if subtensor.is_hotkey_delegate(config.wallet.hotkey.ss58_address): + return { + "Success": False, + "error": f"Hotkey {config.wallet.hotkey.ss58_address} is already a delegate.", + } + if not subtensor.nominate(config.wallet): + return { + "Success": False, + "error": f"Could not became a delegate on {subtensor.network}", + } + if not subtensor.is_hotkey_delegate(config.wallet.hotkey.ss58_address): + return { + "Success": False, + "error": f"Could not became a delegate on {subtensor.network}", + } + else: + return { + "Success": True, + "msg": "Subnetwork registered successfully. You can set an identity with the identity set command.", + } + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Nominate wallet.""" diff --git a/bittensor/commands/identity.py b/bittensor/commands/identity.py index 15232c4440..2210fc239e 100644 --- a/bittensor/commands/identity.py +++ b/bittensor/commands/identity.py @@ -1,4 +1,7 @@ import argparse +import asyncio +from typing import Any + from rich.table import Table from rich.prompt import Prompt from sys import getsizeof @@ -55,6 +58,7 @@ class SetIdentityCommand: part of other scripts or applications. """ + @staticmethod def run(cli: "bittensor.cli"): r"""Create a new or update existing identity on-chain.""" try: @@ -67,6 +71,34 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params: dict + ) -> dict[str, Any]: + for field, string in params.items(): + if getsizeof(string) > 113: # 64 + 49 overhead bytes for string + raise ValueError(f"Identity value `{field}` must be <= 64 raw bytes") + identified = ( + config.wallet.hotkey.ss58_address + if params.pop("operating_hotkey_identity") + else None + ) + # TODO forewarn in the frontend that the cost to register an identity is 0.1 TAO + try: + subtensor.update_identity( + identified=identified, + wallet=config.wallet, + params=params, + ) + except Exception as e: + return {"success": False, "error": str(e)} + + identity = subtensor.query_identity( + identified or config.wallet.coldkey.ss58_address + ) + return {"success": True, "identity": identity} + + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Create a new or update existing identity on-chain.""" console = bittensor.__console__ @@ -272,6 +304,7 @@ class GetIdentityCommand: primarily used for informational purposes and has no side effects on the network state. """ + @staticmethod def run(cli: "bittensor.cli"): r"""Queries the subtensor chain for user identity.""" try: @@ -284,6 +317,14 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params=None): + identity = asyncio.get_event_loop().run_in_executor( + None, subtensor.query_identity, params.get("key") + ) + return await identity + + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): console = bittensor.__console__ diff --git a/bittensor/commands/inspect.py b/bittensor/commands/inspect.py index 76b015b774..c6f12be865 100644 --- a/bittensor/commands/inspect.py +++ b/bittensor/commands/inspect.py @@ -16,7 +16,9 @@ # DEALINGS IN THE SOFTWARE. import argparse +import asyncio import bittensor +from dataclasses import dataclass, asdict from tqdm import tqdm from rich.table import Table from rich.prompt import Prompt @@ -26,6 +28,7 @@ get_hotkey_wallets_for_wallet, get_all_wallets_for_path, filter_netuids_by_registered_hotkeys, + filter_netuids_by_registered_hotkeys_using_config, ) from . import defaults @@ -33,7 +36,7 @@ import os import bittensor -from typing import List, Tuple, Optional, Dict +from typing import List, Tuple, Optional, Dict, Any def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]: @@ -123,6 +126,42 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params=None + ) -> List[Dict[str, Any]]: + wallets = ( + _get_coldkey_wallets_for_path(config.wallet.path) + if (all_wallets := params.get("all_wallets", False)) + else [bittensor.wallet(path=config.wallet.path, name=config.wallet.name)] + ) + all_hotkeys = ( + get_all_wallets_for_path(config.wallet.path) + if all_wallets + else [get_hotkey_wallets_for_wallet(wallets[0])] + ) + event_loop = asyncio.get_event_loop() + netuids = await event_loop.run_in_executor( + None, + filter_netuids_by_registered_hotkeys_using_config, + config, + subtensor, + (await event_loop.run_in_executor(None, subtensor.get_all_subnet_netuids)), + all_hotkeys, + ) + registered_delegate_info: Optional[Dict[str, DelegatesDetails]] = ( + get_delegates_details(url=bittensor.__delegates_details_url__) or {} + ) + neuron_state_dict = { + netuid: subtensor.neurons_lite(netuid) or [] for netuid in netuids + } + return [ + asdict(x) + for x in await wallet_processor( + wallets, subtensor, registered_delegate_info, netuids, neuron_state_dict + ) + ] + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): if cli.config.get("all", d=False) == True: @@ -277,3 +316,104 @@ def add_args(parser: argparse.ArgumentParser): bittensor.wallet.add_args(inspect_parser) bittensor.subtensor.add_args(inspect_parser) + + +@dataclass +class WalletInspection: + name: str + balance: dict + delegates: List["Delegate"] + neurons: List["Neuron"] + + +@dataclass +class Delegate: + delegate: str + stake: dict + emission: dict + + +@dataclass +class Neuron: + netuid: int + hotkey: str + stake: dict + emission: dict + + +def map_delegate(delegate_staked, registered_delegate_info): + delegate, staked_ = delegate_staked + delegate_name_ = registered_delegate_info.get( + delegate.hotkey_ss58, delegate.hotkey_ss58 + ).name + return Delegate( + delegate=delegate_name_, + stake=staked_.to_dict(), + emission=( + delegate.total_daily_return.tao * (staked_.tao / delegate.total_stake.tao) + ).to_dict(), + ) + + +def create_neuron(netuid, neuron, hotkeys, wallet): + if neuron.coldkey == wallet.coldkeypub.ss58_address: + hotkey_names = [ + wall.hotkey_str + for wall in hotkeys + if wall.hotkey.ss58_address == neuron.hotkey + ] + hotkey_name = f"{hotkey_names[0]}-" if hotkey_names else "" + return Neuron( + netuid=netuid, + hotkey=f"{hotkey_name}{neuron.hotkey}", + stake=neuron.stake.to_dict(), + emission=bittensor.Balance.from_tao(neuron.emission).to_dict(), + ) + + +async def wallet_processor( + wallets, + subtensor: "bittensor.subtensor", + registered_delegate_info, + netuids, + neuron_state_dict, +) -> List[WalletInspection]: + async def map_wallet(wall): + if not wall.coldkeypub_file.exists_on_device(): + return + # Note: running these concurrently breaks this. Need to redo the subtensor lib for this to work properly + # Ideally, this would be asyncio.gather... + delegates: List[ + Tuple[bittensor.DelegateInfo, bittensor.Balance] + ] = await event_loop.run_in_executor( + None, + lambda: subtensor.get_delegated(coldkey_ss58=wall.coldkeypub.ss58_address), + ) + cold_balance = await event_loop.run_in_executor( + None, subtensor.get_balance, wall.coldkeypub.ss58_address + ) + hotkeys = _get_hotkey_wallets_for_wallet(wall) + wallet_ = WalletInspection( + name=wall.name, + balance=cold_balance.to_dict(), + delegates=[ + map_delegate(x, registered_delegate_info=registered_delegate_info) + for x in delegates + ], + neurons=[ + neuron + for neuron in ( + create_neuron(netuid, neuron_, hotkeys, wall) + for netuid in netuids + for neuron_ in neuron_state_dict[netuid] + ) + if neuron + ], + ) + return wallet_ + + event_loop = asyncio.get_event_loop() + # This should work but like in line 384, it does not. Subtensor needs fully ported + # to asyncio before this can work + # return list(await asyncio.gather(*[map_wallet(x) for x in wallets])) + return [(await map_wallet(x)) for x in wallets] diff --git a/bittensor/commands/list.py b/bittensor/commands/list.py index 6079112ed1..4f8c01ef56 100644 --- a/bittensor/commands/list.py +++ b/bittensor/commands/list.py @@ -15,6 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import asyncio import os import argparse import bittensor @@ -102,6 +103,51 @@ def run(cli): # Uses rich print to display the tree. print(root) + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params=None + ) -> dict: + async def wallet_structure(wallet_name) -> dict: + async def get_hotkeys(hotkey_name) -> dict: + hotkey_for_name = bittensor.wallet( + path=config.wallet.path, name=wallet_name, hotkey=hotkey_name + ) + if ( + hotkey_for_name.hotkey_file.exists_on_device() + and not hotkey_for_name.hotkey_file.is_encrypted() + ): + hotkey_str = hotkey_for_name.hotkey.ss58_address + else: + hotkey_str = "?" + return {"name": hotkey_name, "address": hotkey_str} + + wallet_for_name = bittensor.wallet( + path=config.wallet.path, name=wallet_name + ) + coldkeypub_str = ( + wallet_for_name.coldkeypub.ss58_address + if wallet_for_name.coldkeypub_file.exists_on_device() + and not wallet_for_name.coldkeypub_file.is_encrypted() + else "?" + ) + hotkeys_path = os.path.join( + os.path.expanduser(config.wallet.path), wallet_name, "hotkeys" + ) + hotkeys = [(await get_hotkeys(h)) for h in os.listdir(hotkeys_path)] + return {wallet_name: {"coldkeypub": coldkeypub_str, "hotkeys": hotkeys}} + + wallets = await asyncio.get_event_loop().run_in_executor( + None, + lambda: [ + d + for d in os.listdir(os.path.expanduser(config.wallet.path)) + if os.path.isdir( + os.path.join(os.path.expanduser(config.wallet.path), d) + ) + ], + ) + return {"wallets": [await wallet_structure(w) for w in wallets]} + @staticmethod def check_config(config: "bittensor.config"): pass diff --git a/bittensor/commands/metagraph.py b/bittensor/commands/metagraph.py index b6999fe553..0ce6b0dd29 100644 --- a/bittensor/commands/metagraph.py +++ b/bittensor/commands/metagraph.py @@ -16,8 +16,14 @@ # DEALINGS IN THE SOFTWARE. import argparse +import asyncio +from collections import namedtuple +from functools import reduce + + import bittensor from rich.table import Table + from .utils import check_netuid_set console = bittensor.__console__ # type: ignore @@ -82,6 +88,84 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params=None + ) -> dict: # find out what netuid is + def reducer(x, uid): + ep = metagraph.axons[uid] + new_row = [ + str(metagraph.neurons[uid].uid), + "{:.5f}".format(metagraph.total_stake[uid]), + "{:.5f}".format(metagraph.ranks[uid]), + "{:.5f}".format(metagraph.trust[uid]), + "{:.5f}".format(metagraph.consensus[uid]), + "{:.5f}".format(metagraph.incentive[uid]), + "{:.5f}".format(metagraph.dividends[uid]), + "{}".format(int(metagraph.emission[uid] * 1000000000)), + "{:.5f}".format(metagraph.validator_trust[uid]), + "*" if metagraph.validator_permit[uid] else "", + str((metagraph.block.item() - metagraph.last_update[uid].item())), + str(metagraph.active[uid].item()), + ( + ep.ip + ":" + str(ep.port) + if ep.is_serving + else "[yellow]none[/yellow]" + ), + ep.hotkey[:10], + ep.coldkey[:10], + ] + return [ + x[0] + [new_row] if x[0] else [new_row], # rows + x[1] + metagraph.total_stake[uid], # total_stake + x[2] + metagraph.ranks[uid], # total_rank + x[3] + metagraph.validator_trust[uid], # total_validator_trust + x[4] + metagraph.trust[uid], # total_trust + x[5] + metagraph.consensus[uid], # total_consensus + x[6] + metagraph.incentive[uid], # total_incentive + x[7] + metagraph.dividends[uid], # total_dividends + x[8] + int(metagraph.emission[uid] * 1000000000), # total_emission + ] + + netuid = config.netuid + metagraph: bittensor.metagraph = await asyncio.get_event_loop().run_in_executor( + None, subtensor.metagraph, netuid + ) + metagraph.save() + TableData = namedtuple( + "TableData", + [ + "rows", + "total_stake", + "total_rank", + "total_validator_trust", + "total_trust", + "total_consensus", + "total_incentive", + "total_dividends", + "total_emission", + "total_neurons", + "difficulty", + "subnet_emission", + "total_issuance", + ], + ) + table = TableData( + *( + reduce( + reducer, metagraph.uids, [[], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0] + ) + ), + len(metagraph.uids), + subtensor.difficulty(netuid), + bittensor.Balance.from_tao( + subtensor.get_emission_value_by_subnet(netuid) + ).to_dict(), + bittensor.Balance.from_rao(subtensor.total_issuance().rao).to_dict() + ) + return table._asdict() + + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Prints an entire metagraph.""" console = bittensor.__console__ diff --git a/bittensor/commands/network.py b/bittensor/commands/network.py index f20eac67a6..d3eff1a582 100644 --- a/bittensor/commands/network.py +++ b/bittensor/commands/network.py @@ -15,6 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import asyncio import argparse import bittensor from . import defaults @@ -72,6 +73,19 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params=None): + success = subtensor.register_subnetwork(wallet=config.wallet, prompt=False) + # todo investigate why this isn't working + if success: + # todo params + if params.get("set_identity"): + subtensor.close() + # TODO set identity when set_identity is true + return {"success": True} + else: + return {"success": False} + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Register a subnetwork""" @@ -159,13 +173,21 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params=None): + return bittensor.utils.balance.Balance( + await asyncio.get_event_loop().run_in_executor( + None, subtensor.get_subnet_burn_cost + ) + ).to_dict() + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""View locking cost of creating a new subnetwork""" config = cli.config.copy() try: bittensor.__console__.print( - f"Subnet lock cost: [green]{bittensor.utils.balance.Balance( subtensor.get_subnet_burn_cost() )}[/green]" + f"Subnet lock cost: [green]{bittensor.utils.balance.Balance(subtensor.get_subnet_burn_cost())}[/green]" ) except Exception as e: bittensor.__console__.print( @@ -235,6 +257,38 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params=None + ) -> List[List[str]]: + subnets: List[bittensor.SubnetInfo] + delegate_info: Optional[Dict[str, DelegatesDetails]] + event_loop = asyncio.get_event_loop() + subnets, delegate_info = await asyncio.gather( + event_loop.run_in_executor(None, subtensor.get_all_subnets_info), + event_loop.run_in_executor( + None, + lambda: get_delegates_details(url=bittensor.__delegates_details_url__), + ), + ) + structure = [ + ["NETUID", "N", "MAX_N", "EMISSION", "TEMPO", "BURN", "POW", "SUDO"] + + [ + [ + str(s.netuid), + str(s.subnetwork_n), + bittensor.utils.formatting.millify(s.max_n), + f"{s.emission_value / bittensor.utils.RAOPERTAO * 100:0.2f}%", + str(s.tempo), + str(s.burn), + str(bittensor.utils.formatting.millify(s.difficulty)), + f"{delegate_info[s.owner_ss58].name if s.owner_ss58 in delegate_info else s.owner_ss58}", + ] + for s in subnets + ] + ] + return structure + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""List all subnet netuids in the network.""" @@ -363,6 +417,21 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + await SubnetHyperparamsCommand.commander_run(subtensor, config, params) + result = await asyncio.get_event_loop().run_in_executor( + None, + lambda: subtensor.set_hyperparameter( + config.wallet, + netuid=config.netuid, + parameter=params["param"], + value=params["value"], + prompt=False, + ), + ) + return {"Success": result} + @staticmethod def _run( cli: "bittensor.cli", @@ -472,6 +541,17 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params=None + ) -> dict: + subnet: bittensor.SubnetHyperparameters = ( + await asyncio.get_event_loop().run_in_executor( + None, subtensor.get_subnet_hyperparameters, config.netuid + ) + ) + return subnet.__dict__ + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""View hyperparameters of a subnetwork.""" @@ -576,6 +656,13 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + hyperparameters = await asyncio.get_event_loop().run_in_executor( + None, subtensor.get_subnet_hyperparameters, config.netuid + ) + return hyperparameters.__dict__ + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""View hyperparameters of a subnetwork.""" diff --git a/bittensor/commands/overview.py b/bittensor/commands/overview.py index 477ad9f01a..786f947d68 100644 --- a/bittensor/commands/overview.py +++ b/bittensor/commands/overview.py @@ -16,6 +16,11 @@ # DEALINGS IN THE SOFTWARE. import argparse +import asyncio +import collections +import functools +from itertools import chain + import bittensor from tqdm import tqdm from concurrent.futures import ProcessPoolExecutor @@ -24,7 +29,7 @@ from rich.align import Align from rich.table import Table from rich.prompt import Prompt -from typing import List, Optional, Dict, Tuple +from typing import List, Optional, Dict, Tuple, Any from .utils import ( get_hotkey_wallets_for_wallet, get_coldkey_wallets_for_path, @@ -91,6 +96,172 @@ def run(cli: "bittensor.cli"): bittensor.logging.debug("closing subtensor connection") @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params=None + ) -> Dict[str, Any]: + total_balance = bittensor.Balance(0) + if params.get("all_coldkeys"): + cold_wallets = get_coldkey_wallets_for_path(config.wallet.path) + for cold_wallet in cold_wallets: + if ( + cold_wallet.coldkeypub_file.exists_on_device() + and not cold_wallet.coldkeypub_file.is_encrypted() + ): + total_balance += subtensor.get_balance( + cold_wallet.coldkeypub.ss58_address + ) + all_hotkeys = get_all_wallets_for_path(config.wallet.path) + else: + coldkey_wallet = config.wallet + if ( + coldkey_wallet.coldkeypub_file.exists_on_device() + and not coldkey_wallet.coldkeypub_file.is_encrypted() + ): + total_balance += subtensor.get_balance( + coldkey_wallet.coldkeypub.ss58_address + ) + if not coldkey_wallet.coldkeypub_file.exists_on_device(): + return {"success": False, "error": "No coldkey wallet found."} + all_hotkeys = get_all_wallets_for_path(coldkey_wallet) + + hotkeys_: List[str] + if hotkeys_ := params.get("hotkeys"): + all_hotkeys = ( + [hotkey for hotkey in all_hotkeys if hotkey.hotkey_str in hotkeys_] + if not params.get("all_hotkeys") + else [ + hotkey + for hotkey in all_hotkeys + if hotkey.hotkey_str not in hotkeys_ + ] + ) + + if not all_hotkeys: + return {"success": False, "error": "No wallets found."} + + block = subtensor.block + event_loop = asyncio.get_event_loop() + cli = collections.namedtuple("cli", "config")(config) + netuids = filter_netuids_by_registered_hotkeys( + cli, + subtensor, + (await event_loop.run_in_executor(None, subtensor.get_all_subnet_netuids)), + all_hotkeys, + ) + neurons: Dict[str, List[bittensor.NeuronInfoLite]] = { + str(netuid): [] for netuid in netuids + } + + all_wallet_names = set([wallet.name for wallet in all_hotkeys]) + all_coldkey_wallets = [ + bittensor.wallet(name=wallet_name) for wallet_name in all_wallet_names + ] + hotkey_coldkey_to_hotkey_wallet = {} + for hotkey_wallet in all_hotkeys: + if hotkey_wallet.hotkey.ss58_address not in hotkey_coldkey_to_hotkey_wallet: + hotkey_coldkey_to_hotkey_wallet[hotkey_wallet.hotkey.ss58_address] = {} + + hotkey_coldkey_to_hotkey_wallet[hotkey_wallet.hotkey.ss58_address][ + hotkey_wallet.coldkeypub.ss58_address + ] = hotkey_wallet + all_hotkey_addresses = list(hotkey_coldkey_to_hotkey_wallet.keys()) + errors = [] + with ProcessPoolExecutor(max_workers=(max(len(netuids), 5))) as executor: + results = asyncio.gather( + *[ + event_loop.run_in_executor( + executor, + OverviewCommand._get_neurons_for_netuid, + config, + netuid, + all_hotkey_addresses, + ) + for netuid in netuids + ] + ) + for result in await results: + netuid, neurons_result, err_msg = result + if err_msg: + errors.append(err_msg) + if len(neurons_result) == 0: + netuids.remove(netuid) + del neurons[str(netuid)] + else: + neurons[str(netuid)] = neurons_result + total_coldkey_stake_from_metagraph = defaultdict(lambda: bittensor.Balance(0.0)) + checked_hotkeys = set() + for neuron in chain.from_iterable(neurons.values()): + if neuron.hotkey not in checked_hotkeys: + total_coldkey_stake_from_metagraph[neuron.coldkey] += neuron.stake_dict[ + neuron.coldkey + ] + checked_hotkeys.add(neuron.hotkey) + + def calculate_difference(coldkey_wallet_): + ss58_address = coldkey_wallet_.coldkeypub.ss58_address + total_stake_chain = subtensor.get_total_stake_for_coldkey( + ss58_address=ss58_address + ) + difference = ( + total_stake_chain - total_coldkey_stake_from_metagraph[ss58_address] + ) + if difference != 0: + return difference, coldkey_wallet + + alerts = list(filter(calculate_difference, all_coldkey_wallets)) + if alerts: + if "-1" not in neurons: + neurons["-1"] = [] + + with ProcessPoolExecutor(max_workers=max(len(alerts), 5)) as executor: + results = asyncio.gather( + *[ + event_loop.run_in_executor( + executor, + OverviewCommand._get_de_registered_stake_for_coldkey_wallet, + config, + all_hotkey_addresses, + coldkey_wallet, + ) + for coldkey_wallet in [x[1] for x in alerts] + ] + ) + for result in results: + coldkey_wallet, de_registered_stake, err_msg = result + if err_msg: + errors.append(err_msg) + if len(de_registered_stake) == 0: + continue + + de_registered_neurons = [] + for hotkey_addr, our_stake in de_registered_stake: + de_registered_neuron = bittensor.NeuronInfoLite._null_neuron() + de_registered_neuron.hotkey = hotkey_addr + de_registered_neuron.coldkey = ( + coldkey_wallet.coldkeypub.ss58_address + ) + de_registered_neuron.total_stake = bittensor.Balance(our_stake) + + de_registered_neurons.append(de_registered_neuron) + + # Add this hotkey to the wallets dict + wallet_ = bittensor.wallet( + name=config.wallet.name, + ) + wallet_.hotkey_ss58 = hotkey_addr + wallet_.hotkey_str = hotkey_addr[:5] # Max length of 5 characters + hotkey_coldkey_to_hotkey_wallet.get(hotkey_addr, {})[ + coldkey_wallet.coldkeypub.ss58_address + ] = wallet_ + + # Add neurons to overview. + neurons["-1"].extend(de_registered_neurons) + + processed_netuids = netuid_processor( + netuids, hotkey_coldkey_to_hotkey_wallet, subtensor, block, neurons + ) + return {"data": processed_netuids.as_dict(), "alerts": alerts} + def _get_total_balance( total_balance: "bittensor.Balance", subtensor: "bittensor.subtensor", @@ -331,6 +502,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): name=wallet, ) wallet_.hotkey_ss58 = hotkey_addr + # Unsure if this should be wallet or wallet_ wallet.hotkey_str = hotkey_addr[:5] # Max length of 5 characters # Indicates a hotkey not on local machine but exists in stake_info obj on-chain if hotkey_coldkey_to_hotkey_wallet.get(hotkey_addr) is None: @@ -776,3 +948,122 @@ def check_config(config: "bittensor.config"): config.netuids = [int(config.netuids)] else: config.netuids = [int(netuid) for netuid in config.netuids] + + +def netuid_processor( + netuids: List[int], + hotkey_coldkey_to_hotkey_wallet: Dict[str, dict], + subtensor: "bittensor.subtensor", + block, + neurons, +) -> "Neuron": + class Neuron: + def __init__( + self, + subnet_tempo_=None, + nn=None, + hotkeys_seen: set = None, + netuid: str = None, + ): + if not nn or not ( + hotwallet := hotkey_coldkey_to_hotkey_wallet.get(nn.hotkey, {}).get( + nn.coldkey, None + ) + ): + hotwallet = argparse.Namespace() + hotwallet.name = nn.coldkey[:7] if nn else None + self.hotwallet = hotwallet + self.rank = nn.rank if nn else 0.0 + self.trust = nn.trust if nn else 0.0 + self.consensus = nn.consensus if nn else 0.0 + self.dividends = nn.dividends if nn else 0.0 + self.validator_trust = nn.validator_trust if nn else 0.0 + self.subnet_tempo = subnet_tempo_ + self.emission = ( + int(nn.emission / (subnet_tempo_ + 1) * 1e9) + if (nn and subnet_tempo_) + else 0.0 + ) + self.rows = ( + [ + hotwallet.name, + hotwallet.hotkey_str, + str(nn.uid), + str(nn.active), + nn.stake, + nn.rank, + nn.trust, + nn.consensus, + nn.incentive, + nn.dividends, + self.emission, + nn.validator_trust, + nn.validator_permit, + int(block - nn.last_update), + f"{bittensor.utils.networking.int_to_ip(nn.axon_info.ip)}:{nn.axon_info.port}" + if nn.axon_info != 0 + else None, + nn.hotkey, + ] + if nn + else [] + ) + self.hotkey = nn.hotkey if nn else None + self.coldkey = nn.coldkey if nn else None + self.hotkeys_seen = hotkeys_seen if hotkeys_seen else set() + self.total_neurons = 0 + self.total_stake = nn.total_stake.tao if nn else 0.0 + self.netuid = netuid if netuid else None + + def __add__(self, other): + if isinstance(other, Neuron): + new_obj = Neuron( + subnet_tempo_=self.subnet_tempo, hotkeys_seen=self.hotkeys_seen + ) + if not (other.hotkey, other.coldkey) in self.hotkeys_seen: + # Don't double count stake on hotkey-coldkey pairs. + new_obj.hotkeys_seen.add((other.hotkey, other.coldkey)) + new_obj.total_stake += other.total_stake + # netuid -1 are neurons that are de-registered. + new_obj.total_neurons = ( + self.total_neurons + other.total_neurons + if other.netuid != "-1" + else self.total_neurons + ) + + new_obj.rank = self.rank + other.rank + new_obj.trust = self.trust + other.trust + new_obj.consensus = self.consensus + other.consensus + new_obj.dividends = self.dividends + other.dividends + new_obj.emission = self.emission + other.emission + new_obj.validator_trust = self.validator_trust + other.validator_trust + new_obj.rows = self.rows + [other.rows] + return new_obj + else: + raise NotImplemented("Neuron can only add to other Neurons.") + + def as_dict(self) -> dict: + return { + "total_stake": self.total_stake, + "total_neurons": self.total_neurons, + "rank": self.rank, + "trust": self.trust, + "consensus": self.consensus, + "dividends": self.dividends, + "emission": self.emission, + "validator_trust": self.validator_trust, + "rows": self.rows, + } + + def _netuid_processor(netuid) -> Neuron: + subnet_tempo = subtensor.tempo(netuid=netuid) + summed_neuron = functools.reduce( + lambda x, y: x + y, + [ + Neuron(nn=x, subnet_tempo_=subnet_tempo, netuid=netuid) + for x in neurons[str(netuid)] + ], + ) + return summed_neuron + + return functools.reduce(lambda x, y: x + _netuid_processor(y), netuids, Neuron()) diff --git a/bittensor/commands/register.py b/bittensor/commands/register.py index 8b21a33304..36d1b9246d 100644 --- a/bittensor/commands/register.py +++ b/bittensor/commands/register.py @@ -15,6 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import asyncio import sys import argparse import bittensor @@ -74,6 +75,40 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params=None): + if not subtensor.subnet_exists(netuid=config.netuid): + return {"success": False, "error": f"Subnet {config.netuid} does not exist"} + + event_loop = asyncio.get_event_loop() + # Check current recycle amount + current_recycle, balance = await asyncio.gather( + event_loop.run_in_executor( + None, lambda: subtensor.recycle(netuid=config.netuid) + ), + event_loop.run_in_executor( + None, + lambda: subtensor.get_balance( + address=config.wallet.coldkeypub.ss58_address + ), + ), + ) + # Check balance is sufficient + if balance < current_recycle: + return { + "success": False, + "error": f"Insufficient balance {balance} to register neuron. " + f"Current recycle is {current_recycle}", + } + # TODO get prompt working + await event_loop.run_in_executor( + None, + lambda: subtensor.burned_register( + wallet=config.wallet, netuid=config.netuid, prompt=False + ), + ) + return {"success": True} + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Register neuron by recycling some TAO.""" @@ -204,6 +239,28 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params=None): + # TODO figure out what tpb is + # This does not yet work + if not subtensor.subnet_exists(netuid=config.netuid): + return {"success": False, "msg": f"Subnet {config.netuid} does not exist"} + + registered = subtensor.register( + wallet=config.wallet, + netuid=config.netuid, + prompt=False, + tpb=config.get("tpb"), + update_interval=config.get("update_interval"), + num_processes=config.get( + "num_processes", None + ), # TODO look over these as they need to come from POW reg + cuda=config.cuda, + dev_id=config.dev_id + # TODO output in place and log verbose + ) + return {"success": True, "msg": registered} + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Register neuron.""" @@ -406,6 +463,27 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + # TODO make this work — maybe websockets? + success = subtensor.run_faucet( + wallet=config.wallet, + prompt=False, + tpb=cli.config.pow_register.cuda.get("tpb", None), + update_interval=cli.config.pow_register.get("update_interval", None), + num_processes=cli.config.pow_register.get("num_processes", None), + cuda=cli.config.pow_register.cuda.get( + "use_cuda", defaults.pow_register.cuda.use_cuda + ), + dev_id=cli.config.pow_register.cuda.get("dev_id", None), + output_in_place=cli.config.pow_register.get( + "output_in_place", defaults.pow_register.output_in_place + ), + log_verbose=cli.config.pow_register.get( + "verbose", defaults.pow_register.verbose + ), + ) + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Register neuron.""" @@ -534,6 +612,11 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params=None): + # TODO implement this + pass + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Swap your hotkey for all registered axons on the network.""" diff --git a/bittensor/commands/root.py b/bittensor/commands/root.py index a3658d03ea..3a41ba6136 100644 --- a/bittensor/commands/root.py +++ b/bittensor/commands/root.py @@ -16,14 +16,16 @@ # DEALINGS IN THE SOFTWARE. import re +from dataclasses import make_dataclass, asdict import typing import argparse import numpy as np import bittensor -from typing import List, Optional, Dict +from typing import List, Optional, Dict, Union from rich.prompt import Prompt from rich.table import Table from .utils import get_delegates_details, DelegatesDetails +from bittensor.utils import rpc_request from . import defaults @@ -65,6 +67,13 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params + ) -> dict[str, bool]: + result = subtensor.root_register(wallet=config.wallet, prompt=False) + return {"success": result} + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Register to root network.""" @@ -135,6 +144,48 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params: dict + ) -> dict[str, dict[str, typing.Union[str, float, bool]]]: + senate_members: list[str] = subtensor.get_senate_members() + root_neurons: typing.List[bittensor.NeuronInfoLite] = subtensor.neurons_lite( + netuid=0 + ) + delegate_info: Optional[Dict[str, DelegatesDetails]] = get_delegates_details( + bittensor.__delegates_details_url__ + ) + + # This would be massively sped up by implementation of asyncio in the substrate interface + + NeuronInfo = make_dataclass( + "NeuronInfo", + [ + ("uid", str), + ("name", str), + ("address", str), + ("stake", float), + ("senator", bool), + ], + ) + + total_stakes = await rpc_request.query_subtensor( + subtensor, [n.hotkey for n in root_neurons], "TotalHotkeyStake" + ) + neuron_data = { + str(n.uid): asdict( + NeuronInfo( + str(n.uid), + (delegate_info[n.hotkey].name if n.hotkey in delegate_info else ""), + n.hotkey, + bittensor.Balance.from_rao(total_stakes[n.hotkey].value).to_dict(), + bool(n.hotkey in senate_members), + ) + ) + for n in root_neurons + } + return neuron_data + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""List the root network""" @@ -278,6 +329,31 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + root = subtensor.metagraph(0, lite=False) + try: + my_uid = root.hotkeys.index(config.wallet.hotkey.ss58_address) + except ValueError: + raise ValueError( + f"Wallet hotkey: {config.wallet.hotkey} not found in root metagraph" + ) + my_weights = root.weights[my_uid] + prev_weight = my_weights[config.netuid] + new_weight = prev_weight + params["amount"] + my_weights[config.netuid] = new_weight + all_netuids = np.arange(len(my_weights)) + result = subtensor.root_set_weights( + wallet=config.wallet, + netuids=all_netuids, + weights=my_weights, + version_key=0, + prompt=False, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + return {"success": result} + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Set weights for root network.""" @@ -396,6 +472,33 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params + ) -> dict[str, bool]: + root = subtensor.metagraph(0, lite=False) + try: + my_uid = root.hotkeys.index(config.wallet.hotkey.ss58_address) + except ValueError: + raise ValueError( + f"Wallet hotkey: {config.wallet.hotkey} not found in root metagraph" + ) + my_weights = root.weights[my_uid] + my_weights[config.netuid] -= params["amount"] + my_weights[my_weights < 0] = 0 # Ensure weights don't go negative + all_netuids = np.arange(len(my_weights)) + + result = subtensor.root_set_weights( + wallet=config.wallet, + netuids=all_netuids, + weights=my_weights, + version_key=0, + prompt=False, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + return {"success": result} + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): wallet = bittensor.wallet(config=cli.config) @@ -488,6 +591,21 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + netuids = np.array(params.get("netuids"), dtype=np.int64) + weights = np.array(params.get("weights"), dtype=np.float32) + result = subtensor.root_set_weights( + wallet=config.wallet, + netuids=netuids, + weights=weights, + version_key=0, + prompt=False, + wait_for_finalization=True, + wait_for_inclusion=True, + ) + return {"Success": result} + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Set weights for root network.""" @@ -506,10 +624,7 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): ", ".join( map( str, - [ - "{:.2f}".format(float(1 / len(subnets))) - for subnet in subnets - ][:3], + ["{:.2f}".format(float(1 / len(subnets))) for _ in subnets][:3], ) ) + " ..." @@ -602,6 +717,13 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + weights: list[tuple[int, list[tuple[int, int]]]] = subtensor.weights(0) + uid_to_weights, netuids = process_weights(weights) + rows = create_weight_rows(uid_to_weights, netuids) + return rows + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Get weights for root network.""" @@ -679,3 +801,39 @@ def add_args(parser: argparse.ArgumentParser): @staticmethod def check_config(config: "bittensor.config"): pass + + +def process_weights( + weights: list[tuple[int, list[tuple[int, int]]]] +) -> tuple[dict[int, dict], set[int]]: + netuids = set() + uid_to_weights = { + uid: { + netuid: normalized_weight + for (netuid, _), normalized_weight in zip( + weights_data, + np.array(weights_data)[:, 1] / max(np.sum(weights_data, axis=0)[1], 1), + ) + } + if weights_data + else {} + for uid, weights_data in weights + if not netuids.update(netuid for netuid, _ in weights_data) + } + return uid_to_weights, netuids + + +def create_weight_rows( + uid_to_weights: dict[int, dict], netuids: list[str] +) -> list[dict[str, Union[str, int]]]: + return [ + { + "uid": uid, + **{ + netuid: "{:0.2f}%".format(uid_to_weights[uid][netuid] * 100) + for netuid in netuids + if netuid in uid_to_weights[uid] + }, + } + for uid in uid_to_weights + ] diff --git a/bittensor/commands/senate.py b/bittensor/commands/senate.py index c92290af89..60dcc236bf 100644 --- a/bittensor/commands/senate.py +++ b/bittensor/commands/senate.py @@ -17,10 +17,13 @@ import argparse +import asyncio +import time + import bittensor from rich.prompt import Prompt, Confirm from rich.table import Table -from typing import Optional, Dict +from typing import Optional, Dict, Any, List from .utils import get_delegates_details, DelegatesDetails from . import defaults @@ -143,7 +146,8 @@ def format_call_data(call_data: "bittensor.ProposalCallData") -> str: def display_votes( - vote_data: "bittensor.ProposalVoteData", delegate_info: "bittensor.DelegateInfo" + vote_data: "bittensor.ProposalVoteData", + delegate_info: dict[str, "DelegatesDetails"], ) -> str: vote_list = list() @@ -198,6 +202,61 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params + ) -> list[dict[str, Any]]: + def votes( + vote_data, delegate_info: dict[str, "DelegatesDetails"] + ) -> dict[str, list[str]]: + ayes = [ + delegate_info[address].name if address in delegate_info else address + for address in vote_data["ayes"] + ] + nays = [ + delegate_info[address].name if address in delegate_info else address + for address in vote_data["nays"] + ] + return {"ayes": ayes, "nays": nays} + + def _format_call_data(call_data) -> dict[str, list[dict[str, Any]]]: + def format_arg(arg) -> dict[str, Any]: + arg_value = arg["value"] + if isinstance(arg_value, dict) and "call_function" in arg_value: + return { + arg["name"]: _format_call_data( + { + "call_function": arg_value["call_function"], + "call_args": arg_value["call_args"], + } + ) + } + return {arg["name"]: arg_value} + + human_call_data = [format_arg(arg) for arg in call_data["call_args"]] + + return {call_data["call_function"]: human_call_data} + + # could be asyncio.gather, if refactored rpc calls + senate_members = subtensor.get_senate_members() + proposals = subtensor.get_proposals() + registered_delegate_info: dict[str, DelegatesDetails] = get_delegates_details( + url=bittensor.__delegates_details_url__ + ) + table = [ + { + "hash": hash_, + "threshold": str(vote_data["threshold"]), + "ayes": len(vote_data["ayes"]), + "nays": len(vote_data["nays"]), + "votes": votes(vote_data, registered_delegate_info), + "end": str(vote_data["end"]), + "call_data": format_call_data(call_data), + } + for hash_, (call_data, vote_data) in proposals.items() + ] + return table + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""View Bittensor's governance protocol proposals""" diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index 8fe80b606a..45b1f177a0 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -16,6 +16,8 @@ # DEALINGS IN THE SOFTWARE. import argparse +import asyncio +from functools import reduce import os import sys from typing import List, Union, Optional, Dict, Tuple @@ -29,6 +31,7 @@ from .utils import ( get_hotkey_wallets_for_wallet, get_delegates_details, + a_get_delegates_details, DelegatesDetails, ) from . import defaults @@ -79,6 +82,118 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + a_run = asyncio.get_event_loop().run_in_executor + hotkeys_to_stake_to: List[Tuple[Optional[str], str]] = [] + if params.get("all_hotkeys"): + all_hotkeys: List[bittensor.wallet] = await a_run( + None, lambda: get_hotkey_wallets_for_wallet(wallet=config.wallet) + ) + hotkeys_to_exclude = params.get("excluded_hotkeys") + hotkeys_to_stake_to = [ + (wallet.hotkey_str, wallet.hotkey.ss58_address) + for wallet in all_hotkeys + if wallet.hotkey_str not in hotkeys_to_exclude + ] + elif htu := params.get("hotkeys_to_use"): + for hotkey_ss58_or_hotkey_name in htu: + if bittensor.utils.is_valid_ss58_address(hotkey_ss58_or_hotkey_name): + hotkeys_to_stake_to.append((None, hotkey_ss58_or_hotkey_name)) + else: + wallet_ = bittensor.wallet( + config=config, hotkey=hotkey_ss58_or_hotkey_name + ) + hotkeys_to_stake_to.append( + (wallet_.hotkey_str, wallet_.hotkey.ss58_address) + ) + elif wallet_hotkey := config.wallet.hotkey: + hotkey_ss58_or_name = wallet_hotkey + if bittensor.utils.is_valid_ss58_address(hotkey_ss58_or_name): + hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] + else: + wallet_ = bittensor.wallet(config=config, hotkey=hotkey_ss58_or_name) + hotkeys_to_stake_to = [ + (wallet_.hotkey_str, wallet_.hotkey.ss58_address) + ] + else: + assert wallet_hotkey is not None + hotkeys_to_stake_to = [ + (None, bittensor.wallet(config=config).hotkey.ss58_address) + ] + + wallet_balance: Balance = subtensor.get_balance( + config.wallet.coldkeypub.ss58_address + ) + final_hotkeys: List[Tuple[str, str]] = [] + final_amounts: List[Union[float, Balance]] = [] + for hotkey in hotkeys_to_stake_to: + hotkey: Tuple[Optional[str], str] + if not await a_run( + None, lambda: subtensor.is_hotkey_registered_any(hotkey_ss58=hotkey[1]) + ): + if len(hotkeys_to_stake_to) == 1: + return { + "Success": False, + "Error": f"Hotkey {hotkey[1]} is not registered", + } + else: + continue + stake_amount_tao: float = params.get("amount") + if max_stake := params.get("max_stake"): + hotkey_stake: Balance = await a_run( + None, + lambda: subtensor.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=hotkey[1], + coldkey_ss58=config.wallet.coldkeypub.ss58_address, + ), + ) + stake_amount_tao: float = min( + max_stake - hotkey_stake.tao, wallet_balance.tao + ) + if stake_amount_tao <= 0.000_01: + continue + wallet_balance = Balance.from_tao(wallet_balance.tao - stake_amount_tao) + if wallet_balance.tao < 0: + # No more balance to stake. + break + + final_amounts.append(stake_amount_tao) + final_hotkeys.append(hotkey) # add both the name and the ss58 address. + + if len(final_hotkeys) == 0: + return { + "Success": False, + "Error": "Not enough balance to stake to any hotkeys or max_stake is less than current stake.", + } + + if len(final_hotkeys) == 1: + # do regular stake + do_stake = await a_run( + None, + lambda: subtensor.add_stake( + wallet=config.wallet, + hotkey_ss58=final_hotkeys[0][1], + amount=None if params.get("all_tokens") else final_amounts[0], + wait_for_inclusion=True, + prompt=False, + ), + ) + return {"Success": do_stake} + + else: + do_stake = await a_run( + None, + lambda: subtensor.add_stake_multiple( + wallet=config.wallet, + hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], + amounts=None if params.get("all_tokens") else final_amounts, + wait_for_inclusion=True, + prompt=False, + ), + ) + return {"Success": do_stake} + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Stake token of amount to hotkey(s).""" @@ -331,6 +446,100 @@ def _get_hotkey_wallets_for_wallet(wallet) -> List["bittensor.wallet"]: return hotkey_wallets +def get_stakes_from_hotkeys( + subtensor, wallet +) -> Dict[str, Dict[str, Union[str, Balance]]]: + """Fetch stakes from hotkeys for the provided wallet. + + Args: + wallet: The wallet object to fetch the stakes for. + + Returns: + A dictionary of stakes related to hotkeys. + """ + hotkeys = get_hotkey_wallets_for_wallet(wallet) + stakes = {} + for hot in hotkeys: + emission = sum( + [ + n.emission + for n in subtensor.get_all_neurons_for_pubkey(hot.hotkey.ss58_address) + ] + ) + hotkey_stake = subtensor.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=hot.hotkey.ss58_address, + coldkey_ss58=wallet.coldkeypub.ss58_address, + ) + stakes[hot.hotkey.ss58_address] = { + "name": hot.hotkey_str, + "stake": hotkey_stake, + "rate": emission, + } + return stakes + + +def get_stakes_from_delegates( + subtensor, wallet, registered_delegate_info +) -> Dict[str, Dict[str, Union[str, Balance]]]: + """Fetch stakes from delegates for the provided wallet. + + Args: + wallet: The wallet object to fetch the stakes for. + + Returns: + A dictionary of stakes related to delegates. + """ + delegates = subtensor.get_delegated(coldkey_ss58=wallet.coldkeypub.ss58_address) + stakes = {} + for dele, staked in delegates: + for nom in dele.nominators: + if nom[0] == wallet.coldkeypub.ss58_address: + delegate_name = ( + registered_delegate_info[dele.hotkey_ss58].name + if dele.hotkey_ss58 in registered_delegate_info + else dele.hotkey_ss58 + ) + stakes[dele.hotkey_ss58] = { + "name": delegate_name, + "stake": nom[1], + "rate": dele.total_daily_return.tao + * (nom[1] / dele.total_stake.tao), + } + return stakes + + +def get_stake_accounts( + wallet, subtensor, registered_delegate_info +) -> Dict[str, Dict[str, Union[str, Balance]]]: + """Get stake account details for the given wallet. + + Args: + wallet: The wallet object to fetch the stake account details for. + + Returns: + A dictionary mapping SS58 addresses to their respective stake account details. + """ + + wallet_stake_accounts = {} + + # Get this wallet's coldkey balance. + cold_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) + + # Populate the stake accounts with local hotkeys data. + wallet_stake_accounts.update(get_stakes_from_hotkeys(subtensor, wallet)) + + # Populate the stake accounts with delegations data. + wallet_stake_accounts.update( + get_stakes_from_delegates(subtensor, wallet, registered_delegate_info) + ) + + return { + "name": wallet.name, + "balance": cold_balance, + "accounts": wallet_stake_accounts, + } + + class StakeShow: """ Executes the ``show`` command to list all stake accounts associated with a user's wallet on the Bittensor network. @@ -374,6 +583,66 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params + ) -> dict[ + str, + Union[ + float, + Balance, + list[dict[str, Union[str, dict[str, Union[int, float]], int]]], + ], + ]: + def accumulate_totals(totals, account): + tot_bal = totals[0] + account["balance"] + tot_stake = totals[1] + sum( + value["stake"] for value in account["accounts"].values() + ) + tot_rate = totals[2] + sum( + float(value["rate"]) for value in account["accounts"].values() + ) + rows_ = totals[3] + [ + { + "account": value["name"], + "stake": value["stake"].to_dict(), + "rate": value["rate"], + } + for value in account["accounts"].values() + ] + return tot_bal, tot_stake, tot_rate, rows_ + + wallets = ( + _get_coldkey_wallets_for_path(config.wallet.path) + if params.get("all_wallets") + else [config.wallet] + ) + registered_delegate_info: dict[ + str, DelegatesDetails + ] = await a_get_delegates_details(url=bittensor.__delegates_details_url__) + run = asyncio.get_event_loop().run_in_executor + accounts: list[dict[str, dict[str, Union[str, Balance]]]] = [ + ( + await run( + None, + get_stake_accounts, + wallet, + subtensor, + registered_delegate_info, + ) + ) + for wallet in wallets + ] + total_balance, total_stake, total_rate, rows = reduce( + accumulate_totals, accounts, (Balance(0), Balance(0), 0, []) + ) + return { + "total_balance": total_balance.to_dict(), + "total_stake": total_stake.to_dict(), + "total_rate": total_rate, + "accounts": rows, + } + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Show all stake accounts.""" @@ -385,99 +654,6 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): Dict[str, DelegatesDetails] ] = get_delegates_details(url=bittensor.__delegates_details_url__) - def get_stake_accounts( - wallet, subtensor - ) -> Dict[str, Dict[str, Union[str, Balance]]]: - """Get stake account details for the given wallet. - - Args: - wallet: The wallet object to fetch the stake account details for. - - Returns: - A dictionary mapping SS58 addresses to their respective stake account details. - """ - - wallet_stake_accounts = {} - - # Get this wallet's coldkey balance. - cold_balance = subtensor.get_balance(wallet.coldkeypub.ss58_address) - - # Populate the stake accounts with local hotkeys data. - wallet_stake_accounts.update(get_stakes_from_hotkeys(subtensor, wallet)) - - # Populate the stake accounts with delegations data. - wallet_stake_accounts.update(get_stakes_from_delegates(subtensor, wallet)) - - return { - "name": wallet.name, - "balance": cold_balance, - "accounts": wallet_stake_accounts, - } - - def get_stakes_from_hotkeys( - subtensor, wallet - ) -> Dict[str, Dict[str, Union[str, Balance]]]: - """Fetch stakes from hotkeys for the provided wallet. - - Args: - wallet: The wallet object to fetch the stakes for. - - Returns: - A dictionary of stakes related to hotkeys. - """ - hotkeys = get_hotkey_wallets_for_wallet(wallet) - stakes = {} - for hot in hotkeys: - emission = sum( - [ - n.emission - for n in subtensor.get_all_neurons_for_pubkey( - hot.hotkey.ss58_address - ) - ] - ) - hotkey_stake = subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hot.hotkey.ss58_address, - coldkey_ss58=wallet.coldkeypub.ss58_address, - ) - stakes[hot.hotkey.ss58_address] = { - "name": hot.hotkey_str, - "stake": hotkey_stake, - "rate": emission, - } - return stakes - - def get_stakes_from_delegates( - subtensor, wallet - ) -> Dict[str, Dict[str, Union[str, Balance]]]: - """Fetch stakes from delegates for the provided wallet. - - Args: - wallet: The wallet object to fetch the stakes for. - - Returns: - A dictionary of stakes related to delegates. - """ - delegates = subtensor.get_delegated( - coldkey_ss58=wallet.coldkeypub.ss58_address - ) - stakes = {} - for dele, staked in delegates: - for nom in dele.nominators: - if nom[0] == wallet.coldkeypub.ss58_address: - delegate_name = ( - registered_delegate_info[dele.hotkey_ss58].name - if dele.hotkey_ss58 in registered_delegate_info - else dele.hotkey_ss58 - ) - stakes[dele.hotkey_ss58] = { - "name": delegate_name, - "stake": nom[1], - "rate": dele.total_daily_return.tao - * (nom[1] / dele.total_stake.tao), - } - return stakes - def get_all_wallet_accounts( wallets, subtensor, @@ -495,7 +671,9 @@ def get_all_wallet_accounts( # Create a progress bar using tqdm with tqdm(total=len(wallets), desc="Fetching accounts", ncols=100) as pbar: for wallet in wallets: - accounts.append(get_stake_accounts(wallet, subtensor)) + accounts.append( + get_stake_accounts(wallet, subtensor, registered_delegate_info) + ) pbar.update() return accounts diff --git a/bittensor/commands/transfer.py b/bittensor/commands/transfer.py index 24c6e78402..15d0815d27 100644 --- a/bittensor/commands/transfer.py +++ b/bittensor/commands/transfer.py @@ -16,6 +16,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import asyncio import sys import argparse import bittensor @@ -62,6 +63,20 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params=None): + result = await asyncio.get_event_loop().run_in_executor( + None, + lambda: subtensor.transfer( + wallet=config.wallet, + dest=params["dest"], + amount=params["amount"], + wait_for_inclusion=True, + prompt=False, + ), + ) + return result + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): r"""Transfer token of amount to destination.""" diff --git a/bittensor/commands/utils.py b/bittensor/commands/utils.py index 4ea8fa3dd1..66bcd18c6f 100644 --- a/bittensor/commands/utils.py +++ b/bittensor/commands/utils.py @@ -15,6 +15,7 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import aiohttp import sys import os import bittensor @@ -173,8 +174,8 @@ def get_all_wallets_for_path(path: str) -> List["bittensor.wallet"]: return all_wallets -def filter_netuids_by_registered_hotkeys( - cli, subtensor, netuids, all_hotkeys +def filter_netuids_by_registered_hotkeys_using_config( + config, subtensor, netuids, all_hotkeys ) -> List[int]: netuids_with_registered_hotkeys = [] for wallet in all_hotkeys: @@ -184,16 +185,24 @@ def filter_netuids_by_registered_hotkeys( ) netuids_with_registered_hotkeys.extend(netuids_list) - if cli.config.netuids == None or cli.config.netuids == []: + if config.netuids is None or config.netuids == []: netuids = netuids_with_registered_hotkeys - elif cli.config.netuids != []: - netuids = [netuid for netuid in netuids if netuid in cli.config.netuids] + elif config.netuids: + netuids = [netuid for netuid in netuids if netuid in config.netuids] netuids.extend(netuids_with_registered_hotkeys) return list(set(netuids)) +def filter_netuids_by_registered_hotkeys( + cli, subtensor, netuids, all_hotkeys +) -> List[int]: + return filter_netuids_by_registered_hotkeys_using_config( + cli.config, subtensor, netuids, all_hotkeys + ) + + @dataclass class DelegatesDetails: name: str @@ -233,3 +242,22 @@ def get_delegates_details(url: str) -> Optional[Dict[str, DelegatesDetails]]: return _get_delegates_details_from_github(requests.get, url) except Exception: return None # Fail silently + + +async def a_get_delegates_details(url: str) -> dict[str, DelegatesDetails]: + try: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + all_delegates: Dict[str, Any] = await response.json( + content_type=None + ) + all_delegates_details = { + delegate_hotkey: DelegatesDetails.from_json(delegates_details) + for delegate_hotkey, delegates_details in all_delegates.items() + } + return all_delegates_details + else: + return {} + except aiohttp.ClientError as e: + raise ValueError(e) diff --git a/bittensor/commands/wallets.py b/bittensor/commands/wallets.py index 0f665db7e4..641e75c30f 100644 --- a/bittensor/commands/wallets.py +++ b/bittensor/commands/wallets.py @@ -16,17 +16,42 @@ # DEALINGS IN THE SOFTWARE. import argparse +import asyncio +from dataclasses import dataclass +import aiofiles import bittensor import os import sys from rich.prompt import Prompt, Confirm from rich.table import Table -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Union, Any from . import defaults import requests from ..utils import RAOPERTAO +BalancesDict = dict[str, "WalletBalance"] + + +class WalletBalance: + def __init__( + self, + coldkey_name: str, + free_balance: "bittensor.Balance", + staked_balance: "bittensor.Balance", + ): + self.coldkey_name = coldkey_name + self.free_balance = free_balance + self.staked_balance = staked_balance + + def to_dict(self): + return { + "coldkey_name": self.coldkey_name, + "free_balance": self.free_balance.to_dict(), + "staked_balance": self.staked_balance.to_dict(), + } + + class RegenColdkeyCommand: """ Executes the ``regen_coldkey`` command to regenerate a coldkey for a wallet on the Bittensor network. @@ -76,6 +101,23 @@ def run(cli): overwrite=cli.config.overwrite_coldkey, ) + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + kwargs = await check_json( + config, + { + "mnemonic": params.mnemonic, + "seed": params.seed, + "use_password": params.use_password, + "overwrite": params.overwrite, + }, + ) + # TODO I probably need to do something with this after regeneration. + await asyncio.get_event_loop().run_in_executor( + config.wallet.regenerate_hotkey(**kwargs) + ) + return {"success": True} + @staticmethod def check_config(config: "bittensor.config"): if not config.is_set("wallet.name") and not config.no_prompt: @@ -174,6 +216,7 @@ class RegenColdkeypubCommand: It is a recovery-focused utility that ensures continued access to wallet functionalities. """ + @staticmethod def run(cli): r"""Creates a new coldkeypub under this wallet.""" wallet = bittensor.wallet(config=cli.config) @@ -183,6 +226,18 @@ def run(cli): overwrite=cli.config.overwrite_coldkeypub, ) + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + asyncio.get_event_loop().run_in_executor( + None, + lambda: config.wallet.regen_coldkeypub( + ss58_address=params.ss58_address, + public_key=params.public_key_hex, + overwrite=params.overwrite, + ), + ) + return {"success": True} + @staticmethod def check_config(config: "bittensor.config"): if not config.is_set("wallet.name") and not config.no_prompt: @@ -281,7 +336,6 @@ def run(cli): # Password can be "", assume if None json_password = cli.config.get("json_password", "") - wallet.regenerate_hotkey( mnemonic=cli.config.mnemonic, seed=cli.config.seed, @@ -290,6 +344,23 @@ def run(cli): overwrite=cli.config.overwrite_hotkey, ) + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + kwargs = await check_json( + config, + { + "mnemonic": params.mnemonic, + "seed": params.seed, + "use_password": params.use_password, + "overwrite": params.overwrite, + }, + ) + # TODO I probably need to do something with this after regeneration. + await asyncio.get_event_loop().run_in_executor( + config.wallet.regenerate_hotkey(**kwargs) + ) + return {"success": True} + @staticmethod def check_config(config: "bittensor.config"): if not config.is_set("wallet.name") and not config.no_prompt: @@ -394,8 +465,9 @@ class NewHotkeyCommand: such as running multiple miners or separating operational roles within the network. """ + @staticmethod def run(cli): - """Creates a new hotke under this wallet.""" + """Creates a new hotkey under this wallet.""" wallet = bittensor.wallet(config=cli.config) wallet.create_new_hotkey( n_words=cli.config.n_words, @@ -403,6 +475,18 @@ def run(cli): overwrite=cli.config.overwrite_hotkey, ) + @staticmethod + async def commander_run(subtensor: "bittensor.subtensor", config, params): + asyncio.get_event_loop().run_in_executor( + None, + lambda: config.wallet.create_new_hotkey( + n_words=params.n_words, + use_password=params.use_password, + overwrite=params.overwrite_hotkey, + ), + ) + return {"success": True} + @staticmethod def check_config(config: "bittensor.config"): if not config.is_set("wallet.name") and not config.no_prompt: @@ -549,6 +633,7 @@ class WalletCreateCommand: It ensures a fresh start with new keys for secure and effective participation in the network. """ + @staticmethod def run(cli): r"""Creates a new coldkey and hotkey under this wallet.""" wallet = bittensor.wallet(config=cli.config) @@ -563,6 +648,31 @@ def run(cli): overwrite=cli.config.overwrite_hotkey, ) + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params=None + ) -> bool: + run_in_executor = asyncio.get_event_loop().run_in_executor + await asyncio.gather( + run_in_executor( + None, + lambda: config.wallet.create_new_coldkey( + n_words=params.get("n_words"), + use_password=params.get("use_password"), + overwrite=params.get("overwrite_coldkey"), + ), + ), + run_in_executor( + None, + lambda: config.wallet.create_new_hotkey( + n_words=params.get("n_words"), + use_password=params.get("use_password"), + overwrite=params.get("overwrite_hotkey"), + ), + ), + ) + return True + @staticmethod def check_config(config: "bittensor.config"): if not config.is_set("wallet.name") and not config.no_prompt: @@ -661,6 +771,15 @@ def run(cli): print("\n===== ", wallet, " =====") wallet.coldkey_file.check_and_update_encryption() + @staticmethod + async def commander_run(_, config, params) -> dict[str, bool]: + wallets = ( + _get_coldkey_wallets_for_path(config.wallet.path) + if params.get("all_wallets") + else [config.wallet] + ) + return {x.name: x.coldkey_file.check_and_update_encryption() for x in wallets} + @staticmethod def add_args(parser: argparse.ArgumentParser): update_wallet_parser = parser.add_parser( @@ -772,6 +891,22 @@ def run(cli: "bittensor.cli"): subtensor.close() bittensor.logging.debug("closing subtensor connection") + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params=None + ) -> dict[str, Union[dict, str, int, float]]: + try: + balances, total_free_balance, total_staked_balance = await get_balances( + subtensor, config, params + ) + return { + "wallets": {x: y.to_dict() for x, y in balances.items()}, + "total_free_balance": total_free_balance.to_dict(), + "total_staked_balance": total_staked_balance.to_dict(), + } + except bittensor.KeyFileError: + raise + @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): wallet = bittensor.wallet(config=cli.config) @@ -997,6 +1132,24 @@ def run(cli): bittensor.__console__.print(table) + @staticmethod + async def commander_run( + subtensor: "bittensor.subtensor", config, params + ) -> list[dict[str, Any]]: + transfers = get_wallet_transfers(config.wallet.get_coldkeypub().ss58_address) + return [ + { + "Id": i["id"], + "From": i["from"], + "To": i["to"], + "Amount": i["amount"].to_dict(), + "Extrinsic Id": str(i["extrinsicId"]), + "Block Number": i["blockNumber"], + "URL (taostats)": f"https://x.taostats.io/extrinsic/{i['blockNumber']}-{i['extrinsicId']}", + } + for i in transfers + ] + @staticmethod def add_args(parser: argparse.ArgumentParser): history_parser = parser.add_parser( @@ -1097,3 +1250,105 @@ def create_transfer_history_table(transfers): table.pad_edge = False table.width = None return table + + +async def check_json(config, kwargs: dict) -> dict: + if file_name := config.json_encrypted_path: + if not os.path.exists(file_name) or not os.path.isfile(file_name): + # TODO ensure server error handling for this + raise ValueError(f"File {file_name} does not exist") + with aiofiles.open(file_name, "r") as f: + return { + **kwargs, + **{"json": ((await f.read()), config.json_encrypted_pw)}, + } + return kwargs + + +async def get_balances( + subtensor, config, params +) -> tuple[BalancesDict, "bittensor.Balance", "bittensor.Balance"]: + """ + Gets the balances of all wallets requested + """ + + def fetch_balances( + coldkeys: list[str], + ) -> tuple[ + list["bittensor.Balance"], + list["bittensor.Balance"], + "bittensor.Balance", + "bittensor.Balance", + ]: + free_balances = [subtensor.get_balance(coldkey) for coldkey in coldkeys] + staked_balances = [ + subtensor.get_total_stake_for_coldkey(coldkey) for coldkey in coldkeys + ] + return free_balances, staked_balances, sum(free_balances), sum(staked_balances) + + def generate_balances_dict( + wallet_names: list[str], + coldkeys: list[str], + free_balances: list["bittensor.Balance"], + staked_balances: list["bittensor.Balance"], + ) -> BalancesDict: + return { + name: WalletBalance(coldkey, free, staked) + for name, coldkey, free, staked in sorted( + zip(wallet_names, coldkeys, free_balances, staked_balances) + ) + } + + def handle_all_wallets() -> ( + tuple[BalancesDict, "bittensor.Balance", "bittensor.Balance"] + ): + coldkeys, wallet_names = _get_coldkey_ss58_addresses_for_path( + config.wallet.path + ) + ( + free_balances, + staked_balances, + total_free_balance_, + total_staked_balance_, + ) = fetch_balances(coldkeys) + return ( + generate_balances_dict( + wallet_names, coldkeys, free_balances, staked_balances + ), + total_free_balance_, + total_staked_balance_, + ) + + def handle_single_wallet() -> ( + tuple[BalancesDict, "bittensor.Balance", "bittensor.Balance"] + ): + coldkey_wallet = config.wallet + if ( + coldkey_wallet.coldkeypub_file.exists_on_device() + and not coldkey_wallet.coldkeypub_file.is_encrypted() + ): + coldkeys = [coldkey_wallet.coldkeypub.ss58_address] + wallet_names = [coldkey_wallet.name] + ( + free_balances, + staked_balances, + total_free_balance_, + total_staked_balance_, + ) = fetch_balances(coldkeys) + return ( + generate_balances_dict( + wallet_names, coldkeys, free_balances, staked_balances + ), + total_free_balance_, + total_staked_balance_, + ) + + if not coldkey_wallet.coldkeypub_file.exists_on_device(): + raise bittensor.KeyFileError("File does not exist on device.") + + if params.get("all_wallets"): + balances, total_free_balance, total_staked_balance = handle_all_wallets() + else: + balances, total_free_balance, total_staked_balance = handle_single_wallet() + + return balances, total_free_balance, total_staked_balance diff --git a/bittensor/keyfile.py b/bittensor/keyfile.py index b5157cea4a..5706237e03 100644 --- a/bittensor/keyfile.py +++ b/bittensor/keyfile.py @@ -515,7 +515,7 @@ def _may_overwrite(self) -> bool: def check_and_update_encryption( self, print_result: bool = True, no_prompt: bool = False - ): + ) -> bool: """Check the version of keyfile and update if needed. Args: diff --git a/bittensor/utils/balance.py b/bittensor/utils/balance.py index 63ca6cd5ba..056592fe4b 100644 --- a/bittensor/utils/balance.py +++ b/bittensor/utils/balance.py @@ -244,6 +244,9 @@ def __pos__(self): def __abs__(self): return Balance.from_rao(abs(self.rao)) + def to_dict(self) -> dict: + return {"rao": self.rao, "tao": self.tao} + @staticmethod def from_float(amount: float): """ diff --git a/bittensor/utils/rpc_request.py b/bittensor/utils/rpc_request.py new file mode 100644 index 0000000000..b648a6bd64 --- /dev/null +++ b/bittensor/utils/rpc_request.py @@ -0,0 +1,206 @@ +import asyncio +import json + +from substrateinterface.storage import StorageKey +from scalecodec.base import ScaleBytes +import websockets + +import bittensor + +CHAIN_ENDPOINT = "wss://test.finney.opentensor.ai:443" + + +async def preprocess( + ss58_address: str, substrate_interface, block_hash, storage_function +): + module = "SubtensorModule" + params = [ss58_address] + + substrate_interface.init_runtime(block_hash=block_hash) # TODO + + # Search storage call in metadata + metadata_pallet = substrate_interface.metadata.get_metadata_pallet(module) + + if not metadata_pallet: + raise Exception(f'Pallet "{module}" not found') + + storage_item = metadata_pallet.get_storage_function(storage_function) + + if not metadata_pallet or not storage_item: + raise Exception(f'Storage function "{module}.{storage_function}" not found') + + # SCALE type string of value + value_scale_type = storage_item.get_value_type_string() + + storage_key = StorageKey.create_from_storage_function( + module, + storage_item.value["name"], + params, + runtime_config=substrate_interface.runtime_config, + metadata=substrate_interface.metadata, + ) + method = ( + "state_getStorageAt" + if substrate_interface.supports_rpc_method("state_getStorageAt") + else "state_getStorage" + ) + return ( + ss58_address, + method, + [storage_key.to_hex(), block_hash], + value_scale_type, + storage_item, + ) + + +async def process_response( + response: dict, value_scale_type, storage_item, runtime_config, metadata +): + if value_scale_type: + 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 + + obj = runtime_config.create_scale_object( + type_string=value_scale_type, + data=ScaleBytes(query_value), + metadata=metadata, + ) + obj.decode(check_remaining=True) + obj.meta_info = {"result_found": response.get("result") is not None} + return obj + + +async def make_call( + payloads: dict[int, dict], value_scale_type, storage_item, runtime_config, metadata +): + async with websockets.connect(CHAIN_ENDPOINT) as websocket: + for payload in (x["payload"] for x in payloads.values()): + await websocket.send(json.dumps(payload)) + + responses = {} + + for _ in payloads: + response = json.loads(await websocket.recv()) + decoded_response = await process_response( + response, value_scale_type, storage_item, runtime_config, metadata + ) + + request_id = response.get("id") + responses[payloads[request_id]["ss58"]] = decoded_response + + return responses + + +async def query_subtensor(subtensor, hotkeys: list[str], storage_function) -> dict: + # TODO make this more general + block_hash = subtensor.substrate.get_chain_head() + stuff = await asyncio.gather( + *[ + preprocess(x, subtensor.substrate, block_hash, storage_function) + for x in hotkeys + ] + ) + all_info = { + i: { + "ss58": ss58, + "payload": {"jsonrpc": "2.0", "method": method, "params": params, "id": i}, + } + for (i, (ss58, method, params, *_)) in enumerate(stuff) + } + value_scale_type = stuff[0][3] + storage_item = stuff[0][4] + responses = await make_call( + all_info, + value_scale_type, + storage_item, + subtensor.substrate.runtime_config, + subtensor.substrate.metadata, + ) + return responses + + +if __name__ == "__main__": + import time + + start = time.time() + hotkeys_ = ( + "5GHczYXpzd5xmNwjxWs63hw9DannNBGDp6tG6aPmsqP5WiwM 5CP9yFmNqiafpXDsTrdZvFpGgnGUkmGW2geM4TZPJmWYaVu2 " + "5CvjMLhb492gaZrJFauY87qMMwJomKuK7dTUaXgdUanLTU9Z 5FLDSzoPejcrQW5CYbXvq7WE5X9NL6SFD4xJqmyjc72xUhHE " + "5FqBhsQwGNUeJ2RZuCVQxSxRYD7sEbH15ixc5hMgihbAmcu9 5Hddm3iBFD2GLT5ik7LZnT3XJUnRnN8PoeCFgGQgawUVKNm8 " + "5DvFEV5nBSdoKftxisu5EM63PzPsVTD7zuq6XTnCDcbCy3Rz 5Csui1uEQiKfgcvCf1ZEaEE2AUihzWXnFES3VUNnPtZ8rzBX " + "5GuNeKJGihxrnNokQdviGkeCM3qTGX2vbVPaQ4Ktki9uEqso 5HJua76UDcLwDci6kf8hDHcQBnPM7u3cPfjitBBaJkTVmEYh " + "5DyfyYuESbmzea9jTiy9iMuKE6RvQjowKRxeZ6UnBUiYoC5y 5Dk2y7wgtnrkN5C7BQYTmrTNoU3vhko5d2wtc8w6bVsmoAtF " + "5C86aJ2uQawR6P6veaJQXNK9HaWh6NMbUhTiLs65kq4ZW3NH 5ExZhGZerURnunK2g5rjAaCNvBXYTCJguf2SSzWVZQZfr21T " + "5G9WcjZvv2n6qLdpsMqEDHwvtondiiZJnDmbrGrAyC7B573o 5EhEZN6soubtKJm8RN7ANx9FGZ2JezxBUFxr45cdsHtDp3Uk " + "5D1rWnzozRX68ZqKTPXZAFWTJ8hHpox283yWFmsdNjxhRCdB 5ECDEtiHDP7tXeG3L7PViHsjSUPCsijKEokrFWhdXuATDjH1 " + "5HTZipxVCMqzhLt9QKi2Nxj3Fd6TCSnzTjBKR3vtiuTkuq1B 5CMPL2Fq5tNVM1isoiU6kdaUYg9fi5Cfpstwp2rzY5G9EYM8 " + "5Fjp4r8cvWexkWUVb756LkopTVjmzXHBT4unpDN6SzwmQq8E 5HZ5FuwCarNA1CQqCYBcWMSNdUkcjKoCTDknJWz5ZRwh3ZM3 " + "5HjSBYq1yEt7BNycCMuvdhSB7guwgZXNmauscZVAzBQAro8E 5DHeZzocsGn2qgqENUEa2TT6dT23cwJ6sCXb9Bngrtr9etLQ " + "5FFE2bL4hJgGNJzfm7rCAXiVscZbEF13m9DxCewstp3XPK61 5FYkcKakNcckoA37uDrKVuc6pvzFXWThQLkBbM7yxTkP3AtN " + "5HBMRwHL76GpLXA4VesQPicWL7rPS4CtMWhm5oa4D4kwQgzC 5GQfE6zGrjU2g88ow7cAGdDCE6fXQMgFkeX2ejtHv8C4YWuP " + "5FBfMAnR3PkvvvxBbPfC9YbdW25jsyhyB7p1BZtckCuDjBQn 5CqgbUYinK8k6pgSoDRLggxJMgTv3cBNiyRRYTkpCt3SKQ5Y " + "5GEfySfTspQocWmXra4gJgVWy71tk8WySbXpU1vAPP2evaUF 5GTpXGKbcoA4Wor2LvZpTU913EDV2AbjMXPLGKCkNukjPvwY " + "5EJEfB4wasHRJfkepctznJtsBJPymAJzhhmeT9MFwqbQQvgE 5DHqG94fgPNX54QVuHEkAMrtvXYJ2ph2hS4JzVY4vESEAm9J " + "5CPJnbQXXWZyU5BKBwrRviq5sNktHYn32rGaPNWyCTqYVowh 5G3j9nuzEgLF5mAega9xQWsfEhq7m3jXkuLuSwBeTJYWECfA " + "5F6vjRj1ZqYswpDVXwGvXNX9Wa5N9D2htTHJsU7ZsZLFXzu8 5HY8V849JcmTwcCJvq6t1waTbmfdHbWRXkRdo4dP2yqfKSBf " + "5E2WC93Axc4VTynRBqoh5zkbs1kqykXZvNshyyhUpmNSBJG7 5GsDgsLQYxxdn6o9e1CKR1JTqKsW7fHDrjpsWiTf7WAFPC3o " + "5HNR6ifJh7b5GvCnWAcMSLoQh4G43shBnKKns5p4DEKfuPLq 5DG4VHT3gKZDEQ3Tx4oVPpejaz64FeDtNPhbAYTLFBmygHUW " + "5FcY8CUfGVvYYyzVZ2r4GetWLBN2GAr4Ky4odxJtmQbye4Wz 5FvKvxJz1iKCQisuatwWzNdshUULiWMGyzF7J2RPRuz449eo " + "5Gmr8tvDdQQ4TECrtUfRTHNr8Fu9z5acnYQcWiHjHckRTRsS 5GhLWfykGuSXv9oaMJyacNF45TP3mKr1ivkMfAwpoFuqsb1S " + "5HbthkVNhiLR35wFfxbN4yK1yNwqAqQAVVS62s4ceefNvw2n 5HQvDhp6c3HjwXuBYLfJB92dMewr56tB6Xukk5dL1NG1DFbp " + "5H9Lz17rBTdEYS7LCdXg3w1vrMmxpjhhE8Mw2ZLPNZDUphh3 5GF1KCCc8sxhaVsWkZaJR1CXqis2Eo9u1kbfqxDLAyb1eLL4 " + "5F1hucqLa6DrYRLoyNWLJAcFrgN2ifrLqtuXmTACaBgJ3tza 5HEp14zGbxoutFRGQBMrdHxTVU1w5jrnGeJ3eWKDRe6qrukm " + "5DU7XMB42qnZF2GxXdkW6Cr8dho734ue1PrfKDBUcUAqEKBY 5Fzy8WMsBkCcMn5Gf9CFXbQrAYmEmepjgRuVU7kEGhfEb4Rh " + "5GuxieMeeAACQAMjwi4AC6nKFz4H4inZHTjt9u6hBG3Dm2oD 5G7FYjDKbnqSKdbMfVoA86H3Bdyx3GbZ8vbmBFcFnqopJMKz " + "5GsecT96qyjpRKPVEvS5ANEvGptPAHWsP2m3Nee8wPEJBo72 5DAr1n4kc41bTZLRsMFfooFeacHGXEe7DNe8rFyv3TPkFLQf " + "5GuvM2FCDPZKB9SisJih6t2ienjZwg1MwWxckefzntj9RnUX 5Gp4z53bMPPoRPsfEwrFPzKipPjkTr8XFQixLcqEURRTRRQ8 " + "5ELACY4qAdoVkySjBoBMLHe338dGi3fV9FQoZvC5qUu7W24p 5GFGi6ELC9kzCLa2godKaoVCrKZtMvez4EpT7QTFKyyQgLVt " + "5EWM19AUKEphvECTAk3hMF6Zvkp9ZLtZxc6PkakPoznFa5aC 5CChqsKedYwhgJ9ku8MoBQ2h2UcUXgNd6f8MTdeDAuqtyino " + "5Di5KkU4AoFaPEaQdRMo6DvhfQ5ZJ17kx2aA5dEHw7kxjDAU 5DkGtfPL2VvLedaVrEb4BadJBMe3vzKSSzYUQQgKxPgUuHp2 " + "5HTFRZN2azeq82S9Fos66rZ2Z1GKB9nfESzkQZ92y66rB9x2 5CBDcd5233gumVCggSp6yb4AWMvnft4Qapim1nhTpB4pd2CM " + "5FP8RVi5gchyKvbH1eo4ntpeTPZdfKwTJyGBk6YKujg7dMns 5CM7DYZFri3h47GZsdbWbYPAcSuYdCxJ7gXiZtiqfUmVJtE2 " + "5DoKo13X6RPk3XFnpEFJZPKVZZwju8fbWPEka3KANYdFFFRQ 5GrgH81iP5eD3m3dxL5kqrG9Dt5NiSCJZaiGqz6JyUEyixSf " + "5GqF1QHvjzu9Vi7Jc6SWfG3yNNn9qKkoysvZDv54g346zKwx 5E2MF7htsXr4dXdoJS5yTUmfLp6LMkr13S3EdR4mUzokyH7o " + "5CDP6jVS7Cy9hcWSQSnEy7YKkUYLSKiXYbwkqPDN6Kj7VEiQ 5GxAPx6XZ5x4wGMgFqaoEg6q7A27dRcpcibyFevgQbZYZcrf " + "5HBCug2QsScJL2dqw6rGPqrrJbNZmHLwHzdBgRSTwZraD4SU 5CAdPfTgZCWwoyKVDVYgnwKAjyXmFSLnDxFiW1QmCALG94MB " + "5CyNWDt28qEryf37ZApKgPA6zXS6pdVcMxkMjuMJU8Scstrp 5HeEBi9bvJ2dwn8dXN89rDa254bnKr5z8citYhD3udqicnZ1 " + "5G79Sp3LqJFYdGQ62PVvVrBEVeRiqyRXArVfB535QFWFRn1k 5EPgyPbUs49fAxFHzrgB7KYktkH6EJdTuHNK8RbxRCUCCof7 " + "5FRpmX8pesi7jTUvbcTSFcXY4DgGHSiDCstsrGxRPJp85aAZ 5Dnk42fQBYepNat6MBwv9RQsPKHwLo5BdTTXb6G8eV8FeMmv " + "5CPR7RHcHHtkdrqJGXiLi94iWfknb7f7CbvDzyDe6b2J6wDw 5DkSqMkYZTGhSv973JAM9swvGzgeYFxeY5WhrvJwKHR6gg3e " + "5HfytGTCCyxfRU2Ftcvi1YfGTizvwsCwq8gMpfGa1XJhbtwk 5DUBBm7WzS4FS5Rj74o1Q2QWGEAFSCEKC18urEUN3Lu7GS7q " + "5HRN5GLXHjuLdpuy1wnqsM2MFDBEffsH2SuKkwr8Hz6DQSck 5CXhDxkV22FAFiDx1EQarVtJ8qz1G52n5Z5ptd84Yw2saLhD " + "5G9cBNeDmDi1BqFQWj6z4cn9r83LVQvLsUtMEzfQwMsbtPR9 5EntLszo4dRveuHK7MmKxdMZL87oTkmjvNf6VuWkViSDgAbg " + "5FmyyS5dZnKYYPFweoevVFsFwmKNPcapV5EMerB4ADywK5Qi 5HEuYzyTKnQAx779W9UYNJpSGpjioEF63oVPZNcixV8HvkZn " + "5DwTUHcc239asj7xWm6zudZ4YdJN1yVp1fcxjgjmEH4yUBDo 5GLWYfcBfUB4fGxMXTD5e1qQDGiGRiwgSq6YEVLwoq2vqbmW " + "5GsNxXaz23zvwTW79KUmoiws857pgGUpyHgPcL1tjHCMpt84 5CV1pApJjXKELoqRhe68euv5JfNFZgugCFTDcKNm3a2BRPLE " + "5G1LDxnTJ1JgvaviUZcodaeFYZFYDAWaEPNTPt1ri8U6E8md 5EnFttQYUANAkpqoYCZL1mXhWoA6jvhFLLWR2g4zqNxgMYAe " + "5DhKVLE5bsg8BYY7QC94AJzGHC1LJQ9oCW4R33GekuVMqDY1 5F4RznJAcqnWGnxS9LCn4udLRbesU1iH7tasLmxSDFKLVksE " + "5EM1T8iNWfefpwboW6a451E2amsUrdhD6xepA9iBAt12oFys 5CyvNTsdXMkTUxbh3YM1jzzUxtTXf7QuAkzWur2UE37GVFGs " + "5ERxYp3fmBgWUfgQi7sqXxj1bjA1izhQSx7AJV6Jnke8nyMh 5FvLf8P8DSQTFqnYDuZFTmFry5iWj7McCrh74jJfrjnfVJyC " + "5GqM7hwHtLc4VFVVXWF3gAtY2UTJJPSe1BGVqG7KRcNbmcnE 5EFgTXoyDxGTvC2v6oHC8SXBg79hVwEvFgNk7LGeNQdknLwz " + "5HThAqPgVz4zMvtCgDvFcA2VN9YuHYwATdF8VwGcVJS6ToFC 5E81DC8SFDNTG42rytf8HkveFToeNwafjZJfXLY1ZvLtAEg4 " + "5ChWx1q7dY5B5hB7D5K8xksKQsRGb1YQp1XYgacmh5YguFHY 5DhHWS4CwAdmMFJfGyXuKfJZ3VRrDCApt9eoC2A6HsYyCwdb " + "5FvhdqRVkfzPYarA1ETzWsm8o67CTJy4LDtRfQGjMVn8hFea 5F9WFdvsNo6moEsZymtAVgKswXHrXm4TCktGg4dnqpC6ss9o " + "5EUXpQXK424WRseNeA9ckC877KdQRvpdLQj4U2eyPFLUtxj5 5GDxot8vEsd1LdyG9Q6abknLnjzyXWbqezYRTLGUhwnH1wyd " + "5DkNK5241GGSzK6sYjYVWYwraQvBmqiFkQDDSAQ1SjHtiCqF 5HDnDnySziB2g4W91sFtkmrjromEhhJ1b9U59JT6P7GdiqGa " + "5DZkBQHSogZNhYuLTvz5ZSYz4hVU8jcav1RWNShwdqyfpfSx 5DUiBk255vgnihBTdAXcrBzqdzpK4s8UCtRGR87bj4MUgZcB " + "5HdZQMqNZ44oRxHfE4Ga3AFMykjEqwRsHrnYLFRf6UFQoNm4 5HBTQAwhqYn4fXSFrWfRyDkFXT5J178CbUvuK6g9bWwKuQ6Q " + "5CFzMMPggC9kxiLgYibTDPk5w2JDUgmSdwmqxQD5ykFJXg5K 5D7drqrZ3eLrHdNhJ52d7SLYzSDf1RHqxexB43bXDWgkcXqp " + "5HbkRap76b3S2nCzCPFbvfEWP2EacvardWubbsorCFCVVAmz 5DiLGj6FfR865roh1bfHR76Av91NajBheca4naaCADapeey6 " + "5DhV69RTR4yN6onp8cwNu3Mtgr41jXQDy4wjKxEWkhwvTUS6 5EWcjhdA6HToZ2bRab1WCrJcToWMdKYNTcYsiP88tApsL8Gy" + ) + subtensor_ = bittensor.subtensor("test") + print( + asyncio.run( + query_subtensor(subtensor_, hotkeys_.split(" "), "TotalHotkeyStake") + ) + ) + end = time.time() + print(end - start) diff --git a/bittensor/utils/wallet_utils.py b/bittensor/utils/wallet_utils.py index 39218c33f0..8e3633d441 100644 --- a/bittensor/utils/wallet_utils.py +++ b/bittensor/utils/wallet_utils.py @@ -154,7 +154,7 @@ def create_identity_dict( } -def decode_hex_identity_dict(info_dictionary): +def decode_hex_identity_dict(info_dictionary) -> dict: for key, value in info_dictionary.items(): if isinstance(value, dict): item = list(value.values())[0] diff --git a/bittensor/wallet.py b/bittensor/wallet.py index 6ac808b12a..10723a249f 100644 --- a/bittensor/wallet.py +++ b/bittensor/wallet.py @@ -397,6 +397,17 @@ def get_coldkey(self, password: str = None) -> "bittensor.Keypair": """ return self.coldkey_file.get_keypair(password=password) + def unlock_coldkey(self, password: str = None) -> bool: + if self._coldkey: + return True + else: + try: + keypair = self.get_coldkey(password=password) + self._coldkey = keypair + return True + except bittensor.KeyFileError: + return False + def get_hotkey(self, password: str = None) -> "bittensor.Keypair": """ Gets the hotkey from the wallet. @@ -446,7 +457,7 @@ def coldkey(self) -> "bittensor.Keypair": KeyFileError: Raised if the file is corrupt of non-existent. CryptoKeyError: Raised if the user enters an incorrec password for an encrypted keyfile. """ - if self._coldkey == None: + if self._coldkey is None: self._coldkey = self.coldkey_file.keypair return self._coldkey @@ -631,7 +642,7 @@ def regenerate_coldkeypub( public_key: (str | bytes, optional): Public key as hex string or bytes. overwrite (bool, optional) (default: False): - Determins if this operation overwrites the coldkeypub (if exists) under the same path ``//coldkeypub``. + Determines if this operation overwrites the coldkeypub (if exists) under the same path ``//coldkeypub``. Returns: wallet (bittensor.wallet): Newly re-generated wallet with coldkeypub. diff --git a/requirements/prod.txt b/requirements/prod.txt index d5bbf44b87..482d6c5cb7 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,4 +1,5 @@ aiohttp==3.9.0b0 +aiofiles==23.2.1 ansible==6.7.0 ansible_vault==2.1.0 backoff @@ -9,12 +10,11 @@ cryptography==42.0.5 ddt==1.6.0 eth-utils<2.3.0 fuzzywuzzy>=0.18.0 -fastapi==0.110.1 +fastapi~=0.111.0 munch==2.5.0 netaddr numpy msgpack-numpy-opentensor==0.5.0 -nest_asyncio packaging pycryptodome>=3.18.0,<4.0.0 pyyaml @@ -30,8 +30,9 @@ requests rich scalecodec==1.2.7 # scalecodec should not be changed unless first verifying compatibility with the subtensor's monkeypatching of scalecodec.RuntimeConfiguration.get_decoder_class shtab==1.6.5 +starlette~=0.37.2 substrate-interface==1.7.5 termcolor tqdm -uvicorn==0.22.0 +uvicorn==0.29.0 wheel