Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,28 @@ jobs:
path: dist/

approve-and-publish:
name: Approve and publish to PyPI
needs: build
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
id-token: write

steps:
- name: Download artifact
uses: actions/download-artifact@v8
with:
name: dist
path: dist/

- name: Verify artifact checksums
run: |
echo "Artifacts to be published:"
ls -la dist/
echo ""
echo "SHA256 checksums:"
sha256sum dist/*

- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 9.20.2rc2 /2026-04-06

## What's Changed
* Fix/e2e tests for stake locked as owner alpha feat by @ibraheem-abe in https://github.com/latent-to/btcli/pull/900
* fix: update max_burn to owner/sudo settable and align no-prompt routing by @bitloi in https://github.com/latent-to/btcli/pull/902
* fix: `stake add` operation mapping for multi-hotkey and multi-netuid by @bitloi in https://github.com/latent-to/btcli/pull/897
* Update/swap coldkey restriction by @ibraheem-abe in https://github.com/latent-to/btcli/pull/905
* Update: Log SHA & other info during release by @ibraheem-abe in https://github.com/latent-to/btcli/pull/906

**Full Changelog**: https://github.com/latent-to/btcli/compare/v9.20.1...v9.20.2

## 9.20.1 /2026-04-02

## What's Changed
Expand Down
4 changes: 2 additions & 2 deletions bittensor_cli/src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ class RootSudoOnly(Enum):
RootSudoOnly.TRUE,
),
"min_burn": ("sudo_set_min_burn", RootSudoOnly.FALSE),
"max_burn": ("sudo_set_max_burn", RootSudoOnly.TRUE),
"max_burn": ("sudo_set_max_burn", RootSudoOnly.COMPLICATED),
"bonds_moving_avg": ("sudo_set_bonds_moving_average", RootSudoOnly.FALSE),
"max_regs_per_block": ("sudo_set_max_registrations_per_block", RootSudoOnly.TRUE),
"serving_rate_limit": ("sudo_set_serving_rate_limit", RootSudoOnly.FALSE),
Expand Down Expand Up @@ -745,7 +745,7 @@ class RootSudoOnly(Enum):
"max_burn": {
"description": "Maximum TAO burn amount cap for subnet registration.",
"side_effects": "Caps registration costs, ensuring registration remains accessible even as difficulty increases.",
"owner_settable": False,
"owner_settable": True,
"docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#maxburn",
},
"bonds_moving_avg": {
Expand Down
236 changes: 117 additions & 119 deletions bittensor_cli/src/commands/stake/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,119 +339,134 @@ async def stake_extrinsic(
)

# Determine the amount we are staking.
rows = []
amounts_to_stake = []
current_stake_balances = []
prices_with_tolerance = []
remaining_wallet_balance = current_wallet_balance
max_slippage = 0.0

operation_targets = []
for hotkey in hotkeys_to_stake_to:
for netuid in netuids:
# Check that the subnet exists.
subnet_info = all_subnets.get(netuid)
if not subnet_info:
print_error(f"Subnet with netuid: {netuid} does not exist.")
continue
current_stake_balances.append(hotkey_stake_map[hotkey[1]][netuid])

# Get the amount.
amount_to_stake = Balance(0)
if amount:
amount_to_stake = Balance.from_tao(amount)
elif stake_all:
amount_to_stake = current_wallet_balance / len(netuids)
elif not amount:
amount_to_stake, _ = _prompt_stake_amount(
current_balance=remaining_wallet_balance,
netuid=netuid,
action_name="stake",
)
amounts_to_stake.append(amount_to_stake)
operation_targets.append(
(hotkey, netuid, subnet_info, hotkey_stake_map[hotkey[1]][netuid])
)

# Check enough to stake.
if amount_to_stake > remaining_wallet_balance:
print_error(
f"Not enough stake:[bold white]\n wallet balance:{remaining_wallet_balance} < "
f"staking amount: {amount_to_stake}[/bold white]"
)
return
remaining_wallet_balance -= amount_to_stake

# Calculate slippage
# TODO: Update for V3, slippage calculation is significantly different in v3
# 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)

# Temporary workaround - calculations without slippage
current_price_float = float(subnet_info.price.tao)
rate = _safe_inverse_rate(current_price_float)

# If we are staking safe, add price tolerance
if safe_staking:
if subnet_info.is_dynamic:
price_with_tolerance = current_price_float * (1 + rate_tolerance)
_rate_with_tolerance = _safe_inverse_rate(
price_with_tolerance
) # Rate only for display
rate_with_tolerance = f"{_rate_with_tolerance:.4f}"
price_with_tolerance = Balance.from_tao(
price_with_tolerance
) # 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)
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'}]",
]
if stake_all and not operation_targets:
print_error("No valid staking operations to perform.")
return

rows = []
operations = []
remaining_wallet_balance = current_wallet_balance
max_slippage = 0.0

for hotkey, netuid, subnet_info, current_stake_balance in operation_targets:
staking_address = hotkey[1]

# Get the amount.
amount_to_stake = Balance(0)
if amount:
amount_to_stake = Balance.from_tao(amount)
elif stake_all:
amount_to_stake = current_wallet_balance / len(operation_targets)
elif not amount:
amount_to_stake, _ = _prompt_stake_amount(
current_balance=remaining_wallet_balance,
netuid=netuid,
action_name="stake",
)

# Check enough to stake.
if amount_to_stake > remaining_wallet_balance:
print_error(
f"Not enough stake:[bold white]\n wallet balance:{remaining_wallet_balance} < "
f"staking amount: {amount_to_stake}[/bold white]"
)
return
remaining_wallet_balance -= amount_to_stake

# Calculate slippage
# TODO: Update for V3, slippage calculation is significantly different in v3
# 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)

# Temporary workaround - calculations without slippage
current_price_float = float(subnet_info.price.tao)
rate = _safe_inverse_rate(current_price_float)
price_with_tolerance = None

# If we are staking safe, add price tolerance
if safe_staking:
if subnet_info.is_dynamic:
price_with_tolerance = current_price_float * (1 + rate_tolerance)
_rate_with_tolerance = _safe_inverse_rate(
price_with_tolerance
) # Rate only for display
rate_with_tolerance = f"{_rate_with_tolerance:.4f}"
price_with_tolerance = Balance.from_tao(
price_with_tolerance
) # Actual price to pass to extrinsic
else:
extrinsic_fee = await get_stake_extrinsic_fee(
netuid_=netuid,
amount_=amount_to_stake,
staking_address_=hotkey[1],
safe_staking_=safe_staking,
)
row_extension = []
# TODO this should be asyncio gathered before the for loop
amount_minus_fee = (
(amount_to_stake - extrinsic_fee) if not proxy else amount_to_stake
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_=staking_address,
safe_staking_=safe_staking,
price_limit=price_with_tolerance,
)
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_=staking_address,
safe_staking_=safe_staking,
)
sim_swap = await subtensor.sim_swap(
origin_netuid=0,
destination_netuid=netuid,
amount=amount_minus_fee.rao,
row_extension = []
# TODO this should be asyncio gathered before the for loop
amount_minus_fee = (
(amount_to_stake - extrinsic_fee) if not proxy else amount_to_stake
)
sim_swap = await subtensor.sim_swap(
origin_netuid=0,
destination_netuid=netuid,
amount=amount_minus_fee.rao,
)
received_amount = sim_swap.alpha_amount
# Add rows for the table
base_row = [
str(netuid), # netuid
f"{staking_address}", # 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(sim_swap.tao_fee), # fee
str(extrinsic_fee),
# str(slippage_pct), # slippage
] + row_extension
rows.append(tuple(base_row))
operations.append(
(
netuid,
staking_address,
amount_to_stake,
current_stake_balance,
price_with_tolerance,
)
received_amount = sim_swap.alpha_amount
# 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(sim_swap.tao_fee), # fee
str(extrinsic_fee),
# str(slippage_pct), # slippage
] + row_extension
rows.append(tuple(base_row))
)

# Define and print stake table + slippage warning
table = _define_stake_table(wallet, subtensor, safe_staking, rate_tolerance)
Expand All @@ -467,23 +482,6 @@ async def stake_extrinsic(
if not unlock_key(wallet).success:
return

# Build the list of (netuid, hotkey, amount, current_stake, price_limit) tuples
# that describe each staking operation we need to perform.
# The zip aligns netuids with amounts/balances (which are populated per
# hotkey-netuid pair, but the zip truncates to len(netuids), matching the
# original execution order). Each netuid's amount/price applies to all hotkeys.
operations = []
if safe_staking:
for ni, am, curr, price in zip(
netuids, amounts_to_stake, current_stake_balances, prices_with_tolerance
):
for _, staking_address in hotkeys_to_stake_to:
operations.append((ni, staking_address, am, curr, price))
else:
for ni, am, curr in zip(netuids, amounts_to_stake, current_stake_balances):
for _, staking_address in hotkeys_to_stake_to:
operations.append((ni, staking_address, am, curr, None))

total_ops = len(operations)
use_batch = total_ops > 1

Expand Down
4 changes: 3 additions & 1 deletion bittensor_cli/src/commands/sudo.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,9 @@ async def set_hyperparameter_extrinsic(
)
elif sudo_ is RootSudoOnly.COMPLICATED:
if not prompt:
to_sudo_or_not_to_sudo = True # default to sudo true when no-prompt is set
# In no-prompt mode, owners should take the owner path; non-owners
# should default to sudo.
to_sudo_or_not_to_sudo = subnet_owner != coldkey_ss58
else:
to_sudo_or_not_to_sudo = confirm_action(
"This hyperparam can be executed as sudo or not. Do you want to execute as sudo [y] or not [n]?",
Expand Down
Loading
Loading