diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f850bb076..9123de2c44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 6.12.2 / 2024-05-20
+
+## What's Changed
+* Add setting delegate take
+* fix: deprecated transfer method usage
+
+**Full Changelog**: https://github.com/opentensor/bittensor/compare/v6.12.1...54eee604c00ac4f04a31d5d7bc663124731a34d8
+
+
## 6.12.1 / 2024-05-17
## What's Changed
diff --git a/VERSION b/VERSION
index ff61e18689..cf1279ccf0 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-6.12.1
+6.12.2
\ No newline at end of file
diff --git a/bittensor/__init__.py b/bittensor/__init__.py
index 3f5c1d99ec..8678197e0b 100644
--- a/bittensor/__init__.py
+++ b/bittensor/__init__.py
@@ -28,7 +28,7 @@
# Bittensor code and protocol version.
-__version__ = "6.12.1"
+__version__ = "6.12.2"
version_split = __version__.split(".")
__version_as_int__: int = (
diff --git a/bittensor/cli.py b/bittensor/cli.py
index b1b544b906..0f708aca79 100644
--- a/bittensor/cli.py
+++ b/bittensor/cli.py
@@ -51,6 +51,7 @@
RunFaucetCommand,
SenateCommand,
SetIdentityCommand,
+ SetTakeCommand,
StakeCommand,
StakeShow,
SubnetGetHyperparamsCommand,
@@ -121,6 +122,7 @@
"senate": SenateCommand,
"register": RootRegisterCommand,
"proposals": ProposalsCommand,
+ "set_take": SetTakeCommand,
"delegate": DelegateStakeCommand,
"undelegate": DelegateUnstakeCommand,
"my_delegates": MyDelegatesCommand,
diff --git a/bittensor/commands/__init__.py b/bittensor/commands/__init__.py
index 7e52f0a1ed..fb37044638 100644
--- a/bittensor/commands/__init__.py
+++ b/bittensor/commands/__init__.py
@@ -77,6 +77,7 @@
DelegateStakeCommand,
DelegateUnstakeCommand,
MyDelegatesCommand,
+ SetTakeCommand,
)
from .wallets import (
NewColdkeyCommand,
diff --git a/bittensor/commands/delegates.py b/bittensor/commands/delegates.py
index 1dcacd9149..5d7ff4fcd8 100644
--- a/bittensor/commands/delegates.py
+++ b/bittensor/commands/delegates.py
@@ -21,8 +21,7 @@
import bittensor
from typing import List, Optional
from rich.table import Table
-from rich.prompt import Prompt
-from rich.prompt import Confirm
+from rich.prompt import Confirm, FloatPrompt, Prompt
from rich.console import Text
from tqdm import tqdm
from substrateinterface.exceptions import SubstrateRequestException
@@ -891,3 +890,113 @@ def check_config(config: "bittensor.config"):
):
wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name)
config.wallet.name = str(wallet_name)
+
+
+class SetTakeCommand:
+ """
+ Executes the ``set_take`` command, which sets the delegate take.
+ The command performs several checks:
+ 1. Hotkey is already a delegate
+ 2. New take value is within 0-18% range
+ Optional Arguments:
+ - ``take``: The new take value
+ - ``wallet.name``: The name of the wallet to use for the command.
+ - ``wallet.hotkey``: The name of the hotkey to use for the command.
+ Usage:
+ To run the command, the user must have a configured wallet with both hotkey and coldkey. Also, the hotkey should already be a delegate.
+ Example usage::
+ btcli root set_take --wallet.name my_wallet --wallet.hotkey my_hotkey
+ Note:
+ This function can be used to update the takes individually for every subnet
+ """
+
+ @staticmethod
+ def run(cli: "bittensor.cli"):
+ r"""Set delegate take."""
+ try:
+ subtensor: "bittensor.subtensor" = bittensor.subtensor(
+ config=cli.config, log_verbose=False
+ )
+ SetTakeCommand._run(cli, subtensor)
+ finally:
+ if "subtensor" in locals():
+ subtensor.close()
+ bittensor.logging.debug("closing subtensor connection")
+
+ @staticmethod
+ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"):
+ r"""Set delegate take."""
+ config = cli.config.copy()
+ wallet = bittensor.wallet(config=cli.config)
+
+ # Unlock the wallet.
+ wallet.hotkey
+ wallet.coldkey
+
+ # Check if the hotkey is not a delegate.
+ if not subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address):
+ bittensor.__console__.print(
+ "Aborting: Hotkey {} is NOT a delegate.".format(
+ wallet.hotkey.ss58_address
+ )
+ )
+ return
+
+ # Prompt user for take value.
+ new_take_str = config.get("take")
+ if new_take_str == None:
+ new_take = FloatPrompt.ask(f"Enter take value (0.18 for 18%)")
+ else:
+ new_take = float(new_take_str)
+
+ if new_take > 0.18:
+ bittensor.__console__.print(
+ "ERROR: Take value should be in the range of 0 to 18%"
+ )
+ return
+
+ result: bool = subtensor.set_take(
+ wallet=wallet,
+ delegate_ss58=wallet.hotkey.ss58_address,
+ take=new_take,
+ )
+ if not result:
+ bittensor.__console__.print("Could not set the take")
+ else:
+ # Check if we are a delegate.
+ is_delegate: bool = subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address)
+ if not is_delegate:
+ bittensor.__console__.print(
+ "Could not set the take [white]{}[/white]".format(subtensor.network)
+ )
+ return
+ bittensor.__console__.print(
+ "Successfully set the take on [white]{}[/white]".format(
+ subtensor.network
+ )
+ )
+
+ @staticmethod
+ def add_args(parser: argparse.ArgumentParser):
+ set_take_parser = parser.add_parser(
+ "set_take", help="""Set take for delegate"""
+ )
+ set_take_parser.add_argument(
+ "--take",
+ dest="take",
+ type=float,
+ required=False,
+ help="""Take as a float number""",
+ )
+ bittensor.wallet.add_args(set_take_parser)
+ bittensor.subtensor.add_args(set_take_parser)
+
+ @staticmethod
+ def check_config(config: "bittensor.config"):
+ if not config.is_set("wallet.name") and not config.no_prompt:
+ wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name)
+ config.wallet.name = str(wallet_name)
+
+ if not config.is_set("wallet.hotkey") and not config.no_prompt:
+ hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey)
+ config.wallet.hotkey = str(hotkey)
diff --git a/bittensor/commands/unstake.py b/bittensor/commands/unstake.py
index a2290d5b84..87d13aab91 100644
--- a/bittensor/commands/unstake.py
+++ b/bittensor/commands/unstake.py
@@ -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):
diff --git a/bittensor/errors.py b/bittensor/errors.py
index 13cb43fc1b..de51b5d48a 100644
--- a/bittensor/errors.py
+++ b/bittensor/errors.py
@@ -64,6 +64,12 @@ class NominationError(ChainTransactionError):
pass
+class TakeError(ChainTransactionError):
+ r"""Error raised when a increase / decrease take transaction fails."""
+
+ pass
+
+
class TransferError(ChainTransactionError):
r"""Error raised when a transfer transaction fails."""
diff --git a/bittensor/extrinsics/delegation.py b/bittensor/extrinsics/delegation.py
index 0b3bdb7d64..f568f63998 100644
--- a/bittensor/extrinsics/delegation.py
+++ b/bittensor/extrinsics/delegation.py
@@ -18,7 +18,13 @@
import logging
import bittensor
-from ..errors import NominationError, NotDelegateError, NotRegisteredError, StakeError
+from ..errors import (
+ NominationError,
+ NotDelegateError,
+ NotRegisteredError,
+ StakeError,
+ TakeError,
+)
from rich.prompt import Confirm
from typing import Union, Optional
from bittensor.utils.balance import Balance
@@ -357,3 +363,137 @@ def undelegate_extrinsic(
except StakeError as e:
bittensor.__console__.print(":cross_mark: [red]Stake Error: {}[/red]".format(e))
return False
+
+
+def decrease_take_extrinsic(
+ subtensor: "bittensor.subtensor",
+ wallet: "bittensor.wallet",
+ hotkey_ss58: Optional[str] = None,
+ take: int = 0,
+ wait_for_finalization: bool = False,
+ wait_for_inclusion: bool = True,
+) -> bool:
+ r"""Decrease delegate take for the hotkey.
+
+ Args:
+ wallet (bittensor.wallet):
+ Bittensor wallet object.
+ hotkey_ss58 (Optional[str]):
+ The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey.
+ take (float):
+ The ``take`` of the hotkey.
+ Returns:
+ success (bool): ``True`` if the transaction was successful.
+ """
+ # Unlock the coldkey.
+ wallet.coldkey
+ wallet.hotkey
+
+ with bittensor.__console__.status(
+ ":satellite: Sending decrease_take_extrinsic call on [white]{}[/white] ...".format(
+ subtensor.network
+ )
+ ):
+ try:
+ success = subtensor._do_decrease_take(
+ wallet=wallet,
+ hotkey_ss58=hotkey_ss58,
+ take=take,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ )
+
+ if success == True:
+ bittensor.__console__.print(
+ ":white_heavy_check_mark: [green]Finalized[/green]"
+ )
+ bittensor.logging.success(
+ prefix="Decrease Delegate Take",
+ sufix="Finalized: " + str(success),
+ )
+
+ return success
+
+ except Exception as e:
+ bittensor.__console__.print(
+ ":cross_mark: [red]Failed[/red]: error:{}".format(e)
+ )
+ bittensor.logging.warning(
+ prefix="Set weights", sufix="Failed: " + str(e)
+ )
+ except TakeError as e:
+ bittensor.__console__.print(
+ ":cross_mark: [red]Failed[/red]: error:{}".format(e)
+ )
+ bittensor.logging.warning(
+ prefix="Set weights", sufix="Failed: " + str(e)
+ )
+
+ return False
+
+
+def increase_take_extrinsic(
+ subtensor: "bittensor.subtensor",
+ wallet: "bittensor.wallet",
+ hotkey_ss58: Optional[str] = None,
+ take: int = 0,
+ wait_for_finalization: bool = False,
+ wait_for_inclusion: bool = True,
+) -> bool:
+ r"""Increase delegate take for the hotkey.
+
+ Args:
+ wallet (bittensor.wallet):
+ Bittensor wallet object.
+ hotkey_ss58 (Optional[str]):
+ The ``ss58`` address of the hotkey account to stake to defaults to the wallet's hotkey.
+ take (float):
+ The ``take`` of the hotkey.
+ Returns:
+ success (bool): ``True`` if the transaction was successful.
+ """
+ # Unlock the coldkey.
+ wallet.coldkey
+ wallet.hotkey
+
+ with bittensor.__console__.status(
+ ":satellite: Sending increase_take_extrinsic call on [white]{}[/white] ...".format(
+ subtensor.network
+ )
+ ):
+ try:
+ success = subtensor._do_increase_take(
+ wallet=wallet,
+ hotkey_ss58=hotkey_ss58,
+ take=take,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ )
+
+ if success == True:
+ bittensor.__console__.print(
+ ":white_heavy_check_mark: [green]Finalized[/green]"
+ )
+ bittensor.logging.success(
+ prefix="Increase Delegate Take",
+ sufix="Finalized: " + str(success),
+ )
+
+ return success
+
+ except Exception as e:
+ bittensor.__console__.print(
+ ":cross_mark: [red]Failed[/red]: error:{}".format(e)
+ )
+ bittensor.logging.warning(
+ prefix="Set weights", sufix="Failed: " + str(e)
+ )
+ except TakeError as e:
+ bittensor.__console__.print(
+ ":cross_mark: [red]Failed[/red]: error:{}".format(e)
+ )
+ bittensor.logging.warning(
+ prefix="Set weights", sufix="Failed: " + str(e)
+ )
+
+ return False
diff --git a/bittensor/extrinsics/root.py b/bittensor/extrinsics/root.py
index ab8b314870..5c98eebe8b 100644
--- a/bittensor/extrinsics/root.py
+++ b/bittensor/extrinsics/root.py
@@ -129,6 +129,7 @@ def set_root_weights_extrinsic(
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``.
"""
+
# First convert types.
if isinstance(netuids, list):
netuids = torch.tensor(netuids, dtype=torch.int64)
diff --git a/bittensor/extrinsics/unstaking.py b/bittensor/extrinsics/unstaking.py
index 5fca6b119b..6046124f40 100644
--- a/bittensor/extrinsics/unstaking.py
+++ b/bittensor/extrinsics/unstaking.py
@@ -71,6 +71,32 @@ def __do_remove_stake_single(
return success
+def check_threshold_amount(
+ subtensor: "bittensor.subtensor", 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.
+ """
+ min_req_stake: Balance = subtensor.get_minimum_required_stake()
+
+ 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",
@@ -134,6 +160,11 @@ def unstake_extrinsic(
)
return False
+ if not check_threshold_amount(
+ subtensor=subtensor, unstaking_balance=unstaking_balance
+ ):
+ return False
+
# Ask before moving on.
if prompt:
if not Confirm.ask(
@@ -305,6 +336,11 @@ def unstake_multiple_extrinsic(
)
continue
+ if not check_threshold_amount(
+ subtensor=subtensor, unstaking_balance=unstaking_balance
+ ):
+ return False
+
# Ask before moving on.
if prompt:
if not Confirm.ask(
diff --git a/bittensor/mock/subtensor_mock.py b/bittensor/mock/subtensor_mock.py
index 7b944ec3fe..4ca08cfb22 100644
--- a/bittensor/mock/subtensor_mock.py
+++ b/bittensor/mock/subtensor_mock.py
@@ -1305,6 +1305,18 @@ def _do_unstake(
return True
+ @staticmethod
+ def min_required_stake():
+ """
+ As the minimum required stake may change, this method allows us to dynamically
+ update the amount in the mock without updating the tests
+ """
+ # valid minimum threshold as of 2024/05/01
+ return 100_000_000 # RAO
+
+ def get_minimum_required_stake(self):
+ return Balance.from_rao(self.min_required_stake())
+
def get_delegate_by_hotkey(
self, hotkey_ss58: str, block: Optional[int] = None
) -> Optional["DelegateInfo"]:
diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py
index 7ad6256833..26c1c52bbd 100644
--- a/bittensor/subtensor.py
+++ b/bittensor/subtensor.py
@@ -47,7 +47,7 @@
IPInfo,
custom_rpc_type_registry,
)
-from .errors import IdentityError, NominationError, StakeError
+from .errors import IdentityError, NominationError, StakeError, TakeError
from .extrinsics.network import (
register_subnetwork_extrinsic,
set_hyperparameter_extrinsic,
@@ -73,6 +73,8 @@
delegate_extrinsic,
nominate_extrinsic,
undelegate_extrinsic,
+ increase_take_extrinsic,
+ decrease_take_extrinsic,
)
from .extrinsics.senate import (
register_senate_extrinsic,
@@ -542,6 +544,68 @@ def undelegate(
prompt=prompt,
)
+ def set_take(
+ self,
+ wallet: "bittensor.wallet",
+ delegate_ss58: Optional[str] = None,
+ take: float = 0.0,
+ wait_for_inclusion: bool = True,
+ wait_for_finalization: bool = False,
+ ) -> bool:
+ """
+ Set delegate hotkey take
+ Args:
+ wallet (bittensor.wallet): The wallet containing the hotkey to be nominated.
+ delegate_ss58 (str, optional): Hotkey
+ take (float): Delegate take on subnet ID
+ wait_for_finalization (bool, optional): If ``True``, waits until the transaction is finalized on the blockchain.
+ wait_for_inclusion (bool, optional): If ``True``, waits until the transaction is included in a block.
+ Returns:
+ bool: ``True`` if the process is successful, False otherwise.
+ This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the
+ dynamic selection and participation of validators in the network's consensus process.
+ """
+ # Ensure delegate_ss58 is not None
+ if delegate_ss58 is None:
+ raise ValueError("delegate_ss58 cannot be None")
+
+ # Caulate u16 representation of the take
+ takeu16 = int(take * 0xFFFF)
+
+ # Check if the new take is greater or lower than existing take or if existing is set
+ delegate = self.get_delegate_by_hotkey(delegate_ss58)
+ current_take = None
+ if delegate is not None:
+ current_take = int(float(delegate.take) * 65535.0)
+
+ if takeu16 == current_take:
+ bittensor.__console__.print("Nothing to do, take hasn't changed")
+ return True
+ if current_take is None or current_take < takeu16:
+ bittensor.__console__.print(
+ "Current take is either not set or is lower than the new one. Will use increase_take"
+ )
+ return increase_take_extrinsic(
+ subtensor=self,
+ wallet=wallet,
+ hotkey_ss58=delegate_ss58,
+ take=takeu16,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ )
+ else:
+ bittensor.__console__.print(
+ "Current take is higher than the new one. Will use decrease_take"
+ )
+ return decrease_take_extrinsic(
+ subtensor=self,
+ wallet=wallet,
+ hotkey_ss58=delegate_ss58,
+ take=takeu16,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ )
+
def send_extrinsic(
self,
wallet: "bittensor.wallet",
@@ -1136,7 +1200,7 @@ def get_transfer_fee(
call = self.substrate.compose_call(
call_module="Balances",
- call_function="transfer",
+ call_function="transfer_allow_death",
call_params={"dest": dest, "value": transfer_balance.rao},
)
@@ -1181,7 +1245,7 @@ def _do_transfer(
def make_substrate_call_with_retry():
call = self.substrate.compose_call(
call_module="Balances",
- call_function="transfer",
+ call_function="transfer_allow_death",
call_params={"dest": dest, "value": transfer_balance.rao},
)
extrinsic = self.substrate.create_signed_extrinsic(
@@ -3659,6 +3723,18 @@ def get_stake_info_for_coldkeys(
return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore
+ def get_minimum_required_stake(
+ self,
+ ):
+ @retry(delay=2, tries=3, backoff=2, max_delay=4, logger=logger)
+ def make_substrate_call_with_retry():
+ return self.substrate.query(
+ module="SubtensorModule", storage_function="NominatorMinRequiredStake"
+ )
+
+ result = make_substrate_call_with_retry()
+ return Balance.from_rao(result.decode())
+
########################################
#### Neuron information per subnet ####
########################################
@@ -4312,6 +4388,82 @@ def make_substrate_call_with_retry():
return make_substrate_call_with_retry()
+ def _do_increase_take(
+ self,
+ wallet: "bittensor.wallet",
+ hotkey_ss58: str,
+ take: int,
+ wait_for_inclusion: bool = True,
+ wait_for_finalization: bool = False,
+ ) -> bool:
+ @retry(delay=2, tries=3, backoff=2, max_delay=4)
+ def make_substrate_call_with_retry():
+ with self.substrate as substrate:
+ call = substrate.compose_call(
+ call_module="SubtensorModule",
+ call_function="increase_take",
+ call_params={
+ "hotkey": hotkey_ss58,
+ "take": take,
+ },
+ )
+ extrinsic = substrate.create_signed_extrinsic(
+ call=call, keypair=wallet.coldkey
+ ) # sign with 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 TakeError(response.error_message)
+
+ return make_substrate_call_with_retry()
+
+ def _do_decrease_take(
+ self,
+ wallet: "bittensor.wallet",
+ hotkey_ss58: str,
+ take: int,
+ wait_for_inclusion: bool = True,
+ wait_for_finalization: bool = False,
+ ) -> bool:
+ @retry(delay=2, tries=3, backoff=2, max_delay=4)
+ def make_substrate_call_with_retry():
+ with self.substrate as substrate:
+ call = substrate.compose_call(
+ call_module="SubtensorModule",
+ call_function="decrease_take",
+ call_params={
+ "hotkey": hotkey_ss58,
+ "take": take,
+ },
+ )
+ extrinsic = substrate.create_signed_extrinsic(
+ call=call, keypair=wallet.coldkey
+ ) # sign with 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 TakeError(response.error_message)
+
+ return make_substrate_call_with_retry()
+
################
#### Legacy ####
################
diff --git a/tests/e2e_tests/__init__.py b/tests/e2e_tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py
new file mode 100644
index 0000000000..2300eafc77
--- /dev/null
+++ b/tests/e2e_tests/conftest.py
@@ -0,0 +1,57 @@
+import os
+import signal
+from substrateinterface import SubstrateInterface
+import pytest
+import subprocess
+import logging
+import shlex
+import re
+import time
+
+logging.basicConfig(level=logging.INFO)
+
+
+# Fixture for setting up and tearing down a localnet.sh chain between tests
+@pytest.fixture(scope="function")
+def local_chain():
+ # Get the environment variable for the script path
+ script_path = os.getenv("LOCALNET_SH_PATH")
+
+ if not script_path:
+ # Skip the test if the localhost.sh path is not set
+ logging.warning("LOCALNET_SH_PATH env variable is not set, e2e test skipped.")
+ pytest.skip("LOCALNET_SH_PATH environment variable is not set.")
+
+ # Start new node process
+ cmds = shlex.split(script_path)
+ process = subprocess.Popen(
+ cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid
+ )
+
+ # Pattern match indicates node is compiled and ready
+ pattern = re.compile(r"Successfully ran block step\.")
+
+ def wait_for_node_start(process, pattern):
+ for line in process.stdout:
+ print(line.strip())
+ if pattern.search(line):
+ print("Node started!")
+ break
+
+ wait_for_node_start(process, pattern)
+
+ # Run the test, passing in substrate interface
+ yield SubstrateInterface(url="ws://127.0.0.1:9945")
+
+ # Terminate the process group (includes all child processes)
+ os.killpg(os.getpgid(process.pid), signal.SIGTERM)
+
+ # Give some time for the process to terminate
+ time.sleep(1)
+
+ # If the process is not terminated, send SIGKILL
+ if process.poll() is None:
+ os.killpg(os.getpgid(process.pid), signal.SIGKILL)
+
+ # Ensure the process has terminated
+ process.wait()
diff --git a/tests/e2e_tests/multistep/__init__.py b/tests/e2e_tests/multistep/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/e2e_tests/multistep/test_last_tx_block.py b/tests/e2e_tests/multistep/test_last_tx_block.py
new file mode 100644
index 0000000000..0d1796f5d8
--- /dev/null
+++ b/tests/e2e_tests/multistep/test_last_tx_block.py
@@ -0,0 +1,51 @@
+from bittensor.commands.root import RootRegisterCommand
+from bittensor.commands.delegates import NominateCommand
+from bittensor.commands.network import RegisterSubnetworkCommand
+from bittensor.commands.register import RegisterCommand
+from ..utils import setup_wallet
+
+
+# Automated testing for take related tests described in
+# https://discord.com/channels/799672011265015819/1176889736636407808/1236057424134144152
+def test_takes(local_chain):
+ # Register root as Alice
+ (keypair, exec_command) = setup_wallet("//Alice")
+ exec_command(RootRegisterCommand, ["root", "register"])
+
+ # Create subnet 1 and verify created successfully
+ assert not (local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize())
+
+ exec_command(RegisterSubnetworkCommand, ["s", "create"])
+ assert local_chain.query("SubtensorModule", "NetworksAdded", [1])
+
+ assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize()
+
+ # Register and nominate Bob
+ (keypair, exec_command) = setup_wallet("//Bob")
+ assert (
+ local_chain.query(
+ "SubtensorModule", "LastTxBlock", [keypair.ss58_address]
+ ).serialize()
+ == 0
+ )
+
+ assert (
+ local_chain.query(
+ "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address]
+ ).serialize()
+ == 0
+ )
+ exec_command(RegisterCommand, ["s", "register", "--neduid", "1"])
+ exec_command(NominateCommand, ["root", "nominate"])
+ assert (
+ local_chain.query(
+ "SubtensorModule", "LastTxBlock", [keypair.ss58_address]
+ ).serialize()
+ > 0
+ )
+ assert (
+ local_chain.query(
+ "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address]
+ ).serialize()
+ > 0
+ )
diff --git a/tests/e2e_tests/subcommands/__init__.py b/tests/e2e_tests/subcommands/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/e2e_tests/subcommands/delegation/__init__.py b/tests/e2e_tests/subcommands/delegation/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py b/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py
new file mode 100644
index 0000000000..0453576332
--- /dev/null
+++ b/tests/e2e_tests/subcommands/delegation/test_set_delegate_take.py
@@ -0,0 +1,56 @@
+from bittensor.commands.delegates import SetTakeCommand, NominateCommand
+from bittensor.commands.network import RegisterSubnetworkCommand
+from bittensor.commands.register import RegisterCommand
+from bittensor.commands.root import RootRegisterCommand
+
+from tests.e2e_tests.utils import setup_wallet
+
+
+def test_set_delegate_increase_take(local_chain):
+ # Register root as Alice
+ (keypair, exec_command) = setup_wallet("//Alice")
+ exec_command(RootRegisterCommand, ["root", "register"])
+
+ # Create subnet 1 and verify created successfully
+ assert not (local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize())
+
+ exec_command(RegisterSubnetworkCommand, ["s", "create"])
+ assert local_chain.query("SubtensorModule", "NetworksAdded", [1])
+
+ assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize()
+
+ # Register and nominate Bob
+ (keypair, exec_command) = setup_wallet("//Bob")
+ assert (
+ local_chain.query(
+ "SubtensorModule", "LastTxBlock", [keypair.ss58_address]
+ ).serialize()
+ == 0
+ )
+
+ assert (
+ local_chain.query(
+ "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address]
+ ).serialize()
+ == 0
+ )
+ exec_command(RegisterCommand, ["s", "register", "--netuid", "1"])
+ exec_command(NominateCommand, ["root", "nominate"])
+ assert (
+ local_chain.query(
+ "SubtensorModule", "LastTxBlock", [keypair.ss58_address]
+ ).serialize()
+ > 0
+ )
+ assert (
+ local_chain.query(
+ "SubtensorModule", "LastTxBlockDelegateTake", [keypair.ss58_address]
+ ).serialize()
+ > 0
+ )
+
+ # Set delegate take for Bob
+ exec_command(SetTakeCommand, ["r", "set_take", "--take", "0.15"])
+ assert local_chain.query(
+ "SubtensorModule", "Delegates", [keypair.ss58_address]
+ ).value == int(0.15 * 65535)
diff --git a/tests/e2e_tests/subcommands/wallet/__init__.py b/tests/e2e_tests/subcommands/wallet/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/e2e_tests/subcommands/wallet/test_transfer.py b/tests/e2e_tests/subcommands/wallet/test_transfer.py
new file mode 100644
index 0000000000..de8052e027
--- /dev/null
+++ b/tests/e2e_tests/subcommands/wallet/test_transfer.py
@@ -0,0 +1,32 @@
+from bittensor.commands.transfer import TransferCommand
+from ...utils import setup_wallet
+import bittensor
+
+
+# Example test using the local_chain fixture
+def test_transfer(local_chain):
+ (keypair, exec_command) = setup_wallet("//Alice")
+
+ acc_before = local_chain.query("System", "Account", [keypair.ss58_address])
+ exec_command(
+ TransferCommand,
+ [
+ "wallet",
+ "transfer",
+ "--amount",
+ "2",
+ "--dest",
+ "5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx",
+ ],
+ )
+ acc_after = local_chain.query("System", "Account", [keypair.ss58_address])
+
+ expected_transfer = 2_000_000_000
+ tolerance = 200_000 # Tx fee tolerance
+
+ actual_difference = (
+ acc_before.value["data"]["free"] - acc_after.value["data"]["free"]
+ )
+ assert (
+ expected_transfer <= actual_difference <= expected_transfer + tolerance
+ ), f"Expected transfer with tolerance: {expected_transfer} <= {actual_difference} <= {expected_transfer + tolerance}"
diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py
new file mode 100644
index 0000000000..3ad789dd6d
--- /dev/null
+++ b/tests/e2e_tests/utils.py
@@ -0,0 +1,32 @@
+from substrateinterface import Keypair
+from typing import List
+import bittensor
+
+
+def setup_wallet(uri: str):
+ keypair = Keypair.create_from_uri(uri)
+ wallet_path = "/tmp/btcli-e2e-wallet-{}".format(uri.strip("/"))
+ wallet = bittensor.wallet(path=wallet_path)
+ wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True)
+ wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True)
+ wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True)
+
+ def exec_command(command, extra_args: List[str]):
+ parser = bittensor.cli.__create_parser__()
+ args = extra_args + [
+ "--no_prompt",
+ "--subtensor.network",
+ "local",
+ "--subtensor.chain_endpoint",
+ "ws://localhost:9945",
+ "--wallet.path",
+ wallet_path,
+ ]
+ config = bittensor.config(
+ parser=parser,
+ args=args,
+ )
+ cli_instance = bittensor.cli(config)
+ command.run(cli_instance)
+
+ return (keypair, exec_command)
diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py
index a0e9f425cf..5c6c9f377a 100644
--- a/tests/integration_tests/test_cli.py
+++ b/tests/integration_tests/test_cli.py
@@ -770,6 +770,147 @@ 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
+ # as the minimum required stake may change, this method allows us to dynamically
+ # update the amount in the mock without updating the tests
+ config.amount = Balance.from_rao(_subtensor_mock.min_required_stake() - 1)
+ 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"