diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ed6e42021..f2ffcfbb2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2,6 +2,7 @@ import asyncio import copy import curses +import datetime import importlib import json import logging @@ -13,7 +14,7 @@ import warnings from dataclasses import fields from pathlib import Path -from typing import Coroutine, Optional, Union, Literal +from typing import Coroutine, Optional, Union import numpy as np import rich @@ -24,6 +25,9 @@ InvalidHandshake, ) from bittensor_wallet import Wallet +from bittensor_wallet.utils import ( + is_valid_ss58_address as btwallet_is_valid_ss58_address, +) from rich import box from rich.prompt import Confirm, FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table @@ -64,6 +68,9 @@ prompt_for_subnet_identity, validate_rate_tolerance, get_hotkey_pub_ss58, + ensure_address_book_tables_exist, + ProxyAddressBook, + ProxyAnnouncements, ) from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds @@ -80,6 +87,8 @@ prompt_liquidity, prompt_position_id, ) +from bittensor_cli.src.commands import proxy as proxy_commands +from bittensor_cli.src.commands.proxy import ProxyType from bittensor_cli.src.commands.stake import ( auto_staking as auto_stake, children_hotkeys, @@ -95,7 +104,6 @@ subnets, mechanisms as subnet_mechanisms, ) -from bittensor_cli.src.commands.wallets import SortByBalance from bittensor_cli.version import __version__, __version_as_int__ try: @@ -120,6 +128,27 @@ def arg__(arg_name: str) -> str: return f"[{COLORS.G.ARG}]{arg_name}[/{COLORS.G.ARG}]" +def is_valid_ss58_address_param(address: Optional[str]) -> Optional[str]: + """ + Evaluates whether a non-None address is a valid SS58 address. Used as a callback for + Annotated typer params. + + Args: + address: an SS58 address, or None + + Returns: + the SS58 address (if valid) or None (if None) + + Raises: + typer.BadParameter: if the address is not a valid SS58 address + """ + if address is None: + return None + elif not btwallet_is_valid_ss58_address(address): + raise typer.BadParameter(f"Invalid SS58 address: {address}") + return address + + class Options: """ Re-usable typer args @@ -364,6 +393,28 @@ def edit_help(cls, option_name: str, help_text: str): "--era", help="Length (in blocks) for which the transaction should be valid.", ) + proxy_type: ProxyType = typer.Option( + ProxyType.Any.value, + "--proxy-type", + help="Type of proxy", + prompt=True, + ) + proxy: Optional[str] = typer.Option( + None, + "--proxy", + help="Optional proxy to use for the transaction: either the SS58 or the name of the proxy if you " + f"have added it with {arg__('btcli config add-proxy')}.", + ) + real_proxy: Optional[str] = typer.Option( + None, + "--real", + help="The real account making this call. If omitted, the proxy's ss58 is used.", + ) + announce_only: bool = typer.Option( + False, + help=f"If set along with [{COLORS.G.ARG}]--proxy[/{COLORS.G.ARG}], will not actually make the extrinsic call, " + f"but rather just announce it to be made later.", + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -734,6 +785,7 @@ def __init__(self): # "COLDKEY": True, # }, } + self.proxies = {} self.subtensor = None if sys.version_info < (3, 10): @@ -753,6 +805,9 @@ def __init__(self): self.debug_file_path = os.getenv("BTCLI_DEBUG_FILE") or os.path.expanduser( defaults.config.debug_file_path ) + self.proxies_path = os.getenv("BTCLI_PROXIES_PATH") or os.path.expanduser( + defaults.proxies.path + ) self.app = typer.Typer( rich_markup_mode="rich", @@ -776,6 +831,7 @@ def __init__(self): self.liquidity_app = typer.Typer(epilog=_epilog) self.crowd_app = typer.Typer(epilog=_epilog) self.utils_app = typer.Typer(epilog=_epilog) + self.proxy_app = typer.Typer(epilog=_epilog) # config alias self.app.add_typer( @@ -874,10 +930,22 @@ def __init__(self): no_args_is_help=True, ) + # proxy app + self.app.add_typer( + self.proxy_app, + name="proxy", + short_help="Proxy commands", + no_args_is_help=True, + ) + # config commands self.config_app.command("set")(self.set_config) self.config_app.command("get")(self.get_config) self.config_app.command("clear")(self.del_config) + self.config_app.command("add-proxy")(self.config_add_proxy) + self.config_app.command("proxies")(self.config_get_proxies) + self.config_app.command("remove-proxy")(self.config_remove_proxy) + self.config_app.command("update-proxy")(self.config_update_proxy) # self.config_app.command("metagraph", hidden=True)(self.metagraph_config) # wallet commands @@ -1102,6 +1170,24 @@ def __init__(self): "dashboard", rich_help_panel=HELP_PANELS["VIEW"]["DASHBOARD"] )(self.view_dashboard) + # proxy commands + self.proxy_app.command("create", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])( + self.proxy_create + ) + self.proxy_app.command("add", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])( + self.proxy_add + ) + self.proxy_app.command("remove", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])( + self.proxy_remove + ) + self.proxy_app.command("kill", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])( + self.proxy_kill + ) + self.proxy_app.command( + "execute", + rich_help_panel=HELP_PANELS["PROXY"]["MGMT"], + )(self.proxy_execute_announced) + # Sub command aliases # Wallet self.wallet_app.command( @@ -1405,7 +1491,7 @@ def main_callback( # Load or create the config file if os.path.exists(self.config_path): with open(self.config_path, "r") as f: - config = safe_load(f) + config = safe_load(f) or {} else: directory_path = Path(self.config_base_path) directory_path.mkdir(exist_ok=True, parents=True) @@ -1456,9 +1542,27 @@ def main_callback( asi_logger.addHandler(handler) logger.addHandler(handler) + ensure_address_book_tables_exist() + # load proxies address book + with ProxyAddressBook.get_db() as (conn, cursor): + rows = ProxyAddressBook.read_rows(conn, cursor, include_header=False) + proxies = {} + for name, ss58_address, delay, spawner, proxy_type, _ in rows: + proxies[name] = { + "address": ss58_address, + "spawner": spawner, + "proxy_type": proxy_type, + "delay": delay, + } + self.proxies = proxies + def verbosity_handler( - self, quiet: bool, verbose: bool, json_output: bool = False + self, quiet: bool, verbose: bool, json_output: bool = False, prompt: bool = True ) -> None: + if json_output and prompt: + raise typer.BadParameter( + f"Cannot specify both '--json-output' and '--prompt'" + ) if quiet and verbose: err_console.print("Cannot specify both `--quiet` and `--verbose`") raise typer.Exit() @@ -1762,7 +1866,7 @@ def get_config(self): box=box.SIMPLE_HEAD, title=f"[{COLORS.G.HEADER}]BTCLI Config[/{COLORS.G.HEADER}]: {arg__(self.config_path)}", ) - + value: Optional[str] for key, value in self.config.items(): if key == "network": if value is None: @@ -1785,6 +1889,210 @@ def get_config(self): console.print(table) + def config_add_proxy( + self, + name: Annotated[ + str, + typer.Option( + help="Name of the proxy", prompt="Enter a name for this proxy" + ), + ], + address: Annotated[ + str, + typer.Option( + callback=is_valid_ss58_address_param, + help="The SS58 address of the pure proxy/delegatee", + prompt="Enter the SS58 address of the pure proxy/delegatee", + ), + ], + proxy_type: Annotated[ + ProxyType, + typer.Option( + help="The type of this proxy", + prompt="Enter the type of this proxy", + ), + ], + spawner: Annotated[ + str, + typer.Option( + "--spawner", + "--delegator", + callback=is_valid_ss58_address_param, + help="The SS58 address of the spawner (pure proxy)/delegator (regular proxy)", + prompt="Enter the SS58 address of the spawner (pure proxy)/delegator (regular proxy)", + ), + ], + delay: int = typer.Option(0, help="Delay, in blocks."), + note: str = typer.Option("", help="Any notes about this entry"), + ): + """ + Adds a new pure proxy to the address book. + """ + if self.proxies.get(name) is not None: + err_console.print( + f"Proxy {name} already exists. Use `btcli config update-proxy` to update it." + ) + raise typer.Exit() + proxy_type_val: str + if isinstance(proxy_type, ProxyType): + proxy_type_val = proxy_type.value + else: + proxy_type_val = proxy_type + + self.proxies[name] = { + "proxy_type": proxy_type_val, + "address": address, + "spawner": spawner, + "delay": delay, + "note": note, + } + with ProxyAddressBook.get_db() as (conn, cursor): + ProxyAddressBook.add_entry( + conn, + cursor, + name=name, + ss58_address=address, + spawner=spawner, + proxy_type=proxy_type_val, + delay=delay, + note=note, + ) + self.config_get_proxies() + + def config_remove_proxy( + self, + name: Annotated[ + str, + typer.Option( + help="Name of the proxy to be removed", + prompt="Enter the name of the proxy to be removed", + ), + ], + ): + """ + Removes a pure proxy from the address book. + + Note: Does not remove the proxy on chain. Only removes it from the address book. + """ + if name in self.proxies: + del self.proxies[name] + with ProxyAddressBook.get_db() as (conn, cursor): + ProxyAddressBook.delete_entry(conn, cursor, name=name) + console.print(f"Removed {name} from the address book.") + else: + err_console.print(f"Proxy {name} not found in address book.") + self.config_get_proxies() + + def config_get_proxies(self): + """ + Displays the current proxies address book + """ + table = Table( + Column("[bold white]Name", style=f"{COLORS.G.ARG}"), + Column("Address", style="gold1"), + Column("Spawner/Delegator", style="medium_purple"), + Column("Proxy Type", style="medium_purple"), + Column("Delay", style="dim"), + Column("Note", style="dim"), + box=box.SIMPLE_HEAD, + title=f"[{COLORS.G.HEADER}]BTCLI Proxies Address Book[/{COLORS.G.HEADER}]: {arg__(self.proxies_path)}", + ) + with ProxyAddressBook.get_db() as (conn, cursor): + rows = ProxyAddressBook.read_rows(conn, cursor, include_header=False) + for name, ss58_address, delay, spawner, proxy_type, note in rows: + table.add_row(name, ss58_address, spawner, proxy_type, str(delay), note) + console.print(table) + + def config_update_proxy( + self, + name: Annotated[ + str, + typer.Option( + help="Name of the proxy", prompt="Enter a name for this proxy" + ), + ], + address: Annotated[ + Optional[str], + typer.Option( + callback=is_valid_ss58_address_param, + help="The SS58 address of the pure proxy", + ), + ] = None, + proxy_type: Annotated[ + Optional[ProxyType], + typer.Option( + help="The type of this pure proxy", + ), + ] = None, + spawner: Annotated[ + str, + typer.Option( + callback=is_valid_ss58_address_param, + help="The SS58 address of the spawner", + ), + ] = None, + delay: Optional[int] = typer.Option(None, help="Delay, in blocks."), + note: Optional[str] = typer.Option(None, help="Any notes about this entry"), + ): + if name not in self.proxies: + err_console.print(f"Proxy {name} not found in address book.") + return + else: + if isinstance(proxy_type, ProxyType): + proxy_type_val = proxy_type.value + else: + proxy_type_val = proxy_type + with ProxyAddressBook.get_db() as (conn, cursor): + ProxyAddressBook.update_entry( + conn, + cursor, + name=name, + ss58_address=address, + proxy_type=proxy_type_val, + spawner=spawner, + note=note, + delay=delay, + ) + console.print("Proxy updated") + self.config_get_proxies() + + def is_valid_proxy_name_or_ss58( + self, address: Optional[str], announce_only: bool + ) -> Optional[str]: + """ + Evaluates whether a non-None address is a valid SS58 address. Used as a callback for + Annotated typer params. + + Args: + address: an SS58 address, proxy name in config, or None + announce_only: whether the call should be made as just an announcement or the actual call made + + Returns: + the SS58 address (if valid) or None (if None) + + Raises: + typer.BadParameter: if the address is not a valid SS58 address, or if `--announce-only` is supplied but + without a proxy. + """ + if address is None: + if announce_only is True: + raise typer.BadParameter( + f"Cannot supply '--announce-only' without supplying '--proxy'" + ) + return None + outer_proxy_from_config = self.proxies.get(address, {}) + proxy_from_config = outer_proxy_from_config.get("address") + if proxy_from_config is not None: + if not btwallet_is_valid_ss58_address(proxy_from_config): + raise typer.BadParameter( + f"Invalid SS58 address: {proxy_from_config} from config {address}" + ) + else: + return proxy_from_config + elif not btwallet_is_valid_ss58_address(address): + raise typer.BadParameter(f"Invalid SS58 address: {address}") + return address + def ask_rate_tolerance( self, rate_tolerance: Optional[float], @@ -1902,8 +2210,8 @@ def ask_partial_stake( logger.debug(f"Partial staking {partial_staking}") return False + @staticmethod def ask_subnet_mechanism( - self, mechanism_id: Optional[int], mechanism_count: int, netuid: int, @@ -2050,6 +2358,8 @@ def wallet_ask( else: return wallet + # Wallet + def wallet_list( self, wallet_name: Optional[str] = Options.wallet_name, @@ -2072,7 +2382,7 @@ 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, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = self.wallet_ask( None, wallet_path, None, ask_for=[WO.PATH], validate=WV.NONE ) @@ -2139,7 +2449,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, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) 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." @@ -2223,6 +2533,8 @@ def wallet_transfer( help="Transfer balance even if the resulting balance falls below the existential deposit.", ), period: int = Options.period, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -2254,7 +2566,8 @@ def wallet_transfer( print_error("You have entered an incorrect ss58 address. Please try again.") raise typer.Exit() - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2290,6 +2603,8 @@ def wallet_transfer( era=period, prompt=prompt, json_output=json_output, + proxy=proxy, + announce_only=announce_only, ) ) @@ -2308,6 +2623,8 @@ def wallet_swap_hotkey( verbose: bool = Options.verbose, prompt: bool = Options.prompt, json_output: bool = Options.json_output, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, ): """ 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. @@ -2334,7 +2651,8 @@ def wallet_swap_hotkey( [green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey --netuid 1 """ netuid = get_optional_netuid(netuid, all_netuids) - self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + self.verbosity_handler(quiet, verbose, json_output, prompt) # Warning for netuid 0 - only swaps on root network, not a full swap if netuid == 0 and prompt: @@ -2387,7 +2705,13 @@ def wallet_swap_hotkey( self.initialize_chain(network) return self._run_command( wallets.swap_hotkey( - original_wallet, new_wallet, self.subtensor, netuid, prompt, json_output + original_wallet=original_wallet, + new_wallet=new_wallet, + subtensor=self.subtensor, + netuid=netuid, + proxy=proxy, + prompt=prompt, + json_output=json_output, ) ) @@ -2442,7 +2766,7 @@ def wallet_inspect( """ print_error("This command is disabled on the 'rao' network.") raise typer.Exit() - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if netuids: netuids = parse_to_list( @@ -2605,7 +2929,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, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_path: wallet_path = Prompt.ask( @@ -2666,7 +2990,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, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_path: wallet_path = Prompt.ask( @@ -2737,7 +3061,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, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2789,7 +3113,7 @@ def wallet_regen_hotkey_pub( [bold]Note[/bold]: This command is particularly useful for users who need to regenerate their hotkeypub, perhaps due to file corruption or loss. You will need either ss58 address or public hex key from your old hotkeypub.txt for the wallet. It is a recovery-focused utility that ensures continued access to your wallet functionalities. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_path: wallet_path = Prompt.ask( @@ -2860,7 +3184,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, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_name: wallet_name = Prompt.ask( @@ -2896,6 +3220,8 @@ def wallet_associate_hotkey( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, network: Optional[list[str]] = Options.network, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -2912,7 +3238,8 @@ def wallet_associate_hotkey( [green]$[/green] btcli wallet associate-hotkey --hotkey-name hotkey_name [green]$[/green] btcli wallet associate-hotkey --hotkey-ss58 5DkQ4... """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not wallet_name: wallet_name = Prompt.ask( "Enter the [blue]wallet name[/blue] [dim](which you want to associate with the hotkey)[/dim]", @@ -2964,6 +3291,7 @@ def wallet_associate_hotkey( hotkey_ss58, hotkey_display, prompt, + proxy=proxy, ) ) @@ -2998,7 +3326,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, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_path: wallet_path = Prompt.ask( @@ -3072,7 +3400,7 @@ def wallet_check_ck_swap( [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) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) self.initialize_chain(network) if show_all: @@ -3150,7 +3478,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) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_path: wallet_path = Prompt.ask( "Enter the path of wallets directory", @@ -3229,7 +3557,7 @@ def wallet_balance( [green]$[/green] btcli w balance --ss58 --ss58 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = None if all_balances: ask_for = [WO.PATH] @@ -3335,7 +3663,7 @@ def wallet_history( print_error("This command is disabled on the 'rao' network.") raise typer.Exit() - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, False, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -3388,6 +3716,8 @@ def wallet_set_id( "--github", help="The GitHub repository for the identity.", ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -3410,7 +3740,8 @@ 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, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -3450,17 +3781,17 @@ def wallet_set_id( return self._run_command( wallets.set_id( - wallet, - self.initialize_chain(network), - identity["name"], - identity["url"], - identity["image"], - identity["discord"], - identity["description"], - identity["additional"], - identity["github_repo"], - prompt, - json_output, + wallet=wallet, + subtensor=self.initialize_chain(network), + name=identity["name"], + web_url=identity["url"], + image_url=identity["image"], + discord=identity["discord"], + description=identity["description"], + additional=identity["additional"], + github_repo=identity["github_repo"], + json_output=json_output, + proxy=proxy, ) ) @@ -3501,7 +3832,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. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_name: if coldkey_ss58: if not is_valid_ss58_address(coldkey_ss58): @@ -3527,7 +3858,11 @@ def wallet_get_id( coldkey_ss58 = wallet.coldkeypub.ss58_address return self._run_command( - wallets.get_id(self.initialize_chain(network), coldkey_ss58, json_output) + wallets.get_id( + subtensor=self.initialize_chain(network), + ss58_address=coldkey_ss58, + json_output=json_output, + ) ) def wallet_sign( @@ -3559,7 +3894,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, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) 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}]?" @@ -3616,7 +3951,7 @@ def wallet_verify( [green]$[/green] btcli wallet verify -m "Test message" -s "0xdef456..." -p "0x1234abcd..." """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not public_key_or_ss58: public_key_or_ss58 = Prompt.ask( @@ -3647,6 +3982,8 @@ def wallet_swap_coldkey( help="SS58 address of the new coldkey that will replace the current one.", ), network: Optional[list[str]] = Options.network, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, quiet: bool = Options.quiet, verbose: bool = Options.verbose, force_swap: bool = typer.Option( @@ -3668,7 +4005,8 @@ def wallet_swap_coldkey( [green]$[/green] btcli wallet schedule-coldkey-swap --new-coldkey-ss58 5Dk...X3q """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, prompt=False, json_output=False) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not wallet_name: wallet_name = Prompt.ask( @@ -3720,9 +4058,12 @@ def wallet_swap_coldkey( subtensor=self.initialize_chain(network), new_coldkey_ss58=new_wallet_coldkey_ss58, force_swap=force_swap, + proxy=proxy, ) ) + # Stake + def get_auto_stake( self, network: Optional[list[str]] = Options.network, @@ -3742,7 +4083,7 @@ def get_auto_stake( ): """Display auto-stake destinations for a wallet across all subnets.""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = None if coldkey_ss58: @@ -3784,6 +4125,8 @@ def set_auto_stake( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, netuid: Optional[int] = Options.netuid_not_req, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -3793,7 +4136,8 @@ def set_auto_stake( ): """Set the auto-stake destination hotkey for a coldkey.""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, @@ -3846,6 +4190,7 @@ def set_auto_stake( self.initialize_chain(network), netuid, hotkey_ss58, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt_user=prompt, @@ -3893,7 +4238,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, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = None if coldkey_ss58: @@ -3974,6 +4319,8 @@ def stake_add( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, network: Optional[list[str]] = Options.network, rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, @@ -4023,7 +4370,8 @@ def stake_add( """ netuids = netuids or [] - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: rate_tolerance = self.ask_rate_tolerance(rate_tolerance) @@ -4215,21 +4563,22 @@ def stake_add( ) return self._run_command( add_stake.stake_add( - wallet, - self.initialize_chain(network), - netuids, - stake_all, - amount, - prompt, - all_hotkeys, - include_hotkeys, - exclude_hotkeys, - safe_staking, - rate_tolerance, - allow_partial_stake, - json_output, - period, - mev_protection, + wallet=wallet, + subtensor=self.initialize_chain(network), + netuids=netuids, + stake_all=stake_all, + amount=amount, + prompt=prompt, + all_hotkeys=all_hotkeys, + include_hotkeys=include_hotkeys, + exclude_hotkeys=exclude_hotkeys, + safe_staking=safe_staking, + rate_tolerance=rate_tolerance, + allow_partial_stake=allow_partial_stake, + json_output=json_output, + era=period, + proxy=proxy, + mev_protection=mev_protection, ) ) @@ -4278,6 +4627,8 @@ def stake_remove( help="When set, this command unstakes from all the hotkeys associated with the wallet. Do not use if specifying " "hotkeys in `--include-hotkeys`.", ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, @@ -4327,7 +4678,8 @@ 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, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not unstake_all and not unstake_all_alpha: safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: @@ -4510,6 +4862,7 @@ def stake_remove( json_output=json_output, era=period, mev_protection=mev_protection, + proxy=proxy, ) ) elif ( @@ -4583,6 +4936,7 @@ def stake_remove( allow_partial_stake=allow_partial_stake, json_output=json_output, era=period, + proxy=proxy, mev_protection=mev_protection, ) ) @@ -4628,6 +4982,8 @@ def stake_move( stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, period: int = Options.period, mev_protection: bool = Options.mev_protection, prompt: bool = Options.prompt, @@ -4659,7 +5015,8 @@ def stake_move( 2. Move stake without MEV protection: [green]$[/green] btcli stake move --no-mev-protection """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if prompt: if not Confirm.ask( "This transaction will [bold]move stake[/bold] to another hotkey while keeping the same " @@ -4772,6 +5129,7 @@ def stake_move( f"era: {period}\n" f"interactive_selection: {interactive_selection}\n" f"prompt: {prompt}\n" + f"proxy: {proxy}\n" f"mev_protection: {mev_protection}\n" ) result, ext_id = self._run_command( @@ -4787,6 +5145,7 @@ def stake_move( era=period, interactive_selection=interactive_selection, prompt=prompt, + proxy=proxy, mev_protection=mev_protection, ) ) @@ -4830,6 +5189,8 @@ def stake_transfer( ), mev_protection: bool = Options.mev_protection, period: int = Options.period, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4870,7 +5231,8 @@ def stake_transfer( Transfer stake without MEV protection: [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --amount 100 --no-mev-protection """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if prompt: if not Confirm.ask( "This transaction will [bold]transfer ownership[/bold] from one coldkey to another, in subnets " @@ -4966,6 +5328,7 @@ def stake_transfer( f"era: {period}\n" f"stake_all: {stake_all}\n" f"mev_protection: {mev_protection}" + f"proxy: {proxy}" ) result, ext_id = self._run_command( move_stake.transfer_stake( @@ -4980,6 +5343,7 @@ def stake_transfer( interactive_selection=interactive_selection, stake_all=stake_all, prompt=prompt, + proxy=proxy, mev_protection=mev_protection, ) ) @@ -5021,6 +5385,8 @@ def stake_swap( "--all", help="Swap all available stake", ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, period: int = Options.period, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, @@ -5053,7 +5419,8 @@ def stake_swap( 2. Swap stake without MEV protection: [green]$[/green] btcli stake swap --origin-netuid 1 --dest-netuid 2 --amount 100 --no-mev-protection """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) console.print( "[dim]This command moves stake from one subnet to another subnet while keeping " "the same coldkey-hotkey pair.[/dim]" @@ -5089,6 +5456,7 @@ def stake_swap( f"amount: {amount}\n" f"swap_all: {swap_all}\n" f"era: {period}\n" + f"proxy: {proxy}\n" f"interactive_selection: {interactive_selection}\n" f"prompt: {prompt}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" @@ -5106,6 +5474,7 @@ def stake_swap( era=period, interactive_selection=interactive_selection, prompt=prompt, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, mev_protection=mev_protection, @@ -5244,105 +5613,243 @@ def stake_wizard( return False return result - def stake_get_children( + def stake_set_claim_type( self, + claim_type: Annotated[ + Optional[claim_stake.ClaimType], + typer.Argument( + help="Claim type: 'Keep' or 'Swap'. If omitted, user will be prompted.", + ), + ] = None, + netuids: Optional[str] = typer.Option( + None, + "--netuids", + "-n", + help="Netuids to select. Supports ranges and comma-separated values, e.g., '1-5,10,20-30'.", + ), wallet_name: Optional[str] = Options.wallet_name, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, - netuid: Optional[int] = typer.Option( - None, - help="The netuid of the subnet (e.g. 2)", - prompt=False, - ), - all_netuids: bool = typer.Option( - False, - "--all-netuids", - "--all", - "--allnetuids", - help="When set, gets the child hotkeys from all the subnets.", - ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, + prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): """ - Get all the child hotkeys on a specified subnet. + Set the root claim type for your coldkey. - Users can specify the subnet and see the child hotkeys and the proportion that is given to them. This command is used to view the authority delegated to different hotkeys on the subnet. + Root claim types control how staking emissions are handled on the ROOT network (subnet 0): - EXAMPLE + [bold]Claim Types:[/bold] + • [green]Swap[/green]: Future Root Alpha Emissions are swapped to TAO and added to root stake (default) + • [yellow]Keep[/yellow]: Future Root Alpha Emissions are kept as Alpha tokens + • [cyan]Keep Specific[/cyan]: Keep specific subnets as Alpha, swap others to TAO. You can use this type by selecting the netuids. - [green]$[/green] btcli stake child get --netuid 1 - [green]$[/green] btcli stake child get --all-netuids + USAGE: + + [green]$[/green] btcli stake claim [cyan](Full wizard)[/cyan] + [green]$[/green] btcli stake claim keep [cyan](Keep all subnets)[/cyan] + [green]$[/green] btcli stake claim swap [cyan](Swap all subnets)[/cyan] + [green]$[/green] btcli stake claim keep --netuids 1-5,10,20-30 [cyan](Keep specific subnets)[/cyan] + [green]$[/green] btcli stake claim swap --netuids 1-30 [cyan](Swap specific subnets)[/cyan] + + With specific wallet: + + [green]$[/green] btcli stake claim swap --wallet-name my_wallet """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, + ask_for=[WO.NAME], ) - - if all_netuids and netuid: - err_console.print("Specify either a netuid or `--all`, not both.") - raise typer.Exit() - - if all_netuids: - netuid = None - - elif not netuid: - netuid = IntPrompt.ask( - "Enter a netuid (leave blank for all)", default=None, show_default=True - ) - - result = self._run_command( - children_hotkeys.get_children( - wallet, self.initialize_chain(network), netuid + return self._run_command( + claim_stake.set_claim_type( + wallet=wallet, + subtensor=self.initialize_chain(network), + claim_type=claim_type, + netuids=netuids, + proxy=proxy, + prompt=prompt, + json_output=json_output, ) ) - if json_output: - json_console.print(json.dumps(result)) - return result - def stake_set_children( + def stake_process_claim( self, - children: list[str] = typer.Option( - [], "--children", "-c", help="Enter child hotkeys (ss58)", prompt=False - ), - wallet_name: str = Options.wallet_name, - wallet_hotkey: str = Options.wallet_hotkey, - wallet_path: str = Options.wallet_path, + netuids: Optional[str] = Options.netuids, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, - netuid: Optional[int] = Options.netuid_not_req, - all_netuids: bool = Options.all_netuids, - proportions: list[float] = typer.Option( - [], - "--proportions", - "--prop", - help="Enter the stake weight proportions for the child hotkeys (sum should be less than or equal to 1)", - prompt=False, - ), - wait_for_inclusion: bool = Options.wait_for_inclusion, - wait_for_finalization: bool = Options.wait_for_finalization, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, + prompt: bool = Options.prompt, 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. + Manually claim accumulated root network emissions for your coldkey. - Users can specify the 'proportion' to delegate to child hotkeys (ss58 address). The sum of proportions cannot be greater than 1. + [bold]Note:[/bold] The network will eventually process your pending emissions automatically. + However, you can choose to manually claim your emissions with a small extrinsic fee. - This command is used to delegate authority to different hotkeys, securing their position and influence on the subnet. + A maximum of 5 netuids can be processed in one call. - EXAMPLE + USAGE: + + [green]$[/green] btcli stake process-claim + + Claim from specific netuids: + + [green]$[/green] btcli stake process-claim --netuids 1,2,3 + + Claim with specific wallet: + + [green]$[/green] btcli stake process-claim --netuids 1,2 --wallet-name my_wallet + + """ + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + parsed_netuids = None + if netuids: + parsed_netuids = parse_to_list( + netuids, + int, + "Netuids must be a comma-separated list of ints, e.g., `--netuids 1,2,3`.", + ) + + if len(parsed_netuids) > 5: + print_error("Maximum 5 netuids allowed per claim") + return + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + ) + + return self._run_command( + claim_stake.process_pending_claims( + wallet=wallet, + subtensor=self.initialize_chain(network), + netuids=parsed_netuids, + proxy=proxy, + prompt=prompt, + json_output=json_output, + verbose=verbose, + ) + ) + + def stake_get_children( + self, + wallet_name: Optional[str] = Options.wallet_name, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + wallet_path: Optional[str] = Options.wallet_path, + network: Optional[list[str]] = Options.network, + netuid: Optional[int] = typer.Option( + None, + help="The netuid of the subnet (e.g. 2)", + prompt=False, + ), + all_netuids: bool = typer.Option( + False, + "--all-netuids", + "--all", + "--allnetuids", + help="When set, gets the child hotkeys from all the subnets.", + ), + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Get all the child hotkeys on a specified subnet. + + Users can specify the subnet and see the child hotkeys and the proportion that is given to them. This command is used to view the authority delegated to different hotkeys on the subnet. + + EXAMPLE + + [green]$[/green] btcli stake child get --netuid 1 + [green]$[/green] btcli stake child get --all-netuids + """ + self.verbosity_handler(quiet, verbose, json_output, False) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + + if all_netuids and netuid: + err_console.print("Specify either a netuid or `--all`, not both.") + raise typer.Exit() + + if all_netuids: + netuid = None + + elif not netuid: + netuid = IntPrompt.ask( + "Enter a netuid (leave blank for all)", default=None, show_default=True + ) + + 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, + children: list[str] = typer.Option( + [], "--children", "-c", help="Enter child hotkeys (ss58)", prompt=False + ), + wallet_name: str = Options.wallet_name, + wallet_hotkey: str = Options.wallet_hotkey, + wallet_path: str = Options.wallet_path, + network: Optional[list[str]] = Options.network, + netuid: Optional[int] = Options.netuid_not_req, + all_netuids: bool = Options.all_netuids, + proportions: list[float] = typer.Option( + [], + "--proportions", + "--prop", + help="Enter the stake weight proportions for the child hotkeys (sum should be less than or equal to 1)", + prompt=False, + ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + 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. + + Users can specify the 'proportion' to delegate to child hotkeys (ss58 address). The sum of proportions cannot be greater than 1. + + This command is used to delegate authority to different hotkeys, securing their position and influence on the subnet. + + EXAMPLE [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 --prop 0.3 --prop 0.7 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) netuid = get_optional_netuid(netuid, all_netuids) children = list_prompt( @@ -5377,6 +5884,7 @@ def stake_set_children( "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" + f"proxy: {proxy}\n" f"children: {children}\n" f"proportions: {proportions}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" @@ -5393,6 +5901,7 @@ def stake_set_children( wait_for_inclusion=wait_for_inclusion, prompt=prompt, json_output=json_output, + proxy=proxy, ) ) @@ -5414,6 +5923,8 @@ def stake_revoke_children( "--allnetuids", help="When this flag is used it sets child hotkeys on all the subnets.", ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, @@ -5430,7 +5941,8 @@ def stake_revoke_children( [green]$[/green] btcli stake child revoke --hotkey --netuid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5451,16 +5963,18 @@ def stake_revoke_children( "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" + f"proxy: {proxy}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" ) return self._run_command( children_hotkeys.revoke_children( - wallet, - self.initialize_chain(network), - netuid, - wait_for_inclusion, - wait_for_finalization, + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + proxy=proxy, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, prompt=prompt, json_output=json_output, ) @@ -5498,6 +6012,8 @@ def stake_childkey_take( "take value.", prompt=False, ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -5520,7 +6036,8 @@ def stake_childkey_take( [green]$[/green] btcli stake child take --child-hotkey-ss58 --take 0.12 --netuid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5542,6 +6059,7 @@ def stake_childkey_take( f"network: {network}\n" f"netuid: {netuid}\n" f"take: {take}\n" + f"proxy: {proxy}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" ) @@ -5551,6 +6069,7 @@ def stake_childkey_take( subtensor=self.initialize_chain(network), netuid=netuid, take=take, + proxy=proxy, hotkey=child_hotkey_ss58, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -5564,6 +6083,8 @@ def stake_childkey_take( json_console.print(json.dumps(output)) return results + # Mechanism + def mechanism_count_set( self, network: Optional[list[str]] = Options.network, @@ -5577,6 +6098,8 @@ def mechanism_count_set( "--mech-count", help="Number of mechanisms to set for the subnet.", ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -5598,8 +6121,8 @@ def mechanism_count_set( [green]$[/green] btcli subnet mech set --netuid 12 --count 2 --wallet.name my_wallet --wallet.hotkey admin """ - - self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) if not json_output: @@ -5659,6 +6182,7 @@ def mechanism_count_set( f"network: {network}\n" f"netuid: {netuid}\n" f"mechanism_count: {mechanism_count}\n" + f"proxy: {proxy}\n" ) result, err_msg, ext_id = self._run_command( @@ -5667,6 +6191,7 @@ def mechanism_count_set( subtensor=subtensor, netuid=netuid, mechanism_count=mechanism_count, + proxy=proxy, previous_count=current_count or 0, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -5702,7 +6227,7 @@ def mechanism_count_get( [green]$[/green] btcli subnet mech count --netuid 12 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) subtensor = self.initialize_chain(network) return self._run_command( subnet_mechanisms.count( @@ -5724,6 +6249,8 @@ def mechanism_emission_set( "--split", help="Comma-separated relative weights for each mechanism (normalised automatically).", ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -5746,8 +6273,8 @@ def mechanism_emission_set( 2. Apply a 70/30 distribution in one command: [green]$[/green] btcli subnet mech emissions-split --netuid 12 --split 70,30 --wallet.name my_wallet --wallet.hotkey admin """ - - self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) wallet = self.wallet_ask( wallet_name, @@ -5761,6 +6288,7 @@ def mechanism_emission_set( subtensor=subtensor, wallet=wallet, netuid=netuid, + proxy=proxy, new_emission_split=split, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -5786,7 +6314,7 @@ def mechanism_emission_get( [green]$[/green] btcli subnet mech emissions --netuid 12 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) subtensor = self.initialize_chain(network) return self._run_command( subnet_mechanisms.get_emission_split( @@ -5796,6 +6324,8 @@ def mechanism_emission_get( ) ) + # Sudo + def sudo_set( self, network: Optional[list[str]] = Options.network, @@ -5809,6 +6339,8 @@ def sudo_set( param_value: Optional[str] = typer.Option( "", "--value", help="Value to set the hyperparameter to." ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5823,7 +6355,8 @@ def sudo_set( [green]$[/green] btcli sudo set --netuid 1 --param tempo --value 400 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not param_name or not param_value: hyperparams = self._run_command( @@ -5831,6 +6364,7 @@ def sudo_set( exit_early=False, ) if not hyperparams: + # TODO this will cause a hanging connection, subtensor needs to be gracefully exited raise typer.Exit() if not param_name: @@ -5912,18 +6446,20 @@ def sudo_set( "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" + f"proxy: {proxy}\n" f"param_name: {param_name}\n" f"param_value: {param_value}" ) result, err_msg, ext_id = self._run_command( sudo.sudo_set_hyperparameter( - wallet, - self.initialize_chain(network), - netuid, - param_name, - param_value, - prompt, - json_output, + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + proxy=proxy, + param_name=param_name, + param_value=param_value, + prompt=prompt, + json_output=json_output, ) ) if json_output: @@ -5953,7 +6489,7 @@ def sudo_get( [green]$[/green] btcli sudo get --netuid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) return self._run_command( sudo.get_hyperparameters( self.initialize_chain(network), netuid, json_output @@ -5975,7 +6511,7 @@ def sudo_senate( EXAMPLE [green]$[/green] btcli sudo senate """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) return self._run_command( sudo.get_senate(self.initialize_chain(network), json_output) ) @@ -5995,7 +6531,7 @@ def sudo_proposals( EXAMPLE [green]$[/green] btcli sudo proposals """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) return self._run_command( sudo.proposals(self.initialize_chain(network), verbose, json_output) ) @@ -6013,6 +6549,8 @@ def sudo_senate_vote( prompt="Enter the proposal hash", help="The hash of the proposal to vote on.", ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -6035,7 +6573,8 @@ def sudo_senate_vote( [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) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6043,10 +6582,17 @@ def sudo_senate_vote( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - logger.debug(f"args:\nnetwork: {network}\nproposal: {proposal}\nvote: {vote}\n") + logger.debug( + f"args:\nnetwork: {network}\nproposal: {proposal}\nvote: {vote}\nproxy: {proxy}" + ) return self._run_command( sudo.senate_vote( - wallet, self.initialize_chain(network), proposal, vote, prompt + wallet=wallet, + subtensor=self.initialize_chain(network), + proxy=proxy, + proposal_hash=proposal, + vote=vote, + prompt=prompt, ) ) @@ -6056,6 +6602,8 @@ def sudo_set_take( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, take: float = typer.Option(None, help="The new take value."), quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -6072,7 +6620,8 @@ def sudo_set_take( """ max_value = 0.18 min_value = 0.00 - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, @@ -6083,6 +6632,7 @@ def sudo_set_take( ) self._run_command( + # TODO does this need to take the proxy account? sudo.display_current_take(self.initialize_chain(network), wallet), exit_early=False, ) @@ -6096,9 +6646,14 @@ def sudo_set_take( f"Take value must be between {min_value} and {max_value}. Provided value: {take}" ) raise typer.Exit() - logger.debug(f"args:\nnetwork: {network}\ntake: {take}") + logger.debug(f"args:\nnetwork: {network}\ntake: {take}\nproxy: {proxy}\n") result, ext_id = self._run_command( - sudo.set_take(wallet, self.initialize_chain(network), take) + sudo.set_take( + wallet=wallet, + subtensor=self.initialize_chain(network), + take=take, + proxy=proxy, + ) ) if json_output: json_console.print( @@ -6124,7 +6679,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, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) wallet = self.wallet_ask( wallet_name, @@ -6150,6 +6705,8 @@ def sudo_trim( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, netuid: int = Options.netuid, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, max_uids: int = typer.Option( None, "--max", @@ -6169,7 +6726,8 @@ def sudo_trim( EXAMPLE [green]$[/green] btcli sudo trim --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey --max 64 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, @@ -6185,11 +6743,14 @@ def sudo_trim( netuid=netuid, max_n=max_uids, period=period, + proxy=proxy, json_output=json_output, prompt=prompt, ) ) + # Subnets + def subnets_list( self, network: Optional[list[str]] = Options.network, @@ -6227,7 +6788,7 @@ def subnets_list( 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) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) subtensor = self.initialize_chain(network) return self._run_command( subnets.subnets_list( @@ -6302,7 +6863,9 @@ def subnets_price( f"Cannot specify both {arg__('--current')} and {arg__('--html')}" ) return - self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) + self.verbosity_handler( + quiet=quiet, verbose=verbose, json_output=json_output, prompt=False + ) subtensor = self.initialize_chain(network) non_archives = ["finney", "latent-lite"] @@ -6385,7 +6948,7 @@ def subnets_show( 2. Pick mechanism 1 explicitly: [green]$[/green] btcli subnets show --netuid 12 --mechid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) if netuid == 0: mechanism_count = 1 @@ -6433,7 +6996,7 @@ def subnets_burn_cost( [green]$[/green] btcli subnets burn_cost """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) return self._run_command( subnets.burn_cost(self.initialize_chain(network), json_output) ) @@ -6444,6 +7007,8 @@ def subnets_create( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, subnet_name: Optional[str] = typer.Option( None, "--subnet-name", help="Name of the subnet" ), @@ -6493,7 +7058,8 @@ def subnets_create( 3. Create subnet without MEV protection: [green]$[/green] btcli subnets create --no-mev-protection """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6503,7 +7069,7 @@ def subnets_create( WO.HOTKEY, WO.PATH, ], - validate=WV.WALLET_AND_HOTKEY, + validate=WV.WALLET, ) identity = prompt_for_subnet_identity( current_identity={}, @@ -6516,15 +7082,18 @@ def subnets_create( logo_url=logo_url, additional=additional_info, ) - logger.debug(f"args:\nnetwork: {network}\nidentity: {identity}\n") + logger.debug( + f"args:\nnetwork: {network}\nidentity: {identity}\nproxy: {proxy}\n" + ) self._run_command( subnets.create( - wallet, - self.initialize_chain(network), - identity, - json_output, - prompt, - mev_protection, + wallet=wallet, + subtensor=self.initialize_chain(network), + subnet_identity=identity, + proxy=proxy, + json_output=json_output, + prompt=prompt, + mev_protection=mev_protection, ) ) @@ -6543,7 +7112,7 @@ def subnets_check_start( Example: [green]$[/green] btcli subnets check-start --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) return self._run_command( subnets.get_start_schedule(self.initialize_chain(network), netuid) ) @@ -6554,6 +7123,8 @@ def subnets_start( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, netuid: int = Options.netuid, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -6568,7 +7139,8 @@ def subnets_start( [green]$[/green] btcli subnets start --netuid 1 [green]$[/green] btcli subnets start --netuid 1 --wallet-name alice """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not wallet_name: wallet_name = Prompt.ask( "Enter the [blue]wallet name[/blue] [dim](which you used to create the subnet)[/dim]", @@ -6583,13 +7155,14 @@ def subnets_start( ], validate=WV.WALLET, ) - logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\n") + logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\nproxy: {proxy}\n") return self._run_command( subnets.start_subnet( - wallet, - self.initialize_chain(network), - netuid, - prompt, + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + proxy=proxy, + prompt=prompt, ) ) @@ -6608,7 +7181,7 @@ def subnets_get_identity( [green]$[/green] btcli subnets get-identity --netuid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) return self._run_command( subnets.get_identity( self.initialize_chain(network), netuid, json_output=json_output @@ -6622,6 +7195,8 @@ def subnets_set_identity( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, subnet_name: Optional[str] = typer.Option( None, "--subnet-name", "--sn-name", help="Name of the subnet" ), @@ -6666,7 +7241,8 @@ 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, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6700,11 +7276,16 @@ def subnets_set_identity( additional=additional_info, ) logger.debug( - f"args:\nnetwork: {network}\nnetuid: {netuid}\nidentity: {identity}" + f"args:\nnetwork: {network}\nnetuid: {netuid}\nidentity: {identity}\nproxy: {proxy}\n" ) success, ext_id = self._run_command( subnets.set_identity( - wallet, self.initialize_chain(network), netuid, identity, prompt + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + subnet_identity=identity, + prompt=prompt, + proxy=proxy, ) ) if json_output: @@ -6817,6 +7398,8 @@ def subnets_register( 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.", ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -6833,7 +7416,8 @@ def subnets_register( [green]$[/green] btcli subnets register --netuid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6841,15 +7425,18 @@ def subnets_register( ask_for=[WO.NAME, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\nperiod: {period}\n") + logger.debug( + f"args:\nnetwork: {network}\nnetuid: {netuid}\nperiod: {period}\nproxy: {proxy}\n" + ) return self._run_command( subnets.register( - wallet, - self.initialize_chain(network), - netuid, - period, - json_output, - prompt, + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + era=period, + json_output=json_output, + prompt=prompt, + proxy=proxy, ) ) @@ -6919,7 +7506,7 @@ def subnets_metagraph( [blue bold]Note[/blue bold]: This command is not intended to be used as a standalone function within user code. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) if (reuse_last or html_output) and self.config.get("use_cache") is False: err_console.print( "Unable to use `--reuse-last` or `--html` when config `no-cache` is set to `True`. " @@ -6961,6 +7548,9 @@ def subnets_set_symbol( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, + period: int = Options.period, json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -6981,7 +7571,8 @@ def subnets_set_symbol( If --json-output is used, the output will be in the following schema: [#AFEFFF]{success: [dark_orange]bool[/dark_orange], message: [dark_orange]str[/dark_orange]}[/#AFEFFF] """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if len(symbol) > 1: err_console.print("Your symbol must be a single character.") return False @@ -6992,17 +7583,28 @@ def subnets_set_symbol( ask_for=[WO.NAME, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug( + "args:\n" + f"network: {network}\n" + f"netuid: {netuid}\n" + f"proxy: {proxy}\n" + f"symbol: {symbol}\n" + ) return self._run_command( subnets.set_symbol( wallet=wallet, subtensor=self.initialize_chain(network), netuid=netuid, symbol=symbol, + proxy=proxy, + period=period, prompt=prompt, json_output=json_output, ) ) + # Weights + def weights_reveal( self, network: Optional[list[str]] = Options.network, @@ -7010,6 +7612,8 @@ def weights_reveal( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: int = Options.netuid, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, uids: str = typer.Option( None, "--uids", @@ -7037,12 +7641,8 @@ 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, json_output) - # TODO think we need to ','.split uids and weights ? - uids = list_prompt(uids, int, "UIDs of interest for the specified netuid") - weights = list_prompt( - weights, float, "Corresponding weights for the specified UIDs" - ) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if uids: uids = parse_to_list( uids, @@ -7051,7 +7651,7 @@ def weights_reveal( ) else: uids = list_prompt( - uids, int, "Corresponding UIDs for the specified netuid (eg: 1,2,3)" + [], int, "Corresponding UIDs for the specified netuid (eg: 1,2,3)" ) if weights: @@ -7062,7 +7662,7 @@ def weights_reveal( ) else: weights = list_prompt( - weights, + [], float, "Corresponding weights for the specified UIDs (eg: 0.2,0.3,0.4)", ) @@ -7080,7 +7680,7 @@ def weights_reveal( "Salt must be a comma-separated list of ints, e.g., `--weights 123,163,194`.", ) else: - salt = list_prompt(salt, int, "Corresponding salt for the hash function") + salt = list_prompt([], int, "Corresponding salt for the hash function") wallet = self.wallet_ask( wallet_name, @@ -7091,13 +7691,14 @@ def weights_reveal( ) return self._run_command( weights_cmds.reveal_weights( - self.initialize_chain(network), - wallet, - netuid, - uids, - weights, - salt, - __version_as_int__, + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + proxy=proxy, + uids=uids, + weights=weights, + salt=salt, + version=__version_as_int__, prompt=prompt, json_output=json_output, ) @@ -7110,6 +7711,8 @@ def weights_commit( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: int = Options.netuid, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, uids: str = typer.Option( None, "--uids", @@ -7141,8 +7744,8 @@ 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, json_output) - + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if uids: uids = parse_to_list( uids, @@ -7151,7 +7754,7 @@ def weights_commit( ) else: uids = list_prompt( - uids, int, "UIDs of interest for the specified netuid (eg: 1,2,3)" + [], int, "UIDs of interest for the specified netuid (eg: 1,2,3)" ) if weights: @@ -7162,7 +7765,7 @@ def weights_commit( ) else: weights = list_prompt( - weights, + [], float, "Corresponding weights for the specified UIDs (eg: 0.2,0.3,0.4)", ) @@ -7179,7 +7782,7 @@ def weights_commit( "Salt must be a comma-separated list of ints, e.g., `--weights 123,163,194`.", ) else: - salt = list_prompt(salt, int, "Corresponding salt for the hash function") + salt = list_prompt([], int, "Corresponding salt for the hash function") wallet = self.wallet_ask( wallet_name, @@ -7190,18 +7793,21 @@ def weights_commit( ) return self._run_command( weights_cmds.commit_weights( - self.initialize_chain(network), - wallet, - netuid, - uids, - weights, - salt, - __version_as_int__, + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + uids=uids, + proxy=proxy, + weights=weights, + salt=salt, + version=__version_as_int__, json_output=json_output, prompt=prompt, ) ) + # View + def view_dashboard( self, network: Optional[list[str]] = Options.network, @@ -7227,7 +7833,7 @@ def view_dashboard( """ Display html dashboard with subnets list, stake, and neuron information. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) if use_wry and save_file: print_error("Cannot save file when using browser output.") @@ -7262,146 +7868,23 @@ def view_dashboard( ) ) - def stake_set_claim_type( + # Liquidity + + def liquidity_add( self, - claim_type: Optional[str] = typer.Argument( + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: Optional[int] = Options.netuid, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, + liquidity_: Optional[float] = typer.Option( None, - help="Claim type: 'keep' or 'swap'. If not provided, you'll be prompted to choose.", + "--liquidity", + help="Amount of liquidity to add to the subnet.", ), - netuids: Optional[str] = typer.Option( - None, - "--netuids", - "-n", - help="Netuids to select. Supports ranges and comma-separated values, e.g., '1-5,10,20-30'.", - ), - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - json_output: bool = Options.json_output, - ): - """ - Set the root claim type for your coldkey. - - Root claim types control how staking emissions are handled on the ROOT network (subnet 0): - - [bold]Claim Types:[/bold] - • [green]Swap[/green]: Future Root Alpha Emissions are swapped to TAO and added to root stake (default) - • [yellow]Keep[/yellow]: Future Root Alpha Emissions are kept as Alpha tokens - • [cyan]Keep Specific[/cyan]: Keep specific subnets as Alpha, swap others to TAO. You can use this type by selecting the netuids. - - USAGE: - - [green]$[/green] btcli stake claim [cyan](Full wizard)[/cyan] - [green]$[/green] btcli stake claim keep [cyan](Keep all subnets)[/cyan] - [green]$[/green] btcli stake claim swap [cyan](Swap all subnets)[/cyan] - [green]$[/green] btcli stake claim keep --netuids 1-5,10,20-30 [cyan](Keep specific subnets)[/cyan] - [green]$[/green] btcli stake claim swap --netuids 1-30 [cyan](Swap specific subnets)[/cyan] - - With specific wallet: - - [green]$[/green] btcli stake claim swap --wallet-name my_wallet - """ - self.verbosity_handler(quiet, verbose, json_output) - - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME], - ) - return self._run_command( - claim_stake.set_claim_type( - wallet=wallet, - subtensor=self.initialize_chain(network), - claim_type=claim_type, - netuids=netuids, - prompt=prompt, - json_output=json_output, - ) - ) - - def stake_process_claim( - self, - netuids: Optional[str] = Options.netuids, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - json_output: bool = Options.json_output, - ): - """ - Manually claim accumulated root network emissions for your coldkey. - - [bold]Note:[/bold] The network will eventually process your pending emissions automatically. - However, you can choose to manually claim your emissions with a small extrinsic fee. - - A maximum of 5 netuids can be processed in one call. - - USAGE: - - [green]$[/green] btcli stake process-claim - - Claim from specific netuids: - - [green]$[/green] btcli stake process-claim --netuids 1,2,3 - - Claim with specific wallet: - - [green]$[/green] btcli stake process-claim --netuids 1,2 --wallet-name my_wallet - - """ - self.verbosity_handler(quiet, verbose, json_output) - - parsed_netuids = None - if netuids: - parsed_netuids = parse_to_list( - netuids, - int, - "Netuids must be a comma-separated list of ints, e.g., `--netuids 1,2,3`.", - ) - - if len(parsed_netuids) > 5: - print_error("Maximum 5 netuids allowed per claim") - return - - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME], - ) - - return self._run_command( - claim_stake.process_pending_claims( - wallet=wallet, - subtensor=self.initialize_chain(network), - netuids=parsed_netuids, - prompt=prompt, - json_output=json_output, - verbose=verbose, - ) - ) - - def liquidity_add( - self, - network: Optional[list[str]] = Options.network, - wallet_name: str = Options.wallet_name, - wallet_path: str = Options.wallet_path, - wallet_hotkey: str = Options.wallet_hotkey, - netuid: Optional[int] = Options.netuid, - liquidity_: Optional[float] = typer.Option( - None, - "--liquidity", - help="Amount of liquidity to add to the subnet.", - ), - price_low: Optional[float] = typer.Option( + price_low: Optional[float] = typer.Option( None, "--price-low", "--price_low", @@ -7423,7 +7906,8 @@ def liquidity_add( json_output: bool = Options.json_output, ): """Add liquidity to the swap (as a combination of TAO + Alpha).""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not netuid: netuid = Prompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -7468,6 +7952,7 @@ def liquidity_add( f"liquidity: {liquidity_}\n" f"price_low: {price_low}\n" f"price_high: {price_high}\n" + f"proxy: {proxy}\n" ) return self._run_command( liquidity.add_liquidity( @@ -7475,6 +7960,7 @@ def liquidity_add( wallet=wallet, hotkey_ss58=hotkey, netuid=netuid, + proxy=proxy, liquidity=liquidity_, price_low=price_low, price_high=price_high, @@ -7495,7 +7981,7 @@ def liquidity_list( json_output: bool = Options.json_output, ): """Displays liquidity positions in given subnet.""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) if not netuid: netuid = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -7526,6 +8012,8 @@ def liquidity_remove( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, position_id: Optional[int] = typer.Option( None, "--position-id", @@ -7545,8 +8033,8 @@ def liquidity_remove( ): """Remove liquidity from the swap (as a combination of TAO + Alpha).""" - self.verbosity_handler(quiet, verbose, json_output) - + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if all_liquidity_ids and position_id: print_error("Cannot specify both --all and --position-id.") return @@ -7583,6 +8071,7 @@ def liquidity_remove( wallet=wallet, hotkey_ss58=hotkey, netuid=netuid, + proxy=proxy, position_id=position_id, prompt=prompt, all_liquidity_ids=all_liquidity_ids, @@ -7597,6 +8086,8 @@ def liquidity_modify( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, position_id: Optional[int] = typer.Option( None, "--position-id", @@ -7615,7 +8106,8 @@ def liquidity_modify( json_output: bool = Options.json_output, ): """Modifies the liquidity position for the given subnet.""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not netuid: netuid = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -7647,7 +8139,8 @@ def liquidity_modify( f"hotkey: {hotkey}\n" f"netuid: {netuid}\n" f"position_id: {position_id}\n" - f"liquidity_delta: {liquidity_delta}" + f"liquidity_delta: {liquidity_delta}\n" + f"proxy: {proxy}\n" ) return self._run_command( @@ -7656,6 +8149,7 @@ def liquidity_modify( wallet=wallet, hotkey_ss58=hotkey, netuid=netuid, + proxy=proxy, position_id=position_id, liquidity_delta=liquidity_delta, prompt=prompt, @@ -7663,6 +8157,8 @@ def liquidity_modify( ) ) + # Crowd + def crowd_list( self, network: Optional[list[str]] = Options.network, @@ -7685,7 +8181,7 @@ def crowd_list( [green]$[/green] btcli crowd list --verbose """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) return self._run_command( view_crowdloan.list_crowdloans( subtensor=self.initialize_chain(network), @@ -7722,7 +8218,7 @@ def crowd_info( [green]$[/green] btcli crowd info --id 1 --verbose """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( @@ -7757,6 +8253,8 @@ def crowd_create( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, deposit: Optional[float] = typer.Option( None, "--deposit", @@ -7832,8 +8330,8 @@ def crowd_create( Subnet lease ending at block 500000: [green]$[/green] btcli crowd create --subnet-lease --emissions-share 25 --lease-end-block 500000 """ - self.verbosity_handler(quiet, verbose, json_output) - + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, @@ -7846,6 +8344,7 @@ def crowd_create( create_crowdloan.create_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, deposit_tao=deposit, min_contribution_tao=min_contribution, cap_tao=cap, @@ -7881,6 +8380,8 @@ def crowd_contribute( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -7899,8 +8400,8 @@ def crowd_contribute( [green]$[/green] btcli crowd contribute --id 1 """ - self.verbosity_handler(quiet, verbose, json_output) - + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -7920,6 +8421,7 @@ def crowd_contribute( crowd_contribute.contribute_to_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, crowdloan_id=crowdloan_id, amount=amount, prompt=prompt, @@ -7942,6 +8444,8 @@ def crowd_withdraw( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -7955,8 +8459,8 @@ def crowd_withdraw( Non-creators can withdraw their full contribution. Creators can only withdraw amounts above their initial deposit. """ - self.verbosity_handler(quiet, verbose, json_output) - + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -7976,6 +8480,7 @@ def crowd_withdraw( crowd_contribute.withdraw_from_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, crowdloan_id=crowdloan_id, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -7997,6 +8502,8 @@ def crowd_finalize( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8010,8 +8517,8 @@ def crowd_finalize( Only the creator can finalize. This will transfer funds to the target address (if specified) and execute any attached call (e.g., subnet creation). """ - self.verbosity_handler(quiet, verbose, json_output) - + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8032,6 +8539,7 @@ def crowd_finalize( subtensor=self.initialize_chain(network), wallet=wallet, crowdloan_id=crowdloan_id, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, @@ -8069,6 +8577,8 @@ def crowd_update( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8085,8 +8595,8 @@ def crowd_update( against the chain constants (absolute minimum contribution, block-duration bounds, etc.). """ - self.verbosity_handler(quiet, verbose, json_output) - + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8111,6 +8621,7 @@ def crowd_update( crowd_update.update_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, crowdloan_id=crowdloan_id, min_contribution=min_contribution_balance, end=end, @@ -8131,6 +8642,8 @@ def crowd_refund( "--id", help="The ID of the crowdloan to refund", ), + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, @@ -8150,8 +8663,8 @@ def crowd_refund( Contributors can call `btcli crowdloan withdraw` at will. """ - self.verbosity_handler(quiet, verbose, json_output) - + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8171,6 +8684,7 @@ def crowd_refund( crowd_refund.refund_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, crowdloan_id=crowdloan_id, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -8192,6 +8706,8 @@ def crowd_dissolve( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8210,8 +8726,8 @@ def crowd_dissolve( If there are funds still available other than the creator's contribution, you can run `btcli crowd refund` to refund the remaining contributors. """ - self.verbosity_handler(quiet, verbose, json_output) - + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8231,6 +8747,7 @@ def crowd_dissolve( crowd_dissolve.dissolve_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, crowdloan_id=crowdloan_id, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -8239,6 +8756,500 @@ def crowd_dissolve( ) ) + # Proxy + # TODO check announcements: how do they work? + + def proxy_create( + self, + network: Optional[list[str]] = Options.network, + proxy_type: ProxyType = Options.proxy_type, + delay: int = typer.Option(0, help="Delay, in number of blocks"), + idx: int = typer.Option( + 0, + "--index", + help="A disambiguation index, in case this is called multiple times in the same transaction" + " (e.g. with utility::batch). Unless you're using batch you probably just want to use 0.", + ), + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + period: int = Options.period, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Creates a new pure proxy account. The pure proxy account is a keyless account controlled by your wallet. + + [bold]Note[/bold]: The proxy account has no private key and cannot sign transactions independently. + All operations must be initiated and signed by the delegator. + + + [bold]Common Examples:[/bold] + 1. Create a pure proxy account + [green]$[/green] btcli proxy create --proxy-type Any + + 2. Create a delayed pure proxy account + [green]$[/green] btcli proxy create --proxy-type Any --delay 1000 + + """ + self.verbosity_handler(quiet, verbose, json_output, prompt) + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + logger.debug( + "args:\n" + f"network: {network}\n" + f"proxy_type: {proxy_type}\n" + f"delay: {delay}\n" + f"idx: {idx}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + f"era: {period}\n" + f"prompt: {prompt}\n" + ) + + self._run_command( + proxy_commands.create_proxy( + subtensor=self.initialize_chain(network), + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + idx=idx, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + json_output=json_output, + period=period, + ) + ) + + def proxy_add( + self, + delegate: Annotated[ + str, + typer.Option( + callback=is_valid_ss58_address_param, + prompt="Enter the SS58 address of the delegate to add, e.g. 5dxds...", + help="The SS58 address of the delegate to add", + ), + ] = "", + network: Optional[list[str]] = Options.network, + proxy_type: ProxyType = Options.proxy_type, + delay: int = typer.Option(0, help="Delay, in number of blocks"), + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + period: int = Options.period, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Registers an existing account as a standard proxy for the delegator. + + Grants an existing account permission to execute transactions on your behalf with + specified restrictions. + + [bold]Common Examples:[/bold] + 1. Create a standard proxy account + [green]$[/green] btcli proxy add --delegate 5GDeleg... --proxy-type SmallTransfer + + 2. Create a delayed standard proxy account + [green]$[/green] btcli proxy add --delegate 5GDeleg... --proxy-type Transfer --delay 500 + + """ + logger.debug( + "args:\n" + f"network: {network}\n" + f"delegate: {delegate}\n" + f"proxy_type: {proxy_type}\n" + f"delay: {delay}\n" + f"prompt: {prompt}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"era: {period}\n" + ) + self.verbosity_handler(quiet, verbose, json_output, prompt) + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + return self._run_command( + proxy_commands.add_proxy( + subtensor=self.initialize_chain(network), + wallet=wallet, + delegate=delegate, + proxy_type=proxy_type, + delay=delay, + prompt=prompt, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + json_output=json_output, + ) + ) + + def proxy_remove( + self, + delegate: Annotated[ + str, + typer.Option( + callback=is_valid_ss58_address_param, + prompt="Enter the SS58 address of the delegate to remove, e.g. 5dxds...", + help="The SS58 address of the delegate to remove", + ), + ] = "", + network: Optional[list[str]] = Options.network, + proxy_type: ProxyType = Options.proxy_type, + delay: int = typer.Option(0, help="Delay, in number of blocks"), + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + period: int = Options.period, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Unregisters a proxy from an account. + + Revokes proxy permissions previously granted to another account. This prevents the delegate account from executing any further transactions on your behalf. + + [bold]Note[/bold]: You can specify a delegate to remove a single proxy or use the `--all` flag to remove all existing proxies linked to an account. + + + [bold]Common Examples:[/bold] + 1. Revoke proxy permissions from a single proxy account + [green]$[/green] btcli proxy remove --delegate 5GDel... --proxy-type Transfer + + 2. Remove all proxies linked to an account + [green]$[/green] btcli proxy remove --all + + """ + # TODO should add a --all flag to call Proxy.remove_proxies ? + logger.debug( + "args:\n" + f"delegate: {delegate}\n" + f"network: {network}\n" + f"proxy_type: {proxy_type}\n" + f"delay: {delay}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"era: {period}\n" + ) + self.verbosity_handler(quiet, verbose, json_output, prompt) + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + return self._run_command( + proxy_commands.remove_proxy( + subtensor=self.initialize_chain(network), + wallet=wallet, + delegate=delegate, + proxy_type=proxy_type, + delay=delay, + prompt=prompt, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + json_output=json_output, + ) + ) + + def proxy_kill( + self, + height: int = typer.Option( + help="The block number that the proxy was created at", + prompt="Enter the block number at which the proxy was created", + ), + ext_index: int = typer.Option( + help=f"The extrinsic index of the Proxy.PureCreated event" + f" ([{COLORS.G.ARG}]btcli proxy create[/{COLORS.G.ARG}])", + prompt="Enter the extrinsic index of the `btcli proxy create` event.", + ), + spawner: Annotated[ + Optional[str], + typer.Option( + callback=is_valid_ss58_address_param, + help="The SS58 of the pure proxy creator account. If omitted, the wallet's coldkeypub is used.", + ), + ] = None, + network: Optional[list[str]] = Options.network, + proxy_type: ProxyType = Options.proxy_type, + proxy: str = Options.proxy, + announce_only: bool = Options.announce_only, + idx: int = typer.Option( + 0, + "--index", + help="A disambiguation index, in case this is called multiple times in the same transaction" + " (e.g. with utility::batch). Unless you're using batch you probably just want to use 0.", + ), + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + period: int = Options.period, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Permanently removes a pure proxy account. + + Once killed, the pure proxy account is cleared from chain storage and cannot be recovered. + + [bold]⚠️ WARNING[/bold]: Killing a pure proxy permanently removes access to the account, and any funds remaining in it are lost. + + EXAMPLE + + [green]$[/green] btcli proxy kill --height 6345834 --index 3 --proxy-type Any --spawner 5x34SPAWN... --proxy 5CCProxy... + """ + logger.debug( + "args:\n" + f"height: {height}\n" + f"ext_index: {ext_index}\n" + f"proxy_type: {proxy_type}\n" + f"spawner: {spawner}\n" + f"proxy: {proxy}\n" + f"network: {network}\n" + f"idx: {idx}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + f"era: {period}\n" + ) + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + return self._run_command( + proxy_commands.kill_proxy( + subtensor=self.initialize_chain(network), + wallet=wallet, + proxy_type=proxy_type, + height=height, + proxy=proxy, + announce_only=announce_only, + ext_index=ext_index, + idx=idx, + spawner=spawner, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + json_output=json_output, + period=period, + ) + ) + + def proxy_execute_announced( + self, + proxy: str = Options.proxy, + real: Optional[str] = Options.real_proxy, + delegate: Optional[str] = typer.Option( + None, + "--delegate", + help="The delegate of the call. If omitted, the wallet's coldkey ss58 is used.", + ), + call_hash: Optional[str] = typer.Option( + None, + help="The hash proxy call to execute", + ), + call_hex: Optional[str] = typer.Option( + None, help="The hex of the call to specify" + ), + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + period: int = Options.period, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + self.verbosity_handler(quiet, verbose, json_output, prompt) + outer_proxy_from_config = self.proxies.get(proxy, {}) + proxy_from_config = outer_proxy_from_config.get("address") + delay = 0 + got_delay_from_config = False + if proxy_from_config is not None: + proxy = proxy_from_config + delay = outer_proxy_from_config["delay"] + got_delay_from_config = True + else: + if not is_valid_ss58_address(proxy): + raise typer.BadParameter( + f"proxy {proxy} is not a valid SS58 address or proxy address book name." + ) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + real = self.is_valid_proxy_name_or_ss58(real, False) or proxy + delegate = delegate or wallet.coldkeypub.ss58_address + with ProxyAnnouncements.get_db() as (conn, cursor): + announcements = ProxyAnnouncements.read_rows(conn, cursor) + if not got_delay_from_config: + proxies = ProxyAddressBook.read_rows(conn, cursor) + else: + proxies = [] + potential_matches = [] + if not got_delay_from_config: + for row in proxies: + p_name, ss58_address, delay_, spawner, proxy_type, note = row + if proxy == ss58_address: + potential_matches.append(row) + if len(potential_matches) == 1: + delay = potential_matches[0][2] + got_delay_from_config = True + elif len(potential_matches) > 1: + if not prompt: + err_console.print( + f":cross_mark:[red]Error: The proxy ss58 you provided: {proxy} matched the address book" + f" ambiguously (more than one match). To use this (rather than the address book name), you will " + f"have to use without {arg__('--no-prompt')}" + ) + return + else: + console.print( + f"The proxy ss58 you provided matches the address book ambiguously. The results will be" + f"iterated, for you to select your intended proxy." + ) + for row in potential_matches: + p_name, ss58_address, delay_, spawner, proxy_type, note = row + console.print( + f"Name: {p_name}\n" + f"Delay: {delay_}\n" + f"Spawner/Delegator: {spawner}\n" + f"Proxy Type: {proxy_type}\n" + f"Note: {note}\n" + ) + if Confirm.ask("Is this the intended proxy?"): + delay = delay_ + got_delay_from_config = True + break + + if not got_delay_from_config: + verbose_console.print( + f"Unable to retrieve proxy from address book: {proxy}" + ) + block = None + # index of the call if retrieved from DB + got_call_from_db: Optional[int] = None + if not call_hex: + potential_call_matches = [] + for row in announcements: + ( + id_, + address, + epoch_time, + block_, + call_hash_, + call_hex_, + call_serialized, + executed_int, + ) = row + executed = bool(executed_int) + if call_hash_ == call_hash and address == proxy and executed is False: + potential_call_matches.append(row) + if len(potential_call_matches) == 1: + block = potential_call_matches[0][3] + call_hex = potential_call_matches[0][5] + got_call_from_db = potential_call_matches[0][0] + elif len(potential_call_matches) > 1: + if not prompt: + err_console.print( + f":cross_mark:[red]Error: The call hash you have provided matches {len(potential_call_matches)}" + f" possible entries. In order to choose which one, you will need to run " + f"without {arg__('--no-prompt')}" + ) + return + else: + console.print( + f"The call hash you have provided matches {len(potential_call_matches)}" + f" possible entries. The results will be iterated for you to selected your intended" + f"call." + ) + for row in potential_call_matches: + ( + id_, + address, + epoch_time, + block_, + call_hash_, + call_hex_, + call_serialized, + executed_int, + ) = row + console.print( + f"Time: {datetime.datetime.fromtimestamp(epoch_time)}\nCall:\n" + ) + console.print_json(call_serialized) + if Confirm.ask("Is this the intended call?"): + call_hex = call_hex_ + block = block_ + got_call_from_db = row + break + if got_call_from_db is None: + console.print("Unable to retrieve call from DB. Proceeding without.") + if isinstance(call_hex, str) and call_hex[0:2] == "0x": + call_hex = call_hex[2:] + + success = self._run_command( + proxy_commands.execute_announced( + subtensor=self.initialize_chain(network), + wallet=wallet, + # TODO this might be backwards with pure vs non-pure proxies + delegate=delegate, + real=real, + period=period, + call_hex=call_hex, + delay=delay, + created_block=block, + prompt=prompt, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + json_output=json_output, + ) + ) + if success and got_call_from_db is not None: + with ProxyAnnouncements.get_db() as (conn, cursor): + ProxyAnnouncements.mark_as_executed(conn, cursor, got_call_from_db) + @staticmethod def convert( from_rao: Optional[str] = typer.Option( diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index cc27ad38a..9b4e1c3c3 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -114,6 +114,11 @@ class config: }, } + class proxies: + base_path = "~/.bittensor" + path = "~/.bittensor/bittensor.db" + dictionary = {} + class subtensor: network = "finney" chain_endpoint = None @@ -734,6 +739,9 @@ class RootSudoOnly(Enum): "PARTICIPANT": "Crowdloan Participation", "INFO": "Crowdloan Information", }, + "PROXY": { + "MGMT": "Proxy Account Management", + }, } diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index a32bc1c3d..0c4e4f585 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -681,6 +681,7 @@ async def burned_register_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, era: Optional[int] = None, + proxy: Optional[str] = None, ) -> tuple[bool, str, Optional[str]]: """Registers the wallet to chain by recycling TAO. @@ -693,7 +694,7 @@ async def burned_register_extrinsic( :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. + :param proxy: the proxy address to use for the call. :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`. @@ -758,7 +759,12 @@ async def burned_register_extrinsic( }, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization, era=era_ + call, + wallet, + wait_for_inclusion, + wait_for_finalization, + era=era_, + proxy=proxy, ) if not success: @@ -1752,6 +1758,7 @@ async def swap_hotkey_extrinsic( wallet: Wallet, new_wallet: Wallet, netuid: Optional[int] = None, + proxy: Optional[str] = None, prompt: bool = False, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """ @@ -1837,7 +1844,7 @@ async def swap_hotkey_extrinsic( call_params=call_params, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call=call, wallet=wallet, proxy=proxy ) if success: diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index ea515ed1a..f95d9990e 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -292,6 +292,7 @@ async def root_register_extrinsic( wallet: Wallet, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, + proxy: Optional[str] = None, ) -> tuple[bool, str, Optional[str]]: r"""Registers the wallet to root network. @@ -301,7 +302,7 @@ async def root_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 prompt: If `True`, the call waits for confirmation from the user before proceeding. + :param proxy: Optional proxy to use for making the call. :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`. @@ -331,6 +332,7 @@ async def root_register_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) if not success: diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 6a7976d11..cbc53683a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -33,6 +33,8 @@ async def transfer_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, + proxy: Optional[str] = None, + announce_only: bool = False, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Transfers funds from this wallet to the destination public key address. @@ -48,6 +50,9 @@ async def transfer_extrinsic( :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 prompt: If `True`, the call waits for confirmation from the user before proceeding. + :param proxy: Optional proxy to use for this call. + :param announce_only: If set along with proxy, will make this call as an announcement, rather than making the call + :return: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is `True`, regardless of its inclusion. """ @@ -63,22 +68,11 @@ async def get_transfer_fee() -> Balance: call_function=call_function, call_params=call_params, ) + return await subtensor.get_extrinsic_fee( + call=call, keypair=wallet.coldkeypub, proxy=proxy + ) - try: - payment_info = await subtensor.substrate.get_payment_info( - call=call, keypair=wallet.coldkeypub - ) - except SubstrateRequestException as e: - payment_info = {"partial_fee": int(2e7)} # assume 0.02 Tao - err_console.print( - f":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n" - f" {format_error_message(e)}[/bold white]\n" - f" Defaulting to default transfer fee: {payment_info['partialFee']}" - ) - - return Balance.from_rao(payment_info["partial_fee"]) - - async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: + async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt]]: """ Makes transfer from wallet to destination public key address. :return: success, block hash, formatted error message @@ -88,29 +82,17 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: call_function=call_function, call_params=call_params, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, + success_, error_msg_, receipt_ = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + proxy=proxy, + era={"period": era}, + announce_only=announce_only, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, "", "", response - - # Otherwise continue with finalization. - if await response.is_success: - block_hash_ = response.block_hash - return True, block_hash_, "", response - else: - return ( - False, - "", - format_error_message(await response.error_message), - response, - ) + block_hash_ = receipt_.block_hash if receipt_ is not None else "" + return success_, block_hash_, error_msg_, receipt_ # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): @@ -142,36 +124,66 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: # check existential deposit and fee print_verbose("Fetching existential and fee", status) block_hash = await subtensor.substrate.get_chain_head() - account_balance, existential_deposit = await asyncio.gather( + if proxy: + proxy_balance = await subtensor.get_balance(proxy, block_hash=block_hash) + account_balance, existential_deposit, fee = await asyncio.gather( subtensor.get_balance( wallet.coldkeypub.ss58_address, block_hash=block_hash ), subtensor.get_existential_deposit(block_hash=block_hash), + get_transfer_fee(), ) - fee = await get_transfer_fee() if allow_death: # Check if the transfer should keep alive the account existential_deposit = Balance(0) - if account_balance < (amount + fee + existential_deposit) and not allow_death: - err_console.print( - ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" - f" balance: [bright_cyan]{account_balance}[/bright_cyan]\n" - f" amount: [bright_cyan]{amount}[/bright_cyan]\n" - f" for fee: [bright_cyan]{fee}[/bright_cyan]\n" - f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n" - f"You can try again with `--allow-death`." - ) - return False, None - elif account_balance < (amount + fee) and allow_death: - print_error( - ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" - f" balance: [bright_red]{account_balance}[/bright_red]\n" - f" amount: [bright_red]{amount}[/bright_red]\n" - f" for fee: [bright_red]{fee}[/bright_red]" - ) - return False, None + if proxy: + if proxy_balance < (amount + existential_deposit) and not allow_death: + err_console.print( + ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" + f" balance: [bright_cyan]{proxy_balance}[/bright_cyan]\n" + f" amount: [bright_cyan]{amount}[/bright_cyan]\n" + f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n" + f"You can try again with `--allow-death`." + ) + return False, None + if account_balance < fee: + err_console.print( + ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" + f" balance: [bright_cyan]{account_balance}[/bright_cyan]\n" + f" fee: [bright_cyan]{fee}[/bright_cyan]\n" + f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n" + ) + return False, None + if account_balance < amount and allow_death: + print_error( + ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" + f" balance: [bright_red]{account_balance}[/bright_red]\n" + f" amount: [bright_red]{amount}[/bright_red]\n" + ) + return False, None + else: + if account_balance < (amount + fee + existential_deposit) and not allow_death: + err_console.print( + ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" + f" balance: [bright_cyan]{account_balance}[/bright_cyan]\n" + f" amount: [bright_cyan]{amount}[/bright_cyan]\n" + f" for fee: [bright_cyan]{fee}[/bright_cyan]\n" + f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n" + f"You can try again with `--allow-death`." + ) + return False, None + elif account_balance < (amount + fee) and allow_death: + print_error( + ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" + f" balance: [bright_red]{account_balance}[/bright_red]\n" + f" amount: [bright_red]{amount}[/bright_red]\n" + f" for fee: [bright_red]{fee}[/bright_red]" + ) + return False, None + if proxy: + account_balance = proxy_balance # Ask before moving on. if prompt: @@ -213,7 +225,7 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: if success: with console.status(":satellite: Checking Balance...", spinner="aesthetic"): new_balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address, reuse_block=False + proxy or wallet.coldkeypub.ss58_address, reuse_block=False ) console.print( f"Balance:\n" diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 057c5172a..70af13cdc 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,7 +1,7 @@ import asyncio import os import time -from typing import Optional, Any, Union, TypedDict, Iterable +from typing import Optional, Any, Union, TypedDict, Iterable, Literal from async_substrate_interface import AsyncExtrinsicReceipt from async_substrate_interface.async_substrate import ( @@ -43,6 +43,7 @@ u16_normalized_float, MEV_SHIELD_PUBLIC_KEY_SIZE, get_hotkey_pub_ss58, + ProxyAnnouncements, ) GENESIS_ADDRESS = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" @@ -1173,6 +1174,10 @@ async def sign_and_send_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, era: Optional[dict[str, int]] = None, + proxy: Optional[str] = None, + nonce: Optional[int] = None, + sign_with: Literal["coldkey", "hotkey", "coldkeypub"] = "coldkey", + announce_only: bool = False, ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1182,18 +1187,45 @@ async def sign_and_send_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: dict[str, Union[GenericCall, Keypair, dict[str, int]]] = { + :param proxy: The real account used to create the proxy. None if not using a proxy for this call. + :param nonce: The nonce used to submit this extrinsic call. + :param sign_with: Determine which of the wallet's keypairs to use to sign the extrinsic call. + :param announce_only: If set, makes the call as an announcement, rather than making the call. + + :return: (success, error message, extrinsic receipt | None) + """ + if proxy is not None: + if announce_only: + call_to_announce = call + call = await self.substrate.compose_call( + "Proxy", + "announce", + { + "real": proxy, + "call_hash": f"0x{call_to_announce.call_hash.hex()}", + }, + ) + else: + call = await self.substrate.compose_call( + "Proxy", + "proxy", + {"real": proxy, "call": call, "force_proxy_type": None}, + ) + keypair = getattr(wallet, sign_with) + call_args: dict[str, Union[GenericCall, Keypair, dict[str, int], int]] = { "call": call, - "keypair": wallet.coldkey, + # sign with specified key + "keypair": keypair, } if era is not None: call_args["era"] = era - extrinsic = await self.substrate.create_signed_extrinsic( - **call_args - ) # sign with coldkey + if nonce is not None: + call_args["nonce"] = nonce + else: + call_args["nonce"] = await self.substrate.get_account_next_index( + keypair.ss58_address + ) + extrinsic = await self.substrate.create_signed_extrinsic(**call_args) try: response = await self.substrate.submit_extrinsic( extrinsic, @@ -1204,11 +1236,40 @@ async def sign_and_send_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True, "", response if await response.is_success: + if announce_only: + block = await self.substrate.get_block_number(response.block_hash) + with ProxyAnnouncements.get_db() as (conn, cursor): + ProxyAnnouncements.add_entry( + conn, + cursor, + address=proxy, + epoch_time=int(time.time()), + block=block, + call_hash=call_to_announce.call_hash.hex(), + call=call_to_announce, + ) + console.print( + f"Added entry {call_to_announce.call_hash} at block {block} to your ProxyAnnouncements address book." + ) return True, "", response else: return False, format_error_message(await response.error_message), None except SubstrateRequestException as e: - return False, format_error_message(e), None + err_msg = format_error_message(e) + if proxy and "Invalid Transaction" in err_msg: + extrinsic_fee, signer_balance = await asyncio.gather( + self.get_extrinsic_fee( + call, keypair=wallet.coldkeypub, proxy=proxy + ), + self.get_balance(wallet.coldkeypub.ss58_address), + ) + if extrinsic_fee > signer_balance: + err_msg += ( + "\nAs this is a proxy transaction, the signing account needs to pay the extrinsic fee. " + f"However, the balance of the signing account is {signer_balance}, and the extrinsic fee is " + f"{extrinsic_fee}." + ) + return False, err_msg, None async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: """ @@ -1615,16 +1676,25 @@ async def get_owned_hotkeys( return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] - async def get_extrinsic_fee(self, call: GenericCall, keypair: Keypair) -> Balance: + async def get_extrinsic_fee( + self, call: GenericCall, keypair: Keypair, proxy: Optional[str] = None + ) -> Balance: """ Determines the fee for the extrinsic call. Args: call: Created extrinsic call keypair: The keypair that would sign the extrinsic (usually you would just want to use the *pub for this) + proxy: Optional proxy for the extrinsic call Returns: Balance object representing the fee for this extrinsic. """ + if proxy is not None: + call = await self.substrate.compose_call( + "Proxy", + "proxy", + {"real": proxy, "call": call, "force_proxy_type": None}, + ) fee_dict = await self.substrate.get_payment_info(call, keypair) return Balance.from_rao(fee_dict["partial_fee"]) @@ -2240,6 +2310,7 @@ async def get_subnet_price( :return: The current Alpha price in TAO units for the specified subnet. """ + # TODO update this to use the runtime call SwapRuntimeAPI.current_alpha_price current_sqrt_price = await self.query( module="Swap", storage_function="AlphaSqrtPrice", diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index d49c0313c..ca4b56099 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1,11 +1,13 @@ import ast +import json from collections import namedtuple import math import os import sqlite3 import webbrowser +from contextlib import contextmanager from pathlib import Path -from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable +from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable, Generator from urllib.parse import urlparse from functools import partial import re @@ -22,6 +24,7 @@ from numpy.typing import NDArray from rich.console import Console from rich.prompt import Prompt +from scalecodec import GenericCall from scalecodec.utils.ss58 import ss58_encode, ss58_decode import typer @@ -405,6 +408,15 @@ def is_valid_ss58_address(address: str) -> bool: return False +def is_valid_ss58_address_prompt(text: str) -> str: + valid = False + address = "" + while not valid: + address = Prompt.ask(text).strip() + valid = is_valid_ss58_address(address) + return address + + def is_valid_ed25519_pubkey(public_key: Union[str, bytes]) -> bool: """ Checks if the given public_key is a valid ed25519 key. @@ -807,16 +819,302 @@ def normalize_hyperparameters( return normalized_values +class TableDefinition: + """ + Base class for address book table definitions/functions + """ + + name: str + cols: tuple[tuple[str, str], ...] + + @staticmethod + @contextmanager + def get_db() -> Generator[tuple[sqlite3.Connection, sqlite3.Cursor], None, None]: + """ + Helper function to get a DB connection + """ + with DB() as (conn, cursor): + yield conn, cursor + + @classmethod + def create_if_not_exists(cls, conn: sqlite3.Connection, _: sqlite3.Cursor) -> None: + """ + Creates the table if it doesn't exist. + Args: + conn: sqlite3 connection + _: sqlite3 cursor + """ + columns_ = ", ".join([" ".join(x) for x in cls.cols]) + conn.execute(f"CREATE TABLE IF NOT EXISTS {cls.name} ({columns_})") + conn.commit() + + @classmethod + def read_rows( + cls, + _: sqlite3.Connection, + cursor: sqlite3.Cursor, + include_header: bool = True, + ) -> list[tuple[Union[str, int], ...]]: + """ + Reads rows from a table. + + Args: + _: sqlite3 connection + cursor: sqlite3 cursor + include_header: Whether to include the header row + + Returns: + rows of the table, with column names as the header row if `include_header` is set + + """ + header = tuple(x[0] for x in cls.cols) + cols = ", ".join(header) + cursor.execute(f"SELECT {cols} FROM {cls.name}") + rows = cursor.fetchall() + if not include_header: + return rows + else: + return [header] + rows + + @classmethod + def update_entry(cls, *args, **kwargs): + """ + Updates an existing entry in the table. + """ + raise NotImplementedError() + + @classmethod + def add_entry(cls, *args, **kwargs): + """ + Adds an entry to the table. + """ + raise NotImplementedError() + + @classmethod + def delete_entry(cls, *args, **kwargs): + """ + Deletes an entry from the table. + """ + raise NotImplementedError() + + +class AddressBook(TableDefinition): + name = "address_book" + cols = (("name", "TEXT"), ("ss58_address", "TEXT"), ("note", "TEXT")) + + @classmethod + def add_entry( + cls, + conn: sqlite3.Connection, + _: sqlite3.Cursor, + *, + name: str, + ss58_address: str, + note: str, + ) -> None: + conn.execute( + f"INSERT INTO {cls.name} (name, ss58_address, note) VALUES (?, ?, ?)", + (name, ss58_address, note), + ) + conn.commit() + + @classmethod + def update_entry( + cls, + conn: sqlite3.Connection, + cursor: sqlite3.Cursor, + *, + name: str, + ss58_address: Optional[str] = None, + note: Optional[str] = None, + ): + cursor.execute( + f"SELECT ss58_address, note FROM {cls.name} WHERE name = ?", + (name,), + ) + row = cursor.fetchone()[0] + ss58_address_ = ss58_address or row[0] + note_ = note or row[1] + conn.execute( + f"UPDATE {cls.name} SET ss58_address = ?, note = ? WHERE name = ?", + (ss58_address_, note_, name), + ) + conn.commit() + + @classmethod + def delete_entry( + cls, conn: sqlite3.Connection, cursor: sqlite3.Cursor, *, name: str + ): + conn.execute( + f"DELETE FROM {cls.name} WHERE name = ?", + (name,), + ) + conn.commit() + + +class ProxyAddressBook(TableDefinition): + name = "proxy_address_book" + cols = ( + ("name", "TEXT"), + ("ss58_address", "TEXT"), + ("delay", "INTEGER"), + ("spawner", "TEXT"), + ("proxy_type", "TEXT"), + ("note", "TEXT"), + ) + + @classmethod + def update_entry( + cls, + conn: sqlite3.Connection, + cursor: sqlite3.Cursor, + *, + name: str, + ss58_address: Optional[str] = None, + delay: Optional[int] = None, + spawner: Optional[str] = None, + proxy_type: Optional[str] = None, + note: Optional[str] = None, + ) -> None: + cursor.execute( + f"SELECT ss58_address, spawner, proxy_type, delay, note FROM {cls.name} WHERE name = ?", + (name,), + ) + row = cursor.fetchone()[0] + ss58_address_ = ss58_address or row[0] + spawner_ = spawner or row[1] + proxy_type_ = proxy_type or row[2] + delay = delay if delay is not None else row[3] + note_ = note or row[4] + conn.execute( + f"UPDATE {cls.name} SET ss58_address = ?, spawner = ?, proxy_type = ?, delay = ?, note = ? WHERE name = ?", + (ss58_address_, spawner_, proxy_type_, note_, delay, name), + ) + conn.commit() + + @classmethod + def add_entry( + cls, + conn: sqlite3.Connection, + _: sqlite3.Cursor, + *, + name: str, + ss58_address: str, + delay: int, + spawner: str, + proxy_type: str, + note: str, + ) -> None: + conn.execute( + f"INSERT INTO {cls.name} (name, ss58_address, delay, spawner, proxy_type, note) VALUES (?, ?, ?, ?, ?, ?)", + (name, ss58_address, delay, spawner, proxy_type, note), + ) + conn.commit() + + @classmethod + def delete_entry( + cls, + conn: sqlite3.Connection, + _: sqlite3.Cursor, + *, + name: str, + ): + conn.execute( + f"DELETE FROM {cls.name} WHERE name = ?", + (name,), + ) + conn.commit() + + +class ProxyAnnouncements(TableDefinition): + name = "proxy_announcements" + cols = ( + ("id", "INTEGER PRIMARY KEY"), + ("address", "TEXT"), + ("epoch_time", "INTEGER"), + ("block", "INTEGER"), + ("call_hash", "TEXT"), + ("call", "TEXT"), + ("call_serialized", "TEXT"), + ("executed", "INTEGER"), + ) + + @classmethod + def add_entry( + cls, + conn: sqlite3.Connection, + _: sqlite3.Cursor, + *, + address: str, + epoch_time: int, + block: int, + call_hash: str, + call: GenericCall, + executed: bool = False, + ) -> None: + call_hex = call.data.to_hex() + call_serialized = json.dumps(call.serialize()) + executed_int = int(executed) + conn.execute( + f"INSERT INTO {cls.name} (address, epoch_time, block, call_hash, call, call_serialized, executed)" + " VALUES (?, ?, ?, ?, ?, ?, ?)", + ( + address, + epoch_time, + block, + call_hash, + call_hex, + call_serialized, + executed_int, + ), + ) + conn.commit() + + @classmethod + def delete_entry( + cls, + conn: sqlite3.Connection, + _: sqlite3.Cursor, + *, + address: str, + epoch_time: int, + block: int, + call_hash: str, + ): + conn.execute( + f"DELETE FROM {cls.name} WHERE call_hash = ?, address = ?, epoch_time = ?, block = ?", + (call_hash, address, epoch_time, block), + ) + conn.commit() + + @classmethod + def mark_as_executed(cls, conn: sqlite3.Connection, _: sqlite3.Cursor, idx: int): + conn.execute( + f"UPDATE {cls.name} SET executed = ? WHERE id = ?", + (1, idx), + ) + conn.commit() + + class DB: """ For ease of interaction with the SQLite database used for --reuse-last and --html outputs of tables + + Also for address book """ def __init__( self, - db_path: str = os.path.expanduser("~/.bittensor/bittensor.db"), + db_path: Optional[str] = None, row_factory=None, ): + if db_path is None: + if path_from_env := os.getenv("BTCLI_PROXIES_PATH"): + db_path = path_from_env + else: + db_path = os.path.join( + os.path.expanduser(defaults.config.base_path), "bittensor.db" + ) self.db_path = db_path self.conn: Optional[sqlite3.Connection] = None self.row_factory = row_factory @@ -831,10 +1129,15 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.conn.close() -def create_table(title: str, columns: list[tuple[str, str]], rows: list[list]) -> None: +def create_and_populate_table( + title: str, columns: list[tuple[str, str]], rows: list[list] +) -> None: """ Creates and populates the rows of a table in the SQLite database. + Warning: + Will overwrite the existing table. + :param title: title of the table :param columns: [(column name, column type), ...] :param rows: [[element, element, ...], ...] @@ -854,8 +1157,7 @@ def create_table(title: str, columns: list[tuple[str, str]], rows: list[list]) - conn.commit() columns_ = ", ".join([" ".join(x) for x in columns]) creation_query = f"CREATE TABLE IF NOT EXISTS {title} ({columns_})" - conn.commit() - cursor.execute(creation_query) + conn.execute(creation_query) conn.commit() query = f"INSERT INTO {title} ({', '.join([x[0] for x in columns])}) VALUES ({', '.join(['?'] * len(columns))})" cursor.executemany(query, rows) @@ -1027,6 +1329,17 @@ def render_tree( webbrowser.open(f"file://{output_file}") +def ensure_address_book_tables_exist(): + """ + Creates address book tables if they don't exist. + + Should be run at startup to ensure that the address book tables exist. + """ + with DB() as (conn, cursor): + for table in (AddressBook, ProxyAddressBook, ProxyAnnouncements): + table.create_if_not_exists(conn, cursor) + + def group_subnets(registrations): if not registrations: return "" diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py index 480f6a7fa..fef4d7f65 100644 --- a/bittensor_cli/src/commands/crowd/contribute.py +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -53,6 +53,7 @@ def validate_for_contribution( async def contribute_to_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, amount: Optional[float], prompt: bool, @@ -65,11 +66,13 @@ async def contribute_to_crowdloan( Args: subtensor: SubtensorInterface object for chain interaction wallet: Wallet object containing coldkey for contribution + proxy: Optional proxy to use for this extrinsic crowdloan_id: ID of the crowdloan to contribute to amount: Amount to contribute in TAO (None to prompt) prompt: Whether to prompt for confirmation wait_for_inclusion: Wait for transaction inclusion wait_for_finalization: Wait for transaction finalization + json_output: Whether to output JSON output or human-readable text Returns: tuple[bool, str]: Success status and message @@ -96,7 +99,7 @@ async def contribute_to_crowdloan( print_error(f"[red]{error_message}[/red]") return False, error_message - contributor_address = wallet.coldkeypub.ss58_address + contributor_address = proxy or wallet.coldkeypub.ss58_address current_contribution, user_balance, _ = await asyncio.gather( subtensor.get_crowdloan_contribution(crowdloan_id, contributor_address), subtensor.get_balance(contributor_address), @@ -159,8 +162,13 @@ async def contribute_to_crowdloan( "amount": contribution_amount.rao, }, ) - extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) - updated_balance = user_balance - actual_contribution - extrinsic_fee + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) + if proxy: + updated_balance = user_balance - actual_contribution + else: + updated_balance = user_balance - actual_contribution - extrinsic_fee table = Table( Column("[bold white]Field", style=COLORS.G.SUBHEAD), @@ -243,6 +251,7 @@ async def contribute_to_crowdloan( ) = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -345,6 +354,7 @@ async def contribute_to_crowdloan( async def withdraw_from_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, wait_for_inclusion: bool, wait_for_finalization: bool, @@ -360,10 +370,12 @@ async def withdraw_from_crowdloan( Args: subtensor: SubtensorInterface instance for blockchain interaction wallet: Wallet instance containing the user's keys + proxy: Optional proxy to use with this extrinsic submission. crowdloan_id: The ID of the crowdloan to withdraw from wait_for_inclusion: Whether to wait for transaction inclusion wait_for_finalization: Whether to wait for transaction finalization prompt: Whether to prompt for user confirmation + json_output: Whether to output the results as JSON or human-readable Returns: Tuple of (success, message) indicating the result @@ -390,11 +402,12 @@ async def withdraw_from_crowdloan( print_error(f"[red]{error_msg}[/red]") return False, "Cannot withdraw from finalized crowdloan." + contributor_address = proxy or wallet.coldkeypub.ss58_address user_contribution, user_balance = await asyncio.gather( subtensor.get_crowdloan_contribution( - crowdloan_id, wallet.coldkeypub.ss58_address + crowdloan_id, ), - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance(contributor_address), ) if user_contribution == Balance.from_tao(0): @@ -429,7 +442,9 @@ async def withdraw_from_crowdloan( "crowdloan_id": crowdloan_id, }, ) - extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) await show_crowdloan_details( subtensor=subtensor, crowdloan_id=crowdloan_id, @@ -440,7 +455,10 @@ async def withdraw_from_crowdloan( ) if prompt: - new_balance = user_balance + withdrawable - extrinsic_fee + if proxy: + new_balance = user_balance + withdrawable + else: + new_balance = user_balance + withdrawable - extrinsic_fee new_raised = crowdloan.raised - withdrawable table = Table( Column("[bold white]Field", style=COLORS.G.SUBHEAD), @@ -518,6 +536,7 @@ async def withdraw_from_crowdloan( ) = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -539,10 +558,8 @@ async def withdraw_from_crowdloan( return False, error_message or "Failed to withdraw from crowdloan." new_balance, updated_contribution, updated_crowdloan = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), - subtensor.get_crowdloan_contribution( - crowdloan_id, wallet.coldkeypub.ss58_address - ), + subtensor.get_balance(contributor_address), + subtensor.get_crowdloan_contribution(crowdloan_id, contributor_address), subtensor.get_single_crowdloan(crowdloan_id), ) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 2c2625b2f..cb1ab4558 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -5,6 +5,7 @@ from bittensor_wallet import Wallet from rich.prompt import Confirm, IntPrompt, Prompt, FloatPrompt from rich.table import Table, Column, box +from scalecodec import GenericCall from bittensor_cli.src import COLORS from bittensor_cli.src.commands.crowd.view import show_crowdloan_details @@ -25,6 +26,7 @@ async def create_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], deposit_tao: Optional[int], min_contribution_tao: Optional[int], cap_tao: Optional[int], @@ -53,7 +55,7 @@ async def create_crowdloan( print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message - crowdloan_type = None + crowdloan_type: str if subnet_lease is not None: crowdloan_type = "subnet" if subnet_lease else "fundraising" elif prompt: @@ -125,7 +127,7 @@ async def create_crowdloan( else: print_error(f"[red]{error_msg}[/red]") return False, "Missing required options when prompts are disabled." - + duration = 0 deposit_value = deposit_tao while True: if deposit_value is None: @@ -210,8 +212,8 @@ async def create_crowdloan( break current_block = await subtensor.substrate.get_block_number(None) - call_to_attach = None - + call_to_attach: Optional[GenericCall] + lease_perpetual = None if crowdloan_type == "subnet": target_address = None @@ -266,7 +268,9 @@ async def create_crowdloan( call_to_attach = None - creator_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + creator_balance = await subtensor.get_balance( + proxy or wallet.coldkeypub.ss58_address + ) if deposit > creator_balance: print_error( f"[red]Insufficient balance to cover the deposit. " @@ -289,7 +293,9 @@ async def create_crowdloan( }, ) - extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) if prompt: duration_text = blocks_to_duration(duration) @@ -334,7 +340,9 @@ async def create_crowdloan( table.add_row("Duration", f"[bold]{duration}[/bold] blocks (~{duration_text})") table.add_row("Ends at block", f"[bold]{end_block}[/bold]") table.add_row( - "Estimated fee", f"[{COLORS.P.TAO}]{extrinsic_fee}[/{COLORS.P.TAO}]" + "Estimated fee", + f"[{COLORS.P.TAO}]{extrinsic_fee}[/{COLORS.P.TAO}]" + + (" (paid by real account)" if proxy else ""), ) console.print(table) @@ -352,6 +360,7 @@ async def create_crowdloan( success, error_message, extrinsic_receipt = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -432,6 +441,7 @@ async def create_crowdloan( async def finalize_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, wait_for_inclusion: bool, wait_for_finalization: bool, @@ -449,10 +459,12 @@ async def finalize_crowdloan( Args: subtensor: SubtensorInterface instance for blockchain interaction wallet: Wallet instance containing the user's keys + proxy: Optional proxy to use for this extrinsic submission crowdloan_id: The ID of the crowdloan to finalize wait_for_inclusion: Whether to wait for transaction inclusion wait_for_finalization: Whether to wait for transaction finalization prompt: Whether to prompt for user confirmation + json_output: Whether to output the crowdloan info as JSON or human-readable Returns: Tuple of (success, message) indicating the result @@ -512,7 +524,9 @@ async def finalize_crowdloan( "crowdloan_id": crowdloan_id, }, ) - extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) await show_crowdloan_details( subtensor=subtensor, @@ -558,7 +572,11 @@ async def finalize_crowdloan( else: table.add_row("Call to Execute", "[dim]None[/dim]") - table.add_row("Transaction Fee", str(extrinsic_fee)) + table.add_row( + "Transaction Fee", + f"[{COLORS.S.TAO}]{extrinsic_fee.tao}[/{COLORS.S.TAO}]" + + (" (paid by real account)" if proxy else ""), + ) table.add_section() table.add_row( @@ -601,6 +619,7 @@ async def finalize_crowdloan( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) if not success: diff --git a/bittensor_cli/src/commands/crowd/dissolve.py b/bittensor_cli/src/commands/crowd/dissolve.py index b7513fb19..ce3f6c145 100644 --- a/bittensor_cli/src/commands/crowd/dissolve.py +++ b/bittensor_cli/src/commands/crowd/dissolve.py @@ -1,5 +1,6 @@ import asyncio import json +from typing import Optional from bittensor_wallet import Wallet from rich.prompt import Confirm @@ -21,6 +22,7 @@ async def dissolve_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -35,10 +37,12 @@ async def dissolve_crowdloan( Args: subtensor: SubtensorInterface object for chain interaction. wallet: Wallet object containing the creator's coldkey. + proxy: Optional proxy to use for this extrinsic submission crowdloan_id: ID of the crowdloan to dissolve. wait_for_inclusion: Wait for transaction inclusion. wait_for_finalization: Wait for transaction finalization. prompt: Whether to prompt for confirmation. + json_output: Whether to output the results as JSON or human-readable. Returns: tuple[bool, str]: Success status and message. @@ -172,6 +176,7 @@ async def dissolve_crowdloan( ) = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor_cli/src/commands/crowd/refund.py b/bittensor_cli/src/commands/crowd/refund.py index d08f91291..be264b3de 100644 --- a/bittensor_cli/src/commands/crowd/refund.py +++ b/bittensor_cli/src/commands/crowd/refund.py @@ -1,5 +1,6 @@ import asyncio import json +from typing import Optional from bittensor_wallet import Wallet from rich.prompt import Confirm @@ -22,6 +23,7 @@ async def refund_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -40,10 +42,12 @@ async def refund_crowdloan( Args: subtensor: SubtensorInterface object for chain interaction wallet: Wallet object containing coldkey (any wallet can call this) + proxy: Optional proxy to use for extrinsic submission crowdloan_id: ID of the crowdloan to refund wait_for_inclusion: Wait for transaction inclusion wait_for_finalization: Wait for transaction finalization prompt: Whether to prompt for confirmation + json_output: Whether to output as JSON or human-readable Returns: tuple[bool, str]: Success status and message @@ -183,6 +187,7 @@ async def refund_crowdloan( ) = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor_cli/src/commands/crowd/update.py b/bittensor_cli/src/commands/crowd/update.py index 2b2ee04f1..39ac69c64 100644 --- a/bittensor_cli/src/commands/crowd/update.py +++ b/bittensor_cli/src/commands/crowd/update.py @@ -24,6 +24,7 @@ async def update_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, min_contribution: Optional[Balance] = None, end: Optional[int] = None, @@ -38,6 +39,7 @@ async def update_crowdloan( Args: subtensor: SubtensorInterface object for chain interaction wallet: Wallet object containing coldkey (must be creator) + proxy: Optional proxy to use for this extrinsic submissions crowdloan_id: ID of the crowdloan to update min_contribution: New minimum contribution in TAO (None to prompt) end: New end block (None to prompt) @@ -45,6 +47,7 @@ async def update_crowdloan( wait_for_inclusion: Wait for transaction inclusion wait_for_finalization: Wait for transaction finalization prompt: Whether to prompt for values + json_output: Whether to output JSON or human-readable Returns: tuple[bool, str]: Success status and message @@ -368,6 +371,7 @@ async def update_crowdloan( ) = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index a262e8874..48383e605 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -33,6 +33,7 @@ async def add_liquidity_extrinsic( wallet: "Wallet", hotkey_ss58: str, netuid: int, + proxy: Optional[str], liquidity: Balance, price_low: Balance, price_high: Balance, @@ -47,6 +48,7 @@ async def add_liquidity_extrinsic( wallet: The wallet used to sign the extrinsic (must be unlocked). hotkey_ss58: the SS58 of the hotkey to use for this transaction. netuid: The UID of the target subnet for which the call is being initiated. + proxy: Optional proxy to use for this extrinsic submission. liquidity: The amount of liquidity to be added. price_low: The lower bound of the price tick range. price_high: The upper bound of the price tick range. @@ -54,9 +56,10 @@ async def add_liquidity_extrinsic( wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. Returns: - Tuple[bool, str]: - - True and a success message if the extrinsic is successfully submitted or processed. - - False and an error message if the submission fails or the wallet cannot be unlocked. + tuple: + bool: True if successful, False otherwise. + str: success message if successful, error message otherwise. + AsyncExtrinsicReceipt: extrinsic receipt if successful, None otherwise. Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. @@ -82,6 +85,7 @@ async def add_liquidity_extrinsic( return await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -92,6 +96,7 @@ async def modify_liquidity_extrinsic( wallet: "Wallet", hotkey_ss58: str, netuid: int, + proxy: Optional[str], position_id: int, liquidity_delta: Balance, wait_for_inclusion: bool = True, @@ -104,15 +109,17 @@ async def modify_liquidity_extrinsic( wallet: The wallet used to sign the extrinsic (must be unlocked). hotkey_ss58: the SS58 of the hotkey to use for this transaction. netuid: The UID of the target subnet for which the call is being initiated. + proxy: Optional proxy to use for this extrinsic submission. position_id: The id of the position record in the pool. liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. Returns: - Tuple[bool, str]: - - True and a success message if the extrinsic is successfully submitted or processed. - - False and an error message if the submission fails or the wallet cannot be unlocked. + tuple: + bool: True if successful, False otherwise. + str: success message if successful, error message otherwise. + AsyncExtrinsicReceipt: extrinsic receipt if successful, None otherwise. Note: Modifying is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. @@ -134,6 +141,7 @@ async def modify_liquidity_extrinsic( return await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -143,6 +151,7 @@ async def remove_liquidity_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", hotkey_ss58: str, + proxy: Optional[str], netuid: int, position_id: int, wait_for_inclusion: bool = True, @@ -154,15 +163,17 @@ async def remove_liquidity_extrinsic( subtensor: The Subtensor client instance used for blockchain interaction. wallet: The wallet used to sign the extrinsic (must be unlocked). hotkey_ss58: the SS58 of the hotkey to use for this transaction. + proxy: Optional proxy to use for this extrinsic submission. netuid: The UID of the target subnet for which the call is being initiated. position_id: The id of the position record in the pool. wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. Returns: - Tuple[bool, str]: - - True and a success message if the extrinsic is successfully submitted or processed. - - False and an error message if the submission fails or the wallet cannot be unlocked. + tuple: + bool: True if successful, False otherwise. + str: success message if successful, error message otherwise. + AsyncExtrinsicReceipt: extrinsic receipt if successful, None otherwise. Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. @@ -183,6 +194,7 @@ async def remove_liquidity_extrinsic( return await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -234,6 +246,7 @@ async def add_liquidity( wallet: "Wallet", hotkey_ss58: str, netuid: Optional[int], + proxy: Optional[str], liquidity: Balance, price_low: Balance, price_high: Balance, @@ -267,17 +280,23 @@ async def add_liquidity( wallet=wallet, hotkey_ss58=hotkey_ss58, netuid=netuid, + proxy=proxy, liquidity=liquidity, price_low=price_low, price_high=price_high, ) - await print_extrinsic_id(ext_receipt) - ext_id = await ext_receipt.get_extrinsic_identifier() + if success: + await print_extrinsic_id(ext_receipt) + ext_id = await ext_receipt.get_extrinsic_identifier() + else: + ext_id = None if json_output: - json_console.print( - json.dumps( - {"success": success, "message": message, "extrinsic_identifier": ext_id} - ) + json_console.print_json( + data={ + "success": success, + "message": message, + "extrinsic_identifier": ext_id, + } ) else: if success: @@ -545,6 +564,7 @@ async def remove_liquidity( wallet: "Wallet", hotkey_ss58: str, netuid: int, + proxy: Optional[str], position_id: Optional[int] = None, prompt: Optional[bool] = None, all_liquidity_ids: Optional[bool] = None, @@ -579,12 +599,14 @@ async def remove_liquidity( if not Confirm.ask("Would you like to continue?"): return None + # TODO does this never break because of the nonce? results = await asyncio.gather( *[ remove_liquidity_extrinsic( subtensor=subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, + proxy=proxy, netuid=netuid, position_id=pos_id, ) @@ -615,6 +637,7 @@ async def modify_liquidity( wallet: "Wallet", hotkey_ss58: str, netuid: int, + proxy: Optional[str], position_id: int, liquidity_delta: Balance, prompt: Optional[bool] = None, @@ -646,6 +669,7 @@ async def modify_liquidity( wallet=wallet, hotkey_ss58=hotkey_ss58, netuid=netuid, + proxy=proxy, position_id=position_id, liquidity_delta=liquidity_delta, ) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py new file mode 100644 index 000000000..580636881 --- /dev/null +++ b/bittensor_cli/src/commands/proxy.py @@ -0,0 +1,634 @@ +from typing import TYPE_CHECKING, Optional +import sys + +from rich.prompt import Confirm, Prompt, FloatPrompt, IntPrompt +from scalecodec import GenericCall, ScaleBytes + +from bittensor_cli.src import COLORS +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.utils import ( + print_extrinsic_id, + json_console, + console, + err_console, + unlock_key, + ProxyAddressBook, + is_valid_ss58_address_prompt, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + from bittensor_wallet.bittensor_wallet import Wallet + + +# TODO when 3.10 support is dropped in Oct 2026, remove this +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from enum import Enum + + class StrEnum(str, Enum): + pass + + +class ProxyType(StrEnum): + Any = "Any" + Owner = "Owner" + NonCritical = "NonCritical" + NonTransfer = "NonTransfer" + Senate = "Senate" + NonFungible = "NonFungible" + Triumvirate = "Triumvirate" + Governance = "Governance" + Staking = "Staking" + Registration = "Registration" + Transfer = "Transfer" + SmallTransfer = "SmallTransfer" + RootWeights = "RootWeights" + ChildKeys = "ChildKeys" + SudoUncheckedSetCode = "SudoUncheckedSetCode" + SwapHotkey = "SwapHotkey" + SubnetLeaseBeneficiary = "SubnetLeaseBeneficiary" + RootClaim = "RootClaim" + + +# TODO add announce with also --reject and --remove + + +async def submit_proxy( + subtensor: "SubtensorInterface", + wallet: "Wallet", + call: GenericCall, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, + proxy: Optional[str] = None, + announce_only: bool = False, +) -> None: + """ + Submits the prepared call to the chain + + Returns: + None, prints out the result according to `json_output` flag. + + """ + success, msg, receipt = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + era={"period": period}, + proxy=proxy, + announce_only=announce_only, + ) + if success: + await print_extrinsic_id(receipt) + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_identifier": await receipt.get_extrinsic_identifier(), + } + ) + else: + console.print(":white_check_mark:[green]Success![/green]") + + else: + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_identifier": None, + } + ) + else: + console.print(":white_check_mark:[green]Success![/green]") + + +async def create_proxy( + subtensor: "SubtensorInterface", + wallet: "Wallet", + proxy_type: ProxyType, + delay: int, + idx: int, + prompt: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, +) -> None: + """ + Executes the create pure proxy call on the chain + """ + if prompt: + if not Confirm.ask( + f"This will create a Pure Proxy of type {proxy_type.value}. Do you want to proceed?", + ): + return None + if delay > 0: + if not Confirm.ask( + f"By adding a non-zero delay ({delay}), all proxy calls must be announced " + f"{delay} blocks before they will be able to be made. Continue?" + ): + return None + if not (ulw := unlock_key(wallet, print_out=not json_output)).success: + if not json_output: + err_console.print(ulw.message) + else: + json_console.print_json( + data={ + "success": ulw.success, + "message": ulw.message, + "extrinsic_identifier": None, + } + ) + return None + call = await subtensor.substrate.compose_call( + call_module="Proxy", + call_function="create_pure", + call_params={"proxy_type": proxy_type.value, "delay": delay, "index": idx}, + ) + success, msg, receipt = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + era={"period": period}, + nonce=await subtensor.substrate.get_account_next_index( + wallet.coldkeypub.ss58_address + ), + ) + if success: + await print_extrinsic_id(receipt) + created_pure = None + created_spawner = None + created_proxy_type = None + for event in await receipt.triggered_events: + if event["event_id"] == "PureCreated": + attrs = event["attributes"] + created_pure = attrs["pure"] + created_spawner = attrs["who"] + created_proxy_type = getattr(ProxyType, attrs["proxy_type"]) + arg_start = "`" if json_output else "[blue]" + arg_end = "`" if json_output else "[/blue]" + msg = ( + f"Created pure '{created_pure}' " + f"from spawner '{created_spawner}' " + f"with proxy type '{created_proxy_type.value}' " + f"with delay {delay}." + ) + console.print(msg) + if not prompt: + console.print( + f" You can add this to your config with {arg_start}" + f"btcli config add-proxy " + f"--name --address {created_pure} --proxy-type {created_proxy_type.value} " + f"--delay {delay} --spawner {created_spawner}" + f"{arg_end}" + ) + else: + if Confirm.ask("Would you like to add this to your address book?"): + proxy_name = Prompt.ask("Name this proxy") + note = Prompt.ask("[Optional] Add a note for this proxy", default="") + with ProxyAddressBook.get_db() as (conn, cursor): + ProxyAddressBook.add_entry( + conn, + cursor, + name=proxy_name, + ss58_address=created_pure, + delay=delay, + proxy_type=created_proxy_type.value, + note=note, + spawner=created_spawner, + ) + console.print( + f"Added to Proxy Address Book.\n" + f"Show this information with [{COLORS.G.ARG}]btcli config proxies[/{COLORS.G.ARG}]" + ) + return None + + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "data": { + "pure": created_pure, + "spawner": created_spawner, + "proxy_type": created_proxy_type.value, + "delay": delay, + }, + "extrinsic_identifier": await receipt.get_extrinsic_identifier(), + } + ) + + else: + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "data": None, + "extrinsic_identifier": None, + } + ) + else: + err_console.print(f":cross_mark:[red]Failed to create pure proxy: {msg}") + return None + + +async def remove_proxy( + subtensor: "SubtensorInterface", + wallet: "Wallet", + proxy_type: ProxyType, + delegate: str, + delay: int, + prompt: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, +) -> None: + """ + Executes the remove proxy call on the chain + """ + if prompt: + if not Confirm.ask( + f"This will remove a proxy of type {proxy_type.value} for delegate {delegate}." + f"Do you want to proceed?" + ): + return None + if not (ulw := unlock_key(wallet, print_out=not json_output)).success: + if not json_output: + err_console.print(ulw.message) + else: + json_console.print_json( + data={ + "success": ulw.success, + "message": ulw.message, + "extrinsic_identifier": None, + } + ) + return None + call = await subtensor.substrate.compose_call( + call_module="Proxy", + call_function="remove_proxy", + call_params={ + "proxy_type": proxy_type.value, + "delay": delay, + "delegate": delegate, + }, + ) + return await submit_proxy( + subtensor=subtensor, + wallet=wallet, + call=call, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + json_output=json_output, + ) + + +async def add_proxy( + subtensor: "SubtensorInterface", + wallet: "Wallet", + proxy_type: ProxyType, + delegate: str, + delay: int, + prompt: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, +): + """ + Executes the add proxy call on the chain + """ + if prompt: + if not Confirm.ask( + f"This will add a proxy of type {proxy_type.value} for delegate {delegate}." + f"Do you want to proceed?" + ): + return None + if delay > 0: + if not Confirm.ask( + f"By adding a non-zero delay ({delay}), all proxy calls must be announced " + f"{delay} blocks before they will be able to be made. Continue?" + ): + return None + if not (ulw := unlock_key(wallet, print_out=not json_output)).success: + if not json_output: + err_console.print(ulw.message) + else: + json_console.print_json( + data={ + "success": ulw.success, + "message": ulw.message, + "extrinsic_identifier": None, + } + ) + return None + call = await subtensor.substrate.compose_call( + call_module="Proxy", + call_function="add_proxy", + call_params={ + "proxy_type": proxy_type.value, + "delay": delay, + "delegate": delegate, + }, + ) + success, msg, receipt = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + era={"period": period}, + ) + if success: + await print_extrinsic_id(receipt) + delegatee = None + delegator = None + created_proxy_type = None + for event in await receipt.triggered_events: + if event["event_id"] == "ProxyAdded": + attrs = event["attributes"] + delegatee = attrs["delegatee"] + delegator = attrs["delegator"] + created_proxy_type = getattr(ProxyType, attrs["proxy_type"]) + break + arg_start = "`" if json_output else "[blue]" + arg_end = "`" if json_output else "[/blue]" + msg = ( + f"Added proxy delegatee '{delegatee}' " + f"from delegator '{delegator}' " + f"with proxy type '{created_proxy_type.value}' " + f"with delay {delay}." + ) + console.print(msg) + if not prompt: + console.print( + f" You can add this to your config with {arg_start}" + f"btcli config add-proxy " + f"--name --address {delegatee} --proxy-type {created_proxy_type.value} --delegator " + f"{delegator} --delay {delay}" + f"{arg_end}" + ) + else: + if Confirm.ask("Would you like to add this to your address book?"): + proxy_name = Prompt.ask("Name this proxy") + note = Prompt.ask("[Optional] Add a note for this proxy", default="") + with ProxyAddressBook.get_db() as (conn, cursor): + ProxyAddressBook.add_entry( + conn, + cursor, + name=proxy_name, + ss58_address=delegator, + delay=delay, + proxy_type=created_proxy_type.value, + note=note, + spawner=delegatee, + ) + console.print( + f"Added to Proxy Address Book.\n" + f"Show this information with [{COLORS.G.ARG}]btcli config proxies[/{COLORS.G.ARG}]" + ) + + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "data": { + "delegatee": delegatee, + "delegator": delegator, + "proxy_type": created_proxy_type.value, + "delay": delay, + }, + "extrinsic_identifier": await receipt.get_extrinsic_identifier(), + } + ) + + else: + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "data": None, + "extrinsic_identifier": None, + } + ) + else: + err_console.print(f":cross_mark:[red]Failed to add proxy: {msg}") + return None + + +async def kill_proxy( + subtensor: "SubtensorInterface", + wallet: "Wallet", + proxy_type: ProxyType, + height: int, + ext_index: int, + spawner: Optional[str], + idx: int, + proxy: Optional[str], + announce_only: bool, + prompt: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, +) -> None: + """ + Executes the pure proxy kill call on the chain + """ + if prompt: + confirmation = Prompt.ask( + f"This will kill a Pure Proxy account of type {proxy_type.value}.\n" + f"[red]All access to this account will be lost. Any funds held in it will be inaccessible.[/red]" + f"To proceed, enter [red]KILL[/red]" + ) + if confirmation != "KILL": + err_console.print("Invalid input. Exiting.") + return None + if not (ulw := unlock_key(wallet, print_out=not json_output)).success: + if not json_output: + err_console.print(ulw.message) + else: + json_console.print_json( + data={ + "success": ulw.success, + "message": ulw.message, + "extrinsic_identifier": None, + } + ) + return None + spawner = spawner or wallet.coldkeypub.ss58_address + call = await subtensor.substrate.compose_call( + call_module="Proxy", + call_function="kill_pure", + call_params={ + "proxy_type": proxy_type.value, + "index": idx, + "height": height, + "ext_index": ext_index, + "spawner": spawner, + }, + ) + return await submit_proxy( + subtensor=subtensor, + wallet=wallet, + call=call, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + json_output=json_output, + proxy=proxy, + announce_only=announce_only, + ) + + +async def execute_announced( + subtensor: "SubtensorInterface", + wallet: "Wallet", + delegate: str, + real: str, + period: int, + call_hex: Optional[str], + delay: int = 0, + created_block: Optional[int] = None, + prompt: bool = True, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, + json_output: bool = False, +) -> bool: + """ + Executes the previously-announced call on the chain. + + Returns: + True if the submission was successful, False otherwise. + + """ + if prompt and created_block is not None: + current_block = await subtensor.substrate.get_block_number() + if current_block - delay > created_block: + if not Confirm.ask( + f"The delay for this account is set to {delay} blocks, but the call was created" + f" at block {created_block}. It is currently only {current_block}. The call will likely fail." + f" Do you want to proceed?" + ): + return False + + if call_hex is None: + if not prompt: + err_console.print( + f":cross_mark:[red]You have not provided a call, and are using" + f" [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}], so we are unable to request" + f"the information to craft this call." + ) + return False + else: + call_args = {} + failure_ = f"Instead create the call using btcli commands with [{COLORS.G.ARG}]--announce-only[/{COLORS.G.ARG}]" + block_hash = await subtensor.substrate.get_chain_head() + fns = await subtensor.substrate.get_metadata_call_functions( + block_hash=block_hash + ) + module = Prompt.ask( + "Enter the module name for the call", + choices=list(fns.keys()), + show_choices=True, + ) + call_fn = Prompt.ask( + "Enter the call function for the call", + choices=list(fns[module].keys()), + show_choices=True, + ) + for arg in fns[module][call_fn].keys(): + type_name = fns[module][call_fn][arg]["typeName"] + if type_name == "AccountIdLookupOf": + value = is_valid_ss58_address_prompt( + f"Enter the SS58 Address for {arg}" + ) + elif type_name == "T::Balance": + value = FloatPrompt.ask(f"Enter the amount of Tao for {arg}") + value = Balance.from_tao(value) + elif "RuntimeCall" in type_name: + err_console.print( + f":cross_mark:[red]Unable to craft a Call Type for arg {arg}. {failure_}" + ) + return False + elif type_name == "NetUid": + value = IntPrompt.ask(f"Enter the netuid for {arg}") + elif type_name in ("u16", "u64"): + value = IntPrompt.ask(f"Enter the int value for {arg}") + elif type_name == "bool": + value = Prompt.ask( + f"Enter the bool value for {arg}", + choices=["True", "False"], + show_choices=True, + ) + if value == "True": + value = True + else: + value = False + else: + err_console.print( + f":cross_mark:[red]Unrecognized type name {type_name}. {failure_}" + ) + return False + call_args[arg] = value + inner_call = await subtensor.substrate.compose_call( + module, + call_fn, + call_params=call_args, + block_hash=block_hash, + ) + else: + runtime = await subtensor.substrate.init_runtime(block_id=created_block) + inner_call = GenericCall( + data=ScaleBytes(data=bytearray.fromhex(call_hex)), metadata=runtime.metadata + ) + inner_call.process() + + announced_call = await subtensor.substrate.compose_call( + "Proxy", + "proxy_announced", + { + "delegate": delegate, + "real": real, + "call": inner_call, + "force_proxy_type": None, + }, + ) + success, msg, receipt = await subtensor.sign_and_send_extrinsic( + call=announced_call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + era={"period": period}, + ) + if success is True: + if json_output: + json_console.print_json( + data={ + "success": True, + "message": msg, + "extrinsic_identifier": await receipt.get_extrinsic_identifier(), + } + ) + else: + console.print(":white_check_mark:[green]Success![/green]") + await print_extrinsic_id(receipt) + else: + if json_output: + json_console.print_json( + data={"success": False, "message": msg, "extrinsic_identifier": None} + ) + else: + err_console.print(f":cross_mark:[red]Failed[/red]. {msg} ") + return success diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index d88692261..36e7ad7f3 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -8,7 +8,6 @@ from rich.table import Table from rich.prompt import Confirm, Prompt -from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( @@ -19,7 +18,6 @@ from bittensor_cli.src.bittensor.utils import ( console, err_console, - format_error_message, get_hotkey_wallets_for_wallet, is_valid_ss58_address, print_error, @@ -52,6 +50,7 @@ async def stake_add( json_output: bool, era: int, mev_protection: bool, + proxy: Optional[str], ): """ Args: @@ -69,6 +68,8 @@ async def stake_add( 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. + proxy: Optional proxy to use for staking. + mev_protection: If true, will encrypt the extrinsic behind the mev protection shield. Returns: bool: True if stake operation is successful, False otherwise @@ -111,7 +112,7 @@ async def get_stake_extrinsic_fee( call_function=call_fn, call_params=call_params, ) - return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy) async def safe_stake_extrinsic( netuid_: int, @@ -119,15 +120,15 @@ async def safe_stake_extrinsic( current_stake: Balance, hotkey_ss58_: str, price_limit: Balance, - status=None, + status_=None, ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - err_out = partial(print_error, status=status) + err_out = partial(print_error, status=status_) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount_} on Netuid {netuid_}" ) 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.get_balance(coldkey_ss58), + subtensor.substrate.get_account_next_index(coldkey_ss58), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake_limit", @@ -142,95 +143,88 @@ async def safe_stake_extrinsic( ) if mev_protection: call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, - keypair=wallet.coldkey, + wallet=wallet, nonce=next_nonce, era={"period": era}, + proxy=proxy, ) - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - except SubstrateRequestException as e: - if "Custom error: 8" in str(e): + if not success_: + if "Custom error: 8" in err_msg: err_msg = ( f"{failure_prelude}: Price exceeded tolerance limit. " f"Transaction rejected because partial staking is disabled. " f"Either increase price tolerance or enable partial staking." ) - print_error("\n" + err_msg, status=status) + print_error("\n" + err_msg, status=status_) else: - err_msg = f"{failure_prelude} with error: {format_error_message(e)}" + err_msg = f"{failure_prelude} with error: {err_msg}" err_out("\n" + err_msg) return False, err_msg, None - if not await response.is_success: - err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" - err_out("\n" + err_msg) - return False, err_msg, None + else: + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status_ + ) + if not mev_success: + status_.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None + if json_output: + # the rest of this checking is not necessary if using json_output + return True, "", response + await print_extrinsic_id(response) + block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance(coldkey_ss58, block_hash), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58_, + coldkey_ss58=coldkey_ss58, + 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}" + ) - if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status + 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}" + f"[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"instead of " + f"[blue]{amount_}[/blue]" ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None - - if json_output: - # the rest of this checking is not necessary if using json_output - return True, "", response - await print_extrinsic_id(response) - 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( - "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]" + 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" ) - - 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, "", response + return True, "", response async def stake_extrinsic( - netuid_i, amount_, current, staking_address_ss58, status=None + netuid_i, amount_, current, staking_address_ss58, status_=None ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - err_out = partial(print_error, status=status) + err_out = partial(print_error, status=status_) + block_hash = await subtensor.substrate.get_chain_head() 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.get_balance(coldkey_ss58, block_hash=block_hash), + subtensor.substrate.get_account_next_index(coldkey_ss58), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake", @@ -239,6 +233,7 @@ async def stake_extrinsic( "netuid": netuid_i, "amount_staked": amount_.rao, }, + block_hash=block_hash, ), ) failure_prelude = ( @@ -246,71 +241,67 @@ async def stake_extrinsic( ) if mev_protection: call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + nonce=next_nonce, + era={"period": era}, + proxy=proxy, ) - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - except SubstrateRequestException as e: - err_msg = f"{failure_prelude} with error: {format_error_message(e)}" - err_out("\n" + err_msg) - return False, err_msg, None - if not await response.is_success: - err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" + if not success_: + err_msg = f"{failure_prelude} with error: {err_msg}" err_out("\n" + err_msg) return False, err_msg, None - - if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None - - if json_output: - # the rest of this is not necessary if using json_output + else: + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status_ + ) + if not mev_success: + status_.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None + if json_output: + # the rest of this is not necessary if using json_output + return True, "", response + await print_extrinsic_id(response) + new_block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + 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: " + 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: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + 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, "", response - await print_extrinsic_id(response) - new_block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - 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: " - 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: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - console.print( - 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, "", response netuids = ( netuids if netuids is not None else await subtensor.get_all_subnet_netuids() ) + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address hotkeys_to_stake_to = _get_hotkeys_to_stake_to( wallet=wallet, @@ -324,10 +315,10 @@ async def stake_extrinsic( _all_subnets, _stake_info, current_wallet_balance = await asyncio.gather( subtensor.all_subnets(block_hash=chain_head), subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, block_hash=chain_head, ), - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=chain_head), + subtensor.get_balance(coldkey_ss58, block_hash=chain_head), ) all_subnets = {di.netuid: di for di in _all_subnets} @@ -436,10 +427,13 @@ async def stake_extrinsic( ) row_extension = [] # TODO this should be asyncio gathered before the for loop + amount_minus_fee = ( + (amount_to_stake - extrinsic_fee) if not proxy else amount_to_stake + ) sim_swap = await subtensor.sim_swap( origin_netuid=0, destination_netuid=netuid, - amount=(amount_to_stake - extrinsic_fee).rao, + amount=amount_minus_fee.rao, ) received_amount = sim_swap.alpha_amount # Add rows for the table @@ -490,7 +484,7 @@ async def stake_extrinsic( amount_=am, current=curr, staking_address_ss58=staking_address, - status=status, + status_=status, ) else: stake_coroutines[(ni, staking_address)] = safe_stake_extrinsic( @@ -499,7 +493,7 @@ async def stake_extrinsic( current_stake=curr, hotkey_ss58_=staking_address, price_limit=price_with_tolerance, - status=status, + status_=status, ) else: stake_coroutines = { @@ -508,7 +502,7 @@ async def stake_extrinsic( amount_=am, current=curr, staking_address_ss58=staking_address, - status=status, + status_=status, ) for i, (ni, am, curr) in enumerate( zip(netuids, amounts_to_stake, current_stake_balances) diff --git a/bittensor_cli/src/commands/stake/auto_staking.py b/bittensor_cli/src/commands/stake/auto_staking.py index 6e8bf3632..d917cc439 100644 --- a/bittensor_cli/src/commands/stake/auto_staking.py +++ b/bittensor_cli/src/commands/stake/auto_staking.py @@ -174,6 +174,7 @@ async def set_auto_stake_destination( subtensor: "SubtensorInterface", netuid: int, hotkey_ss58: str, + proxy: Optional[str] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt_user: bool = True, @@ -269,6 +270,7 @@ async def set_auto_stake_destination( wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) ext_id = await ext_receipt.get_extrinsic_identifier() if success else None diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index d50ecc65a..eda0b53b4 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -56,6 +56,7 @@ async def set_children_extrinsic( wallet: Wallet, hotkey: str, netuid: int, + proxy: Optional[str], children_with_proportions: list[tuple[float, str]], wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -122,7 +123,7 @@ async def set_children_extrinsic( }, ) success, error_message, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization, proxy=proxy ) if not wait_for_finalization and not wait_for_inclusion: @@ -151,6 +152,7 @@ async def set_childkey_take_extrinsic( hotkey: str, netuid: int, take: float, + proxy: Optional[str] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = True, @@ -163,6 +165,7 @@ async def set_childkey_take_extrinsic( :param: hotkey: Child hotkey. :param: take: Childkey Take value. :param: netuid: Unique identifier of for the subnet. + :param: proxy: Optional proxy to use to make this extrinsic submission. :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. :param: wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning ` @@ -206,7 +209,7 @@ async def set_childkey_take_extrinsic( error_message, ext_receipt, ) = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization, proxy=proxy ) if not wait_for_finalization and not wait_for_inclusion: @@ -223,17 +226,9 @@ async def set_childkey_take_extrinsic( if wait_for_finalization: modifier = "finalized" console.print(":white_heavy_check_mark: [green]Finalized[/green]") - # bittensor.logging.success( - # prefix="Setting childkey take", - # suffix="Finalized: " + str(success), - # ) return True, f"Successfully {modifier} childkey take", ext_id else: console.print(f":cross_mark: [red]Failed[/red]: {error_message}") - # bittensor.logging.warning( - # prefix="Setting childkey take", - # suffix="Failed: " + str(error_message), - # ) return False, error_message, None except SubstrateRequestException as e: @@ -509,8 +504,10 @@ async def set_children( wait_for_finalization: bool = True, prompt: bool = True, json_output: bool = False, + proxy: Optional[str] = None, ): """Set children hotkeys.""" + # TODO holy shit I hate this. It needs to be rewritten. # Validate children SS58 addresses # TODO check to see if this should be allowed to be specified by user instead of pulling from wallet hotkey = get_hotkey_pub_ss58(wallet) @@ -536,6 +533,7 @@ async def set_children( wallet=wallet, netuid=netuid, hotkey=hotkey, + proxy=proxy, children_with_proportions=children_with_proportions, prompt=prompt, wait_for_inclusion=wait_for_inclusion, @@ -579,6 +577,7 @@ async def set_children( wallet=wallet, netuid=netuid_, hotkey=hotkey, + proxy=proxy, children_with_proportions=children_with_proportions, prompt=prompt, wait_for_inclusion=True, @@ -609,6 +608,7 @@ async def revoke_children( wallet: Wallet, subtensor: "SubtensorInterface", netuid: Optional[int] = None, + proxy: Optional[str] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, @@ -625,6 +625,7 @@ async def revoke_children( netuid=netuid, hotkey=get_hotkey_pub_ss58(wallet), children_with_proportions=[], + proxy=proxy, prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -665,6 +666,7 @@ async def revoke_children( netuid=netuid, # TODO should this be able to allow netuid = None ? hotkey=get_hotkey_pub_ss58(wallet), children_with_proportions=[], + proxy=proxy, prompt=prompt, wait_for_inclusion=True, wait_for_finalization=False, @@ -701,6 +703,7 @@ async def childkey_take( take: Optional[float], hotkey: Optional[str] = None, netuid: Optional[int] = None, + proxy: Optional[str] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, @@ -761,28 +764,29 @@ async def set_chk_take_subnet( subnet: int, chk_take: float ) -> tuple[bool, Optional[str]]: """Set the childkey take for a single subnet""" - success, message, ext_id = await set_childkey_take_extrinsic( + success_, message, ext_id_ = await set_childkey_take_extrinsic( subtensor=subtensor, wallet=wallet, netuid=subnet, hotkey=get_hotkey_pub_ss58(wallet), take=chk_take, + proxy=proxy, prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) # Result - if success: + if success_: console.print(":white_heavy_check_mark: [green]Set childkey take.[/green]") console.print( f"The childkey take for {get_hotkey_pub_ss58(wallet)} is now set to {take * 100:.2f}%." ) - return True, ext_id + return True, ext_id_ else: console.print( f":cross_mark:[red] Unable to set childkey take.[/red] {message}" ) - return False, ext_id + return False, ext_id_ # Print childkey take for other user and return (dont offer to change take rate) wallet_hk = get_hotkey_pub_ss58(wallet) @@ -847,6 +851,7 @@ async def set_chk_take_subnet( netuid=netuid_, hotkey=wallet_hk, take=take, + proxy=proxy, prompt=prompt, wait_for_inclusion=True, wait_for_finalization=False, diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index daf2aae62..2648ad926 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -1,5 +1,6 @@ import asyncio import json +from enum import Enum from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet @@ -25,10 +26,16 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +class ClaimType(Enum): + Keep = "Keep" + Swap = "Swap" + + async def set_claim_type( wallet: Wallet, subtensor: "SubtensorInterface", - claim_type: Optional[str] = None, + claim_type: Optional[ClaimType], + proxy: Optional[str], netuids: Optional[str] = None, prompt: bool = True, json_output: bool = False, @@ -44,7 +51,8 @@ async def set_claim_type( Args: wallet: Bittensor wallet object subtensor: SubtensorInterface object - claim_type: Optional claim type ("Keep" or "Swap"). If None, user will be prompted. + claim_type: Claim type ("Keep" or "Swap"). If omitted, user will be prompted. + proxy: Optional proxy to use with this extrinsic submission. netuids: Optional string of subnet IDs (e.g., "1-5,10,20-30"). Will be parsed internally. prompt: Whether to prompt for user confirmation json_output: Whether to output JSON @@ -57,13 +65,7 @@ async def set_claim_type( """ if claim_type is not None: - claim_type = claim_type.capitalize() - if claim_type not in ["Keep", "Swap"]: - msg = f"Invalid claim type: {claim_type}. Use 'Keep' or 'Swap', or omit for interactive mode." - err_console.print(f"[red]{msg}[/red]") - if json_output: - json_console.print(json.dumps({"success": False, "message": msg})) - return False, msg, None + claim_type = claim_type.value current_claim_info, all_netuids = await asyncio.gather( subtensor.get_coldkey_claim_type(coldkey_ss58=wallet.coldkeypub.ss58_address), @@ -193,7 +195,7 @@ async def set_claim_type( call_params={"new_root_claim_type": claim_type_param}, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) if success: @@ -224,6 +226,7 @@ async def process_pending_claims( wallet: Wallet, subtensor: "SubtensorInterface", netuids: Optional[list[int]] = None, + proxy: Optional[str] = None, prompt: bool = True, json_output: bool = False, verbose: bool = False, @@ -325,8 +328,13 @@ async def process_pending_claims( call_function="claim_root", call_params={"subnets": selected_netuids}, ) - extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) - console.print(f"\n[dim]Estimated extrinsic fee: {extrinsic_fee.tao:.9f} τ[/dim]") + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) + console.print( + f"\n[dim]Estimated extrinsic fee: {extrinsic_fee.tao:.9f} τ" + + (" (paid by real account)" if proxy else "") + ) if prompt: if not Confirm.ask("Do you want to proceed?"): @@ -366,7 +374,7 @@ async def process_pending_claims( spinner="earth", ): success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) if success: ext_id = await ext_receipt.get_extrinsic_identifier() diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index 2ced76d80..d4a087970 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -50,9 +50,9 @@ async def get_stake_data(block_hash_: str = None): subtensor.all_subnets(block_hash=block_hash_), ) - claimable_amounts = {} + claimable_amounts_ = {} if sub_stakes_: - claimable_amounts = await subtensor.get_claimable_stakes_for_coldkey( + claimable_amounts_ = await subtensor.get_claimable_stakes_for_coldkey( coldkey_ss58=coldkey_address, stakes_info=sub_stakes_, block_hash=block_hash_, @@ -63,7 +63,7 @@ async def get_stake_data(block_hash_: str = None): sub_stakes_, registered_delegate_info_, dynamic_info__, - claimable_amounts, + claimable_amounts_, ) def define_table( diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index a7f3f0782..c38fc0893 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -1,6 +1,6 @@ import asyncio -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet from rich.table import Table @@ -17,7 +17,6 @@ console, err_console, print_error, - format_error_message, group_subnets, get_subnet_name, unlock_key, @@ -41,14 +40,15 @@ async def display_stake_movement_cross_subnets( amount_to_move: Balance, stake_fee: Balance, extrinsic_fee: Balance, + proxy: Optional[str] = None, ) -> tuple[Balance, str]: """Calculate and display stake movement information""" if origin_netuid == destination_netuid: subnet = await subtensor.subnet(origin_netuid) - received_amount_tao = ( - subnet.alpha_to_tao(amount_to_move - stake_fee) - extrinsic_fee - ) + received_amount_tao = subnet.alpha_to_tao(amount_to_move - stake_fee) + if not proxy: + received_amount_tao -= extrinsic_fee received_amount = subnet.tao_to_alpha(received_amount_tao) if received_amount < Balance.from_tao(0).set_unit(destination_netuid): @@ -463,6 +463,7 @@ async def move_stake( era: int, interactive_selection: bool = False, prompt: bool = True, + proxy: Optional[str] = None, mev_protection: bool = True, ) -> tuple[bool, str]: if interactive_selection: @@ -478,6 +479,7 @@ async def move_stake( # Get the wallet stake balances. block_hash = await subtensor.substrate.get_chain_head() + # TODO should this use `proxy if proxy else wallet.coldkeypub.ss58_address`? origin_stake_balance, destination_stake_balance = await asyncio.gather( subtensor.get_stake( coldkey_ss58=wallet.coldkeypub.ss58_address, @@ -496,23 +498,23 @@ async def move_stake( if origin_stake_balance.tao == 0: print_error( f"Your balance is " - f"[{COLOR_PALETTE['POOLS']['TAO']}]0[/{COLOR_PALETTE['POOLS']['TAO']}] " + f"[{COLOR_PALETTE.POOLS.TAO}]0[/{COLOR_PALETTE.POOLS.TAO}] " f"in Netuid: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"[{COLOR_PALETTE.G.SUBHEAD}]{origin_netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" ) return False, "" console.print( f"\nOrigin Netuid: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], " + f"[{COLOR_PALETTE.G.SUBHEAD}]{origin_netuid}[/{COLOR_PALETTE.G.SUBHEAD}], " f"Origin stake: " - f"[{COLOR_PALETTE['POOLS']['TAO']}]{origin_stake_balance}[/{COLOR_PALETTE['POOLS']['TAO']}]" + f"[{COLOR_PALETTE.POOLS.TAO}]{origin_stake_balance}[/{COLOR_PALETTE.POOLS.TAO}]" ) console.print( f"Destination netuid: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{destination_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], " + f"[{COLOR_PALETTE.G.SUBHEAD}]{destination_netuid}[/{COLOR_PALETTE.G.SUBHEAD}], " f"Destination stake: " - f"[{COLOR_PALETTE['POOLS']['TAO']}]{destination_stake_balance}[/{COLOR_PALETTE['POOLS']['TAO']}]\n" + f"[{COLOR_PALETTE.POOLS.TAO}]{destination_stake_balance}[/{COLOR_PALETTE.POOLS.TAO}]\n" ) # Determine the amount we are moving. @@ -530,10 +532,8 @@ 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']}]" - 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']}]" + f" Stake balance: [{COLOR_PALETTE.S.AMOUNT}]{origin_stake_balance}[/{COLOR_PALETTE.S.AMOUNT}]" + f" < Moving amount: [{COLOR_PALETTE.S.AMOUNT}]{amount_to_move_as_balance}[/{COLOR_PALETTE.S.AMOUNT}]" ) return False, "" @@ -554,7 +554,7 @@ async def move_stake( destination_netuid=destination_netuid, amount=amount_to_move_as_balance.rao, ), - subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), ) # Display stake movement details @@ -571,6 +571,7 @@ async def move_stake( if origin_netuid != 0 else sim_swap.tao_fee, extrinsic_fee=extrinsic_fee, + proxy=proxy, ) except ValueError: return False, "" @@ -587,11 +588,11 @@ async def move_stake( ) as status: if mev_protection: call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, ) if mev_protection: @@ -605,20 +606,13 @@ async def move_stake( err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" - ext_id = await response.get_extrinsic_identifier() - - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, ext_id - else: - 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 False, "" + ext_id = await response.get_extrinsic_identifier() if response else "" + if success_: + await print_extrinsic_id(response) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, ext_id else: - await print_extrinsic_id(response) console.print( ":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]" ) @@ -650,6 +644,9 @@ async def move_stake( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" ) return True, ext_id + else: + err_console.print(f"\n:cross_mark: [red]Failed[/red] with error: {err_msg}") + return False, "" async def transfer_stake( @@ -664,23 +661,30 @@ async def transfer_stake( interactive_selection: bool = False, stake_all: bool = False, prompt: bool = True, + proxy: Optional[str] = None, mev_protection: bool = True, ) -> tuple[bool, str]: """Transfers stake from one network to another. Args: - wallet (Wallet): Bittensor wallet object. - subtensor (SubtensorInterface): Subtensor interface instance. - amount (float): Amount to transfer. - origin_hotkey (str): The hotkey SS58 to transfer the stake from. - origin_netuid (int): The netuid to transfer stake from. - dest_netuid (int): The netuid to transfer stake to. - dest_coldkey_ss58 (str): The destination coldkey to transfer stake to. - interactive_selection (bool): If true, prompts for selection of origin and destination subnets. - prompt (bool): If true, prompts for confirmation before executing transfer. + wallet: Bittensor wallet object. + subtensor: Subtensor interface instance. + amount: Amount to transfer. + origin_hotkey: The hotkey SS58 to transfer the stake from. + origin_netuid: The netuid to transfer stake from. + dest_netuid: The netuid to transfer stake to. + dest_coldkey_ss58: The destination coldkey to transfer stake to. + interactive_selection: If true, prompts for selection of origin and destination subnets. + prompt: If true, prompts for confirmation before executing transfer. + era: number of blocks for which the extrinsic should be valid + stake_all: If true, transfer all stakes. + proxy: Optional proxy to use for this extrinsic + mev_protection: If true, will encrypt the extrinsic behind the mev protection shield. Returns: - bool: True if transfer was successful, False otherwise. + tuple: + bool: True if transfer was successful, False otherwise. + str: error message """ if interactive_selection: selection = await stake_move_transfer_selection(subtensor, wallet) @@ -706,6 +710,7 @@ async def transfer_stake( # Get current stake balances with console.status(f"Retrieving stake data from {subtensor.network}..."): + # TODO should use proxy for these checks? current_stake = await subtensor.get_stake( coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=origin_hotkey, @@ -758,7 +763,7 @@ async def transfer_stake( destination_netuid=dest_netuid, amount=amount_to_transfer.rao, ), - subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), ) # Display stake movement details @@ -775,6 +780,7 @@ async def transfer_stake( if origin_netuid != 0 else sim_swap.tao_fee, extrinsic_fee=extrinsic_fee, + proxy=proxy, ) except ValueError: return False, "" @@ -789,14 +795,14 @@ async def transfer_stake( with console.status("\n:satellite: Transferring stake ...") as status: if mev_protection: call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) - - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, ) + if success_: if mev_protection: mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: @@ -807,43 +813,39 @@ async def transfer_stake( status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, ext_id + else: + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=dest_coldkey_ss58, + hotkey_ss58=origin_hotkey, + netuid=dest_netuid, + ), + ) - ext_id = await response.get_extrinsic_identifier() - - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, ext_id + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" + ) + return True, ext_id - if not await response.is_success: - err_console.print( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message)}" - ) + else: + err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") return False, "" - await print_extrinsic_id(response) - # Get and display new stake balances - new_stake, new_dest_stake = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=origin_hotkey, - netuid=origin_netuid, - ), - subtensor.get_stake( - coldkey_ss58=dest_coldkey_ss58, - hotkey_ss58=origin_hotkey, - netuid=dest_netuid, - ), - ) - - console.print( - f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" - ) - console.print( - f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}" - ) - return True, ext_id async def swap_stake( @@ -854,6 +856,7 @@ async def swap_stake( amount: float, swap_all: bool = False, era: int = 3, + proxy: Optional[str] = None, interactive_selection: bool = False, prompt: bool = True, wait_for_inclusion: bool = True, @@ -863,15 +866,19 @@ async def swap_stake( """Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership. Args: - wallet (Wallet): The wallet to swap stake from. - subtensor (SubtensorInterface): Subtensor interface instance. - origin_netuid (int): The netuid from which stake is removed. - destination_netuid (int): The netuid to which stake is added. - amount (float): The amount to swap. - interactive_selection (bool): If true, prompts for selection of origin and destination subnets. - prompt (bool): If true, prompts for confirmation before executing swap. - wait_for_inclusion (bool): If true, waits for the transaction to be included in a block. - wait_for_finalization (bool): If true, waits for the transaction to be finalized. + wallet: The wallet to swap stake from. + subtensor: Subtensor interface instance. + origin_netuid: The netuid from which stake is removed. + destination_netuid: The netuid to which stake is added. + amount: The amount to swap. + swap_all: Whether to swap all stakes. + era: The period (number of blocks) that the extrinsic is valid for + proxy: Optional proxy to use for this extrinsic submission + interactive_selection: If true, prompts for selection of origin and destination subnets. + prompt: If true, prompts for confirmation before executing swap. + wait_for_inclusion: If true, waits for the transaction to be included in a block. + wait_for_finalization: If true, waits for the transaction to be finalized. + mev_protection: If true, will encrypt the extrinsic behind the mev protection shield. Returns: (success, extrinsic_identifier): @@ -945,7 +952,7 @@ async def swap_stake( destination_netuid=destination_netuid, amount=amount_to_swap.rao, ), - subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), ) # Display stake movement details @@ -962,6 +969,7 @@ async def swap_stake( if origin_netuid != 0 else sim_swap.tao_fee, extrinsic_fee=extrinsic_fee, + proxy=proxy, ) except ValueError: return False, "" @@ -979,14 +987,13 @@ async def swap_stake( ) as status: if mev_protection: call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) - - response = await subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, ) if mev_protection: @@ -1002,37 +1009,36 @@ async def swap_stake( ext_id = await response.get_extrinsic_identifier() - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, ext_id + if success_: + await print_extrinsic_id(response) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, await response.get_extrinsic_identifier() + else: + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=destination_netuid, + ), + ) - if not await response.is_success: - err_console.print( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message)}" - ) - return False, "" - await print_extrinsic_id(response) - # Get and display new stake balances - new_stake, new_dest_stake = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - netuid=origin_netuid, - ), - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - netuid=destination_netuid, - ), - ) + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" + ) + return True, ext_id - console.print( - f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" - ) - console.print( - f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}" - ) - return True, ext_id + else: + err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") + return False, "" diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index afd9310da..15cf134a3 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -9,7 +9,6 @@ from rich.prompt import Confirm, Prompt from rich.table import Table -from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( encrypt_call, @@ -53,10 +52,11 @@ async def unstake( allow_partial_stake: bool, json_output: bool, era: int, + proxy: Optional[str], mev_protection: bool, ): """Unstake from hotkey(s).""" - + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address with console.status( f"Retrieving subnet data & identities from {subtensor.network}...", spinner="earth", @@ -71,9 +71,7 @@ async def unstake( subtensor.all_subnets(block_hash=chain_head), subtensor.fetch_coldkey_hotkey_identities(block_hash=chain_head), subtensor.get_delegate_identities(block_hash=chain_head), - subtensor.get_stake_for_coldkey( - wallet.coldkeypub.ss58_address, block_hash=chain_head - ), + subtensor.get_stake_for_coldkey(coldkey_ss58, block_hash=chain_head), ) all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} @@ -229,6 +227,7 @@ async def unstake( netuid=netuid, price_limit=price_limit, allow_partial_stake=allow_partial_stake, + proxy=proxy, ) else: extrinsic_fee = await _get_extrinsic_fee( @@ -238,11 +237,14 @@ async def unstake( hotkey_ss58=staking_address_ss58, netuid=netuid, amount=amount_to_unstake_as_balance, + proxy=proxy, ) sim_swap = await subtensor.sim_swap( netuid, 0, amount_to_unstake_as_balance.rao ) - received_amount = sim_swap.tao_amount - extrinsic_fee + received_amount = sim_swap.tao_amount + if not proxy: + received_amount -= extrinsic_fee except ValueError: continue total_received_amount += received_amount @@ -303,7 +305,7 @@ async def unstake( table = _create_unstake_table( wallet_name=wallet.name, - wallet_coldkey_ss58=wallet.coldkeypub.ss58_address, + wallet_coldkey_ss58=coldkey_ss58, network=subtensor.network, total_received_amount=total_received_amount, safe_staking=safe_staking, @@ -332,6 +334,7 @@ async def unstake( "hotkey_ss58": op["hotkey_ss58"], "status": status, "era": era, + "proxy": proxy, "mev_protection": mev_protection, } @@ -377,11 +380,13 @@ async def unstake_all( era: int = 3, prompt: bool = True, json_output: bool = False, + proxy: Optional[str] = None, mev_protection: bool = True, ) -> None: """Unstakes all stakes from all hotkeys in all subnets.""" include_hotkeys = include_hotkeys or [] exclude_hotkeys = exclude_hotkeys or [] + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address with console.status( f"Retrieving stake information & identities from {subtensor.network}...", spinner="earth", @@ -393,11 +398,11 @@ async def unstake_all( all_sn_dynamic_info_, current_wallet_balance, ) = await asyncio.gather( - subtensor.get_stake_for_coldkey(wallet.coldkeypub.ss58_address), + subtensor.get_stake_for_coldkey(coldkey_ss58), subtensor.fetch_coldkey_hotkey_identities(), subtensor.get_delegate_identities(), subtensor.all_subnets(), - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance(coldkey_ss58), ) if all_hotkeys: @@ -439,10 +444,10 @@ async def unstake_all( ) table = Table( title=( - f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{table_title}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" - f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " - f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f"Network: [{COLOR_PALETTE['GENERAL']['HEADER']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" + f"\n[{COLOR_PALETTE.G.HEADER}]{table_title}[/{COLOR_PALETTE.G.HEADER}]\n" + f"Wallet: [{COLOR_PALETTE.G.COLDKEY}]{wallet.name}[/{COLOR_PALETTE.G.COLDKEY}], " + f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{coldkey_ss58}[/{COLOR_PALETTE.G.CK}]\n" + f"Network: [{COLOR_PALETTE.G.HEADER}]{subtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n" ), show_footer=True, show_edge=False, @@ -508,9 +513,12 @@ async def unstake_all( wallet, subtensor, hotkey_ss58=stake.hotkey_ss58, + proxy=proxy, ) sim_swap = await subtensor.sim_swap(stake.netuid, 0, stake_amount.rao) - received_amount = sim_swap.tao_amount - extrinsic_fee + received_amount = sim_swap.tao_amount + if not proxy: + received_amount -= extrinsic_fee if received_amount < Balance.from_tao(0): print_error("Not enough Alpha to pay the transaction fee.") @@ -554,6 +562,7 @@ async def unstake_all( unstake_all_alpha=unstake_all_alpha, status=status, era=era, + proxy=proxy, mev_protection=mev_protection, ) ext_id = await ext_receipt.get_extrinsic_identifier() if success else None @@ -575,6 +584,7 @@ async def _unstake_extrinsic( hotkey_ss58: str, status=None, era: int = 3, + proxy: Optional[str] = None, mev_protection: bool = True, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a standard unstake extrinsic. @@ -588,11 +598,14 @@ async def _unstake_extrinsic( subtensor: Subtensor interface status: Optional status for console updates era: blocks for which the transaction is valid + proxy: Optional proxy to use for this extrinsic submission + """ err_out = partial(print_error, status=status) failure_prelude = ( f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}" ) + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address if status: status.update( @@ -600,7 +613,7 @@ async def _unstake_extrinsic( ) current_balance, call = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance(coldkey_ss58), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake", @@ -614,21 +627,11 @@ async def _unstake_extrinsic( if mev_protection: call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) - - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - if not await response.is_success: - err_out( - f"{failure_prelude} with error: " - f"{format_error_message(await response.error_message)}" - ) - return False, None + success, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, wallet=wallet, era={"period": era}, proxy=proxy + ) + if success: if mev_protection: mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: @@ -640,15 +643,13 @@ async def _unstake_extrinsic( err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, None - - # Fetch latest balance and stake await print_extrinsic_id(response) 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_balance(coldkey_ss58, block_hash), subtensor.get_stake( hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, netuid=netuid, block_hash=block_hash, ), @@ -656,16 +657,18 @@ async def _unstake_extrinsic( console.print(":white_heavy_check_mark: [green]Finalized[/green]") 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: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}" ) console.print( - 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}" + f"Subnet: [{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" + f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_stake}" ) return True, response - - except Exception as e: - err_out(f"{failure_prelude} with error: {str(e)}") + else: + err_out( + f"{failure_prelude} with error: " + f"{format_error_message(await response.error_message)}" + ) return False, None @@ -679,6 +682,7 @@ async def _safe_unstake_extrinsic( allow_partial_stake: bool, status=None, era: int = 3, + proxy: Optional[str] = None, mev_protection: bool = True, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a safe unstake extrinsic with price limit. @@ -692,11 +696,14 @@ async def _safe_unstake_extrinsic( subtensor: Subtensor interface allow_partial_stake: Whether to allow partial unstaking status: Optional status for console updates + proxy: Optional proxy to use for unstake extrinsic + """ err_out = partial(print_error, status=status) failure_prelude = ( f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}" ) + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address if status: status.update( @@ -705,11 +712,12 @@ async def _safe_unstake_extrinsic( block_hash = await subtensor.substrate.get_chain_head() - current_balance, current_stake, call = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), + current_balance, next_nonce, current_stake, call = await asyncio.gather( + subtensor.get_balance(coldkey_ss58, block_hash), + subtensor.substrate.get_account_next_index(coldkey_ss58), subtensor.get_stake( hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, netuid=netuid, block_hash=block_hash, ), @@ -729,75 +737,68 @@ async def _safe_unstake_extrinsic( if mev_protection: call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} + success, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + nonce=next_nonce, + era={"period": era}, + proxy=proxy, ) - - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False + if success: + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None + await print_extrinsic_id(response) + block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance(coldkey_ss58, block_hash), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=coldkey_ss58, + netuid=netuid, + block_hash=block_hash, + ), ) - except SubstrateRequestException as e: - if "Custom error: 8" in str(e): - print_error( - f"\n{failure_prelude}: Price exceeded tolerance limit. " - f"Transaction rejected because partial unstaking is disabled. " - f"Either increase price tolerance or enable partial unstaking.", - status=status, - ) - else: - err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") - return False, None - if not await response.is_success: - err_out( - f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}" ) - return False, None - if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status + amount_unstaked = current_stake - new_stake + if allow_partial_stake and (amount_unstaked != amount): + console.print( + "Partial unstake transaction. Unstaked:\n" + f" [{COLOR_PALETTE.S.AMOUNT}]{amount_unstaked.set_unit(netuid=netuid)}[/{COLOR_PALETTE.S.AMOUNT}] " + f"instead of " + f"[blue]{amount}[/blue]" ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None - - await print_extrinsic_id(response) - 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(":white_heavy_check_mark: [green]Finalized[/green]") - console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - - amount_unstaked = current_stake - new_stake - if allow_partial_stake and (amount_unstaked != amount): console.print( - "Partial unstake transaction. Unstaked:\n" - f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_unstaked.set_unit(netuid=netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " - f"instead of " - f"[blue]{amount}[/blue]" + f"Subnet: [{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] " + f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_stake}" ) - - console.print( - 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, response + return True, response + elif "Custom error: 8" in err_msg: + print_error( + f"\n{failure_prelude}: Price exceeded tolerance limit. " + f"Transaction rejected because partial unstaking is disabled. " + f"Either increase price tolerance or enable partial unstaking.", + status=status, + ) + else: + err_out( + f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" + ) + return False, None async def _unstake_all_extrinsic( @@ -808,6 +809,7 @@ async def _unstake_all_extrinsic( unstake_all_alpha: bool, status=None, era: int = 3, + proxy: Optional[str] = None, mev_protection: bool = True, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute an unstake all extrinsic. @@ -824,6 +826,7 @@ async def _unstake_all_extrinsic( failure_prelude = ( f":cross_mark: [red]Failed[/red] to unstake all from {hotkey_name}" ) + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address if status: status.update( @@ -835,17 +838,15 @@ async def _unstake_all_extrinsic( previous_root_stake, current_balance = await asyncio.gather( subtensor.get_stake( hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, netuid=0, block_hash=block_hash, ), - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash - ), + subtensor.get_balance(coldkey_ss58, block_hash=block_hash), ) else: current_balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash + coldkey_ss58, block_hash=block_hash ) previous_root_stake = None @@ -860,19 +861,15 @@ async def _unstake_all_extrinsic( call = await encrypt_call(subtensor, wallet, call) try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic=await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ), - wait_for_inclusion=True, - wait_for_finalization=False, + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, ) - if not await response.is_success: - err_out( - f"{failure_prelude} with error: " - f"{format_error_message(await response.error_message)}" - ) + if not success_: + err_out(f"{failure_prelude} with error: {err_msg}") return False, None if mev_protection: @@ -895,35 +892,33 @@ async def _unstake_all_extrinsic( new_root_stake, new_balance = await asyncio.gather( subtensor.get_stake( hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, netuid=0, block_hash=block_hash, ), - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash - ), + subtensor.get_balance(coldkey_ss58, block_hash=block_hash), ) else: new_balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash + coldkey_ss58, block_hash=block_hash ) new_root_stake = None + msg_modifier = "Alpha " if unstake_all_alpha else "" success_message = ( - ":white_heavy_check_mark: [green]Finalized: Successfully unstaked all stakes[/green]" - if not unstake_all_alpha - else ":white_heavy_check_mark: [green]Finalized: Successfully unstaked all Alpha stakes[/green]" + f":white_heavy_check_mark: [green]Included:" + f" Successfully unstaked all {msg_modifier}stakes[/green]" ) console.print(f"{success_message} from {hotkey_name}") 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: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}" ) if unstake_all_alpha: console.print( f"Root Stake for {hotkey_name}:\n " f"[blue]{previous_root_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_root_stake}" + f"[{COLOR_PALETTE.S.AMOUNT}]{new_root_stake}" ) return True, response @@ -941,6 +936,7 @@ async def _get_extrinsic_fee( amount: Optional[Balance] = None, price_limit: Optional[Balance] = None, allow_partial_stake: bool = False, + proxy: Optional[str] = None, ) -> Balance: """ Retrieves the extrinsic fee for a given unstaking call. @@ -986,7 +982,7 @@ async def _get_extrinsic_fee( call_function=call_fn, call_params=call_params, ) - return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy) # Helpers @@ -1037,7 +1033,7 @@ async def _unstake_selection( # Display existing hotkeys, id, and staked netuids. subnet_filter = f" for Subnet {netuid}" if netuid is not None else "" table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes{subnet_filter}\n", + title=f"\n[{COLOR_PALETTE.G.HEADER}]Hotkeys with Stakes{subnet_filter}\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1048,9 +1044,9 @@ async def _unstake_selection( pad_edge=True, ) table.add_column("Index", justify="right") - table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) - table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"]) - table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) + table.add_column("Identity", style=COLOR_PALETTE.G.SUBHEAD) + table.add_column("Netuids", style=COLOR_PALETTE.G.NETUID) + table.add_column("Hotkey Address", style=COLOR_PALETTE.G.HK) for hotkey_info in hotkeys_info: index = str(hotkey_info["index"]) @@ -1123,7 +1119,8 @@ async def _unstake_selection( invalid_netuids = [n for n in netuid_list if n not in netuid_stakes] if invalid_netuids: print_error( - f"The following netuids are invalid or not available: {', '.join(map(str, invalid_netuids))}. Please try again." + f"The following netuids are invalid or not available: " + f"{', '.join(map(str, invalid_netuids))}. Please try again." ) else: selected_netuids = netuid_list @@ -1316,10 +1313,10 @@ def _create_unstake_table( Rich Table object configured for unstake summary """ title = ( - f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking to: \n" - f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet_name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " - f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet_coldkey_ss58}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f"Network: {network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" + f"\n[{COLOR_PALETTE.G.HEADER}]Unstaking to: \n" + f"Wallet: [{COLOR_PALETTE.G.CK}]{wallet_name}[/{COLOR_PALETTE.G.CK}], " + f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet_coldkey_ss58}[/{COLOR_PALETTE.G.CK}]\n" + f"Network: {network}[/{COLOR_PALETTE.G.HEADER}]\n" ) table = Table( title=title, @@ -1395,10 +1392,11 @@ def _print_table_and_slippage( if max_float_slippage > 5: console.print( "\n" - f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_float_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]," + f"[{COLOR_PALETTE.S.SLIPPAGE_TEXT}]{'-' * console.width}\n" + f"[bold]WARNING:[/bold] The slippage on one of your operations is high: " + f"[{COLOR_PALETTE.S.SLIPPAGE_PERCENT}]{max_float_slippage} %[/{COLOR_PALETTE.S.SLIPPAGE_PERCENT}]," " this may result in a loss of funds.\n" - f"-------------------------------------------------------------------------------------------------------------------\n" + f"{'-' * console.width}\n" ) base_description = """ [bold white]Description[/bold white]: diff --git a/bittensor_cli/src/commands/subnets/mechanisms.py b/bittensor_cli/src/commands/subnets/mechanisms.py index 2ad5d72db..dfa7c165b 100644 --- a/bittensor_cli/src/commands/subnets/mechanisms.py +++ b/bittensor_cli/src/commands/subnets/mechanisms.py @@ -182,6 +182,7 @@ async def set_emission_split( wallet: Wallet, netuid: int, new_emission_split: Optional[str], + proxy: Optional[str], wait_for_inclusion: bool, wait_for_finalization: bool, prompt: bool, @@ -356,6 +357,7 @@ async def set_emission_split( subtensor=subtensor, netuid=netuid, split=normalized_weights, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, json_output=json_output, @@ -406,6 +408,7 @@ async def set_mechanism_count( netuid: int, mechanism_count: int, previous_count: int, + proxy: Optional[str], wait_for_inclusion: bool, wait_for_finalization: bool, json_output: bool, @@ -436,6 +439,7 @@ async def set_mechanism_count( wallet=wallet, netuid=netuid, mech_count=mechanism_count, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -461,6 +465,7 @@ async def set_mechanism_emission( subtensor: "SubtensorInterface", netuid: int, split: list[int], + proxy: Optional[str], wait_for_inclusion: bool, wait_for_finalization: bool, json_output: bool, @@ -480,6 +485,7 @@ async def set_mechanism_emission( split=split, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) ext_id = await ext_receipt.get_extrinsic_identifier() if success else None diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 14c026b01..9ed457c3d 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -3,6 +3,7 @@ import sqlite3 from typing import TYPE_CHECKING, Optional, cast +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet from rich.prompt import Confirm, Prompt from rich.console import Group @@ -27,11 +28,10 @@ from bittensor_cli.src.commands.wallets import set_id, get_id from bittensor_cli.src.bittensor.utils import ( console, - create_table, + create_and_populate_table, err_console, print_verbose, print_error, - format_error_message, get_metadata_table, millify_tao, render_table, @@ -116,6 +116,7 @@ async def register_subnetwork_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, subnet_identity: dict, + proxy: Optional[str], wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, @@ -139,8 +140,9 @@ async def register_subnetwork_extrinsic( extrinsic_identifier: Optional extrinsic identifier, if the extrinsic was included. """ + # TODO why doesn't this have an era? async def _find_event_attributes_in_extrinsic_receipt( - response_, event_name: str + response_: AsyncExtrinsicReceipt, event_name: str ) -> list: """ Searches for the attributes of a specified event within an extrinsic receipt. @@ -157,18 +159,19 @@ async def _find_event_attributes_in_extrinsic_receipt( if event_details["event_id"] == event_name: # Once found, you can access the attributes of the event_name return event_details["attributes"] - return [-1] + return [] print_verbose("Fetching balance") - your_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + + your_balance = await subtensor.get_balance(proxy or wallet.coldkeypub.ss58_address) print_verbose("Fetching burn_cost") sn_burn_cost = await burn_cost(subtensor) if sn_burn_cost > your_balance: err_console.print( - f"Your balance of: [{COLOR_PALETTE['POOLS']['TAO']}]{your_balance}[{COLOR_PALETTE['POOLS']['TAO']}]" + f"Your balance of: [{COLOR_PALETTE.POOLS.TAO}]{your_balance}[{COLOR_PALETTE.POOLS.TAO}]" f" is not enough to burn " - f"[{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}] " + f"[{COLOR_PALETTE.POOLS.TAO}]{sn_burn_cost}[{COLOR_PALETTE.POOLS.TAO}] " f"to register a subnet." ) return False, None, None @@ -240,26 +243,22 @@ async def _find_event_attributes_in_extrinsic_receipt( ) if mev_protection: call = await encrypt_call(subtensor, wallet, call) - extrinsic = await substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await substrate.submit_extrinsic( - extrinsic, + success, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None, None - - 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, None, None + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, None, None + if not success: + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + return False, None, None + else: # Check for MEV shield execution if mev_protection: mev_shield_id = await extract_mev_shield_id(response) @@ -275,14 +274,22 @@ async def _find_event_attributes_in_extrinsic_receipt( return False, None, None # Successful registration, final check for membership + attributes = await _find_event_attributes_in_extrinsic_receipt( response, "NetworkAdded" ) await print_extrinsic_id(response) ext_id = await response.get_extrinsic_identifier() - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" - ) + if not attributes: + console.print( + ":exclamation: [yellow]A possible error has occurred[/yellow]. The extrinsic reports success, but " + "we are unable to locate the 'NetworkAdded' event inside the extrinsic's events." + "" + ) + else: + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + ) return True, int(attributes[0]), ext_id @@ -1709,15 +1716,21 @@ async def create( wallet: Wallet, subtensor: "SubtensorInterface", subnet_identity: dict, + proxy: Optional[str], json_output: bool, prompt: bool, mev_protection: bool = True, ): """Register a subnetwork""" - + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address # Call register command. success, netuid, ext_id = await register_subnetwork_extrinsic( - subtensor, wallet, subnet_identity, prompt=prompt, mev_protection=mev_protection + subtensor=subtensor, + wallet=wallet, + subnet_identity=subnet_identity, + prompt=prompt, + proxy=proxy, + mev_protection=mev_protection, ) if json_output: # technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present @@ -1736,7 +1749,7 @@ async def create( if do_set_identity: current_identity = await get_id( - subtensor, wallet.coldkeypub.ss58_address, "Current on-chain identity" + subtensor, coldkey_ss58, "Current on-chain identity" ) if prompt: if not Confirm.ask( @@ -1758,16 +1771,16 @@ async def create( ) await set_id( - wallet, - subtensor, - identity["name"], - identity["url"], - identity["image"], - identity["discord"], - identity["description"], - identity["additional"], - identity["github_repo"], - prompt, + wallet=wallet, + subtensor=subtensor, + name=identity["name"], + web_url=identity["url"], + image_url=identity["image"], + discord=identity["discord"], + description=identity["description"], + additional=identity["additional"], + github_repo=identity["github_repo"], + proxy=proxy, ) @@ -1808,9 +1821,10 @@ async def register( era: Optional[int], json_output: bool, prompt: bool, + proxy: Optional[str] = None, ): """Register neuron by recycling some TAO.""" - + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address # Verify subnet exists print_verbose("Checking subnet status") block_hash = await subtensor.substrate.get_chain_head() @@ -1832,7 +1846,7 @@ async def register( subtensor.get_hyperparameter( param_name="Burn", netuid=netuid, block_hash=block_hash ), - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), + subtensor.get_balance(coldkey_ss58, block_hash=block_hash), ) current_recycle = ( Balance.from_rao(int(current_recycle_)) if current_recycle_ else Balance(0) @@ -1852,8 +1866,11 @@ async def register( # TODO make this a reusable function, also used in subnets list # Show creation table. table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Register to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]netuid: {netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", + title=( + f"\n[{COLOR_PALETTE.G.HEADER}]" + f"Register to [{COLOR_PALETTE.G.SUBHEAD}]netuid: {netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" + f"\nNetwork: [{COLOR_PALETTE.G.SUBHEAD}]{subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n" + ), show_footer=True, show_edge=False, header_style="bold white", @@ -1895,7 +1912,7 @@ async def register( f"{Balance.get_unit(netuid)}", f"τ {current_recycle.tao:.4f}", f"{get_hotkey_pub_ss58(wallet)}", - f"{wallet.coldkeypub.ss58_address}", + f"{coldkey_ss58}", ) console.print(table) if not ( @@ -1910,7 +1927,9 @@ async def register( return if netuid == 0: - success, msg, ext_id = await root_register_extrinsic(subtensor, wallet=wallet) + success, msg, ext_id = await root_register_extrinsic( + subtensor, wallet=wallet, proxy=proxy + ) else: success, msg, ext_id = await burned_register_extrinsic( subtensor, @@ -1918,6 +1937,7 @@ async def register( netuid=netuid, old_balance=balance, era=era, + proxy=proxy, ) if json_output: json_console.print( @@ -2072,7 +2092,7 @@ async def metagraph_cmd( } if not no_cache: update_metadata_table("metagraph", metadata_info) - create_table( + create_and_populate_table( "metagraph", columns=[ ("UID", "INTEGER"), @@ -2448,6 +2468,7 @@ async def set_identity( netuid: int, subnet_identity: dict, prompt: bool = False, + proxy: Optional[str] = None, ) -> tuple[bool, Optional[str]]: """Set identity information for a subnet""" @@ -2508,7 +2529,7 @@ async def set_identity( spinner="earth", ): success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) if not success: @@ -2651,10 +2672,11 @@ async def start_subnet( wallet: "Wallet", subtensor: "SubtensorInterface", netuid: int, + proxy: Optional[str], prompt: bool = False, ) -> bool: """Start a subnet's emission schedule""" - + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address if not await subtensor.subnet_exists(netuid): print_error(f"Subnet {netuid} does not exist.") return False @@ -2664,7 +2686,8 @@ async def start_subnet( storage_function="SubnetOwner", params=[netuid], ) - if subnet_owner != wallet.coldkeypub.ss58_address: + # TODO should this check against proxy as well? + if subnet_owner != coldkey_ss58: print_error(":cross_mark: This wallet doesn't own the specified subnet.") return False @@ -2685,26 +2708,21 @@ async def start_subnet( call_function="start_call", call_params={"netuid": netuid}, ) - - signed_ext = await subtensor.substrate.create_signed_extrinsic( + success, error_msg, response = await subtensor.sign_and_send_extrinsic( call=start_call, - keypair=wallet.coldkey, - ) - - response = await subtensor.substrate.submit_extrinsic( - extrinsic=signed_ext, + wallet=wallet, wait_for_inclusion=True, wait_for_finalization=True, + proxy=proxy, ) - if await response.is_success: + if success: await print_extrinsic_id(response) console.print( f":white_heavy_check_mark: [green]Successfully started subnet {netuid}'s emission schedule.[/green]" ) return True else: - error_msg = format_error_message(await response.error_message) if "FirstEmissionBlockNumberAlreadySet" in error_msg: console.print( f"[dark_sea_green3]Subnet {netuid} already has an emission schedule.[/dark_sea_green3]" @@ -2721,6 +2739,8 @@ async def set_symbol( subtensor: "SubtensorInterface", netuid: int, symbol: str, + proxy: Optional[str], + period: int, prompt: bool = False, json_output: bool = False, ) -> bool: @@ -2761,16 +2781,11 @@ async def set_symbol( call_params={"netuid": netuid, "symbol": symbol.encode("utf-8")}, ) - signed_ext = await subtensor.substrate.create_signed_extrinsic( - call=start_call, - keypair=wallet.coldkey, + success, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=start_call, wallet=wallet, proxy=proxy, era={"period": period} ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic=signed_ext, - wait_for_inclusion=True, - ) - if await response.is_success: + if success: ext_id = await response.get_extrinsic_identifier() await print_extrinsic_id(response) message = f"Successfully updated SN{netuid}'s symbol to {symbol}." @@ -2786,11 +2801,14 @@ async def set_symbol( console.print(f":white_heavy_check_mark:[dark_sea_green3] {message}\n") return True else: - err = format_error_message(await response.error_message) if json_output: json_console.print_json( - data={"success": False, "message": err, "extrinsic_identifier": None} + data={ + "success": False, + "message": err_msg, + "extrinsic_identifier": None, + } ) else: - err_console.print(f":cross_mark: [red]Failed[/red]: {err}") + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") return False diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 76cc0addd..9992fe77e 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -176,6 +176,7 @@ async def set_mechanism_count_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", netuid: int, + proxy: Optional[str], mech_count: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, @@ -204,6 +205,7 @@ async def set_mechanism_count_extrinsic( wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) if not success: @@ -216,6 +218,7 @@ async def set_mechanism_emission_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", netuid: int, + proxy: Optional[str], split: list[int], wait_for_inclusion: bool = True, wait_for_finalization: bool = True, @@ -242,6 +245,7 @@ async def set_mechanism_emission_extrinsic( wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) if not success: @@ -254,6 +258,7 @@ async def set_hyperparameter_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", netuid: int, + proxy: Optional[str], parameter: str, value: Optional[Union[str, float, list[float]]], wait_for_inclusion: bool = False, @@ -265,6 +270,7 @@ async def set_hyperparameter_extrinsic( :param subtensor: initialized SubtensorInterface object :param wallet: bittensor wallet object. :param netuid: Subnetwork `uid`. + :param proxy: Optional proxy to use for this extrinsic submission. :param parameter: Hyperparameter name. :param value: New hyperparameter value. :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns @@ -280,17 +286,12 @@ async def set_hyperparameter_extrinsic( extrinsic_identifier: optional extrinsic identifier if the extrinsic was included """ print_verbose("Confirming subnet owner") + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address subnet_owner = await subtensor.query( module="SubtensorModule", storage_function="SubnetOwner", params=[netuid], ) - if subnet_owner != wallet.coldkeypub.ss58_address: - err_msg = ( - ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" - ) - err_console.print(err_msg) - return False, err_msg, None if not (ulw := unlock_key(wallet)).success: return False, ulw.message, None @@ -374,8 +375,18 @@ async def set_hyperparameter_extrinsic( call_params={"call": call_}, ) else: + if subnet_owner != coldkey_ss58: + err_msg = ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" + err_console.print(err_msg) + return False, err_msg, None call = call_ else: + if subnet_owner != coldkey_ss58: + err_msg = ( + ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" + ) + err_console.print(err_msg) + return False, err_msg, None call = call_ with console.status( f":satellite: Setting hyperparameter [{COLOR_PALETTE.G.SUBHEAD}]{parameter}[/{COLOR_PALETTE.G.SUBHEAD}]" @@ -384,7 +395,7 @@ async def set_hyperparameter_extrinsic( spinner="earth", ): success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization, proxy=proxy ) if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") @@ -565,6 +576,7 @@ async def _is_senate_member(subtensor: "SubtensorInterface", hotkey_ss58: str) - async def vote_senate_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, + proxy: Optional[str], proposal_hash: str, proposal_idx: int, vote: bool, @@ -576,6 +588,7 @@ async def vote_senate_extrinsic( :param subtensor: The SubtensorInterface object to use for the query :param wallet: Bittensor wallet object, with coldkey and hotkey unlocked. + :param proxy: Optional proxy address to use for the extrinsic submission :param proposal_hash: The hash of the proposal for which voting data is requested. :param proposal_idx: The index of the proposal to vote. :param vote: Whether to vote aye or nay. @@ -606,11 +619,10 @@ async def vote_senate_extrinsic( }, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization, proxy=proxy ) if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - await asyncio.sleep(0.5) return False # Successful vote, final check for data else: @@ -638,6 +650,7 @@ async def set_take_extrinsic( wallet: Wallet, delegate_ss58: str, take: float = 0.0, + proxy: Optional[str] = None, ) -> tuple[bool, Optional[str]]: """ Set delegate hotkey take @@ -646,6 +659,7 @@ async def set_take_extrinsic( :param wallet: The wallet containing the hotkey to be nominated. :param delegate_ss58: Hotkey :param take: Delegate take on subnet ID + :param proxy: Optional proxy address to use for the extrinsic submission :return: `True` if the process is successful, `False` otherwise. @@ -682,7 +696,7 @@ async def set_take_extrinsic( }, ) success, err, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) else: @@ -702,7 +716,7 @@ async def set_take_extrinsic( }, ) success, err, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) if not success: @@ -724,6 +738,7 @@ async def sudo_set_hyperparameter( wallet: Wallet, subtensor: "SubtensorInterface", netuid: int, + proxy: Optional[str], param_name: str, param_value: Optional[str], prompt: bool, @@ -741,7 +756,7 @@ async def sudo_set_hyperparameter( if json_output: prompt = False success, err_msg, ext_id = await set_hyperparameter_extrinsic( - subtensor, wallet, netuid, param_name, value, prompt=prompt + subtensor, wallet, netuid, proxy, param_name, value, prompt=prompt ) if json_output: return success, err_msg, ext_id @@ -955,6 +970,7 @@ async def proposals( async def senate_vote( wallet: Wallet, subtensor: "SubtensorInterface", + proxy: Optional[str], proposal_hash: str, vote: bool, prompt: bool, @@ -991,6 +1007,7 @@ async def senate_vote( success = await vote_senate_extrinsic( subtensor=subtensor, wallet=wallet, + proxy=proxy, proposal_hash=proposal_hash, proposal_idx=vote_data.index, vote=vote, @@ -1015,7 +1032,7 @@ async def display_current_take(subtensor: "SubtensorInterface", wallet: Wallet) async def set_take( - wallet: Wallet, subtensor: "SubtensorInterface", take: float + wallet: Wallet, subtensor: "SubtensorInterface", take: float, proxy: Optional[str] ) -> tuple[bool, Optional[str]]: """Set delegate take.""" @@ -1042,6 +1059,7 @@ async def _do_set_take() -> tuple[bool, Optional[str]]: wallet=wallet, delegate_ss58=hotkey_ss58, take=take, + proxy=proxy, ) success, ext_id = result @@ -1069,6 +1087,7 @@ async def trim( wallet: Wallet, subtensor: "SubtensorInterface", netuid: int, + proxy: Optional[str], max_n: int, period: int, prompt: bool, @@ -1083,6 +1102,7 @@ async def trim( storage_function="SubnetOwner", params=[netuid], ) + # TODO should this check proxy also? if subnet_owner != wallet.coldkeypub.ss58_address: err_msg = "This wallet doesn't own the specified subnet." if json_output: @@ -1102,7 +1122,7 @@ async def trim( call_params={"netuid": netuid, "max_n": max_n}, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call=call, wallet=wallet, era={"period": period} + call=call, wallet=wallet, era={"period": period}, proxy=proxy ) if not success: if json_output: diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index a0cb24ceb..8813e6839 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -84,6 +84,7 @@ async def associate_hotkey( hotkey_ss58: str, hotkey_display: str, prompt: bool = False, + proxy: Optional[str] = None, ): """Associates a hotkey with a wallet""" @@ -129,6 +130,7 @@ async def associate_hotkey( wallet, wait_for_inclusion=True, wait_for_finalization=False, + proxy=proxy, ) if not success: @@ -1533,6 +1535,8 @@ async def transfer( era: int, prompt: bool, json_output: bool, + proxy: Optional[str] = None, + announce_only: bool = False, ): """Transfer token of amount to destination.""" result, ext_receipt = await transfer_extrinsic( @@ -1544,6 +1548,8 @@ async def transfer( allow_death=allow_death, era=era, prompt=prompt, + proxy=proxy, + announce_only=announce_only, ) ext_id = (await ext_receipt.get_extrinsic_identifier()) if result else None if json_output: @@ -1737,6 +1743,7 @@ async def swap_hotkey( new_wallet: Wallet, subtensor: SubtensorInterface, netuid: Optional[int], + proxy: Optional[str], prompt: bool, json_output: bool, ): @@ -1747,6 +1754,7 @@ async def swap_hotkey( new_wallet, netuid=netuid, prompt=prompt, + proxy=proxy, ) if result: ext_id = await ext_receipt.get_extrinsic_identifier() @@ -1796,8 +1804,8 @@ async def set_id( description: str, additional: str, github_repo: str, - prompt: bool, json_output: bool = False, + proxy: Optional[str] = None, ) -> bool: """Create a new or update existing identity on-chain.""" output_dict = {"success": False, "identity": None, "error": ""} @@ -1824,7 +1832,7 @@ async def set_id( " :satellite: [dark_sea_green3]Updating identity on-chain...", spinner="earth" ): success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) if not success: @@ -2040,6 +2048,7 @@ async def schedule_coldkey_swap( subtensor: SubtensorInterface, new_coldkey_ss58: str, force_swap: bool = False, + proxy: Optional[str] = None, ) -> bool: """Schedules a coldkey swap operation to be executed at a future block. @@ -2097,6 +2106,7 @@ async def schedule_coldkey_swap( wallet, wait_for_inclusion=True, wait_for_finalization=True, + proxy=proxy, ) block_post_call = await subtensor.substrate.get_block_number() diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 4bccb28a7..068edf6ca 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -36,6 +36,7 @@ def __init__( subtensor: "SubtensorInterface", wallet: Wallet, netuid: int, + proxy: Optional[str], uids: NDArray, weights: NDArray, salt: list[int], @@ -47,6 +48,7 @@ def __init__( self.subtensor = subtensor self.wallet = wallet self.netuid = netuid + self.proxy = proxy self.uids = uids self.weights = weights self.salt = salt @@ -222,19 +224,12 @@ async def reveal(self, weight_uids, weight_vals) -> tuple[bool, str, Optional[st console.print( ":white_heavy_check_mark: [green]Weights hash revealed on chain[/green]" ) - # bittensor.logging.success(prefix="Weights hash revealed", suffix=str(msg)) - return ( True, "Successfully revealed previously committed weights hash.", ext_id, ) else: - # bittensor.logging.error( - # msg=msg, - # prefix=f"Failed to reveal previously committed weights hash for salt: {salt}", - # suffix="Failed: ", - # ) return False, "Failed to reveal weights.", None async def _set_weights_without_commit_reveal( @@ -254,29 +249,25 @@ async def _do_set_weights() -> tuple[bool, str, Optional[str]]: }, ) # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = await self.subtensor.substrate.create_signed_extrinsic( + success, err_msg, response = await self.subtensor.sign_and_send_extrinsic( call=call, - keypair=self.wallet.hotkey, + sign_with="hotkey", + wallet=self.wallet, era={"period": 5}, + wait_for_finalization=True, + wait_for_inclusion=True, + proxy=self.proxy, ) - try: - response = await self.subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=self.wait_for_inclusion, - wait_for_finalization=self.wait_for_finalization, - ) - except SubstrateRequestException as e: - return False, format_error_message(e), None # We only wait here if we expect finalization. if not self.wait_for_finalization and not self.wait_for_inclusion: return True, "Not waiting for finalization or inclusion.", None - if await response.is_success: + if success: ext_id_ = await response.get_extrinsic_identifier() await print_extrinsic_id(response) return True, "Successfully set weights.", ext_id_ else: - return False, format_error_message(await response.error_message), None + return False, err_msg, None with console.status( f":satellite: Setting weights on [white]{self.subtensor.network}[/white] ..." @@ -311,43 +302,27 @@ async def reveal_weights_extrinsic( "version_key": self.version_key, }, ) - extrinsic = await self.subtensor.substrate.create_signed_extrinsic( + success, error_message, response = await self.subtensor.sign_and_send_extrinsic( call=call, - keypair=self.wallet.hotkey, + wallet=self.wallet, + sign_with="hotkey", + wait_for_inclusion=self.wait_for_inclusion, + wait_for_finalization=self.wait_for_finalization, + proxy=self.proxy, ) - try: - response = await self.subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=self.wait_for_inclusion, - wait_for_finalization=self.wait_for_finalization, - ) - except SubstrateRequestException as e: - return False, format_error_message(e), None if not self.wait_for_finalization and not self.wait_for_inclusion: - success, error_message, ext_id = True, "", None - - else: - if await response.is_success: - success, error_message, ext_id = ( - True, - "", - await response.get_extrinsic_identifier(), - ) - await print_extrinsic_id(response) - else: - success, error_message, ext_id = ( - False, - format_error_message(await response.error_message), - None, - ) + return True, "", None if success: - # bittensor.logging.info("Successfully revealed weights.") - return True, "Successfully revealed weights.", ext_id + await print_extrinsic_id(response) + return ( + True, + "Successfully revealed weights.", + await response.get_extrinsic_identifier(), + ) else: - # bittensor.logging.error(f"Failed to reveal weights: {error_message}") - return False, error_message, ext_id + return False, error_message, None async def do_commit_weights( self, commit_hash @@ -360,25 +335,24 @@ async def do_commit_weights( "commit_hash": commit_hash, }, ) - extrinsic = await self.subtensor.substrate.create_signed_extrinsic( + success, err_msg, response = await self.subtensor.sign_and_send_extrinsic( call=call, - keypair=self.wallet.hotkey, - ) - response = await self.subtensor.substrate.submit_extrinsic( - extrinsic, + wallet=self.wallet, + sign_with="hotkey", wait_for_inclusion=self.wait_for_inclusion, wait_for_finalization=self.wait_for_finalization, + proxy=self.proxy, ) if not self.wait_for_finalization and not self.wait_for_inclusion: return True, None, None - if await response.is_success: + if success: ext_id = await response.get_extrinsic_identifier() await print_extrinsic_id(response) return True, None, ext_id else: - return False, await response.error_message, None + return False, err_msg, None # commands @@ -388,6 +362,7 @@ async def reveal_weights( subtensor: "SubtensorInterface", wallet: Wallet, netuid: int, + proxy: Optional[str], uids: list[int], weights: list[float], salt: list[int], @@ -413,7 +388,15 @@ async def reveal_weights( ) # Call the reveal function in the module set_weights from extrinsics package extrinsic = SetWeightsExtrinsic( - subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + uids=uids_, + weights=weights_, + salt=list(salt_), + version_key=version, + prompt=prompt, + proxy=proxy, ) success, message, ext_id = await extrinsic.reveal(weight_uids, weight_vals) if json_output: @@ -434,6 +417,7 @@ async def commit_weights( wallet: Wallet, netuid: int, uids: list[int], + proxy: Optional[str], weights: list[float], salt: list[int], version: int, @@ -454,7 +438,15 @@ async def commit_weights( dtype=np.int64, ) extrinsic = SetWeightsExtrinsic( - subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + uids=uids_, + weights=weights_, + salt=list(salt_), + version_key=version, + prompt=prompt, + proxy=proxy, ) success, message, ext_id = await extrinsic.set_weights_extrinsic() if json_output: diff --git a/pyproject.toml b/pyproject.toml index fbe506980..d802f19d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "bittensor-cli" -version = "9.16.0rc1" +version = "9.15.3" description = "Bittensor CLI" readme = "README.md" authors = [ diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py new file mode 100644 index 000000000..e5e76724e --- /dev/null +++ b/tests/e2e_tests/test_proxy.py @@ -0,0 +1,686 @@ +import json +import os +import time + +from bittensor_cli.src.bittensor.utils import ProxyAnnouncements + +""" +Verify commands: + +* btcli proxy create +* btcli proxy add +* btcli proxy remove +* btcli proxy kill +* btcli proxy execute +""" + + +def test_proxy_create(local_chain, wallet_setup): + """ + Tests the pure proxy logic (create/kill) + + Steps: + 1. Creates pure proxy (with delay) + 2. Fund pure proxy + 3. Verifies pure proxy balance + 4. Ensures unannounced call fails (bc of delay at creation) + 4. Makes announcement of pure proxy's intent to transfer to Bob + 5. Executes previous announcement of transfer to Bob + 6. Ensures Bob has received the funds + 7. Makes announcement of pure proxy's intent to kill + 8. Kills pure proxy + + """ + testing_db_loc = "/tmp/btcli-test.db" + os.environ["BTCLI_PROXIES_PATH"] = testing_db_loc + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + + # Create wallets for Alice and Bob + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup( + wallet_path_bob + ) + proxy_type = "Any" + delay = 1 + + try: + # create a pure proxy + create_result = exec_command_alice( + command="proxy", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy-type", + proxy_type, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + create_result_output = json.loads(create_result.stdout) + assert create_result_output["success"] is True + assert create_result_output["message"] is not None + assert create_result_output["extrinsic_identifier"] is not None + created_extrinsic_id = create_result_output["extrinsic_identifier"].split("-") + created_block = int(created_extrinsic_id[0]) + created_extrinsic_idx = int(created_extrinsic_id[1]) + created_pure = create_result_output["data"]["pure"] + spawner = create_result_output["data"]["spawner"] + created_proxy_type = create_result_output["data"]["proxy_type"] + created_delay = create_result_output["data"]["delay"] + assert isinstance(created_pure, str) + assert isinstance(spawner, str) + assert spawner == wallet_alice.coldkeypub.ss58_address + assert created_proxy_type == proxy_type + assert created_delay == delay + print("Passed pure creation.") + + # transfer some funds from alice to the pure proxy + amount_to_transfer = 1_000 + transfer_result = exec_command_alice( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--dest", + created_pure, + "--amount", + str(amount_to_transfer), + "--no-prompt", + "--json-output", + ], + ) + transfer_result_output = json.loads(transfer_result.stdout) + assert transfer_result_output["success"] is True + + # ensure the proxy has the transferred funds + balance_result = exec_command_alice( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--ss58", + created_pure, + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["Provided Address 1"]["coldkey"] + == created_pure + ) + assert balance_result_output["balances"]["Provided Address 1"]["free"] == float( + amount_to_transfer + ) + + # transfer some of the pure proxy's funds to bob, but don't announce it + amount_to_transfer_proxy = 100 + transfer_result_proxy = exec_command_alice( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--dest", + keypair_bob.ss58_address, + "--amount", + str(amount_to_transfer_proxy), + "--no-prompt", + "--json-output", + ], + ) + transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) + # should fail, because it wasn't announced + assert transfer_result_proxy_output["success"] is False + + # announce the same extrinsic + transfer_result_proxy = exec_command_alice( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--dest", + keypair_bob.ss58_address, + "--amount", + str(amount_to_transfer_proxy), + "--no-prompt", + "--json-output", + "--announce-only", + ], + ) + print(transfer_result_proxy.stdout, transfer_result_proxy.stderr) + transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) + assert transfer_result_proxy_output["success"] is True + with ProxyAnnouncements.get_db() as (conn, cursor): + rows = ProxyAnnouncements.read_rows(conn, cursor, include_header=False) + latest_announcement = next( + iter(sorted(rows, key=lambda row: row[2], reverse=True)) + ) # sort by epoch time + ( + idx, + address, + epoch_time, + block, + call_hash, + call, + call_serialized, + executed_int, + ) = latest_announcement + assert address == created_pure + assert executed_int == 0 + + # wait for delay (probably already happened if fastblocks is on) + time.sleep(3) + + # get Bob's initial balance + balance_result = exec_command_bob( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["default"]["coldkey"] + == wallet_bob.coldkeypub.ss58_address + ) + bob_init_balance = balance_result_output["balances"]["default"]["free"] + + announce_execution_result = exec_command_alice( + command="proxy", + sub_command="execute", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--call-hash", + call_hash, + "--no-prompt", + "--json-output", + ], + ) + announce_execution_result_output = json.loads(announce_execution_result.stdout) + assert announce_execution_result_output["success"] is True + assert announce_execution_result_output["message"] == "" + + # ensure bob has the transferred funds + balance_result = exec_command_bob( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["default"]["coldkey"] + == wallet_bob.coldkeypub.ss58_address + ) + assert ( + balance_result_output["balances"]["default"]["free"] + == float(amount_to_transfer_proxy) + bob_init_balance + ) + print("Passed transfer with announcement") + + # announce kill of the created pure proxy + announce_kill_result = exec_command_alice( + command="proxy", + sub_command="kill", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--height", + str(created_block), + "--ext-index", + str(created_extrinsic_idx), + "--spawner", + spawner, + "--proxy-type", + created_proxy_type, + "--proxy", + created_pure, + "--json-output", + "--no-prompt", + "--announce-only", + ], + ) + print(announce_kill_result.stdout, announce_kill_result.stderr) + kill_result_output = json.loads(announce_kill_result.stdout) + assert kill_result_output["success"] is True + assert kill_result_output["message"] == "" + assert isinstance(kill_result_output["extrinsic_identifier"], str) + print("Passed kill announcement") + + with ProxyAnnouncements.get_db() as (conn, cursor): + rows = ProxyAnnouncements.read_rows(conn, cursor, include_header=False) + latest_announcement = next( + iter(sorted(rows, key=lambda row: row[2], reverse=True)) + ) # sort by epoch time + ( + idx, + address, + epoch_time, + block, + call_hash, + call, + call_serialized, + executed_int, + ) = latest_announcement + assert address == created_pure + assert executed_int == 0 + # wait for delay (probably already happened if fastblocks is on) + time.sleep(3) + + kill_announce_execution_result = exec_command_alice( + command="proxy", + sub_command="execute", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--call-hash", + call_hash, + "--no-prompt", + "--json-output", + ], + ) + kill_announce_execution_result_output = json.loads( + kill_announce_execution_result.stdout + ) + assert kill_announce_execution_result_output["success"] is True + assert kill_announce_execution_result_output["message"] == "" + finally: + os.environ["BTCLI_PROXIES_PATH"] = "" + if os.path.exists(testing_db_loc): + os.remove(testing_db_loc) + + +def test_add_proxy(local_chain, wallet_setup): + """ + Tests the non-pure (delegated) proxy logic (add/remove) + + Steps: + 1. Add Dave as a proxy of Alice (with delay) + 2. Attempt proxy transfer without announcement (it should fail) + 3. Make proxy transfer to Bob + 4. Ensure Bob got the funds, the funds were deducted from Alice, and that Dave paid the ext fee + 5. Remove Dave as a proxy of Alice + """ + testing_db_loc = "/tmp/btcli-test.db" + os.environ["BTCLI_PROXIES_PATH"] = testing_db_loc + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + wallet_path_dave = "//Dave" + + # Create wallets for Alice and Bob + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup( + wallet_path_bob + ) + keypair_dave, wallet_dave, wallet_path_dave, exec_command_dave = wallet_setup( + wallet_path_dave + ) + proxy_type = "Any" + delay = 1 + + try: + # add Dave as a proxy of Alice + add_result = exec_command_alice( + command="proxy", + sub_command="add", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + wallet_dave.coldkeypub.ss58_address, + "--proxy-type", + proxy_type, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + add_result_output = json.loads(add_result.stdout) + assert add_result_output["success"] is True + assert "Added proxy delegatee" in add_result_output["message"] + assert ( + add_result_output["data"]["delegatee"] + == wallet_dave.coldkeypub.ss58_address + ) + assert ( + add_result_output["data"]["delegator"] + == wallet_alice.coldkeypub.ss58_address + ) + assert add_result_output["data"]["proxy_type"] == proxy_type + assert add_result_output["data"]["delay"] == delay + print("Proxy Add successful") + + # Check dave's init balance + dave_balance_result = exec_command_dave( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--wallet-name", + "default", + "--json-output", + "--chain", + "ws://127.0.0.1:9945", + ], + ) + dave_balance_output = json.loads(dave_balance_result.stdout) + assert ( + dave_balance_output["balances"]["default"]["coldkey"] + == wallet_dave.coldkeypub.ss58_address + ) + dave_init_balance = dave_balance_output["balances"]["default"]["free"] + + # Check Bob's init balance + balance_result = exec_command_bob( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["default"]["coldkey"] + == wallet_bob.coldkeypub.ss58_address + ) + bob_init_balance = balance_result_output["balances"]["default"]["free"] + + # check alice's init balance + balance_result = exec_command_alice( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["default"]["coldkey"] + == wallet_alice.coldkeypub.ss58_address + ) + alice_init_balance = balance_result_output["balances"]["default"]["free"] + + # transfer some of alice's funds to bob through the proxy, but don't announce it + amount_to_transfer_proxy = 100 + transfer_result_proxy = exec_command_dave( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + wallet_alice.coldkeypub.ss58_address, + "--dest", + keypair_bob.ss58_address, + "--amount", + str(amount_to_transfer_proxy), + "--no-prompt", + "--json-output", + ], + ) + transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) + # should fail, because it wasn't announced + assert transfer_result_proxy_output["success"] is False + + # announce the same extrinsic + transfer_result_proxy = exec_command_dave( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + wallet_alice.coldkeypub.ss58_address, + "--dest", + keypair_bob.ss58_address, + "--amount", + str(amount_to_transfer_proxy), + "--no-prompt", + "--json-output", + "--announce-only", + ], + ) + print(transfer_result_proxy.stdout, transfer_result_proxy.stderr) + transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) + assert transfer_result_proxy_output["success"] is True + with ProxyAnnouncements.get_db() as (conn, cursor): + rows = ProxyAnnouncements.read_rows(conn, cursor, include_header=False) + latest_announcement = next( + iter(sorted(rows, key=lambda row: row[2], reverse=True)) + ) # sort by epoch time + ( + idx, + address, + epoch_time, + block, + call_hash, + call, + call_serialized, + executed_int, + ) = latest_announcement + assert address == wallet_alice.coldkeypub.ss58_address + assert executed_int == 0 + + # wait for delay (probably already happened if fastblocks is on) + time.sleep(3) + + announce_execution_result = exec_command_dave( + command="proxy", + sub_command="execute", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + wallet_alice.coldkeypub.ss58_address, + "--call-hash", + call_hash, + "--no-prompt", + "--json-output", + ], + ) + announce_execution_result_output = json.loads(announce_execution_result.stdout) + assert announce_execution_result_output["success"] is True + assert announce_execution_result_output["message"] == "" + + # ensure bob has the transferred funds + balance_result = exec_command_bob( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["default"]["coldkey"] + == wallet_bob.coldkeypub.ss58_address + ) + assert ( + balance_result_output["balances"]["default"]["free"] + == float(amount_to_transfer_proxy) + bob_init_balance + ) + + # ensure the amount was subtracted from alice's balance, not dave's + balance_result = exec_command_alice( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["default"]["coldkey"] + == wallet_alice.coldkeypub.ss58_address + ) + assert balance_result_output["balances"]["default"][ + "free" + ] == alice_init_balance - float(amount_to_transfer_proxy) + + # ensure dave paid the extrinsic fee + balance_result = exec_command_dave( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["default"]["coldkey"] + == wallet_dave.coldkeypub.ss58_address + ) + assert balance_result_output["balances"]["default"]["free"] < dave_init_balance + + print("Passed transfer with announcement") + + # remove the proxy + remove_result = exec_command_alice( + command="proxy", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + wallet_dave.coldkeypub.ss58_address, + "--proxy-type", + proxy_type, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + remove_result_output = json.loads(remove_result.stdout) + assert remove_result_output["success"] is True + assert remove_result_output["message"] == "" + assert isinstance(remove_result_output["extrinsic_identifier"], str) + print("Passed proxy removal") + finally: + os.environ["BTCLI_PROXIES_PATH"] = "" + if os.path.exists(testing_db_loc): + os.remove(testing_db_loc) diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 072df278c..8f17341a0 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -388,9 +388,7 @@ def test_unstaking(local_chain, wallet_setup): ], ) - assert ( - "✅ Finalized: Successfully unstaked all Alpha stakes" in unstake_alpha.stdout - ) + assert "✅ Included: Successfully unstaked all Alpha stakes" in unstake_alpha.stdout assert "Your extrinsic has been included" in unstake_alpha.stdout, ( unstake_alpha.stdout ) @@ -443,6 +441,6 @@ def test_unstaking(local_chain, wallet_setup): "144", ], ) - assert "✅ Finalized: Successfully unstaked all stakes from" in unstake_all.stdout + assert "✅ Included: Successfully unstaked all stakes from" in unstake_all.stdout assert "Your extrinsic has been included" in unstake_all.stdout, unstake_all.stdout print("Passed unstaking tests 🎉") diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index a17ed8406..a061910e5 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -228,3 +228,434 @@ def test_swap_hotkey_netuid_1_no_warning(mock_console): assert not any( "WARNING" in str(call) and "netuid 0" in str(call) for call in warning_calls ) + + +# ============================================================================ +# Tests for proxy parameter handling +# ============================================================================ + + +def test_is_valid_proxy_name_or_ss58_with_none_proxy(): + """Test that None proxy is accepted when announce_only is False""" + cli_manager = CLIManager() + result = cli_manager.is_valid_proxy_name_or_ss58(None, announce_only=False) + assert result is None + + +def test_is_valid_proxy_name_or_ss58_raises_with_announce_only_without_proxy(): + """Test that announce_only=True without proxy raises BadParameter""" + cli_manager = CLIManager() + with pytest.raises(typer.BadParameter) as exc_info: + cli_manager.is_valid_proxy_name_or_ss58(None, announce_only=True) + assert "Cannot supply '--announce-only' without supplying '--proxy'" in str( + exc_info.value + ) + + +def test_is_valid_proxy_name_or_ss58_with_valid_ss58(): + """Test that a valid SS58 address is accepted""" + cli_manager = CLIManager() + valid_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + result = cli_manager.is_valid_proxy_name_or_ss58(valid_ss58, announce_only=False) + assert result == valid_ss58 + + +def test_is_valid_proxy_name_or_ss58_with_invalid_ss58(): + """Test that an invalid SS58 address raises BadParameter""" + cli_manager = CLIManager() + invalid_ss58 = "invalid_address" + with pytest.raises(typer.BadParameter) as exc_info: + cli_manager.is_valid_proxy_name_or_ss58(invalid_ss58, announce_only=False) + assert "Invalid SS58 address" in str(exc_info.value) + + +def test_is_valid_proxy_name_or_ss58_with_proxy_from_config(): + """Test that a proxy name from config is resolved to SS58 address""" + cli_manager = CLIManager() + valid_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + cli_manager.proxies = {"my_proxy": {"address": valid_ss58}} + + result = cli_manager.is_valid_proxy_name_or_ss58("my_proxy", announce_only=False) + assert result == valid_ss58 + + +def test_is_valid_proxy_name_or_ss58_with_invalid_proxy_from_config(): + """Test that an invalid SS58 in config raises BadParameter""" + cli_manager = CLIManager() + cli_manager.proxies = {"my_proxy": {"address": "invalid_address"}} + + with pytest.raises(typer.BadParameter) as exc_info: + cli_manager.is_valid_proxy_name_or_ss58("my_proxy", announce_only=False) + assert "Invalid SS58 address" in str(exc_info.value) + assert "from config" in str(exc_info.value) + + +@patch("bittensor_cli.cli.is_valid_ss58_address") +def test_wallet_transfer_calls_proxy_validation(mock_is_valid_ss58): + """Test that wallet_transfer calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + mock_is_valid_ss58.return_value = True + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet_ask.return_value = Mock() + + cli_manager.wallet_transfer( + destination_ss58_address="5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + amount=10.0, + transfer_all=False, + allow_death=False, + period=100, + proxy=valid_proxy, + announce_only=False, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + network=None, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +@patch("bittensor_cli.cli.is_valid_ss58_address") +def test_wallet_transfer_with_announce_only_requires_proxy(mock_is_valid_ss58): + """Test that wallet_transfer with announce_only=True requires proxy""" + cli_manager = CLIManager() + mock_is_valid_ss58.return_value = True + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + ): + mock_wallet_ask.return_value = Mock() + + with pytest.raises(typer.BadParameter) as exc_info: + cli_manager.wallet_transfer( + destination_ss58_address="5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + amount=10.0, + transfer_all=False, + allow_death=False, + period=100, + proxy=None, + announce_only=True, # announce_only without proxy should fail + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + network=None, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + assert "Cannot supply '--announce-only' without supplying '--proxy'" in str( + exc_info.value + ) + + +def test_stake_add_calls_proxy_validation(): + """Test that stake_add calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch.object(cli_manager, "ask_safe_staking", return_value=False), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet_ask.return_value = Mock() + + cli_manager.stake_add( + stake_all=False, + amount=10.0, + include_hotkeys="", + exclude_hotkeys="", + all_hotkeys=False, + netuids="1", + all_netuids=False, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + proxy=valid_proxy, + announce_only=False, + network=None, + rate_tolerance=None, + safe_staking=False, + allow_partial_stake=None, + period=100, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_stake_remove_calls_proxy_validation(): + """Test that stake_remove calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch.object(cli_manager, "ask_safe_staking", return_value=False), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet_ask.return_value = Mock() + + cli_manager.stake_remove( + network=None, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + netuid=1, + all_netuids=False, + unstake_all=False, + unstake_all_alpha=False, + amount=10.0, + hotkey_ss58_address="", + include_hotkeys="", + exclude_hotkeys="", + all_hotkeys=False, + proxy=valid_proxy, + announce_only=False, + rate_tolerance=None, + safe_staking=False, + allow_partial_stake=None, + period=100, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_wallet_associate_hotkey_calls_proxy_validation(): + """Test that wallet_associate_hotkey calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + valid_hotkey = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch("bittensor_cli.cli.is_valid_ss58_address", return_value=True), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet = Mock() + mock_wallet.name = "test_wallet" + mock_wallet_ask.return_value = mock_wallet + + cli_manager.wallet_associate_hotkey( + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey=valid_hotkey, + network=None, + proxy=valid_proxy, + announce_only=False, + prompt=False, + quiet=True, + verbose=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_wallet_set_id_calls_proxy_validation(): + """Test that wallet_set_id calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet_ask.return_value = Mock() + + cli_manager.wallet_set_id( + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + network=None, + name="Test Name", + web_url="https://example.com", + image_url="https://example.com/image.png", + discord="testuser", + description="Test description", + additional="Additional info", + github_repo="test/repo", + proxy=valid_proxy, + announce_only=False, + quiet=True, + verbose=False, + prompt=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_wallet_swap_coldkey_calls_proxy_validation(): + """Test that wallet_swap_coldkey calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + new_coldkey = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch("bittensor_cli.cli.is_valid_ss58_address", return_value=True), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet = Mock() + mock_wallet.coldkeypub = Mock() + mock_wallet.coldkeypub.ss58_address = ( + "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + ) + mock_wallet_ask.return_value = mock_wallet + + cli_manager.wallet_swap_coldkey( + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + new_wallet_or_ss58=new_coldkey, + network=None, + proxy=valid_proxy, + announce_only=False, + quiet=True, + verbose=False, + force_swap=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_stake_move_calls_proxy_validation(): + """Test that stake_move calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + dest_hotkey = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command", return_value=(None, None)), + patch("bittensor_cli.cli.is_valid_ss58_address", return_value=True), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet = Mock() + mock_wallet.hotkey_str = "test_hotkey" + mock_wallet_ask.return_value = mock_wallet + + cli_manager.stake_move( + network=None, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + origin_netuid=1, + destination_netuid=2, + destination_hotkey=dest_hotkey, + amount=10.0, + stake_all=False, + proxy=valid_proxy, + announce_only=False, + period=100, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_stake_transfer_calls_proxy_validation(): + """Test that stake_transfer calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + dest_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command", return_value=(None, None)), + patch("bittensor_cli.cli.is_valid_ss58_address", return_value=True), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet = Mock() + mock_wallet.hotkey_str = "test_hotkey" + mock_wallet_ask.return_value = mock_wallet + + cli_manager.stake_transfer( + network=None, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + origin_netuid=1, + dest_netuid=2, + dest_ss58=dest_ss58, + amount=10.0, + stake_all=False, + period=100, + proxy=valid_proxy, + announce_only=False, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False)