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 in check for minimum stake for unstaking #1832

Merged
merged 10 commits into from
May 1, 2024
31 changes: 16 additions & 15 deletions bittensor/commands/unstake.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,22 +85,23 @@ def check_config(cls, config: "bittensor.config"):
hotkeys = str(config.hotkeys).replace("[", "").replace("]", "")
else:
hotkeys = str(config.wallet.hotkey)
if not Confirm.ask(
"Unstake all Tao from: [bold]'{}'[/bold]?".format(hotkeys)
):
amount = Prompt.ask("Enter Tao amount to unstake")
config.unstake_all = False
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:
if config.no_prompt:
config.unstake_all = True
else:
# I really don't like this logic flow. It can be a bit confusing to read for something
# as serious as unstaking all.
if Confirm.ask(f"Unstake all Tao from: [bold]'{hotkeys}'[/bold]?"):
config.unstake_all = True
else:
config.unstake_all = False
amount = Prompt.ask("Enter Tao amount to unstake")
try:
config.amount = float(amount)
except ValueError:
console.print(
f":cross_mark:[red] Invalid Tao amount[/red] [bold white]{amount}[/bold white]"
)
sys.exit()

@staticmethod
def add_args(command_parser):
Expand Down
32 changes: 32 additions & 0 deletions bittensor/extrinsics/unstaking.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,32 @@ def __do_remove_stake_single(
return success


def check_threshold_amount(unstaking_balance: Balance) -> bool:
"""
Checks if the unstaking amount is above the threshold or 0

Args:
unstaking_balance (Balance):
the balance to check for threshold limits.

Returns:
success (bool):
``true`` if the unstaking is above the threshold or 0, or ``false`` if the
unstaking is below the threshold, but not 0.
"""
# This is a hard-coded value but should not be in the future. It is currently 0.1 TAO but will change to 1
# Hopefully soon, the get_nominator_min_required_stake fn will be exposed for rpc call
min_req_stake: Balance = Balance.from_float(0.1) # TAO

if min_req_stake > unstaking_balance > 0:
bittensor.__console__.print(
f":cross_mark: [red]Unstaking balance of {unstaking_balance} less than minimum of {min_req_stake} TAO[/red]"
)
return False
else:
return True


def unstake_extrinsic(
subtensor: "bittensor.subtensor",
wallet: "bittensor.wallet",
Expand Down Expand Up @@ -134,6 +160,9 @@ def unstake_extrinsic(
)
return False

if not check_threshold_amount(unstaking_balance=unstaking_balance):
return False

# Ask before moving on.
if prompt:
if not Confirm.ask(
Expand Down Expand Up @@ -305,6 +334,9 @@ def unstake_multiple_extrinsic(
)
continue

if not check_threshold_amount(unstaking_balance=unstaking_balance):
return False

# Ask before moving on.
if prompt:
if not Confirm.ask(
Expand Down
139 changes: 139 additions & 0 deletions tests/integration_tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,145 @@ def mock_get_wallet(*args, **kwargs):
stake.tao, mock_stakes[wallet.hotkey_str].tao, places=4
)

def test_unstake_with_thresholds(self, _):
config = self.config
config.command = "stake"
config.subcommand = "remove"
config.no_prompt = True
config.amount = 0.02
config.wallet.name = "fake_wallet"
config.hotkeys = ["hk0", "hk1", "hk2"]
config.all_hotkeys = False
# Notice no max_stake specified

mock_stakes: Dict[str, Balance] = {
"hk0": Balance.from_float(10.0),
"hk1": Balance.from_float(11.1),
"hk2": Balance.from_float(12.2),
}

mock_coldkey_kp = _get_mock_keypair(0, self.id())

mock_wallets = [
SimpleNamespace(
name=config.wallet.name,
coldkey=mock_coldkey_kp,
coldkeypub=mock_coldkey_kp,
hotkey_str=hk,
hotkey=_get_mock_keypair(idx + 100, self.id()),
)
for idx, hk in enumerate(config.hotkeys)
]

# Register mock wallets and give them stakes

for wallet in mock_wallets:
_ = _subtensor_mock.force_register_neuron(
netuid=1,
hotkey=wallet.hotkey.ss58_address,
coldkey=wallet.coldkey.ss58_address,
stake=mock_stakes[wallet.hotkey_str].rao,
)

cli = bittensor.cli(config)

def mock_get_wallet(*args, **kwargs):
if kwargs.get("hotkey"):
for wallet in mock_wallets:
if wallet.hotkey_str == kwargs.get("hotkey"):
return wallet
else:
return mock_wallets[0]

with patch("bittensor.wallet") as mock_create_wallet:
mock_create_wallet.side_effect = mock_get_wallet

# Check stakes before unstaking
for wallet in mock_wallets:
stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey(
hotkey_ss58=wallet.hotkey.ss58_address,
coldkey_ss58=wallet.coldkey.ss58_address,
)
self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao)

cli.run()

# Check stakes after unstaking
for wallet in mock_wallets:
stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey(
hotkey_ss58=wallet.hotkey.ss58_address,
coldkey_ss58=wallet.coldkey.ss58_address,
)
# because the amount is less than the threshold, none of these should unstake
self.assertEqual(stake.tao, mock_stakes[wallet.hotkey_str].tao)

def test_unstake_all(self, _):
config = self.config
config.command = "stake"
config.subcommand = "remove"
config.no_prompt = True
config.amount = 0.0 # 0 implies full unstake
config.wallet.name = "fake_wallet"
config.hotkeys = ["hk0"]
config.all_hotkeys = False

mock_stakes: Dict[str, Balance] = {"hk0": Balance.from_float(10.0)}

mock_coldkey_kp = _get_mock_keypair(0, self.id())

mock_wallets = [
SimpleNamespace(
name=config.wallet.name,
coldkey=mock_coldkey_kp,
coldkeypub=mock_coldkey_kp,
hotkey_str=hk,
hotkey=_get_mock_keypair(idx + 100, self.id()),
)
for idx, hk in enumerate(config.hotkeys)
]

# Register mock wallets and give them stakes

for wallet in mock_wallets:
_ = _subtensor_mock.force_register_neuron(
netuid=1,
hotkey=wallet.hotkey.ss58_address,
coldkey=wallet.coldkey.ss58_address,
stake=mock_stakes[wallet.hotkey_str].rao,
)

cli = bittensor.cli(config)

def mock_get_wallet(*args, **kwargs):
if kwargs.get("hotkey"):
for wallet in mock_wallets:
if wallet.hotkey_str == kwargs.get("hotkey"):
return wallet
else:
return mock_wallets[0]

with patch("bittensor.wallet") as mock_create_wallet:
mock_create_wallet.side_effect = mock_get_wallet

# Check stakes before unstaking
for wallet in mock_wallets:
stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey(
hotkey_ss58=wallet.hotkey.ss58_address,
coldkey_ss58=wallet.coldkey.ss58_address,
)
self.assertEqual(stake.rao, mock_stakes[wallet.hotkey_str].rao)

cli.run()

# Check stakes after unstaking
for wallet in mock_wallets:
stake = _subtensor_mock.get_stake_for_coldkey_and_hotkey(
hotkey_ss58=wallet.hotkey.ss58_address,
coldkey_ss58=wallet.coldkey.ss58_address,
)
# because the amount is less than the threshold, none of these should unstake
self.assertEqual(stake.tao, Balance.from_tao(0))

def test_stake_with_specific_hotkeys(self, _):
config = self.config
config.command = "stake"
Expand Down