Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e24ab24
Adds a warning on transfers that transferring is not the same as stak…
thewhaleking Sep 9, 2025
7f806ba
Merge pull request #618 from opentensor/chore/thewhaleking/add-warnin…
thewhaleking Sep 9, 2025
d167d4b
Better type annotations
thewhaleking Sep 9, 2025
b4045fd
Adds command to turn off hyperparams freeze window, applies it as nec…
thewhaleking Sep 9, 2025
81401f4
CI's PYTHON_PATH not set correctly. This is a bandage.
thewhaleking Sep 9, 2025
9cc03ae
Merge pull request #620 from opentensor/fix/thewhaleking/update-e2e-t…
thewhaleking Sep 9, 2025
5397f36
Corrects the stake fee calculation
thewhaleking Sep 10, 2025
4a69417
Added SimSwap Runtime call
thewhaleking Sep 10, 2025
e6e7629
Swap fee calc in stake add for simswap
thewhaleking Sep 10, 2025
323f9a0
Swapped all fee calculation for simswap
thewhaleking Sep 10, 2025
e9a24b6
Removed get_stake_fee as we no longer use it.
thewhaleking Sep 10, 2025
59c2e5a
Merge pull request #621 from opentensor/fix/thewhaleking/incorrect-sn…
thewhaleking Sep 10, 2025
9f49b1e
handles encrypted hotkeys
ibraheem-abe Sep 12, 2025
99afe67
Merge pull request #622 from opentensor/fix/encrypted-hotkey-handling
ibraheem-abe Sep 12, 2025
3e07125
coldkeypub is not a file
ibraheem-abe Sep 12, 2025
66fef1d
'hotkey' is not a directory
ibraheem-abe Sep 12, 2025
8e8efad
individual hotkey file is malformed
ibraheem-abe Sep 12, 2025
74c5e10
Merge pull request #623 from opentensor/fix/handle-malformed-wallet-e…
ibraheem-abe Sep 12, 2025
6741ebc
`min_burn` now not root sudo only
thewhaleking Sep 16, 2025
08ffd14
Merge pull request #624 from opentensor/fix/thewhaleking/min-burn-now…
thewhaleking Sep 16, 2025
5606875
bumps version and updates changelog
ibraheem-abe Sep 16, 2025
aad303e
Merge pull request #625 from opentensor/changelog/9111
ibraheem-abe Sep 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 9.11.1 /2025-09-16

* Transfer not staking warning by @thewhaleking in https://github.com/opentensor/btcli/pull/618
* update e2e tests for hyperparam freeze window by @thewhaleking in https://github.com/opentensor/btcli/pull/620
* Corrects the stake fee calculation by @thewhaleking in https://github.com/opentensor/btcli/pull/621
* Fix: Handle encrypted wallet hotkeys by @ibraheem-abe in https://github.com/opentensor/btcli/pull/622
* Fix: Handle malformed wallets/files by @ibraheem-abe in https://github.com/opentensor/btcli/pull/623
* `min_burn` now not root sudo only by @thewhaleking in https://github.com/opentensor/btcli/pull/624

**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.11.0...v9.11.1

## 9.11.0 /2025-09-05
* Better arg naming + type annotations by @thewhaleking in https://github.com/opentensor/btcli/pull/590
* disk cache in config by @thewhaleking in https://github.com/opentensor/btcli/pull/588
Expand Down
2 changes: 1 addition & 1 deletion bittensor_cli/src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ class WalletValidationTypes(Enum):
"adjustment_interval": ("sudo_set_adjustment_interval", True),
"activity_cutoff": ("sudo_set_activity_cutoff", False),
"target_regs_per_interval": ("sudo_set_target_registrations_per_interval", True),
"min_burn": ("sudo_set_min_burn", True),
"min_burn": ("sudo_set_min_burn", False),
"max_burn": ("sudo_set_max_burn", True),
"bonds_moving_avg": ("sudo_set_bonds_moving_average", False),
"max_regs_per_block": ("sudo_set_max_registrations_per_block", True),
Expand Down
17 changes: 17 additions & 0 deletions bittensor_cli/src/bittensor/chain_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
4 changes: 3 additions & 1 deletion bittensor_cli/src/bittensor/extrinsics/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ async def do_transfer() -> tuple[bool, str, str]:
f" amount: [bright_cyan]{amount if not transfer_all else account_balance}[/bright_cyan]\n"
f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : "
f"[bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]"
f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]"
f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]\n"
f":warning:[bright_yellow]Transferring is not the same as staking. To instead stake, use "
f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow]:warning:"
):
return False

Expand Down
113 changes: 67 additions & 46 deletions bittensor_cli/src/bittensor/subtensor_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1502,59 +1503,79 @@ 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 get_stake_fee(
async def sim_swap(
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,
origin_netuid: int,
destination_netuid: int,
amount: int,
block_hash: Optional[str] = None,
) -> Balance:
) -> SimSwapResult:
"""
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:
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.

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
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.

For all other operations, no None values - provide all parameters:
3. Moving between subnets
4. Moving between hotkeys
5. Moving between coldkeys
Returns:
SimSwapResult object representing the result
"""

if origin_netuid is None:
origin_netuid = 0

fee_rate = await self.query("Swap", "FeeRate", [origin_netuid])
fee = amount * (fee_rate / U16_MAX)

result = Balance.from_rao(fee)
result.set_unit(origin_netuid)

return result
block_hash = block_hash or await self.substrate.get_chain_head()
if origin_netuid > 0 and destination_netuid > 0:
# for cross-subnet moves where neither origin nor destination is root
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_scheduled_coldkey_swap(
self,
Expand Down
3 changes: 2 additions & 1 deletion bittensor_cli/src/bittensor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def get_hotkey_wallets_for_wallet(
hotkeys_path = wallet_path / wallet.name / "hotkeys"
try:
hotkeys = [entry.name for entry in hotkeys_path.iterdir()]
except FileNotFoundError:
except (FileNotFoundError, NotADirectoryError):
hotkeys = []
for h_name in hotkeys:
if h_name.endswith("pub.txt"):
Expand Down Expand Up @@ -307,6 +307,7 @@ def get_hotkey_wallets_for_wallet(
AttributeError,
TypeError,
KeyFileError,
ValueError,
): # usually an unrelated file like .DS_Store
continue

Expand Down
21 changes: 8 additions & 13 deletions bittensor_cli/src/commands/stake/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
36 changes: 15 additions & 21 deletions bittensor_cli/src/commands/stake/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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:
Expand Down Expand Up @@ -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),
Expand All @@ -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:
Expand Down Expand Up @@ -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),
Expand All @@ -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:
Expand Down
Loading
Loading