From e7b4fb97b88df4940bc0ed61977e120c6dd4a259 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 26 Jan 2023 16:15:38 -0500 Subject: [PATCH 1/4] change to balance check --- bittensor/_subtensor/extrinsics/delegation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor/_subtensor/extrinsics/delegation.py b/bittensor/_subtensor/extrinsics/delegation.py index c624d6b548..ce747be8e0 100644 --- a/bittensor/_subtensor/extrinsics/delegation.py +++ b/bittensor/_subtensor/extrinsics/delegation.py @@ -197,9 +197,9 @@ def delegate_extrinsic( staking_fee = bittensor.Balance.from_tao( 0.2 ) bittensor.__console__.print(":cross_mark: [red]Failed[/red]: could not estimate delegation fee, assuming base fee of 0.2") - # Check enough to stake. - if staking_balance > my_prev_coldkey_balance + staking_fee: - bittensor.__console__.print(":cross_mark: [red]Not enough stake[/red]:[bold white]\n balance:{}\n amount: {}\n fee: {}\n coldkey: {}[/bold white]".format(my_prev_coldkey_balance, staking_balance, staking_fee, wallet.name)) + # Check enough balance to stake. + if staking_balance + staking_fee > my_prev_coldkey_balance: + bittensor.__console__.print(":cross_mark: [red]Not enough balance[/red]:[bold white]\n balance:{}\n amount: {}\n fee: {}\n coldkey: {}[/bold white]".format(my_prev_coldkey_balance, staking_balance, staking_fee, wallet.name)) return False # Ask before moving on. From ff88193cea4e8bd74aa8a5c8cb23ddb746cb040e Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Thu, 26 Jan 2023 16:32:14 -0500 Subject: [PATCH 2/4] add delegateunstake command --- bittensor/_cli/__init__.py | 3 + bittensor/_cli/cli_impl.py | 2 + bittensor/_cli/commands/__init__.py | 2 +- bittensor/_cli/commands/delegates.py | 91 ++++++++++ bittensor/_subtensor/extrinsics/delegation.py | 162 +++++++++++++++++- bittensor/_subtensor/subtensor_impl.py | 22 ++- 6 files changed, 279 insertions(+), 3 deletions(-) diff --git a/bittensor/_cli/__init__.py b/bittensor/_cli/__init__.py index de50317773..bd96ea98b9 100644 --- a/bittensor/_cli/__init__.py +++ b/bittensor/_cli/__init__.py @@ -84,6 +84,7 @@ def config(args: List[str]) -> 'bittensor.config': RegenHotkeyCommand.add_args( cmd_parsers ) RegenColdkeyCommand.add_args( cmd_parsers ) DelegateStakeCommand.add_args( cmd_parsers ) + DelegateUnstakeCommand.add_args( cmd_parsers ) ListDelegatesCommand.add_args( cmd_parsers ) RegenColdkeypubCommand.add_args( cmd_parsers ) @@ -144,6 +145,8 @@ def check_config (config: 'bittensor.Config'): ListSubnetsCommand.check_config( config ) elif config.command == "delegate": DelegateStakeCommand.check_config( config ) + elif config.command == "undelegate": + DelegateUnstakeCommand.check_config( config ) else: console.print(":cross_mark:[red]Unknown command: {}[/red]".format(config.command)) sys.exit() diff --git a/bittensor/_cli/cli_impl.py b/bittensor/_cli/cli_impl.py index 0e4dc21d44..36000ea98a 100644 --- a/bittensor/_cli/cli_impl.py +++ b/bittensor/_cli/cli_impl.py @@ -82,6 +82,8 @@ def run ( self ): NominateCommand.run( self ) elif self.config.command == 'delegate': DelegateStakeCommand.run( self ) + elif self.config.command == 'undelegate': + DelegateUnstakeCommand.run( self ) elif self.config.command == 'list_delegates': ListDelegatesCommand.run( self ) elif self.config.command == 'list_subnets': diff --git a/bittensor/_cli/commands/__init__.py b/bittensor/_cli/commands/__init__.py index a67262b2e8..5060180f7e 100644 --- a/bittensor/_cli/commands/__init__.py +++ b/bittensor/_cli/commands/__init__.py @@ -3,7 +3,7 @@ from .unstake import UnStakeCommand from .overview import OverviewCommand from .register import RegisterCommand -from .delegates import NominateCommand, ListDelegatesCommand, DelegateStakeCommand +from .delegates import NominateCommand, ListDelegatesCommand, DelegateStakeCommand, DelegateUnstakeCommand from .wallets import NewColdkeyCommand, NewHotkeyCommand, RegenColdkeyCommand, RegenColdkeypubCommand, RegenHotkeyCommand from .transfer import TransferCommand from .inspect import InspectCommand diff --git a/bittensor/_cli/commands/delegates.py b/bittensor/_cli/commands/delegates.py index defed372cd..3ce5186346 100644 --- a/bittensor/_cli/commands/delegates.py +++ b/bittensor/_cli/commands/delegates.py @@ -134,6 +134,97 @@ def check_config( config: 'bittensor.Config' ): else: config.stake_all = True +class DelegateUnstakeCommand: + + @staticmethod + def run( cli ): + '''Undelegates stake from a chain delegate.''' + config = cli.config.copy() + wallet = bittensor.wallet( config = config ) + subtensor: bittensor.Subtensor = bittensor.subtensor( config = config ) + subtensor.undelegate( + wallet = wallet, + delegate_ss58 = config.get('delegate_ss58key'), + amount = config.get('amount'), + wait_for_inclusion = True, + prompt = not config.no_prompt + ) + + @staticmethod + def add_args( parser: argparse.ArgumentParser ): + undelegate_stake_parser = parser.add_parser( + 'undelegate', + help='''Undelegate Stake from an account.''' + ) + undelegate_stake_parser.add_argument( + '--no_version_checking', + action='store_true', + help='''Set false to stop cli version checking''', + default = False + ) + undelegate_stake_parser.add_argument( + '--delegate_ss58key', + dest = "delegate_ss58key", + type = float, + required = False, + help='''The ss58 address of the choosen delegate''', + ) + undelegate_stake_parser.add_argument( + '--all', + dest="unstake_all", + action='store_true' + ) + undelegate_stake_parser.add_argument( + '--amount', + dest="amount", + type=float, + required=False + ) + undelegate_stake_parser.add_argument( + '--no_prompt', + dest='no_prompt', + action='store_true', + help='''Set true to avoid prompting the user.''', + default=False, + ) + bittensor.wallet.add_args( undelegate_stake_parser ) + bittensor.subtensor.add_args( undelegate_stake_parser ) + + @staticmethod + def check_config( config: 'bittensor.Config' ): + if config.subtensor.get('network') == bittensor.defaults.subtensor.network and not config.no_prompt: + config.subtensor.network = Prompt.ask("Enter subtensor network", choices=bittensor.__networks__, default = bittensor.defaults.subtensor.network) + + if config.wallet.get('name') == bittensor.defaults.wallet.name and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) + config.wallet.name = str(wallet_name) + + # Check for delegates. + with bittensor.__console__.status(":satellite: Loading delegates..."): + subtensor = bittensor.subtensor( config = config ) + delegates: List[bittensor.DelegateInfo] = subtensor.get_delegates() + + if len(delegates) == 0: + console.print(":cross_mark:[red]There are no delegates on {}[/red]".format(subtensor.network)) + sys.exit() + + if not config.get('delegate_ss58key'): + show_delegates( delegates ) + delegate_ss58key = Prompt.ask("Enter the delegate's ss58key") + config.delegate_ss58key = str(delegate_ss58key) + + # Get amount. + if not config.get('amount') and not config.get('unstake_all'): + if not Confirm.ask("Unstake all Tao to account: [bold]'{}'[/bold]?".format(config.wallet.get('name', bittensor.defaults.wallet.name))): + amount = Prompt.ask("Enter Tao amount to unstake") + try: + config.amount = float(amount) + except ValueError: + console.print(":cross_mark:[red]Invalid Tao amount[/red] [bold white]{}[/bold white]".format(amount)) + sys.exit() + else: + config.stake_all = True + class ListDelegatesCommand: @staticmethod diff --git a/bittensor/_subtensor/extrinsics/delegation.py b/bittensor/_subtensor/extrinsics/delegation.py index ce747be8e0..46aa1eb614 100644 --- a/bittensor/_subtensor/extrinsics/delegation.py +++ b/bittensor/_subtensor/extrinsics/delegation.py @@ -114,6 +114,34 @@ def do_delegation( else: raise StakeError(response.error_message) +def do_undelegation( + subtensor: 'bittensor.Subtensor', + wallet: 'bittensor.wallet', + delegate_ss58: str, + amount: 'bittensor.Balance', + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> bool: + with subtensor.substrate as substrate: + call = substrate.compose_call( + call_module='Paratensor', + call_function='remove_stake', + call_params={ + 'hotkey': delegate_ss58, + 'amount_unstaked': amount.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: + return True + response.process_events() + if response.is_success: + return True + else: + raise StakeError(response.error_message) + def delegate_extrinsic( subtensor: 'bittensor.Subtensor', @@ -124,7 +152,7 @@ def delegate_extrinsic( wait_for_finalization: bool = False, prompt: bool = False, ) -> bool: - r""" Delegates the specified amount of stake to passed hotkey uid. + r""" Delegates the specified amount of stake to the passed delegate. Args: wallet (bittensor.wallet): Bittensor wallet object. @@ -241,6 +269,138 @@ def delegate_extrinsic( bittensor.__console__.print(":cross_mark: [red]Failed[/red]: Error unknown.") return False + except NotRegisteredError as e: + bittensor.__console__.print(":cross_mark: [red]Hotkey: {} is not registered.[/red]".format(wallet.hotkey_str)) + return False + except StakeError as e: + bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e)) + return False + +def undelegate_extrinsic( + subtensor: 'bittensor.Subtensor', + wallet: 'bittensor.wallet', + delegate_ss58: Optional[str] = None, + amount: Union[Balance, float] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> bool: + r""" Un-delegates stake from the passed delegate. + Args: + wallet (bittensor.wallet): + Bittensor wallet object. + delegate_ss58 (Optional[str]): + ss58 address of the delegate. + amount (Union[Balance, float]): + Amount to unstake as bittensor balance, or float interpreted as Tao. + 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 uncluded in the block. + If we did not wait for finalization / inclusion, the response is true. + + Raises: + NotRegisteredError: + If the wallet is not registered on the chain. + NotDelegateError: + If the hotkey is not a delegate on the chain. + """ + # Decrypt keys, + wallet.coldkey + if not subtensor.is_hotkey_delegate( delegate_ss58 ): + raise NotDelegateError("Hotkey: {} is not a delegate.".format( delegate_ss58 )) + + # Get state. + my_prev_coldkey_balance = subtensor.get_balance( wallet.coldkey.ss58_address ) + delegate_take = subtensor.get_delegate_take( delegate_ss58 ) + delegate_owner = subtensor.get_hotkey_owner( delegate_ss58 ) + my_prev_delegated_stake = subtensor.get_stake_for_coldkey_and_hotkey( coldkey_ss58 = wallet.coldkeypub.ss58_address, hotkey_ss58 = delegate_ss58 ) + + # Convert to bittensor.Balance + if amount == None: + # Stake it all. + staking_balance = bittensor.Balance.from_tao( my_prev_coldkey_balance.tao ) + elif not isinstance(amount, bittensor.Balance ): + staking_balance = bittensor.Balance.from_tao( amount ) + else: + staking_balance = amount + + # Estimate transfer fee. + staking_fee = None # To be filled. + with bittensor.__console__.status(":satellite: Estimating Delegation Fees..."): + with subtensor.substrate as substrate: + call = substrate.compose_call( + call_module='Paratensor', + call_function='remove_stake', + call_params={ + 'hotkey': delegate_ss58, + 'amount_unstaked': staking_balance.rao + } + ) + payment_info = substrate.get_payment_info(call = call, keypair = wallet.coldkey) + if payment_info: + staking_fee = bittensor.Balance.from_rao(payment_info['partialFee']) + bittensor.__console__.print("[green]Estimated Fee: {}[/green]".format( staking_fee )) + else: + staking_fee = bittensor.Balance.from_tao( 0.2 ) + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: could not estimate delegation fee, assuming base fee of 0.2") + + # Check enough stake to unstake. + if staking_balance > my_prev_delegated_stake: + bittensor.__console__.print(":cross_mark: [red]Not enough stake[/red]:[bold white]\n stake:{}\n amount: {}\n coldkey: {}[/bold white]".format(my_prev_delegated_stake, staking_balance, wallet.name)) + return False + + # Check enough balance to unstake. + if my_prev_coldkey_balance < staking_fee: + bittensor.__console__.print(":cross_mark: [red]Not enough balance[/red]:[bold white]\n balance:{}\n amount: {}\n fee: {}\n coldkey: {}[/bold white]".format(my_prev_coldkey_balance, staking_balance, staking_fee, wallet.name)) + return False + + # Ask before moving on. + if prompt: + if not Confirm.ask("Do you want to un-delegate:[bold white]\n amount: {}\n to: {}\n fee: {}\n take: {}\n owner: {}[/bold white]".format( staking_balance, delegate_ss58, staking_fee, delegate_take, delegate_owner) ): + return False + + try: + with bittensor.__console__.status(":satellite: Unstaking from: [bold white]{}[/bold white] ...".format(subtensor.network)): + staking_response: bool = do_undelegation( + subtensor = subtensor, + wallet = wallet, + delegate_ss58 = delegate_ss58, + amount = staking_balance, + wait_for_inclusion = wait_for_inclusion, + wait_for_finalization = wait_for_finalization, + ) + + if staking_response: # If we successfully staked. + # 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]") + return True + + bittensor.__console__.print(":white_heavy_check_mark: [green]Finalized[/green]") + with bittensor.__console__.status(":satellite: Checking Balance on: [white]{}[/white] ...".format(subtensor.network)): + new_balance = subtensor.get_balance( address = wallet.coldkey.ss58_address ) + block = subtensor.get_current_block() + new_delegate_stake = subtensor.get_stake_for_coldkey_and_hotkey( + coldkey_ss58 = wallet.coldkeypub.ss58_address, + hotkey_ss58 = delegate_ss58, + block=block + ) # Get current stake + + bittensor.__console__.print("Balance:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( my_prev_coldkey_balance, new_balance )) + bittensor.__console__.print("Stake:\n [blue]{}[/blue] :arrow_right: [green]{}[/green]".format( my_prev_delegated_stake, new_delegate_stake )) + return True + else: + bittensor.__console__.print(":cross_mark: [red]Failed[/red]: Error unknown.") + return False + except NotRegisteredError as e: bittensor.__console__.print(":cross_mark: [red]Hotkey: {} is not registered.[/red]".format(wallet.hotkey_str)) return False diff --git a/bittensor/_subtensor/subtensor_impl.py b/bittensor/_subtensor/subtensor_impl.py index 93f8718478..59d11a9ce0 100644 --- a/bittensor/_subtensor/subtensor_impl.py +++ b/bittensor/_subtensor/subtensor_impl.py @@ -106,7 +106,7 @@ def delegate( wait_for_finalization: bool = False, prompt: bool = False, ) -> bool: - """ Adds the specified amount of stake to passed hotkey uid. """ + """ Adds the specified amount of stake to the passed delegate using the passed wallet. """ return delegate_extrinsic( subtensor = self, wallet = wallet, @@ -117,6 +117,26 @@ def delegate( prompt = prompt ) + def undelegate( + self, + wallet: 'bittensor.wallet', + delegate_ss58: Optional[str] = None, + amount: Union[Balance, float] = None, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> bool: + """ Removes the specified amount of stake from the passed delegate using the passed wallet. """ + return undelegate_extrinsic( + subtensor = self, + wallet = wallet, + delegate_ss58 = delegate_ss58, + amount = amount, + wait_for_inclusion = wait_for_inclusion, + wait_for_finalization = wait_for_finalization, + prompt = prompt + ) + ##################### #### Set Weights #### ##################### From 7f25af0d12702f4b409f741a706d9cb09fa54da0 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Wed, 8 Feb 2023 10:27:51 -0500 Subject: [PATCH 3/4] fix type of arg --- bittensor/_cli/commands/delegates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor/_cli/commands/delegates.py b/bittensor/_cli/commands/delegates.py index 3ce5186346..0131c3f1e1 100644 --- a/bittensor/_cli/commands/delegates.py +++ b/bittensor/_cli/commands/delegates.py @@ -165,7 +165,7 @@ def add_args( parser: argparse.ArgumentParser ): undelegate_stake_parser.add_argument( '--delegate_ss58key', dest = "delegate_ss58key", - type = float, + type = str, required = False, help='''The ss58 address of the choosen delegate''', ) From 6367b92167f0f539dc7300e579d5976c213b877b Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Tue, 14 Feb 2023 14:18:01 -0500 Subject: [PATCH 4/4] move delegate pull --- bittensor/_cli/commands/delegates.py | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/bittensor/_cli/commands/delegates.py b/bittensor/_cli/commands/delegates.py index 0131c3f1e1..98e92c1e09 100644 --- a/bittensor/_cli/commands/delegates.py +++ b/bittensor/_cli/commands/delegates.py @@ -108,16 +108,16 @@ def check_config( config: 'bittensor.Config' ): wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) config.wallet.name = str(wallet_name) - # Check for delegates. - with bittensor.__console__.status(":satellite: Loading delegates..."): - subtensor = bittensor.subtensor( config = config ) - delegates: List[bittensor.DelegateInfo] = subtensor.get_delegates() - - if len(delegates) == 0: - console.print(":cross_mark:[red]There are no delegates on {}[/red]".format(subtensor.network)) - sys.exit() - if not config.get('delegate_ss58key'): + # Check for delegates. + with bittensor.__console__.status(":satellite: Loading delegates..."): + subtensor = bittensor.subtensor( config = config ) + delegates: List[bittensor.DelegateInfo] = subtensor.get_delegates() + + if len(delegates) == 0: + console.print(":cross_mark:[red]There are no delegates on {}[/red]".format(subtensor.network)) + sys.exit(1) + show_delegates( delegates ) delegate_ss58key = Prompt.ask("Enter the delegate's ss58key") config.delegate_ss58key = str(delegate_ss58key) @@ -199,16 +199,16 @@ def check_config( config: 'bittensor.Config' ): wallet_name = Prompt.ask("Enter wallet name", default = bittensor.defaults.wallet.name) config.wallet.name = str(wallet_name) - # Check for delegates. - with bittensor.__console__.status(":satellite: Loading delegates..."): - subtensor = bittensor.subtensor( config = config ) - delegates: List[bittensor.DelegateInfo] = subtensor.get_delegates() - - if len(delegates) == 0: - console.print(":cross_mark:[red]There are no delegates on {}[/red]".format(subtensor.network)) - sys.exit() - if not config.get('delegate_ss58key'): + # Check for delegates. + with bittensor.__console__.status(":satellite: Loading delegates..."): + subtensor = bittensor.subtensor( config = config ) + delegates: List[bittensor.DelegateInfo] = subtensor.get_delegates() + + if len(delegates) == 0: + console.print(":cross_mark:[red]There are no delegates on {}[/red]".format(subtensor.network)) + sys.exit(1) + show_delegates( delegates ) delegate_ss58key = Prompt.ask("Enter the delegate's ss58key") config.delegate_ss58key = str(delegate_ss58key)