diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 2f117f147..3d8a632d5 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -5,6 +5,7 @@ import aiohttp from async_substrate_interface.utils.storage import StorageKey from bittensor_wallet import Wallet +from bittensor_wallet.bittensor_wallet import Keypair from bittensor_wallet.utils import SS58_FORMAT from scalecodec import GenericCall from async_substrate_interface.errors import SubstrateRequestException @@ -1488,6 +1489,19 @@ async def get_owned_hotkeys( return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + async def get_extrinsic_fee(self, call: GenericCall, keypair: Keypair) -> Balance: + """ + Determines the fee for the extrinsic call. + Args: + call: Created extrinsic call + keypair: The keypair that would sign the extrinsic (usually you would just want to use the *pub for this) + + Returns: + Balance object representing the fee for this extrinsic. + """ + fee_dict = await self.substrate.get_payment_info(call, keypair) + return Balance.from_rao(fee_dict["partial_fee"]) + async def get_stake_fee( self, origin_hotkey_ss58: Optional[str], diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 45b17dfbf..b223eaf2e 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -65,6 +65,45 @@ async def stake_add( bool: True if stake operation is successful, False otherwise """ + async def get_stake_extrinsic_fee( + netuid_: int, + amount_: Balance, + staking_address_: str, + safe_staking_: bool, + price_limit: Optional[Balance] = None, + ): + """ + Quick method to get the extrinsic fee for adding stake depending on the args supplied. + Args: + netuid_: The netuid where the stake will be added + amount_: the amount of stake to add + staking_address_: the hotkey ss58 to stake to + safe_staking_: whether to use safe staking + price_limit: rate with tolerance + + Returns: + Balance object representing the extrinsic fee for adding stake. + """ + call_fn = "add_stake" if not safe_staking_ else "add_stake_limit" + call_params = { + "hotkey": staking_address_, + "netuid": netuid_, + "amount_staked": amount_.rao, + } + if safe_staking_: + call_params.update( + { + "limit_price": price_limit, + "allow_partial": allow_partial_stake, + } + ) + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_fn, + call_params=call_params, + ) + return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + async def safe_stake_extrinsic( netuid_: int, amount_: Balance, @@ -87,7 +126,7 @@ async def safe_stake_extrinsic( "hotkey": hotkey_ss58_, "netuid": netuid_, "amount_staked": amount_.rao, - "limit_price": price_limit, + "limit_price": price_limit.rao, "allow_partial": allow_partial_stake, }, ), @@ -332,19 +371,6 @@ async def stake_extrinsic( # Temporary workaround - calculations without slippage current_price_float = float(subnet_info.price.tao) rate = 1.0 / current_price_float - received_amount = rate * amount_to_stake - - # Add rows for the table - base_row = [ - str(netuid), # netuid - f"{hotkey[1]}", # hotkey - str(amount_to_stake), # amount - 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 - ] # If we are staking safe, add price tolerance if safe_staking: @@ -356,21 +382,45 @@ async def stake_extrinsic( rate_with_tolerance = f"{_rate_with_tolerance:.4f}" price_with_tolerance = Balance.from_tao( price_with_tolerance - ).rao # Actual price to pass to extrinsic + ) # Actual price to pass to extrinsic else: rate_with_tolerance = "1" price_with_tolerance = Balance.from_rao(1) + extrinsic_fee = await get_stake_extrinsic_fee( + netuid_=netuid, + amount_=amount_to_stake, + staking_address_=hotkey[1], + safe_staking_=safe_staking, + price_limit=price_with_tolerance, + ) prices_with_tolerance.append(price_with_tolerance) - - base_row.extend( - [ - f"{rate_with_tolerance} {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", - f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]" - # safe staking - f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", - ] + row_extension = [ + f"{rate_with_tolerance} {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", + f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]" + # safe staking + f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", + ] + else: + extrinsic_fee = await get_stake_extrinsic_fee( + netuid_=netuid, + amount_=amount_to_stake, + staking_address_=hotkey[1], + safe_staking_=safe_staking, ) - + row_extension = [] + received_amount = rate * (amount_to_stake - stake_fee - extrinsic_fee) + # Add rows for the table + base_row = [ + str(netuid), # netuid + f"{hotkey[1]}", # hotkey + str(amount_to_stake), # amount + str(rate) + + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", # rate + str(received_amount.set_unit(netuid)), # received + str(stake_fee), # fee + str(extrinsic_fee), + # str(slippage_pct), # slippage + ] + row_extension rows.append(tuple(base_row)) # Define and print stake table + slippage warning @@ -569,17 +619,17 @@ def _define_stake_table( "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] ) table.add_column( - f"Amount ({Balance.get_unit(0)})", + "Amount (τ)", justify="center", style=COLOR_PALETTE["POOLS"]["TAO"], ) table.add_column( - f"Rate (per {Balance.get_unit(0)})", + "Rate (per τ)", justify="center", style=COLOR_PALETTE["POOLS"]["RATE"], ) table.add_column( - "Received", + "Est. Received", justify="center", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) @@ -588,6 +638,11 @@ def _define_stake_table( justify="center", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], ) + table.add_column( + "Extrinsic Fee (τ)", + justify="center", + style=COLOR_PALETTE.STAKE.TAO, + ) # TODO: Uncomment when slippage is reimplemented for v3 # table.add_column( # "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index ede380597..b4efbd127 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -33,12 +33,15 @@ async def display_stake_movement_cross_subnets( destination_hotkey: str, amount_to_move: Balance, stake_fee: Balance, + extrinsic_fee: Balance, ) -> tuple[Balance, str]: """Calculate and display stake movement information""" if origin_netuid == destination_netuid: subnet = await subtensor.subnet(origin_netuid) - received_amount_tao = subnet.alpha_to_tao(amount_to_move - stake_fee) + received_amount_tao = ( + subnet.alpha_to_tao(amount_to_move - stake_fee) - extrinsic_fee + ) received_amount = subnet.tao_to_alpha(received_amount_tao) if received_amount < Balance.from_tao(0).set_unit(destination_netuid): @@ -62,7 +65,9 @@ async def display_stake_movement_cross_subnets( price_destination = dynamic_destination.price.tao rate = price_origin / (price_destination or 1) - received_amount_tao = dynamic_origin.alpha_to_tao(amount_to_move - stake_fee) + received_amount_tao = ( + dynamic_origin.alpha_to_tao(amount_to_move - stake_fee) - extrinsic_fee + ) received_amount = dynamic_destination.tao_to_alpha(received_amount_tao) received_amount.set_unit(destination_netuid) @@ -81,14 +86,14 @@ async def display_stake_movement_cross_subnets( # Create and display table table = Table( title=( - f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]" + f"\n[{COLOR_PALETTE.G.HEADER}]" f"Moving stake from: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})" - f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"[{COLOR_PALETTE.G.SUBHEAD}]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})" + f"[/{COLOR_PALETTE.G.SUBHEAD}] " f"to: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})" - f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\nNetwork: {subtensor.network}\n" - f"[/{COLOR_PALETTE['GENERAL']['HEADER']}]" + f"[{COLOR_PALETTE.G.SUBHEAD}]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})" + f"[/{COLOR_PALETTE.G.SUBHEAD}]\nNetwork: {subtensor.network}\n" + f"[/{COLOR_PALETTE.G.HEADER}]" ), show_footer=True, show_edge=False, @@ -132,6 +137,9 @@ async def display_stake_movement_cross_subnets( justify="center", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], ) + table.add_column( + "Extrinsic Fee (τ)", justify="center", style=COLOR_PALETTE.STAKE.TAO + ) table.add_row( f"{Balance.get_unit(origin_netuid)}({origin_netuid})", @@ -142,6 +150,7 @@ async def display_stake_movement_cross_subnets( price_str, str(received_amount), str(stake_fee.set_unit(origin_netuid)), + str(extrinsic_fee), ) console.print(table) @@ -165,10 +174,10 @@ def prompt_stake_amount( while True: amount_input = Prompt.ask( f"\nEnter the amount to {action_name} " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] " + f"[{COLOR_PALETTE.S.STAKE_AMOUNT}](max: {current_balance})[/{COLOR_PALETTE.S.STAKE_AMOUNT}] " f"or " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]'all'[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]'all'[/{COLOR_PALETTE.S.STAKE_AMOUNT}] " f"for entire balance" ) @@ -183,7 +192,7 @@ def prompt_stake_amount( if amount > current_balance.tao: console.print( f"[red]Amount exceeds available balance of " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_balance}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]" f"[/red]" ) continue @@ -270,8 +279,8 @@ async def stake_move_transfer_selection( # Display available netuids for selected hotkey table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Available Stakes for Hotkey\n[/{COLOR_PALETTE['GENERAL']['HEADER']}]" - f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{origin_hotkey_ss58}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n", + title=f"\n[{COLOR_PALETTE.G.HEADER}]Available Stakes for Hotkey\n[/{COLOR_PALETTE.G.HEADER}]" + f"[{COLOR_PALETTE.G.HK}]{origin_hotkey_ss58}[/{COLOR_PALETTE.G.HK}]\n", show_edge=False, header_style="bold white", border_style="bright_black", @@ -279,7 +288,7 @@ async def stake_move_transfer_selection( width=len(origin_hotkey_ss58) + 20, ) table.add_column("Netuid", style="cyan") - table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) + table.add_column("Stake Amount", style=COLOR_PALETTE.STAKE.STAKE_AMOUNT) available_netuids = [] for netuid in origin_hotkey_info["netuids"]: @@ -347,8 +356,8 @@ async def stake_swap_selection( # Display available stakes table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Available Stakes for Hotkey\n[/{COLOR_PALETTE['GENERAL']['HEADER']}]" - f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey_str}: {wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n", + title=f"\n[{COLOR_PALETTE.G.HEADER}]Available Stakes for Hotkey\n[/{COLOR_PALETTE.G.HEADER}]" + f"[{COLOR_PALETTE.G.HK}]{wallet.hotkey_str}: {wallet.hotkey.ss58_address}[/{COLOR_PALETTE.G.HK}]\n", show_edge=False, header_style="bold white", border_style="bright_black", @@ -366,7 +375,7 @@ async def stake_swap_selection( for idx, (netuid, stake_info) in enumerate(sorted(hotkey_stakes.items())): subnet_info = subnet_dict[netuid] subnet_name_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet_info.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f"[{COLOR_PALETTE.G.SYM}]{subnet_info.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE.G.SYM}]" f" {get_subnet_name(subnet_info)}" ) @@ -498,14 +507,28 @@ 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, + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": origin_hotkey, + "origin_netuid": origin_netuid, + "destination_hotkey": destination_hotkey, + "destination_netuid": destination_netuid, + "alpha_amount": amount_to_move_as_balance.rao, + }, + ) + stake_fee, extrinsic_fee = await asyncio.gather( + 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, + ), + subtensor.get_extrinsic_fee(call, wallet.coldkeypub), ) # Display stake movement details @@ -519,6 +542,7 @@ async def move_stake( destination_hotkey=destination_hotkey, amount_to_move=amount_to_move_as_balance, stake_fee=stake_fee, + extrinsic_fee=extrinsic_fee, ) except ValueError: return False @@ -529,20 +553,10 @@ async def move_stake( if not unlock_key(wallet).success: return False with console.status( - f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: [blue]{origin_netuid}[/blue] \nto " + f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: " + f"[blue]{origin_netuid}[/blue] \nto " f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="move_stake", - call_params={ - "origin_hotkey": origin_hotkey, - "origin_netuid": origin_netuid, - "destination_hotkey": destination_hotkey, - "destination_netuid": destination_netuid, - "alpha_amount": amount_to_move_as_balance.rao, - }, - ) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -677,19 +691,33 @@ async def transfer_stake( if amount_to_transfer > current_stake: err_console.print( f"[red]Not enough stake to transfer[/red]:\n" - f"Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < " - f"Transfer amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_transfer}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < " + f"Transfer amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_transfer}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]" ) 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, + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="transfer_stake", + call_params={ + "destination_coldkey": dest_coldkey_ss58, + "hotkey": origin_hotkey, + "origin_netuid": origin_netuid, + "destination_netuid": dest_netuid, + "alpha_amount": amount_to_transfer.rao, + }, + ) + stake_fee, extrinsic_fee = await asyncio.gather( + 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, + ), + subtensor.get_extrinsic_fee(call, wallet.coldkeypub), ) # Display stake movement details @@ -703,6 +731,7 @@ async def transfer_stake( destination_hotkey=origin_hotkey, amount_to_move=amount_to_transfer, stake_fee=stake_fee, + extrinsic_fee=extrinsic_fee, ) except ValueError: return False @@ -715,18 +744,6 @@ async def transfer_stake( return False with console.status("\n:satellite: Transferring stake ..."): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="transfer_stake", - call_params={ - "destination_coldkey": dest_coldkey_ss58, - "hotkey": origin_hotkey, - "origin_netuid": origin_netuid, - "destination_netuid": dest_netuid, - "alpha_amount": amount_to_transfer.rao, - }, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -846,19 +863,32 @@ async def swap_stake( if amount_to_swap > current_stake: err_console.print( f"[red]Not enough stake to swap[/red]:\n" - f"Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < " - f"Swap amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_swap}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < " + f"Swap amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_swap}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]" ) 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, + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="swap_stake", + call_params={ + "hotkey": hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_netuid": destination_netuid, + "alpha_amount": amount_to_swap.rao, + }, + ) + stake_fee, extrinsic_fee = await asyncio.gather( + 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, + ), + subtensor.get_extrinsic_fee(call, wallet.coldkeypub), ) # Display stake movement details @@ -872,6 +902,7 @@ async def swap_stake( destination_hotkey=hotkey_ss58, amount_to_move=amount_to_swap, stake_fee=stake_fee, + extrinsic_fee=extrinsic_fee, ) except ValueError: return False @@ -887,17 +918,6 @@ async def swap_stake( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " f"to netuid [blue]{destination_netuid}[/blue]..." ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="swap_stake", - call_params={ - "hotkey": hotkey_ss58, - "origin_netuid": origin_netuid, - "destination_netuid": destination_netuid, - "alpha_amount": amount_to_swap.rao, - }, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 5dbef5fe9..a28254e32 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -47,6 +47,7 @@ async def unstake( era: int, ): """Unstake from hotkey(s).""" + with console.status( f"Retrieving subnet data & identities from {subtensor.network}...", spinner="earth", @@ -82,7 +83,7 @@ async def unstake( hotkey_to_unstake_all = hotkeys_to_unstake_from[0] unstake_all_alpha = Confirm.ask( "\nDo you want to:\n" - "[blue]Yes[/blue]: Unstake from all subnets and automatically restake to subnet 0 (root)\n" + "[blue]Yes[/blue]: Unstake from all subnets and automatically re-stake to subnet 0 (root)\n" "[blue]No[/blue]: Unstake everything (including subnet 0)", default=True, ) @@ -210,8 +211,38 @@ async def unstake( try: current_price = subnet_info.price.tao + if safe_staking: + if subnet_info.is_dynamic: + price_with_tolerance = current_price * (1 - rate_tolerance) + rate_with_tolerance = price_with_tolerance + price_limit = Balance.from_tao( + rate_with_tolerance + ) # Actual price to pass to extrinsic + else: + price_limit = Balance.from_rao(1) + extrinsic_fee = await _get_extrinsic_fee( + "unstake_safe", + wallet, + subtensor, + hotkey_ss58=staking_address_ss58, + amount=amount_to_unstake_as_balance, + netuid=netuid, + price_limit=price_limit, + allow_partial_stake=allow_partial_stake, + ) + else: + extrinsic_fee = await _get_extrinsic_fee( + "unstake", + wallet, + subtensor, + hotkey_ss58=staking_address_ss58, + netuid=netuid, + amount=amount_to_unstake_as_balance, + ) rate = current_price - received_amount = amount_to_unstake_as_balance * rate + received_amount = ( + (amount_to_unstake_as_balance - stake_fee) * rate + ) - extrinsic_fee except ValueError: continue total_received_amount += received_amount @@ -233,8 +264,9 @@ async def unstake( staking_address_name, # Hotkey Name str(amount_to_unstake_as_balance), # Amount to Unstake f"{subnet_info.price.tao:.6f}" - + f"({Balance.get_unit(0)}/{Balance.get_unit(netuid)})", # Rate + + f"(τ/{Balance.get_unit(netuid)})", # Rate str(stake_fee.set_unit(netuid)), # Fee + str(extrinsic_fee), # Extrinsic fee str(received_amount), # Received Amount # slippage_pct, # Slippage Percent ] @@ -431,10 +463,15 @@ async def unstake_all( style=COLOR_PALETTE["POOLS"]["RATE"], ) table.add_column( - f"Fee ({Balance.unit})", + f"Fee ({Balance.get_unit(1)})", justify="center", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], ) + table.add_column( + "Extrinsic Fee (τ)", + justify="center", + style=COLOR_PALETTE.STAKE.TAO, + ) table.add_column( f"Received ({Balance.unit})", justify="center", @@ -467,8 +504,17 @@ async def unstake_all( try: current_price = subnet_info.price.tao + extrinsic_type = ( + "unstake_all" if not unstake_all_alpha else "unstake_all_alpha" + ) + extrinsic_fee = await _get_extrinsic_fee( + extrinsic_type, + wallet, + subtensor, + hotkey_ss58=stake.hotkey_ss58, + ) rate = current_price - received_amount = stake_amount * rate - stake_fee + received_amount = ((stake_amount - stake_fee) * rate) - extrinsic_fee if received_amount < Balance.from_tao(0): print_error("Not enough Alpha to pay the transaction fee.") @@ -485,6 +531,7 @@ async def unstake_all( f"{float(subnet_info.price):.6f}" + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})", str(stake_fee), + str(extrinsic_fee), str(received_amount), ) console.print(table) @@ -830,6 +877,63 @@ async def _unstake_all_extrinsic( err_out(f"{failure_prelude} with error: {str(e)}") +async def _get_extrinsic_fee( + _type: str, + wallet: Wallet, + subtensor: "SubtensorInterface", + hotkey_ss58: str, + netuid: Optional[int] = None, + amount: Optional[Balance] = None, + price_limit: Optional[Balance] = None, + allow_partial_stake: bool = False, +) -> Balance: + """ + Retrieves the extrinsic fee for a given unstaking call. + Args: + _type: 'unstake', 'unstake_safe', 'unstake_all', 'unstake_all_alpha' depending on the specific + extrinsic to be called + wallet: Wallet object + subtensor: SubtensorInterface object + hotkey_ss58: the hotkey ss58 to unstake from + netuid: the netuid from which to remove the stake + amount: the amount of stake to remove + price_limit: the price limit + allow_partial_stake: whether to allow partial unstaking + + Returns: + Balance object representing the extrinsic fee. + """ + lookup_table = { + "unstake": lambda: ( + "remove_stake", + { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": amount.rao, + }, + ), + "unstake_safe": lambda: ( + "remove_stake_limit", + { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": amount.rao, + "limit_price": price_limit, + "allow_partial": allow_partial_stake, + }, + ), + "unstake_all": lambda: ("unstake_all", {"hotkey": hotkey_ss58}), + "unstake_all_alpha": lambda: ("unstake_all_alpha", {"hotkey": hotkey_ss58}), + } + call_fn, call_params = lookup_table[_type]() + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_fn, + call_params=call_params, + ) + return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + + # Helpers async def _unstake_selection( dynamic_info, @@ -1184,7 +1288,7 @@ def _create_unstake_table( style=COLOR_PALETTE["POOLS"]["TAO"], ) table.add_column( - f"Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})", + f"Rate (τ/{Balance.get_unit(1)})", justify="center", style=COLOR_PALETTE["POOLS"]["RATE"], ) @@ -1194,7 +1298,10 @@ def _create_unstake_table( style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], ) table.add_column( - f"Received ({Balance.get_unit(0)})", + "Extrinsic Fee (τ)", justify="center", style=COLOR_PALETTE.STAKE.TAO + ) + table.add_column( + "Received (τ)", justify="center", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], footer=str(total_received_amount),