From fbef78ae8af002e4e0d991761411928090b7bc8c Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 09:39:19 -0700 Subject: [PATCH 01/17] add stake before check validator_permit --- tests/e2e_tests/test_root_set_weights.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/e2e_tests/test_root_set_weights.py b/tests/e2e_tests/test_root_set_weights.py index 5973a144cb..da55c05920 100644 --- a/tests/e2e_tests/test_root_set_weights.py +++ b/tests/e2e_tests/test_root_set_weights.py @@ -1,10 +1,13 @@ import asyncio + import pytest +from bittensor.utils.balance import Balance from tests.e2e_tests.utils.chain_interactions import ( wait_epoch, sudo_set_hyperparameter_values, ) +from tests.e2e_tests.utils.e2e_test_utils import wait_to_start_call FAST_BLOCKS_SPEEDUP_FACTOR = 5 @@ -58,9 +61,9 @@ async def test_root_reg_hyperparams( """ print("Testing root register, weights, and hyperparams") - netuid = 2 + netuid = subtensor.get_total_subnets() # 2 - # Default immunity period & tempo set through the subtensor side + # Default immunity period and tempo set through the subtensor side default_immunity_period = 5000 default_tempo = 10 if subtensor.is_fast_blocks() else 360 @@ -79,10 +82,22 @@ async def test_root_reg_hyperparams( # Create netuid = 2 assert subtensor.register_subnet(alice_wallet) - # Ensure correct immunity period & tempo is being fetched + assert wait_to_start_call(subtensor=subtensor, subnet_owner_wallet=alice_wallet, netuid=netuid) + + # Ensure correct immunity period and tempo is being fetched assert subtensor.immunity_period(netuid=netuid) == default_immunity_period assert subtensor.tempo(netuid=netuid) == default_tempo + assert subtensor.add_stake( + wallet=bob_wallet, + hotkey_ss58=alice_wallet.hotkey.ss58_address, + netuid=netuid, + amount=Balance.from_tao(1), + wait_for_inclusion=True, + wait_for_finalization=True, + period=16, + ), "Unable to stake from Bob to Alice" + async with templates.validator(alice_wallet, netuid): await asyncio.sleep(5) # Wait a bit for chain to process data From a473b73f513f0710529d824c85e582c95d28da96 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 09:42:05 -0700 Subject: [PATCH 02/17] ruff --- tests/e2e_tests/test_root_set_weights.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_root_set_weights.py b/tests/e2e_tests/test_root_set_weights.py index da55c05920..bec739148e 100644 --- a/tests/e2e_tests/test_root_set_weights.py +++ b/tests/e2e_tests/test_root_set_weights.py @@ -82,7 +82,9 @@ async def test_root_reg_hyperparams( # Create netuid = 2 assert subtensor.register_subnet(alice_wallet) - assert wait_to_start_call(subtensor=subtensor, subnet_owner_wallet=alice_wallet, netuid=netuid) + assert wait_to_start_call( + subtensor=subtensor, subnet_owner_wallet=alice_wallet, netuid=netuid + ) # Ensure correct immunity period and tempo is being fetched assert subtensor.immunity_period(netuid=netuid) == default_immunity_period From 9649cc58a5710ac144928c5c5b6f1f5c628d434d Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 14:09:32 -0700 Subject: [PATCH 03/17] improve `add_stake_extrinsic` (avoid Balance warnings) --- bittensor/core/extrinsics/asyncex/staking.py | 25 +++++++++-------- bittensor/core/extrinsics/staking.py | 29 ++++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/staking.py b/bittensor/core/extrinsics/asyncex/staking.py index 41076f0178..b257f8610c 100644 --- a/bittensor/core/extrinsics/asyncex/staking.py +++ b/bittensor/core/extrinsics/asyncex/staking.py @@ -28,25 +28,27 @@ async def add_stake_extrinsic( period: Optional[int] = None, ) -> bool: """ - Adds the specified amount of stake to passed hotkey `uid`. + Adds a stake from the specified wallet to the neuron identified by the SS58 address of its hotkey in specified subnet. + Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn incentives. Arguments: - subtensor: the initialized SubtensorInterface object to use + subtensor: Subtensor instance with the connection to the chain. wallet: Bittensor wallet object. old_balance: the balance prior to the staking - hotkey_ss58: The `ss58` address of the hotkey account to stake to defaults to the wallet's hotkey. - netuid: The netuid of the stake to be added - amount: Amount to stake as Bittensor balance, `None` if staking all. + hotkey_ss58: The `ss58` address of the hotkey account to stake to default to the wallet's hotkey. If not + specified, the wallet's hotkey will be used. Defaults to ``None``. + netuid: The unique identifier of the subnet to which the neuron belongs. + amount: Amount to stake as Bittensor balance in TAO always, `None` if staking all. Defaults is ``None``. wait_for_inclusion: 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. + `False` if the extrinsic fails to enter the block within the timeout. Defaults to ``True``. wait_for_finalization: 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. - safe_staking: If set, uses safe staking logic - allow_partial_stake: If set, allows partial stake - rate_tolerance: The rate tolerance for safe staking + or returns `False` if the extrinsic fails to be finalized within the timeout. Defaults to ``False``. + safe_staking: If True, enables price safety checks. Default is ``False``. + allow_partial_stake: If True, allows partial unstaking if price tolerance exceeded. Default is ``False``. + rate_tolerance: Maximum allowed price increase percentage (0.005 = 0.5%). Default is ``0.005``. period: The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. + You can think of it as an expiration date for the transaction. Defaults to ``None``. Returns: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for @@ -93,7 +95,6 @@ async def add_stake_extrinsic( ) else: staking_balance = amount - staking_balance.set_unit(netuid) # Leave existential balance to keep key alive. if staking_balance > old_balance - existential_deposit: diff --git a/bittensor/core/extrinsics/staking.py b/bittensor/core/extrinsics/staking.py index 58f5ed0b71..0e582fff5c 100644 --- a/bittensor/core/extrinsics/staking.py +++ b/bittensor/core/extrinsics/staking.py @@ -26,24 +26,26 @@ def add_stake_extrinsic( period: Optional[int] = None, ) -> bool: """ - Adds the specified amount of stake to passed hotkey `uid`. + Adds a stake from the specified wallet to the neuron identified by the SS58 address of its hotkey in specified subnet. + Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn incentives. Arguments: - subtensor: the Subtensor object to use + subtensor: Subtensor instance with the connection to the chain. wallet: Bittensor wallet object. - hotkey_ss58: The `ss58` address of the hotkey account to stake to default to the wallet's hotkey. - netuid (Optional[int]): Subnet unique ID. - amount: Amount to stake as Bittensor balance, `None` if staking all. + hotkey_ss58: The `ss58` address of the hotkey account to stake to default to the wallet's hotkey. If not + specified, the wallet's hotkey will be used. Defaults to ``None``. + netuid: The unique identifier of the subnet to which the neuron belongs. + amount: Amount to stake as Bittensor balance in TAO always, `None` if staking all. Defaults is ``None``. wait_for_inclusion: 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. + `False` if the extrinsic fails to enter the block within the timeout. Defaults to ``True``. wait_for_finalization: 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. - safe_staking (bool): If true, enables price safety checks - allow_partial_stake (bool): If true, allows partial unstaking if price tolerance exceeded - rate_tolerance (float): Maximum allowed price increase percentage (0.005 = 0.5%) + or returns `False` if the extrinsic fails to be finalized within the timeout. Defaults to ``False``. + safe_staking: If True, enables price safety checks. Default is ``False``. + allow_partial_stake: If True, allows partial unstaking if price tolerance exceeded. Default is ``False``. + rate_tolerance: Maximum allowed price increase percentage (0.005 = 0.5%). Default is ``0.005``. period: The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. - You can think of it as an expiration date for the transaction. + You can think of it as an expiration date for the transaction. Defaults to ``None``. Returns: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for @@ -87,14 +89,11 @@ def add_stake_extrinsic( ) else: staking_balance = amount - staking_balance.set_unit(netuid) # Leave existential balance to keep key alive. if staking_balance > old_balance - existential_deposit: # If we are staking all, we need to leave at least the existential deposit. staking_balance = old_balance - existential_deposit - else: - staking_balance = staking_balance # Check enough to stake. if staking_balance > old_balance: @@ -200,7 +199,7 @@ def add_stake_extrinsic( except SubstrateRequestException as error: logging.error( - f":cross_mark: [red]Add Stake Error: {format_error_message((error))}[/red]" + f":cross_mark: [red]Add Stake Error: {format_error_message(error)}[/red]" ) return False From 688e2d76739423aeab70f7454b2f822491c900be Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 14:10:21 -0700 Subject: [PATCH 04/17] improve `unstake_extrinsic`, `unstake_multiple_extrinsic` (add `unstake_all` argument) --- bittensor/core/extrinsics/asyncex/unstaking.py | 10 ++++++++++ bittensor/core/extrinsics/unstaking.py | 17 +++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/bittensor/core/extrinsics/asyncex/unstaking.py b/bittensor/core/extrinsics/asyncex/unstaking.py index 8125b603e9..0d0bf6670e 100644 --- a/bittensor/core/extrinsics/asyncex/unstaking.py +++ b/bittensor/core/extrinsics/asyncex/unstaking.py @@ -25,6 +25,7 @@ async def unstake_extrinsic( allow_partial_stake: bool = False, rate_tolerance: float = 0.005, period: Optional[int] = None, + unstake_all: bool = False, ) -> bool: """Removes stake into the wallet coldkey from the specified hotkey ``uid``. @@ -45,11 +46,15 @@ async def unstake_extrinsic( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + unstake_all: If true, unstakes all tokens. Default is ``False``. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is ``True``. """ + if amount and unstake_all: + raise ValueError("Cannot specify both `amount` and `unstake_all`.") + # Decrypt keys, if not (unlock := unlock_key(wallet)).success: logging.error(unlock.message) @@ -204,6 +209,7 @@ async def unstake_multiple_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + unstake_all: bool = False, ) -> bool: """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. @@ -220,11 +226,15 @@ async def unstake_multiple_extrinsic( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + unstake_all: If true, unstakes all tokens. Default is ``False``. 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 amounts and unstake_all: + raise ValueError("Cannot specify both `amounts` and `unstake_all`.") + if not isinstance(hotkey_ss58s, list) or not all( isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s ): diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index 34fe47d7ac..772cced28c 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -24,6 +24,7 @@ def unstake_extrinsic( allow_partial_stake: bool = False, rate_tolerance: float = 0.005, period: Optional[int] = None, + unstake_all: bool = False, ) -> bool: """Removes stake into the wallet coldkey from the specified hotkey ``uid``. @@ -44,11 +45,15 @@ def unstake_extrinsic( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + unstake_all: If true, unstakes all tokens. Default is ``False``. Returns: success (bool): Flag is ``True`` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is ``True``. """ + if amount and unstake_all: + raise ValueError("Cannot specify both `amount` and `unstake_all`.") + # Decrypt keys, if not (unlock := unlock_key(wallet)).success: logging.error(unlock.message) @@ -102,16 +107,12 @@ def unstake_extrinsic( base_price = pool.price.rao price_with_tolerance = base_price * (1 - rate_tolerance) - # For logging - base_rate = pool.price.tao - rate_with_tolerance = base_rate * (1 - rate_tolerance) - logging.info( f":satellite: [magenta]Safe Unstaking from:[/magenta] " f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green], " f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " - f"price limit: [green]{rate_with_tolerance}[/green], " - f"original price: [green]{base_rate}[/green], " + f"price limit: [green]{price_with_tolerance}[/green], " + f"original price: [green]{base_price}[/green], " f"with partial unstake: [green]{allow_partial_stake}[/green] " f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" ) @@ -201,6 +202,7 @@ def unstake_multiple_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + unstake_all: bool = False, ) -> bool: """Removes stake from each ``hotkey_ss58`` in the list, using each amount, to a common coldkey. @@ -217,11 +219,14 @@ def unstake_multiple_extrinsic( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + unstake_all: If true, unstakes all tokens. Default is ``False``. 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 amounts and unstake_all: + raise ValueError("Cannot specify both `amounts` and `unstake_all`.") if not isinstance(hotkey_ss58s, list) or not all( isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s From 33a32e7737726e31fc8d46f52f247eca2c6330ae Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 14:10:38 -0700 Subject: [PATCH 05/17] update calls in subtensors --- bittensor/core/async_subtensor.py | 44 ++++++++++--------- bittensor/core/subtensor.py | 71 +++++++++++++++++-------------- 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index 0d99e887c3..b18f057bc5 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3380,27 +3380,27 @@ async def add_stake( period: Optional[int] = None, ) -> bool: """ - Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. - Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn - incentives. + Adds a stake from the specified wallet to the neuron identified by the SS58 address of its hotkey in specified subnet. + Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn incentives. Args: - wallet (bittensor_wallet.Wallet): The wallet to be used for staking. - hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey associated with the neuron. - netuid: subnet UID - amount (Balance): The amount of TAO to stake. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The stake - will only execute if the price change doesn't exceed the rate tolerance. Default is False. - allow_partial_stake (bool): If true and safe_staking is enabled, allows partial staking when - the full amount would exceed the price threshold. If false, the entire stake fails if it would - exceed the threshold. Default is False. - rate_tolerance (float): The maximum allowed price change ratio when staking. For example, - 0.005 = 0.5% maximum price increase. Only used when safe_staking is True. Default is 0.005. - period (Optional[int]): The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. + wallet: The wallet to be used for staking. + hotkey_ss58: The SS58 address of the hotkey associated with the neuron to which you intend to delegate your + stake. If not specified, the wallet's hotkey will be used. Defaults to ``None``. + netuid: The unique identifier of the subnet to which the neuron belongs. + amount: The amount of TAO to stake. + wait_for_inclusion: Waits for the transaction to be included in a block. Defaults to ``True``. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Defaults to ``False``. + safe_staking: If true, enables price safety checks to protect against fluctuating prices. The stake will + only execute if the price change doesn't exceed the rate tolerance. Default is ``False``. + allow_partial_stake: If true and safe_staking is enabled, allows partial staking when the full amount would + exceed the price tolerance. If false, the entire stake fails if it would exceed the tolerance. + Default is ``False``. + rate_tolerance: The maximum allowed price change ratio when staking. For example, + 0.005 = 0.5% maximum price increase. Only used when safe_staking is True. Default is ``0.005``. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. Defaults to ``None``. Returns: bool: ``True`` if the staking is successful, False otherwise. @@ -4428,6 +4428,7 @@ async def unstake( allow_partial_stake: bool = False, rate_tolerance: float = 0.005, period: Optional[int] = None, + unstake_all: bool = False, ) -> bool: """ Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting @@ -4451,6 +4452,7 @@ async def unstake( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + unstake_all: If true, unstakes all tokens. Default is ``False``. Returns: bool: ``True`` if the unstaking process is successful, False otherwise. @@ -4471,6 +4473,7 @@ async def unstake( allow_partial_stake=allow_partial_stake, rate_tolerance=rate_tolerance, period=period, + unstake_all=unstake_all, ) async def unstake_multiple( @@ -4482,6 +4485,7 @@ async def unstake_multiple( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + unstake_all: bool = False, ) -> bool: """ Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts @@ -4499,6 +4503,7 @@ async def unstake_multiple( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + unstake_all: If true, unstakes all tokens. Default is ``False``. Returns: bool: ``True`` if the batch unstaking is successful, False otherwise. @@ -4515,6 +4520,7 @@ async def unstake_multiple( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, + unstake_all=unstake_all, ) diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 5e356f9666..6e66f0b130 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -610,14 +610,14 @@ def get_all_subnets_info(self, block: Optional[int] = None) -> list["SubnetInfo" def get_balance(self, address: str, block: Optional[int] = None) -> Balance: """ - Retrieves the balance for given coldkey. + Retrieves the balance for given coldkey. Always in TAO. Arguments: - address (str): coldkey address. + address: coldkey address. block (Optional[int]): The blockchain block number for the query. Returns: - Balance object. + Balance object in TAO. """ balance = self.substrate.query( module="System", @@ -1122,7 +1122,7 @@ def get_delegates(self, block: Optional[int] = None) -> list["DelegateInfo"]: def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balance]: """ - Retrieves the existential deposit amount for the Bittensor blockchain. + Retrieves the existential deposit amount for the Bittensor blockchain. Always in TAO. The existential deposit is the minimum amount of TAO required for an account to exist on the blockchain. Accounts with balances below this threshold can be reaped to conserve network resources. @@ -1130,7 +1130,7 @@ def get_existential_deposit(self, block: Optional[int] = None) -> Optional[Balan block (Optional[int]): The blockchain block number for the query. Returns: - The existential deposit amount. + The existential deposit amount. Always in TAO. The existential deposit is a fundamental economic parameter in the Bittensor network, ensuring efficient use of storage and preventing the proliferation of dust accounts. @@ -1424,16 +1424,19 @@ def get_stake( block: Optional[int] = None, ) -> Balance: """ - Returns the stake under a coldkey - hotkey pairing. + Returns the amount of Alpha staked by a specific coldkey to a specific hotkey within a given subnet. + This function retrieves the delegated stake balance, referred to as the 'Alpha' value. Args: - hotkey_ss58 (str): The SS58 address of the hotkey. - coldkey_ss58 (str): The SS58 address of the coldkey. - netuid (int): The subnet ID - block (Optional[int]): The block number at which to query the stake information. + coldkey_ss58: The SS58 address of the coldkey that delegated the stake. This address owns the stake. + hotkey_ss58: The ss58 address of the hotkey which the stake is on. + netuid: The unique identifier of the subnet to query. + block: The specific block number at which to retrieve the stake information. If None, the current stake at + the latest block is returned. Defaults to ``None``. Returns: - Balance: The stake under the coldkey - hotkey pairing. + An object representing the amount of Alpha (TAO ONLY if the subnet's netuid is 0) currently staked from the + specified coldkey to the specified hotkey within the given subnet. """ alpha_shares_query = self.query_module( module="SubtensorModule", @@ -2632,27 +2635,27 @@ def add_stake( period: Optional[int] = None, ) -> bool: """ - Adds the specified amount of stake to a neuron identified by the hotkey ``SS58`` address. - Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn - incentives. + Adds a stake from the specified wallet to the neuron identified by the SS58 address of its hotkey in specified subnet. + Staking is a fundamental process in the Bittensor network that enables neurons to participate actively and earn incentives. Args: - wallet (bittensor_wallet.Wallet): The wallet to be used for staking. - hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey associated with the neuron. - netuid (Optional[int]): The unique identifier of the subnet to which the neuron belongs. - amount (Balance): The amount of TAO to stake. - wait_for_inclusion (bool): Waits for the transaction to be included in a block. - wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. - safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The stake - will only execute if the price change doesn't exceed the rate tolerance. Default is False. - allow_partial_stake (bool): If true and safe_staking is enabled, allows partial staking when - the full amount would exceed the price tolerance. If false, the entire stake fails if it would - exceed the tolerance. Default is False. - rate_tolerance (float): The maximum allowed price change ratio when staking. For example, - 0.005 = 0.5% maximum price increase. Only used when safe_staking is True. Default is 0.005. - period (Optional[int]): The number of blocks during which the transaction will remain valid after it's - submitted. If the transaction is not included in a block within that number of blocks, it will expire - and be rejected. You can think of it as an expiration date for the transaction. + wallet: The wallet to be used for staking. + hotkey_ss58: The SS58 address of the hotkey associated with the neuron to which you intend to delegate your + stake. If not specified, the wallet's hotkey will be used. Defaults to ``None``. + netuid: The unique identifier of the subnet to which the neuron belongs. + amount: The amount of TAO to stake. + wait_for_inclusion: Waits for the transaction to be included in a block. Defaults to ``True``. + wait_for_finalization: Waits for the transaction to be finalized on the blockchain. Defaults to ``False``. + safe_staking: If true, enables price safety checks to protect against fluctuating prices. The stake will + only execute if the price change doesn't exceed the rate tolerance. Default is ``False``. + allow_partial_stake: If true and safe_staking is enabled, allows partial staking when the full amount would + exceed the price tolerance. If false, the entire stake fails if it would exceed the tolerance. + Default is ``False``. + rate_tolerance: The maximum allowed price change ratio when staking. For example, + 0.005 = 0.5% maximum price increase. Only used when safe_staking is True. Default is ``0.005``. + period: The number of blocks during which the transaction will remain valid after it's submitted. If the + transaction is not included in a block within that number of blocks, it will expire and be rejected. You + can think of it as an expiration date for the transaction. Defaults to ``None``. Returns: bool: True if the staking is successful, False otherwise. @@ -3668,6 +3671,7 @@ def unstake( allow_partial_stake: bool = False, rate_tolerance: float = 0.005, period: Optional[int] = None, + unstake_all: bool = False, ) -> bool: """ Removes a specified amount of stake from a single hotkey account. This function is critical for adjusting @@ -3678,7 +3682,7 @@ def unstake( removed. hotkey_ss58 (Optional[str]): The ``SS58`` address of the hotkey account to unstake from. netuid (Optional[int]): The unique identifier of the subnet. - amount (Balance): The amount of alpha to unstake. If not specified, unstakes all. + amount (Balance): The amount of alpha to unstake. If not specified, unstakes all. Alpha amount. wait_for_inclusion (bool): Waits for the transaction to be included in a block. wait_for_finalization (bool): Waits for the transaction to be finalized on the blockchain. safe_staking (bool): If true, enables price safety checks to protect against fluctuating prices. The unstake @@ -3691,6 +3695,7 @@ def unstake( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + unstake_all: If true, unstakes all tokens. Default is ``False``. Returns: bool: ``True`` if the unstaking process is successful, False otherwise. @@ -3712,6 +3717,7 @@ def unstake( allow_partial_stake=allow_partial_stake, rate_tolerance=rate_tolerance, period=period, + unstake_all=unstake_all, ) def unstake_multiple( @@ -3723,6 +3729,7 @@ def unstake_multiple( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, period: Optional[int] = None, + unstake_all: bool = False, ) -> bool: """ Performs batch unstaking from multiple hotkey accounts, allowing a neuron to reduce its staked amounts @@ -3740,6 +3747,7 @@ def unstake_multiple( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. + unstake_all: If true, unstakes all tokens. Default is ``False``. Returns: bool: ``True`` if the batch unstaking is successful, False otherwise. @@ -3756,4 +3764,5 @@ def unstake_multiple( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, + unstake_all=unstake_all, ) From 5feede0654a828e1c3bebd389bfd1fb9bdaf0748 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 14:10:54 -0700 Subject: [PATCH 06/17] fix unit tests --- tests/unit_tests/test_subtensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index b1020d7648..0db7c0cafe 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -2945,6 +2945,7 @@ def test_unstake_success(mocker, subtensor, fake_wallet): allow_partial_stake=False, rate_tolerance=0.005, period=None, + unstake_all=False, ) assert result == mock_unstake_extrinsic.return_value @@ -2982,6 +2983,7 @@ def test_unstake_with_safe_staking(mocker, subtensor, fake_wallet): allow_partial_stake=True, rate_tolerance=fake_rate_tolerance, period=None, + unstake_all=False, ) assert result == mock_unstake_extrinsic.return_value @@ -3105,6 +3107,7 @@ def test_unstake_multiple_success(mocker, subtensor, fake_wallet): wait_for_inclusion=True, wait_for_finalization=False, period=None, + unstake_all=False, ) assert result == mock_unstake_multiple_extrinsic.return_value From 717798588c6b8525489fdd9f3cff34139b1d80dc Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 14:14:08 -0700 Subject: [PATCH 07/17] improve docstrings, fix private function usage --- bittensor/core/async_subtensor.py | 7 +++++-- bittensor/core/subtensor.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/bittensor/core/async_subtensor.py b/bittensor/core/async_subtensor.py index b18f057bc5..aa5fa9c5f1 100644 --- a/bittensor/core/async_subtensor.py +++ b/bittensor/core/async_subtensor.py @@ -3119,6 +3119,9 @@ async def wait_for_block(self, block: Optional[int] = None): bool: True if the target block was reached, False if timeout occurred. Example: + import bittensor as bt + subtensor = bt.Subtensor() + await subtensor.wait_for_block() # Waits for next block await subtensor.wait_for_block(block=1234) # Waits for a specific block """ @@ -4452,7 +4455,7 @@ async def unstake( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. - unstake_all: If true, unstakes all tokens. Default is ``False``. + unstake_all: If true, unstakes all tokens. Default is ``False``. If `True` amount is ignored. Returns: bool: ``True`` if the unstaking process is successful, False otherwise. @@ -4503,7 +4506,7 @@ async def unstake_multiple( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. - unstake_all: If true, unstakes all tokens. Default is ``False``. + unstake_all: If true, unstakes all tokens. Default is ``False``. If `True` amounts are ignored. Returns: bool: ``True`` if the batch unstaking is successful, False otherwise. diff --git a/bittensor/core/subtensor.py b/bittensor/core/subtensor.py index 6e66f0b130..015b918cd7 100644 --- a/bittensor/core/subtensor.py +++ b/bittensor/core/subtensor.py @@ -2412,8 +2412,11 @@ def wait_for_block(self, block: Optional[int] = None): bool: True if the target block was reached, False if timeout occurred. Example: - >>> subtensor.wait_for_block() # Waits for the next block - >>> subtensor.wait_for_block(block=1234) # Waits for a specific block + import bittensor as bt + subtensor = bt.Subtensor() + + subtensor.wait_for_block() # Waits for the next block + subtensor.wait_for_block(block=1234) # Waits for a specific block """ def handler(block_data: dict): @@ -2431,7 +2434,7 @@ def handler(block_data: dict): else: target_block = current_block["header"]["number"] + 1 - self.substrate._get_block_handler( + self.substrate.get_block_handler( current_block_hash, header_only=True, subscription_handler=handler ) return True @@ -3695,7 +3698,7 @@ def unstake( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. - unstake_all: If true, unstakes all tokens. Default is ``False``. + unstake_all: If true, unstakes all tokens. Default is ``False``. If `True` amount is ignored. Returns: bool: ``True`` if the unstaking process is successful, False otherwise. @@ -3747,7 +3750,7 @@ def unstake_multiple( period (Optional[int]): The number of blocks during which the transaction will remain valid after it's submitted. If the transaction is not included in a block within that number of blocks, it will expire and be rejected. You can think of it as an expiration date for the transaction. - unstake_all: If true, unstakes all tokens. Default is ``False``. + unstake_all: If true, unstakes all tokens. Default is ``False``. If `True` amounts are ignored. Returns: bool: ``True`` if the batch unstaking is successful, False otherwise. From ff6fe2e0228af8d68eff4f99b06e8ccff33df69e Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 14:42:24 -0700 Subject: [PATCH 08/17] fix one more unit test --- tests/unit_tests/test_subtensor_extended.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/test_subtensor_extended.py b/tests/unit_tests/test_subtensor_extended.py index 45f65f24db..78851574cf 100644 --- a/tests/unit_tests/test_subtensor_extended.py +++ b/tests/unit_tests/test_subtensor_extended.py @@ -1564,7 +1564,7 @@ def get_block_handler( }, }, ] - mock_substrate._get_block_handler.side_effect = get_block_handler + mock_substrate.get_block_handler.side_effect = get_block_handler subtensor.wait_for_block(block=9) From 343cf4c6c50db2f07a938d7b3492b15164cde2ec Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 16:25:01 -0700 Subject: [PATCH 09/17] add defaults from local env --- bittensor/core/settings.py | 4 ++-- bittensor/core/types.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor/core/settings.py b/bittensor/core/settings.py index 39798ed295..bf0d66bebf 100644 --- a/bittensor/core/settings.py +++ b/bittensor/core/settings.py @@ -121,8 +121,8 @@ "maxsize": int(_BT_PRIORITY_MAXSIZE) if _BT_PRIORITY_MAXSIZE else 10, }, "subtensor": { - "chain_endpoint": DEFAULT_ENDPOINT, - "network": DEFAULT_NETWORK, + "chain_endpoint": os.getenv("BT_CHAIN_ENDPOINT") or DEFAULT_ENDPOINT, + "network": os.getenv("BT_NETWORK") or DEFAULT_NETWORK, "_mock": False, }, "wallet": { diff --git a/bittensor/core/types.py b/bittensor/core/types.py index db5ab0b24e..d0c8b79cc9 100644 --- a/bittensor/core/types.py +++ b/bittensor/core/types.py @@ -124,8 +124,8 @@ def add_args(cls, parser: "argparse.ArgumentParser", prefix: Optional[str] = Non """ prefix_str = "" if prefix is None else f"{prefix}." try: - default_network = settings.DEFAULT_NETWORK - default_chain_endpoint = settings.FINNEY_ENTRYPOINT + default_network = settings.DEFAULTS.subtensor.network + default_chain_endpoint = settings.DEFAULTS.subtensor.chain_endpoint parser.add_argument( f"--{prefix_str}subtensor.network", From b28d153e70caf8b5e107636a5afdabab73a8d9e3 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 16:29:15 -0700 Subject: [PATCH 10/17] replace network --- tests/unit_tests/test_subtensor_api.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/test_subtensor_api.py b/tests/unit_tests/test_subtensor_api.py index 35d6c6a15c..5007370d96 100644 --- a/tests/unit_tests/test_subtensor_api.py +++ b/tests/unit_tests/test_subtensor_api.py @@ -6,8 +6,12 @@ def test_properties_methods_comparable(other_class: "Subtensor" = None): """Verifies that methods in SubtensorApi and its properties contains all Subtensors methods.""" # Preps - subtensor = other_class(_mock=True) if other_class else Subtensor(_mock=True) - subtensor_api = SubtensorApi(mock=True) + subtensor = ( + other_class(network="latent-lite", _mock=True) + if other_class + else Subtensor(network="latent-lite", _mock=True) + ) + subtensor_api = SubtensorApi(network="latent-lite", mock=True) subtensor_methods = [m for m in dir(subtensor) if not m.startswith("_")] @@ -62,8 +66,12 @@ def test__methods_comparable_with_passed_legacy_methods( ): """Verifies that methods in SubtensorApi contains all Subtensors methods if `legacy_methods=True` is passed.""" # Preps - subtensor = other_class(mock=True) if other_class else Subtensor(_mock=True) - subtensor_api = SubtensorApi(mock=True, legacy_methods=True) + subtensor = ( + other_class(network="latent-lite", mock=True) + if other_class + else Subtensor(network="latent-lite", _mock=True) + ) + subtensor_api = SubtensorApi(network="latent-lite", mock=True, legacy_methods=True) subtensor_methods = [m for m in dir(subtensor) if not m.startswith("_")] subtensor_api_methods = [m for m in dir(subtensor_api) if not m.startswith("_")] From f5a659f824739c3d759678e16361420176994d4b Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 18:10:15 -0700 Subject: [PATCH 11/17] improve error message --- bittensor/utils/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bittensor/utils/__init__.py b/bittensor/utils/__init__.py index 5efe1cbb25..14fb383847 100644 --- a/bittensor/utils/__init__.py +++ b/bittensor/utils/__init__.py @@ -22,6 +22,8 @@ if TYPE_CHECKING: from bittensor_wallet import Wallet +BT_DOCS_LINK = "https://docs.bittensor.com" + # redundant aliases logging = logging @@ -219,6 +221,7 @@ def format_error_message(error_message: Union[dict, Exception]) -> str: pass if new_error_message is None: return_val = " ".join(error_message.args) + return f"Subtensor returned: {return_val}" else: error_message = new_error_message @@ -236,7 +239,9 @@ def format_error_message(error_message: Union[dict, Exception]) -> str: # subtensor custom error marker if err_data.startswith("Custom error:"): - err_description = f"{err_data} | Please consult https://docs.bittensor.com/subtensor-nodes/subtensor-error-messages" + err_description = ( + f"{err_data} | Please consult {BT_DOCS_LINK}/errors/custom" + ) else: err_description = err_data @@ -249,6 +254,9 @@ def format_error_message(error_message: Union[dict, Exception]) -> str: err_name = error_message.get("name", err_name) err_docs = error_message.get("docs", [err_description]) err_description = " ".join(err_docs) + err_description += ( + f" | Please consult {BT_DOCS_LINK}/errors/subtensor#{err_name.lower()}" + ) elif error_message.get("code") and error_message.get("message"): err_type = error_message.get("code", err_name) From b997985bcf88716f77b779d9ec3d34856d16dbc2 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 18:10:27 -0700 Subject: [PATCH 12/17] fix tests --- tests/unit_tests/extrinsics/test__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/extrinsics/test__init__.py b/tests/unit_tests/extrinsics/test__init__.py index 9eb994a2a3..7772201364 100644 --- a/tests/unit_tests/extrinsics/test__init__.py +++ b/tests/unit_tests/extrinsics/test__init__.py @@ -22,8 +22,9 @@ def test_format_error_message_with_right_error_message(): # Assertions assert ( - result - == "Subtensor returned `SomeErrorName(SomeType)` error. This means: `Some error description. I'm second part. Hah, I'm the last one.`." + result == "Subtensor returned `SomeErrorName(SomeType)` error. " + "This means: `Some error description. I'm second part. Hah, I'm the last one." + " | Please consult https://docs.bittensor.com/errors/subtensor#someerrorname`." ) @@ -79,8 +80,7 @@ def test_format_error_message_with_custom_error_message_with_index(): assert ( result == f"Subtensor returned `SubstrateRequestException({fake_subtensor_error['name']})` error. This means: " - f"`{fake_custom_error['data']} | Please consult " - f"https://docs.bittensor.com/subtensor-nodes/subtensor-error-messages`." + f"`{fake_custom_error['data']} | Please consult https://docs.bittensor.com/errors/custom`." ) From 10872b55304f1b41cb52ce097530f4324e8a301a Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 18:15:15 -0700 Subject: [PATCH 13/17] dynamic link in test --- tests/unit_tests/extrinsics/test__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/extrinsics/test__init__.py b/tests/unit_tests/extrinsics/test__init__.py index 7772201364..ba17c001be 100644 --- a/tests/unit_tests/extrinsics/test__init__.py +++ b/tests/unit_tests/extrinsics/test__init__.py @@ -1,6 +1,6 @@ """Tests for bittensor/extrinsics/__ini__ module.""" -from bittensor.utils import format_error_message +from bittensor.utils import format_error_message, BT_DOCS_LINK def test_format_error_message_with_right_error_message(): @@ -24,7 +24,7 @@ def test_format_error_message_with_right_error_message(): assert ( result == "Subtensor returned `SomeErrorName(SomeType)` error. " "This means: `Some error description. I'm second part. Hah, I'm the last one." - " | Please consult https://docs.bittensor.com/errors/subtensor#someerrorname`." + f" | Please consult {BT_DOCS_LINK}/errors/subtensor#someerrorname`." ) @@ -80,7 +80,7 @@ def test_format_error_message_with_custom_error_message_with_index(): assert ( result == f"Subtensor returned `SubstrateRequestException({fake_subtensor_error['name']})` error. This means: " - f"`{fake_custom_error['data']} | Please consult https://docs.bittensor.com/errors/custom`." + f"`{fake_custom_error['data']} | Please consult {BT_DOCS_LINK}/errors/custom`." ) From d4c8a16c0d99851e14e920191b4aac829503809a Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 20 May 2025 18:33:22 -0700 Subject: [PATCH 14/17] oops --- bittensor/core/extrinsics/unstaking.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bittensor/core/extrinsics/unstaking.py b/bittensor/core/extrinsics/unstaking.py index 772cced28c..9e252cdaaf 100644 --- a/bittensor/core/extrinsics/unstaking.py +++ b/bittensor/core/extrinsics/unstaking.py @@ -107,12 +107,16 @@ def unstake_extrinsic( base_price = pool.price.rao price_with_tolerance = base_price * (1 - rate_tolerance) + # For logging + base_rate = pool.price.tao + rate_with_tolerance = base_rate * (1 - rate_tolerance) + logging.info( f":satellite: [magenta]Safe Unstaking from:[/magenta] " f"netuid: [green]{netuid}[/green], amount: [green]{unstaking_balance}[/green], " f"tolerance percentage: [green]{rate_tolerance * 100}%[/green], " - f"price limit: [green]{price_with_tolerance}[/green], " - f"original price: [green]{base_price}[/green], " + f"price limit: [green]{rate_with_tolerance}[/green], " + f"original price: [green]{base_rate}[/green], " f"with partial unstake: [green]{allow_partial_stake}[/green] " f"on [blue]{subtensor.network}[/blue][magenta]...[/magenta]" ) From 5a799c1e61d2fde7b2b9f92f22641aeaf9db4f80 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 May 2025 12:28:41 -0700 Subject: [PATCH 15/17] sign_with="hotkey" to `publish_metadata` --- bittensor/core/extrinsics/asyncex/serving.py | 1 + bittensor/core/extrinsics/serving.py | 1 + 2 files changed, 2 insertions(+) diff --git a/bittensor/core/extrinsics/asyncex/serving.py b/bittensor/core/extrinsics/asyncex/serving.py index b942a88dc1..6fd5410838 100644 --- a/bittensor/core/extrinsics/asyncex/serving.py +++ b/bittensor/core/extrinsics/asyncex/serving.py @@ -282,6 +282,7 @@ async def publish_metadata( success, message = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + sign_with="hotkey", wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, diff --git a/bittensor/core/extrinsics/serving.py b/bittensor/core/extrinsics/serving.py index b49196bf89..88da8997bc 100644 --- a/bittensor/core/extrinsics/serving.py +++ b/bittensor/core/extrinsics/serving.py @@ -278,6 +278,7 @@ def publish_metadata( success, message = subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + sign_with="hotkey", wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, period=period, From f82317f2071f2e4b79a7a8699b4bcabfab29efaf Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 21 May 2025 12:39:50 -0700 Subject: [PATCH 16/17] improve unit test --- tests/unit_tests/extrinsics/test_serving.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/extrinsics/test_serving.py b/tests/unit_tests/extrinsics/test_serving.py index c1688fd3d4..7095ea0bc1 100644 --- a/tests/unit_tests/extrinsics/test_serving.py +++ b/tests/unit_tests/extrinsics/test_serving.py @@ -355,7 +355,7 @@ def test_publish_metadata( patch.object(mock_subtensor.substrate, "compose_call"), patch.object( mock_subtensor, "sign_and_send_extrinsic", return_value=response_success - ), + ) as mocked_sign_and_send_extrinsic, ): # Act result = serving.publish_metadata( @@ -369,3 +369,11 @@ def test_publish_metadata( ) # Assert assert result == expected_result, f"Test ID: {test_id}" + mocked_sign_and_send_extrinsic.assert_called_once_with( + call=mock_subtensor.substrate.compose_call.return_value, + wallet=mock_wallet, + sign_with="hotkey", + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=None, + ) From 75aaa92318544dd25aecf3a0dbb3bf4438c8de0e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 22 May 2025 12:49:47 -0700 Subject: [PATCH 17/17] bumps version and changelog --- CHANGELOG.md | 14 +++++++++++++- pyproject.toml | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e7ccd6c02..5190ea2525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ # Changelog -## 9.5.0 /2025-05-15 +## 9.6.1 /2025-05-22 + +## What's Changed +* Release/9.6.0 by @ibraheem-abe in https://github.com/opentensor/bittensor/pull/2882 +* Add stake before check `validator_permit` by @basfroman in https://github.com/opentensor/bittensor/pull/2884 +* Add defaults for endpoint and network from local env by @basfroman in https://github.com/opentensor/bittensor/pull/2886 +* Improve error message by @basfroman in https://github.com/opentensor/bittensor/pull/2888 +* Make `unstake` and `unstake_multiple` for all Alphas more clear by @basfroman in https://github.com/opentensor/bittensor/pull/2885 +* fix publish metadata by @basfroman in https://github.com/opentensor/bittensor/pull/2890 + +**Full Changelog**: https://github.com/opentensor/bittensor/compare/v9.6.0...v9.6.1 + +## 9.6.0 /2025-05-15 * Add manual way to show the size of virtual environments in the PR by @basfroman in https://github.com/opentensor/bittensor/pull/2875 * Improve `Monitor SDK Requirements Size` workflow by @basfroman in https://github.com/opentensor/bittensor/pull/2878 diff --git a/pyproject.toml b/pyproject.toml index 4b3d8fbe99..244fcd3a53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor" -version = "9.6.0" +version = "9.6.1" description = "Bittensor" readme = "README.md" authors = [