From 655efc36789be931af2adc865d7b1e89d9af30c1 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 14 Mar 2025 12:33:45 -0700 Subject: [PATCH 01/35] init --- .../src/bittensor/subtensor_interface.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index acb5fa870..18ea8e452 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1435,3 +1435,62 @@ async def get_owned_hotkeys( ) return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + + +async def get_stake_fee( + self, + origin_hotkey_ss58: Optional[str], + origin_netuid: Optional[int], + origin_coldkey_ss58: str, + destination_hotkey_ss58: Optional[str], + destination_netuid: Optional[int], + destination_coldkey_ss58: str, + amount: int, + block_hash: Optional[str] = None, +) -> Balance: + """ + Calculates the fee for a staking operation. + + :param origin_hotkey_ss58: SS58 address of source hotkey (None for new stake) + :param origin_netuid: Netuid of source subnet (None for new stake) + :param origin_coldkey_ss58: SS58 address of source coldkey + :param destination_hotkey_ss58: SS58 address of destination hotkey (None for removing stake) + :param destination_netuid: Netuid of destination subnet (None for removing stake) + :param destination_coldkey_ss58: SS58 address of destination coldkey + :param amount: Amount of stake to transfer in RAO + :param block_hash: Optional block hash at which to perform the calculation + + :return: The calculated stake fee as a Balance object + + Example scenarios: + - Adding stake: origin_hotkey=None, origin_netuid=None + - Removing stake: destination_hotkey=None, destination_netuid=None + - Moving stake between subnets: Both origin and destination parameters provided + """ + + origin = ( + (origin_hotkey_ss58, origin_netuid) + if origin_hotkey_ss58 is not None and origin_netuid is not None + else None + ) + + destination = ( + (destination_hotkey_ss58, destination_netuid) + if destination_hotkey_ss58 is not None and destination_netuid is not None + else None + ) + + result = await self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_fee", + params=[ + origin, + origin_coldkey_ss58, + destination, + destination_coldkey_ss58, + amount, + ], + block_hash=block_hash, + ) + + return Balance.from_rao(result if result is not None else 0) From 67994978dc07d95b8167416f74a1aa689dbe6314 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 14 Mar 2025 12:33:59 -0700 Subject: [PATCH 02/35] add api examples --- .../src/bittensor/subtensor_interface.py | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 18ea8e452..9c0bb6cd3 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1462,18 +1462,30 @@ async def get_stake_fee( :return: The calculated stake fee as a Balance object - Example scenarios: - - Adding stake: origin_hotkey=None, origin_netuid=None - - Removing stake: destination_hotkey=None, destination_netuid=None - - Moving stake between subnets: Both origin and destination parameters provided + When to use None: + + 1. Adding new stake (default fee): + - origin_hotkey_ss58 = None + - origin_netuid = None + - All other fields required + + 2. Removing stake (default fee): + - destination_hotkey_ss58 = None + - destination_netuid = None + - All other fields required + + For all other operations, no None values - provide all parameters: + 3. Moving between subnets + 4. Moving between hotkeys + 5. Moving between coldkeys """ origin = ( - (origin_hotkey_ss58, origin_netuid) - if origin_hotkey_ss58 is not None and origin_netuid is not None + (origin_hotkey_ss58, origin_netuid) + if origin_hotkey_ss58 is not None and origin_netuid is not None else None ) - + destination = ( (destination_hotkey_ss58, destination_netuid) if destination_hotkey_ss58 is not None and destination_netuid is not None @@ -1492,5 +1504,5 @@ async def get_stake_fee( ], block_hash=block_hash, ) - + return Balance.from_rao(result if result is not None else 0) From 17cbe783d15ad076283c9bf0ec72b6368af28690 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 14 Mar 2025 16:02:54 -0700 Subject: [PATCH 03/35] get_stake_fee added --- .../src/bittensor/subtensor_interface.py | 133 +++++++++--------- 1 file changed, 65 insertions(+), 68 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 9c0bb6cd3..c82b71d54 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -36,6 +36,8 @@ decode_hex_identity_dict, validate_chain_endpoint, u16_normalized_float, + encode_account_id, + decode_account_id, ) SubstrateClass = ( @@ -1436,73 +1438,68 @@ async def get_owned_hotkeys( return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + async def get_stake_fee( + self, + origin_hotkey_ss58: Optional[str], + origin_netuid: Optional[int], + origin_coldkey_ss58: str, + destination_hotkey_ss58: Optional[str], + destination_netuid: Optional[int], + destination_coldkey_ss58: str, + amount: int, + block_hash: Optional[str] = None, + ) -> Balance: + """ + Calculates the fee for a staking operation. -async def get_stake_fee( - self, - origin_hotkey_ss58: Optional[str], - origin_netuid: Optional[int], - origin_coldkey_ss58: str, - destination_hotkey_ss58: Optional[str], - destination_netuid: Optional[int], - destination_coldkey_ss58: str, - amount: int, - block_hash: Optional[str] = None, -) -> Balance: - """ - Calculates the fee for a staking operation. - - :param origin_hotkey_ss58: SS58 address of source hotkey (None for new stake) - :param origin_netuid: Netuid of source subnet (None for new stake) - :param origin_coldkey_ss58: SS58 address of source coldkey - :param destination_hotkey_ss58: SS58 address of destination hotkey (None for removing stake) - :param destination_netuid: Netuid of destination subnet (None for removing stake) - :param destination_coldkey_ss58: SS58 address of destination coldkey - :param amount: Amount of stake to transfer in RAO - :param block_hash: Optional block hash at which to perform the calculation - - :return: The calculated stake fee as a Balance object - - When to use None: - - 1. Adding new stake (default fee): - - origin_hotkey_ss58 = None - - origin_netuid = None - - All other fields required - - 2. Removing stake (default fee): - - destination_hotkey_ss58 = None - - destination_netuid = None - - All other fields required - - For all other operations, no None values - provide all parameters: - 3. Moving between subnets - 4. Moving between hotkeys - 5. Moving between coldkeys - """ + :param origin_hotkey_ss58: SS58 address of source hotkey (None for new stake) + :param origin_netuid: Netuid of source subnet (None for new stake) + :param origin_coldkey_ss58: SS58 address of source coldkey + :param destination_hotkey_ss58: SS58 address of destination hotkey (None for removing stake) + :param destination_netuid: Netuid of destination subnet (None for removing stake) + :param destination_coldkey_ss58: SS58 address of destination coldkey + :param amount: Amount of stake to transfer in RAO + :param block_hash: Optional block hash at which to perform the calculation + + :return: The calculated stake fee as a Balance object + + When to use None: + + 1. Adding new stake (default fee): + - origin_hotkey_ss58 = None + - origin_netuid = None + - All other fields required + + 2. Removing stake (default fee): + - destination_hotkey_ss58 = None + - destination_netuid = None + - All other fields required + + For all other operations, no None values - provide all parameters: + 3. Moving between subnets + 4. Moving between hotkeys + 5. Moving between coldkeys + """ + + origin = None + if origin_hotkey_ss58 is not None and origin_netuid is not None: + origin = (origin_hotkey_ss58, origin_netuid) + + destination = None + if destination_hotkey_ss58 is not None and destination_netuid is not None: + destination = (destination_hotkey_ss58, destination_netuid) + + result = await self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_fee", + params=[ + origin, + origin_coldkey_ss58, + destination, + destination_coldkey_ss58, + amount, + ], + block_hash=block_hash, + ) - origin = ( - (origin_hotkey_ss58, origin_netuid) - if origin_hotkey_ss58 is not None and origin_netuid is not None - else None - ) - - destination = ( - (destination_hotkey_ss58, destination_netuid) - if destination_hotkey_ss58 is not None and destination_netuid is not None - else None - ) - - result = await self.query_runtime_api( - runtime_api="StakeInfoRuntimeApi", - method="get_stake_fee", - params=[ - origin, - origin_coldkey_ss58, - destination, - destination_coldkey_ss58, - amount, - ], - block_hash=block_hash, - ) - - return Balance.from_rao(result if result is not None else 0) + return Balance.from_rao(result if result is not None else 0) From 3921aa1d7eaeb7e6df77d096ac4b0a553b9eee7e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Sun, 16 Mar 2025 15:06:54 -0700 Subject: [PATCH 04/35] Dynamic fee in stake remove --- bittensor_cli/src/commands/stake/remove.py | 84 +++++++++++++++++++--- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 07718e405..d62ff75ce 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -194,9 +194,25 @@ async def unstake( ) continue # Skip to the next subnet - useful when single amount is specified for all subnets - received_amount, slippage_pct, slippage_pct_float = _calculate_slippage( - subnet_info=subnet_info, amount=amount_to_unstake_as_balance + stake_fee = await subtensor.get_stake_fee( + origin_hotkey_ss58=staking_address_ss58, + origin_netuid=netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_hotkey_ss58=None, + destination_netuid=None, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + amount=amount_to_unstake_as_balance.rao, ) + + try: + received_amount, slippage_pct, slippage_pct_float = _calculate_slippage( + subnet_info=subnet_info, + amount=amount_to_unstake_as_balance, + stake_fee=stake_fee, + ) + except ValueError: + continue + total_received_amount += received_amount max_float_slippage = max(max_float_slippage, slippage_pct_float) @@ -220,6 +236,7 @@ async def unstake( str(amount_to_unstake_as_balance), # Amount to Unstake str(subnet_info.price.tao) + f"({Balance.get_unit(0)}/{Balance.get_unit(netuid)})", # Rate + str(stake_fee), # Fee str(received_amount), # Received Amount slippage_pct, # Slippage Percent ] @@ -411,6 +428,11 @@ async def unstake_all( justify="center", style=COLOR_PALETTE["POOLS"]["RATE"], ) + table.add_column( + f"Fee ({Balance.unit})", + justify="center", + style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], + ) table.add_column( f"Recieved ({Balance.unit})", justify="center", @@ -432,9 +454,22 @@ async def unstake_all( hotkey_display = hotkey_names.get(stake.hotkey_ss58, stake.hotkey_ss58) subnet_info = all_sn_dynamic_info.get(stake.netuid) stake_amount = stake.stake - received_amount, slippage_pct, slippage_pct_float = _calculate_slippage( - subnet_info=subnet_info, amount=stake_amount + stake_fee = await subtensor.get_stake_fee( + origin_hotkey_ss58=stake.hotkey_ss58, + origin_netuid=stake.netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_hotkey_ss58=None, + destination_netuid=None, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + amount=stake_amount.rao, ) + try: + received_amount, slippage_pct, slippage_pct_float = _calculate_slippage( + subnet_info=subnet_info, amount=stake_amount, stake_fee=stake_fee + ) + except ValueError: + continue + max_slippage = max(max_slippage, slippage_pct_float) total_received_value += received_amount @@ -444,6 +479,7 @@ async def unstake_all( str(stake_amount), str(float(subnet_info.price)) + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})", + str(stake_fee), str(received_amount), slippage_pct, ) @@ -791,12 +827,15 @@ async def _unstake_all_extrinsic( # Helpers -def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]: +def _calculate_slippage( + subnet_info, amount: Balance, stake_fee: Balance +) -> tuple[Balance, str, float]: """Calculate slippage and received amount for unstaking operation. Args: subnet_info: Subnet information containing price data amount: Amount being unstaked + stake_fee: Stake fee to include in slippage calculation Returns: tuple containing: @@ -804,15 +843,32 @@ def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, flo - slippage_pct: Formatted string of slippage percentage - slippage_pct_float: Float value of slippage percentage """ - received_amount, _, slippage_pct_float = subnet_info.alpha_to_tao_with_slippage( - amount - ) + received_amount, _, _ = subnet_info.alpha_to_tao_with_slippage(amount) + received_amount -= stake_fee + + if received_amount < Balance.from_tao(0): + print_error("Not enough Alpha to pay the transaction fee.") + raise ValueError if subnet_info.is_dynamic: + # Ideal amount w/o slippage + ideal_amount = subnet_info.alpha_to_tao(amount) + ideal_amount -= stake_fee + + # Total slippage including fees + total_slippage = ideal_amount - received_amount + slippage_pct_float = ( + 100 * (float(total_slippage.tao) / float(ideal_amount.tao)) + if ideal_amount.tao != 0 + else 0 + ) slippage_pct = f"{slippage_pct_float:.4f} %" else: - slippage_pct_float = 0 - slippage_pct = "[red]N/A[/red]" + # Root will only have fee-based slippage + slippage_pct_float = ( + 100 * float(stake_fee.tao) / float(amount.tao) if amount.tao != 0 else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f} %" return received_amount, slippage_pct, slippage_pct_float @@ -1174,6 +1230,11 @@ def _create_unstake_table( justify="center", style=COLOR_PALETTE["POOLS"]["RATE"], ) + table.add_column( + f"Fee ({Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], + ) table.add_column( f"Received ({Balance.get_unit(0)})", justify="center", @@ -1227,7 +1288,8 @@ def _print_table_and_slippage( - [bold white]Hotkey[/bold white]: The ss58 address or identity of the hotkey you are unstaking from. - [bold white]Amount to Unstake[/bold white]: The stake amount you are removing from this key. - [bold white]Rate[/bold white]: The rate of exchange between TAO and the subnet's stake. - - [bold white]Received[/bold white]: The amount of free balance TAO you will receive on this subnet after slippage. + - [bold white]Fee[/bold white]: The transaction fee for this unstake operation. + - [bold white]Received[/bold white]: The amount of free balance TAO you will receive on this subnet after slippage and fees. - [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root).""" safe_staking_description = """ From f5ef40fa92ddc641f45ef77b252c9ee59d02c56a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Sun, 16 Mar 2025 15:24:11 -0700 Subject: [PATCH 05/35] Dynamic pricing in movements --- bittensor_cli/src/commands/stake/move.py | 49 +++++++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 70afaa453..403dc9668 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -32,13 +32,14 @@ async def display_stake_movement_cross_subnets( origin_hotkey: str, destination_hotkey: str, amount_to_move: Balance, + stake_fee: Balance, ) -> tuple[Balance, float, str, str]: """Calculate and display slippage information""" if origin_netuid == destination_netuid: subnet = await subtensor.subnet(origin_netuid) received_amount_tao = subnet.alpha_to_tao(amount_to_move) - received_amount_tao -= MIN_STAKE_FEE + received_amount_tao -= stake_fee if received_amount_tao < Balance.from_tao(0): print_error("Not enough Alpha to pay the transaction fee.") @@ -46,7 +47,7 @@ async def display_stake_movement_cross_subnets( received_amount = subnet.tao_to_alpha(received_amount_tao) slippage_pct_float = ( - 100 * float(MIN_STAKE_FEE) / float(MIN_STAKE_FEE + received_amount_tao) + 100 * float(stake_fee) / float(stake_fee + received_amount_tao) if received_amount_tao != 0 else 0 ) @@ -67,7 +68,7 @@ async def display_stake_movement_cross_subnets( received_amount_tao, _, _ = dynamic_origin.alpha_to_tao_with_slippage( amount_to_move ) - received_amount_tao -= MIN_STAKE_FEE + received_amount_tao -= stake_fee received_amount, _, _ = dynamic_destination.tao_to_alpha_with_slippage( received_amount_tao ) @@ -135,6 +136,11 @@ async def display_stake_movement_cross_subnets( justify="center", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) + table.add_column( + "Fee (τ)", + justify="center", + style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], + ) table.add_column( "slippage", justify="center", @@ -149,13 +155,11 @@ async def display_stake_movement_cross_subnets( str(amount_to_move), price_str, str(received_amount), + str(stake_fee), str(slippage_pct), ) console.print(table) - # console.print( - # f"[dim]A fee of {MIN_STAKE_FEE} applies.[/dim]" - # ) # Display slippage warning if necessary if slippage_pct_float > 5: @@ -513,6 +517,16 @@ async def move_stake( ) return False + stake_fee = await subtensor.get_stake_fee( + origin_hotkey_ss58=origin_hotkey, + origin_netuid=origin_netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_hotkey_ss58=destination_hotkey, + destination_netuid=destination_netuid, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + amount=amount_to_move_as_balance.rao, + ) + # Slippage warning if prompt: try: @@ -523,6 +537,7 @@ async def move_stake( origin_hotkey=origin_hotkey, destination_hotkey=destination_hotkey, amount_to_move=amount_to_move_as_balance, + stake_fee=stake_fee, ) except ValueError: return False @@ -686,6 +701,16 @@ async def transfer_stake( ) return False + stake_fee = await subtensor.get_stake_fee( + origin_hotkey_ss58=origin_hotkey, + origin_netuid=origin_netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_hotkey_ss58=origin_hotkey, + destination_netuid=dest_netuid, + destination_coldkey_ss58=dest_coldkey_ss58, + amount=amount_to_transfer.rao, + ) + # Slippage warning if prompt: try: @@ -696,6 +721,7 @@ async def transfer_stake( origin_hotkey=origin_hotkey, destination_hotkey=origin_hotkey, amount_to_move=amount_to_transfer, + stake_fee=stake_fee, ) except ValueError: return False @@ -844,6 +870,16 @@ async def swap_stake( ) return False + stake_fee = await subtensor.get_stake_fee( + origin_hotkey_ss58=hotkey_ss58, + origin_netuid=origin_netuid, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_hotkey_ss58=hotkey_ss58, + destination_netuid=destination_netuid, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + amount=amount_to_swap.rao, + ) + # Slippage warning if prompt: try: @@ -854,6 +890,7 @@ async def swap_stake( origin_hotkey=hotkey_ss58, destination_hotkey=hotkey_ss58, amount_to_move=amount_to_swap, + stake_fee=stake_fee, ) except ValueError: return False From 76fae6a24a18a8d1df81b0f883042dbbbfabae16 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Sun, 16 Mar 2025 15:26:09 -0700 Subject: [PATCH 06/35] Removes unused imports --- bittensor_cli/src/bittensor/subtensor_interface.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index c82b71d54..ea47fd1e4 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -36,8 +36,6 @@ decode_hex_identity_dict, validate_chain_endpoint, u16_normalized_float, - encode_account_id, - decode_account_id, ) SubstrateClass = ( From 3960c7118c64b4d2bf71dd3101f4ed4815084296 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 17 Mar 2025 09:37:51 -0700 Subject: [PATCH 07/35] Adds dynamic fee to stake add --- bittensor_cli/src/commands/stake/add.py | 52 ++++++++++++++++++++----- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 6d156f522..2451d51b2 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -282,10 +282,24 @@ async def stake_extrinsic( return False remaining_wallet_balance -= amount_to_stake - # Calculate slippage - received_amount, slippage_pct, slippage_pct_float, rate = ( - _calculate_slippage(subnet_info, amount_to_stake) + stake_fee = await subtensor.get_stake_fee( + origin_hotkey_ss58=None, + origin_netuid=None, + origin_coldkey_ss58=wallet.coldkeypub.ss58_address, + destination_hotkey_ss58=hotkey[1], + destination_netuid=netuid, + destination_coldkey_ss58=wallet.coldkeypub.ss58_address, + amount=amount_to_stake.rao, ) + + # Calculate slippage + try: + received_amount, slippage_pct, slippage_pct_float, rate = ( + _calculate_slippage(subnet_info, amount_to_stake, stake_fee) + ) + except ValueError: + return False + max_slippage = max(slippage_pct_float, max_slippage) # Add rows for the table @@ -296,6 +310,7 @@ async def stake_extrinsic( str(rate) + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", # rate str(received_amount.set_unit(netuid)), # received + str(stake_fee), # fee str(slippage_pct), # slippage ] @@ -531,6 +546,11 @@ def _define_stake_table( justify="center", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) + table.add_column( + "Fee (τ)", + justify="center", + style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], + ) table.add_column( "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] ) @@ -585,29 +605,41 @@ def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: b def _calculate_slippage( - subnet_info, amount: Balance + subnet_info, amount: Balance, stake_fee: Balance ) -> tuple[Balance, str, float, str]: """Calculate slippage when adding stake. Args: subnet_info: Subnet dynamic info amount: Amount being staked + stake_fee: Transaction fee for the stake operation Returns: tuple containing: - - received_amount: Amount received after slippage + - received_amount: Amount received after slippage and fees - slippage_str: Formatted slippage percentage string - slippage_float: Raw slippage percentage value + - rate: Exchange rate string """ - received_amount, _, slippage_pct_float = subnet_info.tao_to_alpha_with_slippage( - amount - ) + amount_after_fee = amount - stake_fee + + if amount_after_fee < 0: + print_error("You don't have enough balance to cover the stake fee.") + raise ValueError() + + received_amount, _, _ = subnet_info.tao_to_alpha_with_slippage(amount_after_fee) + if subnet_info.is_dynamic: + ideal_amount = subnet_info.tao_to_alpha(amount) + total_slippage = ideal_amount - received_amount + slippage_pct_float = 100 * (total_slippage.tao / ideal_amount.tao) slippage_str = f"{slippage_pct_float:.4f} %" rate = f"{(1 / subnet_info.price.tao or 1):.4f}" else: - slippage_pct_float = 0 - slippage_str = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]" + slippage_pct_float = ( + 100 * float(stake_fee.tao) / float(amount.tao) if amount.tao != 0 else 0 + ) + slippage_str = f"{slippage_pct_float:.4f} %" rate = "1" return received_amount, slippage_str, slippage_pct_float, rate From 9ac84e4791f2f87e69ecd6ef281973349a1c73b2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 17 Mar 2025 09:39:27 -0700 Subject: [PATCH 08/35] Updates e2e tests to devnet ready --- .github/workflows/e2e-subtensor-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index d66eadbee..9d2e97cc1 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -61,7 +61,7 @@ jobs: - name: Setup subtensor repo working-directory: ${{ github.workspace }}/subtensor - run: git checkout testnet + run: git checkout devnet-ready - name: Install Python dependencies run: python3 -m pip install -e . pytest From e9e0e7003488db0eaa612fab994b136c1f5cfb5a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 17 Mar 2025 09:57:51 -0700 Subject: [PATCH 09/35] Update remove slippage calc --- bittensor_cli/src/commands/stake/remove.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index d62ff75ce..ff7daa1e3 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -853,7 +853,6 @@ def _calculate_slippage( if subnet_info.is_dynamic: # Ideal amount w/o slippage ideal_amount = subnet_info.alpha_to_tao(amount) - ideal_amount -= stake_fee # Total slippage including fees total_slippage = ideal_amount - received_amount From ebecac00cd41851361253889409df0bdc153977f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 17 Mar 2025 09:59:14 -0700 Subject: [PATCH 10/35] Updates to devnet ready --- .github/workflows/e2e-subtensor-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index d66eadbee..9d2e97cc1 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -61,7 +61,7 @@ jobs: - name: Setup subtensor repo working-directory: ${{ github.workspace }}/subtensor - run: git checkout testnet + run: git checkout devnet-ready - name: Install Python dependencies run: python3 -m pip install -e . pytest From e1a751f9eda94ee9aa1de108ecd7f56fa6a19d04 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 17 Mar 2025 12:52:38 -0700 Subject: [PATCH 11/35] Bumps async substrate --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d7e557dae..4c9f3d3b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ requires-python = ">=3.9,<3.13" dependencies = [ "wheel", "async-property==0.2.2", - "async-substrate-interface>=1.0.7", + "async-substrate-interface>=1.0.8", "aiohttp~=3.10.2", "backoff~=2.2.1", "GitPython>=3.0.0", From 219539351abd02da24ef84735c7af0e61b15bcc1 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 17 Mar 2025 21:58:43 +0200 Subject: [PATCH 12/35] Adds instructions for pip installation for README. --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d8fcc0dd..5d33a2760 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,20 @@ Installation steps are described below. For a full documentation on how to use ` ## Install on macOS and Linux -You can install `btcli` on your local machine directly from source. **Make sure you verify your installation after you install**: +You can install `btcli` on your local machine directly from source, or from from PyPI. **Make sure you verify your installation after you install**: + + +### Install from PyPI + +Run +``` +pip install -U bittensor-cli +``` + +Alternatively, if you prefer to use [uv](https://pypi.org/project/uv/): +``` +uv pip install bittensor-cli +``` ### Install from source @@ -69,6 +82,14 @@ cd btcli pip3 install . ``` +### Also install bittensor (SDK) + +If you prefer to install the btcli alongside the bittensor SDK, you can do this in a single command with + +``` +pip install -U bittensor[cli] +``` + --- ## Install on Windows From 8eac12c0c8dc2388890386f8e4c375acf7fea36a Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 17 Mar 2025 22:18:51 +0200 Subject: [PATCH 13/35] Adds support for py 3.13 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d7e557dae..9fc0c9a00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,11 +12,11 @@ authors = [ ] license = { file = "LICENSE" } scripts = { btcli = "bittensor_cli.cli:main" } -requires-python = ">=3.9,<3.13" +requires-python = ">=3.9,<3.14" dependencies = [ "wheel", "async-property==0.2.2", - "async-substrate-interface>=1.0.7", + "async-substrate-interface>=1.0.8", "aiohttp~=3.10.2", "backoff~=2.2.1", "GitPython>=3.0.0", From ac1f249191a91f10d3579a96bd633ebc86869a7f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 17 Mar 2025 15:38:40 -0700 Subject: [PATCH 14/35] Removes check for none --- bittensor_cli/src/bittensor/subtensor_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index ea47fd1e4..dba786369 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1500,4 +1500,4 @@ async def get_stake_fee( block_hash=block_hash, ) - return Balance.from_rao(result if result is not None else 0) + return Balance.from_rao(result) From 2ba6e5ad8e542c1056d4c84fc1acb6740f303660 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 15:45:57 -0700 Subject: [PATCH 15/35] improve e2e tests' workflow --- .github/workflows/e2e-subtensor-tests.yml | 89 ++++++++++++++--------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 9d2e97cc1..7a7c68754 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -24,48 +24,71 @@ env: VERBOSE: ${{ github.event.inputs.verbose }} jobs: - run-tests: - runs-on: SubtensorCI - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} - timeout-minutes: 180 - env: - RELEASE_NAME: development - RUSTV: stable - RUST_BACKTRACE: full - RUST_BIN_DIR: target/x86_64-unknown-linux-gnu - TARGET: x86_64-unknown-linux-gnu + find-tests: + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + outputs: + test-files: ${{ steps.get-tests.outputs.test-files }} steps: - name: Check-out repository under $GITHUB_WORKSPACE - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: Install dependencies + - name: Find test files + id: get-tests run: | - sudo apt-get update && - sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler + test_files=$(find tests/e2e_tests -name "test*.py" | jq -R -s -c 'split("\n") | map(select(. != ""))') + echo "::set-output name=test-files::$test_files" + shell: bash + + pull-docker-image: + runs-on: ubuntu-latest + steps: + - name: Log in to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin + + - name: Pull Docker Image + run: docker pull ghcr.io/opentensor/subtensor-localnet:latest + + - name: Save Docker Image to Cache + run: docker save -o subtensor-localnet.tar ghcr.io/opentensor/subtensor-localnet:latest - - name: Install Rust ${{ env.RUSTV }} - uses: actions-rs/toolchain@v1.0.6 + - name: Upload Docker Image as Artifact + uses: actions/upload-artifact@v4 with: - toolchain: ${{ env.RUSTV }} - components: rustfmt - profile: minimal + name: subtensor-localnet + path: subtensor-localnet.tar - - name: Add wasm32-unknown-unknown target - run: | - rustup target add wasm32-unknown-unknown --toolchain stable-x86_64-unknown-linux-gnu - rustup component add rust-src --toolchain stable-x86_64-unknown-linux-gnu + run-e2e-tests: + needs: + - find-tests + - pull-docker-image + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false # Allow other matrix jobs to run even if this job fails + max-parallel: 32 # Set the maximum number of parallel jobs (same as we have cores in SubtensorCI runner) + matrix: + os: + - ubuntu-latest + test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} + steps: + - name: Check-out repository + uses: actions/checkout@v4 - - name: Clone subtensor repo - run: git clone https://github.com/opentensor/subtensor.git + - name: Install uv + uses: astral-sh/setup-uv@v4 - - name: Setup subtensor repo - working-directory: ${{ github.workspace }}/subtensor - run: git checkout devnet-ready + - name: install dependencies + run: uv sync --all-extras --dev - - name: Install Python dependencies - run: python3 -m pip install -e . pytest + - name: Download Cached Docker Image + uses: actions/download-artifact@v4 + with: + name: subtensor-localnet - - name: Run all tests - run: | - LOCALNET_SH_PATH="${{ github.workspace }}/subtensor/scripts/localnet.sh" pytest tests/e2e_tests -s \ No newline at end of file + - name: Load Docker Image + run: docker load -i subtensor-localnet.tar + + - name: Run tests + run: uv run pytest ${{ matrix.test-file }} -s From 76da6a2e30dd3362316ae758e399556885cd51fb Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 15:50:04 -0700 Subject: [PATCH 16/35] avoid cubit installation --- .github/workflows/e2e-subtensor-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 7a7c68754..907c2a6db 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -80,7 +80,7 @@ jobs: uses: astral-sh/setup-uv@v4 - name: install dependencies - run: uv sync --all-extras --dev + run: uv run pip install -e . pytest - name: Download Cached Docker Image uses: actions/download-artifact@v4 From 2721b55de2a9c3fbbf8bcfac6950ec36781f93ef Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 15:55:43 -0700 Subject: [PATCH 17/35] opps --- .github/workflows/e2e-subtensor-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 907c2a6db..8a6897ff7 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -80,7 +80,9 @@ jobs: uses: astral-sh/setup-uv@v4 - name: install dependencies - run: uv run pip install -e . pytest + run: | + uv pip install . --without-extras + uv pop install pytest - name: Download Cached Docker Image uses: actions/download-artifact@v4 From 63b7516b40f4b76ba2ced73c8252b4408151c81e Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 16:04:08 -0700 Subject: [PATCH 18/35] opps 2 --- .github/workflows/e2e-subtensor-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 8a6897ff7..0dbf53287 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -81,7 +81,7 @@ jobs: - name: install dependencies run: | - uv pip install . --without-extras + uv pip install . -- --without-extras uv pop install pytest - name: Download Cached Docker Image From 00e04758e561fc2783e75bee4b97934b5157a58f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 16:07:53 -0700 Subject: [PATCH 19/35] opps 3 --- .github/workflows/e2e-subtensor-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 0dbf53287..013ff6a71 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -81,8 +81,8 @@ jobs: - name: install dependencies run: | - uv pip install . -- --without-extras - uv pop install pytest + uv pip install . + uv pip install pytest - name: Download Cached Docker Image uses: actions/download-artifact@v4 From 7f21261bf654c21316d6c4056738f75f2ea6719c Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 16:09:32 -0700 Subject: [PATCH 20/35] opps 4 --- .github/workflows/e2e-subtensor-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 013ff6a71..d521d352f 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -81,8 +81,8 @@ jobs: - name: install dependencies run: | - uv pip install . - uv pip install pytest + uv pip install --system . + uv pip install --system pytest - name: Download Cached Docker Image uses: actions/download-artifact@v4 From bd305c1dcd3172d169e0be5c4550bae5ea6f2e2f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 16:17:16 -0700 Subject: [PATCH 21/35] try venv --- .github/workflows/e2e-subtensor-tests.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index d521d352f..b56bbb213 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -81,8 +81,10 @@ jobs: - name: install dependencies run: | - uv pip install --system . - uv pip install --system pytest + uv venv .venv + source .venv/bin/activate + uv pip install . + uv pip install pytest - name: Download Cached Docker Image uses: actions/download-artifact@v4 @@ -93,4 +95,6 @@ jobs: run: docker load -i subtensor-localnet.tar - name: Run tests - run: uv run pytest ${{ matrix.test-file }} -s + run: | + source .venv/bin/activate + uv run pytest ${{ matrix.test-file }} -s From b93e205fba6f9b378d8fbe6d0672bffc5acb9a9d Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 16:21:39 -0700 Subject: [PATCH 22/35] astral-sh wants python version --- .github/workflows/e2e-subtensor-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index b56bbb213..568350af0 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -78,6 +78,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v4 + with: + python-version: 3.13 - name: install dependencies run: | From 95eac6fa5f4af38f0493f56a01b93bc4aab42b9a Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 16:22:30 -0700 Subject: [PATCH 23/35] astral-sh wants python version --- .github/workflows/e2e-subtensor-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 568350af0..b1af0f289 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -72,6 +72,7 @@ jobs: os: - ubuntu-latest test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - name: Check-out repository uses: actions/checkout@v4 From 014c868de01a5447adca846c4a89265bf9aeab39 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 16:36:34 -0700 Subject: [PATCH 24/35] try without cubit deps --- .github/workflows/e2e-subtensor-tests.yml | 1 + pyproject.toml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index b1af0f289..c1b158537 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -87,6 +87,7 @@ jobs: uv venv .venv source .venv/bin/activate uv pip install . + uv pip install --verbose git+https://github.com/opentensor/cubit.git uv pip install pytest - name: Download Cached Docker Image diff --git a/pyproject.toml b/pyproject.toml index 9fc0c9a00..8ff9e58fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ dependencies = [ [project.optional-dependencies] cuda = [ "torch>=1.13.1,<2.6.0", - "cubit>=1.1.0" ] [project.urls] From fe41fb82bc6943f0444dc66fb56a257061ba492f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 16:38:35 -0700 Subject: [PATCH 25/35] comment cubit --- .github/workflows/e2e-subtensor-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index c1b158537..8eb7664c3 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -87,7 +87,7 @@ jobs: uv venv .venv source .venv/bin/activate uv pip install . - uv pip install --verbose git+https://github.com/opentensor/cubit.git +# pip install --verbose git+https://github.com/opentensor/cubit.git uv pip install pytest - name: Download Cached Docker Image From c74931e6e58d3c93646893390b4b2bf6f33b0717 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 16:39:30 -0700 Subject: [PATCH 26/35] remove cubit --- .github/workflows/e2e-subtensor-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 8eb7664c3..b1af0f289 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -87,7 +87,6 @@ jobs: uv venv .venv source .venv/bin/activate uv pip install . -# pip install --verbose git+https://github.com/opentensor/cubit.git uv pip install pytest - name: Download Cached Docker Image From 7061f6580aa70beda65abd37d10be80b64db2736 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 16:44:09 -0700 Subject: [PATCH 27/35] try short name for tests in matrix --- .github/workflows/e2e-subtensor-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index b1af0f289..6fecafa03 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -60,6 +60,7 @@ jobs: path: subtensor-localnet.tar run-e2e-tests: + name: ${{ matrix.test-file }} / Python ${{ matrix.python-version }} needs: - find-tests - pull-docker-image From 5db5bd202a20e344f7b5a06ed24ce7cbcf2af223 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 17:22:20 -0700 Subject: [PATCH 28/35] update test's runners --- tests/e2e_tests/conftest.py | 154 +++++++++++++++++++++-- tests/e2e_tests/test_senate.py | 8 +- tests/e2e_tests/test_staking_sudo.py | 1 - tests/e2e_tests/test_wallet_creations.py | 14 --- 4 files changed, 146 insertions(+), 31 deletions(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 4d9f9c7b1..9a471ee82 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -5,6 +5,7 @@ import shutil import signal import subprocess +import sys import time import pytest @@ -13,9 +14,48 @@ from .utils import setup_wallet +def wait_for_node_start(process, pattern, timestamp: int = None): + for line in process.stdout: + print(line.strip()) + # 20 min as timeout + timestamp = timestamp or int(time.time()) + if int(time.time()) - timestamp > 20 * 60: + pytest.fail("Subtensor not started in time") + if pattern.search(line): + print("Node started!") + break + + # Fixture for setting up and tearing down a localnet.sh chain between tests @pytest.fixture(scope="function") def local_chain(request): + """Determines whether to run the localnet.sh script in a subprocess or a Docker container.""" + args = request.param if hasattr(request, "param") else None + params = "" if args is None else f"{args}" + if shutil.which("docker") and not os.getenv("USE_DOCKER") == "0": + yield from docker_runner(params) + else: + if not os.getenv("USE_DOCKER") == "0": + if sys.platform.startswith("linux"): + docker_command = ( + "Install docker with command " + "[blue]sudo apt-get update && sudo apt-get install docker.io -y[/blue]" + " or use documentation [blue]https://docs.docker.com/engine/install/[/blue]" + ) + elif sys.platform == "darwin": + docker_command = ( + "Install docker with command [blue]brew install docker[/blue]" + ) + else: + docker_command = "[blue]Unknown OS, install Docker manually: https://docs.docker.com/get-docker/[/blue]" + + logging.warning("Docker not found in the operating system!") + logging.warning(docker_command) + logging.warning("Tests are run in legacy mode.") + yield from legacy_runner(request) + + +def legacy_runner(request): param = request.param if hasattr(request, "param") else None # Get the environment variable for the script path script_path = os.getenv("LOCALNET_SH_PATH") @@ -41,18 +81,6 @@ def local_chain(request): # Install neuron templates logging.info("Downloading and installing neuron templates from github") - timestamp = int(time.time()) - - def wait_for_node_start(process, pattern): - for line in process.stdout: - print(line.strip()) - # 20 min as timeout - if int(time.time()) - timestamp > 20 * 60: - pytest.fail("Subtensor not started in time") - if pattern.search(line): - print("Node started!") - break - wait_for_node_start(process, pattern) # Run the test, passing in substrate interface @@ -72,6 +100,108 @@ def wait_for_node_start(process, pattern): process.wait() +def docker_runner(params): + """Starts a Docker container before tests and gracefully terminates it after.""" + + def is_docker_running(): + """Check if Docker has been run.""" + try: + subprocess.run( + ["docker", "info"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + ) + return True + except subprocess.CalledProcessError: + return False + + def try_start_docker(): + """Run docker based on OS.""" + try: + subprocess.run(["open", "-a", "Docker"], check=True) # macOS + except (FileNotFoundError, subprocess.CalledProcessError): + try: + subprocess.run(["systemctl", "start", "docker"], check=True) # Linux + except (FileNotFoundError, subprocess.CalledProcessError): + try: + subprocess.run( + ["sudo", "service", "docker", "start"], check=True + ) # Linux alternative + except (FileNotFoundError, subprocess.CalledProcessError): + print("Failed to start Docker. Manual start may be required.") + return False + + # Wait Docker run 10 attempts with 3 sec waits + for _ in range(10): + if is_docker_running(): + return True + time.sleep(3) + + print("Docker wasn't run. Manual start may be required.") + return False + + container_name = f"test_local_chain_{str(time.time()).replace('.', '_')}" + image_name = "ghcr.io/opentensor/subtensor-localnet:latest" + + # Command to start container + cmds = [ + "docker", + "run", + "--rm", + "--name", + container_name, + "-p", + "9944:9944", + "-p", + "9945:9945", + image_name, + params, + ] + + try_start_docker() + + # Start container + with subprocess.Popen( + cmds, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + start_new_session=True, + ) as process: + try: + substrate = None + try: + pattern = re.compile(r"Imported #1") + wait_for_node_start(process, pattern, int(time.time())) + except TimeoutError: + raise + + result = subprocess.run( + ["docker", "ps", "-q", "-f", f"name={container_name}"], + capture_output=True, + text=True, + ) + if not result.stdout.strip(): + raise RuntimeError("Docker container failed to start.") + + substrate = AsyncSubstrateInterface(url="ws://127.0.0.1:9944") + yield substrate + + finally: + try: + if substrate: + substrate.close() + except Exception: + pass + + try: + subprocess.run(["docker", "kill", container_name]) + process.wait() + except subprocess.TimeoutExpired: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + + @pytest.fixture(scope="function") def wallet_setup(): wallet_paths = [] diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index 7d8a52418..9253d1083 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -216,17 +216,17 @@ def test_senate(local_chain, wallet_setup): proposals_after_nay_output = proposals_after_nay.stdout.splitlines() # Total Ayes to remain 1 - proposals_after_nay_output[9].split()[2] == "1" + assert proposals_after_nay_output[9].split()[2] == "1" # Total Nays increased to 1 - proposals_after_nay_output[9].split()[4] == "1" + assert proposals_after_nay_output[9].split()[4] == "1" # Assert Alice has voted Nay - proposals_after_nay_output[10].split()[0].strip( + assert proposals_after_nay_output[10].split()[0].strip( ":" ) == wallet_alice.hotkey.ss58_address # Assert vote casted as Nay - proposals_after_nay_output[9].split()[1] == "Nay" + assert proposals_after_nay_output[9].split()[1] == "Nay" print("✅ Passed senate commands") diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index c1b939c8f..f8bfc6d0c 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -1,5 +1,4 @@ import re -import time from bittensor_cli.src.bittensor.balances import Balance diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index ac82ec2a4..d72d848a0 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -486,20 +486,6 @@ def test_wallet_balance_all(local_chain, wallet_setup, capfd): wallet_name = f"test_wallet_{i}" wallet_names.append(wallet_name) - result = exec_command( - command="wallet", - sub_command="new-coldkey", - extra_args=[ - "--wallet-name", - wallet_name, - "--wallet-path", - wallet_path, - "--n-words", - "12", - "--no-use-password", - ], - ) - wallet_status, message = verify_wallet_dir(wallet_path, wallet_name) assert wallet_status, message From d762c9aaa1f22606b5056eda3e5ed4f16dedf283 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 17 Mar 2025 17:33:18 -0700 Subject: [PATCH 29/35] Dummy commit --- bittensor_cli/src/commands/stake/remove.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index ff7daa1e3..898af9cf8 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -839,7 +839,7 @@ def _calculate_slippage( Returns: tuple containing: - - received_amount: Balance after slippage + - received_amount: Balance after slippage deduction - slippage_pct: Formatted string of slippage percentage - slippage_pct_float: Float value of slippage percentage """ From 18f5efff09d15a70cc64412c19740b9c0a9c46f4 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 18:10:51 -0700 Subject: [PATCH 30/35] add dev deps --- .github/workflows/e2e-subtensor-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 6fecafa03..7e3d4a1d3 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -87,7 +87,7 @@ jobs: run: | uv venv .venv source .venv/bin/activate - uv pip install . + uv pip install .[dev] uv pip install pytest - name: Download Cached Docker Image From 57b4c3d768e4aa60d80e08609972bb2cc5c75e0a Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 18:11:12 -0700 Subject: [PATCH 31/35] improve + fix --- tests/e2e_tests/test_senate.py | 2 +- tests/e2e_tests/test_wallet_creations.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index 9253d1083..dfd2cfa57 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -227,6 +227,6 @@ def test_senate(local_chain, wallet_setup): ) == wallet_alice.hotkey.ss58_address # Assert vote casted as Nay - assert proposals_after_nay_output[9].split()[1] == "Nay" + assert proposals_after_nay_output[10].split()[1] == "Nay" print("✅ Passed senate commands") diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index d72d848a0..ea0959985 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -486,6 +486,20 @@ def test_wallet_balance_all(local_chain, wallet_setup, capfd): wallet_name = f"test_wallet_{i}" wallet_names.append(wallet_name) + exec_command( + command="wallet", + sub_command="new-coldkey", + extra_args=[ + "--wallet-name", + wallet_name, + "--wallet-path", + wallet_path, + "--n-words", + "12", + "--no-use-password", + ], + ) + wallet_status, message = verify_wallet_dir(wallet_path, wallet_name) assert wallet_status, message From 67b919600cbeb69db5d6f35c19e05742bb4f9cb5 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 18:16:57 -0700 Subject: [PATCH 32/35] check unstaking behavior --- .github/workflows/e2e-subtensor-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 7e3d4a1d3..b4dc3120f 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -73,7 +73,7 @@ jobs: os: - ubuntu-latest test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + # python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - name: Check-out repository uses: actions/checkout@v4 From bc87b8d12f423e608f7f3ab350cce8dbf96d5220 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 18:23:49 -0700 Subject: [PATCH 33/35] add timeout to wait --- tests/e2e_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 9a471ee82..e3625683f 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -197,7 +197,7 @@ def try_start_docker(): try: subprocess.run(["docker", "kill", container_name]) - process.wait() + process.wait(timeout=10) except subprocess.TimeoutExpired: os.killpg(os.getpgid(process.pid), signal.SIGKILL) From 3abaf628d829c3a0122e7d43461e450edaebf67a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 17 Mar 2025 18:41:37 -0700 Subject: [PATCH 34/35] bumps version and changelog --- CHANGELOG.md | 10 ++++++++++ pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a5da6e32..18a999205 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 9.2.0 /2025-03-18 + +## What's Changed +* Updates to E2E suubtensor tests to devnet ready by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/390 +* Allow Py 3.13 install by @thewhaleking in https://github.com/opentensor/btcli/pull/392 +* pip install readme by @thewhaleking in https://github.com/opentensor/btcli/pull/391 +* Feat/dynamic staking fee by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/389 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.1.4...v9.2.0 + ## 9.1.4 /2025-03-13 ## What's Changed diff --git a/pyproject.toml b/pyproject.toml index 9fc0c9a00..f9793d4a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.1.4" +version = "9.2.0" description = "Bittensor CLI" readme = "README.md" authors = [ From fe10f98e936af4d64004fba7594c65a28c07aba2 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 18:52:12 -0700 Subject: [PATCH 35/35] CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18a999205..75397415f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 9.2.0 /2025-03-18 ## What's Changed +* Improve e2e tests' workflow by @roman-opentensor in https://github.com/opentensor/btcli/pull/393 * Updates to E2E suubtensor tests to devnet ready by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/390 * Allow Py 3.13 install by @thewhaleking in https://github.com/opentensor/btcli/pull/392 * pip install readme by @thewhaleking in https://github.com/opentensor/btcli/pull/391