diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index b4dc3120..241abdc7 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -48,10 +48,10 @@ jobs: run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - name: Pull Docker Image - run: docker pull ghcr.io/opentensor/subtensor-localnet:latest + run: docker pull ghcr.io/opentensor/subtensor-localnet:devnet-ready - name: Save Docker Image to Cache - run: docker save -o subtensor-localnet.tar ghcr.io/opentensor/subtensor-localnet:latest + run: docker save -o subtensor-localnet.tar ghcr.io/opentensor/subtensor-localnet:devnet-ready - name: Upload Docker Image as Artifact uses: actions/upload-artifact@v4 @@ -73,7 +73,7 @@ jobs: os: - ubuntu-latest test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} - # python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - name: Check-out repository uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 75397415..61ec3c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 9.3.0 /2025-04-09 + +## What's Changed +* Fix e2e test by @basfroman in https://github.com/opentensor/btcli/pull/396 +* Btwallet e2e test - verbose printing by @ibraheem-abe in https://github.com/opentensor/btcli/pull/397 +* Feat/swap coldkey by @ibraheem-abe in https://github.com/opentensor/btcli/pull/399 +* Add logic for keep docker image up to date by @basfroman in https://github.com/opentensor/btcli/pull/400 +* Feat/associate hotkey by @ibraheem-abe in https://github.com/opentensor/btcli/pull/401 +* Fixes staking/unstaking e2e tests by @ibraheem-abe in https://github.com/opentensor/btcli/pull/404 +* Adds `era` param for stake transactions by @thewhaleking in https://github.com/opentensor/btcli/pull/406 +* Fix: Removes name conflict in Sn create by @ibraheem-abe in https://github.com/opentensor/btcli/pull/405 +* Pull version.py version from package metadata by @thewhaleking in https://github.com/opentensor/btcli/pull/409 +* json output for commands by @thewhaleking in https://github.com/opentensor/btcli/pull/369 +* General code cleanup by @thewhaleking in https://github.com/opentensor/btcli/pull/411 +* More json outputs by @thewhaleking in https://github.com/opentensor/btcli/pull/412 +* new color palette by @thewhaleking in https://github.com/opentensor/btcli/pull/413 +* bump versions by @thewhaleking in https://github.com/opentensor/btcli/pull/410 +* spelling fix "Received" by @dougsillars in https://github.com/opentensor/btcli/pull/414 +* Updates Subnet symbols by @ibraheem-abe in https://github.com/opentensor/btcli/pull/416 +* Fix calculation for childkey set by @thewhaleking in https://github.com/opentensor/btcli/pull/418 +* Revoke children msg by @thewhaleking in https://github.com/opentensor/btcli/pull/419 +* Update revoke children language by @thewhaleking in https://github.com/opentensor/btcli/pull/417 +* Revert "new color palette" by @thewhaleking in https://github.com/opentensor/btcli/pull/420 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.2.0...v9.3.0 + ## 9.2.0 /2025-03-18 ## What's Changed diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 68d95db4..84497ce6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2,6 +2,7 @@ import asyncio import curses import importlib +import json import os.path import re import ssl @@ -14,11 +15,19 @@ import rich import typer import numpy as np +from async_substrate_interface.errors import ( + SubstrateRequestException, + ConnectionClosed, + InvalidHandshake, +) from bittensor_wallet import Wallet from rich import box from rich.prompt import Confirm, FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table from rich.tree import Tree +from typing_extensions import Annotated +from yaml import safe_dump, safe_load + from bittensor_cli.src import ( defaults, HELP_PANELS, @@ -31,7 +40,6 @@ from bittensor_cli.version import __version__, __version_as_int__ from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance -from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.subnets import price, subnets @@ -48,6 +56,7 @@ console, err_console, verbose_console, + json_console, is_valid_ss58_address, print_error, validate_chain_endpoint, @@ -61,9 +70,6 @@ is_linux, validate_rate_tolerance, ) -from typing_extensions import Annotated -from websockets import ConnectionClosed, InvalidHandshake -from yaml import safe_dump, safe_load try: from git import Repo, GitError @@ -109,6 +115,17 @@ class Options: "--wallet.hotkey", help="Hotkey of the wallet", ) + wallet_ss58_address = typer.Option( + None, + "--wallet-name", + "--name", + "--wallet_name", + "--wallet.name", + "--address", + "--ss58", + "--ss58-address", + help="SS58 address or wallet name to check. Leave empty to be prompted.", + ) wallet_hotkey_ss58 = typer.Option( None, "--hotkey", @@ -268,6 +285,15 @@ class Options: "--dashboard.path", help="Path to save the dashboard HTML file. For example: `~/.bittensor/dashboard`.", ) + json_output = typer.Option( + False, + "--json-output", + "--json-out", + help="Outputs the result of the command as JSON.", + ) + era: int = typer.Option( + 3, help="Length (in blocks) for which the transaction should be valid." + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -313,22 +339,31 @@ def verbosity_console_handler(verbosity_level: int = 1) -> None: :param verbosity_level: int corresponding to verbosity level of console output (0 is quiet, 1 is normal, 2 is verbose) """ - if verbosity_level not in range(3): + if verbosity_level not in range(4): raise ValueError( - f"Invalid verbosity level: {verbosity_level}. Must be one of: 0 (quiet), 1 (normal), 2 (verbose)" + f"Invalid verbosity level: {verbosity_level}. " + f"Must be one of: 0 (quiet + json output), 1 (normal), 2 (verbose), 3 (json output + verbose)" ) if verbosity_level == 0: console.quiet = True err_console.quiet = True verbose_console.quiet = True + json_console.quiet = False elif verbosity_level == 1: console.quiet = False err_console.quiet = False verbose_console.quiet = True + json_console.quiet = True elif verbosity_level == 2: console.quiet = False err_console.quiet = False verbose_console.quiet = False + json_console.quiet = True + elif verbosity_level == 3: + console.quiet = True + err_console.quiet = True + verbose_console.quiet = False + json_console.quiet = False def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[int]: @@ -684,6 +719,12 @@ def __init__(self): self.wallet_app.command( "swap-hotkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_swap_hotkey) + self.wallet_app.command( + "swap-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] + )(self.wallet_swap_coldkey) + self.wallet_app.command( + "swap-check", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] + )(self.wallet_check_ck_swap) self.wallet_app.command( "regen-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_regen_coldkey) @@ -699,6 +740,9 @@ def __init__(self): self.wallet_app.command( "new-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["MANAGEMENT"] )(self.wallet_new_coldkey) + self.wallet_app.command( + "associate-hotkey", rich_help_panel=HELP_PANELS["WALLET"]["MANAGEMENT"] + )(self.wallet_associate_hotkey) self.wallet_app.command( "create", rich_help_panel=HELP_PANELS["WALLET"]["MANAGEMENT"] )(self.wallet_create_wallet) @@ -934,6 +978,7 @@ def initialize_chain( """ if not self.subtensor: if network: + network_ = None for item in network: if item.startswith("ws"): network_ = item @@ -1068,12 +1113,15 @@ def main_callback( except ModuleNotFoundError: self.asyncio_runner = asyncio.run - def verbosity_handler(self, quiet: bool, verbose: bool): + def verbosity_handler( + self, quiet: bool, verbose: bool, json_output: bool = False + ) -> None: if quiet and verbose: err_console.print("Cannot specify both `--quiet` and `--verbose`") raise typer.Exit() - - if quiet: + if json_output and verbose: + verbosity_console_handler(3) + elif json_output or quiet: verbosity_console_handler(0) elif verbose: verbosity_console_handler(2) @@ -1203,7 +1251,8 @@ def set_config( elif arg == "rate_tolerance": while True: val = FloatPrompt.ask( - f"What percentage would you like to set for [red]{arg}[/red]?\nValues are percentages (e.g. 0.05 for 5%)", + f"What percentage would you like to set for [red]{arg}[/red]?\n" + f"Values are percentages (e.g. 0.05 for 5%)", default=0.05, ) try: @@ -1503,7 +1552,7 @@ def wallet_ask( wallet_name: Optional[str], wallet_path: Optional[str], wallet_hotkey: Optional[str], - ask_for: list[str] = [], + ask_for: Optional[list[str]] = None, validate: WV = WV.WALLET, ) -> Wallet: """ @@ -1512,9 +1561,10 @@ def wallet_ask( :param wallet_path: root path of the wallets :param wallet_hotkey: name of the wallet hotkey file :param validate: flag whether to check for the wallet's validity - :param ask_type: aspect of the wallet (name, path, hotkey) to prompt the user for + :param ask_for: aspect of the wallet (name, path, hotkey) to prompt the user for :return: created Wallet object """ + ask_for = ask_for or [] # Prompt for missing attributes specified in ask_for if WO.NAME in ask_for and not wallet_name: if self.config.get("wallet_name"): @@ -1587,6 +1637,7 @@ def wallet_list( wallet_path: str = Options.wallet_path, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Displays all the wallets and their corresponding hotkeys that are located in the wallet path specified in the config. @@ -1602,11 +1653,11 @@ def wallet_list( [bold]NOTE[/bold]: This command is read-only and does not modify the filesystem or the blockchain state. It is intended for use with the Bittensor CLI to provide a quick overview of the user's wallets. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( None, wallet_path, None, ask_for=[WO.PATH], validate=WV.NONE ) - return self._run_command(wallets.wallet_list(wallet.path)) + return self._run_command(wallets.wallet_list(wallet.path, json_output)) def wallet_overview( self, @@ -1646,6 +1697,7 @@ def wallet_overview( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Displays a detailed overview of the user's registered accounts on the Bittensor network. @@ -1662,7 +1714,7 @@ def wallet_overview( It provides a quick and comprehensive view of the user's network presence, making it useful for monitoring account status, stake distribution, and overall contribution to the Bittensor network. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if include_hotkeys and exclude_hotkeys: utils.err_console.print( "[red]You have specified both the inclusion and exclusion options. Only one of these options is allowed currently." @@ -1707,6 +1759,7 @@ def wallet_overview( exclude_hotkeys, netuids_filter=netuids, verbose=verbose, + json_output=json_output, ) ) @@ -1729,6 +1782,7 @@ def wallet_transfer( transfer_all: bool = typer.Option( False, "--all", prompt=False, help="Transfer all available balance." ), + era: int = Options.era, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -1736,6 +1790,7 @@ def wallet_transfer( prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Send TAO tokens from one wallet to another wallet on the Bittensor network. @@ -1759,7 +1814,7 @@ def wallet_transfer( print_error("You have entered an incorrect ss58 address. Please try again.") raise typer.Exit() - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -1777,12 +1832,14 @@ def wallet_transfer( amount = FloatPrompt.ask("Enter amount (in TAO) to transfer.") return self._run_command( wallets.transfer( - wallet, - subtensor, - destination_ss58_address, - amount, - transfer_all, - prompt, + wallet=wallet, + subtensor=subtensor, + destination=destination_ss58_address, + amount=amount, + transfer_all=transfer_all, + era=era, + prompt=prompt, + json_output=json_output, ) ) @@ -1798,6 +1855,7 @@ def wallet_swap_hotkey( quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, + json_output: bool = Options.json_output, ): """ Swap hotkeys of a given wallet on the blockchain. For a registered key pair, for example, a (coldkeyA, hotkeyA) pair, this command swaps the hotkeyA with a new, unregistered, hotkeyB to move the original registration to the (coldkeyA, hotkeyB) pair. @@ -1816,7 +1874,7 @@ def wallet_swap_hotkey( [green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) original_wallet = self.wallet_ask( wallet_name, wallet_path, @@ -1838,7 +1896,9 @@ def wallet_swap_hotkey( ) self.initialize_chain(network) return self._run_command( - wallets.swap_hotkey(original_wallet, new_wallet, self.subtensor, prompt) + wallets.swap_hotkey( + original_wallet, new_wallet, self.subtensor, prompt, json_output + ) ) def wallet_inspect( @@ -1857,6 +1917,7 @@ def wallet_inspect( netuids: str = Options.netuids, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Displays the details of the user's wallet pairs (coldkey, hotkey) on the Bittensor network. @@ -1891,7 +1952,7 @@ def wallet_inspect( """ print_error("This command is disabled on the 'rao' network.") raise typer.Exit() - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if netuids: netuids = parse_to_list( @@ -1988,6 +2049,7 @@ def wallet_faucet( [bold]Note[/bold]: This command is meant for used in local environments where users can experiment with the blockchain without using real TAO tokens. Users must have the necessary hardware setup, especially when opting for CUDA-based GPU calculations. It is currently disabled on testnet and mainnet (finney). You can only use this command on a local blockchain. """ + # TODO should we add json_output? wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2024,6 +2086,7 @@ def wallet_regen_coldkey( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Regenerate a coldkey for a wallet on the Bittensor blockchain network. @@ -2041,7 +2104,7 @@ def wallet_regen_coldkey( [bold]Note[/bold]: This command is critical for users who need to regenerate their coldkey either for recovery or for security reasons. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not wallet_path: wallet_path = Prompt.ask( @@ -2069,6 +2132,7 @@ def wallet_regen_coldkey( json_password, use_password, overwrite, + json_output, ) ) @@ -2082,6 +2146,7 @@ def wallet_regen_coldkey_pub( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Regenerates the public part of a coldkey (coldkeypub.txt) for a wallet. @@ -2098,7 +2163,7 @@ def wallet_regen_coldkey_pub( [bold]Note[/bold]: This command is particularly useful for users who need to regenerate their coldkeypub, perhaps due to file corruption or loss. You will need either ss58 address or public hex key from your old coldkeypub.txt for the wallet. It is a recovery-focused utility that ensures continued access to your wallet functionalities. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not wallet_path: wallet_path = Prompt.ask( @@ -2127,7 +2192,9 @@ def wallet_regen_coldkey_pub( rich.print("[red]Error: Invalid SS58 address or public key![/red]") raise typer.Exit() return self._run_command( - wallets.regen_coldkey_pub(wallet, ss58_address, public_key_hex, overwrite) + wallets.regen_coldkey_pub( + wallet, ss58_address, public_key_hex, overwrite, json_output + ) ) def wallet_regen_hotkey( @@ -2146,6 +2213,7 @@ def wallet_regen_hotkey( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Regenerates a hotkey for a wallet. @@ -2164,7 +2232,7 @@ def wallet_regen_hotkey( [bold]Note[/bold]: This command is essential for users who need to regenerate their hotkey, possibly for security upgrades or key recovery. It should be used with caution to avoid accidental overwriting of existing keys. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2184,6 +2252,7 @@ def wallet_regen_hotkey( json_password, use_password, overwrite, + json_output, ) ) @@ -2206,6 +2275,7 @@ def wallet_new_hotkey( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Create a new hotkey for a wallet. @@ -2221,7 +2291,7 @@ def wallet_new_hotkey( [italic]Note[/italic]: This command is useful to create additional hotkeys for different purposes, such as running multiple subnet miners or subnet validators or separating operational roles within the Bittensor network. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not wallet_name: wallet_name = Prompt.ask( @@ -2245,7 +2315,77 @@ def wallet_new_hotkey( if not uri: n_words = get_n_words(n_words) return self._run_command( - wallets.new_hotkey(wallet, n_words, use_password, uri, overwrite) + wallets.new_hotkey( + wallet, n_words, use_password, uri, overwrite, json_output + ) + ) + + def wallet_associate_hotkey( + self, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, + network: Optional[list[str]] = Options.network, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Associate a hotkey with a wallet(coldkey). + + USAGE + + This command is used to associate a hotkey with a wallet(coldkey). + + EXAMPLE + + [green]$[/green] btcli wallet associate-hotkey --hotkey-name hotkey_name + [green]$[/green] btcli wallet associate-hotkey --hotkey-ss58 5DkQ4... + """ + self.verbosity_handler(quiet, verbose) + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue] [dim](which you want to associate with the hotkey)[/dim]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + if not wallet_hotkey: + wallet_hotkey = Prompt.ask( + "Enter the [blue]hotkey[/blue] name or " + "[blue]hotkey ss58 address[/blue] [dim](to associate with your coldkey)[/dim]" + ) + + hotkey_display = None + if is_valid_ss58_address(wallet_hotkey): + hotkey_ss58 = wallet_hotkey + wallet = self.wallet_ask( + wallet_name, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + hotkey_display = ( + f"hotkey [{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}]" + ) + else: + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + hotkey_ss58 = wallet.hotkey.ss58_address + hotkey_display = f"hotkey [blue]{wallet_hotkey}[/blue] [{COLORS.GENERAL.HK}]({hotkey_ss58})[/{COLORS.GENERAL.HK}]" + + return self._run_command( + wallets.associate_hotkey( + wallet, + self.initialize_chain(network), + hotkey_ss58, + hotkey_display, + prompt, + ) ) def wallet_new_coldkey( @@ -2264,6 +2404,7 @@ def wallet_new_coldkey( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Create a new coldkey. A coldkey is required for holding TAO balances and performing high-value transactions. @@ -2278,7 +2419,7 @@ def wallet_new_coldkey( [bold]Note[/bold]: This command is crucial for users who need to create a new coldkey for enhanced security or as part of setting up a new wallet. It is a foundational step in establishing a secure presence on the Bittensor network. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not wallet_path: wallet_path = Prompt.ask( @@ -2301,33 +2442,100 @@ def wallet_new_coldkey( if not uri: n_words = get_n_words(n_words) return self._run_command( - wallets.new_coldkey(wallet, n_words, use_password, uri, overwrite) + wallets.new_coldkey( + wallet, n_words, use_password, uri, overwrite, json_output + ) ) def wallet_check_ck_swap( self, - wallet_name: Optional[str] = Options.wallet_name, + wallet_ss58_address: Optional[str] = Options.wallet_ss58_address, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, + scheduled_block: Optional[int] = typer.Option( + None, + "--block", + help="Block number where the swap was scheduled", + ), + show_all: bool = typer.Option( + False, + "--all", + "-a", + help="Show all pending coldkey swaps", + ), network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): """ - Check the status of your scheduled coldkey swap. + Check the status of scheduled coldkey swaps. USAGE - Users should provide the old coldkey wallet to check the swap status. + This command can be used in three ways: + 1. Show all pending swaps (--all) + 2. Check status of a specific wallet's swap or SS58 address + 3. Check detailed swap status with block number (--block) - EXAMPLE + EXAMPLES + + Show all pending swaps: + [green]$[/green] btcli wallet swap-check --all - [green]$[/green] btcli wallet check_coldkey_swap + Check specific wallet's swap: + [green]$[/green] btcli wallet swap-check --wallet-name my_wallet + + Check swap using SS58 address: + [green]$[/green] btcli wallet swap-check --ss58 5DkQ4... + + Check swap details with block number: + [green]$[/green] btcli wallet swap-check --wallet-name my_wallet --block 12345 """ + # TODO add json_output if this ever gets used again (doubtful) self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask(wallet_name, wallet_path, wallet_hotkey) self.initialize_chain(network) - return self._run_command(wallets.check_coldkey_swap(wallet, self.subtensor)) + + if show_all: + return self._run_command( + wallets.check_swap_status(self.subtensor, None, None) + ) + + if not wallet_ss58_address: + wallet_ss58_address = Prompt.ask( + "Enter [blue]wallet name[/blue] or [blue]SS58 address[/blue] [dim](leave blank to show all pending swaps)[/dim]" + ) + if not wallet_ss58_address: + return self._run_command( + wallets.check_swap_status(self.subtensor, None, None) + ) + + if is_valid_ss58_address(wallet_ss58_address): + ss58_address = wallet_ss58_address + else: + wallet = self.wallet_ask( + wallet_ss58_address, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + ss58_address = wallet.coldkeypub.ss58_address + + if not scheduled_block: + block_input = Prompt.ask( + "[blue]Enter the block number[/blue] where the swap was scheduled [dim](optional, press enter to skip)[/dim]", + default="", + ) + if block_input: + try: + scheduled_block = int(block_input) + except ValueError: + print_error("Invalid block number") + raise typer.Exit() + + return self._run_command( + wallets.check_swap_status(self.subtensor, ss58_address, scheduled_block) + ) def wallet_create_wallet( self, @@ -2340,6 +2548,7 @@ def wallet_create_wallet( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Create a complete wallet by setting up both coldkey and hotkeys. @@ -2354,6 +2563,7 @@ def wallet_create_wallet( [bold]Note[/bold]: This command is for new users setting up their wallet for the first time, or for those who wish to completely renew their wallet keys. It ensures a fresh start with new keys for secure and effective participation in the Bittensor network. """ + self.verbosity_handler(quiet, verbose, json_output) if not wallet_path: wallet_path = Prompt.ask( "Enter the path of wallets directory", default=defaults.wallet.path @@ -2370,7 +2580,6 @@ def wallet_create_wallet( default=defaults.wallet.hotkey, ) - self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2381,7 +2590,9 @@ def wallet_create_wallet( if not uri: n_words = get_n_words(n_words) return self._run_command( - wallets.wallet_create(wallet, n_words, use_password, uri, overwrite) + wallets.wallet_create( + wallet, n_words, use_password, uri, overwrite, json_output + ) ) def wallet_balance( @@ -2399,6 +2610,7 @@ def wallet_balance( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Check the balance of the wallet. This command shows a detailed view of the wallet's coldkey balances, including free and staked balances. @@ -2424,7 +2636,7 @@ def wallet_balance( [green]$[/green] btcli w balance --ss58 --ss58 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = None if all_balances: ask_for = [WO.PATH] @@ -2487,7 +2699,9 @@ def wallet_balance( ) subtensor = self.initialize_chain(network) return self._run_command( - wallets.wallet_balance(wallet, subtensor, all_balances, ss58_addresses) + wallets.wallet_balance( + wallet, subtensor, all_balances, ss58_addresses, json_output + ) ) def wallet_history( @@ -2511,6 +2725,7 @@ def wallet_history( """ # TODO: Fetch effective network and redirect users accordingly - this only works on finney + # TODO: Add json_output if this gets re-enabled # no_use_config_str = "Using the network [dark_orange]finney[/dark_orange] and ignoring network/chain configs" # if self.config.get("network"): @@ -2577,6 +2792,7 @@ def wallet_set_id( quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, + json_output: bool = Options.json_output, ): """ Create or update the on-chain identity of a coldkey or a hotkey on the Bittensor network. [bold]Incurs a 1 TAO transaction fee.[/bold] @@ -2595,7 +2811,7 @@ def wallet_set_id( [bold]Note[/bold]: This command should only be used if the user is willing to incur the a recycle fee associated with setting an identity on the blockchain. It is a high-level command that makes changes to the blockchain state and should not be used programmatically as part of other scripts or applications. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2644,6 +2860,7 @@ def wallet_set_id( identity["additional"], identity["github_repo"], prompt, + json_output, ) ) @@ -2665,6 +2882,7 @@ def wallet_get_id( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Shows the identity details of a user's coldkey or hotkey. @@ -2683,7 +2901,7 @@ def wallet_get_id( [bold]Note[/bold]: This command is primarily used for informational purposes and has no side effects on the blockchain network state. """ - wallet = None + self.verbosity_handler(quiet, verbose, json_output) if not wallet_name: if coldkey_ss58: if not is_valid_ss58_address(coldkey_ss58): @@ -2708,9 +2926,8 @@ def wallet_get_id( ) coldkey_ss58 = wallet.coldkeypub.ss58_address - self.verbosity_handler(quiet, verbose) return self._run_command( - wallets.get_id(self.initialize_chain(network), coldkey_ss58) + wallets.get_id(self.initialize_chain(network), coldkey_ss58, json_output) ) def wallet_sign( @@ -2726,6 +2943,7 @@ def wallet_sign( message: str = typer.Option("", help="The message to encode and sign"), quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Allows users to sign a message with the provided wallet or wallet hotkey. Use this command to easily prove your ownership of a coldkey or a hotkey. @@ -2741,7 +2959,7 @@ def wallet_sign( [green]$[/green] btcli wallet sign --wallet-name default --wallet-hotkey hotkey --message '{"something": "here", "timestamp": 1719908486}' """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if use_hotkey is None: use_hotkey = Confirm.ask( f"Would you like to sign the transaction using your [{COLORS.G.HK}]hotkey[/{COLORS.G.HK}]?" @@ -2760,7 +2978,92 @@ def wallet_sign( if not message: message = Prompt.ask("Enter the [blue]message[/blue] to encode and sign") - return self._run_command(wallets.sign(wallet, message, use_hotkey)) + return self._run_command(wallets.sign(wallet, message, use_hotkey, json_output)) + + def wallet_swap_coldkey( + self, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + new_wallet_or_ss58: Optional[str] = typer.Option( + None, + "--new-coldkey", + "--new-coldkey-ss58", + "--new-wallet", + "--new", + help="SS58 address of the new coldkey that will replace the current one.", + ), + network: Optional[list[str]] = Options.network, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + force_swap: bool = typer.Option( + False, + "--force", + "-f", + "--force-swap", + help="Force the swap even if the new coldkey is already scheduled for a swap.", + ), + ): + """ + Schedule a coldkey swap for a wallet. + + This command allows you to schedule a coldkey swap for a wallet. You can either provide a new wallet name, or SS58 address. + + EXAMPLES + + [green]$[/green] btcli wallet schedule-coldkey-swap --new-wallet my_new_wallet + + [green]$[/green] btcli wallet schedule-coldkey-swap --new-coldkey-ss58 5Dk...X3q + """ + self.verbosity_handler(quiet, verbose) + + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue] which you want to swap the coldkey for", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + validate=WV.WALLET, + ) + console.print( + f"\nWallet selected to swap the [blue]coldkey[/blue] from: \n" + f"[dark_sea_green3]{wallet}[/dark_sea_green3]\n" + ) + + if not new_wallet_or_ss58: + new_wallet_or_ss58 = Prompt.ask( + "Enter the [blue]new wallet name[/blue] or [blue]SS58 address[/blue] of the new coldkey", + ) + + if is_valid_ss58_address(new_wallet_or_ss58): + new_wallet_coldkey_ss58 = new_wallet_or_ss58 + else: + new_wallet_name = new_wallet_or_ss58 + new_wallet = self.wallet_ask( + new_wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + validate=WV.WALLET, + ) + console.print( + f"\nNew wallet to swap the [blue]coldkey[/blue] to: \n" + f"[dark_sea_green3]{new_wallet}[/dark_sea_green3]\n" + ) + new_wallet_coldkey_ss58 = new_wallet.coldkeypub.ss58_address + + return self._run_command( + wallets.schedule_coldkey_swap( + wallet=wallet, + subtensor=self.initialize_chain(network), + new_coldkey_ss58=new_wallet_coldkey_ss58, + force_swap=force_swap, + ) + ) def stake_list( self, @@ -2780,6 +3083,7 @@ def stake_list( quiet: bool = Options.quiet, verbose: bool = Options.verbose, no_prompt: bool = Options.prompt, + json_output: bool = Options.json_output, # TODO add: all-wallets, reuse_last, html_output ): """ @@ -2801,7 +3105,7 @@ def stake_list( 4. Verbose output with full values: [green]$[/green] btcli stake list --wallet.name my_wallet --verbose """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = None if coldkey_ss58: @@ -2832,6 +3136,7 @@ def stake_list( live, verbose, no_prompt, + json_output, ) ) @@ -2875,9 +3180,11 @@ def stake_add( rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, + era: int = Options.era, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Stake TAO to one or more hotkeys on specific netuids with your coldkey. @@ -2910,7 +3217,7 @@ def stake_add( • [blue]--partial[/blue]: Complete partial stake if rates exceed tolerance """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: rate_tolerance = self.ask_rate_tolerance(rate_tolerance) @@ -3068,6 +3375,8 @@ def stake_add( safe_staking, rate_tolerance, allow_partial_stake, + json_output, + era, ) ) @@ -3119,6 +3428,7 @@ def stake_remove( rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, + era: int = Options.era, prompt: bool = Options.prompt, interactive: bool = typer.Option( False, @@ -3128,6 +3438,7 @@ def stake_remove( ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Unstake TAO from one or more hotkeys and transfer them back to the user's coldkey wallet. @@ -3159,7 +3470,7 @@ def stake_remove( • [blue]--tolerance[/blue]: Max allowed rate change (0.05 = 5%) • [blue]--partial[/blue]: Complete partial unstake if rates exceed tolerance """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not unstake_all and not unstake_all_alpha: safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: @@ -3171,7 +3482,8 @@ def stake_remove( [hotkey_ss58_address, include_hotkeys, exclude_hotkeys, all_hotkeys] ): print_error( - "Interactive mode cannot be used with hotkey selection options like --include-hotkeys, --exclude-hotkeys, --all-hotkeys, or --hotkey." + "Interactive mode cannot be used with hotkey selection options like " + "--include-hotkeys, --exclude-hotkeys, --all-hotkeys, or --hotkey." ) raise typer.Exit() @@ -3308,6 +3620,8 @@ def stake_remove( include_hotkeys=include_hotkeys, exclude_hotkeys=exclude_hotkeys, prompt=prompt, + json_output=json_output, + era=era, ) ) elif ( @@ -3362,6 +3676,8 @@ def stake_remove( safe_staking=safe_staking, rate_tolerance=rate_tolerance, allow_partial_stake=allow_partial_stake, + json_output=json_output, + era=era, ) ) @@ -3389,9 +3705,11 @@ def stake_move( stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), + era: int = Options.era, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Move staked TAO between hotkeys while keeping the same coldkey ownership. @@ -3413,13 +3731,14 @@ def stake_move( [green]$[/green] btcli stake move """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) console.print( "[dim]This command moves stake from one hotkey to another hotkey while keeping the same coldkey.[/dim]" ) if not destination_hotkey: dest_wallet_or_ss58 = Prompt.ask( - "Enter the [blue]destination wallet[/blue] where destination hotkey is located or [blue]ss58 address[/blue]" + "Enter the [blue]destination wallet[/blue] where destination hotkey is located or " + "[blue]ss58 address[/blue]" ) if is_valid_ss58_address(dest_wallet_or_ss58): destination_hotkey = dest_wallet_or_ss58 @@ -3506,7 +3825,7 @@ def stake_move( "Enter the [blue]destination subnet[/blue] (netuid) to move stake to" ) - return self._run_command( + result = self._run_command( move_stake.move_stake( subtensor=self.initialize_chain(network), wallet=wallet, @@ -3516,10 +3835,14 @@ def stake_move( destination_hotkey=destination_hotkey, amount=amount, stake_all=stake_all, + era=era, interactive_selection=interactive_selection, prompt=prompt, ) ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def stake_transfer( self, @@ -3553,9 +3876,11 @@ def stake_transfer( stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), + era: int = Options.era, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Transfer stake between coldkeys while keeping the same hotkey ownership. @@ -3589,10 +3914,10 @@ def stake_transfer( Transfer all available stake from origin hotkey: [green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2 """ + self.verbosity_handler(quiet, verbose, json_output) console.print( "[dim]This command transfers stake from one coldkey to another while keeping the same hotkey.[/dim]" ) - self.verbosity_handler(quiet, verbose) if not dest_ss58: dest_ss58 = Prompt.ask( @@ -3664,7 +3989,7 @@ def stake_transfer( "Enter the [blue]destination subnet[/blue] (netuid)" ) - return self._run_command( + result = self._run_command( move_stake.transfer_stake( wallet=wallet, subtensor=self.initialize_chain(network), @@ -3673,11 +3998,15 @@ def stake_transfer( dest_netuid=dest_netuid, dest_coldkey_ss58=dest_ss58, amount=amount, + era=era, interactive_selection=interactive_selection, stake_all=stake_all, prompt=prompt, ) ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def stake_swap( self, @@ -3711,11 +4040,13 @@ def stake_swap( "--all", help="Swap all available stake", ), + era: int = Options.era, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Swap stake between different subnets while keeping the same coldkey-hotkey pair ownership. @@ -3737,10 +4068,11 @@ def stake_swap( Swap 100 TAO from subnet 1 to subnet 2: [green]$[/green] btcli stake swap --wallet-name default --wallet-hotkey default --origin-netuid 1 --dest-netuid 2 --amount 100 """ + self.verbosity_handler(quiet, verbose, json_output) console.print( - "[dim]This command moves stake from one subnet to another subnet while keeping the same coldkey-hotkey pair.[/dim]" + "[dim]This command moves stake from one subnet to another subnet while keeping " + "the same coldkey-hotkey pair.[/dim]" ) - self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( wallet_name, @@ -3765,7 +4097,7 @@ def stake_swap( if not amount and not swap_all: amount = FloatPrompt.ask("Enter the [blue]amount[/blue] to swap") - return self._run_command( + result = self._run_command( move_stake.swap_stake( wallet=wallet, subtensor=self.initialize_chain(network), @@ -3773,12 +4105,16 @@ def stake_swap( destination_netuid=dest_netuid, amount=amount, swap_all=swap_all, + era=era, interactive_selection=interactive_selection, prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def stake_get_children( self, @@ -3800,6 +4136,7 @@ def stake_get_children( ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Get all the child hotkeys on a specified subnet. @@ -3811,7 +4148,7 @@ def stake_get_children( [green]$[/green] btcli stake child get --netuid 1 [green]$[/green] btcli stake child get --all-netuids """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -3832,11 +4169,14 @@ def stake_get_children( "Enter a netuid (leave blank for all)", default=None, show_default=True ) - return self._run_command( + result = self._run_command( children_hotkeys.get_children( wallet, self.initialize_chain(network), netuid ) ) + if json_output: + json_console.print(json.dumps(result)) + return result def stake_set_children( self, @@ -3861,6 +4201,7 @@ def stake_set_children( quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, + json_output: bool = Options.json_output, ): """ Set child hotkeys on a specified subnet (or all). Overrides currently set children. @@ -3873,7 +4214,7 @@ def stake_set_children( [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 -p 0.3 -p 0.7 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) netuid = get_optional_netuid(netuid, all_netuids) children = list_prompt( @@ -3885,7 +4226,8 @@ def stake_set_children( proportions = list_prompt( proportions, float, - "Enter comma-separated proportions equal to the number of children (sum not exceeding a total of 1.0)", + "Enter comma-separated proportions equal to the number of children " + "(sum not exceeding a total of 1.0)", ) if len(proportions) != len(children): @@ -3913,6 +4255,7 @@ def stake_set_children( wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, prompt=prompt, + json_output=json_output, ) ) @@ -3939,6 +4282,7 @@ def stake_revoke_children( quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, + json_output: bool = Options.json_output, ): """ Remove all children hotkeys on a specified subnet (or all). @@ -3949,7 +4293,7 @@ def stake_revoke_children( [green]$[/green] btcli stake child revoke --hotkey --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -3974,6 +4318,7 @@ def stake_revoke_children( wait_for_inclusion, wait_for_finalization, prompt=prompt, + json_output=json_output, ) ) @@ -4000,7 +4345,8 @@ def stake_childkey_take( None, "--take", "-t", - help="Use to set the take value for your child hotkey. When not used, the command will fetch the current take value.", + help="Use to set the take value for your child hotkey. When not used, the command will fetch the current " + "take value.", prompt=False, ), wait_for_inclusion: bool = Options.wait_for_inclusion, @@ -4008,6 +4354,7 @@ def stake_childkey_take( prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Get and set your child hotkey take on a specified subnet. @@ -4024,7 +4371,7 @@ def stake_childkey_take( [green]$[/green] btcli stake child take --hotkey --take 0.12 --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -4041,7 +4388,7 @@ def stake_childkey_take( netuid = IntPrompt.ask( "Enter netuid (leave blank for all)", default=None, show_default=True ) - return self._run_command( + results: list[tuple[Optional[int], bool]] = self._run_command( children_hotkeys.childkey_take( wallet=wallet, subtensor=self.initialize_chain(network), @@ -4053,6 +4400,12 @@ def stake_childkey_take( prompt=prompt, ) ) + if json_output: + output = {} + for netuid_, success in results: + output[netuid_] = success + json_console.print(json.dumps(output)) + return results def sudo_set( self, @@ -4069,6 +4422,7 @@ def sudo_set( ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Used to set hyperparameters for a specific subnet. @@ -4079,7 +4433,7 @@ def sudo_set( [green]$[/green] btcli sudo set --netuid 1 --param tempo --value 400 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not param_name or not param_value: hyperparams = self._run_command( @@ -4124,15 +4478,19 @@ def sudo_set( wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) - return self._run_command( + result = self._run_command( sudo.sudo_set_hyperparameter( wallet, self.initialize_chain(network), netuid, param_name, param_value, + json_output, ) ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def sudo_get( self, @@ -4140,6 +4498,7 @@ def sudo_get( netuid: int = Options.netuid, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Shows a list of the hyperparameters for the specified subnet. @@ -4148,9 +4507,11 @@ def sudo_get( [green]$[/green] btcli sudo get --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) return self._run_command( - sudo.get_hyperparameters(self.initialize_chain(network), netuid) + sudo.get_hyperparameters( + self.initialize_chain(network), netuid, json_output + ) ) def sudo_senate( @@ -4158,6 +4519,7 @@ def sudo_senate( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Shows the Senate members of the Bittensor's governance protocol. @@ -4167,14 +4529,17 @@ def sudo_senate( EXAMPLE [green]$[/green] btcli sudo senate """ - self.verbosity_handler(quiet, verbose) - return self._run_command(sudo.get_senate(self.initialize_chain(network))) + self.verbosity_handler(quiet, verbose, json_output) + return self._run_command( + sudo.get_senate(self.initialize_chain(network), json_output) + ) def sudo_proposals( self, network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ View active proposals for the senate in the Bittensor's governance protocol. @@ -4184,9 +4549,9 @@ def sudo_proposals( EXAMPLE [green]$[/green] btcli sudo proposals """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) return self._run_command( - sudo.proposals(self.initialize_chain(network), verbose) + sudo.proposals(self.initialize_chain(network), verbose, json_output) ) def sudo_senate_vote( @@ -4223,6 +4588,7 @@ def sudo_senate_vote( EXAMPLE [green]$[/green] btcli sudo senate_vote --proposal """ + # TODO discuss whether this should receive json_output. I don't think it should. self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( wallet_name, @@ -4246,6 +4612,7 @@ def sudo_set_take( take: float = typer.Option(None, help="The new take value."), quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Allows users to change their delegate take percentage. @@ -4258,7 +4625,7 @@ def sudo_set_take( """ max_value = 0.18 min_value = 0.00 - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, @@ -4283,9 +4650,12 @@ def sudo_set_take( ) raise typer.Exit() - return self._run_command( + result = self._run_command( sudo.set_take(wallet, self.initialize_chain(network), take) ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def sudo_get_take( self, @@ -4295,6 +4665,7 @@ def sudo_get_take( wallet_hotkey: Optional[str] = Options.wallet_hotkey, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Allows users to check their delegate take percentage. @@ -4304,7 +4675,7 @@ def sudo_get_take( EXAMPLE [green]$[/green] btcli sudo get-take --wallet-name my_wallet --wallet-hotkey my_hotkey """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, @@ -4313,10 +4684,15 @@ def sudo_get_take( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - - self._run_command( - sudo.display_current_take(self.initialize_chain(network), wallet) - ) + if json_output: + result = self._run_command( + sudo.get_current_take(self.initialize_chain(network), wallet) + ) + json_console.print(json.dumps({"current_take": result})) + else: + self._run_command( + sudo.display_current_take(self.initialize_chain(network), wallet) + ) def subnets_list( self, @@ -4324,6 +4700,7 @@ def subnets_list( quiet: bool = Options.quiet, verbose: bool = Options.verbose, live_mode: bool = Options.live, + json_output: bool = Options.json_output, ): """ List all subnets and their detailed information. @@ -4351,7 +4728,10 @@ def subnets_list( [green]$[/green] btcli subnets list """ - self.verbosity_handler(quiet, verbose) + if json_output and live_mode: + print_error("Cannot use `--json-output` and `--live` at the same time.") + return + self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) return self._run_command( subnets.subnets_list( @@ -4361,6 +4741,7 @@ def subnets_list( not self.config.get("use_cache", True), verbose, live_mode, + json_output, ) ) @@ -4393,6 +4774,9 @@ def subnets_price( help="Show the price in log scale.", ), html_output: bool = Options.html_output, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Shows the historical price of a subnet for the past 24 hours. @@ -4410,6 +4794,10 @@ def subnets_price( [green]$[/green] btcli subnets price --all --html [green]$[/green] btcli subnets price --netuids 1,2,3,4 --html """ + if json_output and html_output: + print_error("Cannot specify both `--json-output` and `--html`") + return + self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) if netuids: netuids = parse_to_list( netuids, @@ -4418,15 +4806,15 @@ def subnets_price( ) if all_netuids and netuids: print_error("Cannot specify both --netuid and --all-netuids") - raise typer.Exit() + return if not netuids and not all_netuids: netuids = Prompt.ask( - "Enter the [blue]netuid(s)[/blue] to view the price of in comma-separated format [dim](or Press Enter to view all subnets)[/dim]", + "Enter the [blue]netuid(s)[/blue] to view the price of in comma-separated format [dim]" + "(or Press Enter to view all subnets)[/dim]", ) if not netuids: all_netuids = True - html_output = True else: netuids = parse_to_list( netuids, @@ -4434,7 +4822,7 @@ def subnets_price( "Netuids must be a comma-separated list of ints, e.g., `--netuids 1,2,3,4`.", ) - if all_netuids: + if all_netuids and not json_output: html_output = True if html_output and is_linux(): @@ -4448,6 +4836,7 @@ def subnets_price( interval_hours, html_output, log_scale, + json_output, ) ) @@ -4463,6 +4852,7 @@ def subnets_show( quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, + json_output: bool = Options.json_output, ): """ Displays detailed information about a subnet including participants and their state. @@ -4471,7 +4861,7 @@ def subnets_show( [green]$[/green] btcli subnets list """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) return self._run_command( subnets.show( @@ -4482,6 +4872,7 @@ def subnets_show( delegate_selection=False, verbose=verbose, prompt=prompt, + json_output=json_output, ) ) @@ -4490,6 +4881,7 @@ def subnets_burn_cost( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Shows the required amount of TAO to be recycled for creating a new subnet, i.e., cost of registering a new subnet. @@ -4500,8 +4892,10 @@ def subnets_burn_cost( [green]$[/green] btcli subnets burn_cost """ - self.verbosity_handler(quiet, verbose) - return self._run_command(subnets.burn_cost(self.initialize_chain(network))) + self.verbosity_handler(quiet, verbose, json_output) + return self._run_command( + subnets.burn_cost(self.initialize_chain(network), json_output) + ) def subnets_create( self, @@ -4510,7 +4904,7 @@ def subnets_create( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, subnet_name: Optional[str] = typer.Option( - None, "--subnet-name", "--name", help="Name of the subnet" + None, "--subnet-name", help="Name of the subnet" ), github_repo: Optional[str] = typer.Option( None, "--github-repo", "--repo", help="GitHub repository URL" @@ -4534,6 +4928,7 @@ def subnets_create( additional_info: Optional[str] = typer.Option( None, "--additional-info", help="Additional information" ), + json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4552,7 +4947,7 @@ def subnets_create( 2. Create with GitHub repo and contact email: [green]$[/green] btcli subnets create --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -4574,34 +4969,19 @@ def subnets_create( description=description, additional=additional_info, ) - success = self._run_command( - subnets.create(wallet, self.initialize_chain(network), identity, prompt), - exit_early=False, + self._run_command( + subnets.create( + wallet, self.initialize_chain(network), identity, json_output, prompt + ) ) - if success and prompt: - set_id = Confirm.ask( - "[dark_sea_green3]Do you want to set/update your identity?", - default=False, - show_default=True, - ) - if set_id: - self.wallet_set_id( - wallet_name=wallet.name, - wallet_hotkey=wallet.hotkey, - wallet_path=wallet.path, - network=network, - prompt=prompt, - quiet=quiet, - verbose=verbose, - ) - def subnets_get_identity( self, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Get the identity information for a subnet. @@ -4610,11 +4990,10 @@ def subnets_get_identity( [green]$[/green] btcli subnets get-identity --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) return self._run_command( subnets.get_identity( - self.initialize_chain(network), - netuid, + self.initialize_chain(network), netuid, json_output=json_output ) ) @@ -4650,6 +5029,7 @@ def subnets_set_identity( additional_info: Optional[str] = typer.Option( None, "--additional-info", help="Additional information" ), + json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4667,7 +5047,7 @@ def subnets_set_identity( 2. Set subnet identity with specific values: [green]$[/green] btcli subnets set-identity --netuid 1 --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -4685,7 +5065,9 @@ def subnets_set_identity( exit_early=False, ) if current_identity is None: - raise typer.Exit() + if json_output: + json_console.print('{"success": false}') + return identity = prompt_for_subnet_identity( current_identity=current_identity, @@ -4698,15 +5080,13 @@ def subnets_set_identity( additional=additional_info, ) - return self._run_command( + success = self._run_command( subnets.set_identity( - wallet, - self.initialize_chain(network), - netuid, - identity, - prompt, + wallet, self.initialize_chain(network), netuid, identity, prompt ) ) + if json_output: + json_console.print(json.dumps({"success": success})) def subnets_pow_register( self, @@ -4804,6 +5184,14 @@ def subnets_register( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, + era: Optional[ + int + ] = typer.Option( # Should not be Options.era bc this needs to be an Optional[int] + None, + help="Length (in blocks) for which the transaction should be valid. Note that it is possible that if you " + "use an era for this transaction that you may pay a different fee to register than the one stated.", + ), + json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4819,7 +5207,7 @@ def subnets_register( [green]$[/green] btcli subnets register --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -4832,6 +5220,8 @@ def subnets_register( wallet, self.initialize_chain(network), netuid, + era, + json_output, prompt, ) ) @@ -4957,6 +5347,7 @@ def weights_reveal( "-s", help="Corresponding salt for the hash function, e.g. -s 163,241,217 ...", ), + json_output: bool = Options.json_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -4970,7 +5361,7 @@ def weights_reveal( [green]$[/green] btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) uids = list_prompt(uids, int, "UIDs of interest for the specified netuid") weights = list_prompt( weights, float, "Corresponding weights for the specified UIDs" @@ -5003,7 +5394,7 @@ def weights_reveal( err_console.print( "The number of UIDs you specify must match up with the specified number of weights" ) - raise typer.Exit() + return if salt: salt = parse_to_list( @@ -5032,6 +5423,7 @@ def weights_reveal( salt, __version_as_int__, prompt=prompt, + json_output=json_output, ) ) @@ -5055,6 +5447,7 @@ def weights_commit( "-s", help="Corresponding salt for the hash function, e.g. -s 163 -s 241 -s 217 ...", ), + json_output: bool = Options.json_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -5072,7 +5465,7 @@ def weights_commit( [italic]Note[/italic]: This command is used to commit weights for a specific subnet and requires the user to have the necessary permissions. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if uids: uids = parse_to_list( @@ -5101,7 +5494,7 @@ def weights_commit( err_console.print( "The number of UIDs you specify must match up with the specified number of weights" ) - raise typer.Exit() + return if salt: salt = parse_to_list( @@ -5128,6 +5521,7 @@ def weights_commit( weights, salt, __version_as_int__, + json_output=json_output, prompt=prompt, ) ) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 1ecd29a3..e8dcdc53 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -33,6 +33,10 @@ class Constants: "latent-lite": latent_lite_entrypoint, "subvortex": subvortex_entrypoint, } + genesis_block_hash_map = { + "finney": "0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03", + "test": "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105", + } delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" @@ -164,471 +168,446 @@ class WalletValidationTypes(Enum): } UNITS = [ - # Greek Alphabet (0-24) - "\u03c4", # τ (tau, 0) - "\u03b1", # α (alpha, 1) - "\u03b2", # β (beta, 2) - "\u03b3", # γ (gamma, 3) - "\u03b4", # δ (delta, 4) - "\u03b5", # ε (epsilon, 5) - "\u03b6", # ζ (zeta, 6) - "\u03b7", # η (eta, 7) - "\u03b8", # θ (theta, 8) - "\u03b9", # ι (iota, 9) - "\u03ba", # κ (kappa, 10) - "\u03bb", # λ (lambda, 11) - "\u03bc", # μ (mu, 12) - "\u03bd", # ν (nu, 13) - "\u03be", # ξ (xi, 14) - "\u03bf", # ο (omicron, 15) - "\u03c0", # π (pi, 16) - "\u03c1", # ρ (rho, 17) - "\u03c3", # σ (sigma, 18) - "t", # t (tau, 19) - "\u03c5", # υ (upsilon, 20) - "\u03c6", # φ (phi, 21) - "\u03c7", # χ (chi, 22) - "\u03c8", # ψ (psi, 23) - "\u03c9", # ω (omega, 24) - # Hebrew Alphabet (25-51) - "\u05d0", # א (aleph, 25) - "\u05d1", # ב (bet, 26) - "\u05d2", # ג (gimel, 27) - "\u05d3", # ד (dalet, 28) - "\u05d4", # ה (he, 29) - "\u05d5", # ו (vav, 30) - "\u05d6", # ז (zayin, 31) - "\u05d7", # ח (het, 32) - "\u05d8", # ט (tet, 33) - "\u05d9", # י (yod, 34) - "\u05da", # ך (final kaf, 35) - "\u05db", # כ (kaf, 36) - "\u05dc", # ל (lamed, 37) - "\u05dd", # ם (final mem, 38) - "\u05de", # מ (mem, 39) - "\u05df", # ן (final nun, 40) - "\u05e0", # נ (nun, 41) - "\u05e1", # ס (samekh, 42) - "\u05e2", # ע (ayin, 43) - "\u05e3", # ף (final pe, 44) - "\u05e4", # פ (pe, 45) - "\u05e5", # ץ (final tsadi, 46) - "\u05e6", # צ (tsadi, 47) - "\u05e7", # ק (qof, 48) - "\u05e8", # ר (resh, 49) - "\u05e9", # ש (shin, 50) - "\u05ea", # ת (tav, 51) - # Arabic Alphabet (52-81) - "\u0627", # ا (alif, 52) - "\u0628", # ب (ba, 53) - "\u062a", # ت (ta, 54) - "\u062b", # ث (tha, 55) - "\u062c", # ج (jeem, 56) - "\u062d", # ح (ha, 57) - "\u062e", # خ (kha, 58) - "\u062f", # د (dal, 59) - "\u0630", # ذ (dhal, 60) - "\u0631", # ر (ra, 61) - "\u0632", # ز (zay, 62) - "\u0633", # س (seen, 63) - "\u0634", # ش (sheen, 64) - "\u0635", # ص (sad, 65) - "\u0636", # ض (dad, 66) - "\u0637", # ط (ta, 67) - "\u0638", # ظ (dha, 68) - "\u0639", # ع (ain, 69) - "\u063a", # غ (ghain, 70) - "\u0641", # ف (fa, 71) - "\u0642", # ق (qaf, 72) - "\u0643", # ك (kaf, 73) - "\u0644", # ل (lam, 74) - "\u0645", # م (meem, 75) - "\u0646", # ن (noon, 76) - "\u0647", # ه (ha, 77) - "\u0648", # و (waw, 78) - "\u064a", # ي (ya, 79) - "\u0649", # ى (alef maksura, 80) - "\u064a", # ي (ya, 81) - # Runic Alphabet (82-90) - "\u16a0", # ᚠ (fehu, 82) - "\u16a2", # ᚢ (uruz, 83) - "\u16a6", # ᚦ (thurisaz, 84) - "\u16a8", # ᚨ (ansuz, 85) - "\u16b1", # ᚱ (raidho, 86) - "\u16b3", # ᚲ (kaunan, 87) - "\u16c7", # ᛇ (eihwaz, 88) - "\u16c9", # ᛉ (algiz, 89) - "\u16d2", # ᛒ (berkanan, 90) - # Ogham Alphabet (91-97) - "\u1680", #   (Space, 91) - "\u1681", # ᚁ (Beith, 92) - "\u1682", # ᚂ (Luis, 93) - "\u1683", # ᚃ (Fearn, 94) - "\u1684", # ᚄ (Sail, 95) - "\u1685", # ᚅ (Nion, 96) - "\u169b", # ᚛ (Forfeda, 97) - # Georgian Alphabet (98-103) - "\u10d0", # ა (ani, 98) - "\u10d1", # ბ (bani, 99) - "\u10d2", # გ (gani, 100) - "\u10d3", # დ (doni, 101) - "\u10d4", # ე (eni, 102) - "\u10d5", # ვ (vini, 103) - # Armenian Alphabet (104-110) - "\u0531", # Ա (Ayp, 104) - "\u0532", # Բ (Ben, 105) - "\u0533", # Գ (Gim, 106) - "\u0534", # Դ (Da, 107) - "\u0535", # Ե (Ech, 108) - "\u0536", # Զ (Za, 109) - "\u055e", # ՞ (Question mark, 110) - # Cyrillic Alphabet (111-116) - "\u0400", # Ѐ (Ie with grave, 111) - "\u0401", # Ё (Io, 112) - "\u0402", # Ђ (Dje, 113) - "\u0403", # Ѓ (Gje, 114) - "\u0404", # Є (Ukrainian Ie, 115) - "\u0405", # Ѕ (Dze, 116) - # Coptic Alphabet (117-122) - "\u2c80", # Ⲁ (Alfa, 117) - "\u2c81", # ⲁ (Small Alfa, 118) - "\u2c82", # Ⲃ (Vida, 119) - "\u2c83", # ⲃ (Small Vida, 120) - "\u2c84", # Ⲅ (Gamma, 121) - "\u2c85", # ⲅ (Small Gamma, 122) - # Brahmi Script (123-127) - "\U00011000", # 𑀀 (A, 123) - "\U00011001", # 𑀁 (Aa, 124) - "\U00011002", # 𑀂 (I, 125) - "\U00011003", # 𑀃 (Ii, 126) - "\U00011005", # 𑀅 (U, 127) - # Tifinagh Alphabet (128-133) - "\u2d30", # ⴰ (Ya, 128) - "\u2d31", # ⴱ (Yab, 129) - "\u2d32", # ⴲ (Yabh, 130) - "\u2d33", # ⴳ (Yag, 131) - "\u2d34", # ⴴ (Yagh, 132) - "\u2d35", # ⴵ (Yaj, 133) - # Glagolitic Alphabet (134-166) - "\u2c00", # Ⰰ (Az, 134) - "\u2c01", # Ⰱ (Buky, 135) - "\u2c02", # Ⰲ (Vede, 136) - "\u2c03", # Ⰳ (Glagoli, 137) - "\u2c04", # Ⰴ (Dobro, 138) - "\u2c05", # Ⰵ (Yest, 139) - "\u2c06", # Ⰶ (Zhivete, 140) - "\u2c07", # Ⰷ (Zemlja, 141) - "\u2c08", # Ⰸ (Izhe, 142) - "\u2c09", # Ⰹ (Initial Izhe, 143) - "\u2c0a", # Ⰺ (I, 144) - "\u2c0b", # Ⰻ (Djerv, 145) - "\u2c0c", # Ⰼ (Kako, 146) - "\u2c0d", # Ⰽ (Ljudije, 147) - "\u2c0e", # Ⰾ (Myse, 148) - "\u2c0f", # Ⰿ (Nash, 149) - "\u2c10", # Ⱀ (On, 150) - "\u2c11", # Ⱁ (Pokoj, 151) - "\u2c12", # Ⱂ (Rtsy, 152) - "\u2c13", # Ⱃ (Slovo, 153) - "\u2c14", # Ⱄ (Tvrido, 154) - "\u2c15", # Ⱅ (Uku, 155) - "\u2c16", # Ⱆ (Fert, 156) - "\u2c17", # Ⱇ (Xrivi, 157) - "\u2c18", # Ⱈ (Ot, 158) - "\u2c19", # Ⱉ (Cy, 159) - "\u2c1a", # Ⱊ (Shcha, 160) - "\u2c1b", # Ⱋ (Er, 161) - "\u2c1c", # Ⱌ (Yeru, 162) - "\u2c1d", # Ⱍ (Small Yer, 163) - "\u2c1e", # Ⱎ (Yo, 164) - "\u2c1f", # Ⱏ (Yu, 165) - "\u2c20", # Ⱐ (Ja, 166) - # Thai Alphabet (167-210) - "\u0e01", # ก (Ko Kai, 167) - "\u0e02", # ข (Kho Khai, 168) - "\u0e03", # ฃ (Kho Khuat, 169) - "\u0e04", # ค (Kho Khon, 170) - "\u0e05", # ฅ (Kho Rakhang, 171) - "\u0e06", # ฆ (Kho Khwai, 172) - "\u0e07", # ง (Ngo Ngu, 173) - "\u0e08", # จ (Cho Chan, 174) - "\u0e09", # ฉ (Cho Ching, 175) - "\u0e0a", # ช (Cho Chang, 176) - "\u0e0b", # ซ (So So, 177) - "\u0e0c", # ฌ (Cho Choe, 178) - "\u0e0d", # ญ (Yo Ying, 179) - "\u0e0e", # ฎ (Do Chada, 180) - "\u0e0f", # ฏ (To Patak, 181) - "\u0e10", # ฐ (Tho Than, 182) - "\u0e11", # ฑ (Tho Nangmontho, 183) - "\u0e12", # ฒ (Tho Phuthao, 184) - "\u0e13", # ณ (No Nen, 185) - "\u0e14", # ด (Do Dek, 186) - "\u0e15", # ต (To Tao, 187) - "\u0e16", # ถ (Tho Thung, 188) - "\u0e17", # ท (Tho Thahan, 189) - "\u0e18", # ธ (Tho Thong, 190) - "\u0e19", # น (No Nu, 191) - "\u0e1a", # บ (Bo Baimai, 192) - "\u0e1b", # ป (Po Pla, 193) - "\u0e1c", # ผ (Pho Phung, 194) - "\u0e1d", # ฝ (Fo Fa, 195) - "\u0e1e", # พ (Pho Phan, 196) - "\u0e1f", # ฟ (Fo Fan, 197) - "\u0e20", # ภ (Pho Samphao, 198) - "\u0e21", # ม (Mo Ma, 199) - "\u0e22", # ย (Yo Yak, 200) - "\u0e23", # ร (Ro Rua, 201) - "\u0e25", # ล (Lo Ling, 202) - "\u0e27", # ว (Wo Waen, 203) - "\u0e28", # ศ (So Sala, 204) - "\u0e29", # ษ (So Rusi, 205) - "\u0e2a", # ส (So Sua, 206) - "\u0e2b", # ห (Ho Hip, 207) - "\u0e2c", # ฬ (Lo Chula, 208) - "\u0e2d", # อ (O Ang, 209) - "\u0e2e", # ฮ (Ho Nokhuk, 210) - # Hangul Consonants (211-224) - "\u1100", # ㄱ (Giyeok, 211) - "\u1101", # ㄴ (Nieun, 212) - "\u1102", # ㄷ (Digeut, 213) - "\u1103", # ㄹ (Rieul, 214) - "\u1104", # ㅁ (Mieum, 215) - "\u1105", # ㅂ (Bieup, 216) - "\u1106", # ㅅ (Siot, 217) - "\u1107", # ㅇ (Ieung, 218) - "\u1108", # ㅈ (Jieut, 219) - "\u1109", # ㅊ (Chieut, 220) - "\u110a", # ㅋ (Kieuk, 221) - "\u110b", # ㅌ (Tieut, 222) - "\u110c", # ㅍ (Pieup, 223) - "\u110d", # ㅎ (Hieut, 224) - # Hangul Vowels (225-245) - "\u1161", # ㅏ (A, 225) - "\u1162", # ㅐ (Ae, 226) - "\u1163", # ㅑ (Ya, 227) - "\u1164", # ㅒ (Yae, 228) - "\u1165", # ㅓ (Eo, 229) - "\u1166", # ㅔ (E, 230) - "\u1167", # ㅕ (Yeo, 231) - "\u1168", # ㅖ (Ye, 232) - "\u1169", # ㅗ (O, 233) - "\u116a", # ㅘ (Wa, 234) - "\u116b", # ㅙ (Wae, 235) - "\u116c", # ㅚ (Oe, 236) - "\u116d", # ㅛ (Yo, 237) - "\u116e", # ㅜ (U, 238) - "\u116f", # ㅝ (Weo, 239) - "\u1170", # ㅞ (We, 240) - "\u1171", # ㅟ (Wi, 241) - "\u1172", # ㅠ (Yu, 242) - "\u1173", # ㅡ (Eu, 243) - "\u1174", # ㅢ (Ui, 244) - "\u1175", # ㅣ (I, 245) - # Ethiopic Alphabet (246-274) - "\u12a0", # አ (Glottal A, 246) - "\u12a1", # ኡ (Glottal U, 247) - "\u12a2", # ኢ (Glottal I, 248) - "\u12a3", # ኣ (Glottal Aa, 249) - "\u12a4", # ኤ (Glottal E, 250) - "\u12a5", # እ (Glottal Ie, 251) - "\u12a6", # ኦ (Glottal O, 252) - "\u12a7", # ኧ (Glottal Wa, 253) - "\u12c8", # ወ (Wa, 254) - "\u12c9", # ዉ (Wu, 255) - "\u12ca", # ዊ (Wi, 256) - "\u12cb", # ዋ (Waa, 257) - "\u12cc", # ዌ (We, 258) - "\u12cd", # ው (Wye, 259) - "\u12ce", # ዎ (Wo, 260) - "\u12b0", # ኰ (Ko, 261) - "\u12b1", # ኱ (Ku, 262) - "\u12b2", # ኲ (Ki, 263) - "\u12b3", # ኳ (Kua, 264) - "\u12b4", # ኴ (Ke, 265) - "\u12b5", # ኵ (Kwe, 266) - "\u12b6", # ኶ (Ko, 267) - "\u12a0", # ጐ (Go, 268) - "\u12a1", # ጑ (Gu, 269) - "\u12a2", # ጒ (Gi, 270) - "\u12a3", # መ (Gua, 271) - "\u12a4", # ጔ (Ge, 272) - "\u12a5", # ጕ (Gwe, 273) - "\u12a6", # ጖ (Go, 274) - # Devanagari Alphabet (275-318) - "\u0905", # अ (A, 275) - "\u0906", # आ (Aa, 276) - "\u0907", # इ (I, 277) - "\u0908", # ई (Ii, 278) - "\u0909", # उ (U, 279) - "\u090a", # ऊ (Uu, 280) - "\u090b", # ऋ (R, 281) - "\u090f", # ए (E, 282) - "\u0910", # ऐ (Ai, 283) - "\u0913", # ओ (O, 284) - "\u0914", # औ (Au, 285) - "\u0915", # क (Ka, 286) - "\u0916", # ख (Kha, 287) - "\u0917", # ग (Ga, 288) - "\u0918", # घ (Gha, 289) - "\u0919", # ङ (Nga, 290) - "\u091a", # च (Cha, 291) - "\u091b", # छ (Chha, 292) - "\u091c", # ज (Ja, 293) - "\u091d", # झ (Jha, 294) - "\u091e", # ञ (Nya, 295) - "\u091f", # ट (Ta, 296) - "\u0920", # ठ (Tha, 297) - "\u0921", # ड (Da, 298) - "\u0922", # ढ (Dha, 299) - "\u0923", # ण (Na, 300) - "\u0924", # त (Ta, 301) - "\u0925", # थ (Tha, 302) - "\u0926", # द (Da, 303) - "\u0927", # ध (Dha, 304) - "\u0928", # न (Na, 305) - "\u092a", # प (Pa, 306) - "\u092b", # फ (Pha, 307) - "\u092c", # ब (Ba, 308) - "\u092d", # भ (Bha, 309) - "\u092e", # म (Ma, 310) - "\u092f", # य (Ya, 311) - "\u0930", # र (Ra, 312) - "\u0932", # ल (La, 313) - "\u0935", # व (Va, 314) - "\u0936", # श (Sha, 315) - "\u0937", # ष (Ssa, 316) - "\u0938", # स (Sa, 317) - "\u0939", # ह (Ha, 318) - # Katakana Alphabet (319-364) - "\u30a2", # ア (A, 319) - "\u30a4", # イ (I, 320) - "\u30a6", # ウ (U, 321) - "\u30a8", # エ (E, 322) - "\u30aa", # オ (O, 323) - "\u30ab", # カ (Ka, 324) - "\u30ad", # キ (Ki, 325) - "\u30af", # ク (Ku, 326) - "\u30b1", # ケ (Ke, 327) - "\u30b3", # コ (Ko, 328) - "\u30b5", # サ (Sa, 329) - "\u30b7", # シ (Shi, 330) - "\u30b9", # ス (Su, 331) - "\u30bb", # セ (Se, 332) - "\u30bd", # ソ (So, 333) - "\u30bf", # タ (Ta, 334) - "\u30c1", # チ (Chi, 335) - "\u30c4", # ツ (Tsu, 336) - "\u30c6", # テ (Te, 337) - "\u30c8", # ト (To, 338) - "\u30ca", # ナ (Na, 339) - "\u30cb", # ニ (Ni, 340) - "\u30cc", # ヌ (Nu, 341) - "\u30cd", # ネ (Ne, 342) - "\u30ce", # ノ (No, 343) - "\u30cf", # ハ (Ha, 344) - "\u30d2", # ヒ (Hi, 345) - "\u30d5", # フ (Fu, 346) - "\u30d8", # ヘ (He, 347) - "\u30db", # ホ (Ho, 348) - "\u30de", # マ (Ma, 349) - "\u30df", # ミ (Mi, 350) - "\u30e0", # ム (Mu, 351) - "\u30e1", # メ (Me, 352) - "\u30e2", # モ (Mo, 353) - "\u30e4", # ヤ (Ya, 354) - "\u30e6", # ユ (Yu, 355) - "\u30e8", # ヨ (Yo, 356) - "\u30e9", # ラ (Ra, 357) - "\u30ea", # リ (Ri, 358) - "\u30eb", # ル (Ru, 359) - "\u30ec", # レ (Re, 360) - "\u30ed", # ロ (Ro, 361) - "\u30ef", # ワ (Wa, 362) - "\u30f2", # ヲ (Wo, 363) - "\u30f3", # ン (N, 364) - # Tifinagh Alphabet (365-400) - "\u2d30", # ⴰ (Ya, 365) - "\u2d31", # ⴱ (Yab, 366) - "\u2d32", # ⴲ (Yabh, 367) - "\u2d33", # ⴳ (Yag, 368) - "\u2d34", # ⴴ (Yagh, 369) - "\u2d35", # ⴵ (Yaj, 370) - "\u2d36", # ⴶ (Yach, 371) - "\u2d37", # ⴷ (Yad, 372) - "\u2d38", # ⴸ (Yadh, 373) - "\u2d39", # ⴹ (Yadh, emphatic, 374) - "\u2d3a", # ⴺ (Yaz, 375) - "\u2d3b", # ⴻ (Yazh, 376) - "\u2d3c", # ⴼ (Yaf, 377) - "\u2d3d", # ⴽ (Yak, 378) - "\u2d3e", # ⴾ (Yak, variant, 379) - "\u2d3f", # ⴿ (Yaq, 380) - "\u2d40", # ⵀ (Yah, 381) - "\u2d41", # ⵁ (Yahh, 382) - "\u2d42", # ⵂ (Yahl, 383) - "\u2d43", # ⵃ (Yahm, 384) - "\u2d44", # ⵄ (Yayn, 385) - "\u2d45", # ⵅ (Yakh, 386) - "\u2d46", # ⵆ (Yakl, 387) - "\u2d47", # ⵇ (Yahq, 388) - "\u2d48", # ⵈ (Yash, 389) - "\u2d49", # ⵉ (Yi, 390) - "\u2d4a", # ⵊ (Yij, 391) - "\u2d4b", # ⵋ (Yizh, 392) - "\u2d4c", # ⵌ (Yink, 393) - "\u2d4d", # ⵍ (Yal, 394) - "\u2d4e", # ⵎ (Yam, 395) - "\u2d4f", # ⵏ (Yan, 396) - "\u2d50", # ⵐ (Yang, 397) - "\u2d51", # ⵑ (Yany, 398) - "\u2d52", # ⵒ (Yap, 399) - "\u2d53", # ⵓ (Yu, 400) - # Sinhala Alphabet (401-444) - "\u0d85", # අ (A, 401) - "\u0d86", # ආ (Aa, 402) - "\u0d87", # ඉ (I, 403) - "\u0d88", # ඊ (Ii, 404) - "\u0d89", # උ (U, 405) - "\u0d8a", # ඌ (Uu, 406) - "\u0d8b", # ඍ (R, 407) - "\u0d8c", # ඎ (Rr, 408) - "\u0d8f", # ඏ (L, 409) - "\u0d90", # ඐ (Ll, 410) - "\u0d91", # එ (E, 411) - "\u0d92", # ඒ (Ee, 412) - "\u0d93", # ඓ (Ai, 413) - "\u0d94", # ඔ (O, 414) - "\u0d95", # ඕ (Oo, 415) - "\u0d96", # ඖ (Au, 416) - "\u0d9a", # ක (Ka, 417) - "\u0d9b", # ඛ (Kha, 418) - "\u0d9c", # ග (Ga, 419) - "\u0d9d", # ඝ (Gha, 420) - "\u0d9e", # ඞ (Nga, 421) - "\u0d9f", # ච (Cha, 422) - "\u0da0", # ඡ (Chha, 423) - "\u0da1", # ජ (Ja, 424) - "\u0da2", # ඣ (Jha, 425) - "\u0da3", # ඤ (Nya, 426) - "\u0da4", # ට (Ta, 427) - "\u0da5", # ඥ (Tha, 428) - "\u0da6", # ඦ (Da, 429) - "\u0da7", # ට (Dha, 430) - "\u0da8", # ඨ (Na, 431) - "\u0daa", # ඪ (Pa, 432) - "\u0dab", # ණ (Pha, 433) - "\u0dac", # ඬ (Ba, 434) - "\u0dad", # ත (Bha, 435) - "\u0dae", # ථ (Ma, 436) - "\u0daf", # ද (Ya, 437) - "\u0db0", # ධ (Ra, 438) - "\u0db1", # ඲ (La, 439) - "\u0db2", # ඳ (Va, 440) - "\u0db3", # ප (Sha, 441) - "\u0db4", # ඵ (Ssa, 442) - "\u0db5", # බ (Sa, 443) - "\u0db6", # භ (Ha, 444) + b"\xCE\xA4".decode(), # Τ (Upper case Tau, 0) + b"\xCE\xB1".decode(), # α (Alpha, 1) + b"\xCE\xB2".decode(), # β (Beta, 2) + b"\xCE\xB3".decode(), # γ (Gamma, 3) + b"\xCE\xB4".decode(), # δ (Delta, 4) + b"\xCE\xB5".decode(), # ε (Epsilon, 5) + b"\xCE\xB6".decode(), # ζ (Zeta, 6) + b"\xCE\xB7".decode(), # η (Eta, 7) + b"\xCE\xB8".decode(), # θ (Theta, 8) + b"\xCE\xB9".decode(), # ι (Iota, 9) + b"\xCE\xBA".decode(), # κ (Kappa, 10) + b"\xCE\xBB".decode(), # λ (Lambda, 11) + b"\xCE\xBC".decode(), # μ (Mu, 12) + b"\xCE\xBD".decode(), # ν (Nu, 13) + b"\xCE\xBE".decode(), # ξ (Xi, 14) + b"\xCE\xBF".decode(), # ο (Omicron, 15) + b"\xCF\x80".decode(), # π (Pi, 16) + b"\xCF\x81".decode(), # ρ (Rho, 17) + b"\xCF\x83".decode(), # σ (Sigma, 18) + "t", # t (Tau, 19) + b"\xCF\x85".decode(), # υ (Upsilon, 20) + b"\xCF\x86".decode(), # φ (Phi, 21) + b"\xCF\x87".decode(), # χ (Chi, 22) + b"\xCF\x88".decode(), # ψ (Psi, 23) + b"\xCF\x89".decode(), # ω (Omega, 24) + b"\xD7\x90".decode(), # א (Aleph, 25) + b"\xD7\x91".decode(), # ב (Bet, 26) + b"\xD7\x92".decode(), # ג (Gimel, 27) + b"\xD7\x93".decode(), # ד (Dalet, 28) + b"\xD7\x94".decode(), # ה (He, 29) + b"\xD7\x95".decode(), # ו (Vav, 30) + b"\xD7\x96".decode(), # ז (Zayin, 31) + b"\xD7\x97".decode(), # ח (Het, 32) + b"\xD7\x98".decode(), # ט (Tet, 33) + b"\xD7\x99".decode(), # י (Yod, 34) + b"\xD7\x9A".decode(), # ך (Final Kaf, 35) + b"\xD7\x9B".decode(), # כ (Kaf, 36) + b"\xD7\x9C".decode(), # ל (Lamed, 37) + b"\xD7\x9D".decode(), # ם (Final Mem, 38) + b"\xD7\x9E".decode(), # מ (Mem, 39) + b"\xD7\x9F".decode(), # ן (Final Nun, 40) + b"\xD7\xA0".decode(), # נ (Nun, 41) + b"\xD7\xA1".decode(), # ס (Samekh, 42) + b"\xD7\xA2".decode(), # ע (Ayin, 43) + b"\xD7\xA3".decode(), # ף (Final Pe, 44) + b"\xD7\xA4".decode(), # פ (Pe, 45) + b"\xD7\xA5".decode(), # ץ (Final Tsadi, 46) + b"\xD7\xA6".decode(), # צ (Tsadi, 47) + b"\xD7\xA7".decode(), # ק (Qof, 48) + b"\xD7\xA8".decode(), # ר (Resh, 49) + b"\xD7\xA9".decode(), # ש (Shin, 50) + b"\xD7\xAA".decode(), # ת (Tav, 51) + b"\xD8\xA7".decode(), # ا (Alif, 52) + b"\xD8\xA8".decode(), # ب (Ba, 53) + b"\xD8\xAA".decode(), # ت (Ta, 54) + b"\xD8\xAB".decode(), # ث (Tha, 55) + b"\xD8\xAC".decode(), # ج (Jim, 56) + b"\xD8\xAD".decode(), # ح (Ha, 57) + b"\xD8\xAE".decode(), # خ (Kha, 58) + b"\xD8\xAF".decode(), # د (Dal, 59) + b"\xD8\xB0".decode(), # ذ (Dhal, 60) + b"\xD8\xB1".decode(), # ر (Ra, 61) + b"\xD8\xB2".decode(), # ز (Zay, 62) + b"\xD8\xB3".decode(), # س (Sin, 63) + b"\xD8\xB4".decode(), # ش (Shin, 64) + b"\xD8\xB5".decode(), # ص (Sad, 65) + b"\xD8\xB6".decode(), # ض (Dad, 66) + b"\xD8\xB7".decode(), # ط (Ta, 67) + b"\xD8\xB8".decode(), # ظ (Dha, 68) + b"\xD8\xB9".decode(), # ع (Ain, 69) + b"\xD8\xBA".decode(), # غ (Ghayn, 70) + b"\xD9\x81".decode(), # ف (Fa, 71) + b"\xD9\x82".decode(), # ق (Qaf, 72) + b"\xD9\x83".decode(), # ك (Kaf, 73) + b"\xD9\x84".decode(), # ل (Lam, 74) + b"\xD9\x85".decode(), # م (Mim, 75) + b"\xD9\x86".decode(), # ن (Nun, 76) + b"\xD9\x87".decode(), # ه (Ha, 77) + b"\xD9\x88".decode(), # و (Waw, 78) + b"\xD9\x8A".decode(), # ي (Ya, 79) + b"\xD9\x89".decode(), # ى (Alef Maksura, 80) + b"\xE1\x9A\xA0".decode(), # ᚠ (Fehu, wealth, 81) + b"\xE1\x9A\xA2".decode(), # ᚢ (Uruz, strength, 82) + b"\xE1\x9A\xA6".decode(), # ᚦ (Thurisaz, giant, 83) + b"\xE1\x9A\xA8".decode(), # ᚨ (Ansuz, god, 84) + b"\xE1\x9A\xB1".decode(), # ᚱ (Raidho, ride, 85) + b"\xE1\x9A\xB3".decode(), # ᚲ (Kaunan, ulcer, 86) + b"\xD0\xAB".decode(), # Ы (Cyrillic Yeru, 87) + b"\xE1\x9B\x89".decode(), # ᛉ (Algiz, protection, 88) + b"\xE1\x9B\x92".decode(), # ᛒ (Berkanan, birch, 89) + b"\xE1\x9A\x80".decode(), #   (Space, 90) + b"\xE1\x9A\x81".decode(), # ᚁ (Beith, birch, 91) + b"\xE1\x9A\x82".decode(), # ᚂ (Luis, rowan, 92) + b"\xE1\x9A\x83".decode(), # ᚃ (Fearn, alder, 93) + b"\xE1\x9A\x84".decode(), # ᚄ (Sail, willow, 94) + b"\xE1\x9A\x85".decode(), # ᚅ (Nion, ash, 95) + b"\xE1\x9A\x9B".decode(), # ᚛ (Forfeda, 96) + b"\xE1\x83\x90".decode(), # ა (Ani, 97) + b"\xE1\x83\x91".decode(), # ბ (Bani, 98) + b"\xE1\x83\x92".decode(), # გ (Gani, 99) + b"\xE1\x83\x93".decode(), # დ (Doni, 100) + b"\xE1\x83\x94".decode(), # ე (Eni, 101) + b"\xE1\x83\x95".decode(), # ვ (Vini, 102) + b"\xD4\xB1".decode(), # Ա (Ayp, 103) + b"\xD4\xB2".decode(), # Բ (Ben, 104) + b"\xD4\xB3".decode(), # Գ (Gim, 105) + b"\xD4\xB4".decode(), # Դ (Da, 106) + b"\xD4\xB5".decode(), # Ե (Ech, 107) + b"\xD4\xB6".decode(), # Զ (Za, 108) + b"\xD5\x9E".decode(), # ՞ (Question mark, 109) + b"\xD0\x80".decode(), # Ѐ (Ie with grave, 110) + b"\xD0\x81".decode(), # Ё (Io, 111) + b"\xD0\x82".decode(), # Ђ (Dje, 112) + b"\xD0\x83".decode(), # Ѓ (Gje, 113) + b"\xD0\x84".decode(), # Є (Ukrainian Ie, 114) + b"\xD0\x85".decode(), # Ѕ (Dze, 115) + b"\xD1\x8A".decode(), # Ъ (Hard sign, 116) + b"\xE2\xB2\x80".decode(), # Ⲁ (Alfa, 117) + b"\xE2\xB2\x81".decode(), # ⲁ (Small Alfa, 118) + b"\xE2\xB2\x82".decode(), # Ⲃ (Vida, 119) + b"\xE2\xB2\x83".decode(), # ⲃ (Small Vida, 120) + b"\xE2\xB2\x84".decode(), # Ⲅ (Gamma, 121) + b"\xE2\xB2\x85".decode(), # ⲅ (Small Gamma, 122) + b"\xF0\x91\x80\x80".decode(), # 𑀀 (A, 123) + b"\xF0\x91\x80\x81".decode(), # 𑀁 (Aa, 124) + b"\xF0\x91\x80\x82".decode(), # 𑀂 (I, 125) + b"\xF0\x91\x80\x83".decode(), # 𑀃 (Ii, 126) + b"\xF0\x91\x80\x85".decode(), # 𑀅 (U, 127) + b"\xE0\xB6\xB1".decode(), # ඲ (La, 128) + b"\xE0\xB6\xB2".decode(), # ඳ (Va, 129) + b"\xE0\xB6\xB3".decode(), # ප (Sha, 130) + b"\xE0\xB6\xB4".decode(), # ඵ (Ssa, 131) + b"\xE0\xB6\xB5".decode(), # බ (Sa, 132) + b"\xE0\xB6\xB6".decode(), # භ (Ha, 133) + b"\xE2\xB0\x80".decode(), # Ⰰ (Az, 134) + b"\xE2\xB0\x81".decode(), # Ⰱ (Buky, 135) + b"\xE2\xB0\x82".decode(), # Ⰲ (Vede, 136) + b"\xE2\xB0\x83".decode(), # Ⰳ (Glagoli, 137) + b"\xE2\xB0\x84".decode(), # Ⰴ (Dobro, 138) + b"\xE2\xB0\x85".decode(), # Ⰵ (Yest, 139) + b"\xE2\xB0\x86".decode(), # Ⰶ (Zhivete, 140) + b"\xE2\xB0\x87".decode(), # Ⰷ (Zemlja, 141) + b"\xE2\xB0\x88".decode(), # Ⰸ (Izhe, 142) + b"\xE2\xB0\x89".decode(), # Ⰹ (Initial Izhe, 143) + b"\xE2\xB0\x8A".decode(), # Ⰺ (I, 144) + b"\xE2\xB0\x8B".decode(), # Ⰻ (Djerv, 145) + b"\xE2\xB0\x8C".decode(), # Ⰼ (Kako, 146) + b"\xE2\xB0\x8D".decode(), # Ⰽ (Ljudije, 147) + b"\xE2\xB0\x8E".decode(), # Ⰾ (Myse, 148) + b"\xE2\xB0\x8F".decode(), # Ⰿ (Nash, 149) + b"\xE2\xB0\x90".decode(), # Ⱀ (On, 150) + b"\xE2\xB0\x91".decode(), # Ⱁ (Pokoj, 151) + b"\xE2\xB0\x92".decode(), # Ⱂ (Rtsy, 152) + b"\xE2\xB0\x93".decode(), # Ⱃ (Slovo, 153) + b"\xE2\xB0\x94".decode(), # Ⱄ (Tvrido, 154) + b"\xE2\xB0\x95".decode(), # Ⱅ (Uku, 155) + b"\xE2\xB0\x96".decode(), # Ⱆ (Fert, 156) + b"\xE2\xB0\x97".decode(), # Ⱇ (Xrivi, 157) + b"\xE2\xB0\x98".decode(), # Ⱈ (Ot, 158) + b"\xE2\xB0\x99".decode(), # Ⱉ (Cy, 159) + b"\xE2\xB0\x9A".decode(), # Ⱊ (Shcha, 160) + b"\xE2\xB0\x9B".decode(), # Ⱋ (Er, 161) + b"\xE2\xB0\x9C".decode(), # Ⱌ (Yeru, 162) + b"\xE2\xB0\x9D".decode(), # Ⱍ (Small Yer, 163) + b"\xE2\xB0\x9E".decode(), # Ⱎ (Yo, 164) + b"\xE2\xB0\x9F".decode(), # Ⱏ (Yu, 165) + b"\xE2\xB0\xA0".decode(), # Ⱐ (Ja, 166) + b"\xE0\xB8\x81".decode(), # ก (Ko Kai, 167) + b"\xE0\xB8\x82".decode(), # ข (Kho Khai, 168) + b"\xE0\xB8\x83".decode(), # ฃ (Kho Khuat, 169) + b"\xE0\xB8\x84".decode(), # ค (Kho Khon, 170) + b"\xE0\xB8\x85".decode(), # ฅ (Kho Rakhang, 171) + b"\xE0\xB8\x86".decode(), # ฆ (Kho Khwai, 172) + b"\xE0\xB8\x87".decode(), # ง (Ngo Ngu, 173) + b"\xE0\xB8\x88".decode(), # จ (Cho Chan, 174) + b"\xE0\xB8\x89".decode(), # ฉ (Cho Ching, 175) + b"\xE0\xB8\x8A".decode(), # ช (Cho Chang, 176) + b"\xE0\xB8\x8B".decode(), # ซ (So So, 177) + b"\xE0\xB8\x8C".decode(), # ฌ (Cho Choe, 178) + b"\xE0\xB8\x8D".decode(), # ญ (Yo Ying, 179) + b"\xE0\xB8\x8E".decode(), # ฎ (Do Chada, 180) + b"\xE0\xB8\x8F".decode(), # ฏ (To Patak, 181) + b"\xE0\xB8\x90".decode(), # ฐ (Tho Than, 182) + b"\xE0\xB8\x91".decode(), # ฑ (Tho Nangmontho, 183) + b"\xE0\xB8\x92".decode(), # ฒ (Tho Phuthao, 184) + b"\xE0\xB8\x93".decode(), # ณ (No Nen, 185) + b"\xE0\xB8\x94".decode(), # ด (Do Dek, 186) + b"\xE0\xB8\x95".decode(), # ต (To Tao, 187) + b"\xE0\xB8\x96".decode(), # ถ (Tho Thung, 188) + b"\xE0\xB8\x97".decode(), # ท (Tho Thahan, 189) + b"\xE0\xB8\x98".decode(), # ธ (Tho Thong, 190) + b"\xE0\xB8\x99".decode(), # น (No Nu, 191) + b"\xE0\xB8\x9A".decode(), # บ (Bo Baimai, 192) + b"\xE0\xB8\x9B".decode(), # ป (Po Pla, 193) + b"\xE0\xB8\x9C".decode(), # ผ (Pho Phung, 194) + b"\xE0\xB8\x9D".decode(), # ฝ (Fo Fa, 195) + b"\xE0\xB8\x9E".decode(), # พ (Pho Phan, 196) + b"\xE0\xB8\x9F".decode(), # ฟ (Fo Fan, 197) + b"\xE0\xB8\xA0".decode(), # ภ (Pho Samphao, 198) + b"\xE0\xB8\xA1".decode(), # ม (Mo Ma, 199) + b"\xE0\xB8\xA2".decode(), # ย (Yo Yak, 200) + b"\xE0\xB8\xA3".decode(), # ร (Ro Rua, 201) + b"\xE0\xB8\xA5".decode(), # ล (Lo Ling, 202) + b"\xE0\xB8\xA7".decode(), # ว (Wo Waen, 203) + b"\xE0\xB8\xA8".decode(), # ศ (So Sala, 204) + b"\xE0\xB8\xA9".decode(), # ษ (So Rusi, 205) + b"\xE0\xB8\xAA".decode(), # ส (So Sua, 206) + b"\xE0\xB8\xAB".decode(), # ห (Ho Hip, 207) + b"\xE0\xB8\xAC".decode(), # ฬ (Lo Chula, 208) + b"\xE0\xB8\xAD".decode(), # อ (O Ang, 209) + b"\xE0\xB8\xAE".decode(), # ฮ (Ho Nokhuk, 210) + b"\xE1\x84\x80".decode(), # ㄱ (Giyeok, 211) + b"\xE1\x84\x81".decode(), # ㄴ (Nieun, 212) + b"\xE1\x84\x82".decode(), # ㄷ (Digeut, 213) + b"\xE1\x84\x83".decode(), # ㄹ (Rieul, 214) + b"\xE1\x84\x84".decode(), # ㅁ (Mieum, 215) + b"\xE1\x84\x85".decode(), # ㅂ (Bieup, 216) + b"\xE1\x84\x86".decode(), # ㅅ (Siot, 217) + b"\xE1\x84\x87".decode(), # ㅇ (Ieung, 218) + b"\xE1\x84\x88".decode(), # ㅈ (Jieut, 219) + b"\xE1\x84\x89".decode(), # ㅊ (Chieut, 220) + b"\xE1\x84\x8A".decode(), # ㅋ (Kieuk, 221) + b"\xE1\x84\x8B".decode(), # ㅌ (Tieut, 222) + b"\xE1\x84\x8C".decode(), # ㅍ (Pieup, 223) + b"\xE1\x84\x8D".decode(), # ㅎ (Hieut, 224) + b"\xE1\x85\xA1".decode(), # ㅏ (A, 225) + b"\xE1\x85\xA2".decode(), # ㅐ (Ae, 226) + b"\xE1\x85\xA3".decode(), # ㅑ (Ya, 227) + b"\xE1\x85\xA4".decode(), # ㅒ (Yae, 228) + b"\xE1\x85\xA5".decode(), # ㅓ (Eo, 229) + b"\xE1\x85\xA6".decode(), # ㅔ (E, 230) + b"\xE1\x85\xA7".decode(), # ㅕ (Yeo, 231) + b"\xE1\x85\xA8".decode(), # ㅖ (Ye, 232) + b"\xE1\x85\xA9".decode(), # ㅗ (O, 233) + b"\xE1\x85\xAA".decode(), # ㅘ (Wa, 234) + b"\xE1\x85\xAB".decode(), # ㅙ (Wae, 235) + b"\xE1\x85\xAC".decode(), # ㅚ (Oe, 236) + b"\xE1\x85\xAD".decode(), # ㅛ (Yo, 237) + b"\xE1\x85\xAE".decode(), # ㅜ (U, 238) + b"\xE1\x85\xAF".decode(), # ㅝ (Weo, 239) + b"\xE1\x85\xB0".decode(), # ㅞ (We, 240) + b"\xE1\x85\xB1".decode(), # ㅟ (Wi, 241) + b"\xE1\x85\xB2".decode(), # ㅠ (Yu, 242) + b"\xE1\x85\xB3".decode(), # ㅡ (Eu, 243) + b"\xE1\x85\xB4".decode(), # ㅢ (Ui, 244) + b"\xE1\x85\xB5".decode(), # ㅣ (I, 245) + b"\xE1\x8A\xA0".decode(), # አ (Glottal A, 246) + b"\xE1\x8A\xA1".decode(), # ኡ (Glottal U, 247) + b"\xE1\x8A\xA2".decode(), # ኢ (Glottal I, 248) + b"\xE1\x8A\xA3".decode(), # ኣ (Glottal Aa, 249) + b"\xE1\x8A\xA4".decode(), # ኤ (Glottal E, 250) + b"\xE1\x8A\xA5".decode(), # እ (Glottal Ie, 251) + b"\xE1\x8A\xA6".decode(), # ኦ (Glottal O, 252) + b"\xE1\x8A\xA7".decode(), # ኧ (Glottal Wa, 253) + b"\xE1\x8B\x88".decode(), # ወ (Wa, 254) + b"\xE1\x8B\x89".decode(), # ዉ (Wu, 255) + b"\xE1\x8B\x8A".decode(), # ዊ (Wi, 256) + b"\xE1\x8B\x8B".decode(), # ዋ (Waa, 257) + b"\xE1\x8B\x8C".decode(), # ዌ (We, 258) + b"\xE1\x8B\x8D".decode(), # ው (Wye, 259) + b"\xE1\x8B\x8E".decode(), # ዎ (Wo, 260) + b"\xE1\x8A\xB0".decode(), # ኰ (Ko, 261) + b"\xE1\x8A\xB1".decode(), # ኱ (Ku, 262) + b"\xE1\x8A\xB2".decode(), # ኲ (Ki, 263) + b"\xE1\x8A\xB3".decode(), # ኳ (Kua, 264) + b"\xE1\x8A\xB4".decode(), # ኴ (Ke, 265) + b"\xE1\x8A\xB5".decode(), # ኵ (Kwe, 266) + b"\xE1\x8A\xB6".decode(), # ኶ (Ko, 267) + b"\xE1\x8A\x90".decode(), # ጐ (Go, 268) + b"\xE1\x8A\x91".decode(), # ጑ (Gu, 269) + b"\xE1\x8A\x92".decode(), # ጒ (Gi, 270) + b"\xE1\x8A\x93".decode(), # መ (Gua, 271) + b"\xE1\x8A\x94".decode(), # ጔ (Ge, 272) + b"\xE1\x8A\x95".decode(), # ጕ (Gwe, 273) + b"\xE1\x8A\x96".decode(), # ጖ (Go, 274) + b"\xE0\xA4\x85".decode(), # अ (A, 275) + b"\xE0\xA4\x86".decode(), # आ (Aa, 276) + b"\xE0\xA4\x87".decode(), # इ (I, 277) + b"\xE0\xA4\x88".decode(), # ई (Ii, 278) + b"\xE0\xA4\x89".decode(), # उ (U, 279) + b"\xE0\xA4\x8A".decode(), # ऊ (Uu, 280) + b"\xE0\xA4\x8B".decode(), # ऋ (R, 281) + b"\xE0\xA4\x8F".decode(), # ए (E, 282) + b"\xE0\xA4\x90".decode(), # ऐ (Ai, 283) + b"\xE0\xA4\x93".decode(), # ओ (O, 284) + b"\xE0\xA4\x94".decode(), # औ (Au, 285) + b"\xE0\xA4\x95".decode(), # क (Ka, 286) + b"\xE0\xA4\x96".decode(), # ख (Kha, 287) + b"\xE0\xA4\x97".decode(), # ग (Ga, 288) + b"\xE0\xA4\x98".decode(), # घ (Gha, 289) + b"\xE0\xA4\x99".decode(), # ङ (Nga, 290) + b"\xE0\xA4\x9A".decode(), # च (Cha, 291) + b"\xE0\xA4\x9B".decode(), # छ (Chha, 292) + b"\xE0\xA4\x9C".decode(), # ज (Ja, 293) + b"\xE0\xA4\x9D".decode(), # झ (Jha, 294) + b"\xE0\xA4\x9E".decode(), # ञ (Nya, 295) + b"\xE0\xA4\x9F".decode(), # ट (Ta, 296) + b"\xE0\xA4\xA0".decode(), # ठ (Tha, 297) + b"\xE0\xA4\xA1".decode(), # ड (Da, 298) + b"\xE0\xA4\xA2".decode(), # ढ (Dha, 299) + b"\xE0\xA4\xA3".decode(), # ण (Na, 300) + b"\xE0\xA4\xA4".decode(), # त (Ta, 301) + b"\xE0\xA4\xA5".decode(), # थ (Tha, 302) + b"\xE0\xA4\xA6".decode(), # द (Da, 303) + b"\xE0\xA4\xA7".decode(), # ध (Dha, 304) + b"\xE0\xA4\xA8".decode(), # न (Na, 305) + b"\xE0\xA4\xAA".decode(), # प (Pa, 306) + b"\xE0\xA4\xAB".decode(), # फ (Pha, 307) + b"\xE0\xA4\xAC".decode(), # ब (Ba, 308) + b"\xE0\xA4\xAD".decode(), # भ (Bha, 309) + b"\xE0\xA4\xAE".decode(), # म (Ma, 310) + b"\xE0\xA4\xAF".decode(), # य (Ya, 311) + b"\xE0\xA4\xB0".decode(), # र (Ra, 312) + b"\xE0\xA4\xB2".decode(), # ल (La, 313) + b"\xE0\xA4\xB5".decode(), # व (Va, 314) + b"\xE0\xA4\xB6".decode(), # श (Sha, 315) + b"\xE0\xA4\xB7".decode(), # ष (Ssa, 316) + b"\xE0\xA4\xB8".decode(), # स (Sa, 317) + b"\xE0\xA4\xB9".decode(), # ह (Ha, 318) + b"\xE3\x82\xA2".decode(), # ア (A, 319) + b"\xE3\x82\xA4".decode(), # イ (I, 320) + b"\xE3\x82\xA6".decode(), # ウ (U, 321) + b"\xE3\x82\xA8".decode(), # エ (E, 322) + b"\xE3\x82\xAA".decode(), # オ (O, 323) + b"\xE3\x82\xAB".decode(), # カ (Ka, 324) + b"\xE3\x82\xAD".decode(), # キ (Ki, 325) + b"\xE3\x82\xAF".decode(), # ク (Ku, 326) + b"\xE3\x82\xB1".decode(), # ケ (Ke, 327) + b"\xE3\x82\xB3".decode(), # コ (Ko, 328) + b"\xE3\x82\xB5".decode(), # サ (Sa, 329) + b"\xE3\x82\xB7".decode(), # シ (Shi, 330) + b"\xE3\x82\xB9".decode(), # ス (Su, 331) + b"\xE3\x82\xBB".decode(), # セ (Se, 332) + b"\xE3\x82\xBD".decode(), # ソ (So, 333) + b"\xE3\x82\xBF".decode(), # タ (Ta, 334) + b"\xE3\x83\x81".decode(), # チ (Chi, 335) + b"\xE3\x83\x84".decode(), # ツ (Tsu, 336) + b"\xE3\x83\x86".decode(), # テ (Te, 337) + b"\xE3\x83\x88".decode(), # ト (To, 338) + b"\xE3\x83\x8A".decode(), # ナ (Na, 339) + b"\xE3\x83\x8B".decode(), # ニ (Ni, 340) + b"\xE3\x83\x8C".decode(), # ヌ (Nu, 341) + b"\xE3\x83\x8D".decode(), # ネ (Ne, 342) + b"\xE3\x83\x8E".decode(), # ノ (No, 343) + b"\xE3\x83\x8F".decode(), # ハ (Ha, 344) + b"\xE3\x83\x92".decode(), # ヒ (Hi, 345) + b"\xE3\x83\x95".decode(), # フ (Fu, 346) + b"\xE3\x83\x98".decode(), # ヘ (He, 347) + b"\xE3\x83\x9B".decode(), # ホ (Ho, 348) + b"\xE3\x83\x9E".decode(), # マ (Ma, 349) + b"\xE3\x83\x9F".decode(), # ミ (Mi, 350) + b"\xE3\x83\xA0".decode(), # ム (Mu, 351) + b"\xE3\x83\xA1".decode(), # メ (Me, 352) + b"\xE3\x83\xA2".decode(), # モ (Mo, 353) + b"\xE3\x83\xA4".decode(), # ヤ (Ya, 354) + b"\xE3\x83\xA6".decode(), # ユ (Yu, 355) + b"\xE3\x83\xA8".decode(), # ヨ (Yo, 356) + b"\xE3\x83\xA9".decode(), # ラ (Ra, 357) + b"\xE3\x83\xAA".decode(), # リ (Ri, 358) + b"\xE3\x83\xAB".decode(), # ル (Ru, 359) + b"\xE3\x83\xAC".decode(), # レ (Re, 360) + b"\xE3\x83\xAD".decode(), # ロ (Ro, 361) + b"\xE3\x83\xAF".decode(), # ワ (Wa, 362) + b"\xE3\x83\xB2".decode(), # ヲ (Wo, 363) + b"\xE3\x83\xB3".decode(), # ン (N, 364) + b"\xE2\xB4\xB0".decode(), # ⴰ (Ya, 365) + b"\xE2\xB4\xB1".decode(), # ⴱ (Yab, 366) + b"\xE2\xB4\xB2".decode(), # ⴲ (Yabh, 367) + b"\xE2\xB4\xB3".decode(), # ⴳ (Yag, 368) + b"\xE2\xB4\xB4".decode(), # ⴴ (Yagh, 369) + b"\xE2\xB4\xB5".decode(), # ⴵ (Yaj, 370) + b"\xE2\xB4\xB6".decode(), # ⴶ (Yach, 371) + b"\xE2\xB4\xB7".decode(), # ⴷ (Yad, 372) + b"\xE2\xB4\xB8".decode(), # ⴸ (Yadh, 373) + b"\xE2\xB4\xB9".decode(), # ⴹ (Yadh, emphatic, 374) + b"\xE2\xB4\xBA".decode(), # ⴺ (Yaz, 375) + b"\xE2\xB4\xBB".decode(), # ⴻ (Yazh, 376) + b"\xE2\xB4\xBC".decode(), # ⴼ (Yaf, 377) + b"\xE2\xB4\xBD".decode(), # ⴽ (Yak, 378) + b"\xE2\xB4\xBE".decode(), # ⴾ (Yak, variant, 379) + b"\xE2\xB4\xBF".decode(), # ⴿ (Yaq, 380) + b"\xE2\xB5\x80".decode(), # ⵀ (Yah, 381) + b"\xE2\xB5\x81".decode(), # ⵁ (Yahh, 382) + b"\xE2\xB5\x82".decode(), # ⵂ (Yahl, 383) + b"\xE2\xB5\x83".decode(), # ⵃ (Yahm, 384) + b"\xE2\xB5\x84".decode(), # ⵄ (Yayn, 385) + b"\xE2\xB5\x85".decode(), # ⵅ (Yakh, 386) + b"\xE2\xB5\x86".decode(), # ⵆ (Yakl, 387) + b"\xE2\xB5\x87".decode(), # ⵇ (Yahq, 388) + b"\xE2\xB5\x88".decode(), # ⵈ (Yash, 389) + b"\xE2\xB5\x89".decode(), # ⵉ (Yi, 390) + b"\xE2\xB5\x8A".decode(), # ⵊ (Yij, 391) + b"\xE2\xB5\x8B".decode(), # ⵋ (Yizh, 392) + b"\xE2\xB5\x8C".decode(), # ⵌ (Yink, 393) + b"\xE2\xB5\x8D".decode(), # ⵍ (Yal, 394) + b"\xE2\xB5\x8E".decode(), # ⵎ (Yam, 395) + b"\xE2\xB5\x8F".decode(), # ⵏ (Yan, 396) + b"\xE2\xB5\x90".decode(), # ⵐ (Yang, 397) + b"\xE2\xB5\x91".decode(), # ⵑ (Yany, 398) + b"\xE2\xB5\x92".decode(), # ⵒ (Yap, 399) + b"\xE2\xB5\x93".decode(), # ⵓ (Yu, 400) + b"\xE0\xB6\x85".decode(), # අ (A, 401) + b"\xE0\xB6\x86".decode(), # ආ (Aa, 402) + b"\xE0\xB6\x87".decode(), # ඉ (I, 403) + b"\xE0\xB6\x88".decode(), # ඊ (Ii, 404) + b"\xE0\xB6\x89".decode(), # උ (U, 405) + b"\xE0\xB6\x8A".decode(), # ඌ (Uu, 406) + b"\xE0\xB6\x8B".decode(), # ඍ (R, 407) + b"\xE0\xB6\x8C".decode(), # ඎ (Rr, 408) + b"\xE0\xB6\x8F".decode(), # ඏ (L, 409) + b"\xE0\xB6\x90".decode(), # ඐ (Ll, 410) + b"\xE0\xB6\x91".decode(), # එ (E, 411) + b"\xE0\xB6\x92".decode(), # ඒ (Ee, 412) + b"\xE0\xB6\x93".decode(), # ඓ (Ai, 413) + b"\xE0\xB6\x94".decode(), # ඔ (O, 414) + b"\xE0\xB6\x95".decode(), # ඕ (Oo, 415) + b"\xE0\xB6\x96".decode(), # ඖ (Au, 416) + b"\xE0\xB6\x9A".decode(), # ක (Ka, 417) + b"\xE0\xB6\x9B".decode(), # ඛ (Kha, 418) + b"\xE0\xB6\x9C".decode(), # ග (Ga, 419) + b"\xE0\xB6\x9D".decode(), # ඝ (Gha, 420) + b"\xE0\xB6\x9E".decode(), # ඞ (Nga, 421) + b"\xE0\xB6\x9F".decode(), # ච (Cha, 422) + b"\xE0\xB6\xA0".decode(), # ඡ (Chha, 423) + b"\xE0\xB6\xA1".decode(), # ජ (Ja, 424) + b"\xE0\xB6\xA2".decode(), # ඣ (Jha, 425) + b"\xE0\xB6\xA3".decode(), # ඤ (Nya, 426) + b"\xE0\xB6\xA4".decode(), # ට (Ta, 427) + b"\xE0\xB6\xA5".decode(), # ඥ (Tha, 428) + b"\xE0\xB6\xA6".decode(), # ඦ (Da, 429) + b"\xE0\xB6\xA7".decode(), # ට (Dha, 430) + b"\xE0\xB6\xA8".decode(), # ඨ (Na, 431) + b"\xE0\xB6\xAA".decode(), # ඪ (Pa, 432) + b"\xE0\xB6\xAB".decode(), # ණ (Pha, 433) + b"\xE0\xB6\xAC".decode(), # ඬ (Ba, 434) + b"\xE0\xB6\xAD".decode(), # ත (Bha, 435) + b"\xE0\xB6\xAE".decode(), # ථ (Ma, 436) + b"\xE0\xB6\xAF".decode(), # ද (Ya, 437) + b"\xE0\xB6\xB0".decode(), # ධ (Ra, 438) + ] NETWORK_EXPLORER_MAP = { diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index ddbbc724..5fc29ba5 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -749,7 +749,9 @@ def tao_to_alpha_with_slippage( self, tao: Balance ) -> tuple[Balance, Balance, float]: """ - Returns an estimate of how much Alpha would a staker receive if they stake their tao using the current pool state. + Returns an estimate of how much Alpha a staker would receive if they stake their tao using the current pool + state. + Args: tao: Amount of TAO to stake. Returns: @@ -792,7 +794,9 @@ def alpha_to_tao_with_slippage( self, alpha: Balance ) -> tuple[Balance, Balance, float]: """ - Returns an estimate of how much TAO would a staker receive if they unstake their alpha using the current pool state. + Returns an estimate of how much TAO a staker would receive if they unstake their alpha using the current pool + state. + Args: alpha: Amount of Alpha to stake. Returns: diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 65d8f348..2632a69d 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -524,9 +524,11 @@ async def get_neuron_for_pubkey_and_subnet(): if prompt: if not Confirm.ask( f"Continue Registration?\n" - f" hotkey [{COLOR_PALETTE['GENERAL']['HOTKEY']}]({wallet.hotkey_str})[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]:\t[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" - f" coldkey [{COLOR_PALETTE['GENERAL']['COLDKEY']}]({wallet.name})[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]:\t[{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f" network:\t\t[{COLOR_PALETTE['GENERAL']['LINKS']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['LINKS']}]\n" + f" hotkey [{COLOR_PALETTE.G.HK}]({wallet.hotkey_str})[/{COLOR_PALETTE.G.HK}]:" + f"\t[{COLOR_PALETTE.G.HK}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE.G.HK}]\n" + f" coldkey [{COLOR_PALETTE.G.CK}]({wallet.name})[/{COLOR_PALETTE.G.CK}]:" + f"\t[{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n" + f" network:\t\t[{COLOR_PALETTE.G.LINKS}]{subtensor.network}[/{COLOR_PALETTE.G.LINKS}]\n" ): return False @@ -611,7 +613,6 @@ async def get_neuron_for_pubkey_and_subnet(): if not wait_for_finalization and not wait_for_inclusion: success, err_msg = True, "" else: - await response.process_events() success = await response.is_success if not success: success, err_msg = ( @@ -676,8 +677,9 @@ async def burned_register_extrinsic( old_balance: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, + era: Optional[int] = None, prompt: bool = False, -) -> bool: +) -> tuple[bool, str]: """Registers the wallet to chain by recycling TAO. :param subtensor: The SubtensorInterface object to use for the call, initialized @@ -688,10 +690,11 @@ async def burned_register_extrinsic( `False` if the extrinsic fails to enter the block within the timeout. :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + :param era: the period (in blocks) for which the transaction should remain valid. :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. + :return: (success, msg), where success is `True` if extrinsic was finalized or included in the block. If we did not + wait for finalization/inclusion, the response is `True`. """ if not (unlock_status := unlock_key(wallet, print_out=False)).success: @@ -704,23 +707,42 @@ async def burned_register_extrinsic( my_uid = await subtensor.query( "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] ) + block_hash = await subtensor.substrate.get_chain_head() print_verbose("Checking if already registered", status) neuron = await subtensor.neuron_for_uid( - uid=my_uid, - netuid=netuid, - block_hash=subtensor.substrate.last_block_hash, + uid=my_uid, netuid=netuid, block_hash=block_hash ) + if not era: + current_block, tempo, blocks_since_last_step = await asyncio.gather( + subtensor.substrate.get_block_number(block_hash=block_hash), + subtensor.get_hyperparameter( + "Tempo", netuid=netuid, block_hash=block_hash + ), + subtensor.query( + "SubtensorModule", + "BlocksSinceLastStep", + [netuid], + block_hash=block_hash, + ), + ) + validity_period = tempo - blocks_since_last_step + era_ = { + "period": validity_period, + "current": current_block, + } + else: + era_ = {"period": era} if not neuron.is_null: console.print( ":white_heavy_check_mark: [dark_sea_green3]Already Registered[/dark_sea_green3]:\n" - f"uid: [{COLOR_PALETTE['GENERAL']['NETUID_EXTRA']}]{neuron.uid}[/{COLOR_PALETTE['GENERAL']['NETUID_EXTRA']}]\n" - f"netuid: [{COLOR_PALETTE['GENERAL']['NETUID']}]{neuron.netuid}[/{COLOR_PALETTE['GENERAL']['NETUID']}]\n" - f"hotkey: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{neuron.hotkey}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" - f"coldkey: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{neuron.coldkey}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" + f"uid: [{COLOR_PALETTE.G.NETUID_EXTRA}]{neuron.uid}[/{COLOR_PALETTE.G.NETUID_EXTRA}]\n" + f"netuid: [{COLOR_PALETTE.G.NETUID}]{neuron.netuid}[/{COLOR_PALETTE.G.NETUID}]\n" + f"hotkey: [{COLOR_PALETTE.G.HK}]{neuron.hotkey}[/{COLOR_PALETTE.G.HK}]\n" + f"coldkey: [{COLOR_PALETTE.G.CK}]{neuron.coldkey}[/{COLOR_PALETTE.G.CK}]" ) - return True + return True, "Already registered" with console.status( ":satellite: Recycling TAO for Registration...", spinner="aesthetic" @@ -734,13 +756,13 @@ async def burned_register_extrinsic( }, ) success, err_msg = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization, era=era_ ) if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") await asyncio.sleep(0.5) - return False + return False, err_msg # Successful registration, final check for neuron and pubkey else: with console.status(":satellite: Checking Balance...", spinner="aesthetic"): @@ -761,20 +783,21 @@ async def burned_register_extrinsic( console.print( "Balance:\n" - f" [blue]{old_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" [blue]{old_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{new_balance}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]" ) if len(netuids_for_hotkey) > 0: console.print( f":white_heavy_check_mark: [green]Registered on netuid {netuid} with UID {my_uid}[/green]" ) - return True + return True, f"Registered on {netuid} with UID {my_uid}" else: # neuron not found, try again err_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) - return False + return False, "Unknown error. Neuron not found." async def run_faucet_extrinsic( @@ -894,7 +917,6 @@ async def run_faucet_extrinsic( ) # process if registration successful, try again if pow is still valid - await response.process_events() if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red]: " @@ -1737,7 +1759,8 @@ async def swap_hotkey_extrinsic( ) if not len(netuids_registered) > 0: err_console.print( - f"Destination hotkey [dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] is not registered. Please register and try again" + f"Destination hotkey [dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] is not registered. " + f"Please register and try again" ) return False @@ -1754,7 +1777,8 @@ async def swap_hotkey_extrinsic( ): return False print_verbose( - f"Swapping {wallet.name}'s hotkey ({wallet.hotkey.ss58_address}) with {new_wallet.name}s hotkey ({new_wallet.hotkey.ss58_address})" + f"Swapping {wallet.name}'s hotkey ({wallet.hotkey.ss58_address}) with " + f"{new_wallet.name}s hotkey ({new_wallet.hotkey.ss58_address})" ) with console.status(":satellite: Swapping hotkeys...", spinner="aesthetic"): call = await subtensor.substrate.compose_call( diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index 29b9e510..aca5f07d 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -291,7 +291,7 @@ async def root_register_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = False, -) -> bool: +) -> tuple[bool, str]: r"""Registers the wallet to root network. :param subtensor: The SubtensorInterface object @@ -302,12 +302,12 @@ async def root_register_extrinsic( or returns `False` if the extrinsic fails to be finalized within the timeout. :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - :return: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, - the response is `True`. + :return: (success, msg), with success being `True` if extrinsic was finalized or included in the block. If we did + not wait for finalization/inclusion, the response is `True`. """ - if not unlock_key(wallet).success: - return False + if not (unlock := unlock_key(wallet)).success: + return False, unlock.message print_verbose(f"Checking if hotkey ({wallet.hotkey_str}) is registered on root") is_registered = await is_hotkey_registered( @@ -317,7 +317,7 @@ async def root_register_extrinsic( console.print( ":white_heavy_check_mark: [green]Already registered on root network.[/green]" ) - return True + return True, "Already registered on root network" with console.status(":satellite: Registering to root network...", spinner="earth"): call = await subtensor.substrate.compose_call( @@ -334,8 +334,8 @@ async def root_register_extrinsic( if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - time.sleep(0.5) - return False + await asyncio.sleep(0.5) + return False, err_msg # Successful registration, final check for neuron and pubkey else: @@ -348,13 +348,13 @@ async def root_register_extrinsic( console.print( f":white_heavy_check_mark: [green]Registered with UID {uid}[/green]" ) - return True + return True, f"Registered with UID {uid}" else: # neuron not found, try again err_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) - return False + return False, "Unknown error. Neuron not found." async def set_root_weights_extrinsic( @@ -410,7 +410,6 @@ async def _do_set_weights(): if not wait_for_finalization and not wait_for_inclusion: return True, "Not waiting for finalization or inclusion." - await response.process_events() if await response.is_success: return True, "Successfully set weights." else: diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 5a167f0b..3796353e 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -24,6 +24,7 @@ async def transfer_extrinsic( wallet: Wallet, destination: str, amount: Balance, + era: int = 3, transfer_all: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -36,6 +37,7 @@ async def transfer_extrinsic( :param wallet: Bittensor wallet object to make transfer from. :param destination: Destination public key address (ss58_address or ed25519) of recipient. :param amount: Amount to stake as Bittensor balance. + :param era: Length (in blocks) for which the transaction should be valid. :param transfer_all: Whether to transfer all funds from this wallet to the destination address. :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. @@ -84,7 +86,7 @@ async def do_transfer() -> tuple[bool, str, str]: call_params={"dest": destination, "value": amount.rao}, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, era={"period": era} ) response = await subtensor.substrate.submit_extrinsic( extrinsic, @@ -96,7 +98,6 @@ async def do_transfer() -> tuple[bool, str, str]: return True, "", "" # Otherwise continue with finalization. - await response.process_events() if await response.is_success: block_hash_ = response.block_hash return True, block_hash_, "" @@ -155,7 +156,8 @@ async def do_transfer() -> tuple[bool, str, str]: if not Confirm.ask( "Do you want to transfer:[bold white]\n" f" amount: [bright_cyan]{amount}[/bright_cyan]\n" - f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : [bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]" + f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : " + f"[bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]" f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]" ): return False diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index dba78636..7f21f90d 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1048,6 +1048,7 @@ async def sign_and_send_extrinsic( wallet: Wallet, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + era: Optional[dict[str, int]] = None, ) -> tuple[bool, str]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1056,11 +1057,15 @@ async def sign_and_send_extrinsic( :param wallet: the wallet whose coldkey will be used to sign the extrinsic :param wait_for_inclusion: whether to wait until the extrinsic call is included on the chain :param wait_for_finalization: whether to wait until the extrinsic call is finalized on the chain + :param era: The length (in blocks) for which a transaction should be valid. :return: (success, error message) """ + call_args = {"call": call, "keypair": wallet.coldkey} + if era is not None: + call_args["era"] = era extrinsic = await self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + **call_args ) # sign with coldkey try: response = await self.substrate.submit_extrinsic( @@ -1071,7 +1076,6 @@ async def sign_and_send_extrinsic( # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: return True, "" - await response.process_events() if await response.is_success: return True, "" else: @@ -1368,11 +1372,11 @@ async def get_stake_for_coldkeys( This function is useful for analyzing the stake distribution and delegation patterns of multiple accounts simultaneously, offering a broader perspective on network participation and investment strategies. """ - BATCH_SIZE = 60 + batch_size = 60 tasks = [] - for i in range(0, len(coldkey_ss58_list), BATCH_SIZE): - ss58_chunk = coldkey_ss58_list[i : i + BATCH_SIZE] + for i in range(0, len(coldkey_ss58_list), batch_size): + ss58_chunk = coldkey_ss58_list[i : i + batch_size] tasks.append( self.query_runtime_api( runtime_api="StakeInfoRuntimeApi", @@ -1501,3 +1505,53 @@ async def get_stake_fee( ) return Balance.from_rao(result) + + async def get_scheduled_coldkey_swap( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[list[str]]: + """ + Queries the chain to fetch the list of coldkeys that are scheduled for a swap. + + :param block_hash: Block hash at which to perform query. + :param reuse_block: Whether to reuse the last-used block hash. + + :return: A list of SS58 addresses of the coldkeys that are scheduled for a coldkey swap. + """ + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="ColdkeySwapScheduled", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + keys_pending_swap = [] + async for ss58, _ in result: + keys_pending_swap.append(decode_account_id(ss58)) + return keys_pending_swap + + async def get_coldkey_swap_schedule_duration( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> int: + """ + Retrieves the duration (in blocks) required for a coldkey swap to be executed. + + Args: + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + int: The number of blocks required for the coldkey swap schedule duration. + """ + result = await self.query( + module="SubtensorModule", + storage_function="ColdkeySwapScheduleDuration", + params=[], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + return result diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 318847c8..b24cea56 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -33,6 +33,7 @@ from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters console = Console() +json_console = Console() err_console = Console(stderr=True) verbose_console = Console(quiet=True) @@ -1052,6 +1053,7 @@ def get_effective_network(config, network: Optional[list[str]]) -> str: the configuration, and the default. """ if network: + network_ = "" for item in network: if item.startswith("ws"): network_ = item @@ -1308,7 +1310,8 @@ def print_linux_dependency_message(): "\tAdd this into the file and save: [green]deb http://archive.ubuntu.com/ubuntu jammy main universe[/green]" ) console.print( - "\tUpdate the repository and install the webkit dependency: [green]sudo apt update && sudo apt install libwebkit2gtk-4.0-dev[/green]" + "\tUpdate the repository and install the webkit dependency: [green]sudo apt update && sudo apt install " + "libwebkit2gtk-4.0-dev[/green]" ) console.print("\nFedora / CentOS / AlmaLinux:") console.print("[green]sudo dnf install gtk3-devel webkit2gtk3-devel[/green]\n\n") diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 2451d51b..a4af67ff 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -1,4 +1,6 @@ import asyncio +import json +from collections import defaultdict from functools import partial from typing import TYPE_CHECKING, Optional @@ -17,6 +19,7 @@ print_error, print_verbose, unlock_key, + json_console, ) from bittensor_wallet import Wallet @@ -38,6 +41,8 @@ async def stake_add( safe_staking: bool, rate_tolerance: float, allow_partial_stake: bool, + json_output: bool, + era: int, ): """ Args: @@ -53,6 +58,8 @@ async def stake_add( safe_staking: whether to use safe staking rate_tolerance: rate tolerance percentage for stake operations allow_partial_stake: whether to allow partial stake + json_output: whether to output stake info in JSON format + era: Blocks for which the transaction should be valid. Returns: bool: True if stake operation is successful, False otherwise @@ -65,28 +72,31 @@ async def safe_stake_extrinsic( hotkey_ss58_: str, price_limit: Balance, status=None, - ) -> None: + ) -> bool: err_out = partial(print_error, status=status) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount_} on Netuid {netuid_}" ) - current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - next_nonce = await subtensor.substrate.get_account_next_index( - wallet.coldkeypub.ss58_address - ) - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake_limit", - call_params={ - "hotkey": hotkey_ss58_, - "netuid": netuid_, - "amount_staked": amount_.rao, - "limit_price": price_limit, - "allow_partial": allow_partial_stake, - }, + current_balance, next_nonce, call = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake_limit", + call_params={ + "hotkey": hotkey_ss58_, + "netuid": netuid_, + "amount_staked": amount_.rao, + "limit_price": price_limit, + "allow_partial": allow_partial_stake, + }, + ), ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce + call=call, + keypair=wallet.coldkey, + nonce=next_nonce, + era={"period": era}, ) try: response = await subtensor.substrate.submit_extrinsic( @@ -100,73 +110,80 @@ async def safe_stake_extrinsic( f"Either increase price tolerance or enable partial staking.", status=status, ) - return + return False else: err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") - return + return False + if not await response.is_success: + err_out( + f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" + ) + return False else: - await response.process_events() - if not await response.is_success: - err_out( - f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" - ) - else: - block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), - subtensor.get_stake( - hotkey_ss58=hotkey_ss58_, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid_, - block_hash=block_hash, - ), - ) - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid_}[/dark_sea_green3]" - ) - console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - - amount_staked = current_balance - new_balance - if allow_partial_stake and (amount_staked != amount_): - console.print( - "Partial stake transaction. Staked:\n" - f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " - f"instead of " - f"[blue]{amount_}[/blue]" - ) + if json_output: + # the rest of this checking is not necessary if using json_output + return True + block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58_, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid_, + block_hash=block_hash, + ), + ) + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Finalized. " + f"Stake added to netuid: {netuid_}[/dark_sea_green3]" + ) + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + amount_staked = current_balance - new_balance + if allow_partial_stake and (amount_staked != amount_): console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " - f"Stake:\n" - f" [blue]{current_stake}[/blue] " - f":arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + "Partial stake transaction. Staked:\n" + f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}" + f"[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"instead of " + f"[blue]{amount_}[/blue]" ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid_}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Stake:\n" + f" [blue]{current_stake}[/blue] " + f":arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + ) + return True + async def stake_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None - ): + ) -> bool: err_out = partial(print_error, status=status) - current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + current_balance, next_nonce, call = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": staking_address_ss58, + "netuid": netuid_i, + "amount_staked": amount_.rao, + }, + ), + ) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) - next_nonce = await subtensor.substrate.get_account_next_index( - wallet.coldkeypub.ss58_address - ) - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={ - "hotkey": staking_address_ss58, - "netuid": netuid_i, - "amount_staked": amount_.rao, - }, - ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} ) try: response = await subtensor.substrate.submit_extrinsic( @@ -174,35 +191,46 @@ async def stake_extrinsic( ) except SubstrateRequestException as e: err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") - return + return False else: - await response.process_events() if not await response.is_success: err_out( f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) + return False else: + if json_output: + # the rest of this is not necessary if using json_output + return True + new_block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=new_block_hash + ), subtensor.get_stake( hotkey_ss58=staking_address_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address, netuid=netuid_i, + block_hash=new_block_hash, ), ) console.print( - f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" + f":white_heavy_check_mark: " + f"[dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" ) console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " f"Stake:\n" f" [blue]{current}[/blue] " f":arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" ) + return True netuids = ( [int(netuid)] @@ -333,7 +361,9 @@ async def stake_extrinsic( base_row.extend( [ f"{rate_with_tolerance} {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", - f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", # safe staking + f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]" + # safe staking + f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", ] ) @@ -352,7 +382,7 @@ async def stake_extrinsic( return False if safe_staking: - stake_coroutines = [] + stake_coroutines = {} for i, (ni, am, curr, price_with_tolerance) in enumerate( zip( netuids, amounts_to_stake, current_stake_balances, prices_with_tolerance @@ -361,27 +391,23 @@ async def stake_extrinsic( for _, staking_address in hotkeys_to_stake_to: # Regular extrinsic for root subnet if ni == 0: - stake_coroutines.append( - stake_extrinsic( - netuid_i=ni, - amount_=am, - current=curr, - staking_address_ss58=staking_address, - ) + stake_coroutines[(ni, staking_address)] = stake_extrinsic( + netuid_i=ni, + amount_=am, + current=curr, + staking_address_ss58=staking_address, ) else: - stake_coroutines.append( - safe_stake_extrinsic( - netuid_=ni, - amount_=am, - current_stake=curr, - hotkey_ss58_=staking_address, - price_limit=price_with_tolerance, - ) + stake_coroutines[(ni, staking_address)] = safe_stake_extrinsic( + netuid_=ni, + amount_=am, + current_stake=curr, + hotkey_ss58_=staking_address, + price_limit=price_with_tolerance, ) else: - stake_coroutines = [ - stake_extrinsic( + stake_coroutines = { + (ni, staking_address): stake_extrinsic( netuid_i=ni, amount_=am, current=curr, @@ -391,12 +417,15 @@ async def stake_extrinsic( zip(netuids, amounts_to_stake, current_stake_balances) ) for _, staking_address in hotkeys_to_stake_to - ] - + } + successes = defaultdict(dict) with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): # We can gather them all at once but balance reporting will be in race-condition. - for coroutine in stake_coroutines: - await coroutine + for (ni, staking_address), coroutine in stake_coroutines.items(): + success = await coroutine + successes[ni][staking_address] = success + if json_output: + json_console.print(json.dumps({"staking_success": successes})) # Helper functions diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index fff84c65..ef16823e 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -1,8 +1,9 @@ import asyncio +import json from typing import Optional from bittensor_wallet import Wallet -from rich.prompt import Confirm, Prompt, IntPrompt +from rich.prompt import Confirm, IntPrompt, FloatPrompt from rich.table import Table from rich.text import Text from async_substrate_interface.errors import SubstrateRequestException @@ -19,6 +20,7 @@ is_valid_ss58_address, format_error_message, unlock_key, + json_console, ) @@ -28,24 +30,22 @@ async def get_childkey_completion_block( """ Calculates the block at which the childkey set request will complete """ + bh = await subtensor.substrate.get_chain_head() blocks_since_last_step_query = subtensor.query( - "SubtensorModule", - "BlocksSinceLastStep", - params=[netuid], + "SubtensorModule", "BlocksSinceLastStep", params=[netuid], block_hash=bh ) tempo_query = subtensor.get_hyperparameter( - param_name="Tempo", - netuid=netuid, + param_name="Tempo", netuid=netuid, block_hash=bh ) block_number, blocks_since_last_step, tempo = await asyncio.gather( - subtensor.substrate.get_block_number(), + subtensor.substrate.get_block_number(block_hash=bh), blocks_since_last_step_query, tempo_query, ) - cooldown = block_number + 1 + cooldown = block_number + 7200 blocks_left_in_tempo = tempo - blocks_since_last_step next_tempo = block_number + blocks_left_in_tempo - next_epoch_after_cooldown = (cooldown - next_tempo) % tempo + cooldown + next_epoch_after_cooldown = (cooldown - next_tempo) % (tempo + 1) + cooldown return block_number, next_epoch_after_cooldown @@ -84,7 +84,7 @@ async def set_children_extrinsic( if prompt: if all_revoked: if not Confirm.ask( - f"Do you want to revoke all children hotkeys for hotkey {hotkey}?" + f"Do you want to revoke all children hotkeys for hotkey {hotkey} on netuid {netuid}?" ): return False, "Operation Cancelled" else: @@ -134,17 +134,9 @@ async def set_children_extrinsic( console.print(":white_heavy_check_mark: [green]Included[/green]") if wait_for_finalization: console.print(":white_heavy_check_mark: [green]Finalized[/green]") - # bittensor.logging.success( - # prefix=operation, - # suffix="Finalized: " + str(success), - # ) return True, f"Successfully {operation.lower()} and Finalized." else: err_console.print(f":cross_mark: [red]Failed[/red]: {error_message}") - # bittensor.logging.warning( - # prefix=operation, - # suffix="Failed: " + str(error_message), - # ) return False, error_message @@ -503,6 +495,7 @@ async def set_children( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, + json_output: bool = False, ): """Set children hotkeys.""" # Validate children SS58 addresses @@ -523,6 +516,7 @@ async def set_children( f"Proposed sum of proportions is {total_proposed}." ) children_with_proportions = list(zip(proportions, children)) + successes = {} if netuid is not None: success, message = await set_children_extrinsic( subtensor=subtensor, @@ -534,12 +528,20 @@ async def set_children( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + successes[netuid] = { + "success": success, + "error": message, + "completion_block": None, + "set_block": None, + } # Result if success: if wait_for_inclusion and wait_for_finalization: current_block, completion_block = await get_childkey_completion_block( subtensor, netuid ) + successes[netuid]["completion_block"] = completion_block + successes[netuid]["set_block"] = current_block console.print( f"Your childkey request has been submitted. It will be completed around block {completion_block}. " f"The current block is {current_block}" @@ -558,7 +560,7 @@ async def set_children( if netuid_ == 0: # dont include root network continue console.print(f"Setting children on netuid {netuid_}.") - await set_children_extrinsic( + success, message = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid_, @@ -571,6 +573,12 @@ async def set_children( current_block, completion_block = await get_childkey_completion_block( subtensor, netuid_ ) + successes[netuid_] = { + "success": success, + "error": message, + "completion_block": completion_block, + "set_block": current_block, + } console.print( f"Your childkey request for netuid {netuid_} has been submitted. It will be completed around " f"block {completion_block}. The current block is {current_block}." @@ -578,6 +586,8 @@ async def set_children( console.print( ":white_heavy_check_mark: [green]Sent set children request for all subnets.[/green]" ) + if json_output: + json_console.print(json.dumps(successes)) async def revoke_children( @@ -587,11 +597,13 @@ async def revoke_children( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, + json_output: bool = False, ): """ Revokes the children hotkeys associated with a given network identifier (netuid). """ - if netuid: + dict_output = {} + if netuid is not None: success, message = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, @@ -602,13 +614,23 @@ async def revoke_children( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + dict_output[netuid] = { + "success": success, + "error": message, + "set_block": None, + "completion_block": None, + } # Result if success: - if wait_for_finalization and wait_for_inclusion: - await get_children(wallet, subtensor, netuid) + current_block, completion_block = await get_childkey_completion_block( + subtensor, netuid + ) + dict_output[netuid]["completion_block"] = completion_block + dict_output[netuid]["set_block"] = current_block console.print( - ":white_heavy_check_mark: [green]Revoked children hotkeys.[/green]" + f":white_heavy_check_mark: Your childkey revocation request for netuid {netuid} has been submitted. " + f"It will be completed around block {completion_block}. The current block is {current_block}" ) else: console.print( @@ -617,11 +639,11 @@ async def revoke_children( else: # revoke children from ALL netuids netuids = await subtensor.get_all_subnet_netuids() - for netuid in netuids: - if netuid == 0: # dont include root network + for netuid_ in netuids: + if netuid_ == 0: # dont include root network continue - console.print(f"Revoking children from netuid {netuid}.") - await set_children_extrinsic( + console.print(f"Revoking children from netuid {netuid_}.") + success, message = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -631,9 +653,29 @@ async def revoke_children( wait_for_inclusion=True, wait_for_finalization=False, ) - console.print( - ":white_heavy_check_mark: [green]Sent revoke children command. Finalization may take a few minutes.[/green]" - ) + dict_output[netuid_] = { + "success": success, + "error": message, + "set_block": None, + "completion_block": None, + } + if success: + current_block, completion_block = await get_childkey_completion_block( + subtensor, netuid_ + ) + dict_output[netuid_]["completion_block"] = completion_block + dict_output[netuid_]["set_block"] = current_block + console.print( + f":white_heavy_check_mark: Your childkey revocation request for netuid {netuid_} has been " + f"submitted. It will be completed around block {completion_block}. The current block " + f"is {current_block}" + ) + else: + err_console.print( + f"Childkey revocation failed for netuid {netuid_}: {message}." + ) + if json_output: + json_console.print(json.dumps(dict_output)) async def childkey_take( @@ -645,8 +687,13 @@ async def childkey_take( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, -): - """Get or Set childkey take.""" +) -> list[tuple[Optional[int], bool]]: + """ + Get or Set childkey take. + + Returns: + List of (netuid, success) for specified netuid (or all) and their success in setting take + """ def validate_take_value(take_value: float) -> bool: if not (0 <= take_value <= 0.18): @@ -656,20 +703,7 @@ def validate_take_value(take_value: float) -> bool: return False return True - def print_all_takes(takes: list[tuple[int, float]], ss58: str): - """Print table with netuids and Takes""" - table = Table( - title=f"Current Child Takes for [bright_magenta]{ss58}[/bright_magenta]" - ) - table.add_column("Netuid", justify="center", style="cyan") - table.add_column("Take (%)", justify="right", style="magenta") - - for take_netuid, take_value in takes: - table.add_row(str(take_netuid), f"{take_value:.2f}%") - - console.print(table) - - async def display_chk_take(ss58, take_netuid): + async def display_chk_take(ss58, take_netuid) -> float: """Print single key take for hotkey and netuid""" chk_take = await get_childkey_take( subtensor=subtensor, netuid=take_netuid, hotkey=ss58 @@ -680,6 +714,7 @@ async def display_chk_take(ss58, take_netuid): console.print( f"Child take for {ss58} is: {chk_take * 100:.2f}% on netuid {take_netuid}." ) + return chk_take async def chk_all_subnets(ss58): """Aggregate data for childkey take from all subnets""" @@ -694,10 +729,18 @@ async def chk_all_subnets(ss58): if curr_take is not None: take_value = u16_to_float(curr_take) takes.append((subnet, take_value * 100)) + table = Table( + title=f"Current Child Takes for [bright_magenta]{ss58}[/bright_magenta]" + ) + table.add_column("Netuid", justify="center", style="cyan") + table.add_column("Take (%)", justify="right", style="magenta") - print_all_takes(takes, ss58) + for take_netuid, take_value in takes: + table.add_row(str(take_netuid), f"{take_value:.2f}%") + + console.print(table) - async def set_chk_take_subnet(subnet, chk_take): + async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: """Set the childkey take for a single subnet""" success, message = await set_childkey_take_extrinsic( subtensor=subtensor, @@ -715,13 +758,17 @@ async def set_chk_take_subnet(subnet, chk_take): console.print( f"The childkey take for {wallet.hotkey.ss58_address} is now set to {take * 100:.2f}%." ) + return True else: console.print( f":cross_mark:[red] Unable to set childkey take.[/red] {message}" ) + return False # Print childkey take for other user and return (dont offer to change take rate) - if hotkey and hotkey != wallet.hotkey.ss58_address: + if not hotkey or hotkey == wallet.hotkey.ss58_address: + hotkey = wallet.hotkey.ss58_address + if hotkey != wallet.hotkey.ss58_address or not take: # display childkey take for other users if netuid: await display_chk_take(hotkey, netuid) @@ -729,70 +776,64 @@ async def set_chk_take_subnet(subnet, chk_take): console.print( f"Hotkey {hotkey} not associated with wallet {wallet.name}." ) - return + return [(netuid, False)] else: - # show childhotkey take on all subnets + # show child hotkey take on all subnets await chk_all_subnets(hotkey) if take: console.print( f"Hotkey {hotkey} not associated with wallet {wallet.name}." ) - return + return [(netuid, False)] # Validate child SS58 addresses if not take: - # print current Take, ask if change - if netuid: - await display_chk_take(wallet.hotkey.ss58_address, netuid) - else: - # print take from all netuids - await chk_all_subnets(wallet.hotkey.ss58_address) - if not Confirm.ask("Would you like to change the child take?"): - return - new_take_str = Prompt.ask("Enter the new take value (between 0 and 0.18)") - try: - new_take_value = float(new_take_str) - if not validate_take_value(new_take_value): - return - except ValueError: - err_console.print( - ":cross_mark:[red] Invalid input. Please enter a number between 0 and 0.18.[/red]" + return [(netuid, False)] + new_take_value = -1.0 + while not validate_take_value(new_take_value): + new_take_value = FloatPrompt.ask( + "Enter the new take value (between 0 and 0.18)" ) - return take = new_take_value else: if not validate_take_value(take): - return + return [(netuid, False)] if netuid: - await set_chk_take_subnet(subnet=netuid, chk_take=take) - return + return [(netuid, await set_chk_take_subnet(subnet=netuid, chk_take=take))] else: new_take_netuids = IntPrompt.ask( "Enter netuid (leave blank for all)", default=None, show_default=True ) if new_take_netuids: - await set_chk_take_subnet(subnet=new_take_netuids, chk_take=take) - return + return [ + ( + new_take_netuids, + await set_chk_take_subnet(subnet=new_take_netuids, chk_take=take), + ) + ] else: netuids = await subtensor.get_all_subnet_netuids() - for netuid in netuids: - if netuid == 0: + output_list = [] + for netuid_ in netuids: + if netuid_ == 0: continue - console.print(f"Sending to netuid {netuid} take of {take * 100:.2f}%") - await set_childkey_take_extrinsic( + console.print(f"Sending to netuid {netuid_} take of {take * 100:.2f}%") + result = await set_childkey_take_extrinsic( subtensor=subtensor, wallet=wallet, - netuid=netuid, + netuid=netuid_, hotkey=wallet.hotkey.ss58_address, take=take, prompt=prompt, wait_for_inclusion=True, wait_for_finalization=False, ) + output_list.append((netuid_, result)) console.print( f":white_heavy_check_mark: [green]Sent childkey take of {take * 100:.2f}% to all subnets.[/green]" ) + return output_list diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index eb5340fc..2e6d39d7 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -1,5 +1,6 @@ import asyncio - +import json +from collections import defaultdict from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet @@ -18,6 +19,7 @@ print_error, millify_tao, get_subnet_name, + json_console, ) if TYPE_CHECKING: @@ -31,6 +33,7 @@ async def stake_list( live: bool = False, verbose: bool = False, prompt: bool = False, + json_output: bool = False, ): coldkey_address = coldkey_ss58 if coldkey_ss58 else wallet.coldkeypub.ss58_address @@ -152,6 +155,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): reverse=True, ) sorted_substakes = root_stakes + other_stakes + substakes_values = [] for substake_ in sorted_substakes: netuid = substake_.netuid pool = dynamic_info[netuid] @@ -195,7 +199,8 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): if not verbose else f"{substake_.stake.tao:,.4f}" ) - subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {get_subnet_name(dynamic_info[netuid])}" + subnet_name = get_subnet_name(dynamic_info[netuid]) + subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {subnet_name}" rows.append( [ @@ -220,13 +225,28 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): str(Balance.from_tao(per_block_tao_emission)), ] ) + substakes_values.append( + { + "netuid": netuid, + "subnet_name": subnet_name, + "value": tao_value_.tao, + "stake_value": substake_.stake.tao, + "rate": pool.price.tao, + "swap_value": swap_value, + "registered": True if substake_.is_registered else False, + "emission": { + "alpha": per_block_emission, + "tao": per_block_tao_emission, + }, + } + ) created_table = define_table( name_, rows, total_tao_value_, total_swapped_tao_value_ ) for row in rows: created_table.add_row(*row) console.print(created_table) - return total_tao_value_, total_swapped_tao_value_ + return total_tao_value_, total_swapped_tao_value_, substakes_values def create_live_table( substakes: list, @@ -409,22 +429,23 @@ def format_cell( # Main execution block_hash = await subtensor.substrate.get_chain_head() ( - sub_stakes, - registered_delegate_info, - dynamic_info, - ) = await get_stake_data(block_hash) - balance = await subtensor.get_balance(coldkey_address) + ( + sub_stakes, + registered_delegate_info, + dynamic_info, + ), + balance, + ) = await asyncio.gather( + get_stake_data(block_hash), + subtensor.get_balance(coldkey_address, block_hash=block_hash), + ) # Iterate over substakes and aggregate them by hotkey. - hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} + hotkeys_to_substakes: dict[str, list[StakeInfo]] = defaultdict(list) for substake in sub_stakes: - hotkey = substake.hotkey_ss58 - if substake.stake.rao == 0: - continue - if hotkey not in hotkeys_to_substakes: - hotkeys_to_substakes[hotkey] = [] - hotkeys_to_substakes[hotkey].append(substake) + if substake.stake.rao != 0: + hotkeys_to_substakes[substake.hotkey_ss58].append(substake) if not hotkeys_to_substakes: print_error(f"No stakes found for coldkey ss58: ({coldkey_address})") @@ -534,15 +555,24 @@ def format_cell( num_hotkeys = len(hotkeys_to_substakes) all_hks_swapped_tao_value = Balance(0) all_hks_tao_value = Balance(0) - for hotkey in hotkeys_to_substakes.keys(): + dict_output = { + "stake_info": {}, + "coldkey_address": coldkey_address, + "network": subtensor.network, + "free_balance": 0.0, + "total_tao_value": 0.0, + "total_swapped_tao_value": 0.0, + } + for hotkey, substakes in hotkeys_to_substakes.items(): counter += 1 - tao_value, swapped_tao_value = create_table( - hotkey, hotkeys_to_substakes[hotkey] + tao_value, swapped_tao_value, substake_values_ = create_table( + hotkey, substakes ) + dict_output["stake_info"][hotkey] = substake_values_ all_hks_tao_value += tao_value all_hks_swapped_tao_value += swapped_tao_value - if num_hotkeys > 1 and counter < num_hotkeys and prompt: + if num_hotkeys > 1 and counter < num_hotkeys and prompt and not json_output: console.print("\nPress Enter to continue to the next hotkey...") input() @@ -556,7 +586,6 @@ def format_cell( if not verbose else all_hks_swapped_tao_value ) - console.print("\n\n") console.print( f"Wallet:\n" @@ -565,6 +594,11 @@ def format_cell( f" Total TAO Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" f" Total TAO Swapped Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_swapped_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" ) + dict_output["free_balance"] = balance.tao + dict_output["total_tao_value"] = all_hks_tao_value.tao + dict_output["total_swapped_tao_value"] = all_hks_swapped_tao_value.tao + if json_output: + json_console.print(json.dumps(dict_output)) if not sub_stakes: console.print( f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})" diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 403dc966..42f61934 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -444,9 +444,10 @@ async def move_stake( destination_hotkey: str, amount: float, stake_all: bool, + era: int, interactive_selection: bool = False, prompt: bool = True, -): +) -> bool: if interactive_selection: try: selection = await stake_move_transfer_selection(subtensor, wallet) @@ -512,8 +513,10 @@ async def move_stake( if amount_to_move_as_balance > origin_stake_balance: err_console.print( f"[red]Not enough stake[/red]:\n" - f" Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f" < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f"{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f"{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" ) return False @@ -563,7 +566,7 @@ async def move_stake( }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, era={"period": era} ) response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False @@ -573,13 +576,12 @@ async def move_stake( console.print(":white_heavy_check_mark: [green]Sent[/green]") return True else: - await response.process_events() if not await response.is_success: err_console.print( f"\n:cross_mark: [red]Failed[/red] with error:" f" {format_error_message(await response.error_message)}" ) - return + return False else: console.print( ":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]" @@ -611,7 +613,7 @@ async def move_stake( f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" ) - return + return True async def transfer_stake( @@ -622,6 +624,7 @@ async def transfer_stake( origin_netuid: int, dest_netuid: int, dest_coldkey_ss58: str, + era: int, interactive_selection: bool = False, stake_all: bool = False, prompt: bool = True, @@ -747,7 +750,7 @@ async def transfer_stake( ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, era={"period": era} ) response = await subtensor.substrate.submit_extrinsic( @@ -758,7 +761,6 @@ async def transfer_stake( console.print(":white_heavy_check_mark: [green]Sent[/green]") return True - await response.process_events() if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red] with error: " @@ -798,6 +800,7 @@ async def swap_stake( destination_netuid: int, amount: float, swap_all: bool = False, + era: int = 3, interactive_selection: bool = False, prompt: bool = True, wait_for_inclusion: bool = True, @@ -903,7 +906,8 @@ async def swap_stake( return False with console.status( - f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] to netuid [blue]{destination_netuid}[/blue]..." + f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " + f"to netuid [blue]{destination_netuid}[/blue]..." ): call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -917,7 +921,7 @@ async def swap_stake( ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, era={"period": era} ) response = await subtensor.substrate.submit_extrinsic( @@ -930,7 +934,6 @@ async def swap_stake( console.print(":white_heavy_check_mark: [green]Sent[/green]") return True - await response.process_events() if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red] with error: " diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 898af9cf..f80201e2 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -1,4 +1,5 @@ import asyncio +import json from functools import partial from typing import TYPE_CHECKING, Optional @@ -20,6 +21,7 @@ format_error_message, group_subnets, unlock_key, + json_console, ) if TYPE_CHECKING: @@ -41,6 +43,8 @@ async def unstake( safe_staking: bool, rate_tolerance: float, allow_partial_stake: bool, + json_output: bool, + era: int, ): """Unstake from hotkey(s).""" with console.status( @@ -258,8 +262,11 @@ async def unstake( base_unstake_op["price_with_tolerance"] = price_with_tolerance base_table_row.extend( [ - f"{rate_with_tolerance:.4f} {Balance.get_unit(0)}/{Balance.get_unit(netuid)}", # Rate with tolerance - f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", # Partial unstake + # Rate with tolerance + f"{rate_with_tolerance:.4f} {Balance.get_unit(0)}/{Balance.get_unit(netuid)}", + # Partial unstake + f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]" + f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", ] ) @@ -290,44 +297,45 @@ async def unstake( if not unlock_key(wallet).success: return False + successes = [] with console.status("\n:satellite: Performing unstaking operations...") as status: - if safe_staking: - for op in unstake_operations: - if op["netuid"] == 0: - await _unstake_extrinsic( - wallet=wallet, - subtensor=subtensor, - netuid=op["netuid"], - amount=op["amount_to_unstake"], - current_stake=op["current_stake_balance"], - hotkey_ss58=op["hotkey_ss58"], - status=status, - ) - else: - await _safe_unstake_extrinsic( - wallet=wallet, - subtensor=subtensor, - netuid=op["netuid"], - amount=op["amount_to_unstake"], - hotkey_ss58=op["hotkey_ss58"], - price_limit=op["price_with_tolerance"], - allow_partial_stake=allow_partial_stake, - status=status, - ) - else: - for op in unstake_operations: - await _unstake_extrinsic( - wallet=wallet, - subtensor=subtensor, - netuid=op["netuid"], - amount=op["amount_to_unstake"], - current_stake=op["current_stake_balance"], - hotkey_ss58=op["hotkey_ss58"], - status=status, - ) + for op in unstake_operations: + common_args = { + "wallet": wallet, + "subtensor": subtensor, + "netuid": op["netuid"], + "amount": op["amount_to_unstake"], + "hotkey_ss58": op["hotkey_ss58"], + "status": status, + "era": era, + } + + if safe_staking and op["netuid"] != 0: + func = _safe_unstake_extrinsic + specific_args = { + "price_limit": op["price_with_tolerance"], + "allow_partial_stake": allow_partial_stake, + } + else: + func = _unstake_extrinsic + specific_args = {"current_stake": op["current_stake_balance"]} + + suc = await func(**common_args, **specific_args) + + successes.append( + { + "netuid": op["netuid"], + "hotkey_ss58": op["hotkey_ss58"], + "unstake_amount": op["amount_to_unstake"].tao, + "success": suc, + } + ) + console.print( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." ) + if json_output: + json_console.print(json.dumps(successes)) async def unstake_all( @@ -338,7 +346,9 @@ async def unstake_all( all_hotkeys: bool = False, include_hotkeys: Optional[list[str]] = None, exclude_hotkeys: Optional[list[str]] = None, + era: int = 3, prompt: bool = True, + json_output: bool = False, ) -> bool: """Unstakes all stakes from all hotkeys in all subnets.""" include_hotkeys = include_hotkeys or [] @@ -434,7 +444,7 @@ async def unstake_all( style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], ) table.add_column( - f"Recieved ({Balance.unit})", + f"Received ({Balance.unit})", justify="center", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) @@ -484,11 +494,16 @@ async def unstake_all( slippage_pct, ) console.print(table) - message = "" if max_slippage > 5: - message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" - message += "-------------------------------------------------------------------------------------------------------------------\n" + message = ( + f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]--------------------------------------------------------------" + f"-----------------------------------------------------\n" + f"[bold]WARNING:[/bold] The slippage on one of your operations is high: " + f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%" + f"[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" + "----------------------------------------------------------------------------------------------------------" + "---------\n" + ) console.print(message) console.print( @@ -502,17 +517,20 @@ async def unstake_all( if not unlock_key(wallet).success: return False - + successes = {} with console.status("Unstaking all stakes...") as status: for hotkey_ss58 in hotkey_ss58s: - await _unstake_all_extrinsic( + successes[hotkey_ss58] = await _unstake_all_extrinsic( wallet=wallet, subtensor=subtensor, hotkey_ss58=hotkey_ss58, hotkey_name=hotkey_names.get(hotkey_ss58, hotkey_ss58), unstake_all_alpha=unstake_all_alpha, status=status, + era=era, ) + if json_output: + return json_console.print(json.dumps({"success": successes})) # Extrinsics @@ -524,7 +542,8 @@ async def _unstake_extrinsic( current_stake: Balance, hotkey_ss58: str, status=None, -) -> None: + era: int = 3, +) -> bool: """Execute a standard unstake extrinsic. Args: @@ -535,6 +554,7 @@ async def _unstake_extrinsic( wallet: Wallet instance subtensor: Subtensor interface status: Optional status for console updates + era: blocks for which the transaction is valid """ err_out = partial(print_error, status=status) failure_prelude = ( @@ -546,33 +566,32 @@ async def _unstake_extrinsic( f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..." ) - current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "amount_unstaked": amount.rao, - }, + current_balance, call = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": amount.rao, + }, + ), ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, era={"period": era} ) try: response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - await response.process_events() - if not await response.is_success: err_out( f"{failure_prelude} with error: " f"{format_error_message(await response.error_message)}" ) - return - + return False # Fetch latest balance and stake block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -593,9 +612,11 @@ async def _unstake_extrinsic( f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" ) + return True except Exception as e: err_out(f"{failure_prelude} with error: {str(e)}") + return False async def _safe_unstake_extrinsic( @@ -607,7 +628,8 @@ async def _safe_unstake_extrinsic( price_limit: Balance, allow_partial_stake: bool, status=None, -) -> None: + era: int = 3, +) -> bool: """Execute a safe unstake extrinsic with price limit. Args: @@ -632,30 +654,31 @@ async def _safe_unstake_extrinsic( block_hash = await subtensor.substrate.get_chain_head() - current_balance, next_nonce, current_stake = await asyncio.gather( + current_balance, next_nonce, current_stake, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), subtensor.get_stake( hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address, netuid=netuid, + block_hash=block_hash, + ), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake_limit", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": amount.rao, + "limit_price": price_limit, + "allow_partial": allow_partial_stake, + }, + block_hash=block_hash, ), - ) - - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake_limit", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "amount_unstaked": amount.rao, - "limit_price": price_limit, - "allow_partial": allow_partial_stake, - }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} ) try: @@ -670,17 +693,15 @@ async def _safe_unstake_extrinsic( f"Either increase price tolerance or enable partial unstaking.", status=status, ) - return else: err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") - return + return False - await response.process_events() if not await response.is_success: err_out( f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) - return + return False block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -711,6 +732,7 @@ async def _safe_unstake_extrinsic( f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" ) + return True async def _unstake_all_extrinsic( @@ -720,6 +742,7 @@ async def _unstake_all_extrinsic( hotkey_name: str, unstake_all_alpha: bool, status=None, + era: int = 3, ) -> None: """Execute an unstake all extrinsic. @@ -770,13 +793,11 @@ async def _unstake_all_extrinsic( try: response = await subtensor.substrate.submit_extrinsic( extrinsic=await subtensor.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.coldkey, + call=call, keypair=wallet.coldkey, era={"period": era} ), wait_for_inclusion=True, wait_for_finalization=False, ) - await response.process_events() if not await response.is_success: err_out( diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index eb6b7b5c..fa05b946 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -13,6 +13,7 @@ err_console, get_subnet_name, print_error, + json_console, ) if TYPE_CHECKING: @@ -26,6 +27,7 @@ async def price( interval_hours: int = 24, html_output: bool = False, log_scale: bool = False, + json_output: bool = False, ): """ Fetch historical price data for subnets and display it in a chart. @@ -60,7 +62,7 @@ async def price( all_subnet_infos = await asyncio.gather(*subnet_info_cors) subnet_data = _process_subnet_data( - block_numbers, all_subnet_infos, netuids, all_netuids, interval_hours + block_numbers, all_subnet_infos, netuids, all_netuids ) if not subnet_data: @@ -71,17 +73,13 @@ async def price( await _generate_html_output( subnet_data, block_numbers, interval_hours, log_scale ) + elif json_output: + json_console.print(json.dumps(_generate_json_output(subnet_data))) else: _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale) -def _process_subnet_data( - block_numbers, - all_subnet_infos, - netuids, - all_netuids, - interval_hours, -): +def _process_subnet_data(block_numbers, all_subnet_infos, netuids, all_netuids): """ Process subnet data into a structured format for price analysis. """ @@ -772,6 +770,10 @@ async def _generate_html_output( print_error(f"Error generating price chart: {e}") +def _generate_json_output(subnet_data): + return {netuid: data for netuid, data in subnet_data.items()} + + def _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale): """ Render the price data in a textual CLI style with plotille ASCII charts. @@ -802,7 +804,7 @@ def color_label(text): fig.plot( block_numbers, - data["prices"], + prices, label=f"Subnet {netuid} Price", interp="linear", lc="bae98f", diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 81043d8c..fac96979 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -34,6 +34,7 @@ prompt_for_identity, get_subnet_name, unlock_key, + json_console, ) if TYPE_CHECKING: @@ -51,7 +52,7 @@ async def register_subnetwork_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, -) -> bool: +) -> tuple[bool, Optional[int]]: """Registers a new subnetwork. wallet (bittensor.wallet): @@ -100,7 +101,7 @@ async def _find_event_attributes_in_extrinsic_receipt( f"[{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}] " f"to register a subnet." ) - return False + return False, None if prompt: console.print( @@ -109,7 +110,7 @@ async def _find_event_attributes_in_extrinsic_receipt( if not Confirm.ask( f"Do you want to burn [{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost} to register a subnet?" ): - return False + return False, None call_params = { "hotkey": wallet.hotkey.ss58_address, @@ -151,10 +152,10 @@ async def _find_event_attributes_in_extrinsic_receipt( f"[red]Error:[/red] Identity field [white]{field}[/white] must be <= {max_size} bytes.\n" f"Value '{value.decode()}' is {len(value)} bytes." ) - return False + return False, None if not unlock_key(wallet).success: - return False + return False, None with console.status(":satellite: Registering subnet...", spinner="earth"): substrate = subtensor.substrate @@ -175,15 +176,14 @@ async def _find_event_attributes_in_extrinsic_receipt( # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True + return True, None - await response.process_events() if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}" ) await asyncio.sleep(0.5) - return False + return False, None # Successful registration, final check for membership else: @@ -193,7 +193,7 @@ async def _find_event_attributes_in_extrinsic_receipt( console.print( f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" ) - return True + return True, int(attributes[0]) # commands @@ -206,40 +206,41 @@ async def subnets_list( no_cache: bool, verbose: bool, live: bool, + json_output: bool, ): """List all subnet netuids in the network.""" async def fetch_subnet_data(): - block_number = await subtensor.substrate.get_block_number(None) - subnets = await subtensor.all_subnets() + block_number_ = await subtensor.substrate.get_block_number(None) + subnets_ = await subtensor.all_subnets() # Sort subnets by market cap, keeping the root subnet in the first position - root_subnet = next(s for s in subnets if s.netuid == 0) + root_subnet = next(s for s in subnets_ if s.netuid == 0) other_subnets = sorted( - [s for s in subnets if s.netuid != 0], + [s for s in subnets_ if s.netuid != 0], key=lambda x: (x.alpha_in.tao + x.alpha_out.tao) * x.price.tao, reverse=True, ) sorted_subnets = [root_subnet] + other_subnets - return sorted_subnets, block_number + return sorted_subnets, block_number_ def calculate_emission_stats( - subnets: list, block_number: int + subnets_: list, block_number_: int ) -> tuple[Balance, str]: # We do not include the root subnet in the emission calculation total_tao_emitted = sum( - subnet.tao_in.tao for subnet in subnets if subnet.netuid != 0 + subnet.tao_in.tao for subnet in subnets_ if subnet.netuid != 0 ) - emission_percentage = (total_tao_emitted / block_number) * 100 + emission_percentage = (total_tao_emitted / block_number_) * 100 percentage_color = "dark_sea_green" if emission_percentage < 100 else "red" formatted_percentage = ( f"[{percentage_color}]{emission_percentage:.2f}%[/{percentage_color}]" ) if not verbose: - percentage_string = f"τ {millify_tao(total_tao_emitted)}/{millify_tao(block_number)} ({formatted_percentage})" + percentage_string = f"τ {millify_tao(total_tao_emitted)}/{millify_tao(block_number_)} ({formatted_percentage})" else: percentage_string = ( - f"τ {total_tao_emitted:.1f}/{block_number} ({formatted_percentage})" + f"τ {total_tao_emitted:.1f}/{block_number_} ({formatted_percentage})" ) return total_tao_emitted, percentage_string @@ -249,7 +250,7 @@ def define_table( total_netuids: int, tao_emission_percentage: str, ): - table = Table( + defined_table = Table( title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets" f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\n", show_footer=True, @@ -262,61 +263,61 @@ def define_table( pad_edge=True, ) - table.add_column( + defined_table.add_column( "[bold white]Netuid", style="grey89", justify="center", footer=str(total_netuids), ) - table.add_column("[bold white]Name", style="cyan", justify="left") - table.add_column( + defined_table.add_column("[bold white]Name", style="cyan", justify="left") + defined_table.add_column( f"[bold white]Price \n({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", style="dark_sea_green2", justify="left", footer=f"τ {total_rate}", ) - table.add_column( + defined_table.add_column( f"[bold white]Market Cap \n({Balance.get_unit(1)} * Price)", style="steel_blue3", justify="left", ) - table.add_column( + defined_table.add_column( f"[bold white]Emission ({Balance.get_unit(0)})", style=COLOR_PALETTE["POOLS"]["EMISSION"], justify="left", footer=f"τ {total_emissions}", ) - table.add_column( + defined_table.add_column( f"[bold white]P ({Balance.get_unit(0)}_in, {Balance.get_unit(1)}_in)", style=COLOR_PALETTE["STAKE"]["TAO"], justify="left", footer=f"{tao_emission_percentage}", ) - table.add_column( + defined_table.add_column( f"[bold white]Stake ({Balance.get_unit(1)}_out)", style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="left", ) - table.add_column( + defined_table.add_column( f"[bold white]Supply ({Balance.get_unit(1)})", style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], justify="left", ) - table.add_column( + defined_table.add_column( "[bold white]Tempo (k/n)", style=COLOR_PALETTE["GENERAL"]["TEMPO"], justify="left", overflow="fold", ) - return table + return defined_table # Non-live mode - def create_table(subnets, block_number): + def _create_table(subnets_, block_number_): rows = [] - _, percentage_string = calculate_emission_stats(subnets, block_number) + _, percentage_string = calculate_emission_stats(subnets_, block_number_) - for subnet in subnets: + for subnet in subnets_: netuid = subnet.netuid symbol = f"{subnet.symbol}\u200e" @@ -363,7 +364,7 @@ def create_table(subnets, block_number): # Prepare cells netuid_cell = str(netuid) subnet_name_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f"[{COLOR_PALETTE.G.SYM}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE.G.SYM}]" f" {get_subnet_name(subnet)}" ) emission_cell = f"τ {emission_tao:,.4f}" @@ -396,23 +397,76 @@ def create_table(subnets, block_number): ) total_emissions = round( - sum(subnet.tao_in_emission.tao for subnet in subnets if subnet.netuid != 0), + sum( + subnet.tao_in_emission.tao for subnet in subnets_ if subnet.netuid != 0 + ), 4, ) total_rate = round( - sum(float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0), 4 + sum(float(subnet.price.tao) for subnet in subnets_ if subnet.netuid != 0), 4 ) - total_netuids = len(subnets) - table = define_table( + total_netuids = len(subnets_) + defined_table = define_table( total_emissions, total_rate, total_netuids, percentage_string ) for row in rows: - table.add_row(*row) - return table + defined_table.add_row(*row) + return defined_table + + def dict_table(subnets_, block_number_) -> dict: + subnet_rows = {} + total_tao_emitted, _ = calculate_emission_stats(subnets_, block_number_) + total_emissions = 0.0 + total_rate = 0.0 + total_netuids = len(subnets_) + emission_percentage = (total_tao_emitted / block_number_) * 100 + for subnet in subnets_: + total_emissions += subnet.tao_in_emission.tao + total_rate += subnet.price.tao + netuid = subnet.netuid + if netuid == 0: + emission_tao = 0.0 + else: + emission_tao = subnet.tao_in_emission.tao + alpha_in_value = subnet.alpha_in.tao + alpha_out_value = subnet.alpha_out.tao + price_value = subnet.price.tao + market_cap = (subnet.alpha_in.tao + subnet.alpha_out.tao) * subnet.price.tao + tao_in = subnet.tao_in.tao if netuid != 0 else None + alpha_in = alpha_in_value if netuid != 0 else None + alpha_out = alpha_out_value if netuid != 0 else None + supply = subnet.alpha_in.tao + subnet.alpha_out.tao + subnet_name = get_subnet_name(subnet) + tempo = { + "blocks_since_last_step": ( + subnet.blocks_since_last_step if netuid != 0 else None + ), + "sn_tempo": (subnet.tempo if netuid != 0 else None), + } + subnet_rows[netuid] = { + "netuid": netuid, + "subnet_name": subnet_name, + "price": price_value, + "market_cap": market_cap, + "emission": emission_tao, + "liquidity": {"tao_in": tao_in, "alpha_in": alpha_in}, + "alpha_out": alpha_out, + "supply": supply, + "tempo": tempo, + } + output = { + "total_tao_emitted": total_tao_emitted, + "total_emissions": total_emissions, + "total_rate": total_rate, + "total_netuids": total_netuids, + "emission_percentage": emission_percentage, + "subnets": subnet_rows, + } + return output # Live mode - def create_table_live(subnets, previous_data, block_number): + def create_table_live(subnets_, previous_data_, block_number_): def format_cell( value, previous_value, unit="", unit_first=False, precision=4, millify=False ): @@ -516,9 +570,9 @@ def format_liquidity_cell( rows = [] current_data = {} # To store current values for comparison in the next update - _, percentage_string = calculate_emission_stats(subnets, block_number) + _, percentage_string = calculate_emission_stats(subnets_, block_number_) - for subnet in subnets: + for subnet in subnets_: netuid = subnet.netuid symbol = f"{subnet.symbol}\u200e" @@ -541,7 +595,7 @@ def format_liquidity_cell( "supply": supply, "blocks_since_last_step": subnet.blocks_since_last_step, } - prev = previous_data.get(netuid, {}) if previous_data else {} + prev = previous_data_.get(netuid, {}) if previous_data_ else {} # Prepare cells if netuid == 0: @@ -652,9 +706,9 @@ def format_liquidity_cell( ) # Calculate totals - total_netuids = len(subnets) + total_netuids = len(subnets_) _total_emissions = sum( - subnet.tao_in_emission.tao for subnet in subnets if subnet.netuid != 0 + subnet.tao_in_emission.tao for subnet in subnets_ if subnet.netuid != 0 ) total_emissions = ( f"{millify_tao(_total_emissions)}" @@ -662,7 +716,7 @@ def format_liquidity_cell( else f"{_total_emissions:,.2f}" ) - total_rate = sum(subnet.price.tao for subnet in subnets if subnet.netuid != 0) + total_rate = sum(subnet.price.tao for subnet in subnets_ if subnet.netuid != 0) total_rate = ( f"{millify_tao(total_rate)}" if not verbose else f"{total_rate:,.2f}" ) @@ -733,8 +787,11 @@ def format_liquidity_cell( else: # Non-live mode subnets, block_number = await fetch_subnet_data() - table = create_table(subnets, block_number) - console.print(table) + if json_output: + json_console.print(json.dumps(dict_table(subnets, block_number))) + else: + table = _create_table(subnets, block_number) + console.print(table) return # TODO: Temporarily returning till we update docs @@ -804,20 +861,22 @@ async def show( delegate_selection: bool = False, verbose: bool = False, prompt: bool = True, + json_output: bool = False, ) -> Optional[str]: async def show_root(): + # TODO json_output for this, don't forget block_hash = await subtensor.substrate.get_chain_head() - all_subnets = await subtensor.all_subnets(block_hash=block_hash) - root_info = next((s for s in all_subnets if s.netuid == 0), None) - if root_info is None: - print_error("The root subnet does not exist") - return False - root_state, identities, old_identities = await asyncio.gather( + all_subnets, root_state, identities, old_identities = await asyncio.gather( + subtensor.all_subnets(block_hash=block_hash), subtensor.get_subnet_state(netuid=0, block_hash=block_hash), subtensor.query_all_identities(block_hash=block_hash), subtensor.get_delegate_identities(block_hash=block_hash), ) + root_info = next((s for s in all_subnets if s.netuid == 0), None) + if root_info is None: + print_error("The root subnet does not exist") + return False if root_state is None: err_console.print("The root subnet does not exist") @@ -829,12 +888,11 @@ async def show_root(): ) return - tao_sum = sum( - [root_state.tao_stake[idx].tao for idx in range(len(root_state.tao_stake))] - ) + tao_sum = sum(root_state.tao_stake).tao table = Table( - title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Root Network\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", + title=f"[{COLOR_PALETTE.G.HEADER}]Root Network\n[{COLOR_PALETTE.G.SUBHEAD}]" + f"Network: {subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1119,6 +1177,7 @@ async def show_subnet(netuid_: int): ) rows = [] + json_out_rows = [] for idx in sorted_indices: # Get identity for this uid coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get( @@ -1170,6 +1229,22 @@ async def show_subnet(netuid_: int): uid_identity, # Identity ) ) + json_out_rows.append( + { + "uid": idx, + "stake": subnet_state.total_stake[idx].tao, + "alpha_stake": subnet_state.alpha_stake[idx].tao, + "tao_stake": tao_stake.tao, + "dividends": subnet_state.dividends[idx], + "incentive": subnet_state.incentives[idx], + "emissions": Balance.from_tao(subnet_state.emission[idx].tao) + .set_unit(netuid_) + .tao, + "hotkey": subnet_state.hotkeys[idx], + "coldkey": subnet_state.coldkeys[idx], + "identity": uid_identity, + } + ) # Add columns to the table table.add_column("UID", style="grey89", no_wrap=True, justify="center") @@ -1262,6 +1337,24 @@ async def show_subnet(netuid_: int): if current_burn_cost else Balance(0) ) + output_dict = { + "netuid": netuid_, + "name": subnet_name_display, + "owner": subnet_info.owner_coldkey, + "owner_identity": owner_identity, + "rate": subnet_info.price.tao, + "emission": subnet_info.emission.tao, + "tao_pool": subnet_info.tao_in.tao, + "alpha_pool": subnet_info.alpha_in.tao, + "tempo": { + "block_since_last_step": subnet_info.blocks_since_last_step, + "tempo": subnet_info.tempo, + }, + "registration_cost": current_registration_burn.tao, + "uids": json_out_rows, + } + if json_output: + json_console.print(json.dumps(output_dict)) console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" @@ -1334,7 +1427,9 @@ async def show_subnet(netuid_: int): return result -async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: +async def burn_cost( + subtensor: "SubtensorInterface", json_output: bool = False +) -> Optional[Balance]: """View locking cost of creating a new subnetwork""" with console.status( f":satellite:Retrieving lock cost from {subtensor.network}...", @@ -1342,26 +1437,47 @@ async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: ): current_burn_cost = await subtensor.burn_cost() if current_burn_cost: - console.print( - f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_burn_cost}" - ) + if json_output: + json_console.print( + json.dumps({"burn_cost": current_burn_cost.to_dict(), "error": ""}) + ) + else: + console.print( + f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_burn_cost}" + ) return current_burn_cost else: - err_console.print( - "Subnet burn cost: [red]Failed to get subnet burn cost[/red]" - ) + if json_output: + json_console.print( + json.dumps( + {"burn_cost": None, "error": "Failed to get subnet burn cost"} + ) + ) + else: + err_console.print( + "Subnet burn cost: [red]Failed to get subnet burn cost[/red]" + ) return None async def create( - wallet: Wallet, subtensor: "SubtensorInterface", subnet_identity: dict, prompt: bool + wallet: Wallet, + subtensor: "SubtensorInterface", + subnet_identity: dict, + json_output: bool, + prompt: bool, ): """Register a subnetwork""" # Call register command. - success = await register_subnetwork_extrinsic( + success, netuid = await register_subnetwork_extrinsic( subtensor, wallet, subnet_identity, prompt=prompt ) + if json_output: + # technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present + # (2025/04/03), we always use the default `wait_for_finalization=True`, so it will always have a netuid. + json_console.print(json.dumps({"success": success, "netuid": netuid})) + return success if success and prompt: # Prompt for user to set identity. do_set_identity = Confirm.ask( @@ -1436,7 +1552,12 @@ async def pow_register( async def register( - wallet: Wallet, subtensor: "SubtensorInterface", netuid: int, prompt: bool + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + era: Optional[int], + json_output: bool, + prompt: bool, ): """Register neuron by recycling some TAO.""" @@ -1445,6 +1566,12 @@ async def register( block_hash = await subtensor.substrate.get_chain_head() if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): err_console.print(f"[red]Subnet {netuid} does not exist[/red]") + if json_output: + json_console.print( + json.dumps( + {"success": False, "error": f"Subnet {netuid} does not exist"} + ) + ) return # Check current recycle amount @@ -1466,7 +1593,7 @@ async def register( ) return - if prompt: + if prompt and not json_output: # TODO make this a reusable function, also used in subnets list # Show creation table. table = Table( @@ -1518,23 +1645,28 @@ async def register( console.print(table) if not ( Confirm.ask( - f"Your balance is: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\nThe cost to register by recycle is " - f"[{COLOR_PALETTE['GENERAL']['COST']}]{current_recycle}[/{COLOR_PALETTE['GENERAL']['COST']}]\nDo you want to continue?", + f"Your balance is: [{COLOR_PALETTE.G.BAL}]{balance}[/{COLOR_PALETTE.G.BAL}]\n" + f"The cost to register by recycle is " + f"[{COLOR_PALETTE.G.COST}]{current_recycle}[/{COLOR_PALETTE.G.COST}]\n" + f"Do you want to continue?", default=False, ) ): return if netuid == 0: - await root_register_extrinsic(subtensor, wallet=wallet) + success, msg = await root_register_extrinsic(subtensor, wallet=wallet) else: - await burned_register_extrinsic( + success, msg = await burned_register_extrinsic( subtensor, wallet=wallet, netuid=netuid, prompt=False, old_balance=balance, + era=era, ) + if json_output: + json_console.print(json.dumps({"success": success, "msg": msg})) # TODO: Confirm emissions, incentive, Dividends are to be fetched from subnet_state or keep NeuronInfo @@ -2126,10 +2258,15 @@ async def set_identity( return True -async def get_identity(subtensor: "SubtensorInterface", netuid: int, title: str = None): +async def get_identity( + subtensor: "SubtensorInterface", + netuid: int, + title: str = None, + json_output: bool = False, +) -> Optional[dict]: """Fetch and display existing subnet identity information.""" if not title: - title = "Subnet Identity" + title = f"Current Subnet {netuid} Identity" if not await subtensor.subnet_exists(netuid): print_error(f"Subnet {netuid} does not exist.") @@ -2147,10 +2284,12 @@ async def get_identity(subtensor: "SubtensorInterface", netuid: int, title: str f" for subnet [blue]{netuid}[/blue]" f" on {subtensor}" ) + if json_output: + json_console.print("{}") return {} - - if identity: - table = create_identity_table(title=f"Current Subnet {netuid} Identity") + else: + table = create_identity_table(title=title) + dict_out = {} table.add_row("Netuid", str(netuid)) for key in [ "subnet_name", @@ -2163,5 +2302,9 @@ async def get_identity(subtensor: "SubtensorInterface", netuid: int, title: str ]: value = getattr(identity, key, None) table.add_row(key, str(value) if value else "~") - console.print(table) + dict_out[key] = value + if json_output: + json_console.print(json.dumps(dict_out)) + else: + console.print(table) return identity diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 8184bd79..e5502714 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -1,4 +1,5 @@ import asyncio +import json from typing import TYPE_CHECKING, Union, Optional from bittensor_wallet import Wallet @@ -19,6 +20,7 @@ blocks_to_duration, float_to_u64, float_to_u16, + json_console, ) if TYPE_CHECKING: @@ -350,6 +352,19 @@ def display_votes( return "\n".join(vote_list) +def serialize_vote_data( + vote_data: "ProposalVoteData", delegate_info: dict[str, DelegatesDetails] +) -> list[dict[str, bool]]: + vote_list = {} + for address in vote_data.ayes: + f_add = delegate_info[address].display if address in delegate_info else address + vote_list[f_add] = True + for address in vote_data.nays: + f_add = delegate_info[address].display if address in delegate_info else address + vote_list[f_add] = False + return vote_list + + def format_call_data(call_data: dict) -> str: # Extract the module and call details module, call_details = next(iter(call_data.items())) @@ -559,6 +574,7 @@ async def sudo_set_hyperparameter( netuid: int, param_name: str, param_value: Optional[str], + json_output: bool, ): """Set subnet hyperparameters.""" @@ -584,17 +600,22 @@ async def sudo_set_hyperparameter( f"Hyperparameter [dark_orange]{param_name}[/dark_orange] value is not within bounds. " f"Value is {normalized_value} but must be {value}" ) - return + return False success = await set_hyperparameter_extrinsic( subtensor, wallet, netuid, param_name, value ) + if json_output: + return success if success: console.print("\n") print_verbose("Fetching hyperparameters") - return await get_hyperparameters(subtensor, netuid=netuid) + await get_hyperparameters(subtensor, netuid=netuid) + return success -async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): +async def get_hyperparameters( + subtensor: "SubtensorInterface", netuid: int, json_output: bool = False +) -> bool: """View hyperparameters of a subnetwork.""" print_verbose("Fetching hyperparameters") if not await subtensor.subnet_exists(netuid): @@ -607,32 +628,44 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): return False table = Table( - Column("[white]HYPERPARAMETER", style=COLOR_PALETTE["SUDO"]["HYPERPARAMETER"]), - Column("[white]VALUE", style=COLOR_PALETTE["SUDO"]["VALUE"]), - Column("[white]NORMALIZED", style=COLOR_PALETTE["SUDO"]["NORMALIZED"]), - title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]\nSubnet Hyperparameters\n NETUID: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}" + Column("[white]HYPERPARAMETER", style=COLOR_PALETTE.SU.HYPERPARAMETER), + Column("[white]VALUE", style=COLOR_PALETTE.SU.VALUE), + Column("[white]NORMALIZED", style=COLOR_PALETTE.SU.NORMAL), + title=f"[{COLOR_PALETTE.G.HEADER}]\nSubnet Hyperparameters\n NETUID: " + f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}" f"{f' ({subnet_info.subnet_name})' if subnet_info.subnet_name is not None else ''}" - f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f" - Network: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", + f"[/{COLOR_PALETTE.G.SUBHEAD}]" + f" - Network: [{COLOR_PALETTE.G.SUBHEAD}]{subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n", show_footer=True, width=None, pad_edge=False, box=box.SIMPLE, show_edge=True, ) + dict_out = [] normalized_values = normalize_hyperparameters(subnet) for param, value, norm_value in normalized_values: table.add_row(" " + param, value, norm_value) - - console.print(table) + dict_out.append( + { + "hyperparameter": param, + "value": value, + "normalized_value": norm_value, + } + ) + if json_output: + json_console.print(json.dumps(dict_out)) + else: + console.print(table) return True -async def get_senate(subtensor: "SubtensorInterface"): - """View Bittensor's senate memebers""" +async def get_senate( + subtensor: "SubtensorInterface", json_output: bool = False +) -> None: + """View Bittensor's senate members""" with console.status( f":satellite: Syncing with chain: [white]{subtensor}[/white] ...", spinner="aesthetic", @@ -663,21 +696,27 @@ async def get_senate(subtensor: "SubtensorInterface"): border_style="bright_black", leading=True, ) + dict_output = [] for ss58_address in senate_members: + member_name = ( + delegate_info[ss58_address].display + if ss58_address in delegate_info + else "~" + ) table.add_row( - ( - delegate_info[ss58_address].display - if ss58_address in delegate_info - else "~" - ), + member_name, ss58_address, ) - + dict_output.append({"name": member_name, "ss58_address": ss58_address}) + if json_output: + json_console.print(json.dumps(dict_output)) return console.print(table) -async def proposals(subtensor: "SubtensorInterface", verbose: bool): +async def proposals( + subtensor: "SubtensorInterface", verbose: bool, json_output: bool = False +) -> None: console.print( ":satellite: Syncing with chain: [white]{}[/white] ...".format( subtensor.network @@ -723,6 +762,7 @@ async def proposals(subtensor: "SubtensorInterface", verbose: bool): width=None, border_style="bright_black", ) + dict_output = [] for hash_, (call_data, vote_data) in all_proposals.items(): blocks_remaining = vote_data.end - current_block if blocks_remaining > 0: @@ -741,6 +781,7 @@ async def proposals(subtensor: "SubtensorInterface", verbose: bool): if vote_data.threshold > 0 else 0 ) + f_call_data = format_call_data(call_data) table.add_row( hash_ if verbose else f"{hash_[:4]}...{hash_[-4:]}", str(vote_data.threshold), @@ -748,8 +789,21 @@ async def proposals(subtensor: "SubtensorInterface", verbose: bool): f"{len(vote_data.nays)} ({nays_threshold:.2f}%)", display_votes(vote_data, registered_delegate_info), vote_end_cell, - format_call_data(call_data), + f_call_data, + ) + dict_output.append( + { + "hash": hash_, + "threshold": vote_data.threshold, + "ayes": len(vote_data.ayes), + "nays": len(vote_data.nays), + "votes": serialize_vote_data(vote_data, registered_delegate_info), + "end": vote_data.end, + "call_data": f_call_data, + } ) + if json_output: + json_console.print(json.dumps(dict_output)) console.print(table) console.print( "\n[dim]* Both Ayes and Nays percentages are calculated relative to the proposal's threshold.[/dim]" diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 82b1253a..006fb511 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1,5 +1,6 @@ import asyncio import itertools +import json import os from collections import defaultdict from typing import Generator, Optional @@ -14,8 +15,9 @@ from rich.table import Column, Table from rich.tree import Tree from rich.padding import Padding +from rich.prompt import Confirm -from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src import COLOR_PALETTE, COLORS, Constants from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import ( @@ -34,6 +36,7 @@ console, convert_blocks_to_time, err_console, + json_console, print_error, print_verbose, get_all_wallets_for_path, @@ -44,9 +47,78 @@ millify_tao, unlock_key, WalletLike, + blocks_to_duration, + decode_account_id, ) +async def associate_hotkey( + wallet: Wallet, + subtensor: SubtensorInterface, + hotkey_ss58: str, + hotkey_display: str, + prompt: bool = False, +): + """Associates a hotkey with a wallet""" + + owner_ss58 = await subtensor.get_hotkey_owner(hotkey_ss58) + if owner_ss58: + if owner_ss58 == wallet.coldkeypub.ss58_address: + console.print( + f":white_heavy_check_mark: {hotkey_display.capitalize()} is already " + f"associated with \nwallet [blue]{wallet.name}[/blue], " + f"SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]" + ) + return True + else: + owner_wallet = _get_wallet_by_ss58(wallet.path, owner_ss58) + wallet_name = owner_wallet.name if owner_wallet else "unknown wallet" + console.print( + f"[yellow]Warning[/yellow]: {hotkey_display.capitalize()} is already associated with \n" + f"wallet: [blue]{wallet_name}[/blue], SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]" + ) + return False + else: + console.print( + f"{hotkey_display.capitalize()} is not associated with any wallet" + ) + + if prompt and not Confirm.ask("Do you want to continue with the association?"): + return False + + if not unlock_key(wallet).success: + return False + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="try_associate_hotkey", + call_params={ + "hotkey": hotkey_ss58, + }, + ) + + with console.status(":satellite: Associating hotkey on-chain..."): + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + + if not success: + console.print( + f"[red]:cross_mark: Failed to associate hotkey: {err_msg}[/red]" + ) + return False + + console.print( + f":white_heavy_check_mark: Successfully associated {hotkey_display} with \n" + f"wallet [blue]{wallet.name}[/blue], " + f"SS58: [{COLORS.GENERAL.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.GENERAL.CK}]" + ) + return True + + async def regen_coldkey( wallet: Wallet, mnemonic: Optional[str], @@ -55,6 +127,7 @@ async def regen_coldkey( json_password: Optional[str] = "", use_password: Optional[bool] = True, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new coldkey under this wallet""" json_str: Optional[str] = None @@ -71,16 +144,41 @@ async def regen_coldkey( use_password=use_password, overwrite=overwrite, ) - if isinstance(new_wallet, Wallet): console.print( "\n✅ [dark_sea_green]Regenerated coldkey successfully!\n", - f"[dark_sea_green]Wallet name: ({new_wallet.name}), path: ({new_wallet.path}), coldkey ss58: ({new_wallet.coldkeypub.ss58_address})", + f"[dark_sea_green]Wallet name: ({new_wallet.name}), " + f"path: ({new_wallet.path}), " + f"coldkey ss58: ({new_wallet.coldkeypub.ss58_address})", ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": new_wallet.name, + "path": new_wallet.path, + "hotkey": new_wallet.hotkey_str, + "hotkey_ss58": new_wallet.hotkey.ss58_address, + "coldkey_ss58": new_wallet.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) except ValueError: print_error("Mnemonic phrase is invalid") + if json_output: + json_console.print( + '{"success": false, "error": "Mnemonic phrase is invalid", "data": null}' + ) except KeyFileError: print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + '{"success": false, "error": "Keyfile is not writable", "data": null}' + ) async def regen_coldkey_pub( @@ -88,6 +186,7 @@ async def regen_coldkey_pub( ss58_address: str, public_key_hex: str, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new coldkeypub under this wallet.""" try: @@ -99,10 +198,31 @@ async def regen_coldkey_pub( if isinstance(new_coldkeypub, Wallet): console.print( "\n✅ [dark_sea_green]Regenerated coldkeypub successfully!\n", - f"[dark_sea_green]Wallet name: ({new_coldkeypub.name}), path: ({new_coldkeypub.path}), coldkey ss58: ({new_coldkeypub.coldkeypub.ss58_address})", + f"[dark_sea_green]Wallet name: ({new_coldkeypub.name}), path: ({new_coldkeypub.path}), " + f"coldkey ss58: ({new_coldkeypub.coldkeypub.ss58_address})", ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": new_coldkeypub.name, + "path": new_coldkeypub.path, + "hotkey": new_coldkeypub.hotkey_str, + "hotkey_ss58": new_coldkeypub.hotkey.ss58_address, + "coldkey_ss58": new_coldkeypub.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) except KeyFileError: print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + '{"success": false, "error": "Keyfile is not writable", "data": null}' + ) async def regen_hotkey( @@ -113,6 +233,7 @@ async def regen_hotkey( json_password: Optional[str] = "", use_password: Optional[bool] = False, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new hotkey under this wallet.""" json_str: Optional[str] = None @@ -134,13 +255,37 @@ async def regen_hotkey( if isinstance(new_hotkey_, Wallet): console.print( "\n✅ [dark_sea_green]Regenerated hotkey successfully!\n", - f"[dark_sea_green]Wallet name: " - f"({new_hotkey_.name}), path: ({new_hotkey_.path}), hotkey ss58: ({new_hotkey_.hotkey.ss58_address})", + f"[dark_sea_green]Wallet name: ({new_hotkey_.name}), path: ({new_hotkey_.path}), " + f"hotkey ss58: ({new_hotkey_.hotkey.ss58_address})", ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": new_hotkey_.name, + "path": new_hotkey_.path, + "hotkey": new_hotkey_.hotkey_str, + "hotkey_ss58": new_hotkey_.hotkey.ss58_address, + "coldkey_ss58": new_hotkey_.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) except ValueError: print_error("Mnemonic phrase is invalid") + if json_output: + json_console.print( + '{"success": false, "error": "Mnemonic phrase is invalid", "data": null}' + ) except KeyFileError: print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + '{"success": false, "error": "Keyfile is not writable", "data": null}' + ) async def new_hotkey( @@ -149,6 +294,7 @@ async def new_hotkey( use_password: bool, uri: Optional[str] = None, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new hotkey under this wallet.""" try: @@ -157,6 +303,7 @@ async def new_hotkey( keypair = Keypair.create_from_uri(uri) except Exception as e: print_error(f"Failed to create keypair from URI {uri}: {str(e)}") + return wallet.set_hotkey(keypair=keypair, encrypt=use_password) console.print( f"[dark_sea_green]Hotkey created from URI: {uri}[/dark_sea_green]" @@ -168,8 +315,28 @@ async def new_hotkey( overwrite=overwrite, ) console.print("[dark_sea_green]Hotkey created[/dark_sea_green]") + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": wallet.name, + "path": wallet.path, + "hotkey": wallet.hotkey_str, + "hotkey_ss58": wallet.hotkey.ss58_address, + "coldkey_ss58": wallet.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) except KeyFileError: print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + '{"success": false, "error": "Keyfile is not writable", "data": null}' + ) async def new_coldkey( @@ -178,6 +345,7 @@ async def new_coldkey( use_password: bool, uri: Optional[str] = None, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new coldkey under this wallet.""" try: @@ -198,8 +366,32 @@ async def new_coldkey( overwrite=overwrite, ) console.print("[dark_sea_green]Coldkey created[/dark_sea_green]") - except KeyFileError: + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": wallet.name, + "path": wallet.path, + "coldkey_ss58": wallet.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) + except KeyFileError as e: print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "error": f"Keyfile is not writable: {e}", + "data": None, + } + ) + ) async def wallet_create( @@ -208,16 +400,28 @@ async def wallet_create( use_password: bool = True, uri: Optional[str] = None, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new wallet.""" + output_dict = {"success": False, "error": "", "data": None} if uri: try: keypair = Keypair.create_from_uri(uri) wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=False) + output_dict["success"] = True + output_dict["data"] = { + "name": wallet.name, + "path": wallet.path, + "hotkey": wallet.hotkey_str, + "hotkey_ss58": wallet.hotkey.ss58_address, + "coldkey_ss58": wallet.coldkeypub.ss58_address, + } except Exception as e: - print_error(f"Failed to create keypair from URI: {str(e)}") + err = f"Failed to create keypair from URI: {str(e)}" + print_error(err) + output_dict["error"] = err console.print( f"[dark_sea_green]Wallet created from URI: {uri}[/dark_sea_green]" ) @@ -229,9 +433,18 @@ async def wallet_create( overwrite=overwrite, ) console.print("[dark_sea_green]Coldkey created[/dark_sea_green]") + output_dict["success"] = True + output_dict["data"] = { + "name": wallet.name, + "path": wallet.path, + "hotkey": wallet.hotkey_str, + "hotkey_ss58": wallet.hotkey.ss58_address, + "coldkey_ss58": wallet.coldkeypub.ss58_address, + } except KeyFileError: - print_error("KeyFileError: File is not writable") - + err = "KeyFileError: File is not writable" + print_error(err) + output_dict["error"] = err try: wallet.create_new_hotkey( n_words=n_words, @@ -239,8 +452,20 @@ async def wallet_create( overwrite=overwrite, ) console.print("[dark_sea_green]Hotkey created[/dark_sea_green]") + output_dict["success"] = True + output_dict["data"] = { + "name": wallet.name, + "path": wallet.path, + "hotkey": wallet.hotkey_str, + "hotkey_ss58": wallet.hotkey.ss58_address, + "coldkey_ss58": wallet.coldkeypub.ss58_address, + } except KeyFileError: - print_error("KeyFileError: File is not writable") + err = "KeyFileError: File is not writable" + print_error(err) + output_dict["error"] = err + if json_output: + json_console.print(json.dumps(output_dict)) def get_coldkey_wallets_for_path(path: str) -> list[Wallet]: @@ -254,6 +479,15 @@ def get_coldkey_wallets_for_path(path: str) -> list[Wallet]: return wallets +def _get_wallet_by_ss58(path: str, ss58_address: str) -> Optional[Wallet]: + """Find a wallet by its SS58 address in the given path.""" + ss58_addresses, wallet_names = _get_coldkey_ss58_addresses_for_path(path) + for wallet_name, addr in zip(wallet_names, ss58_addresses): + if addr == ss58_address: + return Wallet(path=path, name=wallet_name) + return None + + def _get_coldkey_ss58_addresses_for_path(path: str) -> tuple[list[str], list[str]]: """Get all coldkey ss58 addresses from path.""" @@ -280,6 +514,7 @@ async def wallet_balance( subtensor: SubtensorInterface, all_balances: bool, ss58_addresses: Optional[str] = None, + json_output: bool = False, ): """Retrieves the current balance of the specified wallet""" if ss58_addresses: @@ -395,6 +630,31 @@ async def wallet_balance( ) console.print(Padding(table, (0, 0, 0, 4))) await subtensor.substrate.close() + if json_output: + output_balances = { + key: { + "coldkey": value[0], + "free": value[1].tao, + "staked": value[2].tao, + "staked_with_slippage": value[3].tao, + "total": (value[1] + value[2]).tao, + "total_with_slippage": (value[1] + value[3]).tao, + } + for (key, value) in balances.items() + } + output_dict = { + "balances": output_balances, + "totals": { + "free": total_free_balance.tao, + "staked": total_staked_balance.tao, + "staked_with_slippage": total_staked_with_slippage.tao, + "total": (total_free_balance + total_staked_balance).tao, + "total_with_slippage": ( + total_free_balance + total_staked_with_slippage + ).tao, + }, + } + json_console.print(json.dumps(output_dict)) return total_free_balance @@ -526,7 +786,7 @@ async def wallet_history(wallet: Wallet): console.print(table) -async def wallet_list(wallet_path: str): +async def wallet_list(wallet_path: str, json_output: bool): """Lists wallets.""" wallets = utils.get_coldkey_wallets_for_path(wallet_path) print_verbose(f"Using wallets path: {wallet_path}") @@ -534,6 +794,7 @@ async def wallet_list(wallet_path: str): err_console.print(f"[red]No wallets found in dir: {wallet_path}[/red]") root = Tree("Wallets") + main_data_dict = {"wallets": []} for wallet in wallets: if ( wallet.coldkeypub_file.exists_on_device() @@ -546,23 +807,39 @@ async def wallet_list(wallet_path: str): wallet_tree = root.add( f"[bold blue]Coldkey[/bold blue] [green]{wallet.name}[/green] ss58_address [green]{coldkeypub_str}[/green]" ) + wallet_hotkeys = [] + wallet_dict = { + "name": wallet.name, + "ss58_address": coldkeypub_str, + "hotkeys": wallet_hotkeys, + } + main_data_dict["wallets"].append(wallet_dict) hotkeys = utils.get_hotkey_wallets_for_wallet( wallet, show_nulls=True, show_encrypted=True ) for hkey in hotkeys: data = f"[bold red]Hotkey[/bold red][green] {hkey}[/green] (?)" + hk_data = {"name": hkey.name, "ss58_address": "?"} if hkey: try: - data = f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] ss58_address [green]{hkey.hotkey.ss58_address}[/green]\n" + data = ( + f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] " + f"ss58_address [green]{hkey.hotkey.ss58_address}[/green]\n" + ) + hk_data["name"] = hkey.hotkey_str + hk_data["ss58_address"] = hkey.hotkey.ss58_address except UnicodeDecodeError: pass wallet_tree.add(data) + wallet_hotkeys.append(hk_data) if not wallets: print_verbose(f"No wallets found in path: {wallet_path}") root.add("[bold red]No wallets found.") - - console.print(root) + if json_output: + json_console.print(json.dumps(main_data_dict)) + else: + console.print(root) async def _get_total_balance( @@ -639,23 +916,33 @@ async def overview( exclude_hotkeys: Optional[list[str]] = None, netuids_filter: Optional[list[int]] = None, verbose: bool = False, + json_output: bool = False, ): """Prints an overview for the wallet's coldkey.""" total_balance = Balance(0) - # We are printing for every coldkey. - block_hash = await subtensor.substrate.get_chain_head() - all_hotkeys, total_balance = await _get_total_balance( - total_balance, subtensor, wallet, all_wallets, block_hash=block_hash - ) - _dynamic_info = await subtensor.all_subnets() - dynamic_info = {info.netuid: info for info in _dynamic_info} - with console.status( f":satellite: Synchronizing with chain [white]{subtensor.network}[/white]", spinner="aesthetic", ) as status: + # We are printing for every coldkey. + block_hash = await subtensor.substrate.get_chain_head() + ( + (all_hotkeys, total_balance), + _dynamic_info, + block, + all_netuids, + ) = await asyncio.gather( + _get_total_balance( + total_balance, subtensor, wallet, all_wallets, block_hash=block_hash + ), + subtensor.all_subnets(block_hash=block_hash), + subtensor.substrate.get_block_number(block_hash=block_hash), + subtensor.get_all_subnet_netuids(block_hash=block_hash), + ) + dynamic_info = {info.netuid: info for info in _dynamic_info} + # We are printing for a select number of hotkeys from all_hotkeys. if include_hotkeys or exclude_hotkeys: all_hotkeys = _get_hotkeys(include_hotkeys, exclude_hotkeys, all_hotkeys) @@ -667,10 +954,6 @@ async def overview( # Pull neuron info for all keys. neurons: dict[str, list[NeuronInfoLite]] = {} - block, all_netuids = await asyncio.gather( - subtensor.substrate.get_block_number(None), - subtensor.get_all_subnet_netuids(), - ) netuids = await subtensor.filter_netuids_by_registered_hotkeys( all_netuids, netuids_filter, all_hotkeys, reuse_block=True @@ -705,16 +988,27 @@ async def overview( neurons = _process_neuron_results(results, neurons, netuids) # Setup outer table. grid = Table.grid(pad_edge=True) + data_dict = { + "wallet": "", + "network": subtensor.network, + "subnets": [], + "total_balance": 0.0, + } # Add title if not all_wallets: title = "[underline dark_orange]Wallet[/underline dark_orange]\n" - details = f"[bright_cyan]{wallet.name}[/bright_cyan] : [bright_magenta]{wallet.coldkeypub.ss58_address}[/bright_magenta]" + details = ( + f"[bright_cyan]{wallet.name}[/bright_cyan] : " + f"[bright_magenta]{wallet.coldkeypub.ss58_address}[/bright_magenta]" + ) grid.add_row(Align(title, vertical="middle", align="center")) grid.add_row(Align(details, vertical="middle", align="center")) + data_dict["wallet"] = f"{wallet.name}|{wallet.coldkeypub.ss58_address}" else: title = "[underline dark_orange]All Wallets:[/underline dark_orange]" grid.add_row(Align(title, vertical="middle", align="center")) + data_dict["wallet"] = "All" grid.add_row( Align( @@ -732,6 +1026,14 @@ async def overview( ) for netuid, subnet_tempo in zip(netuids, tempos): table_data = [] + subnet_dict = { + "netuid": netuid, + "tempo": subnet_tempo, + "neurons": [], + "name": "", + "symbol": "", + } + data_dict["subnets"].append(subnet_dict) total_rank = 0.0 total_trust = 0.0 total_consensus = 0.0 @@ -786,6 +1088,26 @@ async def overview( ), nn.hotkey[:10], ] + neuron_dict = { + "coldkey": hotwallet.name, + "hotkey": hotwallet.hotkey_str, + "uid": uid, + "active": active, + "stake": stake, + "rank": rank, + "trust": trust, + "consensus": consensus, + "incentive": incentive, + "dividends": dividends, + "emission": emission, + "validator_trust": validator_trust, + "validator_permit": validator_permit, + "last_update": last_update, + "axon": int_to_ip(nn.axon_info.ip) + ":" + str(nn.axon_info.port) + if nn.axon_info.port != 0 + else None, + "hotkey_ss58": nn.hotkey, + } total_rank += rank total_trust += trust @@ -798,11 +1120,16 @@ async def overview( total_neurons += 1 table_data.append(row) + subnet_dict["neurons"].append(neuron_dict) # Add subnet header + sn_name = get_subnet_name(dynamic_info[netuid]) + sn_symbol = dynamic_info[netuid].symbol grid.add_row( - f"Subnet: [dark_orange]{netuid}: {get_subnet_name(dynamic_info[netuid])} {dynamic_info[netuid].symbol}[/dark_orange]" + f"Subnet: [dark_orange]{netuid}: {sn_name} {sn_symbol}[/dark_orange]" ) + subnet_dict["name"] = sn_name + subnet_dict["symbol"] = sn_symbol width = console.width table = Table( show_footer=False, @@ -937,6 +1264,7 @@ def overview_sort_function(row_): caption = "\n[italic][dim][bright_cyan]Wallet balance: [dark_orange]\u03c4" + str( total_balance.tao ) + data_dict["total_balance"] = total_balance.tao grid.add_row(Align(caption, vertical="middle", align="center")) if console.width < 150: @@ -944,7 +1272,10 @@ def overview_sort_function(row_): "[yellow]Warning: Your terminal width might be too small to view all information clearly" ) # Print the entire table/grid - console.print(grid, width=None) + if not json_output: + console.print(grid, width=None) + else: + json_console.print(json.dumps(data_dict)) def _get_hotkeys( @@ -1109,17 +1440,23 @@ async def transfer( destination: str, amount: float, transfer_all: bool, + era: int, prompt: bool, + json_output: bool, ): """Transfer token of amount to destination.""" - await transfer_extrinsic( + result = await transfer_extrinsic( subtensor=subtensor, wallet=wallet, destination=destination, amount=Balance.from_tao(amount), transfer_all=transfer_all, + era=era, prompt=prompt, ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result async def inspect( @@ -1128,6 +1465,7 @@ async def inspect( netuids_filter: list[int], all_wallets: bool = False, ): + # TODO add json_output when this is re-enabled and updated for dTAO def delegate_row_maker( delegates_: list[tuple[DelegateInfo, Balance]], ) -> Generator[list[str], None, None]: @@ -1303,14 +1641,18 @@ async def swap_hotkey( new_wallet: Wallet, subtensor: SubtensorInterface, prompt: bool, + json_output: bool, ): """Swap your hotkey for all registered axons on the network.""" - return await swap_hotkey_extrinsic( + result = await swap_hotkey_extrinsic( subtensor, original_wallet, new_wallet, prompt=prompt, ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def create_identity_table(title: str = None): @@ -1349,9 +1691,10 @@ async def set_id( additional: str, github_repo: str, prompt: bool, + json_output: bool = False, ): """Create a new or update existing identity on-chain.""" - + output_dict = {"success": False, "identity": None, "error": ""} identity_data = { "name": name.encode(), "url": web_url.encode(), @@ -1378,20 +1721,31 @@ async def set_id( if not success: err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}") + output_dict["error"] = err_msg + if json_output: + json_console.print(json.dumps(output_dict)) return - - console.print(":white_heavy_check_mark: [dark_sea_green3]Success!") - identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) + else: + console.print(":white_heavy_check_mark: [dark_sea_green3]Success!") + output_dict["success"] = True + identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) table = create_identity_table(title="New on-chain Identity") table.add_row("Address", wallet.coldkeypub.ss58_address) for key, value in identity.items(): table.add_row(key, str(value) if value else "~") - - return console.print(table) + output_dict["identity"] = identity + console.print(table) + if json_output: + json_console.print(json.dumps(output_dict)) -async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = None): +async def get_id( + subtensor: SubtensorInterface, + ss58_address: str, + title: str = None, + json_output: bool = False, +): with console.status( ":satellite: [bold green]Querying chain identity...", spinner="earth" ): @@ -1403,6 +1757,8 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = f" for [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" f" on {subtensor}" ) + if json_output: + json_console.print("{}") return {} table = create_identity_table(title) @@ -1411,6 +1767,8 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = table.add_row(key, str(value) if value else "~") console.print(table) + if json_output: + json_console.print(json.dumps(identity)) return identity @@ -1451,7 +1809,9 @@ async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): ) -async def sign(wallet: Wallet, message: str, use_hotkey: str): +async def sign( + wallet: Wallet, message: str, use_hotkey: str, json_output: bool = False +): """Sign a message using the provided wallet or hotkey.""" if not use_hotkey: @@ -1471,4 +1831,260 @@ async def sign(wallet: Wallet, message: str, use_hotkey: str): signed_message = keypair.sign(message.encode("utf-8")).hex() console.print("[dark_sea_green3]Message signed successfully:") + if json_output: + json_console.print(json.dumps({"signed_message": signed_message})) console.print(signed_message) + + +async def schedule_coldkey_swap( + wallet: Wallet, + subtensor: SubtensorInterface, + new_coldkey_ss58: str, + force_swap: bool = False, +) -> bool: + """Schedules a coldkey swap operation to be executed at a future block. + + Args: + wallet (Wallet): The wallet initiating the coldkey swap + subtensor (SubtensorInterface): Connection to the Bittensor network + new_coldkey_ss58 (str): SS58 address of the new coldkey + force_swap (bool, optional): Whether to force the swap even if the new coldkey is already scheduled for a swap. Defaults to False. + Returns: + bool: True if the swap was scheduled successfully, False otherwise + """ + if not is_valid_ss58_address(new_coldkey_ss58): + print_error(f"Invalid SS58 address format: {new_coldkey_ss58}") + return False + + scheduled_coldkey_swap = await subtensor.get_scheduled_coldkey_swap() + if wallet.coldkeypub.ss58_address in scheduled_coldkey_swap: + print_error( + f"Coldkey {wallet.coldkeypub.ss58_address} is already scheduled for a swap." + ) + console.print("[dim]Use the force_swap (--force) flag to override this.[/dim]") + if not force_swap: + return False + else: + console.print( + "[yellow]Continuing with the swap due to force_swap flag.[/yellow]\n" + ) + + prompt = ( + "You are [red]swapping[/red] your [blue]coldkey[/blue] to a new address.\n" + f"Current ss58: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]\n" + f"New ss58: [{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]\n" + "Are you sure you want to continue?" + ) + if not Confirm.ask(prompt): + return False + + if not unlock_key(wallet).success: + return False + + block_pre_call, call = await asyncio.gather( + subtensor.substrate.get_block_number(), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="schedule_swap_coldkey", + call_params={ + "new_coldkey": new_coldkey_ss58, + }, + ), + ) + + with console.status(":satellite: Scheduling coldkey swap on-chain..."): + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + block_post_call = await subtensor.substrate.get_block_number() + + if not success: + print_error(f"Failed to schedule coldkey swap: {err_msg}") + return False + + console.print( + ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" + ) + + swap_info = await find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=block_pre_call, + end_block=block_post_call, + wallet_ss58=wallet.coldkeypub.ss58_address, + ) + + if not swap_info: + console.print( + "[yellow]Warning: Could not find the swap extrinsic in recent blocks" + ) + return True + + console.print( + "\n[green]Coldkey swap details:[/green]" + f"\nBlock number: {swap_info['block_num']}" + f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" + f"\nDestination address: [{COLORS.G.CK}]{swap_info['dest_coldkey']}[/{COLORS.G.CK}]" + f"\nThe swap will be completed at block: [green]{swap_info['execution_block']}[/green]" + f"\n[dim]You can provide the block number to `btcli wallet swap-check`[/dim]" + ) + + +async def find_coldkey_swap_extrinsic( + subtensor: SubtensorInterface, + start_block: int, + end_block: int, + wallet_ss58: str, +) -> dict: + """Search for a coldkey swap event in a range of blocks. + + Args: + subtensor: SubtensorInterface for chain queries + start_block: Starting block number to search + end_block: Ending block number to search (inclusive) + wallet_ss58: SS58 address of the signing wallet + + Returns: + dict: Contains the following keys if found: + - block_num: Block number where swap was scheduled + - dest_coldkey: SS58 address of destination coldkey + - execution_block: Block number when swap will execute + Empty dict if not found + """ + + current_block, genesis_block = await asyncio.gather( + subtensor.substrate.get_block_number(), subtensor.substrate.get_block_hash(0) + ) + if ( + current_block - start_block > 300 + and genesis_block == Constants.genesis_block_hash_map["finney"] + ): + console.print("Querying archive node for coldkey swap events...") + await subtensor.substrate.close() + subtensor = SubtensorInterface("archive") + + block_hashes = await asyncio.gather( + *[ + subtensor.substrate.get_block_hash(block_num) + for block_num in range(start_block, end_block + 1) + ] + ) + block_events = await asyncio.gather( + *[ + subtensor.substrate.get_events(block_hash=block_hash) + for block_hash in block_hashes + ] + ) + + for block_num, events in zip(range(start_block, end_block + 1), block_events): + for event in events: + if ( + event.get("event", {}).get("module_id") == "SubtensorModule" + and event.get("event", {}).get("event_id") == "ColdkeySwapScheduled" + ): + attributes = event["event"].get("attributes", {}) + old_coldkey = decode_account_id(attributes["old_coldkey"][0]) + + if old_coldkey == wallet_ss58: + return { + "block_num": block_num, + "dest_coldkey": decode_account_id(attributes["new_coldkey"][0]), + "execution_block": attributes["execution_block"], + } + + return {} + + +async def check_swap_status( + subtensor: SubtensorInterface, + origin_ss58: Optional[str] = None, + expected_block_number: Optional[int] = None, +) -> None: + """ + Check the status of a coldkey swap. + + Args: + subtensor: Connection to the network + origin_ss58: The SS58 address of the original coldkey + block_number: Optional block number where the swap was scheduled + """ + scheduled_swaps = await subtensor.get_scheduled_coldkey_swap() + + if not origin_ss58: + if not scheduled_swaps: + console.print("[yellow]No pending coldkey swaps found.[/yellow]") + return + + table = Table( + Column( + "Original Coldkey", + justify="Left", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], + no_wrap=True, + ), + Column("Status", style="dark_sea_green3"), + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Pending Coldkey Swaps\n", + show_header=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + + for coldkey in scheduled_swaps: + table.add_row(coldkey, "Pending") + + console.print(table) + console.print( + "\n[dim]Tip: Check specific swap details by providing the original coldkey SS58 address and the block number.[/dim]" + ) + return + + is_pending = origin_ss58 in scheduled_swaps + + if not is_pending: + console.print( + f"[red]No pending swap found for coldkey:[/red] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + ) + return + + console.print( + f"[green]Found pending swap for coldkey:[/green] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + ) + + if expected_block_number is None: + return + + swap_info = await find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=expected_block_number, + end_block=expected_block_number, + wallet_ss58=origin_ss58, + ) + + if not swap_info: + console.print( + f"[yellow]Warning: Could not find swap extrinsic at block {expected_block_number}[/yellow]" + ) + return + + current_block = await subtensor.substrate.get_block_number() + remaining_blocks = swap_info["execution_block"] - current_block + + if remaining_blocks <= 0: + console.print("[green]Swap period has completed![/green]") + return + + console.print( + "\n[green]Coldkey swap details:[/green]" + f"\nScheduled at block: {swap_info['block_num']}" + f"\nOriginal address: [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + f"\nDestination address: [{COLORS.G.CK}]{swap_info['dest_coldkey']}[/{COLORS.G.CK}]" + f"\nCompletion block: {swap_info['execution_block']}" + f"\nTime remaining: {blocks_to_duration(remaining_blocks)}" + ) diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 29337be4..68ec0308 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -1,4 +1,5 @@ import asyncio +import json import os from datetime import datetime, timedelta from typing import TYPE_CHECKING @@ -9,7 +10,12 @@ from rich.prompt import Confirm from async_substrate_interface.errors import SubstrateRequestException -from bittensor_cli.src.bittensor.utils import err_console, console, format_error_message +from bittensor_cli.src.bittensor.utils import ( + err_console, + console, + format_error_message, + json_console, +) from bittensor_cli.src.bittensor.extrinsics.root import ( convert_weights_and_uids_for_emit, generate_weight_hash, @@ -260,7 +266,6 @@ async def _do_set_weights(): if not self.wait_for_finalization and not self.wait_for_inclusion: return True, "Not waiting for finalization or inclusion." - await response.process_events() if await response.is_success: return True, "Successfully set weights." else: @@ -316,7 +321,6 @@ async def reveal_weights_extrinsic( success, error_message = True, "" else: - await response.process_events() if await response.is_success: success, error_message = True, "" else: @@ -354,7 +358,6 @@ async def do_commit_weights(self, commit_hash): if not self.wait_for_finalization and not self.wait_for_inclusion: return True, None - await response.process_events() if await response.is_success: return True, None else: @@ -372,6 +375,7 @@ async def reveal_weights( weights: list[float], salt: list[int], version: int, + json_output: bool = False, prompt: bool = True, ) -> None: """Reveal weights for a specific subnet.""" @@ -395,11 +399,13 @@ async def reveal_weights( subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt ) success, message = await extrinsic.reveal(weight_uids, weight_vals) - - if success: - console.print("Weights revealed successfully") + if json_output: + json_console.print(json.dumps({"success": success, "message": message})) else: - err_console.print(f"Failed to reveal weights: {message}") + if success: + console.print("Weights revealed successfully") + else: + err_console.print(f"Failed to reveal weights: {message}") async def commit_weights( @@ -410,6 +416,7 @@ async def commit_weights( weights: list[float], salt: list[int], version: int, + json_output: bool = False, prompt: bool = True, ): """Commits weights and then reveals them for a specific subnet""" @@ -429,7 +436,10 @@ async def commit_weights( subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt ) success, message = await extrinsic.set_weights_extrinsic() - if success: - console.print("Weights set successfully") + if json_output: + json_console.print(json.dumps({"success": success, "message": message})) else: - err_console.print(f"Failed to commit weights: {message}") + if success: + console.print("Weights set successfully") + else: + err_console.print(f"Failed to commit weights: {message}") diff --git a/bittensor_cli/version.py b/bittensor_cli/version.py index 528f624b..d66077f5 100644 --- a/bittensor_cli/version.py +++ b/bittensor_cli/version.py @@ -1,3 +1,4 @@ +import importlib.metadata import re @@ -16,5 +17,5 @@ def version_as_int(version): return __version_as_int__ -__version__ = "9.1.4" +__version__ = importlib.metadata.version("bittensor-cli") __version_as_int__ = version_as_int(__version__) diff --git a/pyproject.toml b/pyproject.toml index 2652c869..84080770 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["setuptools~=70.0.0", "wheel"] +requires = ["setuptools>=70.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.2.0" +version = "9.3.0" description = "Bittensor CLI" readme = "README.md" authors = [ @@ -16,23 +16,22 @@ requires-python = ">=3.9,<3.14" dependencies = [ "wheel", "async-property==0.2.2", - "async-substrate-interface>=1.0.8", + "async-substrate-interface>=1.1.0", "aiohttp~=3.10.2", "backoff~=2.2.1", "GitPython>=3.0.0", "fuzzywuzzy~=0.18.0", "netaddr~=1.3.0", - "numpy>=2.0.1", + "numpy>=2.0.1,<3.0.0", "Jinja2", "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0.1", "pytest", "python-Levenshtein", - "rich~=13.7", + "rich>=13.7,<15.0", "scalecodec==1.2.11", - "typer~=0.12", - "websockets>=14.1", - "bittensor-wallet>=3.0.4", + "typer>=0.12,<0.16", + "bittensor-wallet>=3.0.7", "plotille>=5.0.0", "pywry>=0.6.2", "plotly>=6.0.0", diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index e3625683..ba2c6c01 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -1,3 +1,4 @@ +import asyncio import logging import os import re @@ -13,6 +14,8 @@ from .utils import setup_wallet +LOCALNET_IMAGE_NAME = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" + def wait_for_node_start(process, pattern, timestamp: int = None): for line in process.stdout: @@ -112,6 +115,7 @@ def is_docker_running(): stderr=subprocess.DEVNULL, check=True, ) + subprocess.run(["docker", "pull", LOCALNET_IMAGE_NAME], check=True) return True except subprocess.CalledProcessError: return False @@ -142,7 +146,6 @@ def try_start_docker(): return False container_name = f"test_local_chain_{str(time.time()).replace('.', '_')}" - image_name = "ghcr.io/opentensor/subtensor-localnet:latest" # Command to start container cmds = [ @@ -155,7 +158,7 @@ def try_start_docker(): "9944:9944", "-p", "9945:9945", - image_name, + LOCALNET_IMAGE_NAME, params, ] @@ -184,16 +187,15 @@ def try_start_docker(): ) if not result.stdout.strip(): raise RuntimeError("Docker container failed to start.") - substrate = AsyncSubstrateInterface(url="ws://127.0.0.1:9944") yield substrate finally: try: if substrate: - substrate.close() + asyncio.run(substrate.close()) except Exception: - pass + logging.warning("Failed to close substrate connection.") try: subprocess.run(["docker", "kill", container_name]) diff --git a/tests/e2e_tests/test_children_hotkeys.py b/tests/e2e_tests/test_children_hotkeys.py new file mode 100644 index 00000000..f286012f --- /dev/null +++ b/tests/e2e_tests/test_children_hotkeys.py @@ -0,0 +1,15 @@ +import pytest + +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.commands.stake.children_hotkeys import ( + get_childkey_completion_block, +) + + +@pytest.mark.asyncio +async def test_get_childkey_completion_block(local_chain): + async with SubtensorInterface("ws://127.0.0.1:9945") as subtensor: + current_block, completion_block = await get_childkey_completion_block( + subtensor, 1 + ) + assert (completion_block - current_block) >= 7200 diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index dfd2cfa5..9a295557 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -8,6 +8,8 @@ """ import asyncio +import json + from .utils import call_add_proposal @@ -112,12 +114,26 @@ def test_senate(local_chain, wallet_setup): # 0 Ayes for the proposal assert proposals_output[2] == "0" - # 0 Nayes for the proposal + # 0 Nays for the proposal assert proposals_output[4] == "0" # Assert initial threshold is 3 assert proposals_output[1] == "3" + json_proposals = exec_command_bob( + command="sudo", + sub_command="proposals", + extra_args=["--chain", "ws://127.0.0.1:9945", "--json-output"], + ) + json_proposals_output = json.loads(json_proposals.stdout) + + assert len(json_proposals_output) == 1 + assert json_proposals_output[0]["threshold"] == 3 + assert json_proposals_output[0]["ayes"] == 0 + assert json_proposals_output[0]["nays"] == 0 + assert json_proposals_output[0]["votes"] == {} + assert json_proposals_output[0]["call_data"] == "System.remark(remark: (0,))" + # Vote on the proposal by Bob (vote aye) vote_aye = exec_command_bob( command="sudo", @@ -161,6 +177,26 @@ def test_senate(local_chain, wallet_setup): # Nay votes remain 0 assert proposals_after_aye_output[4] == "0" + proposals_after_aye_json = exec_command_bob( + command="sudo", + sub_command="proposals", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + proposals_after_aye_json_output = json.loads(proposals_after_aye_json.stdout) + assert len(proposals_after_aye_json_output) == 1 + assert proposals_after_aye_json_output[0]["threshold"] == 3 + assert proposals_after_aye_json_output[0]["ayes"] == 1 + assert proposals_after_aye_json_output[0]["nays"] == 0 + assert len(proposals_after_aye_json_output[0]["votes"]) == 1 + assert proposals_after_aye_json_output[0]["votes"][keypair_bob.ss58_address] is True + assert ( + proposals_after_aye_json_output[0]["call_data"] == "System.remark(remark: (0,))" + ) + # Register Alice to the root network (0) # Registering to root automatically makes you a senator if eligible root_register = exec_command_alice( @@ -222,11 +258,28 @@ def test_senate(local_chain, wallet_setup): assert proposals_after_nay_output[9].split()[4] == "1" # Assert Alice has voted Nay - assert proposals_after_nay_output[10].split()[0].strip( - ":" - ) == wallet_alice.hotkey.ss58_address + assert ( + proposals_after_nay_output[10].split()[0].strip(":") + == wallet_alice.hotkey.ss58_address + ) # Assert vote casted as Nay assert proposals_after_nay_output[10].split()[1] == "Nay" + proposals_after_nay_json = exec_command_bob( + command="sudo", + sub_command="proposals", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + proposals_after_nay_json_output = json.loads(proposals_after_nay_json.stdout) + assert len(proposals_after_nay_json_output) == 1 + assert proposals_after_nay_json_output[0]["nays"] == 1 + assert ( + proposals_after_nay_json_output[0]["votes"][keypair_alice.ss58_address] is False + ) + print("✅ Passed senate commands") diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index f8bfc6d0..b34b05b1 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -1,3 +1,4 @@ +import json import re from bittensor_cli.src.bittensor.balances import Balance @@ -5,7 +6,10 @@ """ Verify commands: +* btcli s burn-cost * btcli subnets create +* btcli subnets set-identity +* btcli subnets get-identity * btcli subnets register * btcli stake add * btcli stake remove @@ -39,6 +43,21 @@ def test_staking(local_chain, wallet_setup): wallet_path_alice ) + burn_cost = exec_command_alice( + "subnets", + "burn-cost", + extra_args=[ + "--network", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + burn_cost_output = json.loads(burn_cost.stdout) + expected_burn_cost = Balance.from_tao(1000.0) + assert burn_cost_output["error"] == "" + assert burn_cost_output["burn_cost"]["rao"] == expected_burn_cost.rao + assert burn_cost_output["burn_cost"]["tao"] == expected_burn_cost.tao + # Register a subnet with sudo as Alice result = exec_command_alice( command="subnets", @@ -52,7 +71,7 @@ def test_staking(local_chain, wallet_setup): wallet_alice.name, "--wallet-hotkey", wallet_alice.hotkey_str, - "--name", + "--subnet-name", "Test Subnet", "--repo", "https://github.com/username/repo", @@ -67,9 +86,12 @@ def test_staking(local_chain, wallet_setup): "--additional-info", "Created by Alice", "--no-prompt", + "--json-output", ], ) - assert f"✅ Registered subnetwork with netuid: {netuid}" in result.stdout + result_output = json.loads(result.stdout) + assert result_output["success"] is True + assert result_output["netuid"] == 2 # Register Alice in netuid = 1 using her hotkey register_subnet = exec_command_alice( @@ -91,6 +113,84 @@ def test_staking(local_chain, wallet_setup): ) assert "✅ Already Registered" in register_subnet.stdout + register_subnet_json = exec_command_alice( + command="subnets", + sub_command="register", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--json-output", + ], + ) + register_subnet_json_output = json.loads(register_subnet_json.stdout) + assert register_subnet_json_output["success"] is True + assert register_subnet_json_output["msg"] == "Already registered" + + # set identity + set_identity = exec_command_alice( + "subnets", + "set-identity", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--subnet-name", + sn_name := "Test Subnet", + "--github-repo", + sn_github := "https://github.com/username/repo", + "--subnet-contact", + sn_contact := "alice@opentensor.dev", + "--subnet-url", + sn_url := "https://testsubnet.com", + "--discord", + sn_discord := "alice#1234", + "--description", + sn_description := "A test subnet for e2e testing", + "--additional-info", + sn_add_info := "Created by Alice", + "--json-output", + "--no-prompt", + ], + ) + set_identity_output = json.loads(set_identity.stdout) + assert set_identity_output["success"] is True + + get_identity = exec_command_alice( + "subnets", + "get-identity", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--json-output", + ], + ) + get_identity_output = json.loads(get_identity.stdout) + assert get_identity_output["subnet_name"] == sn_name + assert get_identity_output["github_repo"] == sn_github + assert get_identity_output["subnet_contact"] == sn_contact + assert get_identity_output["subnet_url"] == sn_url + assert get_identity_output["discord"] == sn_discord + assert get_identity_output["description"] == sn_description + assert get_identity_output["additional"] == sn_add_info + # Add stake to Alice's hotkey add_stake = exec_command_alice( command="stake", @@ -112,9 +212,11 @@ def test_staking(local_chain, wallet_setup): "0.1", "--partial", "--no-prompt", + "--era", + "144", ], ) - assert "✅ Finalized" in add_stake.stdout + assert "✅ Finalized" in add_stake.stdout, add_stake.stderr # Execute stake show for Alice's wallet show_stake = exec_command_alice( @@ -127,8 +229,10 @@ def test_staking(local_chain, wallet_setup): wallet_alice.name, "--chain", "ws://127.0.0.1:9945", + "--verbose", ], ) + # Assert correct stake is added cleaned_stake = [ re.sub(r"\s+", " ", line) for line in show_stake.stdout.splitlines() @@ -136,7 +240,24 @@ def test_staking(local_chain, wallet_setup): stake_added = cleaned_stake[8].split("│")[3].strip().split()[0] assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(90) - # Execute remove_stake command and remove all 100 TAO from Alice + show_stake_json = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + show_stake_json_output = json.loads(show_stake_json.stdout) + alice_stake = show_stake_json_output["stake_info"][keypair_alice.ss58_address][0] + assert Balance.from_tao(alice_stake["stake_value"]) > Balance.from_tao(90.0) + + # Execute remove_stake command and remove all alpha stakes from Alice remove_stake = exec_command_alice( command="stake", sub_command="remove", @@ -152,11 +273,13 @@ def test_staking(local_chain, wallet_setup): "--chain", "ws://127.0.0.1:9945", "--amount", - "100", + str(float(stake_added) - 1), "--tolerance", "0.1", "--partial", "--no-prompt", + "--era", + "144", ], ) assert "✅ Finalized" in remove_stake.stdout @@ -175,10 +298,27 @@ def test_staking(local_chain, wallet_setup): # Parse all hyperparameters and single out max_burn in TAO all_hyperparams = hyperparams.stdout.splitlines() - max_burn_tao = all_hyperparams[22].split()[3] + max_burn_tao = all_hyperparams[22].split()[2].strip('\u200e') # Assert max_burn is 100 TAO from default - assert Balance.from_tao(float(max_burn_tao)) == Balance.from_tao(100) + assert Balance.from_tao(float(max_burn_tao)) == Balance.from_tao(100.0) + + hyperparams_json = exec_command_alice( + command="sudo", + sub_command="get", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--json-output", + ], + ) + hyperparams_json_output = json.loads(hyperparams_json.stdout) + max_burn_tao_from_json = next( + filter(lambda x: x["hyperparameter"] == "max_burn", hyperparams_json_output) + )["value"] + assert Balance.from_rao(max_burn_tao_from_json) == Balance.from_tao(100.0) # Change max_burn hyperparameter to 10 TAO change_hyperparams = exec_command_alice( @@ -219,8 +359,28 @@ def test_staking(local_chain, wallet_setup): # Parse updated hyperparameters all_updated_hyperparams = updated_hyperparams.stdout.splitlines() - updated_max_burn_tao = all_updated_hyperparams[22].split()[3] + updated_max_burn_tao = all_updated_hyperparams[22].split()[2].strip('\u200e') # Assert max_burn is now 10 TAO assert Balance.from_tao(float(updated_max_burn_tao)) == Balance.from_tao(10) + + updated_hyperparams_json = exec_command_alice( + command="sudo", + sub_command="get", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--json-output", + ], + ) + updated_hyperparams_json_output = json.loads(updated_hyperparams_json.stdout) + max_burn_tao_from_json = next( + filter( + lambda x: x["hyperparameter"] == "max_burn", updated_hyperparams_json_output + ) + )["value"] + assert Balance.from_rao(max_burn_tao_from_json) == Balance.from_tao(10.0) + print("✅ Passed staking and sudo commands") diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 66dda33c..c9f99796 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -1,3 +1,4 @@ +import json import re from bittensor_cli.src.bittensor.balances import Balance @@ -46,7 +47,7 @@ def test_unstaking(local_chain, wallet_setup): wallet_alice.name, "--wallet-hotkey", wallet_alice.hotkey_str, - "--name", + "--subnet-name", "Test Subnet 2", "--repo", "https://github.com/username/repo", @@ -78,7 +79,7 @@ def test_unstaking(local_chain, wallet_setup): wallet_alice.name, "--wallet-hotkey", wallet_alice.hotkey_str, - "--name", + "--subnet-name", "Test Subnet 3", "--repo", "https://github.com/username/repo", @@ -139,6 +140,8 @@ def test_unstaking(local_chain, wallet_setup): "--partial", "--tolerance", "0.5", + "--era", + "144", ], ) assert "✅ Finalized" in stake_result.stdout @@ -184,6 +187,8 @@ def test_unstaking(local_chain, wallet_setup): "--partial", "--tolerance", "0.5", + "--era", + "144", ], ) assert "✅ Finalized" in partial_unstake_netuid_2.stdout @@ -208,11 +213,30 @@ def test_unstaking(local_chain, wallet_setup): cleaned_stake = [ re.sub(r"\s+", " ", line) for line in stake_list.stdout.splitlines() ] - stake_after_unstaking_netuid_2 = cleaned_stake[9].split("│")[3].strip().split()[0] + stake_after_unstaking_netuid_2 = cleaned_stake[10].split("│")[3].strip().split()[0] assert Balance.from_tao(float(stake_after_unstaking_netuid_2)) <= Balance.from_tao( float(inital_stake_netuid_2) ) + show_stake_json = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + show_stake_json_output = json.loads(show_stake_json.stdout) + bob_stake = show_stake_json_output["stake_info"][keypair_bob.ss58_address] + assert Balance.from_tao( + next(filter(lambda x: x["netuid"] == 2, bob_stake))["stake_value"] + ) <= Balance.from_tao(float(inital_stake_netuid_2)) + # Remove all alpha stakes unstake_alpha = exec_command_bob( command="stake", @@ -222,13 +246,15 @@ def test_unstaking(local_chain, wallet_setup): wallet_path_bob, "--wallet-name", wallet_bob.name, - "--hotkey", + "--wallet-hotkey", wallet_bob.hotkey_str, "--chain", "ws://127.0.0.1:9945", "--all-alpha", "--no-prompt", "--verbose", + "--era", + "144", ], ) @@ -258,6 +284,8 @@ def test_unstaking(local_chain, wallet_setup): "--partial", "--tolerance", "0.5", + "--era", + "144", ], ) assert "✅ Finalized" in stake_result.stdout @@ -277,6 +305,8 @@ def test_unstaking(local_chain, wallet_setup): "ws://127.0.0.1:9945", "--all", "--no-prompt", + "--era", + "144", ], ) assert "✅ Finalized: Successfully unstaked all stakes from" in unstake_all.stdout diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index ea095998..5dd3bd63 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -1,8 +1,11 @@ +import json import os import re import time from typing import Dict, Optional, Tuple +from bittensor_wallet import Wallet + """ Verify commands: @@ -188,13 +191,22 @@ def test_wallet_creations(wallet_setup): ) # Assert default keys are present before proceeding - f"default ss58_address {wallet.coldkeypub.ss58_address}" in result.stdout - f"default ss58_address {wallet.hotkey.ss58_address}" in result.stdout + assert f"default ss58_address {wallet.coldkeypub.ss58_address}" in result.stdout + assert f"default ss58_address {wallet.hotkey.ss58_address}" in result.stdout wallet_status, message = verify_wallet_dir( wallet_path, "default", hotkey_name="default" ) assert wallet_status, message + json_result = exec_command( + command="wallet", + sub_command="list", + extra_args=["--wallet-path", wallet_path, "--json-output"], + ) + json_wallet = json.loads(json_result.stdout)["wallets"][0] + assert json_wallet["ss58_address"] == wallet.coldkey.ss58_address + assert json_wallet["hotkeys"][0]["ss58_address"] == wallet.hotkey.ss58_address + # ----------------------------- # Command 1: # ----------------------------- @@ -267,6 +279,27 @@ def test_wallet_creations(wallet_setup): wallet_status, message = verify_wallet_dir(wallet_path, "new_coldkey") assert wallet_status, message + json_creation = exec_command( + "wallet", + "new-coldkey", + extra_args=[ + "--wallet-name", + "new_json_coldkey", + "--wallet-path", + wallet_path, + "--n-words", + "12", + "--no-use-password", + "--json-output", + ], + ) + json_creation_output = json.loads(json_creation.stdout) + assert json_creation_output["success"] is True + assert json_creation_output["data"]["name"] == "new_json_coldkey" + assert "coldkey_ss58" in json_creation_output["data"] + assert json_creation_output["error"] == "" + new_json_coldkey_ss58 = json_creation_output["data"]["coldkey_ss58"] + # ----------------------------- # Command 3: # ----------------------------- @@ -303,6 +336,29 @@ def test_wallet_creations(wallet_setup): ) assert wallet_status, message + new_hotkey_json = exec_command( + "wallet", + sub_command="new-hotkey", + extra_args=[ + "--wallet-name", + "new_json_coldkey", + "--hotkey", + "new_json_hotkey", + "--wallet-path", + wallet_path, + "--n-words", + "12", + "--no-use-password", + "--json-output", + ], + ) + new_hotkey_json_output = json.loads(new_hotkey_json.stdout) + assert new_hotkey_json_output["success"] is True + assert new_hotkey_json_output["data"]["name"] == "new_json_coldkey" + assert new_hotkey_json_output["data"]["hotkey"] == "new_json_hotkey" + assert new_hotkey_json_output["data"]["coldkey_ss58"] == new_json_coldkey_ss58 + assert new_hotkey_json_output["error"] == "" + def test_wallet_regen(wallet_setup, capfd): """ @@ -358,6 +414,9 @@ def test_wallet_regen(wallet_setup, capfd): # ----------------------------- print("Testing wallet regen_coldkey command 🧪") coldkey_path = os.path.join(wallet_path, "new_wallet", "coldkey") + initial_coldkey_ss58 = Wallet( + name="new_wallet", path=wallet_path + ).coldkey.ss58_address initial_coldkey_mod_time = os.path.getmtime(coldkey_path) result = exec_command( @@ -385,7 +444,28 @@ def test_wallet_regen(wallet_setup, capfd): assert ( initial_coldkey_mod_time != new_coldkey_mod_time ), "Coldkey file was not regenerated as expected" - print("Passed wallet regen_coldkey command ✅") + json_result = exec_command( + command="wallet", + sub_command="regen-coldkey", + extra_args=[ + "--wallet-name", + "new_wallet", + "--hotkey", + "new_hotkey", + "--wallet-path", + wallet_path, + "--mnemonic", + mnemonics["coldkey"], + "--no-use-password", + "--overwrite", + "--json-output", + ], + ) + + json_result_out = json.loads(json_result.stdout) + assert json_result_out["success"] is True + assert json_result_out["data"]["name"] == "new_wallet" + assert json_result_out["data"]["coldkey_ss58"] == initial_coldkey_ss58 # ----------------------------- # Command 2: @@ -517,4 +597,15 @@ def test_wallet_balance_all(local_chain, wallet_setup, capfd): wallet_name in output ), f"Wallet {wallet_name} not found in balance --all output" + json_results = exec_command( + "wallet", + "balance", + extra_args=["--wallet-path", wallet_path, "--all", "--json-output"], + ) + json_results_output = json.loads(json_results.stdout) + for wallet_name in wallet_names: + assert wallet_name in json_results_output["balances"].keys() + assert json_results_output["balances"][wallet_name]["total"] == 0.0 + assert "coldkey" in json_results_output["balances"][wallet_name] + print("Passed wallet balance --all command with 100 wallets ✅") diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 076f03b3..08b2c73c 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -57,7 +57,7 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): wallet.name, "--wallet-hotkey", wallet.hotkey_str, - "--name", + "--subnet-name", "Test Subnet", "--repo", "https://github.com/username/repo", @@ -245,6 +245,8 @@ def test_wallet_transfer(local_chain, wallet_setup): "--amount", "100", "--no-prompt", + "--era", + "144", ], ) @@ -372,7 +374,7 @@ def test_wallet_identities(local_chain, wallet_setup): wallet_alice.name, "--wallet-hotkey", wallet_alice.hotkey_str, - "--name", + "--subnet-name", "Test Subnet", "--repo", "https://github.com/username/repo", @@ -510,3 +512,112 @@ def test_wallet_identities(local_chain, wallet_setup): assert "Message signed successfully" in sign_using_coldkey.stdout print("✅ Passed wallet set-id, get-id, sign command") + + +def test_wallet_associate_hotkey(local_chain, wallet_setup): + """ + Test the associating hotkeys and their different cases. + + Steps: + 1. Create wallets for Alice, Bob, and Charlie + 2. Associate a hotkey with Alice's wallet using hotkey name + 3. Verify the association is successful + 4. Try to associate Alice's hotkey with Bob's wallet (should fail) + 5. Try to associate Alice's hotkey again (should show already associated) + 6. Associate Charlie's hotkey with Bob's wallet using SS58 address + + Raises: + AssertionError: If any of the checks or verifications fail + """ + print("Testing wallet associate-hotkey command 🧪") + + _, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup("//Alice") + _, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup("//Bob") + _, wallet_charlie, _, _ = wallet_setup("//Charlie") + + # Associate Alice's default hotkey with her wallet + result = exec_command_alice( + command="wallet", + sub_command="associate-hotkey", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--no-prompt", + ], + ) + + # Assert successful association + assert "Successfully associated hotkey" in result.stdout + assert wallet_alice.hotkey.ss58_address in result.stdout + assert wallet_alice.coldkeypub.ss58_address in result.stdout + assert wallet_alice.hotkey_str in result.stdout + + # Try to associate Alice's hotkey with Bob's wallet (should fail) + result = exec_command_bob( + command="wallet", + sub_command="associate-hotkey", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_bob.name, + "--hotkey-ss58", + wallet_alice.hotkey.ss58_address, + "--no-prompt", + ], + ) + + assert "Warning" in result.stdout + assert "is already associated with" in result.stdout + + # Try to associate Alice's hotkey again with Alice's wallet + result = exec_command_alice( + command="wallet", + sub_command="associate-hotkey", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--no-prompt", + ], + ) + + assert "is already associated with" in result.stdout + assert "wallet" in result.stdout + assert wallet_alice.name in result.stdout + + # Associate Charlie's hotkey with Bob's wallet using SS58 address + result = exec_command_bob( + command="wallet", + sub_command="associate-hotkey", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_bob.name, + "--hotkey-ss58", + wallet_charlie.hotkey.ss58_address, + "--no-prompt", + ], + ) + + assert "Successfully associated hotkey" in result.stdout + assert wallet_charlie.hotkey.ss58_address in result.stdout + assert wallet_bob.coldkeypub.ss58_address in result.stdout + + print("✅ Passed wallet associate-hotkey command") diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 665a4fa2..490ae594 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -57,7 +57,7 @@ def exec_command( return keypair, wallet, wallet_path, exec_command -def extract_coldkey_balance(text: str, wallet_name: str, coldkey_address: str) -> dict: +def extract_coldkey_balance(cleaned_text: str, wallet_name: str, coldkey_address: str) -> dict: """ Extracts the free, staked, and total balances for a given wallet name and coldkey address from the input string. @@ -72,11 +72,12 @@ def extract_coldkey_balance(text: str, wallet_name: str, coldkey_address: str) - each containing the corresponding balance as a Balance object. Returns a dictionary with all zeros if the wallet name or coldkey address is not found. """ + cleaned_text = cleaned_text.replace('\u200e', '') pattern = ( - rf"{wallet_name}\s+{coldkey_address}\s+" r"τ\s*([\d,]+\.\d+)" # Free Balance + rf"{wallet_name}\s+{coldkey_address}\s+([\d,]+\.\d+)\s*τ" # Free Balance ) - match = re.search(pattern, text) + match = re.search(pattern, cleaned_text) if not match: return { @@ -297,5 +298,4 @@ async def call_add_proposal( wait_for_finalization=True, ) - await response.process_events() return await response.is_success