diff --git a/bittensor/_cli/__init__.py b/bittensor/_cli/__init__.py index 599b4bd134..97ccd03ead 100644 --- a/bittensor/_cli/__init__.py +++ b/bittensor/_cli/__init__.py @@ -91,6 +91,27 @@ def config() -> 'bittensor.config': help='''Set the output width of the overview. Defaults to automatic width from terminal.''', default=None, ) + overview_parser.add_argument( + '--sort_by', + '--wallet.sort_by', + dest='sort_by', + required=False, + action='store', + default="", + type=str, + help='''Sort the hotkeys by the specified column title (e.g. name, uid, axon).''' + ) + overview_parser.add_argument( + '--sort_order', + '--wallet.sort_order', + dest="sort_order", + required=False, + action='store', + default="ascending", + type=str, + help='''Sort the hotkeys in the specified ordering. (ascending/asc or descending/desc/reverse)''' + ) + bittensor.wallet.add_args( overview_parser ) bittensor.subtensor.add_args( overview_parser ) @@ -501,6 +522,7 @@ def config() -> 'bittensor.config': help='''Set true to avoid prompting the user.''', default=False, ) + bittensor.wallet.add_args( unstake_parser ) bittensor.subtensor.add_args( unstake_parser ) @@ -539,6 +561,7 @@ def config() -> 'bittensor.config': help='''Set true to avoid prompting the user.''', default=False, ) + bittensor.wallet.add_args( stake_parser ) bittensor.subtensor.add_args( stake_parser ) @@ -696,7 +719,7 @@ def check_unstake_config( config: 'bittensor.Config' ): if config.wallet.get('all_hotkeys'): hotkeys = "all hotkeys" elif config.wallet.get('hotkeys'): - hotkeys = str(config.hotkeys).replace('[', '').replace(']', '') + hotkeys = str(config.wallet.hotkeys).replace('[', '').replace(']', '') else: hotkeys = str(config.wallet.hotkey) if not Confirm.ask("Unstake all Tao from: [bold]'{}'[/bold]?".format(hotkeys)): diff --git a/bittensor/_cli/cli_impl.py b/bittensor/_cli/cli_impl.py index 26c9eadc08..bdce4358dc 100644 --- a/bittensor/_cli/cli_impl.py +++ b/bittensor/_cli/cli_impl.py @@ -17,7 +17,7 @@ import os import sys -from typing import List, Union +from typing import List, Union, Optional from cachetools import Cache @@ -259,7 +259,6 @@ def transfer( self ): def unstake( self ): r""" Unstake token of amount from hotkey(s). """ - # TODO: Implement this without re-unlocking the coldkey. config = self.config.copy() config.hotkey = None wallet = bittensor.wallet( config = self.config ) @@ -271,7 +270,7 @@ def unstake( self ): all_hotkeys: List[bittensor.wallet] = self._get_hotkey_wallets_for_wallet( wallet = wallet ) # Exclude hotkeys that are specified. wallets_to_unstake_from = [ - wallet for wallet in all_hotkeys if wallet.hotkey_str not in self.config.wallet.get('hotkeys') + wallet for wallet in all_hotkeys if wallet.hotkey_str not in self.config.wallet.get('hotkeys', []) ] elif self.config.wallet.get('hotkeys'): @@ -284,9 +283,7 @@ def unstake( self ): subtensor.unstake( wallet, amount = None if self.config.get('unstake_all') else self.config.get('amount'), wait_for_inclusion = True, prompt = not self.config.no_prompt ) return None - wallet_0: 'bittensor.wallet' = wallets_to_unstake_from[0] - # Decrypt coldkey for all wallet(s) to use - wallet_0.coldkey + final_wallets: List['bittensor.wallet'] = [] final_amounts: List[Union[float, Balance]] = [] @@ -295,9 +292,6 @@ def unstake( self ): if not wallet.is_registered(): # Skip unregistered hotkeys. continue - # Assign decrypted coldkey from wallet_0 - # so we don't have to decrypt again - wallet._coldkey = wallet_0._coldkey unstake_amount_tao: float = self.config.get('amount') if self.config.get('max_stake'): @@ -315,19 +309,17 @@ def unstake( self ): if not self.config.no_prompt: if not Confirm.ask("Do you want to unstake from the following keys:\n" + \ "".join([ - f" [bold white]- {wallet.hotkey_str}: {amount}𝜏[/bold white]\n" for wallet, amount in zip(final_wallets, final_amounts) + f" [bold white]- {wallet.hotkey_str}: {amount.tao}𝜏[/bold white]\n" for wallet, amount in zip(final_wallets, final_amounts) ]) ): return None - - for wallet, amount in zip(final_wallets, final_amounts): - subtensor.unstake( wallet, amount = None if self.config.get('unstake_all') else amount, wait_for_inclusion = True, prompt = False ) + + subtensor.unstake_multiple( wallets = final_wallets, amounts = None if self.config.get('unstake_all') else final_amounts, wait_for_inclusion = True, prompt = False ) def stake( self ): r""" Stake token of amount to hotkey(s). """ - # TODO: Implement this without re-unlocking the coldkey. config = self.config.copy() config.hotkey = None wallet = bittensor.wallet( config = config ) @@ -339,7 +331,7 @@ def stake( self ): all_hotkeys: List[bittensor.wallet] = self._get_hotkey_wallets_for_wallet( wallet = wallet ) # Exclude hotkeys that are specified. wallets_to_stake_to = [ - wallet for wallet in all_hotkeys if wallet.hotkey_str not in self.config.wallet.get('hotkeys') + wallet for wallet in all_hotkeys if wallet.hotkey_str not in self.config.wallet.get('hotkeys', []) ] elif self.config.wallet.get('hotkeys'): @@ -394,8 +386,7 @@ def stake( self ): ): return None - for wallet, amount in zip(final_wallets, final_amounts): - subtensor.add_stake( wallet, amount = None if self.config.get('stake_all') else amount, wait_for_inclusion = True, prompt = False ) + subtensor.add_stake_multiple( wallets = final_wallets, amounts = None if self.config.get('stake_all') else final_amounts, wait_for_inclusion = True, prompt = False ) def set_weights( self ): @@ -604,24 +595,14 @@ def overview(self): all_hotkeys = [] total_balance = bittensor.Balance(0) - - # We are printing for every wallet. + + # We are printing for every coldkey. if self.config.all: cold_wallets = CLI._get_coldkey_wallets_for_path(self.config.wallet.path) for cold_wallet in tqdm(cold_wallets, desc="Pulling balances"): if cold_wallet.coldkeypub_file.exists_on_device() and not cold_wallet.coldkeypub_file.is_encrypted(): total_balance = total_balance + subtensor.get_balance( cold_wallet.coldkeypub.ss58_address ) all_hotkeys = CLI._get_all_wallets_for_path( self.config.wallet.path ) - - # We are printing for a select number of hotkeys. - elif self.config.wallet.hotkeys: - # Only show hotkeys for wallets in the list - all_hotkeys = [hotkey for hotkey in all_hotkeys if hotkey.hotkey_str in self.config.wallet.hotkeys] - coldkey_wallet = bittensor.wallet( config = self.config ) - if coldkey_wallet.coldkeypub_file.exists_on_device() and not coldkey_wallet.coldkeypub_file.is_encrypted(): - total_balance = subtensor.get_balance( coldkey_wallet.coldkeypub.ss58_address ) - - # We are printing for all keys under the wallet. else: # We are only printing keys for a single coldkey coldkey_wallet = bittensor.wallet( config = self.config ) @@ -632,6 +613,16 @@ def overview(self): return all_hotkeys = CLI._get_hotkey_wallets_for_wallet( coldkey_wallet ) + # We are printing for a select number of hotkeys from all_hotkeys. + + if self.config.wallet.get('hotkeys', []): + if not self.config.get('all_hotkeys', False): + # We are only showing hotkeys that are specified. + all_hotkeys = [hotkey for hotkey in all_hotkeys if hotkey.hotkey_str in self.config.wallet.hotkeys] + else: + # We are excluding the specified hotkeys from all_hotkeys. + all_hotkeys = [hotkey for hotkey in all_hotkeys if hotkey.hotkey_str not in self.config.wallet.hotkeys] + # Check we have keys to display. if len(all_hotkeys) == 0: console.print("[red]No wallets found.[/red]") @@ -740,10 +731,10 @@ def overview(self): console.clear() - sort_by: str = self.config.wallet.sort_by - sort_order: str = self.config.wallet.sort_order + sort_by: Optional[str] = self.config.get('sort_by', None) + sort_order: Optional[str] = self.config.get('sort_order', None) - if sort_by != "": + if sort_by is not None and sort_by != "": column_to_sort_by: int = 0 highest_matching_ratio: int = 0 sort_descending: bool = False # Default sort_order to ascending diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index c58126facd..baac5d3b70 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -762,6 +762,197 @@ def add_stake( return False + def add_stake_multiple ( + self, + wallets: List['bittensor.wallet'], + amounts: List[Union[Balance, float]] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> bool: + r""" Adds stake to each wallet hotkey in the list, using each amount, from the common coldkey. + Args: + wallets (List[bittensor.wallet]): + List of wallets to stake. + amounts (List[Union[Balance, float]]): + List of amounts to stake. If None, stake all to the first hotkey. + wait_for_inclusion (bool): + 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. + wait_for_finalization (bool): + 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. + prompt (bool): + If true, the call waits for confirmation from the user before proceeding. + Returns: + success (bool): + flag is true if extrinsic was finalized or included in the block. + flag is true if any wallet was staked. + If we did not wait for finalization / inclusion, the response is true. + """ + if not isinstance(wallets, list): + raise TypeError("wallets must be a list of bittensor.wallet") + + if len(wallets) == 0: + return True + + if amounts is not None and len(amounts) != len(wallets): + raise ValueError("amounts must be a list of the same length as wallets") + + if amounts is not None and not all(isinstance(amount, (Balance, float)) for amount in amounts): + raise TypeError("amounts must be a [list of bittensor.Balance or float] or None") + + if amounts is None: + amounts = [None] * len(wallets) + else: + # Convert to Balance + amounts = [bittensor.Balance.from_tao(amount) if isinstance(amount, float) else amount for amount in amounts ] + + if sum(amount.tao for amount in amounts) == 0: + # Staking 0 tao + return True + + wallet_0: 'bittensor.wallet' = wallets[0] + # Decrypt coldkey for all wallet(s) to use + wallet_0.coldkey + + neurons = [] + with bittensor.__console__.status(":satellite: Syncing with chain: [white]{}[/white] ...".format(self.network)): + old_balance = self.get_balance( wallet_0.coldkey.ss58_address ) + + for wallet in wallets: + neuron = self.neuron_for_pubkey( ss58_hotkey = wallet.hotkey.ss58_address ) + + if neuron.is_null: + neurons.append( None ) + continue + + neurons.append( neuron ) + + # Remove existential balance to keep key alive. + ## Keys must maintain a balance of at least 1000 rao to stay alive. + total_staking_rao = sum([amount.rao if amount is not None else 0 for amount in amounts]) + if total_staking_rao == 0: + # Staking all to the first wallet. + if old_balance.rao > 1000: + old_balance -= bittensor.Balance.from_rao(1000) + + elif total_staking_rao < 1000: + # Staking less than 1000 rao to the wallets. + pass + else: + # Staking more than 1000 rao to the wallets. + ## Reduce the amount to stake to each wallet to keep the balance above 1000 rao. + percent_reduction = 1 - (1000 / total_staking_rao) + amounts = [amount * percent_reduction for amount in amounts] + + successful_stakes = 0 + for wallet, amount, neuron in zip(wallets, amounts, neurons): + if neuron is None: + bittensor.__console__.print(":cross_mark: [red]Hotkey: {} is not registered. Skipping ...[/red]".format( wallet.hotkey_str )) + continue + + if wallet.coldkeypub.ss58_address != wallet_0.coldkeypub.ss58_address: + bittensor.__console__.print(":cross_mark: [red]Hotkey: {} is not under the same coldkey. Skipping ...[/red]".format( wallet.hotkey_str )) + continue + + # Assign decrypted coldkey from wallet_0 + # so we don't have to decrypt again + wallet._coldkey = wallet_0._coldkey + staking_all = False + # Convert to bittensor.Balance + if amount == None: + # Stake it all. + staking_balance = bittensor.Balance.from_tao( old_balance.tao ) + staking_all = True + + elif not isinstance(amount, bittensor.Balance ): + staking_balance = bittensor.Balance.from_tao( amount ) + else: + staking_balance = amount + + # Estimate staking fee. + stake_fee = None # To be filled. + with bittensor.__console__.status(":satellite: Estimating Staking Fees..."): + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='add_stake', + call_params={ + 'hotkey': wallet.hotkey.ss58_address, + 'ammount_staked': staking_balance.rao + } + ) + payment_info = substrate.get_payment_info(call = call, keypair = wallet.coldkey) + if payment_info: + stake_fee = bittensor.Balance.from_rao(payment_info['partialFee']) + bittensor.__console__.print("[green]Estimated Fee: {}[/green]".format( stake_fee )) + else: + stake_fee = bittensor.Balance.from_tao( 0.2 ) + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: could not estimate staking fee, assuming base fee of 0.2") + + # Check enough to stake + if staking_all: + staking_balance -= stake_fee + max(staking_balance, bittensor.Balance.from_tao(0)) + + if staking_balance > old_balance - stake_fee: + bittensor.__console__.print(":cross_mark: [red]Not enough balance[/red]: [green]{}[/green] to stake: [blue]{}[/blue] from coldkey: [white]{}[/white]".format(old_balance, staking_balance, wallet.name)) + continue + + # Ask before moving on. + if prompt: + if not Confirm.ask("Do you want to stake:\n[bold white] amount: {}\n hotkey: {}\n fee: {}[/bold white ]?".format( staking_balance, wallet.hotkey_str, stake_fee) ): + continue + + with bittensor.__console__.status(":satellite: Staking to chain: [white]{}[/white] ...".format(self.network)): + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='add_stake', + call_params={ + 'hotkey': wallet.hotkey.ss58_address, + 'ammount_staked': staking_balance.rao + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + old_balance -= staking_balance + stake_fee + successful_stakes += 1 + if staking_all: + # If staked all, no need to continue + break + + continue + + response.process_events() + if response.is_success: + bittensor.__console__.print(":white_heavy_check_mark: [green]Finalized[/green]") + else: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) + + if response.is_success: + block = self.get_current_block() + new_stake = bittensor.Balance.from_tao( self.neuron_for_uid( uid = neuron.uid, block = block ).stake) + new_balance = self.get_balance( wallet.coldkey.ss58_address ) + bittensor.__console__.print("Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( neuron.uid, neuron.stake, new_stake )) + old_balance = new_balance + successful_stakes += 1 + if staking_all: + # If staked all, no need to continue + break + + if successful_stakes != 0: + with bittensor.__console__.status(":satellite: Checking Balance on: ([white]{}[/white] ...".format(self.network)): + new_balance = self.get_balance( wallet.coldkey.ss58_address ) + bittensor.__console__.print("Balance: [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_balance, new_balance )) + return True + + return False + def transfer( self, wallet: 'bittensor.wallet', @@ -990,6 +1181,166 @@ def unstake ( return True return False + + def unstake_multiple ( + self, + wallets: List['bittensor.wallet'], + amounts: List[Union[Balance, float]] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> bool: + r""" Removes stake from each wallet hotkey in the list, using each amount, to their common coldkey. + Args: + wallets (List[bittensor.wallet]): + List of wallets to unstake. + amounts (List[Union[Balance, float]]): + List of amounts to unstake. If None, unstake all. + wait_for_inclusion (bool): + 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. + wait_for_finalization (bool): + 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. + prompt (bool): + If true, the call waits for confirmation from the user before proceeding. + Returns: + success (bool): + flag is true if extrinsic was finalized or included in the block. + flag is true if any wallet was unstaked. + If we did not wait for finalization / inclusion, the response is true. + """ + if not isinstance(wallets, list): + raise TypeError("wallets must be a list of bittensor.wallet") + + if len(wallets) == 0: + return True + + if amounts is not None and len(amounts) != len(wallets): + raise ValueError("amounts must be a list of the same length as wallets") + + if amounts is not None and not all(isinstance(amount, (Balance, float)) for amount in amounts): + raise TypeError("amounts must be a [list of bittensor.Balance or float] or None") + + if amounts is None: + amounts = [None] * len(wallets) + else: + # Convert to Balance + amounts = [bittensor.Balance.from_tao(amount) if isinstance(amount, float) else amount for amount in amounts ] + + if sum(amount.tao for amount in amounts) == 0: + # Staking 0 tao + return True + + + wallet_0: 'bittensor.wallet' = wallets[0] + # Decrypt coldkey for all wallet(s) to use + wallet_0.coldkey + + neurons = [] + with bittensor.__console__.status(":satellite: Syncing with chain: [white]{}[/white] ...".format(self.network)): + old_balance = self.get_balance( wallet_0.coldkey.ss58_address ) + + for wallet in wallets: + neuron = self.neuron_for_pubkey( ss58_hotkey = wallet.hotkey.ss58_address ) + + if neuron.is_null: + neurons.append( None ) + continue + + neurons.append( neuron ) + + successful_unstakes = 0 + for wallet, amount, neuron in zip(wallets, amounts, neurons): + if neuron is None: + bittensor.__console__.print(":cross_mark: [red]Hotkey: {} is not registered. Skipping ...[/red]".format( wallet.hotkey_str )) + continue + + if wallet.coldkeypub.ss58_address != wallet_0.coldkeypub.ss58_address: + bittensor.__console__.print(":cross_mark: [red]Hotkey: {} is not under the same coldkey. Skipping ...[/red]".format( wallet.hotkey_str )) + continue + + # Assign decrypted coldkey from wallet_0 + # so we don't have to decrypt again + wallet._coldkey = wallet_0._coldkey + + # Covert to bittensor.Balance + if amount == None: + # Unstake it all. + unstaking_balance = bittensor.Balance.from_tao( neuron.stake ) + elif not isinstance(amount, bittensor.Balance ): + unstaking_balance = bittensor.Balance.from_tao( amount ) + else: + unstaking_balance = amount + + # Check enough to unstake. + stake_on_uid = bittensor.Balance.from_tao( neuron.stake ) + if unstaking_balance > stake_on_uid: + bittensor.__console__.print(":cross_mark: [red]Not enough stake[/red]: [green]{}[/green] to unstake: [blue]{}[/blue] from hotkey: [white]{}[/white]".format(stake_on_uid, unstaking_balance, wallet.hotkey_str)) + continue + + # Estimate unstaking fee. + unstake_fee = None # To be filled. + with bittensor.__console__.status(":satellite: Estimating Staking Fees..."): + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='remove_stake', + call_params={ + 'hotkey': wallet.hotkey.ss58_address, + 'ammount_unstaked': unstaking_balance.rao + } + ) + payment_info = substrate.get_payment_info(call = call, keypair = wallet.coldkey) + if payment_info: + unstake_fee = bittensor.Balance.from_rao(payment_info['partialFee']) + bittensor.__console__.print("[green]Estimated Fee: {}[/green]".format( unstake_fee )) + else: + unstake_fee = bittensor.Balance.from_tao( 0.2 ) + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: could not estimate staking fee, assuming base fee of 0.2") + + # Ask before moving on. + if prompt: + if not Confirm.ask("Do you want to unstake:\n[bold white] amount: {}\n hotkey: {}\n fee: {}[/bold white ]?".format( unstaking_balance, wallet.hotkey_str, unstake_fee) ): + continue + + with bittensor.__console__.status(":satellite: Unstaking from chain: [white]{}[/white] ...".format(self.network)): + with self.substrate as substrate: + call = substrate.compose_call( + call_module='SubtensorModule', + call_function='remove_stake', + call_params={ + 'hotkey': wallet.hotkey.ss58_address, + 'ammount_unstaked': unstaking_balance.rao + } + ) + extrinsic = substrate.create_signed_extrinsic( call = call, keypair = wallet.coldkey ) + response = substrate.submit_extrinsic( extrinsic, wait_for_inclusion = wait_for_inclusion, wait_for_finalization = wait_for_finalization ) + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + bittensor.__console__.print(":white_heavy_check_mark: [green]Sent[/green]") + successful_unstakes += 1 + continue + + response.process_events() + if response.is_success: + bittensor.__console__.print(":white_heavy_check_mark: [green]Finalized[/green]") + else: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: error:{}".format(response.error_message)) + + if response.is_success: + block = self.get_current_block() + new_stake = bittensor.Balance.from_tao( self.neuron_for_uid( uid = neuron.uid, block = block ).stake) + bittensor.__console__.print("Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( neuron.uid, stake_on_uid, new_stake )) + successful_unstakes += 1 + + if successful_unstakes != 0: + with bittensor.__console__.status(":satellite: Checking Balance on: ([white]{}[/white] ...".format(self.network)): + new_balance = self.get_balance( wallet.coldkey.ss58_address ) + bittensor.__console__.print("Balance: [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( old_balance, new_balance )) + return True + + return False def set_weights( self, diff --git a/bittensor/_wallet/__init__.py b/bittensor/_wallet/__init__.py index df8fb4fdf7..9849d50664 100644 --- a/bittensor/_wallet/__init__.py +++ b/bittensor/_wallet/__init__.py @@ -111,11 +111,11 @@ def add_args(cls, parser: argparse.ArgumentParser, prefix: str = None ): parser.add_argument('--' + prefix_str + 'wallet.hotkey', required=False, default=bittensor.defaults.wallet.hotkey, help='''The name of wallet's hotkey.''') parser.add_argument('--' + prefix_str + 'wallet.path', required=False, default=bittensor.defaults.wallet.path, help='''The path to your bittensor wallets''') parser.add_argument('--' + prefix_str + 'wallet._mock', action='store_true', default=bittensor.defaults.wallet._mock, help='To turn on wallet mocking for testing purposes.') + parser.add_argument('--' + prefix_str + 'wallet.hotkeys', '--' + prefix_str + 'wallet.exclude_hotkeys', required=False, action='store', default=bittensor.defaults.wallet.hotkeys, type=str, nargs='*', help='''Specify the hotkeys by name. (e.g. hk1 hk2 hk3)''') parser.add_argument('--' + prefix_str + 'wallet.all_hotkeys', required=False, action='store_true', default=bittensor.defaults.wallet.all_hotkeys, help='''To specify all hotkeys. Specifying hotkeys will exclude them from this all.''') - parser.add_argument('--' + prefix_str + 'wallet.sort_by', required=False, action='store', default=bittensor.defaults.wallet.sort_by, type=str, help='''Sort the hotkeys by the specified column title (e.g. name, uid, axon).''') - parser.add_argument('--' + prefix_str + 'wallet.sort_order', required=False, action='store', default=bittensor.defaults.wallet.sort_order, type=str, help='''Sort the hotkeys in the specified ordering. (ascending/asc or descending/desc/reverse)''') parser.add_argument('--' + prefix_str + 'wallet.reregister', required=False, action='store', default=bittensor.defaults.wallet.reregister, type=bool, help='''Whether to reregister the wallet if it is not already registered.''') + except argparse.ArgumentError as e: import pdb #pdb.set_trace() @@ -134,8 +134,6 @@ def add_defaults(cls, defaults): # CLI defaults for Overview defaults.wallet.hotkeys = [] defaults.wallet.all_hotkeys = False - defaults.wallet.sort_by = "" - defaults.wallet.sort_order = "ascending" # Defaults for registration defaults.wallet.reregister = True @@ -148,6 +146,5 @@ def check_config(cls, config: 'bittensor.Config' ): assert isinstance(config.wallet.get('hotkey', bittensor.defaults.wallet.hotkey), str ) or config.wallet.get('hotkey', bittensor.defaults.wallet.hotkey) == None assert isinstance(config.wallet.path, str) assert isinstance(config.wallet.hotkeys, list) - assert isinstance(config.wallet.sort_by, str) - assert isinstance(config.wallet.sort_order, str) assert isinstance(config.wallet.reregister, bool) + assert isinstance(config.wallet.all_hotkeys, bool) diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py index ef76720630..d8276422e0 100644 --- a/tests/integration_tests/test_cli.py +++ b/tests/integration_tests/test_cli.py @@ -442,7 +442,7 @@ def test_unstake_with_specific_hotkeys( self ): with patch('bittensor.wallet') as mock_create_wallet: mock_create_wallet.side_effect = mock_wallets - with patch('bittensor.Subtensor.unstake', return_value=True) as mock_unstake: + with patch('bittensor.Subtensor.unstake_multiple', return_value=True) as mock_unstake: cli.run() mock_create_wallet.assert_has_calls( [ @@ -451,7 +451,7 @@ def test_unstake_with_specific_hotkeys( self ): any_order=True ) mock_unstake.assert_has_calls( - [call(mock_wallets[i+1], amount=5.0, wait_for_inclusion=True, prompt=False) for i in range(len(mock_wallets[1:]))], + [call(wallets=mock_wallets[1:], amounts=[5.0]*len(mock_wallets[1:]), wait_for_inclusion=True, prompt=False)], any_order = True ) @@ -500,11 +500,11 @@ def test_unstake_with_all_hotkeys( self ): with patch.object(cli, '_get_hotkey_wallets_for_wallet') as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch('bittensor.Subtensor.unstake', return_value=True) as mock_unstake: + with patch('bittensor.Subtensor.unstake_multiple', return_value=True) as mock_unstake: cli.run() mock_get_all_wallets.assert_called_once() mock_unstake.assert_has_calls( - [call(mock_wallets[i+1], amount=5.0, wait_for_inclusion=True, prompt=False) for i in range(len(mock_wallets[1:]))], + [call(wallets=mock_wallets, amounts=[5.0]*len(mock_wallets), wait_for_inclusion=True, prompt=False)], any_order = True ) @@ -553,13 +553,11 @@ def test_unstake_with_exclude_hotkeys_from_all( self ): with patch.object(cli, '_get_hotkey_wallets_for_wallet') as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch('bittensor.Subtensor.unstake', return_value=True) as mock_unstake: + with patch('bittensor.Subtensor.unstake_multiple', return_value=True) as mock_unstake: cli.run() mock_get_all_wallets.assert_called_once() mock_unstake.assert_has_calls( - [call(mock_wallets[i], amount=5.0, wait_for_inclusion=True, prompt=False) - for i in (0, 2) # Don't call for hk1 - ], + [call(wallets=[mock_wallets[0], mock_wallets[2]], amounts=[5.0, 5.0], wait_for_inclusion=True, prompt=False)], any_order = True ) @@ -628,7 +626,7 @@ def test_unstake_with_multiple_hotkeys_max_stake( self ): with patch('bittensor.wallet') as mock_create_wallet: mock_create_wallet.side_effect = mock_wallets - with patch('bittensor.Subtensor.unstake', return_value=True) as mock_unstake: + with patch('bittensor.Subtensor.unstake_multiple', return_value=True) as mock_unstake: cli.run() mock_create_wallet.assert_has_calls( [ @@ -637,7 +635,7 @@ def test_unstake_with_multiple_hotkeys_max_stake( self ): any_order=True ) mock_unstake.assert_has_calls( - [call(mock_wallet, amount=CLOSE_IN_VALUE((mock_stakes[mock_wallet.hotkey_str] - config.max_stake).tao, 0.001), wait_for_inclusion=True, prompt=False) for mock_wallet in mock_wallets[1:]], + [call(wallets=mock_wallets[1:], amounts=[CLOSE_IN_VALUE((mock_stakes[mock_wallet.hotkey_str] - config.max_stake).tao, 0.001) for mock_wallet in mock_wallets[1:]], wait_for_inclusion=True, prompt=False)], any_order = True ) @@ -706,7 +704,7 @@ def test_unstake_with_multiple_hotkeys_max_stake_not_enough_stake( self ): with patch('bittensor.wallet') as mock_create_wallet: mock_create_wallet.side_effect = mock_wallets - with patch('bittensor.Subtensor.unstake', return_value=True) as mock_unstake: + with patch('bittensor.Subtensor.unstake_multiple', return_value=True) as mock_unstake: cli.run() mock_create_wallet.assert_has_calls( [ @@ -715,15 +713,16 @@ def test_unstake_with_multiple_hotkeys_max_stake_not_enough_stake( self ): any_order=True ) mock_unstake.assert_called() - for mock_call in mock_unstake.mock_calls: - # Python 3.7 - ## https://docs.python.org/3.7/library/unittest.mock.html#call - ## Uses the 1st index as args list - ## call.args only works in Python 3.8+ - mock_wallet = mock_call[1][0] - # We shouldn't unstake from hk1 as it has less than max_stake staked - assert mock_wallet.hotkey_str != 'hk1' + # Python 3.7 + ## https://docs.python.org/3.7/library/unittest.mock.html#call + ## Uses the 1st index as args list + ## call.args only works in Python 3.8+ + mock_wallets_ = mock_unstake.mock_calls[0][2]['wallets'] + + + # We shouldn't unstake from hk1 as it has less than max_stake staked + assert all(mock_wallet.hotkey_str != 'hk1' for mock_wallet in mock_wallets_) def test_stake_with_specific_hotkeys( self ): bittensor.subtensor.register = MagicMock(return_value = True) @@ -782,7 +781,7 @@ def test_stake_with_specific_hotkeys( self ): with patch('bittensor.wallet') as mock_create_wallet: mock_create_wallet.side_effect = mock_wallets - with patch('bittensor.Subtensor.add_stake', return_value=True) as mock_add_stake: + with patch('bittensor.Subtensor.add_stake_multiple', return_value=True) as mock_add_stake: cli.run() mock_create_wallet.assert_has_calls( [ @@ -791,7 +790,7 @@ def test_stake_with_specific_hotkeys( self ): any_order=True ) mock_add_stake.assert_has_calls( - [call(mock_wallets[i+1], amount=5.0, wait_for_inclusion=True, prompt=False) for i in range(len(mock_wallets[1:]))], + [call(wallets=mock_wallets[1:], amounts=[5.0] * len(mock_wallets[1:]), wait_for_inclusion=True, prompt=False)], any_order = True ) @@ -826,7 +825,6 @@ def test_stake_with_all_hotkeys( self ): ) for hk in mock_hotkeys ] - # The 0th wallet is created twice during unstake mock_wallets[0]._coldkey = mock_coldkey mock_wallets[0].coldkey = MagicMock( return_value=mock_coldkey @@ -839,11 +837,11 @@ def test_stake_with_all_hotkeys( self ): with patch.object(cli, '_get_hotkey_wallets_for_wallet') as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch('bittensor.Subtensor.add_stake', return_value=True) as mock_add_stake: + with patch('bittensor.Subtensor.add_stake_multiple', return_value=True) as mock_add_stake: cli.run() mock_get_all_wallets.assert_called_once() mock_add_stake.assert_has_calls( - [call(mock_wallets[i+1], amount=5.0, wait_for_inclusion=True, prompt=False) for i in range(len(mock_wallets[1:]))], + [call(wallets=mock_wallets, amounts=[5.0] * len(mock_wallets), wait_for_inclusion=True, prompt=False)], any_order = True ) @@ -891,13 +889,11 @@ def test_stake_with_exclude_hotkeys_from_all( self ): with patch.object(cli, '_get_hotkey_wallets_for_wallet') as mock_get_all_wallets: mock_get_all_wallets.return_value = mock_wallets - with patch('bittensor.Subtensor.add_stake', return_value=True) as mock_add_stake: + with patch('bittensor.Subtensor.add_stake_multiple', return_value=True) as mock_add_stake: cli.run() mock_get_all_wallets.assert_called_once() mock_add_stake.assert_has_calls( - [call(mock_wallets[i], amount=5.0, wait_for_inclusion=True, prompt=False) - for i in (0, 2) # Don't call stake for hk1 - ], + [call(wallets=[mock_wallets[0], mock_wallets[2]], amounts=[5.0, 5.0], wait_for_inclusion=True, prompt=False)], any_order = True ) @@ -971,7 +967,7 @@ def test_stake_with_multiple_hotkeys_max_stake( self ): with patch('bittensor.wallet') as mock_create_wallet: mock_create_wallet.side_effect = mock_wallets - with patch('bittensor.Subtensor.add_stake', return_value=True) as mock_add_stake: + with patch('bittensor.Subtensor.add_stake_multiple', return_value=True) as mock_add_stake: cli.run() mock_create_wallet.assert_has_calls( [ @@ -980,7 +976,7 @@ def test_stake_with_multiple_hotkeys_max_stake( self ): any_order=True ) mock_add_stake.assert_has_calls( - [call(mock_wallet, amount=CLOSE_IN_VALUE((config.max_stake - mock_stakes[mock_wallet.hotkey_str].tao), 0.001), wait_for_inclusion=True, prompt=False) for mock_wallet in mock_wallets[1:]], + [call(wallets=mock_wallets[1:], amounts=[CLOSE_IN_VALUE((config.max_stake - mock_stakes[mock_wallet.hotkey_str].tao), 0.001) for mock_wallet in mock_wallets[1:]], wait_for_inclusion=True, prompt=False)], any_order = True ) @@ -1054,7 +1050,7 @@ def test_stake_with_multiple_hotkeys_max_stake_not_enough_balance( self ): with patch('bittensor.wallet') as mock_create_wallet: mock_create_wallet.side_effect = mock_wallets - with patch('bittensor.Subtensor.add_stake', return_value=True) as mock_add_stake: + with patch('bittensor.Subtensor.add_stake_multiple', return_value=True) as mock_add_stake: cli.run() mock_create_wallet.assert_has_calls( [ @@ -1066,14 +1062,15 @@ def test_stake_with_multiple_hotkeys_max_stake_not_enough_balance( self ): mock_add_stake.assert_called_once() total_staked = 0.0 - for mock_call in mock_add_stake.mock_calls: - # Python 3.7 - ## https://docs.python.org/3.7/library/unittest.mock.html#call - ## Uses the 2nd index as kwargs dict - ## call.kwargs only works in Python 3.8+ - staked_this_call = mock_call[2]['amount'] - - total_staked += staked_this_call + + # Python 3.7 + ## https://docs.python.org/3.7/library/unittest.mock.html#call + ## Uses the 2nd index as kwargs dict + ## call.kwargs only works in Python 3.8+ + amounts_passed = mock_add_stake.mock_calls[0][2]['amounts'] + + total_staked = sum(amounts_passed) + # We should not try to stake more than the mock_balance assert CLOSE_IN_VALUE(total_staked, 0.001) == mock_balance.tao @@ -1265,7 +1262,7 @@ def test_inspect( self ): def test_list( self ): # Mock IO for wallet - with patch('bittensor.wallet.__new__', side_effect=[MagicMock( + with patch('bittensor.wallet', side_effect=[MagicMock( coldkeypub_file=MagicMock( exists_on_device=MagicMock( return_value=True # Wallet exists