diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c2d91a3c..910ed0839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 9.1.1 /2025-03-06 + +## What's Changed +* fix: int() argument must be a string, a bytes-like object or a real n… by @0xxfu in https://github.com/opentensor/btcli/pull/352 +* Change to pyproject toml by @thewhaleking in https://github.com/opentensor/btcli/pull/357 +* Feat: Dashboard improvements by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/350 +* Improves stake transfer, adds interactive selection of delegates by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/358 +* Removes hidden flags for unstaking all by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/359 +* Removes `typer.Exit` exceptions in commands by @thewhaleking in https://github.com/opentensor/btcli/pull/353 +* Add transaction fee check inter-subnet movement by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/361 +* Backmerge main to staging 910 by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/362 + +## New Contributors +* @0xxfu made their first contribution in https://github.com/opentensor/btcli/pull/352 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.1.0...v9.1.1 + ## 9.1.0 /2025-03-01 ## What's Changed diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6619e948b..79f4d2fef 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -109,6 +109,18 @@ class Options: "--wallet.hotkey", help="Hotkey of the wallet", ) + wallet_hotkey_ss58 = typer.Option( + None, + "--hotkey", + "--hotkey-ss58", + "-H", + "--wallet_hotkey", + "--wallet_hotkey_ss58", + "--wallet-hotkey", + "--wallet-hotkey-ss58", + "--wallet.hotkey", + help="Hotkey name or SS58 address of the hotkey", + ) mnemonic = typer.Option( None, help='Mnemonic used to regenerate your key. For example: "horse cart dog ..."', @@ -246,6 +258,15 @@ class Options: "--allow-partial/--not-partial", help="Enable or disable partial stake mode (default: disabled).", ) + dashboard_path = typer.Option( + None, + "--dashboard-path", + "--dashboard_path", + "--dash_path", + "--dash.path", + "--dashboard.path", + help="Path to save the dashboard HTML file. For example: `~/.bittensor/dashboard`.", + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -525,6 +546,7 @@ def __init__(self): "rate_tolerance": None, "safe_staking": True, "allow_partial_stake": False, + "dashboard_path": None, # Commenting this out as this needs to get updated # "metagraph_cols": { # "UID": True, @@ -1119,6 +1141,7 @@ def set_config( "--partial/--no-partial", "--allow/--not-allow", ), + dashboard_path: Optional[str] = Options.dashboard_path, ): """ Sets or updates configuration values in the BTCLI config file. @@ -1148,6 +1171,7 @@ def set_config( "rate_tolerance": rate_tolerance, "safe_staking": safe_staking, "allow_partial_stake": allow_partial_stake, + "dashboard_path": dashboard_path, } bools = ["use_cache", "safe_staking", "allow_partial_stake"] if all(v is None for v in args.values()): @@ -1257,6 +1281,7 @@ def del_config( "--allow/--not-allow", ), all_items: bool = typer.Option(False, "--all"), + dashboard_path: Optional[str] = Options.dashboard_path, ): """ Clears the fields in the config file and sets them to 'None'. @@ -1288,6 +1313,7 @@ def del_config( "rate_tolerance": rate_tolerance, "safe_staking": safe_staking, "allow_partial_stake": allow_partial_stake, + "dashboard_path": dashboard_path, } # If no specific argument is provided, iterate over all @@ -2937,7 +2963,7 @@ def stake_add( ), exit_early=False, ) - if selected_hotkey is None: + if not selected_hotkey: print_error("No delegate selected. Exiting.") raise typer.Exit() include_hotkeys = selected_hotkey @@ -3048,14 +3074,12 @@ def stake_remove( False, "--unstake-all", "--all", - hidden=True, help="When set, this command unstakes all staked TAO + Alpha from the all hotkeys.", ), unstake_all_alpha: bool = typer.Option( False, "--unstake-all-alpha", "--all-alpha", - hidden=True, help="When set, this command unstakes all staked Alpha from the all hotkeys.", ), amount: float = typer.Option( @@ -3337,7 +3361,7 @@ def stake_move( network: Optional[list[str]] = Options.network, wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, + wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, origin_netuid: Optional[int] = typer.Option( None, "--origin-netuid", help="Origin netuid" ), @@ -3357,6 +3381,8 @@ def stake_move( False, "--stake-all", "--all", help="Stake all", prompt=False ), prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, ): """ Move staked TAO between hotkeys while keeping the same coldkey ownership. @@ -3378,6 +3404,7 @@ def stake_move( [green]$[/green] btcli stake move """ + self.verbosity_handler(quiet, verbose) console.print( "[dim]This command moves stake from one hotkey to another hotkey while keeping the same coldkey.[/dim]" ) @@ -3490,7 +3517,7 @@ def stake_transfer( network: Optional[list[str]] = Options.network, wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, + wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, origin_netuid: Optional[int] = typer.Option( None, "--origin-netuid", @@ -3514,6 +3541,9 @@ def stake_transfer( "-a", help="Amount of stake to transfer", ), + stake_all: bool = typer.Option( + False, "--stake-all", "--all", help="Stake all", prompt=False + ), prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3531,6 +3561,8 @@ def stake_transfer( - The destination subnet (--dest-netuid) - The destination wallet/address (--dest) - The amount to transfer (--amount) + - The origin wallet (--wallet-name) + - The origin hotkey wallet/address (--wallet-hotkey) If no arguments are provided, an interactive selection menu will be shown. @@ -3539,14 +3571,37 @@ def stake_transfer( Transfer 100 TAO from subnet 1 to subnet 2: [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --dest wallet2 --amount 100 - Using SS58 address: + Using Destination SS58 address: [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --dest 5FrLxJsyJ5x9n2rmxFwosFraxFCKcXZDngEP9H7qjkKgHLcK --amount 100 + + Using Origin hotkey SS58 address (useful when transferring stake from a delegate): + [green]$[/green] btcli stake transfer --wallet-hotkey 5FrLxJsyJ5x9n2rmxFwosFraxFCKcXZDngEP9H7qjkKgHLcK --wallet-name sample_wallet + + Transfer all available stake from origin hotkey: + [green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2 """ console.print( "[dim]This command transfers stake from one coldkey to another while keeping the same hotkey.[/dim]" ) self.verbosity_handler(quiet, verbose) + if not dest_ss58: + dest_ss58 = Prompt.ask( + "Enter the [blue]destination wallet name[/blue] or [blue]coldkey SS58 address[/blue]" + ) + + if is_valid_ss58_address(dest_ss58): + dest_ss58 = dest_ss58 + else: + dest_wallet = self.wallet_ask( + dest_ss58, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + dest_ss58 = dest_wallet.coldkeypub.ss58_address + if not wallet_name: wallet_name = Prompt.ask( "Enter the [blue]origin wallet name[/blue]", @@ -3556,13 +3611,16 @@ def stake_transfer( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME] ) + interactive_selection = False if not wallet_hotkey: origin_hotkey = Prompt.ask( - "Enter the [blue]origin hotkey[/blue] name or " - "[blue]ss58 address[/blue] where the stake will be moved from", - default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + "Enter the [blue]origin hotkey[/blue] name or ss58 address [bold](stake will be transferred FROM here)[/bold] " + "[dim](or press Enter to select from existing stakes)[/dim]" ) - if is_valid_ss58_address(origin_hotkey): + if origin_hotkey == "": + interactive_selection = True + + elif is_valid_ss58_address(origin_hotkey): origin_hotkey = origin_hotkey else: wallet = self.wallet_ask( @@ -3586,33 +3644,11 @@ def stake_transfer( ) origin_hotkey = wallet.hotkey.ss58_address - if not dest_ss58: - dest_ss58 = Prompt.ask( - "Enter the [blue]destination wallet name[/blue] or [blue]coldkey SS58 address[/blue]" - ) - - if is_valid_ss58_address(dest_ss58): - dest_ss58 = dest_ss58 - else: - dest_wallet = self.wallet_ask( - dest_ss58, - wallet_path, - None, - ask_for=[WO.NAME, WO.PATH], - validate=WV.WALLET, - ) - dest_ss58 = dest_wallet.coldkeypub.ss58_address - - interactive_selection = False - if origin_netuid is None and dest_netuid is None and not amount: - interactive_selection = True - else: + if not interactive_selection: if origin_netuid is None: origin_netuid = IntPrompt.ask( "Enter the [blue]origin subnet[/blue] (netuid)" ) - if not amount: - amount = FloatPrompt.ask("Enter the [blue]amount[/blue] to transfer") if dest_netuid is None: dest_netuid = IntPrompt.ask( @@ -3629,6 +3665,7 @@ def stake_transfer( dest_coldkey_ss58=dest_ss58, amount=amount, interactive_selection=interactive_selection, + stake_all=stake_all, prompt=prompt, ) ) @@ -5091,6 +5128,19 @@ def view_dashboard( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + coldkey_ss58: Optional[str] = typer.Option( + None, + "--coldkey-ss58", + "--ss58", + help="Coldkey SS58 address to view dashboard for", + ), + use_wry: bool = typer.Option( + False, "--use-wry", help="Use PyWry instead of browser window" + ), + save_file: bool = typer.Option( + False, "--save-file", "--save", help="Save the dashboard HTML file" + ), + dashboard_path: Optional[str] = Options.dashboard_path, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -5098,13 +5148,40 @@ def view_dashboard( Display html dashboard with subnets list, stake, and neuron information. """ self.verbosity_handler(quiet, verbose) - if is_linux(): + if use_wry and is_linux(): print_linux_dependency_message() - wallet = self.wallet_ask( - wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] - ) + + if use_wry and save_file: + print_error("Cannot save file when using PyWry.") + raise typer.Exit() + + if save_file: + if not dashboard_path: + dashboard_path = Prompt.ask( + "Enter the [blue]path[/blue] where the dashboard HTML file will be saved", + default=self.config.get("dashboard_path") + or defaults.dashboard.path, + ) + + if coldkey_ss58: + if not is_valid_ss58_address(coldkey_ss58): + print_error(f"Invalid SS58 address: {coldkey_ss58}") + raise typer.Exit() + wallet = None + else: + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] + ) + return self._run_command( - view.display_network_dashboard(wallet, self.initialize_chain(network)) + view.display_network_dashboard( + wallet=wallet, + subtensor=self.initialize_chain(network), + use_wry=use_wry, + save_file=save_file, + dashboard_path=dashboard_path, + coldkey_ss58=coldkey_ss58, + ) ) @staticmethod diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index d990aada9..2b879097f 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -138,6 +138,9 @@ class logging: record_log = False logging_dir = "~/.bittensor/miners" + class dashboard: + path = "~/.bittensor/dashboard/" + defaults = Defaults diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 59dd2fcb7..9e8692e89 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1251,7 +1251,7 @@ async def get_stake_for_coldkey_and_hotkey_on_netuid( if _result is None: return Balance(0).set_unit(netuid) else: - return Balance.from_rao(_result).set_unit(int(netuid)) + return Balance.from_rao(fixed_to_float(_result)).set_unit(int(netuid)) async def get_metagraph_info( self, netuid: int, block_hash: Optional[str] = None diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index ecc142862..5fbf37bf8 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -45,17 +45,33 @@ def __init__(self, hotkey_ss58=None): self.ss58_address = hotkey_ss58 +class _Coldkeypub: + def __init__(self, coldkey_ss58=None): + self.ss58_address = coldkey_ss58 + + class WalletLike: - def __init__(self, name=None, hotkey_ss58=None, hotkey_str=None): + def __init__( + self, + name=None, + hotkey_ss58=None, + hotkey_str=None, + coldkeypub_ss58=None, + ): self.name = name self.hotkey_ss58 = hotkey_ss58 self.hotkey_str = hotkey_str self._hotkey = _Hotkey(hotkey_ss58) + self._coldkeypub = _Coldkeypub(coldkeypub_ss58) @property def hotkey(self): return self._hotkey + @property + def coldkeypub(self): + return self._coldkeypub + def print_console(message: str, colour: str, title: str, console: Console): console.print( diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 2950da876..6932a2298 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -1,7 +1,6 @@ import asyncio from functools import partial -import typer from typing import TYPE_CHECKING, Optional from rich.table import Table from rich.prompt import Confirm, Prompt @@ -20,7 +19,6 @@ unlock_key, ) from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -338,7 +336,7 @@ async def stake_extrinsic( if prompt: if not Confirm.ask("Would you like to continue?"): - raise typer.Exit() + return False if not unlock_key(wallet).success: return False diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index 8523320ac..d5b5493b9 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -1,7 +1,6 @@ import asyncio from typing import TYPE_CHECKING, Optional -import typer from bittensor_wallet import Wallet from rich.prompt import Prompt @@ -428,7 +427,7 @@ def format_cell( if not hotkeys_to_substakes: print_error(f"No stakes found for coldkey ss58: ({coldkey_address})") - raise typer.Exit() + return if live: # Select one hotkey for live monitoring diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 4bbdcedbf..0aa23278d 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -1,10 +1,8 @@ import asyncio from typing import TYPE_CHECKING -import typer from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError from rich.table import Table from rich.prompt import Confirm, Prompt @@ -41,6 +39,11 @@ async def display_stake_movement_cross_subnets( subnet = await subtensor.subnet(origin_netuid) received_amount_tao = subnet.alpha_to_tao(amount_to_move) received_amount_tao -= MIN_STAKE_FEE + + if received_amount_tao < Balance.from_tao(0): + print_error("Not enough Alpha to pay the transaction fee.") + raise ValueError + received_amount = subnet.tao_to_alpha(received_amount_tao) slippage_pct_float = ( 100 * float(MIN_STAKE_FEE) / float(MIN_STAKE_FEE + received_amount_tao) @@ -72,7 +75,7 @@ async def display_stake_movement_cross_subnets( if received_amount < Balance.from_tao(0): print_error("Not enough Alpha to pay the transaction fee.") - raise typer.Exit() + raise ValueError ideal_amount = amount_to_move * price total_slippage = ideal_amount - received_amount @@ -207,7 +210,7 @@ def prompt_stake_amount( console.print("[red]Please enter a valid number or 'all'[/red]") -async def stake_move_selection( +async def stake_move_transfer_selection( subtensor: "SubtensorInterface", wallet: Wallet, ): @@ -228,7 +231,7 @@ async def stake_move_selection( if not hotkey_stakes: print_error("You have no stakes to move.") - raise typer.Exit() + raise ValueError # Display hotkeys with stakes table = Table( @@ -293,25 +296,24 @@ async def stake_move_selection( title_justify="center", width=len(origin_hotkey_ss58) + 20, ) - table.add_column("Index", justify="right") table.add_column("Netuid", style="cyan") table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) available_netuids = [] - for idx, netuid in enumerate(origin_hotkey_info["netuids"]): + for netuid in origin_hotkey_info["netuids"]: stake = origin_hotkey_info["stakes"][netuid] if stake.tao > 0: available_netuids.append(netuid) - table.add_row(str(idx), str(netuid), str(stake)) + table.add_row(str(netuid), str(stake)) console.print("\n", table) # Select origin netuid - netuid_idx = Prompt.ask( - "\nEnter the index of the subnet you want to move stake from", - choices=[str(i) for i in range(len(available_netuids))], + origin_netuid = Prompt.ask( + "\nEnter the netuid you want to move stake from", + choices=[str(netuid) for netuid in available_netuids], ) - origin_netuid = available_netuids[int(netuid_idx)] + origin_netuid = int(origin_netuid) origin_stake = origin_hotkey_info["stakes"][origin_netuid] # Ask for amount to move @@ -334,104 +336,6 @@ async def stake_move_selection( } -async def stake_transfer_selection( - wallet: Wallet, subtensor: "SubtensorInterface", origin_hotkey: str -): - """Selection interface for transferring stakes.""" - ( - stakes, - all_netuids, - all_subnets, - ) = await asyncio.gather( - subtensor.get_stake_for_coldkey(coldkey_ss58=wallet.coldkeypub.ss58_address), - subtensor.get_all_subnet_netuids(), - subtensor.all_subnets(), - ) - all_netuids = sorted(all_netuids) - all_subnets = {di.netuid: di for di in all_subnets} - - available_stakes = {} - for stake in stakes: - if stake.stake.tao > 0 and stake.hotkey_ss58 == origin_hotkey: - available_stakes[stake.netuid] = { - "hotkey_ss58": stake.hotkey_ss58, - "stake": stake.stake, - "is_registered": stake.is_registered, - } - - if not available_stakes: - console.print("[red]No stakes available to transfer.[/red]") - return None - - table = Table( - title=( - f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]" - f"Available Stakes to Transfer\n" - f"for wallet hotkey:\n" - f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey_str}: {wallet.hotkey.ss58_address}" - f"[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" - ), - show_edge=False, - header_style="bold white", - border_style="bright_black", - title_justify="center", - width=len(wallet.hotkey_str + wallet.hotkey.ss58_address) + 10, - ) - - table.add_column("Index", justify="right", style="cyan") - table.add_column("Netuid") - table.add_column("Name", style="cyan", justify="left") - table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) - table.add_column("Registered", justify="center") - - for idx, (netuid, stake_info) in enumerate(available_stakes.items()): - subnet_name_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{all_subnets[netuid].symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" - f" {get_subnet_name(all_subnets[netuid])}" - ) - table.add_row( - str(idx), - str(netuid), - subnet_name_cell, - str(stake_info["stake"]), - "[dark_sea_green3]YES" - if stake_info["is_registered"] - else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", - ) - - console.print(table) - - if not available_stakes: - console.print("[red]No stakes available to transfer.[/red]") - return None - - # Prompt to select index of stake to transfer - selection = Prompt.ask( - "\nEnter the index of the stake you want to transfer", - choices=[str(i) for i in range(len(available_stakes))], - ) - selected_netuid = list(available_stakes.keys())[int(selection)] - selected_stake = available_stakes[selected_netuid] - - # Prompt for amount - stake_balance = selected_stake["stake"] - amount, _ = prompt_stake_amount(stake_balance, selected_netuid, "transfer") - - # Prompt for destination subnet - destination_netuid = Prompt.ask( - "\nEnter the netuid of the subnet you want to move stake to" - + f" ([dim]{group_subnets(all_netuids)}[/dim])", - choices=[str(netuid) for netuid in all_netuids], - show_choices=False, - ) - - return { - "origin_netuid": selected_netuid, - "amount": amount.tao, - "destination_netuid": int(destination_netuid), - } - - async def stake_swap_selection( subtensor: "SubtensorInterface", wallet: Wallet, @@ -457,7 +361,7 @@ async def stake_swap_selection( if not hotkey_stakes: print_error(f"No stakes found for hotkey: {wallet.hotkey_str}") - raise typer.Exit() + raise ValueError # Display available stakes table = Table( @@ -540,7 +444,10 @@ async def move_stake( prompt: bool = True, ): if interactive_selection: - selection = await stake_move_selection(subtensor, wallet) + try: + selection = await stake_move_transfer_selection(subtensor, wallet) + except ValueError: + return False origin_hotkey = selection["origin_hotkey"] origin_netuid = selection["origin_netuid"] amount = selection["amount"] @@ -571,7 +478,7 @@ async def move_stake( f"in Netuid: " f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" ) - raise typer.Exit() + return False console.print( f"\nOrigin Netuid: " @@ -609,16 +516,19 @@ async def move_stake( # Slippage warning if prompt: - await display_stake_movement_cross_subnets( - subtensor=subtensor, - origin_netuid=origin_netuid, - destination_netuid=destination_netuid, - origin_hotkey=origin_hotkey, - destination_hotkey=destination_hotkey, - amount_to_move=amount_to_move_as_balance, - ) + try: + await display_stake_movement_cross_subnets( + subtensor=subtensor, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_hotkey=origin_hotkey, + destination_hotkey=destination_hotkey, + amount_to_move=amount_to_move_as_balance, + ) + except ValueError: + return False if not Confirm.ask("Would you like to continue?"): - raise typer.Exit() + return False # Perform moving operation. if not unlock_key(wallet).success: @@ -699,6 +609,7 @@ async def transfer_stake( dest_netuid: int, dest_coldkey_ss58: str, interactive_selection: bool = False, + stake_all: bool = False, prompt: bool = True, ) -> bool: """Transfers stake from one network to another. @@ -717,12 +628,13 @@ async def transfer_stake( Returns: bool: True if transfer was successful, False otherwise. """ - origin_hotkey = origin_hotkey or wallet.hotkey.ss58_address if interactive_selection: - selection = await stake_transfer_selection(wallet, subtensor, origin_hotkey) + selection = await stake_move_transfer_selection(subtensor, wallet) origin_netuid = selection["origin_netuid"] amount = selection["amount"] dest_netuid = selection["destination_netuid"] + stake_all = selection["stake_all"] + origin_hotkey = selection["origin_hotkey"] # Check if both subnets exist block_hash = await subtensor.substrate.get_chain_head() @@ -750,7 +662,22 @@ async def transfer_stake( hotkey_ss58=origin_hotkey, netuid=dest_netuid, ) - amount_to_transfer = Balance.from_tao(amount).set_unit(origin_netuid) + + if current_stake.tao == 0: + err_console.print( + f"[red]No stake found for hotkey: {origin_hotkey} on netuid: {origin_netuid}[/red]" + ) + return False + + amount_to_transfer = None + if amount: + amount_to_transfer = Balance.from_tao(amount).set_unit(origin_netuid) + elif stake_all: + amount_to_transfer = current_stake + else: + amount_to_transfer, _ = prompt_stake_amount( + current_stake, origin_netuid, "transfer" + ) # Check if enough stake to transfer if amount_to_transfer > current_stake: @@ -763,17 +690,20 @@ async def transfer_stake( # Slippage warning if prompt: - await display_stake_movement_cross_subnets( - subtensor=subtensor, - origin_netuid=origin_netuid, - destination_netuid=dest_netuid, - origin_hotkey=origin_hotkey, - destination_hotkey=origin_hotkey, - amount_to_move=amount_to_transfer, - ) + try: + await display_stake_movement_cross_subnets( + subtensor=subtensor, + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + origin_hotkey=origin_hotkey, + destination_hotkey=origin_hotkey, + amount_to_move=amount_to_transfer, + ) + except ValueError: + return False if not Confirm.ask("Would you like to continue?"): - raise typer.Exit() + return False # Perform transfer operation if not unlock_key(wallet).success: @@ -868,7 +798,10 @@ async def swap_stake( """ hotkey_ss58 = wallet.hotkey.ss58_address if interactive_selection: - selection = await stake_swap_selection(subtensor, wallet) + try: + selection = await stake_swap_selection(subtensor, wallet) + except ValueError: + return False origin_netuid = selection["origin_netuid"] amount = selection["amount"] destination_netuid = selection["destination_netuid"] @@ -916,17 +849,20 @@ async def swap_stake( # Slippage warning if prompt: - await display_stake_movement_cross_subnets( - subtensor=subtensor, - origin_netuid=origin_netuid, - destination_netuid=destination_netuid, - origin_hotkey=hotkey_ss58, - destination_hotkey=hotkey_ss58, - amount_to_move=amount_to_swap, - ) + try: + await display_stake_movement_cross_subnets( + subtensor=subtensor, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + origin_hotkey=hotkey_ss58, + destination_hotkey=hotkey_ss58, + amount_to_move=amount_to_swap, + ) + except ValueError: + return False if not Confirm.ask("Would you like to continue?"): - raise typer.Exit() + return False # Perform swap operation if not unlock_key(wallet).success: diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index fc298de47..1097faf7b 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -2,10 +2,8 @@ from functools import partial from typing import TYPE_CHECKING, Optional -import typer from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm, Prompt from rich.table import Table @@ -67,13 +65,16 @@ async def unstake( all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} if interactive: - hotkeys_to_unstake_from, unstake_all_from_hk = await _unstake_selection( - all_sn_dynamic_info, - ck_hk_identities, - old_identities, - stake_infos, - netuid=netuid, - ) + try: + hotkeys_to_unstake_from, unstake_all_from_hk = await _unstake_selection( + all_sn_dynamic_info, + ck_hk_identities, + old_identities, + stake_infos, + netuid=netuid, + ) + except ValueError: + return False if unstake_all_from_hk: hotkey_to_unstake_all = hotkeys_to_unstake_from[0] unstake_all_alpha = Confirm.ask( @@ -266,7 +267,7 @@ async def unstake( _print_table_and_slippage(table, max_float_slippage, safe_staking) if prompt: if not Confirm.ask("Would you like to continue?"): - raise typer.Exit() + return False # Execute extrinsics if not unlock_key(wallet).success: @@ -823,7 +824,7 @@ async def _unstake_selection( ): if not stake_infos: print_error("You have no stakes to unstake.") - raise typer.Exit() + raise ValueError hotkey_stakes = {} for stake_info in stake_infos: @@ -840,7 +841,7 @@ async def _unstake_selection( print_error(f"You have no stakes to unstake in subnet {netuid}.") else: print_error("You have no stakes to unstake.") - raise typer.Exit() + raise ValueError hotkeys_info = [] for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()): diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 856812c01..be595ee8e 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -2,10 +2,8 @@ import json import sqlite3 from typing import TYPE_CHECKING, Optional, cast -import typer from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm, Prompt from rich.console import Group from rich.progress import Progress, BarColumn, TextColumn @@ -814,7 +812,7 @@ async def show_root(): root_info = next((s for s in all_subnets if s.netuid == 0), None) if root_info is None: print_error("The root subnet does not exist") - raise typer.Exit() + return False root_state, identities, old_identities = await asyncio.gather( subtensor.get_subnet_state(netuid=0, block_hash=block_hash), @@ -1017,7 +1015,7 @@ async def show_root(): async def show_subnet(netuid_: int): if not await subtensor.subnet_exists(netuid=netuid): err_console.print(f"[red]Subnet {netuid} does not exist[/red]") - raise typer.Exit() + return False block_hash = await subtensor.substrate.get_chain_head() ( subnet_info, @@ -1036,15 +1034,15 @@ async def show_subnet(netuid_: int): ) if subnet_state is None: print_error(f"Subnet {netuid_} does not exist") - raise typer.Exit() + return False if subnet_info is None: print_error(f"Subnet {netuid_} does not exist") - raise typer.Exit() + return False if len(subnet_state.hotkeys) == 0: print_error(f"Subnet {netuid_} is currently empty with 0 UIDs registered.") - raise typer.Exit() + return False # Define table properties table = Table( @@ -1381,7 +1379,7 @@ async def create( " are you sure you wish to continue?" ): console.print(":cross_mark: Aborted!") - raise typer.Exit() + return False identity = prompt_for_identity( current_identity=current_identity, @@ -2135,7 +2133,7 @@ async def get_identity(subtensor: "SubtensorInterface", netuid: int, title: str if not await subtensor.subnet_exists(netuid): print_error(f"Subnet {netuid} does not exist.") - raise typer.Exit() + return None with console.status( ":satellite: [bold green]Querying subnet identity...", spinner="earth" diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 9e9098f53..ef9895d26 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -1,7 +1,6 @@ import asyncio from typing import TYPE_CHECKING, Union, Optional -import typer from bittensor_wallet import Wallet from rich import box from rich.table import Column, Table @@ -593,7 +592,7 @@ async def sudo_set_hyperparameter( if success: console.print("\n") print_verbose("Fetching hyperparameters") - await get_hyperparameters(subtensor, netuid=netuid) + return await get_hyperparameters(subtensor, netuid=netuid) async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): @@ -606,7 +605,7 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): subnet_info = await subtensor.subnet(netuid) if subnet_info is None: print_error(f"Subnet with netuid {netuid} does not exist.") - raise typer.Exit() + return False table = Table( Column("[white]HYPERPARAMETER", style=COLOR_PALETTE["SUDO"]["HYPERPARAMETER"]), diff --git a/bittensor_cli/src/commands/view.py b/bittensor_cli/src/commands/view.py index d6eb0263a..4108bdefa 100644 --- a/bittensor_cli/src/commands/view.py +++ b/bittensor_cli/src/commands/view.py @@ -1,5 +1,8 @@ import asyncio import json +import os +import tempfile +import webbrowser import netaddr from dataclasses import asdict, is_dataclass from typing import Any, Dict, List @@ -7,8 +10,9 @@ from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -from bittensor_cli.src.bittensor.utils import console +from bittensor_cli.src.bittensor.utils import console, WalletLike from bittensor_wallet import Wallet +from bittensor_cli.src import defaults root_symbol_html = f"{ord('τ'):X};" @@ -29,42 +33,80 @@ def default(self, obj): async def display_network_dashboard( wallet: Wallet, subtensor: "SubtensorInterface", - prompt: bool = True, + use_wry: bool = False, + save_file: bool = False, + dashboard_path: str = None, + coldkey_ss58: str = None, ) -> bool: """ Generate and display the HTML interface. """ + if coldkey_ss58: + wallet = WalletLike(coldkeypub_ss58=coldkey_ss58, name=coldkey_ss58[:7]) try: with console.status("[dark_sea_green3]Fetching data...", spinner="earth"): _subnet_data = await fetch_subnet_data(wallet, subtensor) subnet_data = process_subnet_data(_subnet_data) html_content = generate_full_page(subnet_data) - console.print( - "[dark_sea_green3]Opening dashboard in a window. Press Ctrl+C to close.[/dark_sea_green3]" - ) - window = PyWry() - window.send_html( - html=html_content, - title="Bittensor View", - width=1200, - height=800, - ) - window.start() - await asyncio.sleep(10) - try: - while True: - if _has_exited(window): - break - await asyncio.sleep(1) - except KeyboardInterrupt: - console.print("\n[yellow]Closing Bittensor View...[/yellow]") - finally: - if not _has_exited(window): - try: - window.close() - except Exception: - pass + if use_wry: + console.print( + "[dark_sea_green3]Opening dashboard in a window. Press Ctrl+C to close.[/dark_sea_green3]" + ) + window = PyWry() + window.send_html( + html=html_content, + title="Bittensor View", + width=1200, + height=800, + ) + window.start() + await asyncio.sleep(10) + try: + while True: + if _has_exited(window): + break + await asyncio.sleep(1) + except KeyboardInterrupt: + console.print("\n[yellow]Closing Bittensor View...[/yellow]") + finally: + if not _has_exited(window): + try: + window.close() + except Exception: + pass + else: + if save_file: + dir_path = os.path.expanduser(dashboard_path) + else: + dir_path = os.path.expanduser(defaults.dashboard.path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + with tempfile.NamedTemporaryFile( + delete=not save_file, + suffix=".html", + mode="w", + dir=dir_path, + prefix=f"{wallet.name}_{subnet_data['block_number']}_", + ) as f: + f.write(html_content) + temp_path = f.name + file_url = f"file://{os.path.abspath(temp_path)}" + + if not save_file: + with console.status( + "[dark_sea_green3]Loading dashboard...[/dark_sea_green3]", + spinner="material", + ): + webbrowser.open(file_url) + await asyncio.sleep(10) + return True + + console.print("[green]Dashboard View opened in your browser[/green]") + console.print(f"[yellow]The HTML file is saved at: {temp_path}[/yellow]") + webbrowser.open(file_url) + return True except Exception as e: print(f"Error: {e}") @@ -76,21 +118,33 @@ def int_to_ip(int_val: int) -> str: return str(netaddr.IPAddress(int_val)) -def get_hotkey_identity( +def get_identity( hotkey_ss58: str, identities: dict, old_identities: dict, trucate_length: int = 4, + return_bool: bool = False, + lookup_hk: bool = True, ) -> str: """Fetch identity of hotkey from both sources""" - if hk_identity := identities["hotkeys"].get(hotkey_ss58): - return hk_identity.get("identity", {}).get("name", "") or hk_identity.get( - "display", "~" - ) - elif old_identity := old_identities.get(hotkey_ss58): + if lookup_hk: + if hk_identity := identities["hotkeys"].get(hotkey_ss58): + return hk_identity.get("identity", {}).get("name", "") or hk_identity.get( + "display", "~" + ) + else: + if ck_identity := identities["coldkeys"].get(hotkey_ss58): + return ck_identity.get("identity", {}).get("name", "") or ck_identity.get( + "display", "~" + ) + + if old_identity := old_identities.get(hotkey_ss58): return old_identity.display else: - return f"{hotkey_ss58[:trucate_length]}...{hotkey_ss58[-trucate_length:]}" + if return_bool: + return False + else: + return f"{hotkey_ss58[:trucate_length]}...{hotkey_ss58[-trucate_length:]}" async def fetch_subnet_data( @@ -164,7 +218,7 @@ def process_subnet_data(raw_data: Dict[str, Any]) -> Dict[str, Any]: stake_dict.setdefault(stake.netuid, []).append( { "hotkey": stake.hotkey_ss58, - "hotkey_identity": get_hotkey_identity( + "hotkey_identity": get_identity( stake.hotkey_ss58, ck_hk_identities, old_identities ), "amount": stake.stake.tao, @@ -228,7 +282,7 @@ def process_subnet_data(raw_data: Dict[str, Any]) -> Dict[str, Any]: # Add identities for hotkey in meta_info.hotkeys: - identity = get_hotkey_identity( + identity = get_identity( hotkey, ck_hk_identities, old_identities, trucate_length=2 ) metagraph_info["updated_identities"].append(identity) @@ -278,9 +332,22 @@ def process_subnet_data(raw_data: Dict[str, Any]) -> Dict[str, Any]: } ) subnets.sort(key=lambda x: x["market_cap"], reverse=True) + + wallet_identity = get_identity( + wallet.coldkeypub.ss58_address, + ck_hk_identities, + old_identities, + return_bool=True, + lookup_hk=False, + ) + if not wallet_identity: + wallet_identity = wallet.name + else: + wallet_identity = f"{wallet_identity} ({wallet.name})" + return { "wallet_info": { - "name": wallet.name, + "name": wallet_identity, "balance": balance.tao, "coldkey": wallet.coldkeypub.ss58_address, "total_ideal_stake_value": total_ideal_stake_value.tao, @@ -319,6 +386,7 @@ def generate_full_page(data: Dict[str, Any]) -> str:
+