From 5397f36849046f802f1e54bf9a8ab5ce8e84650c Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 10 Sep 2025 17:03:41 +0200 Subject: [PATCH 1/5] Corrects the stake fee calculation --- .../src/bittensor/subtensor_interface.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index cb3b295f3..02a62baaf 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1547,14 +1547,24 @@ async def get_stake_fee( if origin_netuid is None: origin_netuid = 0 + if destination_netuid is None: + destination_netuid = 0 - fee_rate = await self.query("Swap", "FeeRate", [origin_netuid]) - fee = amount * (fee_rate / U16_MAX) + fee_rate, mechanism = await asyncio.gather( + self.query("Swap", "FeeRate", [origin_netuid]), + self.query("SubtensorModule", "SubnetMechanism", [destination_netuid]), + ) + if mechanism == 0: + # Stake Swap Fee is only charged if subnet mechanism is not 0 (stable), otherwise it is dynamic + fee = Balance(0).set_unit(origin_netuid) + return fee + else: + fee = amount * (fee_rate / U16_MAX) - result = Balance.from_rao(fee) - result.set_unit(origin_netuid) + result = Balance.from_rao(fee) + result.set_unit(origin_netuid) - return result + return result async def get_scheduled_coldkey_swap( self, From 4a694170370b6d4b690a07be8bf9da3f4802e373 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 10 Sep 2025 17:47:18 +0200 Subject: [PATCH 2/5] Added SimSwap Runtime call --- bittensor_cli/src/bittensor/chain_data.py | 17 +++++ .../src/bittensor/subtensor_interface.py | 75 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 0f64d8519..07fd8c906 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1193,3 +1193,20 @@ def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo": for adphk in decoded["alpha_dividends_per_hotkey"] ], ) + + +@dataclass +class SimSwapResult: + tao_amount: Balance + alpha_amount: Balance + tao_fee: Balance + alpha_fee: Balance + + @classmethod + def from_dict(cls, d: dict, netuid: int) -> "SimSwapResult": + return cls( + tao_amount=Balance.from_rao(d["tao_amount"]).set_unit(0), + alpha_amount=Balance.from_rao(d["alpha_amount"]).set_unit(netuid), + tao_fee=Balance.from_rao(d["tao_fee"]).set_unit(0), + alpha_fee=Balance.from_rao(d["alpha_fee"]).set_unit(netuid), + ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 02a62baaf..6b9171cde 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -28,6 +28,7 @@ DynamicInfo, SubnetState, MetagraphInfo, + SimSwapResult, ) from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float @@ -1502,6 +1503,80 @@ async def get_extrinsic_fee(self, call: GenericCall, keypair: Keypair) -> Balanc fee_dict = await self.substrate.get_payment_info(call, keypair) return Balance.from_rao(fee_dict["partial_fee"]) + async def sim_swap( + self, + origin_netuid: int, + destination_netuid: int, + amount: int, + block_hash: Optional[str] = None, + ) -> SimSwapResult: + """ + Hits the SimSwap Runtime API to calculate the fee and result for a given transaction. This should be used + instead of get_stake_fee for staking fee calculations. The SimSwapResult contains the staking fees and expected + returned amounts of a given transaction. This does not include the transaction (extrinsic) fee. + + Args: + origin_netuid: Netuid of the source subnet (0 if new stake) + destination_netuid: Netuid of the destination subnet + amount: Amount to transfer in Rao + block_hash: The hash of the blockchain block number for the query. + + Returns: + SimSwapResult object representing the result + """ + block_hash = block_hash or await self.substrate.get_chain_head() + if origin_netuid > 0 and destination_netuid > 0: + # for cross-subnet moves + intermediate_result_, sn_price = await asyncio.gather( + self.query_runtime_api( + "SwapRuntimeApi", + "sim_swap_alpha_for_tao", + params={"netuid": origin_netuid, "alpha": amount}, + block_hash=block_hash, + ), + self.get_subnet_price(origin_netuid, block_hash=block_hash), + ) + intermediate_result = SimSwapResult.from_dict( + intermediate_result_, origin_netuid + ) + result = SimSwapResult.from_dict( + await self.query_runtime_api( + "SwapRuntimeApi", + "sim_swap_tao_for_alpha", + params={ + "netuid": destination_netuid, + "tao": intermediate_result.tao_amount, + }, + block_hash=block_hash, + ), + destination_netuid, + ) + secondary_fee = (result.tao_fee * sn_price).set_unit(origin_netuid) + result.alpha_fee = result.alpha_fee + secondary_fee + return result + elif origin_netuid > 0: + # dynamic to tao + return SimSwapResult.from_dict( + await self.query_runtime_api( + "SwapRuntimeApi", + "sim_swap_alpha_for_tao", + params={"netuid": origin_netuid, "alpha": amount}, + block_hash=block_hash, + ), + origin_netuid, + ) + else: + # tao to dynamic or unstaked to staked tao (SN0) + return SimSwapResult.from_dict( + await self.query_runtime_api( + "SwapRuntimeApi", + "sim_swap_tao_for_alpha", + params={"netuid": destination_netuid, "tao": amount}, + block_hash=block_hash, + ), + destination_netuid, + ) + async def get_stake_fee( self, origin_hotkey_ss58: Optional[str], From e6e76294c2a76f2beec04c5bc5d04017f9d989b4 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 10 Sep 2025 17:54:19 +0200 Subject: [PATCH 3/5] Swap fee calc in stake add for simswap --- bittensor_cli/src/commands/stake/add.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 6579d9767..18a507c6d 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -347,17 +347,6 @@ async def stake_extrinsic( return False remaining_wallet_balance -= amount_to_stake - # TODO this should be asyncio gathered before the for loop - 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 # TODO: Update for V3, slippage calculation is significantly different in v3 # try: @@ -409,7 +398,13 @@ async def stake_extrinsic( safe_staking_=safe_staking, ) row_extension = [] - received_amount = rate * (amount_to_stake - stake_fee - extrinsic_fee) + # TODO this should be asyncio gathered before the for loop + sim_swap = await subtensor.sim_swap( + origin_netuid=0, + destination_netuid=netuid, + amount=(amount_to_stake - extrinsic_fee).rao, + ) + received_amount = sim_swap.alpha_amount # Add rows for the table base_row = [ str(netuid), # netuid @@ -418,7 +413,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(sim_swap.tao_fee), # fee str(extrinsic_fee), # str(slippage_pct), # slippage ] + row_extension From 323f9a059ddfcf3c1ac2a472593db472c15c0bc7 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 10 Sep 2025 18:15:39 +0200 Subject: [PATCH 4/5] Swapped all fee calculation for simswap --- .../src/bittensor/subtensor_interface.py | 2 +- bittensor_cli/src/commands/stake/move.py | 36 ++++++++----------- bittensor_cli/src/commands/stake/remove.py | 35 +++++------------- 3 files changed, 24 insertions(+), 49 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 6b9171cde..38b4213ad 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1526,7 +1526,7 @@ async def sim_swap( """ block_hash = block_hash or await self.substrate.get_chain_head() if origin_netuid > 0 and destination_netuid > 0: - # for cross-subnet moves + # for cross-subnet moves where neither origin nor destination is root intermediate_result_, sn_price = await asyncio.gather( self.query_runtime_api( "SwapRuntimeApi", diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index c72bbb41e..b4360ffdf 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -520,14 +520,10 @@ async def move_stake( "alpha_amount": amount_to_move_as_balance.rao, }, ) - stake_fee, extrinsic_fee = await asyncio.gather( - subtensor.get_stake_fee( - origin_hotkey_ss58=origin_hotkey, + sim_swap, extrinsic_fee = await asyncio.gather( + subtensor.sim_swap( 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), @@ -543,7 +539,9 @@ async def move_stake( origin_hotkey=origin_hotkey, destination_hotkey=destination_hotkey, amount_to_move=amount_to_move_as_balance, - stake_fee=stake_fee, + stake_fee=sim_swap.alpha_fee + if origin_netuid != 0 + else sim_swap.tao_fee, extrinsic_fee=extrinsic_fee, ) except ValueError: @@ -709,14 +707,10 @@ async def transfer_stake( "alpha_amount": amount_to_transfer.rao, }, ) - stake_fee, extrinsic_fee = await asyncio.gather( - subtensor.get_stake_fee( - origin_hotkey_ss58=origin_hotkey, + sim_swap, extrinsic_fee = await asyncio.gather( + subtensor.sim_swap( 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), @@ -732,7 +726,9 @@ async def transfer_stake( origin_hotkey=origin_hotkey, destination_hotkey=origin_hotkey, amount_to_move=amount_to_transfer, - stake_fee=stake_fee, + stake_fee=sim_swap.alpha_fee + if origin_netuid != 0 + else sim_swap.tao_fee, extrinsic_fee=extrinsic_fee, ) except ValueError: @@ -880,14 +876,10 @@ async def swap_stake( "alpha_amount": amount_to_swap.rao, }, ) - stake_fee, extrinsic_fee = await asyncio.gather( - subtensor.get_stake_fee( - origin_hotkey_ss58=hotkey_ss58, + sim_swap, extrinsic_fee = await asyncio.gather( + subtensor.sim_swap( 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), @@ -903,7 +895,9 @@ async def swap_stake( origin_hotkey=hotkey_ss58, destination_hotkey=hotkey_ss58, amount_to_move=amount_to_swap, - stake_fee=stake_fee, + stake_fee=sim_swap.alpha_fee + if origin_netuid != 0 + else sim_swap.tao_fee, extrinsic_fee=extrinsic_fee, ) except ValueError: diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 3a37b8cbe..ecec77fa5 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -200,16 +200,6 @@ async def unstake( ) continue # Skip to the next subnet - useful when single amount is specified for all subnets - 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: current_price = subnet_info.price.tao if safe_staking: @@ -240,10 +230,10 @@ async def unstake( netuid=netuid, amount=amount_to_unstake_as_balance, ) - rate = current_price - received_amount = ( - (amount_to_unstake_as_balance - stake_fee) * rate - ) - extrinsic_fee + sim_swap = await subtensor.sim_swap( + netuid, 0, amount_to_unstake_as_balance.rao + ) + received_amount = sim_swap.tao_amount - extrinsic_fee except ValueError: continue total_received_amount += received_amount @@ -266,7 +256,7 @@ async def unstake( str(amount_to_unstake_as_balance), # Amount to Unstake f"{subnet_info.price.tao:.6f}" + f"(τ/{Balance.get_unit(netuid)})", # Rate - str(stake_fee.set_unit(netuid)), # Fee + str(sim_swap.alpha_fee), # Fee str(extrinsic_fee), # Extrinsic fee str(received_amount), # Received Amount # slippage_pct, # Slippage Percent @@ -494,15 +484,6 @@ 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 - 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: current_price = subnet_info.price.tao @@ -515,8 +496,8 @@ async def unstake_all( subtensor, hotkey_ss58=stake.hotkey_ss58, ) - rate = current_price - received_amount = ((stake_amount - stake_fee) * rate) - extrinsic_fee + sim_swap = await subtensor.sim_swap(stake.netuid, 0, stake_amount.rao) + received_amount = sim_swap.tao_amount - extrinsic_fee if received_amount < Balance.from_tao(0): print_error("Not enough Alpha to pay the transaction fee.") @@ -532,7 +513,7 @@ async def unstake_all( str(stake_amount), f"{float(subnet_info.price):.6f}" + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})", - str(stake_fee), + str(sim_swap.alpha_fee), str(extrinsic_fee), str(received_amount), ) From e9a24b60393ea454fa260e21a2e1ce4168ee77c5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 10 Sep 2025 18:16:11 +0200 Subject: [PATCH 5/5] Removed get_stake_fee as we no longer use it. --- .../src/bittensor/subtensor_interface.py | 64 ------------------- 1 file changed, 64 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 38b4213ad..cafef0439 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1577,70 +1577,6 @@ async def sim_swap( destination_netuid, ) - 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 - """ - - if origin_netuid is None: - origin_netuid = 0 - if destination_netuid is None: - destination_netuid = 0 - - fee_rate, mechanism = await asyncio.gather( - self.query("Swap", "FeeRate", [origin_netuid]), - self.query("SubtensorModule", "SubnetMechanism", [destination_netuid]), - ) - if mechanism == 0: - # Stake Swap Fee is only charged if subnet mechanism is not 0 (stable), otherwise it is dynamic - fee = Balance(0).set_unit(origin_netuid) - return fee - else: - fee = amount * (fee_rate / U16_MAX) - - result = Balance.from_rao(fee) - result.set_unit(origin_netuid) - - return result - async def get_scheduled_coldkey_swap( self, block_hash: Optional[str] = None,