Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add undelegate command #1076

Merged
merged 4 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bittensor/_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 )

Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions bittensor/_cli/cli_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
2 changes: 1 addition & 1 deletion bittensor/_cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
109 changes: 100 additions & 9 deletions bittensor/_cli/commands/delegates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 = str,
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)

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)

# 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
Expand Down
168 changes: 164 additions & 4 deletions bittensor/_subtensor/extrinsics/delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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.
Expand Down Expand Up @@ -197,9 +225,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.
Expand Down Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion bittensor/_subtensor/subtensor_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 ####
#####################
Expand Down