Skip to content
66 changes: 66 additions & 0 deletions bittensor_cli/src/bittensor/subtensor_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1435,3 +1435,69 @@ 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

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,
)

return Balance.from_rao(result)
52 changes: 42 additions & 10 deletions bittensor_cli/src/commands/stake/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
]

Expand Down Expand Up @@ -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"]
)
Expand Down Expand Up @@ -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
49 changes: 43 additions & 6 deletions bittensor_cli/src/commands/stake/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,22 @@ 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.")
raise ValueError

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
)
Expand All @@ -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
)
Expand Down Expand Up @@ -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",
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
Loading
Loading