From 34328adf5045ef860b431e2b5f160d00bbe804e8 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 2 Apr 2026 20:24:01 +0200 Subject: [PATCH 01/72] Compatibility init --- bittensor_cli/src/bittensor/chain_data.py | 46 ++++--------------- .../src/bittensor/subtensor_interface.py | 3 +- bittensor_cli/src/bittensor/utils.py | 10 ++-- pyproject.toml | 2 +- 4 files changed, 15 insertions(+), 46 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 0e8f27c43..e172549fd 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -70,32 +70,6 @@ def _tbwu(val: int, netuid: Optional[int] = 0) -> Balance: return Balance.from_rao(val).set_unit(netuid) -def _chr_str(codes: tuple[int]) -> str: - """Converts a tuple of integer Unicode code points into a string.""" - return "".join(map(chr, codes)) - - -def process_nested( - data: Sequence[dict[Hashable, tuple[int]]] | dict | Any, - chr_transform: Callable[[tuple[int]], str], -) -> list[dict[Hashable, str]] | dict[Hashable, str] | Any: - """Processes nested data structures by applying a transformation function to their elements.""" - if isinstance(data, Sequence): - if len(data) > 0 and isinstance(data[0], dict): - return [ - {k: chr_transform(v) for k, v in item.items()} - if item is not None - else None - for item in data - ] - # TODO @abe why do we kind of silently fail here? - return {} - elif isinstance(data, dict): - return {k: chr_transform(v) for k, v in data.items()} - else: - return data - - @dataclass class AxonInfo: version: int @@ -695,14 +669,14 @@ class SubnetIdentity(InfoBase): @classmethod def _fix_decoded(cls, decoded: dict) -> "SubnetIdentity": return cls( - subnet_name=bytes(decoded["subnet_name"]).decode(), - github_repo=bytes(decoded["github_repo"]).decode(), - subnet_contact=bytes(decoded["subnet_contact"]).decode(), - subnet_url=bytes(decoded["subnet_url"]).decode(), - discord=bytes(decoded["discord"]).decode(), - description=bytes(decoded["description"]).decode(), - logo_url=bytes(decoded["logo_url"]).decode(), - additional=bytes(decoded["additional"]).decode(), + subnet_name=decoded["subnet_name"], + github_repo=decoded["github_repo"], + subnet_contact=decoded["subnet_contact"], + subnet_url=decoded["subnet_url"], + discord=decoded["discord"], + description=decoded["description"], + logo_url=decoded["logo_url"], + additional=decoded["additional"], ) @@ -1122,10 +1096,6 @@ def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo": # Name and symbol decoded.update({"name": bytes(decoded.get("name")).decode()}) decoded.update({"symbol": bytes(decoded.get("symbol")).decode()}) - for key in ["identities", "identity"]: - raw_data = decoded.get(key) - processed = process_nested(raw_data, _chr_str) - decoded.update({key: processed}) return cls( # Subnet index diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 6cb3f592a..9b4fe11e5 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2221,8 +2221,7 @@ async def get_all_coldkeys_claim_type( root_claim_types = {} for coldkey, claim_type_data in result.records: coldkey_ss58 = decode_account_id(coldkey[0]) - - claim_type_key = next(iter(claim_type_data.value.keys())) + claim_type_key = claim_type_data.value if claim_type_key == "KeepSubnets": subnets_data = claim_type_data.value["KeepSubnets"]["subnets"] diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index fe6c7c6b9..4657ee4c5 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -633,11 +633,11 @@ def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool return False -def decode_account_id(account_id_bytes: Union[tuple[int], tuple[tuple[int]]]): - if isinstance(account_id_bytes, tuple) and isinstance(account_id_bytes[0], tuple): - account_id_bytes = account_id_bytes[0] - # Convert the AccountId bytes to a Base64 string - return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) +def decode_account_id(account_id_bytes: str) -> str: + """ + Does nothing. Retained for compatibility til v10 + """ + return account_id_bytes def encode_account_id(ss58_address: str) -> bytes: diff --git a/pyproject.toml b/pyproject.toml index cf15c659d..786fdbb29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0", "rich>=13.7,<15.0", - "scalecodec==1.2.12", + "cyscale==0.1.5", "typer>=0.16", "typing_extensions>4.0.0; python_version<'3.11'", "bittensor-wallet==4.0.1", From a34def9cf7e2be67ff2960accd631c85c59101d3 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 2 Apr 2026 20:57:34 +0200 Subject: [PATCH 02/72] More stripping --- bittensor_cli/src/bittensor/chain_data.py | 18 ------------------ .../src/bittensor/subtensor_interface.py | 12 +++++------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index e172549fd..df81dc633 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -37,24 +37,6 @@ class ChainDataType(Enum): SubnetIdentity = 11 -def decode_hex_identity(info_dictionary): - decoded_info = {} - for k, v in info_dictionary.items(): - if isinstance(v, dict): - item = next(iter(v.values())) - else: - item = v - - if isinstance(item, tuple): - try: - decoded_info[k] = bytes(item).decode() - except UnicodeDecodeError: - print(f"Could not decode: {k}: {item}") - else: - decoded_info[k] = item - return decoded_info - - def process_stake_data(stake_data, netuid): decoded_stake_data = {} for account_id_bytes, stake_ in stake_data: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 9b4fe11e5..4ab14f304 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -24,7 +24,6 @@ NeuronInfo, SubnetHyperparameters, decode_account_id, - decode_hex_identity, DynamicInfo, SubnetState, MetagraphInfo, @@ -953,11 +952,10 @@ async def query_all_identities( reuse_block_hash=reuse_block, fully_exhaust=True, ) - all_identities = {} - for ss58_address, identity in identities.records: - all_identities[decode_account_id(ss58_address[0])] = decode_hex_identity( - identity.value - ) + all_identities = { + ss58_address: identity.value + for (ss58_address, identity) in identities.records + } return all_identities @@ -995,7 +993,7 @@ async def query_identity( if not identity_info: return {} try: - return decode_hex_identity(identity_info) + return identity_info except TypeError: return {} From f5ed67bba2135e6fd4bd237f213ed545b874473e Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 2 Apr 2026 21:02:14 +0200 Subject: [PATCH 03/72] More stripping --- bittensor_cli/src/bittensor/subtensor_interface.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 4ab14f304..75cc7540d 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1653,8 +1653,7 @@ async def get_stake_for_coldkeys( for result in results: if result is None: continue - for coldkey_bytes, stake_info_list in result: - coldkey_ss58 = decode_account_id(coldkey_bytes) + for coldkey_ss58, stake_info_list in result: stake_info_map[coldkey_ss58] = StakeInfo.list_from_any(stake_info_list) return stake_info_map if stake_info_map else None @@ -1719,8 +1718,7 @@ async def get_owned_hotkeys( block_hash=block_hash, reuse_block_hash=reuse_block, ) - - return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] + return owned_hotkeys async def get_extrinsic_fee( self, call: GenericCall, keypair: Keypair, proxy: Optional[str] = None From b9c5d839ccd251405c49093f943250cb6e31b713 Mon Sep 17 00:00:00 2001 From: bitloi Date: Fri, 3 Apr 2026 00:39:48 +0200 Subject: [PATCH 04/72] Fix stake add operation mapping for multi-target staking --- bittensor_cli/src/commands/stake/add.py | 236 ++++++++++++------------ tests/unit_tests/test_stake_add.py | 142 +++++++++++++- 2 files changed, 257 insertions(+), 121 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index d2bd7407d..f165bf3d6 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -339,13 +339,7 @@ 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. @@ -353,105 +347,126 @@ async def stake_extrinsic( 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) @@ -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 diff --git a/tests/unit_tests/test_stake_add.py b/tests/unit_tests/test_stake_add.py index 5537a1fa9..974f88353 100644 --- a/tests/unit_tests/test_stake_add.py +++ b/tests/unit_tests/test_stake_add.py @@ -1,12 +1,15 @@ from types import SimpleNamespace -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.commands.stake.add import stake_add -from tests.unit_tests.conftest import COLDKEY_SS58 as TEST_SS58 +from tests.unit_tests.conftest import ( + ALT_HOTKEY_SS58, + COLDKEY_SS58 as TEST_SS58, +) class MockSubnetInfo: @@ -105,3 +108,138 @@ async def test_stake_add_mixed_prices_including_zero_does_not_raise( assert mock_subtensor.substrate.compose_call.await_count == 2 assert mock_subtensor.sim_swap.await_count == 2 + + +@pytest.mark.asyncio +async def test_stake_add_multi_hotkey_multi_netuid_preserves_operation_mapping( + mock_wallet, + mock_subtensor, +): + mock_subtensor.sim_swap = _sim_swap_side_effect() + mock_subtensor.all_subnets.return_value = [ + MockSubnetInfo(netuid=427, price_tao=1.5), + MockSubnetInfo(netuid=1, price_tao=2.0), + ] + mock_subtensor.sign_and_send_batch_extrinsic = AsyncMock( + return_value=( + True, + "", + MagicMock(get_extrinsic_identifier=AsyncMock(return_value="0x1")), + ) + ) + + prompt_amounts = [ + (Balance.from_tao(1.0), False), + (Balance.from_tao(2.0), False), + (Balance.from_tao(3.0), False), + (Balance.from_tao(4.0), False), + ] + + with ( + patch( + "bittensor_cli.src.commands.stake.add._prompt_stake_amount", + side_effect=prompt_amounts, + ), + patch( + "bittensor_cli.src.commands.stake.add.unlock_key", + return_value=MagicMock(success=True), + ), + ): + await stake_add( + wallet=mock_wallet, + subtensor=mock_subtensor, + netuids=[427, 1], + stake_all=False, + amount=0, + prompt=False, + decline=False, + quiet=True, + all_hotkeys=False, + include_hotkeys=[TEST_SS58, ALT_HOTKEY_SS58], + exclude_hotkeys=[], + safe_staking=False, + rate_tolerance=0.05, + allow_partial_stake=True, + json_output=True, + era=16, + mev_protection=False, + proxy=None, + ) + + batched_stake_calls = [ + call + for call in mock_subtensor.substrate.compose_call.await_args_list + if call.kwargs.get("block_hash") == "0xabc123" + ] + + assert len(batched_stake_calls) == 4 + assert [ + ( + call.kwargs["call_params"]["hotkey"], + call.kwargs["call_params"]["netuid"], + call.kwargs["call_params"]["amount_staked"], + ) + for call in batched_stake_calls + ] == [ + (TEST_SS58, 427, Balance.from_tao(1.0).rao), + (TEST_SS58, 1, Balance.from_tao(2.0).rao), + (ALT_HOTKEY_SS58, 427, Balance.from_tao(3.0).rao), + (ALT_HOTKEY_SS58, 1, Balance.from_tao(4.0).rao), + ] + + +@pytest.mark.asyncio +async def test_stake_add_stake_all_distributes_across_all_operations( + mock_wallet, + mock_subtensor, +): + mock_subtensor.sim_swap = _sim_swap_side_effect() + mock_subtensor.all_subnets.return_value = [ + MockSubnetInfo(netuid=427, price_tao=1.5), + MockSubnetInfo(netuid=1, price_tao=2.0), + ] + mock_subtensor.sign_and_send_batch_extrinsic = AsyncMock( + return_value=( + True, + "", + MagicMock(get_extrinsic_identifier=AsyncMock(return_value="0x1")), + ) + ) + + with patch( + "bittensor_cli.src.commands.stake.add.unlock_key", + return_value=MagicMock(success=True), + ): + await stake_add( + wallet=mock_wallet, + subtensor=mock_subtensor, + netuids=[427, 1], + stake_all=True, + amount=0, + prompt=False, + decline=False, + quiet=True, + all_hotkeys=False, + include_hotkeys=[TEST_SS58, ALT_HOTKEY_SS58], + exclude_hotkeys=[], + safe_staking=False, + rate_tolerance=0.05, + allow_partial_stake=True, + json_output=True, + era=16, + mev_protection=False, + proxy=None, + ) + + batched_stake_calls = [ + call + for call in mock_subtensor.substrate.compose_call.await_args_list + if call.kwargs.get("block_hash") == "0xabc123" + ] + expected_amount = (Balance.from_tao(100) / 4).rao + + assert len(batched_stake_calls) == 4 + assert all( + call.kwargs["call_params"]["amount_staked"] == expected_amount + for call in batched_stake_calls + ) From 45e623ec4f0f37ece818d3935d49013cc44d6d32 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 2 Apr 2026 18:04:16 -0700 Subject: [PATCH 05/72] update stake mvmt and utils --- tests/e2e_tests/test_stake_movement.py | 3 +-- tests/e2e_tests/utils.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py index 5a34aec4f..16ba309a2 100644 --- a/tests/e2e_tests/test_stake_movement.py +++ b/tests/e2e_tests/test_stake_movement.py @@ -530,8 +530,7 @@ def test_stake_movement(local_chain, wallet_setup): "--chain", "ws://127.0.0.1:9945", "--no-prompt", - "--rate-tolerance", - "0.1", + "--unsafe", ], ) assert "✅ Sent" in swap_with_limit_result.stdout, swap_with_limit_result.stderr diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 9f65759ce..b67ae0a63 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -202,7 +202,7 @@ def validate_wallet_overview( pattern += rf"{hotkey}\s+" # HOTKEY pattern += rf"{uid}\s+" # UID pattern += r"True\s+" # ACTIVE - pattern += r"[\d.]+\s+" # STAKE + pattern += r"[\d.,]+[k]?\s+" # STAKE pattern += r"[\d.]+\s+" # RANK pattern += r"[\d.]+\s+" # TRUST pattern += r"[\d.]+\s+" # CONSENSUS From 3f134936dafb6d55c8d0f21fe9e0dfc536aa8437 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 2 Apr 2026 18:04:44 -0700 Subject: [PATCH 06/72] update stake selection for parsing --- tests/e2e_tests/test_staking_sudo.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index e76ff1627..ec4c7786b 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -1,6 +1,5 @@ import asyncio import json -import re import pytest from typing import Union @@ -397,14 +396,7 @@ def test_staking(local_chain, wallet_setup): "--verbose", ], ) - - # Assert correct stake is added - cleaned_stake = [ - re.sub(r"\s+", " ", line) - for line in show_stake_adding_single.stdout.splitlines() - ] - stake_added = cleaned_stake[8].split("│")[3].strip().split()[0] - assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(87) + assert str(netuid) in show_stake_adding_single.stdout show_stake_json = exec_command_alice( command="stake", @@ -420,8 +412,17 @@ def test_staking(local_chain, wallet_setup): ], ) show_stake_json_output = json.loads(show_stake_json.stdout) - alice_stake = show_stake_json_output["stake_info"][keypair_alice.ss58_address][0] - assert Balance.from_tao(alice_stake["stake_value"]) >= Balance.from_tao(87.0) + alice_stakes = show_stake_json_output["stake_info"][keypair_alice.ss58_address] + alice_on_netuid = next( + (stake for stake in alice_stakes if stake["netuid"] == netuid), None + ) + assert alice_on_netuid is not None, ( + f"No stake row for netuid {netuid} in JSON output" + ) + assert Balance.from_tao(float(alice_on_netuid["stake_value"])) >= Balance.from_tao( + 87.0 + ) + remove_amount = float(alice_on_netuid["stake_value"]) - 1.0 # Execute remove_stake command and remove all alpha stakes from Alice's wallet remove_stake = exec_command_alice( @@ -439,7 +440,7 @@ def test_staking(local_chain, wallet_setup): "--chain", "ws://127.0.0.1:9945", "--amount", - str(float(stake_added) - 1), + str(remove_amount), "--tolerance", "0.1", "--partial", From 58d32f69316b488ef6a8ac8894125560cb3dde27 Mon Sep 17 00:00:00 2001 From: bitloi Date: Mon, 6 Apr 2026 02:59:53 +0200 Subject: [PATCH 07/72] Allow max_burn updates for subnet owners and root --- bittensor_cli/src/__init__.py | 4 +- bittensor_cli/src/commands/sudo.py | 4 +- tests/e2e_tests/test_staking_sudo.py | 2 + tests/unit_tests/test_hyperparams.py | 9 ++ .../test_sudo_hyperparameter_permissions.py | 122 ++++++++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/test_sudo_hyperparameter_permissions.py diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 95a4d3c39..50ab2e35a 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -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), @@ -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": { diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 4f37d1613..9a7136729 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -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]?", diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index ec4c7786b..6f27a5bf7 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -543,6 +543,7 @@ def line(key: str) -> Union[str, bool]: assert "description" in max_burn_param, "Missing description for max_burn" assert "side_effects" in max_burn_param, "Missing side_effects for max_burn" assert "owner_settable" in max_burn_param, "Missing owner_settable for max_burn" + assert max_burn_param["owner_settable"] is True assert "docs_link" in max_burn_param, "Missing docs_link for max_burn" max_burn_tao_from_json = max_burn_param["value"] assert Balance.from_rao(max_burn_tao_from_json) == Balance.from_tao(100.0) @@ -626,6 +627,7 @@ def line(key: str) -> Union[str, bool]: assert "owner_settable" in max_burn_updated, ( "Missing owner_settable for max_burn after update" ) + assert max_burn_updated["owner_settable"] is True assert "docs_link" in max_burn_updated, ( "Missing docs_link for max_burn after update" ) diff --git a/tests/unit_tests/test_hyperparams.py b/tests/unit_tests/test_hyperparams.py index 8f6b42647..43f23a599 100644 --- a/tests/unit_tests/test_hyperparams.py +++ b/tests/unit_tests/test_hyperparams.py @@ -38,3 +38,12 @@ def test_new_hyperparams_have_metadata(): def test_new_hyperparams_owner_settable_true(): for key in NEW_HYPERPARAMS_826: assert HYPERPARAMS_METADATA[key]["owner_settable"] is True + + +def test_max_burn_is_owner_or_root_settable(): + _, root_only = HYPERPARAMS["max_burn"] + assert root_only is RootSudoOnly.COMPLICATED + + +def test_max_burn_metadata_owner_settable_true(): + assert HYPERPARAMS_METADATA["max_burn"]["owner_settable"] is True diff --git a/tests/unit_tests/test_sudo_hyperparameter_permissions.py b/tests/unit_tests/test_sudo_hyperparameter_permissions.py new file mode 100644 index 000000000..ede1a188e --- /dev/null +++ b/tests/unit_tests/test_sudo_hyperparameter_permissions.py @@ -0,0 +1,122 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock, patch + +from tests.unit_tests.conftest import COLDKEY_SS58 + + +MODULE = "bittensor_cli.src.commands.sudo" + + +def _receipt() -> MagicMock: + receipt = MagicMock() + receipt.get_extrinsic_identifier = AsyncMock(return_value="0xabc-1") + return receipt + + +@pytest.mark.asyncio +async def test_max_burn_no_prompt_owner_uses_owner_path(mock_wallet, mock_subtensor): + from bittensor_cli.src.commands.sudo import set_hyperparameter_extrinsic + + direct_call = MagicMock(name="direct_call") + mock_subtensor.query = AsyncMock(return_value=COLDKEY_SS58) + mock_subtensor.substrate.metadata = MagicMock() + mock_subtensor.substrate.get_metadata_call_function = AsyncMock( + return_value={"fields": [{"name": "netuid"}, {"name": "max_burn"}]} + ) + mock_subtensor.substrate.compose_call = AsyncMock(return_value=direct_call) + mock_subtensor.sign_and_send_extrinsic = AsyncMock( + return_value=(True, "", _receipt()) + ) + + with ( + patch(f"{MODULE}.unlock_key", return_value=MagicMock(success=True)), + patch(f"{MODULE}.requires_bool", return_value=False), + patch(f"{MODULE}.print_extrinsic_id", new_callable=AsyncMock), + ): + success, err_msg, ext_id = await set_hyperparameter_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=1, + proxy=None, + parameter="max_burn", + value="10000000000", + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=False, + ) + + assert success is True + assert err_msg == "" + assert ext_id == "0xabc-1" + mock_subtensor.substrate.compose_call.assert_awaited_once_with( + call_module="AdminUtils", + call_function="sudo_set_max_burn", + call_params={"netuid": 1, "max_burn": "10000000000"}, + ) + mock_subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + direct_call, + mock_wallet, + False, + False, + proxy=None, + ) + + +@pytest.mark.asyncio +async def test_max_burn_no_prompt_non_owner_uses_sudo_path(mock_wallet, mock_subtensor): + from bittensor_cli.src.commands.sudo import set_hyperparameter_extrinsic + + direct_call = MagicMock(name="direct_call") + sudo_call = MagicMock(name="sudo_call") + mock_subtensor.query = AsyncMock( + return_value="5FLSigC9H8M5Xo6z8xN7f6cXnHboRcgk4v6R7zDNz6w5jN3q" + ) + mock_subtensor.substrate.metadata = MagicMock() + mock_subtensor.substrate.get_metadata_call_function = AsyncMock( + return_value={"fields": [{"name": "netuid"}, {"name": "max_burn"}]} + ) + mock_subtensor.substrate.compose_call = AsyncMock( + side_effect=[direct_call, sudo_call] + ) + mock_subtensor.sign_and_send_extrinsic = AsyncMock( + return_value=(True, "", _receipt()) + ) + + with ( + patch(f"{MODULE}.unlock_key", return_value=MagicMock(success=True)), + patch(f"{MODULE}.requires_bool", return_value=False), + patch(f"{MODULE}.print_extrinsic_id", new_callable=AsyncMock), + ): + success, err_msg, ext_id = await set_hyperparameter_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=1, + proxy=None, + parameter="max_burn", + value="10000000000", + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=False, + ) + + assert success is True + assert err_msg == "" + assert ext_id == "0xabc-1" + assert mock_subtensor.substrate.compose_call.await_count == 2 + assert mock_subtensor.substrate.compose_call.await_args_list[0].kwargs == { + "call_module": "AdminUtils", + "call_function": "sudo_set_max_burn", + "call_params": {"netuid": 1, "max_burn": "10000000000"}, + } + assert mock_subtensor.substrate.compose_call.await_args_list[1].kwargs == { + "call_module": "Sudo", + "call_function": "sudo", + "call_params": {"call": direct_call}, + } + mock_subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + sudo_call, + mock_wallet, + False, + False, + proxy=None, + ) From 8ed0d4a5a5921e280206780713fc0c6b272f63a5 Mon Sep 17 00:00:00 2001 From: bitloi Date: Mon, 6 Apr 2026 03:48:28 +0200 Subject: [PATCH 08/72] Add interactive coverage for max_burn permission paths --- .../test_sudo_hyperparameter_permissions.py | 126 +++++++++++++++--- 1 file changed, 111 insertions(+), 15 deletions(-) diff --git a/tests/unit_tests/test_sudo_hyperparameter_permissions.py b/tests/unit_tests/test_sudo_hyperparameter_permissions.py index ede1a188e..f975e5446 100644 --- a/tests/unit_tests/test_sudo_hyperparameter_permissions.py +++ b/tests/unit_tests/test_sudo_hyperparameter_permissions.py @@ -5,16 +5,13 @@ MODULE = "bittensor_cli.src.commands.sudo" - - -def _receipt() -> MagicMock: - receipt = MagicMock() - receipt.get_extrinsic_identifier = AsyncMock(return_value="0xabc-1") - return receipt +NON_OWNER_SS58 = "5FLSigC9H8M5Xo6z8xN7f6cXnHboRcgk4v6R7zDNz6w5jN3q" @pytest.mark.asyncio -async def test_max_burn_no_prompt_owner_uses_owner_path(mock_wallet, mock_subtensor): +async def test_max_burn_no_prompt_owner_uses_owner_path( + mock_wallet, mock_subtensor, successful_receipt +): from bittensor_cli.src.commands.sudo import set_hyperparameter_extrinsic direct_call = MagicMock(name="direct_call") @@ -25,7 +22,7 @@ async def test_max_burn_no_prompt_owner_uses_owner_path(mock_wallet, mock_subten ) mock_subtensor.substrate.compose_call = AsyncMock(return_value=direct_call) mock_subtensor.sign_and_send_extrinsic = AsyncMock( - return_value=(True, "", _receipt()) + return_value=(True, "", successful_receipt) ) with ( @@ -47,7 +44,7 @@ async def test_max_burn_no_prompt_owner_uses_owner_path(mock_wallet, mock_subten assert success is True assert err_msg == "" - assert ext_id == "0xabc-1" + assert ext_id == "0x123-1" mock_subtensor.substrate.compose_call.assert_awaited_once_with( call_module="AdminUtils", call_function="sudo_set_max_burn", @@ -63,14 +60,14 @@ async def test_max_burn_no_prompt_owner_uses_owner_path(mock_wallet, mock_subten @pytest.mark.asyncio -async def test_max_burn_no_prompt_non_owner_uses_sudo_path(mock_wallet, mock_subtensor): +async def test_max_burn_no_prompt_non_owner_uses_sudo_path( + mock_wallet, mock_subtensor, successful_receipt +): from bittensor_cli.src.commands.sudo import set_hyperparameter_extrinsic direct_call = MagicMock(name="direct_call") sudo_call = MagicMock(name="sudo_call") - mock_subtensor.query = AsyncMock( - return_value="5FLSigC9H8M5Xo6z8xN7f6cXnHboRcgk4v6R7zDNz6w5jN3q" - ) + mock_subtensor.query = AsyncMock(return_value=NON_OWNER_SS58) mock_subtensor.substrate.metadata = MagicMock() mock_subtensor.substrate.get_metadata_call_function = AsyncMock( return_value={"fields": [{"name": "netuid"}, {"name": "max_burn"}]} @@ -79,7 +76,7 @@ async def test_max_burn_no_prompt_non_owner_uses_sudo_path(mock_wallet, mock_sub side_effect=[direct_call, sudo_call] ) mock_subtensor.sign_and_send_extrinsic = AsyncMock( - return_value=(True, "", _receipt()) + return_value=(True, "", successful_receipt) ) with ( @@ -101,7 +98,7 @@ async def test_max_burn_no_prompt_non_owner_uses_sudo_path(mock_wallet, mock_sub assert success is True assert err_msg == "" - assert ext_id == "0xabc-1" + assert ext_id == "0x123-1" assert mock_subtensor.substrate.compose_call.await_count == 2 assert mock_subtensor.substrate.compose_call.await_args_list[0].kwargs == { "call_module": "AdminUtils", @@ -120,3 +117,102 @@ async def test_max_burn_no_prompt_non_owner_uses_sudo_path(mock_wallet, mock_sub False, proxy=None, ) + + +@pytest.mark.asyncio +async def test_max_burn_interactive_owner_chooses_non_sudo_path( + mock_wallet, mock_subtensor, successful_receipt +): + from bittensor_cli.src.commands.sudo import set_hyperparameter_extrinsic + + direct_call = MagicMock(name="direct_call") + mock_subtensor.query = AsyncMock(return_value=COLDKEY_SS58) + mock_subtensor.substrate.metadata = MagicMock() + mock_subtensor.substrate.get_metadata_call_function = AsyncMock( + return_value={"fields": [{"name": "netuid"}, {"name": "max_burn"}]} + ) + mock_subtensor.substrate.compose_call = AsyncMock(return_value=direct_call) + mock_subtensor.sign_and_send_extrinsic = AsyncMock( + return_value=(True, "", successful_receipt) + ) + + with ( + patch(f"{MODULE}.unlock_key", return_value=MagicMock(success=True)), + patch(f"{MODULE}.requires_bool", return_value=False), + patch(f"{MODULE}.confirm_action", return_value=False), + patch(f"{MODULE}.print_extrinsic_id", new_callable=AsyncMock), + ): + success, err_msg, ext_id = await set_hyperparameter_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=1, + proxy=None, + parameter="max_burn", + value="10000000000", + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=True, + decline=False, + quiet=True, + ) + + assert success is True + assert err_msg == "" + assert ext_id == "0x123-1" + mock_subtensor.substrate.compose_call.assert_awaited_once_with( + call_module="AdminUtils", + call_function="sudo_set_max_burn", + call_params={"netuid": 1, "max_burn": "10000000000"}, + ) + mock_subtensor.sign_and_send_extrinsic.assert_awaited_once_with( + direct_call, + mock_wallet, + False, + False, + proxy=None, + ) + + +@pytest.mark.asyncio +async def test_max_burn_interactive_non_owner_chooses_non_sudo_errors( + mock_wallet, mock_subtensor +): + from bittensor_cli.src.commands.sudo import set_hyperparameter_extrinsic + + direct_call = MagicMock(name="direct_call") + mock_subtensor.query = AsyncMock(return_value=NON_OWNER_SS58) + mock_subtensor.substrate.metadata = MagicMock() + mock_subtensor.substrate.get_metadata_call_function = AsyncMock( + return_value={"fields": [{"name": "netuid"}, {"name": "max_burn"}]} + ) + mock_subtensor.substrate.compose_call = AsyncMock(return_value=direct_call) + mock_subtensor.sign_and_send_extrinsic = AsyncMock() + + with ( + patch(f"{MODULE}.unlock_key", return_value=MagicMock(success=True)), + patch(f"{MODULE}.requires_bool", return_value=False), + patch(f"{MODULE}.confirm_action", return_value=False), + ): + success, err_msg, ext_id = await set_hyperparameter_extrinsic( + subtensor=mock_subtensor, + wallet=mock_wallet, + netuid=1, + proxy=None, + parameter="max_burn", + value="10000000000", + wait_for_inclusion=False, + wait_for_finalization=False, + prompt=True, + decline=False, + quiet=True, + ) + + assert success is False + assert err_msg == "This wallet doesn't own the specified subnet." + assert ext_id is None + mock_subtensor.substrate.compose_call.assert_awaited_once_with( + call_module="AdminUtils", + call_function="sudo_set_max_burn", + call_params={"netuid": 1, "max_burn": "10000000000"}, + ) + mock_subtensor.sign_and_send_extrinsic.assert_not_awaited() From e05de204bf3d06d2a06d00ac10b07bd074aa8af9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 6 Apr 2026 11:14:15 -0700 Subject: [PATCH 09/72] stop dest having staked hotkeys --- bittensor_cli/src/commands/wallets.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 68da4a5fa..c4d7fea36 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2232,11 +2232,32 @@ async def announce_coldkey_swap( return False # Proceed with the announcement - swap_cost, delay = await asyncio.gather( + swap_cost, delay, dest_staking_hotkeys = await asyncio.gather( subtensor.get_coldkey_swap_cost(block_hash=block_hash), subtensor.get_coldkey_swap_announcement_delay(block_hash=block_hash), + subtensor.get_staking_hotkeys(new_coldkey_ss58, block_hash=block_hash), ) + if dest_staking_hotkeys: + print_error( + "Destination coldkey cannot have any staking hotkeys. " + "Please use a new coldkey for the swap." + ) + identity_map = await subtensor.fetch_coldkey_hotkey_identities( + block_hash=block_hash + ) + hk_table = create_key_value_table( + f"Staking Hotkeys Associated with Destination Coldkey \n Count: ({len(dest_staking_hotkeys)})\n" + ) + for hk_ss58 in dest_staking_hotkeys: + hk_name = get_hotkey_identity_name(identity_map, hk_ss58) or "~" + hk_table.add_row( + f"[{COLORS.G.HK}]{hk_ss58}[/{COLORS.G.HK}]", + f"[{COLORS.G.CK}]{hk_name}[/{COLORS.G.CK}]", + ) + console.print(hk_table) + return False + table = create_key_value_table("Announcing Coldkey Swap\n") table.add_row( "Current Coldkey", From 13b5cbfd944132334a8b0f55b74983bc48a669c3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 6 Apr 2026 11:14:38 -0700 Subject: [PATCH 10/72] ability to define titles in kv tables --- bittensor_cli/src/commands/wallets.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index c4d7fea36..85abee04f 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1874,23 +1874,29 @@ async def swap_hotkey( return result -def create_key_value_table(title: str = "Details") -> Table: +def create_key_value_table( + title: str = "Details", + key_label: str = "Item", + value_label: str = "Value", +) -> Table: """Creates a key-value table for displaying information for various cmds. Args: title: The title shown above the table. + key_label: The header for the key column. + value_label: The header for the value column. Returns: A Rich Table for key-value display. """ return Table( Column( - "Item", + key_label, justify="right", style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], no_wrap=True, ), - Column("Value", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]), + Column(value_label, style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]), title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{title}", show_footer=True, show_edge=False, From 5050eae3c4cad0be7ad550611778b252f8e93a1e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 6 Apr 2026 11:15:04 -0700 Subject: [PATCH 11/72] update kv table for staking hotkeys --- bittensor_cli/src/commands/wallets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 85abee04f..5c16b9733 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2253,7 +2253,9 @@ async def announce_coldkey_swap( block_hash=block_hash ) hk_table = create_key_value_table( - f"Staking Hotkeys Associated with Destination Coldkey \n Count: ({len(dest_staking_hotkeys)})\n" + f"Staking Hotkeys Associated with Destination Coldkey \n Count: ({len(dest_staking_hotkeys)})\n", + key_label="Hotkey", + value_label="Identity", ) for hk_ss58 in dest_staking_hotkeys: hk_name = get_hotkey_identity_name(identity_map, hk_ss58) or "~" From b593bf27bf5dfc1f8f25669819c1d01d52398374 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 6 Apr 2026 16:50:21 -0700 Subject: [PATCH 12/72] Log SHA checksums --- .github/workflows/release.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c10dfeef..0d57dac3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,13 +53,13 @@ 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 @@ -67,6 +67,14 @@ jobs: 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: From 0bc801502666ff85a54db2828fad2ec8ccc78222 Mon Sep 17 00:00:00 2001 From: bitloi <89318445+bitloi@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:37:30 -0300 Subject: [PATCH 13/72] Merge pull request #908 from bitloi/fix/issue-crowdloan-proxy-creator-checks fix(crowdloan): normalize creator checks for proxy actors --- bittensor_cli/src/commands/crowd/create.py | 3 +- bittensor_cli/src/commands/crowd/dissolve.py | 2 +- bittensor_cli/src/commands/crowd/update.py | 2 +- .../test_crowd_proxy_creator_checks.py | 133 ++++++++++++++++++ 4 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/test_crowd_proxy_creator_checks.py diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 2195ecda9..2afa15695 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -586,7 +586,8 @@ async def finalize_crowdloan( print_error(error_msg) return False, error_msg - if wallet.coldkeypub.ss58_address != crowdloan.creator: + creator_address = proxy or wallet.coldkeypub.ss58_address + if creator_address != crowdloan.creator: error_msg = ( f"Only the creator can finalize a crowdloan. Creator: {crowdloan.creator}" ) diff --git a/bittensor_cli/src/commands/crowd/dissolve.py b/bittensor_cli/src/commands/crowd/dissolve.py index de134d964..6a1d657f9 100644 --- a/bittensor_cli/src/commands/crowd/dissolve.py +++ b/bittensor_cli/src/commands/crowd/dissolve.py @@ -50,7 +50,7 @@ async def dissolve_crowdloan( tuple[bool, str]: Success status and message. """ - creator_ss58 = wallet.coldkeypub.ss58_address + creator_ss58 = proxy or wallet.coldkeypub.ss58_address crowdloan, current_block = await asyncio.gather( subtensor.get_single_crowdloan(crowdloan_id), diff --git a/bittensor_cli/src/commands/crowd/update.py b/bittensor_cli/src/commands/crowd/update.py index 6abefed55..6700770b2 100644 --- a/bittensor_cli/src/commands/crowd/update.py +++ b/bittensor_cli/src/commands/crowd/update.py @@ -88,7 +88,7 @@ async def update_crowdloan( print_error(f"[red]{error_msg}[/red]") return False, f"Crowdloan #{crowdloan_id} is already finalized." - creator_address = wallet.coldkeypub.ss58_address + creator_address = proxy or wallet.coldkeypub.ss58_address if creator_address != crowdloan.creator: error_msg = "Only the creator can update this crowdloan." if json_output: diff --git a/tests/unit_tests/test_crowd_proxy_creator_checks.py b/tests/unit_tests/test_crowd_proxy_creator_checks.py new file mode 100644 index 000000000..2d44cf841 --- /dev/null +++ b/tests/unit_tests/test_crowd_proxy_creator_checks.py @@ -0,0 +1,133 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from bittensor_cli.src.bittensor.balances import Balance +from tests.unit_tests.conftest import COLDKEY_SS58, PROXY_SS58 + + +def _make_crowdloan( + creator: str, + *, + finalized: bool = False, + raised_tao: float = 5.0, + cap_tao: float = 10.0, +) -> MagicMock: + crowdloan = MagicMock() + crowdloan.creator = creator + crowdloan.finalized = finalized + crowdloan.raised = Balance.from_tao(raised_tao) + crowdloan.cap = Balance.from_tao(cap_tao) + return crowdloan + + +@pytest.mark.asyncio +async def test_finalize_crowdloan_allows_proxy_creator_actor( + mock_wallet, mock_subtensor +): + from bittensor_cli.src.commands.crowd.create import finalize_crowdloan + + mock_subtensor.get_single_crowdloan = AsyncMock( + return_value=_make_crowdloan(creator=PROXY_SS58) + ) + mock_subtensor.substrate.get_block_number = AsyncMock(return_value=12345) + + result = await finalize_crowdloan( + subtensor=mock_subtensor, + wallet=mock_wallet, + proxy=PROXY_SS58, + crowdloan_id=7, + wait_for_inclusion=True, + wait_for_finalization=False, + prompt=False, + json_output=False, + ) + + assert result == (False, "Crowdloan has not reached its cap.") + + +@pytest.mark.asyncio +async def test_finalize_crowdloan_rejects_non_creator_proxy_actor( + mock_wallet, mock_subtensor +): + from bittensor_cli.src.commands.crowd.create import finalize_crowdloan + + mock_subtensor.get_single_crowdloan = AsyncMock( + return_value=_make_crowdloan(creator=COLDKEY_SS58) + ) + mock_subtensor.substrate.get_block_number = AsyncMock(return_value=12345) + + result = await finalize_crowdloan( + subtensor=mock_subtensor, + wallet=mock_wallet, + proxy=PROXY_SS58, + crowdloan_id=7, + wait_for_inclusion=True, + wait_for_finalization=False, + prompt=False, + json_output=False, + ) + + assert result == (False, "Only the creator can finalize a crowdloan.") + + +@pytest.mark.asyncio +async def test_update_crowdloan_allows_proxy_creator_actor(mock_wallet, mock_subtensor): + from bittensor_cli.src.commands.crowd.update import update_crowdloan + + mock_subtensor.get_single_crowdloan = AsyncMock( + return_value=_make_crowdloan(creator=PROXY_SS58) + ) + mock_subtensor.substrate.get_chain_head = AsyncMock(return_value="0xhead") + mock_subtensor.substrate.get_block_number = AsyncMock(return_value=12345) + mock_subtensor.substrate.init_runtime = AsyncMock(return_value=MagicMock()) + + with ( + patch( + "bittensor_cli.src.commands.crowd.update.get_constant", + new_callable=AsyncMock, + side_effect=[Balance.from_tao(1).rao, 1, 1000], + ), + patch( + "bittensor_cli.src.commands.crowd.update.show_crowdloan_details", + new_callable=AsyncMock, + ), + ): + result = await update_crowdloan( + subtensor=mock_subtensor, + wallet=mock_wallet, + proxy=PROXY_SS58, + crowdloan_id=9, + min_contribution=None, + end=None, + cap=None, + prompt=False, + json_output=False, + ) + + assert result == (False, "No update parameter specified.") + + +@pytest.mark.asyncio +async def test_dissolve_crowdloan_allows_proxy_creator_actor( + mock_wallet, mock_subtensor +): + from bittensor_cli.src.commands.crowd.dissolve import dissolve_crowdloan + + crowdloan = _make_crowdloan(creator=PROXY_SS58, raised_tao=12.0, cap_tao=20.0) + mock_subtensor.get_single_crowdloan = AsyncMock(return_value=crowdloan) + mock_subtensor.substrate.get_block_number = AsyncMock(return_value=12345) + mock_subtensor.get_crowdloan_contribution = AsyncMock( + return_value=Balance.from_tao(1.0) + ) + + result = await dissolve_crowdloan( + subtensor=mock_subtensor, + wallet=mock_wallet, + proxy=PROXY_SS58, + crowdloan_id=11, + prompt=False, + json_output=False, + ) + + assert result == (False, "Crowdloan not ready to dissolve.") From 4e208a7cdbd7386010aeef4fea317c8d5bf624fa Mon Sep 17 00:00:00 2001 From: bitloi Date: Tue, 7 Apr 2026 14:53:43 +0200 Subject: [PATCH 14/72] Fix cross-subnet received amount display using sim_swap and add missing proxy check --- bittensor_cli/src/commands/stake/move.py | 22 +- tests/unit_tests/test_stake_move_display.py | 277 ++++++++++++++++++++ 2 files changed, 292 insertions(+), 7 deletions(-) create mode 100644 tests/unit_tests/test_stake_move_display.py diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index f618641f7..dea2c42c3 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface - from bittensor_cli.src.bittensor.chain_data import DynamicInfo + from bittensor_cli.src.bittensor.chain_data import DynamicInfo, SimSwapResult MIN_STAKE_FEE = Balance.from_rao(50_000) @@ -99,6 +99,7 @@ async def display_stake_movement_cross_subnets( destination_hotkey: str, amount_to_move: Balance, pricing: MovementPricing, + sim_swap: "SimSwapResult", stake_fee: Balance, extrinsic_fee: Balance, safe_staking: bool = False, @@ -116,6 +117,7 @@ async def display_stake_movement_cross_subnets( destination_hotkey: The destination hotkey SS58 address. amount_to_move: The amount of stake to move/swap. pricing: Pricing information including rates and limits. + sim_swap: SimSwapResult from the runtime API with accurate swap amounts. stake_fee: The fee for the stake transaction. extrinsic_fee: The fee for the extrinsic execution. safe_staking: Whether to enable safe staking. @@ -147,12 +149,15 @@ async def display_stake_movement_cross_subnets( + f"({Balance.get_unit(0)}/{Balance.get_unit(origin_netuid)})" ) else: - dynamic_origin = pricing.origin_subnet - dynamic_destination = pricing.destination_subnet - 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) + if destination_netuid == 0: + received_amount = sim_swap.tao_amount + else: + received_amount = sim_swap.alpha_amount + if not proxy: + extrinsic_fee_as_alpha = pricing.destination_subnet.tao_to_alpha( + extrinsic_fee + ) + received_amount = received_amount - extrinsic_fee_as_alpha received_amount.set_unit(destination_netuid) if received_amount < Balance.from_tao(0).set_unit(destination_netuid): @@ -670,6 +675,7 @@ async def move_stake( destination_hotkey=destination_hotkey, amount_to_move=amount_to_move_as_balance, pricing=pricing, + sim_swap=sim_swap, stake_fee=sim_swap.alpha_fee if origin_netuid != 0 else sim_swap.tao_fee, @@ -889,6 +895,7 @@ async def transfer_stake( destination_hotkey=origin_hotkey, amount_to_move=amount_to_transfer, pricing=pricing, + sim_swap=sim_swap, stake_fee=sim_swap.alpha_fee if origin_netuid != 0 else sim_swap.tao_fee, @@ -1116,6 +1123,7 @@ async def swap_stake( destination_hotkey=hotkey_ss58, amount_to_move=amount_to_swap, pricing=pricing, + sim_swap=sim_swap, stake_fee=sim_swap.alpha_fee if origin_netuid != 0 else sim_swap.tao_fee, diff --git a/tests/unit_tests/test_stake_move_display.py b/tests/unit_tests/test_stake_move_display.py new file mode 100644 index 000000000..d67954dc3 --- /dev/null +++ b/tests/unit_tests/test_stake_move_display.py @@ -0,0 +1,277 @@ +""" +Unit tests for display_stake_movement_cross_subnets in stake/move.py. + +Covers: + - Cross-subnet received amount uses sim_swap.alpha_amount (not linear math) + - Cross-subnet with proxy does not deduct extrinsic fee from received + - Cross-subnet without proxy deducts extrinsic fee from received + - Same-subnet still uses existing linear pricing + - Cross-subnet raises ValueError when received amount is negative +""" + +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.commands.stake.move import ( + display_stake_movement_cross_subnets, + MovementPricing, +) + +MODULE = "bittensor_cli.src.commands.stake.move" + + +def _make_subnet(netuid: int, price_tao: float): + """Build a mock DynamicInfo with working alpha_to_tao / tao_to_alpha.""" + subnet = MagicMock() + subnet.price = Balance.from_tao(price_tao) + subnet.is_dynamic = netuid != 0 + subnet.netuid = netuid + + def alpha_to_tao(alpha: Balance) -> Balance: + return Balance.from_tao(alpha.tao * price_tao) + + def tao_to_alpha(tao: Balance) -> Balance: + if price_tao == 0: + return Balance.from_tao(0) + return Balance.from_tao(tao.tao / price_tao).set_unit(netuid) + + subnet.alpha_to_tao = alpha_to_tao + subnet.tao_to_alpha = tao_to_alpha + return subnet + + +def _make_sim_swap( + alpha_amount_tao: float, + dest_netuid: int, + alpha_fee_tao: float = 1.0, + tao_amount_tao: float = None, +): + """Build a SimpleNamespace matching SimSwapResult shape.""" + if tao_amount_tao is None: + tao_amount_tao = alpha_amount_tao + return SimpleNamespace( + alpha_amount=Balance.from_tao(alpha_amount_tao).set_unit(dest_netuid), + tao_amount=Balance.from_tao(tao_amount_tao), + alpha_fee=Balance.from_tao(alpha_fee_tao).set_unit(dest_netuid), + tao_fee=Balance.from_tao(alpha_fee_tao), + ) + + +# --------------------------------------------------------------------------- +# Cross-subnet tests +# --------------------------------------------------------------------------- + + +class TestCrossSubnetDisplay: + @pytest.mark.asyncio + async def test_received_amount_uses_sim_swap_not_linear_math(self): + """The cross-subnet received amount must come from sim_swap.alpha_amount, + not from linear alpha_to_tao/tao_to_alpha calculations.""" + origin_netuid, dest_netuid = 1, 2 + # Price deliberately set so linear math would give a very different result + origin_subnet = _make_subnet(origin_netuid, price_tao=2.0) + dest_subnet = _make_subnet(dest_netuid, price_tao=0.5) + pricing = MovementPricing( + origin_subnet=origin_subnet, + destination_subnet=dest_subnet, + rate=4.0, + rate_with_tolerance=None, + ) + amount = Balance.from_tao(10.0).set_unit(origin_netuid) + stake_fee = Balance.from_tao(0.5).set_unit(origin_netuid) + extrinsic_fee = Balance.from_tao(0.0) + # sim_swap says user receives 35 alpha on dest — linear math would give ~38 + sim_swap = _make_sim_swap(alpha_amount_tao=35.0, dest_netuid=dest_netuid) + + with patch(f"{MODULE}.console"): + received, _ = await display_stake_movement_cross_subnets( + subtensor=MagicMock(network="test"), + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + origin_hotkey="5C" + "a" * 46, + destination_hotkey="5C" + "b" * 46, + amount_to_move=amount, + pricing=pricing, + sim_swap=sim_swap, + stake_fee=stake_fee, + extrinsic_fee=extrinsic_fee, + proxy="5C" + "p" * 46, # proxy → no extrinsic_fee deduction + ) + + assert received.tao == pytest.approx(35.0, abs=1e-6) + + @pytest.mark.asyncio + async def test_proxy_does_not_deduct_extrinsic_fee(self): + """With a proxy, the extrinsic fee should not reduce the received amount.""" + origin_netuid, dest_netuid = 1, 2 + dest_subnet = _make_subnet(dest_netuid, price_tao=1.0) + pricing = MovementPricing( + origin_subnet=_make_subnet(origin_netuid, price_tao=1.0), + destination_subnet=dest_subnet, + rate=1.0, + rate_with_tolerance=None, + ) + sim_swap = _make_sim_swap(alpha_amount_tao=50.0, dest_netuid=dest_netuid) + extrinsic_fee = Balance.from_tao(0.5) + + with patch(f"{MODULE}.console"): + received, _ = await display_stake_movement_cross_subnets( + subtensor=MagicMock(network="test"), + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + origin_hotkey="5C" + "a" * 46, + destination_hotkey="5C" + "b" * 46, + amount_to_move=Balance.from_tao(50).set_unit(origin_netuid), + pricing=pricing, + sim_swap=sim_swap, + stake_fee=Balance.from_tao(0), + extrinsic_fee=extrinsic_fee, + proxy="5C" + "p" * 46, + ) + + # Full sim_swap amount — extrinsic fee NOT deducted + assert received.tao == pytest.approx(50.0, abs=1e-6) + + @pytest.mark.asyncio + async def test_no_proxy_deducts_extrinsic_fee(self): + """Without a proxy, the extrinsic fee should reduce the received amount.""" + origin_netuid, dest_netuid = 1, 2 + dest_subnet = _make_subnet(dest_netuid, price_tao=1.0) + pricing = MovementPricing( + origin_subnet=_make_subnet(origin_netuid, price_tao=1.0), + destination_subnet=dest_subnet, + rate=1.0, + rate_with_tolerance=None, + ) + sim_swap = _make_sim_swap(alpha_amount_tao=50.0, dest_netuid=dest_netuid) + extrinsic_fee = Balance.from_tao(0.5) + + with patch(f"{MODULE}.console"): + received, _ = await display_stake_movement_cross_subnets( + subtensor=MagicMock(network="test"), + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + origin_hotkey="5C" + "a" * 46, + destination_hotkey="5C" + "b" * 46, + amount_to_move=Balance.from_tao(50).set_unit(origin_netuid), + pricing=pricing, + sim_swap=sim_swap, + stake_fee=Balance.from_tao(0), + extrinsic_fee=extrinsic_fee, + ) + + # Extrinsic fee converted to dest alpha (price=1.0 so 0.5 TAO → 0.5 alpha) + assert received.tao == pytest.approx(49.5, abs=1e-6) + + @pytest.mark.asyncio + async def test_negative_received_raises_value_error(self): + """When fees exceed the swap result, ValueError must be raised.""" + origin_netuid, dest_netuid = 1, 2 + dest_subnet = _make_subnet(dest_netuid, price_tao=1.0) + pricing = MovementPricing( + origin_subnet=_make_subnet(origin_netuid, price_tao=1.0), + destination_subnet=dest_subnet, + rate=1.0, + rate_with_tolerance=None, + ) + # Tiny swap result, large extrinsic fee → negative received + sim_swap = _make_sim_swap(alpha_amount_tao=0.001, dest_netuid=dest_netuid) + extrinsic_fee = Balance.from_tao(1.0) + + with patch(f"{MODULE}.console"), pytest.raises(ValueError): + await display_stake_movement_cross_subnets( + subtensor=MagicMock(network="test"), + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + origin_hotkey="5C" + "a" * 46, + destination_hotkey="5C" + "b" * 46, + amount_to_move=Balance.from_tao(1).set_unit(origin_netuid), + pricing=pricing, + sim_swap=sim_swap, + stake_fee=Balance.from_tao(0), + extrinsic_fee=extrinsic_fee, + ) + + @pytest.mark.asyncio + async def test_destination_root_uses_tao_amount(self): + """When destination is root (netuid 0), received must come from + sim_swap.tao_amount, not sim_swap.alpha_amount.""" + origin_netuid, dest_netuid = 1, 0 + dest_subnet = _make_subnet(dest_netuid, price_tao=1.0) + pricing = MovementPricing( + origin_subnet=_make_subnet(origin_netuid, price_tao=2.0), + destination_subnet=dest_subnet, + rate=2.0, + rate_with_tolerance=None, + ) + # tao_amount and alpha_amount deliberately different + sim_swap = _make_sim_swap( + alpha_amount_tao=999.0, + dest_netuid=origin_netuid, + tao_amount_tao=18.0, + ) + extrinsic_fee = Balance.from_tao(0.0) + + with patch(f"{MODULE}.console"): + received, _ = await display_stake_movement_cross_subnets( + subtensor=MagicMock(network="test"), + origin_netuid=origin_netuid, + destination_netuid=dest_netuid, + origin_hotkey="5C" + "a" * 46, + destination_hotkey="5C" + "b" * 46, + amount_to_move=Balance.from_tao(10).set_unit(origin_netuid), + pricing=pricing, + sim_swap=sim_swap, + stake_fee=Balance.from_tao(0), + extrinsic_fee=extrinsic_fee, + proxy="5C" + "p" * 46, + ) + + # Must use tao_amount (18.0), NOT alpha_amount (999.0) + assert received.tao == pytest.approx(18.0, abs=1e-6) + + +# --------------------------------------------------------------------------- +# Same-subnet tests (behaviour must be unchanged) +# --------------------------------------------------------------------------- + + +class TestSameSubnetDisplay: + @pytest.mark.asyncio + async def test_same_subnet_uses_linear_pricing(self): + """Same-subnet moves use linear alpha_to_tao/tao_to_alpha, not sim_swap.""" + netuid = 3 + price = 2.0 + subnet = _make_subnet(netuid, price_tao=price) + pricing = MovementPricing( + origin_subnet=subnet, + destination_subnet=subnet, + rate=1.0, + rate_with_tolerance=None, + ) + amount = Balance.from_tao(10.0).set_unit(netuid) + stake_fee = Balance.from_tao(0.5).set_unit(netuid) + extrinsic_fee = Balance.from_tao(0.0) + # sim_swap with a wildly different alpha_amount to prove it's not used + sim_swap = _make_sim_swap(alpha_amount_tao=999.0, dest_netuid=netuid) + + with patch(f"{MODULE}.console"): + received, _ = await display_stake_movement_cross_subnets( + subtensor=MagicMock(network="test"), + origin_netuid=netuid, + destination_netuid=netuid, + origin_hotkey="5C" + "a" * 46, + destination_hotkey="5C" + "b" * 46, + amount_to_move=amount, + pricing=pricing, + sim_swap=sim_swap, + stake_fee=stake_fee, + extrinsic_fee=extrinsic_fee, + proxy="5C" + "p" * 46, + ) + + # Linear: (10 - 0.5) * 2.0 / 2.0 = 9.5 (proxy → no extrinsic fee deduction) + assert received.tao == pytest.approx(9.5, abs=1e-6) From 15448affeace8a2ce4bf5bd6423032a5631e6396 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 7 Apr 2026 17:02:11 +0200 Subject: [PATCH 15/72] No longer needed to extract bytes to hex --- bittensor_cli/src/bittensor/chain_data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index df81dc633..fbb4b7b83 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -875,11 +875,10 @@ def _fix_decoded( cls, coldkey: str, decoded: tuple ) -> "ColdkeySwapAnnouncementInfo": execution_block, new_coldkey_hash = decoded - hash_str = "0x" + bytes(new_coldkey_hash[0]).hex() return cls( coldkey=coldkey, execution_block=int(execution_block), - new_coldkey_hash=hash_str, + new_coldkey_hash=new_coldkey_hash, ) From fdeab400e9f94ab839680277462ea58e656383e3 Mon Sep 17 00:00:00 2001 From: BD Himes <37844818+thewhaleking@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:08:18 +0200 Subject: [PATCH 16/72] Updates conftest imports to relative so as to avoid name collision with other packages like ASI (#911) --- tests/unit_tests/conftest.py | 2 +- tests/unit_tests/test_crowd_proxy_creator_checks.py | 2 +- tests/unit_tests/test_proxy_address_resolution.py | 3 +-- tests/unit_tests/test_root_extrinsics.py | 2 +- tests/unit_tests/test_stake_add.py | 2 +- tests/unit_tests/test_stake_move.py | 2 +- tests/unit_tests/test_sudo_hyperparameter_permissions.py | 2 +- tests/unit_tests/test_transfer_extrinsic.py | 4 +--- tests/unit_tests/test_unstake_helpers.py | 3 +-- tests/unit_tests/test_utils_pure.py | 2 +- 10 files changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 3a380bb91..04bc2d259 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -4,7 +4,7 @@ Provides common mock objects, SS58 address constants, and receipt helpers that are duplicated across multiple test files. Import constants directly: - from conftest import COLDKEY_SS58, HOTKEY_SS58, ... + from .conftest import COLDKEY_SS58, HOTKEY_SS58, ... Fixtures (mock_wallet, mock_wallet_spec, mock_subtensor, successful_receipt, failed_receipt) are discovered automatically by pytest. diff --git a/tests/unit_tests/test_crowd_proxy_creator_checks.py b/tests/unit_tests/test_crowd_proxy_creator_checks.py index 2d44cf841..e44772516 100644 --- a/tests/unit_tests/test_crowd_proxy_creator_checks.py +++ b/tests/unit_tests/test_crowd_proxy_creator_checks.py @@ -3,7 +3,7 @@ import pytest from bittensor_cli.src.bittensor.balances import Balance -from tests.unit_tests.conftest import COLDKEY_SS58, PROXY_SS58 +from .conftest import COLDKEY_SS58, PROXY_SS58 def _make_crowdloan( diff --git a/tests/unit_tests/test_proxy_address_resolution.py b/tests/unit_tests/test_proxy_address_resolution.py index 5ca7e97a2..48a74eef9 100644 --- a/tests/unit_tests/test_proxy_address_resolution.py +++ b/tests/unit_tests/test_proxy_address_resolution.py @@ -8,8 +8,7 @@ from contextlib import contextmanager from unittest.mock import AsyncMock, MagicMock, patch -from bittensor_cli.src.bittensor.balances import Balance -from tests.unit_tests.conftest import ( +from .conftest import ( PROXY_SS58, HOTKEY_SS58, DEST_SS58 as DEST_HOTKEY_SS58, diff --git a/tests/unit_tests/test_root_extrinsics.py b/tests/unit_tests/test_root_extrinsics.py index d7d789320..de8465f25 100644 --- a/tests/unit_tests/test_root_extrinsics.py +++ b/tests/unit_tests/test_root_extrinsics.py @@ -17,7 +17,7 @@ get_current_weights_for_uid, get_limits, ) -from tests.unit_tests.conftest import COLDKEY_SS58 as _SS58 +from .conftest import COLDKEY_SS58 as _SS58 U16_MAX = 65535 diff --git a/tests/unit_tests/test_stake_add.py b/tests/unit_tests/test_stake_add.py index 974f88353..1df70fc83 100644 --- a/tests/unit_tests/test_stake_add.py +++ b/tests/unit_tests/test_stake_add.py @@ -6,7 +6,7 @@ from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.commands.stake.add import stake_add -from tests.unit_tests.conftest import ( +from .conftest import ( ALT_HOTKEY_SS58, COLDKEY_SS58 as TEST_SS58, ) diff --git a/tests/unit_tests/test_stake_move.py b/tests/unit_tests/test_stake_move.py index bb1257943..2a4de12ac 100644 --- a/tests/unit_tests/test_stake_move.py +++ b/tests/unit_tests/test_stake_move.py @@ -16,7 +16,7 @@ stake_move_transfer_selection, move_stake, ) -from tests.unit_tests.conftest import HOTKEY_SS58, ALT_HOTKEY_SS58 +from .conftest import HOTKEY_SS58, ALT_HOTKEY_SS58 MODULE = "bittensor_cli.src.commands.stake.move" diff --git a/tests/unit_tests/test_sudo_hyperparameter_permissions.py b/tests/unit_tests/test_sudo_hyperparameter_permissions.py index f975e5446..e20602c5e 100644 --- a/tests/unit_tests/test_sudo_hyperparameter_permissions.py +++ b/tests/unit_tests/test_sudo_hyperparameter_permissions.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import AsyncMock, MagicMock, patch -from tests.unit_tests.conftest import COLDKEY_SS58 +from .conftest import COLDKEY_SS58 MODULE = "bittensor_cli.src.commands.sudo" diff --git a/tests/unit_tests/test_transfer_extrinsic.py b/tests/unit_tests/test_transfer_extrinsic.py index 93c09fdf4..9396d2f0b 100644 --- a/tests/unit_tests/test_transfer_extrinsic.py +++ b/tests/unit_tests/test_transfer_extrinsic.py @@ -5,13 +5,11 @@ and mock_subtensor fixtures from conftest.py. """ -import pytest from unittest.mock import AsyncMock, MagicMock, patch from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.transfer import transfer_extrinsic -from tests.unit_tests.conftest import DEST_SS58 as _DEST_SS58 -from tests.unit_tests.conftest import PROXY_SS58 as _PROXY_SS58 +from .conftest import DEST_SS58 as _DEST_SS58, PROXY_SS58 as _PROXY_SS58 # An invalid destination _INVALID_DEST = "not_a_valid_address" diff --git a/tests/unit_tests/test_unstake_helpers.py b/tests/unit_tests/test_unstake_helpers.py index 81f0fc5f2..611287769 100644 --- a/tests/unit_tests/test_unstake_helpers.py +++ b/tests/unit_tests/test_unstake_helpers.py @@ -9,7 +9,6 @@ - _print_table_and_slippage """ -import pytest from types import SimpleNamespace from unittest.mock import MagicMock, patch @@ -22,7 +21,7 @@ get_hotkey_identity, ) from bittensor_cli.src.bittensor.balances import Balance -from tests.unit_tests.conftest import ( +from .conftest import ( PROXY_SS58 as _HOTKEY_SS58, COLDKEY_SS58 as _COLDKEY_SS58, ) diff --git a/tests/unit_tests/test_utils_pure.py b/tests/unit_tests/test_utils_pure.py index 63279c019..1fb900bbd 100644 --- a/tests/unit_tests/test_utils_pure.py +++ b/tests/unit_tests/test_utils_pure.py @@ -24,7 +24,7 @@ format_error_message, validate_netuid, ) -from tests.unit_tests.conftest import COLDKEY_SS58 as _VALID_SS58 +from .conftest import COLDKEY_SS58 as _VALID_SS58 # --------------------------------------------------------------------------- From a5e62a5d7b0a27edad8b589f791ca78d0f34578d Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 7 Apr 2026 17:19:46 +0200 Subject: [PATCH 17/72] No longer needed to decode account id --- bittensor_cli/src/bittensor/subtensor_interface.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 75cc7540d..b015b0698 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2056,9 +2056,8 @@ async def get_crowdloan_contributors( ) contributor_contributions = {} - for contributor_key, contribution_amount in contributors_data.records: + for contributor_address, contribution_amount in contributors_data.records: try: - contributor_address = decode_account_id(contributor_key[0]) contribution_balance = Balance.from_rao(contribution_amount.value) contributor_contributions[contributor_address] = contribution_balance except Exception: From 7fc6afce543ae70b65a09502c208cbf3e01719f1 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 7 Apr 2026 17:29:45 +0200 Subject: [PATCH 18/72] No need to decode from bytes --- bittensor_cli/src/bittensor/subtensor_interface.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index b015b0698..be72aa1dd 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -180,10 +180,9 @@ async def _decode_inline_call( """ if not call_option or "Inline" not in call_option: return None - inline_bytes = bytes(call_option["Inline"][0][0]) call_obj = await self.substrate.create_scale_object( "Call", - data=ScaleBytes(inline_bytes), + data=ScaleBytes(call_option["Inline"]), block_hash=block_hash, ) call_value = call_obj.decode() From b71067e0f6aedbf2e8493a03dd93929d81abb58b Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 7 Apr 2026 19:55:59 +0200 Subject: [PATCH 19/72] Decoding --- .../src/bittensor/subtensor_interface.py | 23 ++++++------------- .../src/commands/stake/children_hotkeys.py | 7 +++--- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index be72aa1dd..dc988d0c8 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -63,17 +63,10 @@ class ProposalVoteData: def __init__(self, proposal_dict: dict) -> None: self.index = proposal_dict["index"] self.threshold = proposal_dict["threshold"] - self.ayes = self.decode_ss58_tuples(proposal_dict["ayes"]) - self.nays = self.decode_ss58_tuples(proposal_dict["nays"]) + self.ayes = proposal_dict["ayes"] + self.nays = proposal_dict["nays"] self.end = proposal_dict["end"] - @staticmethod - def decode_ss58_tuples(data: tuple): - """ - Decodes a tuple of ss58 addresses formatted as bytes tuples - """ - return [decode_account_id(data[x][0]) for x in range(len(data))] - class SubtensorInterface: """ @@ -165,10 +158,7 @@ async def query( subscription_handler, reuse_block_hash, ) - if hasattr(result, "value"): - return result.value - else: - return result + return getattr(result, "value", result) async def _decode_inline_call( self, @@ -180,10 +170,12 @@ async def _decode_inline_call( """ if not call_option or "Inline" not in call_option: return None + runtime = await self.substrate.init_runtime(block_hash=block_hash) call_obj = await self.substrate.create_scale_object( "Call", data=ScaleBytes(call_option["Inline"]), block_hash=block_hash, + runtime=runtime ) call_value = call_obj.decode() @@ -1387,9 +1379,8 @@ async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: formatted_children = [] for proportion, child in children: # Convert U64 to int - formatted_child = decode_account_id(child[0]) int_proportion = int(proportion) - formatted_children.append((int_proportion, formatted_child)) + formatted_children.append((int_proportion, child)) return True, formatted_children, "" else: return True, [], "" @@ -2660,7 +2651,7 @@ async def get_mev_shield_next_key( storage_function="NextKey", block_hash=block_hash, ) - public_key_bytes = bytes(next(iter(result))) + public_key_bytes = bytes.fromhex(result.removeprefix("0x")) if len(public_key_bytes) != MEV_SHIELD_PUBLIC_KEY_SIZE: raise ValueError( diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 69d4083e4..d4bfbfa10 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -477,11 +477,11 @@ async def _render_table( console.print(table) + netuid_children_tuples = [] # Core logic for get_children if netuid is None: # get all netuids netuids = await subtensor.get_all_subnet_netuids() - netuid_children_tuples = [] for netuid_ in netuids: success, children, err_mg = await subtensor.get_children( get_hotkey_pub_ss58(wallet), netuid_ @@ -493,6 +493,7 @@ async def _render_table( f"Failed to get children from subtensor {netuid_}: {err_mg}" ) await _render_table(get_hotkey_pub_ss58(wallet), netuid_children_tuples) + return netuid_children_tuples else: success, children, err_mg = await subtensor.get_children( get_hotkey_pub_ss58(wallet), netuid @@ -503,7 +504,7 @@ async def _render_table( netuid_children_tuples = [(netuid, children)] await _render_table(get_hotkey_pub_ss58(wallet), netuid_children_tuples) - return children + return netuid_children_tuples async def set_children( @@ -790,7 +791,7 @@ async def set_chk_take_subnet( print_error(f"Unable to set childkey take. {message}") return False, ext_id_ - # Print childkey take for other user and return (dont offer to change take rate) + # Print childkey take for other user and return (don't offer to change take rate) wallet_hk = get_hotkey_pub_ss58(wallet) if not hotkey or hotkey == wallet_hk: hotkey = wallet_hk From 7a742765a13f6bfd41a86d06244e634a13064b75 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 7 Apr 2026 21:08:05 +0200 Subject: [PATCH 20/72] Check-in --- .../src/bittensor/subtensor_interface.py | 2 +- .../src/commands/stake/children_hotkeys.py | 15 +- tests/e2e_tests/test_children_hotkeys.py | 286 ++++++++++++++++++ 3 files changed, 297 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index dc988d0c8..240c36ed5 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -175,7 +175,7 @@ async def _decode_inline_call( "Call", data=ScaleBytes(call_option["Inline"]), block_hash=block_hash, - runtime=runtime + runtime=runtime, ) call_value = call_obj.decode() diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index d4bfbfa10..0c7bf9dd1 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -1,5 +1,6 @@ import asyncio import json +from collections import defaultdict from typing import Optional from bittensor_wallet import Wallet @@ -25,6 +26,7 @@ json_console, get_hotkey_pub_ss58, print_extrinsic_id, + err_console, ) @@ -493,7 +495,6 @@ async def _render_table( f"Failed to get children from subtensor {netuid_}: {err_mg}" ) await _render_table(get_hotkey_pub_ss58(wallet), netuid_children_tuples) - return netuid_children_tuples else: success, children, err_mg = await subtensor.get_children( get_hotkey_pub_ss58(wallet), netuid @@ -503,8 +504,11 @@ async def _render_table( if children: netuid_children_tuples = [(netuid, children)] await _render_table(get_hotkey_pub_ss58(wallet), netuid_children_tuples) - - return netuid_children_tuples + output = defaultdict(dict) + for netuid_, children_ in netuid_children_tuples: + for proportion_, addr in children_: + output[netuid][addr] = proportion_ + return output async def set_children( @@ -795,7 +799,8 @@ async def set_chk_take_subnet( wallet_hk = get_hotkey_pub_ss58(wallet) if not hotkey or hotkey == wallet_hk: hotkey = wallet_hk - if hotkey != wallet_hk or not take: + # TODO get rid of this check, holy shit this is all so fucking bad + if hotkey == wallet_hk or not take: # display childkey take for other users if netuid: await display_chk_take(hotkey, netuid) @@ -854,7 +859,7 @@ async def set_chk_take_subnet( subtensor=subtensor, wallet=wallet, netuid=netuid_, - hotkey=wallet_hk, + hotkey=hotkey, take=take, proxy=proxy, prompt=prompt, diff --git a/tests/e2e_tests/test_children_hotkeys.py b/tests/e2e_tests/test_children_hotkeys.py index f286012fd..bf9ce2a95 100644 --- a/tests/e2e_tests/test_children_hotkeys.py +++ b/tests/e2e_tests/test_children_hotkeys.py @@ -1,6 +1,10 @@ +import json +import time + import pytest from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.utils import U64_MAX from bittensor_cli.src.commands.stake.children_hotkeys import ( get_childkey_completion_block, ) @@ -13,3 +17,285 @@ async def test_get_childkey_completion_block(local_chain): subtensor, 1 ) assert (completion_block - current_block) >= 7200 + + +def test_children_hotkeys(local_chain, wallet_setup): + """ + Test child hotkey set, get, take, and revoke flows using Alice (parent) and Bob (child). + + Steps: + 1. Setup: Create a subnet, register Alice and Bob. + 2. Set: Alice sets Bob as a child hotkey with proportion 0.5 on the subnet. + 3. Get: Verify Bob appears as Alice's child hotkey. + 4. Take: Alice sets a child take for Bob on the subnet. + 5. Revoke: Alice revokes Bob as a child hotkey. + 6. Get: Verify Bob no longer appears as Alice's child hotkey. + """ + print("Testing child hotkey commands 🧪") + + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + "//Alice" + ) + keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup("//Bob") + + # Create a subnet for testing + create_subnet_result = exec_command_alice( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--subnet-name", + "Test Subnet", + "--repo", + "https://github.com/username/repo", + "--contact", + "alice@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "alice#1234", + "--description", + "A test subnet for child hotkey e2e testing", + "--additional-info", + "Created by Alice", + "--logo-url", + "https://testsubnet.com/logo.png", + "--no-prompt", + "--json-output", + ], + ) + create_subnet_payload = json.loads(create_subnet_result.stdout) + assert create_subnet_payload["success"] is True + netuid = create_subnet_payload["netuid"] + + # Start emission schedule for the subnet + start_emission_result = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-name", + wallet_alice.name, + "--no-prompt", + "--chain", + "ws://127.0.0.1:9945", + "--wallet-path", + wallet_path_alice, + ], + ) + assert ( + f"Successfully started subnet {netuid}'s emission schedule." + in start_emission_result.stdout + ) + + # Register Bob on the subnet + register_bob_result = exec_command_bob( + command="subnets", + sub_command="register", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "✅ Registered" in register_bob_result.stdout, register_bob_result.stderr + + time.sleep(3) + + ################################ + # TEST 1: Set child hotkey + # Alice sets Bob as a child with 50% proportion + ################################ + + set_children_result = exec_command_alice( + command="stake", + sub_command="child", + extra_args=[ + "set", + "--children", + wallet_bob.hotkey.ss58_address, + "--proportions", + "0.5", # 50% + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--wait-for-inclusion", + "--wait-for-finalization", + ], + ) + assert "Set children hotkeys" in set_children_result.stdout, ( + set_children_result.stderr + ) + + time.sleep(8) + ################################ + # TEST 2: Get child hotkeys + # Verify Bob is listed as Alice's child on the subnet + ################################ + get_children_result = exec_command_alice( + command="stake", + sub_command="child", + extra_args=[ + "get", + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--network", + "ws://127.0.0.1:9945", + "--json-output", + "--verbose", + ], + ) + get_children_result_json = json.loads(get_children_result.stdout) + # should be 50% which is U64_MAX / 2 + assert get_children_result_json[str(netuid)][wallet_bob.hotkey.ss58_address] == int( + U64_MAX / 2 + ), ( + f"Bob's hotkey not found in children output:\n{get_children_result.stdout} | {get_children_result.stderr}" + ) + time.sleep(3) + ################################ + # TEST 3: Set child take + # Alice sets a 10% take for Bob as child hotkey on the subnet + ################################ + + set_take_result = exec_command_alice( + command="stake", + sub_command="child", + extra_args=[ + "take", + "--child-hotkey-ss58", + wallet_bob.hotkey.ss58_address, + "--netuid", + str(netuid), + "--take", + "0.10", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--wait-for-inclusion", + "--wait-for-finalization", + "--verbose", + ], + ) + assert ( + "Child take set" in set_take_result.stdout or "✅" in set_take_result.stdout + ), f"Take not set:\n{set_take_result.stdout}\n{set_take_result.stderr}" + + # TODO Does not work yet. Think the issue is in the take setting above, still investigating + time.sleep(500) + # Verify the take value was applied + get_take_result = exec_command_alice( + command="stake", + sub_command="child", + extra_args=[ + "take", + "--child-hotkey-ss58", + wallet_bob.hotkey.ss58_address, + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + ], + ) + print(get_take_result.stdout, get_take_result.stderr) + assert "10.00%" in get_take_result.stdout, ( + f"Expected 10.00% take not found:\n{get_take_result.stdout}" + ) + + ################################ + # TEST 4: Revoke child hotkeys + # Alice revokes Bob as a child hotkey on the subnet + ################################ + + revoke_children_result = exec_command_alice( + command="stake", + sub_command="child", + extra_args=[ + "revoke", + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--wait-for-inclusion", + "--wait-for-finalization", + ], + ) + assert "revocation request" in revoke_children_result.stdout.lower() or ( + "✅" in revoke_children_result.stdout + ), ( + f"Revoke did not succeed:\n{revoke_children_result.stdout}\n{revoke_children_result.stderr}" + ) + + # Verify Bob is no longer listed as a child + get_children_after_revoke = exec_command_alice( + command="stake", + sub_command="child", + extra_args=[ + "get", + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert wallet_bob.hotkey.ss58_address not in get_children_after_revoke.stdout, ( + f"Bob's hotkey still found after revoke:\n{get_children_after_revoke.stdout}" + ) + + print("Passed child hotkey commands") From 3597358b789d9fc7bbb1f3909ffcf14e4ab301aa Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 8 Apr 2026 13:26:58 +0200 Subject: [PATCH 21/72] Remove `decode_account_id` fn --- bittensor_cli/src/bittensor/chain_data.py | 61 +++++++------------ .../src/bittensor/subtensor_interface.py | 16 ++--- bittensor_cli/src/bittensor/utils.py | 7 --- bittensor_cli/src/commands/sudo.py | 6 +- 4 files changed, 29 insertions(+), 61 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index fbb4b7b83..7fd1e2049 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -13,7 +13,6 @@ SS58_FORMAT, u16_normalized_float as u16tf, u64_normalized_float as u64tf, - decode_account_id, get_netuid_and_subuid_by_storage_index, ) @@ -39,8 +38,7 @@ class ChainDataType(Enum): def process_stake_data(stake_data, netuid): decoded_stake_data = {} - for account_id_bytes, stake_ in stake_data: - account_id = decode_account_id(account_id_bytes) + for account_id, stake_ in stake_data: decoded_stake_data.update( {account_id: Balance.from_rao(stake_).set_unit(netuid)} ) @@ -248,8 +246,8 @@ class StakeInfo(InfoBase): @classmethod def _fix_decoded(cls, decoded: Any) -> "StakeInfo": - hotkey = decode_account_id(decoded.get("hotkey")) - coldkey = decode_account_id(decoded.get("coldkey")) + hotkey = decoded.get("hotkey") + coldkey = decoded.get("coldkey") netuid = int(decoded.get("netuid")) stake = Balance.from_rao(decoded.get("stake")).set_unit(netuid) locked = Balance.from_rao(decoded.get("locked")).set_unit(netuid) @@ -346,8 +344,8 @@ def _fix_decoded(cls, decoded: Any) -> "NeuronInfo": stake_dict = process_stake_data(decoded.get("stake"), netuid=netuid) total_stake = sum(stake_dict.values()) if stake_dict else Balance(0) axon_info = decoded.get("axon_info", {}) - coldkey = decode_account_id(decoded.get("coldkey")) - hotkey = decode_account_id(decoded.get("hotkey")) + coldkey = decoded.get("coldkey") + hotkey = decoded.get("hotkey") return cls( hotkey=hotkey, coldkey=coldkey, @@ -440,11 +438,11 @@ def get_null_neuron() -> "NeuronInfoLite": def _fix_decoded(cls, decoded: Union[dict, "NeuronInfoLite"]) -> "NeuronInfoLite": active = decoded.get("active") axon_info = decoded.get("axon_info", {}) - coldkey = decode_account_id(decoded.get("coldkey")) + coldkey = decoded.get("coldkey") consensus = decoded.get("consensus") dividends = decoded.get("dividends") emission = decoded.get("emission") - hotkey = decode_account_id(decoded.get("hotkey")) + hotkey = decoded.get("hotkey") incentive = decoded.get("incentive") last_update = decoded.get("last_update") netuid = decoded.get("netuid") @@ -525,12 +523,9 @@ class DelegateInfo(InfoBase): @classmethod def _fix_decoded(cls, decoded: "DelegateInfo") -> "DelegateInfo": - hotkey = decode_account_id(decoded.get("hotkey_ss58")) - owner = decode_account_id(decoded.get("owner_ss58")) - nominators = [ - (decode_account_id(x), Balance.from_rao(y)) - for x, y in decoded.get("nominators") - ] + hotkey = decoded.get("hotkey_ss58") + owner = decoded.get("owner_ss58") + nominators = [(x, Balance.from_rao(y)) for x, y in decoded.get("nominators")] total_stake = sum((x[1] for x in nominators)) if nominators else Balance(0) return cls( hotkey_ss58=hotkey, @@ -631,7 +626,7 @@ def _fix_decoded(cls, decoded: "SubnetInfo") -> "SubnetInfo": }, emission_value=decoded.get("emission_value"), burn=Balance.from_rao(decoded.get("burn")), - owner_ss58=decode_account_id(decoded.get("owner")), + owner_ss58=decoded.get("owner"), ) @@ -698,8 +693,8 @@ def _fix_decoded(cls, decoded: Any) -> "DynamicInfo": subnet_name = bytes([int(b) for b in decoded.get("subnet_name")]).decode() is_dynamic = True if netuid > 0 else False # Patching for netuid 0 - owner_hotkey = decode_account_id(decoded.get("owner_hotkey")) - owner_coldkey = decode_account_id(decoded.get("owner_coldkey")) + owner_hotkey = decoded.get("owner_hotkey") + owner_coldkey = decoded.get("owner_coldkey") emission = Balance.from_rao(decoded.get("emission")).set_unit(0) alpha_in = Balance.from_rao(decoded.get("alpha_in")).set_unit(netuid) @@ -908,8 +903,8 @@ def _fix_decoded(cls, decoded: Any) -> "SubnetState": netuid = decoded.get("netuid") return cls( netuid=netuid, - hotkeys=[decode_account_id(val) for val in decoded.get("hotkeys")], - coldkeys=[decode_account_id(val) for val in decoded.get("coldkeys")], + hotkeys=decoded.get("hotkeys"), + coldkeys=decoded.get("coldkeys"), active=decoded.get("active"), validator_permit=decoded.get("validator_permit"), pruning_score=[u16tf(val) for val in decoded.get("pruning_score")], @@ -1145,8 +1140,8 @@ def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo": alpha_low=u16tf(decoded["alpha_low"]), bonds_moving_avg=u64tf(decoded["bonds_moving_avg"]), # Metagraph info. - hotkeys=[decode_account_id(ck) for ck in decoded.get("hotkeys", [])], - coldkeys=[decode_account_id(hk) for hk in decoded.get("coldkeys", [])], + hotkeys=decoded.get("hotkeys", []), + coldkeys=decoded.get("coldkeys", []), identities=decoded["identities"], axons=decoded.get("axons", []), active=decoded["active"], @@ -1165,11 +1160,11 @@ def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo": total_stake=[_tbwu(ts, _netuid) for ts in decoded["total_stake"]], # Dividend break down tao_dividends_per_hotkey=[ - (decode_account_id(alpha[0]), _tbwu(alpha[1])) + (alpha[0], _tbwu(alpha[1])) for alpha in decoded["tao_dividends_per_hotkey"] ], alpha_dividends_per_hotkey=[ - (decode_account_id(adphk[0]), _tbwu(adphk[1], _netuid)) + (adphk[0], _tbwu(adphk[1], _netuid)) for adphk in decoded["alpha_dividends_per_hotkey"] ], ) @@ -1209,21 +1204,9 @@ class CrowdloanData(InfoBase): @classmethod def _fix_decoded(cls, decoded: dict[str, Any]) -> "CrowdloanData": - creator = ( - decode_account_id(creator_raw) - if (creator_raw := decoded.get("creator")) - else None - ) - funds_account = ( - decode_account_id(funds_raw) - if (funds_raw := decoded.get("funds_account")) - else None - ) - target_address = ( - decode_account_id(target_raw) - if (target_raw := decoded.get("target_address")) - else None - ) + creator = decoded.get("creator") + funds_account = decoded.get("funds_account") + target_address = decoded.get("target_address") return cls( creator=creator, funds_account=funds_account, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 240c36ed5..a572fa26f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -23,7 +23,6 @@ NeuronInfoLite, NeuronInfo, SubnetHyperparameters, - decode_account_id, DynamicInfo, SubnetState, MetagraphInfo, @@ -278,8 +277,7 @@ async def get_auto_stake_destinations( ) destinations: dict[int, str] = {} for netuid, destination in query.records: - hotkey_ss58 = decode_account_id(destination.value[0]) - if hotkey_ss58: + if hotkey_ss58 := destination.value[0]: destinations[int(netuid)] = hotkey_ss58 return destinations @@ -1830,8 +1828,7 @@ async def get_coldkey_swap_announcements( ) announcements = [] - for ss58, data in result.records: - coldkey = decode_account_id(ss58) + for coldkey, data in result.records: announcements.append( ColdkeySwapAnnouncementInfo._fix_decoded(coldkey, data) ) @@ -1890,8 +1887,7 @@ async def get_coldkey_swap_disputes( ) disputes: list[tuple[str, int]] = [] - for ss58, data in result.records: - coldkey = decode_account_id(ss58) + for coldkey, data in result.records: disputes.append((coldkey, data.value)) return disputes @@ -2204,8 +2200,7 @@ async def get_all_coldkeys_claim_type( ) root_claim_types = {} - for coldkey, claim_type_data in result.records: - coldkey_ss58 = decode_account_id(coldkey[0]) + for coldkey_ss58, claim_type_data in result.records: claim_type_key = claim_type_data.value if claim_type_key == "KeepSubnets": @@ -2243,8 +2238,7 @@ async def get_staking_hotkeys( block_hash=block_hash, reuse_block_hash=reuse_block, ) - staked_hotkeys = [decode_account_id(hotkey) for hotkey in result] - return staked_hotkeys + return result async def get_claimed_amount( self, diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 4657ee4c5..77fe560db 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -633,13 +633,6 @@ def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool return False -def decode_account_id(account_id_bytes: str) -> str: - """ - Does nothing. Retained for compatibility til v10 - """ - return account_id_bytes - - def encode_account_id(ss58_address: str) -> bytes: return bytes.fromhex(ss58_decode(ss58_address, SS58_FORMAT)) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 9a7136729..296c49c22 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -21,7 +21,6 @@ from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( wait_for_extrinsic_by_hash, ) -from bittensor_cli.src.bittensor.chain_data import decode_account_id from bittensor_cli.src.bittensor.utils import ( confirm_action, console, @@ -686,9 +685,8 @@ async def _get_senate_members( block_hash=block_hash, ) try: - return [ - decode_account_id(i[x][0]) for i in senate_members for x in range(len(i)) - ] + # TODO double-check the decode logic here + return [i[x] for i in senate_members for x in range(len(i))] except (IndexError, TypeError): print_error("Unable to retrieve senate members.") return [] From b7427624e2c6dae76aa38a148b4c16f7088538a1 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 8 Apr 2026 15:22:24 +0200 Subject: [PATCH 22/72] Fixed tests and issues with child key setting. --- bittensor_cli/cli.py | 2 + .../src/commands/stake/children_hotkeys.py | 22 ++-------- tests/e2e_tests/test_children_hotkeys.py | 44 +++++-------------- 3 files changed, 16 insertions(+), 52 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ab92d1753..421a597d6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6551,6 +6551,7 @@ def stake_childkey_take( wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, + decline: bool = Options.decline, quiet: bool = Options.quiet, verbose: bool = Options.verbose, json_output: bool = Options.json_output, @@ -6608,6 +6609,7 @@ def stake_childkey_take( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, + decline=decline, ) ) if json_output: diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 0c7bf9dd1..10031a340 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -799,24 +799,10 @@ async def set_chk_take_subnet( wallet_hk = get_hotkey_pub_ss58(wallet) if not hotkey or hotkey == wallet_hk: hotkey = wallet_hk - # TODO get rid of this check, holy shit this is all so fucking bad - if hotkey == wallet_hk or not take: - # display childkey take for other users - if netuid: - await display_chk_take(hotkey, netuid) - if take: - console.print( - f"Hotkey {hotkey} not associated with wallet {wallet.name}." - ) - return [(netuid, False, None)] - else: - # show child hotkey take on all subnets - await chk_all_subnets(hotkey) - if take: - console.print( - f"Hotkey {hotkey} not associated with wallet {wallet.name}." - ) - return [(netuid, False, None)] + if netuid: + await display_chk_take(hotkey, netuid) + else: + await chk_all_subnets(hotkey) # Validate child SS58 addresses if not take: diff --git a/tests/e2e_tests/test_children_hotkeys.py b/tests/e2e_tests/test_children_hotkeys.py index bf9ce2a95..2b1d80681 100644 --- a/tests/e2e_tests/test_children_hotkeys.py +++ b/tests/e2e_tests/test_children_hotkeys.py @@ -68,11 +68,15 @@ def test_children_hotkeys(local_chain, wallet_setup): "--logo-url", "https://testsubnet.com/logo.png", "--no-prompt", + "--no-mev-protection", "--json-output", ], ) create_subnet_payload = json.loads(create_subnet_result.stdout) - assert create_subnet_payload["success"] is True + assert create_subnet_payload["success"] is True, ( + create_subnet_result.stdout, + create_subnet_result.stderr, + ) netuid = create_subnet_payload["netuid"] # Start emission schedule for the subnet @@ -208,44 +212,16 @@ def test_children_hotkeys(local_chain, wallet_setup): "--no-prompt", "--wait-for-inclusion", "--wait-for-finalization", - "--verbose", - ], - ) - assert ( - "Child take set" in set_take_result.stdout or "✅" in set_take_result.stdout - ), f"Take not set:\n{set_take_result.stdout}\n{set_take_result.stderr}" - - # TODO Does not work yet. Think the issue is in the take setting above, still investigating - time.sleep(500) - # Verify the take value was applied - get_take_result = exec_command_alice( - command="stake", - sub_command="child", - extra_args=[ - "take", - "--child-hotkey-ss58", - wallet_bob.hotkey.ss58_address, - "--netuid", - str(netuid), - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--wallet-hotkey", - wallet_alice.hotkey_str, - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--verbose", + "--json-output", ], ) - print(get_take_result.stdout, get_take_result.stderr) - assert "10.00%" in get_take_result.stdout, ( - f"Expected 10.00% take not found:\n{get_take_result.stdout}" + set_take_result_json = json.loads(set_take_result.stdout) + assert set_take_result_json[str(netuid)]["success"] is True, ( + f"Take not set:\n{set_take_result.stdout}\n{set_take_result.stderr}" ) ################################ - # TEST 4: Revoke child hotkeys + # TEST 3: Revoke child hotkeys # Alice revokes Bob as a child hotkey on the subnet ################################ From 92cce619adf94b6b4c6368404fd3b51182c1083a Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 8 Apr 2026 15:25:37 +0200 Subject: [PATCH 23/72] Bump cyscale --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 786fdbb29..2c132013c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0", "rich>=13.7,<15.0", - "cyscale==0.1.5", + "cyscale==0.1.6", "typer>=0.16", "typing_extensions>4.0.0; python_version<'3.11'", "bittensor-wallet==4.0.1", From 5d0e609aa988f362092a1cc6bc7b35562f3937c3 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 8 Apr 2026 15:46:31 +0200 Subject: [PATCH 24/72] Fix setting to None --- bittensor_cli/src/commands/stake/children_hotkeys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 10031a340..1bf036aff 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -672,7 +672,7 @@ async def revoke_children( success, message, ext_id = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, - netuid=netuid, # TODO should this be able to allow netuid = None ? + netuid=netuid_, hotkey=get_hotkey_pub_ss58(wallet), children_with_proportions=[], proxy=proxy, From 513e4a5747748aeaa05407f53cee580e50a3feac Mon Sep 17 00:00:00 2001 From: bitloi <89318445+bitloi@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:49:42 -0300 Subject: [PATCH 25/72] Remove unused --announce-only from CLI commands (deferred to v10) (#899) Remove the --announce-only flag from 37 CLI methods where it was accepted but never passed to the underlying command functions. Only wallet_transfer and proxy_kill retain the flag (they already thread it through to the extrinsic layer). The full announce-only UX (branched success messaging, slippage disclaimers) is deferred to v10 per maintainer decision. --- bittensor_cli/cli.py | 111 ++++++++++++----------------------- tests/unit_tests/test_cli.py | 6 -- 2 files changed, 37 insertions(+), 80 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ab92d1753..def64c80d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2809,7 +2809,6 @@ def wallet_swap_hotkey( decline: bool = Options.decline, json_output: bool = Options.json_output, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, ): """ Swap hotkeys of a given wallet on the blockchain. For a registered key pair, for example, a (coldkeyA, hotkeyA) pair, this command swaps the hotkeyA with a new, unregistered, hotkeyB to move the original registration to the (coldkeyA, hotkeyB) pair. @@ -2836,7 +2835,7 @@ def wallet_swap_hotkey( [green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey --netuid 1 """ netuid = get_optional_netuid(netuid, all_netuids) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) self.verbosity_handler(quiet, verbose, json_output, prompt, decline) # Warning for netuid 0 - only swaps on root network, not a full swap @@ -3438,7 +3437,6 @@ def wallet_associate_hotkey( wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3456,7 +3454,7 @@ def wallet_associate_hotkey( [green]$[/green] btcli wallet associate-hotkey --hotkey-ss58 5DkQ4... """ self.verbosity_handler(quiet, verbose, json_output=False, prompt=prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if not wallet_name: wallet_name = Prompt.ask( "Enter the [blue]wallet name[/blue] [dim](which you want to associate with the hotkey)[/dim]", @@ -3922,7 +3920,6 @@ def wallet_set_id( help="The GitHub repository for the identity.", ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -3947,7 +3944,7 @@ def wallet_set_id( [bold]Note[/bold]: This command should only be used if the user is willing to incur the a recycle fee associated with setting an identity on the blockchain. It is a high-level command that makes changes to the blockchain state and should not be used programmatically as part of other scripts or applications. """ self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -4575,7 +4572,6 @@ def set_auto_stake( wallet_path: Optional[str] = Options.wallet_path, netuid: Optional[int] = Options.netuid_not_req, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, decline: bool = Options.decline, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4587,7 +4583,7 @@ def set_auto_stake( """Set the auto-stake destination hotkey for a coldkey.""" self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name, @@ -4765,7 +4761,6 @@ def stake_add( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, network: Optional[list[str]] = Options.network, rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, @@ -4817,7 +4812,7 @@ def stake_add( """ netuids = netuids or [] self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: rate_tolerance = self.ask_rate_tolerance(rate_tolerance) @@ -5095,7 +5090,6 @@ def stake_remove( "hotkeys in `--include-hotkeys`.", ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, @@ -5147,7 +5141,7 @@ def stake_remove( • [blue]--partial[/blue]: Complete partial unstake if rates exceed tolerance """ self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if not unstake_all and not unstake_all_alpha: safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: @@ -5459,7 +5453,6 @@ def stake_move( False, "--stake-all", "--all", help="Stake all", prompt=False ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, period: int = Options.period, mev_protection: bool = Options.mev_protection, prompt: bool = Options.prompt, @@ -5493,7 +5486,7 @@ def stake_move( [green]$[/green] btcli stake move --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) print_protection_warnings( mev_protection=mev_protection, safe_staking=None, @@ -5662,7 +5655,6 @@ def stake_transfer( mev_protection: bool = Options.mev_protection, period: int = Options.period, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, decline: bool = Options.decline, quiet: bool = Options.quiet, @@ -5705,7 +5697,7 @@ def stake_transfer( [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --amount 100 --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) print_protection_warnings( mev_protection=mev_protection, safe_staking=None, @@ -5868,7 +5860,6 @@ def stake_swap( help="Swap all available stake", ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, period: int = Options.period, prompt: bool = Options.prompt, decline: bool = Options.decline, @@ -5912,7 +5903,7 @@ def stake_swap( [green]$[/green] btcli stake swap --origin-netuid 1 --dest-netuid 2 --amount 100 --unsafe """ self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) console.print( "[dim]This command moves stake from one subnet to another subnet while keeping " "the same coldkey-hotkey pair.[/dim]" @@ -6160,7 +6151,6 @@ def stake_set_claim_type( wallet_hotkey: Optional[str] = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, decline: bool = Options.decline, quiet: bool = Options.quiet, @@ -6190,7 +6180,7 @@ def stake_set_claim_type( [green]$[/green] btcli stake claim swap --wallet-name my_wallet """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6219,7 +6209,6 @@ def stake_process_claim( wallet_hotkey: Optional[str] = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, decline: bool = Options.decline, quiet: bool = Options.quiet, @@ -6248,7 +6237,7 @@ def stake_process_claim( """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) parsed_netuids = None if netuids: parsed_netuids = parse_to_list( @@ -6363,7 +6352,6 @@ def stake_set_children( prompt=False, ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, @@ -6383,7 +6371,7 @@ def stake_set_children( [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 --prop 0.3 --prop 0.7 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) netuid = get_optional_netuid(netuid, all_netuids) children = list_prompt( @@ -6458,7 +6446,6 @@ def stake_revoke_children( help="When this flag is used it sets child hotkeys on all the subnets.", ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, @@ -6476,7 +6463,7 @@ def stake_revoke_children( [green]$[/green] btcli stake child revoke --hotkey --netuid 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6547,7 +6534,6 @@ def stake_childkey_take( prompt=False, ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -6571,7 +6557,7 @@ def stake_childkey_take( [green]$[/green] btcli stake child take --child-hotkey-ss58 --take 0.12 --netuid 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6633,7 +6619,6 @@ def mechanism_count_set( help="Number of mechanisms to set for the subnet.", ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -6656,7 +6641,7 @@ def mechanism_count_set( [green]$[/green] btcli subnet mech set --netuid 12 --count 2 --wallet.name my_wallet --wallet.hotkey admin """ - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) @@ -6786,7 +6771,6 @@ def mechanism_emission_set( help="Comma-separated relative weights for each mechanism (normalised automatically).", ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -6810,7 +6794,7 @@ def mechanism_emission_set( 2. Apply a 70/30 distribution in one command: [green]$[/green] btcli subnet mech emissions-split --netuid 12 --split 70,30 --wallet.name my_wallet --wallet.hotkey admin """ - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) wallet = self.wallet_ask( @@ -6879,7 +6863,6 @@ def sudo_set( "", "--value", help="Value to set the hyperparameter to." ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, decline: bool = Options.decline, quiet: bool = Options.quiet, @@ -6902,7 +6885,7 @@ def sudo_set( [green]$[/green] btcli sudo set --netuid 1 --param custom_param_name --value 123 """ self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if not param_name or not param_value: hyperparams = self._run_command( @@ -7240,7 +7223,6 @@ def sudo_senate_vote( help="The hash of the proposal to vote on.", ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -7263,7 +7245,7 @@ def sudo_senate_vote( [green]$[/green] btcli sudo senate_vote --proposal """ # TODO discuss whether this should receive json_output. I don't think it should. - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) wallet = self.wallet_ask( wallet_name, @@ -7293,7 +7275,6 @@ def sudo_set_take( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, take: float = typer.Option(None, help="The new take value."), quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -7311,7 +7292,7 @@ def sudo_set_take( max_value = 0.18 min_value = 0.00 self.verbosity_handler(quiet, verbose, json_output, prompt=False) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name, @@ -7396,7 +7377,6 @@ def sudo_trim( wallet_hotkey: Optional[str] = Options.wallet_hotkey, netuid: int = Options.netuid, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, max_uids: int = typer.Option( None, "--max", @@ -7418,7 +7398,7 @@ def sudo_trim( [green]$[/green] btcli sudo trim --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey --max 64 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name, @@ -7802,7 +7782,6 @@ def subnets_create( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, subnet_name: Optional[str] = typer.Option( None, "--subnet-name", help="Name of the subnet" ), @@ -7854,7 +7833,7 @@ def subnets_create( [green]$[/green] btcli subnets create --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) print_protection_warnings( mev_protection=mev_protection, safe_staking=None, @@ -7926,7 +7905,6 @@ def subnets_start( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, netuid: int = Options.netuid, prompt: bool = Options.prompt, decline: bool = Options.decline, @@ -7943,7 +7921,7 @@ def subnets_start( [green]$[/green] btcli subnets start --netuid 1 --wallet-name alice """ self.verbosity_handler(quiet, verbose, json_output=False, prompt=prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if not wallet_name: wallet_name = Prompt.ask( "Enter the [blue]wallet name[/blue] [dim](which you used to create the subnet)[/dim]", @@ -8001,7 +7979,6 @@ def subnets_set_identity( network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, subnet_name: Optional[str] = typer.Option( None, "--subnet-name", "--sn-name", help="Name of the subnet" ), @@ -8048,7 +8025,7 @@ def subnets_set_identity( [green]$[/green] btcli subnets set-identity --netuid 1 --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -8207,7 +8184,6 @@ def subnets_register( "use an era for this transaction that you may pay a different fee to register than the one stated.", ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -8225,7 +8201,7 @@ def subnets_register( [green]$[/green] btcli subnets register --netuid 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -8357,7 +8333,6 @@ def subnets_set_symbol( network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, period: int = Options.period, json_output: bool = Options.json_output, prompt: bool = Options.prompt, @@ -8381,7 +8356,7 @@ def subnets_set_symbol( [#AFEFFF]{success: [dark_orange]bool[/dark_orange], message: [dark_orange]str[/dark_orange]}[/#AFEFFF] """ self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if len(symbol) > 1: print_error("Your symbol must be a single character.") return False @@ -8424,7 +8399,6 @@ def weights_reveal( wallet_hotkey: str = Options.wallet_hotkey, netuid: int = Options.netuid, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, uids: str = typer.Option( None, "--uids", @@ -8453,7 +8427,7 @@ def weights_reveal( [green]$[/green] btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if uids: uids = parse_to_list( uids, @@ -8523,7 +8497,6 @@ def weights_commit( wallet_hotkey: str = Options.wallet_hotkey, netuid: int = Options.netuid, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, uids: str = typer.Option( None, "--uids", @@ -8556,7 +8529,7 @@ def weights_commit( permissions. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if uids: uids = parse_to_list( uids, @@ -8689,7 +8662,6 @@ def liquidity_add( wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, liquidity_: Optional[float] = typer.Option( None, "--liquidity", @@ -8719,7 +8691,7 @@ def liquidity_add( ): """Add liquidity to the swap (as a combination of TAO + Alpha).""" self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if not netuid: netuid = Prompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -8827,7 +8799,6 @@ def liquidity_remove( wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, position_id: Optional[int] = typer.Option( None, "--position-id", @@ -8849,7 +8820,7 @@ def liquidity_remove( """Remove liquidity from the swap (as a combination of TAO + Alpha).""" self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if all_liquidity_ids and position_id: print_error("Cannot specify both --all and --position-id.") return @@ -8904,7 +8875,6 @@ def liquidity_modify( wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, position_id: Optional[int] = typer.Option( None, "--position-id", @@ -8925,7 +8895,7 @@ def liquidity_modify( ): """Modifies the liquidity position for the given subnet.""" self.verbosity_handler(quiet, verbose, json_output, prompt, decline) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if not netuid: netuid = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -9169,7 +9139,6 @@ def crowd_create( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, deposit: Optional[float] = typer.Option( None, "--deposit", @@ -9265,7 +9234,7 @@ def crowd_create( [green]$[/green] btcli crowd create --deposit 10 --cap 1000 --duration 1000 --min-contribution 1 --custom-call-pallet "SomeModule" --custom-call-method "some_method" --custom-call-args '{"param1": "value", "param2": 42}' """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, @@ -9318,7 +9287,6 @@ def crowd_contribute( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -9338,7 +9306,7 @@ def crowd_contribute( [green]$[/green] btcli crowd contribute --id 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -9382,7 +9350,6 @@ def crowd_withdraw( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -9397,7 +9364,7 @@ def crowd_withdraw( Creators can only withdraw amounts above their initial deposit. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -9440,7 +9407,6 @@ def crowd_finalize( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -9455,7 +9421,7 @@ def crowd_finalize( address (if specified) and execute any attached call (e.g., subnet creation). """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -9515,7 +9481,6 @@ def crowd_update( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -9533,7 +9498,7 @@ def crowd_update( bounds, etc.). """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -9580,7 +9545,6 @@ def crowd_refund( help="The ID of the crowdloan to refund", ), proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, @@ -9601,7 +9565,7 @@ def crowd_refund( Contributors can call `btcli crowdloan withdraw` at will. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -9644,7 +9608,6 @@ def crowd_dissolve( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -9664,7 +9627,7 @@ def crowd_dissolve( you can run `btcli crowd refund` to refund the remaining contributors. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 5b73fd68c..abb359313 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -413,7 +413,6 @@ def test_stake_add_calls_proxy_validation(): wallet_path="/tmp/test", wallet_hotkey="test_hotkey", proxy=valid_proxy, - announce_only=False, network=None, rate_tolerance=None, safe_staking=False, @@ -461,7 +460,6 @@ def test_stake_remove_calls_proxy_validation(): exclude_hotkeys="", all_hotkeys=False, proxy=valid_proxy, - announce_only=False, rate_tolerance=None, safe_staking=False, allow_partial_stake=None, @@ -502,7 +500,6 @@ def test_wallet_associate_hotkey_calls_proxy_validation(): wallet_hotkey=valid_hotkey, network=None, proxy=valid_proxy, - announce_only=False, prompt=False, quiet=True, verbose=False, @@ -541,7 +538,6 @@ def test_wallet_set_id_calls_proxy_validation(): additional="Additional info", github_repo="test/repo", proxy=valid_proxy, - announce_only=False, quiet=True, verbose=False, prompt=False, @@ -583,7 +579,6 @@ def test_stake_move_calls_proxy_validation(): amount=10.0, stake_all=False, proxy=valid_proxy, - announce_only=False, period=100, prompt=False, quiet=True, @@ -627,7 +622,6 @@ def test_stake_transfer_calls_proxy_validation(): stake_all=False, period=100, proxy=valid_proxy, - announce_only=False, prompt=False, quiet=True, verbose=False, From 4eabadce4f67986f84775724c634602f68913422 Mon Sep 17 00:00:00 2001 From: bitloi Date: Wed, 8 Apr 2026 20:43:20 +0200 Subject: [PATCH 26/72] Fix move_stake MEV status context --- bittensor_cli/src/commands/stake/move.py | 101 +++++++++++------------ 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index dea2c42c3..229ff76ec 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -15,7 +15,6 @@ confirm_action, console, create_table, - is_valid_ss58_address, print_error, group_subnets, get_subnet_name, @@ -706,57 +705,57 @@ async def move_stake( nonce=next_nonce, ) - ext_id = await response.get_extrinsic_identifier() if response else "" - if success_: - if mev_protection: - inner_hash = err_msg - mev_success, mev_error, response = await wait_for_extrinsic_by_hash( - subtensor=subtensor, - extrinsic_hash=inner_hash, - submit_block_hash=response.block_hash, - status=status, - ) - if not mev_success: - status.stop() - print_error(f"\nFailed: {mev_error}") - return False, "" - await print_extrinsic_id(response) - if not prompt: - print_success("Sent") - return True, ext_id - else: - print_success("[dark_sea_green3]Stake moved.[/dark_sea_green3]") - block_hash = await subtensor.substrate.get_chain_head() - ( - new_origin_stake_balance, - new_destination_stake_balance, - ) = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=coldkey_ss58, - hotkey_ss58=origin_hotkey, - netuid=origin_netuid, - block_hash=block_hash, - ), - subtensor.get_stake( - coldkey_ss58=coldkey_ss58, - hotkey_ss58=destination_hotkey, - netuid=destination_netuid, - block_hash=block_hash, - ), - ) + ext_id = await response.get_extrinsic_identifier() if response else "" + if success_: + if mev_protection: + inner_hash = err_msg + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + submit_block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + print_error(f"\nFailed: {mev_error}") + return False, "" + await print_extrinsic_id(response) + if not prompt: + print_success("Sent") + return True, ext_id + else: + print_success("[dark_sea_green3]Stake moved.[/dark_sea_green3]") + block_hash = await subtensor.substrate.get_chain_head() + ( + new_origin_stake_balance, + new_destination_stake_balance, + ) = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=origin_hotkey, + netuid=origin_netuid, + block_hash=block_hash, + ), + subtensor.get_stake( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=destination_hotkey, + netuid=destination_netuid, + block_hash=block_hash, + ), + ) - console.print( - f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_origin_stake_balance}" - ) - console.print( - f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" - ) - return True, ext_id - else: - print_error(f"\nFailed with error: {err_msg}") - return False, "" + console.print( + f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_origin_stake_balance}" + ) + console.print( + f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" + ) + return True, ext_id + else: + print_error(f"\nFailed with error: {err_msg}") + return False, "" async def transfer_stake( From d6667219568caa7ba125c0f1103941439aaf6fbf Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 10 Apr 2026 16:49:09 +0200 Subject: [PATCH 27/72] Pin to release branch of ASI --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2c132013c..ac9178385 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] dependencies = [ "wheel", - "async-substrate-interface>=1.6.2,<2.0.0", + "async-substrate-interface @ git+https://github.com/opentensor/async-substrate-interface.git@release/2.0.0", "aiohttp~=3.13", "backoff~=2.2.1", "bittensor-drand>=1.3.0", @@ -40,7 +40,7 @@ dependencies = [ "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0", "rich>=13.7,<15.0", - "cyscale==0.1.6", + "cyscale==0.1.10", "typer>=0.16", "typing_extensions>4.0.0; python_version<'3.11'", "bittensor-wallet==4.0.1", From ff44f8fa625649c359a06f1f0037b3db4b51e8b0 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 10 Apr 2026 17:22:14 +0200 Subject: [PATCH 28/72] Update for new query_map and query return types --- .../src/bittensor/subtensor_interface.py | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a572fa26f..46a376606 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -43,6 +43,7 @@ get_hotkey_pub_ss58, ProxyAnnouncements, ) +from scalecodec.base import ScaleType GENESIS_ADDRESS = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" @@ -146,9 +147,9 @@ async def query( reuse_block_hash: bool = False, ) -> Any: """ - Pass-through to substrate.query which automatically returns the .value if it's a ScaleObj + Pass-through to substrate.query which automatically returns the .value if it's a ScaleType """ - result = await self.substrate.query( + result: Optional[ScaleType] = await self.substrate.query( module, storage_function, params, @@ -221,7 +222,7 @@ async def get_all_subnet_netuids( ) res = [] for netuid, exists in result.records: - if exists.value: + if exists: res.append(netuid) return res @@ -277,7 +278,7 @@ async def get_auto_stake_destinations( ) destinations: dict[int, str] = {} for netuid, destination in query.records: - if hotkey_ss58 := destination.value[0]: + if hotkey_ss58 := destination[0]: destinations[int(netuid)] = hotkey_ss58 return destinations @@ -335,9 +336,9 @@ async def query_runtime_api( """ if reuse_block: block_hash = self.substrate.last_block_hash - result = ( - await self.substrate.runtime_call(runtime_api, method, params, block_hash) - ).value + result = await self.substrate.runtime_call( + runtime_api, method, params, block_hash + ) return result @@ -566,7 +567,7 @@ async def get_netuids_for_hotkey( ) res = [] for record in result.records: - if record[1].value: + if record[1]: res.append(record[0]) return res @@ -588,14 +589,14 @@ async def is_subnet_active( This means whether the `start_call` was initiated or not. """ - query = await self.substrate.query( + query = await self.query( module="SubtensorModule", storage_function="FirstEmissionBlockNumber", block_hash=block_hash, reuse_block_hash=reuse_block, params=[netuid], ) - return True if query and query.value > 0 else False + return True if query and query > 0 else False async def subnet_exists( self, netuid: int, block_hash: Optional[str] = None, reuse_block: bool = False @@ -942,8 +943,7 @@ async def query_all_identities( fully_exhaust=True, ) all_identities = { - ss58_address: identity.value - for (ss58_address, identity) in identities.records + ss58_address: identity for (ss58_address, identity) in identities.records } return all_identities @@ -989,12 +989,10 @@ async def query_identity( async def fetch_coldkey_hotkey_identities( self, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict[str, dict]: """ Builds a dictionary containing coldkeys and hotkeys with their associated identities and relationships. :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used blockchain block hash. :return: Dict with 'coldkeys' and 'hotkeys' as keys. """ if block_hash is None: @@ -1051,7 +1049,7 @@ async def weights( ) w_map = [] async for uid, w in w_map_encoded: - w_map.append((uid, w.value)) + w_map.append((uid, w)) return w_map @@ -1442,7 +1440,7 @@ async def get_all_subnet_mechanisms( ) res = {} for netuid, count in results.records: - res[int(netuid)] = int(count.value) + res[int(netuid)] = int(count) return res async def get_mechanism_emission_split( @@ -1888,7 +1886,7 @@ async def get_coldkey_swap_disputes( disputes: list[tuple[str, int]] = [] for coldkey, data in result.records: - disputes.append((coldkey, data.value)) + disputes.append((coldkey, data)) return disputes async def get_coldkey_swap_dispute( @@ -1922,7 +1920,7 @@ async def get_coldkey_swap_dispute( async def get_crowdloans( self, block_hash: Optional[str] = None - ) -> list[CrowdloanData]: + ) -> dict[int, CrowdloanData]: """Retrieves all crowdloans from the network. Args: @@ -1946,7 +1944,7 @@ async def get_crowdloans( fund_info["call"], block_hash=block_hash, ) - info_dict = dict(fund_info.value) + info_dict = dict(fund_info) info_dict["call_details"] = decoded_call crowdloans[fund_id] = CrowdloanData.from_any(info_dict) @@ -2044,7 +2042,7 @@ async def get_crowdloan_contributors( contributor_contributions = {} for contributor_address, contribution_amount in contributors_data.records: try: - contribution_balance = Balance.from_rao(contribution_amount.value) + contribution_balance = Balance.from_rao(contribution_amount) contributor_contributions[contributor_address] = contribution_balance except Exception: continue @@ -2178,7 +2176,7 @@ async def get_all_coldkeys_claim_type( self, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, dict]: + ) -> dict[str, dict[str, str | list[int]]]: """ Retrieves all root claim types for all coldkeys in the network. @@ -2200,11 +2198,20 @@ async def get_all_coldkeys_claim_type( ) root_claim_types = {} - for coldkey_ss58, claim_type_data in result.records: - claim_type_key = claim_type_data.value + coldkey_ss58: str + claim_type_key: str + claim_type_dict: dict + claim_type_data: str | dict + for coldkey_ss58, claim_type_data in result.records: + if isinstance(claim_type_data, str): + claim_type_key = claim_type_data + claim_type_dict = {} + else: + claim_type_key = next(iter(claim_type_data.keys())) + claim_type_dict = claim_type_data if claim_type_key == "KeepSubnets": - subnets_data = claim_type_data.value["KeepSubnets"]["subnets"] + subnets_data = claim_type_dict["KeepSubnets"]["subnets"] subnet_list = sorted([subnet for subnet in subnets_data[0]]) root_claim_types[coldkey_ss58] = { "type": "KeepSubnets", @@ -2298,9 +2305,7 @@ async def get_claimed_amount_all_netuids( ) total_claimed = {} for netuid, claimed in query.records: - total_claimed[netuid] = Balance.from_rao(claimed.value).set_unit( - netuid=netuid - ) + total_claimed[netuid] = Balance.from_rao(claimed).set_unit(netuid=netuid) return total_claimed async def get_claimable_rate_all_netuids( @@ -2556,7 +2561,7 @@ async def get_subnet_prices( map_ = {} for netuid_, current_sqrt_price in query.records: - current_sqrt_price_ = fixed_to_float(current_sqrt_price.value) + current_sqrt_price_ = fixed_to_float(current_sqrt_price) current_price = current_sqrt_price_**2 map_[netuid_] = Balance.from_rao(int(current_price * 1e9)) From c5aaafe3f343bc522fa3f9f8ef1b0f25e01ad0f3 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 13 Apr 2026 20:46:35 +0200 Subject: [PATCH 29/72] Fixes --- bittensor_cli/src/commands/sudo.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 296c49c22..6137ce29a 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -1052,9 +1052,7 @@ async def get_hyperparameters( if not await subtensor.subnet_exists(netuid): error_msg = f"Subnet with netuid {netuid} does not exist." if json_output: - json_str = json.dumps({"error": error_msg}, ensure_ascii=True) - sys.stdout.write(json_str + "\n") - sys.stdout.flush() + json_console.print_json(data={"error": error_msg}) else: print_error(error_msg) return False @@ -1064,17 +1062,13 @@ async def get_hyperparameters( if subnet_info is None: error_msg = f"Subnet with netuid {netuid} does not exist." if json_output: - json_str = json.dumps({"error": error_msg}, ensure_ascii=True) - sys.stdout.write(json_str + "\n") - sys.stdout.flush() + json_console.print_json(data={"error": error_msg}) else: print_error(error_msg) return False except Exception as e: if json_output: - json_str = json.dumps({"error": str(e)}, ensure_ascii=True) - sys.stdout.write(json_str + "\n") - sys.stdout.flush() + json_console.print_json(data={"error": str(e)}) else: raise return False From aa12bcf9194726902e62cf403cb5db3ce4f61235 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 13 Apr 2026 20:46:45 +0200 Subject: [PATCH 30/72] Bump cyscale --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ac9178385..e9f2f190a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0", "rich>=13.7,<15.0", - "cyscale==0.1.10", + "cyscale==0.1.11", "typer>=0.16", "typing_extensions>4.0.0; python_version<'3.11'", "bittensor-wallet==4.0.1", From 67266899beedec2f5e36e41c04c821f1d292e289 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 13 Apr 2026 20:54:49 +0200 Subject: [PATCH 31/72] Raise ValueError instead of implicitly returning None. Ensure we use error console for these prints. --- bittensor_cli/src/bittensor/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 77fe560db..d0a54b115 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -833,7 +833,8 @@ def get_decoded(data: Optional[str]) -> str: try: return hex_to_bytes(data).decode() except (UnicodeDecodeError, ValueError): - print(f"Could not decode: {key}: {item}") + print_error(f"Could not decode: {key}: {item}") + raise ValueError for key, value in info_dictionary.items(): if isinstance(value, dict): @@ -842,7 +843,7 @@ def get_decoded(data: Optional[str]) -> str: try: info_dictionary[key] = get_decoded(item) except UnicodeDecodeError: - print(f"Could not decode: {key}: {item}") + print_error(f"Could not decode: {key}: {item}") else: info_dictionary[key] = item if key == "additional": From 42a77bef11edba70e645ff581fec17b9d96f499e Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 13 Apr 2026 21:13:34 +0200 Subject: [PATCH 32/72] Remove reuse_block --- .../src/bittensor/extrinsics/registration.py | 1 - .../src/bittensor/extrinsics/transfer.py | 2 +- bittensor_cli/src/bittensor/minigraph.py | 1 - .../src/bittensor/subtensor_interface.py | 132 ++---------------- .../src/commands/stake/auto_staking.py | 1 - bittensor_cli/src/commands/sudo.py | 2 +- bittensor_cli/src/commands/wallets.py | 2 +- bittensor_cli/src/commands/weights.py | 2 - 8 files changed, 11 insertions(+), 132 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 237d4c3ec..e4cc4d183 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -784,7 +784,6 @@ async def burned_register_extrinsic( subtensor.get_balance( wallet.coldkeypub.ss58_address, block_hash=block_hash, - reuse_block=False, ), subtensor.get_netuids_for_hotkey( get_hotkey_pub_ss58(wallet), block_hash=block_hash diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 8875e5596..2d2369475 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -228,7 +228,7 @@ async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt] if success: with console.status(":satellite: Checking Balance...", spinner="aesthetic"): new_balance = await subtensor.get_balance( - proxy or wallet.coldkeypub.ss58_address, reuse_block=False + proxy or wallet.coldkeypub.ss58_address ) console.print( f"Balance:\n" diff --git a/bittensor_cli/src/bittensor/minigraph.py b/bittensor_cli/src/bittensor/minigraph.py index 9e149c1bb..24a5b0292 100644 --- a/bittensor_cli/src/bittensor/minigraph.py +++ b/bittensor_cli/src/bittensor/minigraph.py @@ -219,7 +219,6 @@ async def get_total_subnets(): module="SubtensorModule", storage_function="TotalNetworks", params=[], - reuse_block_hash=True, ) return _result diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 46a376606..04c750415 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -144,7 +144,6 @@ async def query( block_hash: Optional[str] = None, raw_storage_key: Optional[bytes] = None, subscription_handler=None, - reuse_block_hash: bool = False, ) -> Any: """ Pass-through to substrate.query which automatically returns the .value if it's a ScaleType @@ -156,7 +155,6 @@ async def query( block_hash, raw_storage_key, subscription_handler, - reuse_block_hash, ) return getattr(result, "value", result) @@ -216,7 +214,6 @@ async def get_all_subnet_netuids( module="SubtensorModule", storage_function="NetworksAdded", block_hash=block_hash, - reuse_block_hash=True, fully_exhaust=True, page_size=200, ) @@ -230,7 +227,6 @@ async def get_stake_for_coldkey( self, coldkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> list[StakeInfo]: """ Retrieves stake information associated with a specific coldkey. This function provides details @@ -238,7 +234,6 @@ async def get_stake_for_coldkey( :param coldkey_ss58: The ``SS58`` address of the account's coldkey. :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used block hash. :return: A list of StakeInfo objects detailing the stake allocations for the account. @@ -251,7 +246,6 @@ async def get_stake_for_coldkey( method="get_stake_info_for_coldkey", params=[coldkey_ss58], block_hash=block_hash, - reuse_block=reuse_block, ) if result is None: @@ -263,7 +257,6 @@ async def get_auto_stake_destinations( self, coldkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict[int, str]: """Retrieve auto-stake destinations configured for a coldkey.""" @@ -272,7 +265,6 @@ async def get_auto_stake_destinations( storage_function="AutoStakeDestination", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, fully_exhaust=True, page_size=200, ) @@ -316,7 +308,6 @@ async def query_runtime_api( method: str, params: Optional[Union[list, dict]] = None, block_hash: Optional[str] = None, - reuse_block: Optional[bool] = False, ) -> Optional[Any]: """ Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying @@ -327,15 +318,12 @@ async def query_runtime_api( :param method: The specific method within the runtime API to call. :param params: The parameters to pass to the method call. :param block_hash: The hash of the blockchain block number at which to perform the query. - :param reuse_block: Whether to reuse the last-used block hash. :return: The decoded result from the runtime API call, or ``None`` if the call fails. This function enables access to the deeper layers of the Bittensor blockchain, allowing for detailed and specific interactions with the network's runtime environment. """ - if reuse_block: - block_hash = self.substrate.last_block_hash result = await self.substrate.runtime_call( runtime_api, method, params, block_hash ) @@ -346,14 +334,12 @@ async def get_balance( self, address: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> Balance: """ Retrieves the balance for a single coldkey address :param address: coldkey address :param block_hash: the block hash, optional - :param reuse_block: Whether to reuse the last-used block hash when retrieving info. :return: Balance object representing the address's balance """ result = await self.query( @@ -361,7 +347,6 @@ async def get_balance( storage_function="Account", params=[address], block_hash=block_hash, - reuse_block_hash=reuse_block, ) value = result or {"data": {"free": 0}} return Balance(value["data"]["free"]) @@ -370,17 +355,15 @@ async def get_balances( self, *addresses: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict[str, Balance]: """ Retrieves the balance for given coldkey(s) :param addresses: coldkey addresses(s) :param block_hash: the block hash, optional - :param reuse_block: Whether to reuse the last-used block hash when retrieving info. :return: dict of {address: Balance objects} """ - if reuse_block: - block_hash = self.substrate.last_block_hash + if not block_hash: + block_hash = await self.substrate.get_chain_head() calls = [ ( await self.substrate.create_storage_key( @@ -450,7 +433,6 @@ async def get_total_stake_for_hotkey( *ss58_addresses, netuids: Optional[list[int]] = None, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict[str, dict[int, Balance]]: """ Returns the total stake held on a hotkey. @@ -458,7 +440,6 @@ async def get_total_stake_for_hotkey( :param ss58_addresses: The SS58 address(es) of the hotkey(s) :param netuids: The netuids to retrieve the stake from. If not specified, will use all subnets. :param block_hash: The hash of the block number to retrieve the stake from. - :param reuse_block: Whether to reuse the last-used block hash when retrieving info. :return: { @@ -476,10 +457,7 @@ async def get_total_stake_for_hotkey( } """ if not block_hash: - if reuse_block: - block_hash = self.substrate.last_block_hash - else: - block_hash = await self.substrate.get_chain_head() + block_hash = await self.substrate.get_chain_head() netuids = netuids or await self.get_all_subnet_netuids(block_hash=block_hash) calls = [ @@ -511,7 +489,6 @@ async def current_take( self, hotkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> Optional[float]: """ Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' @@ -519,7 +496,6 @@ async def current_take( :param hotkey_ss58: The `SS58` address of the neuron's hotkey. :param block_hash: The hash of the block number to retrieve the stake from. - :param reuse_block: Whether to reuse the last-used block hash when retrieving info. :return: The delegate take percentage, None if not available. @@ -531,7 +507,6 @@ async def current_take( storage_function="Delegates", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if result is None: return None @@ -542,7 +517,6 @@ async def get_netuids_for_hotkey( self, hotkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> list[int]: """ Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function @@ -551,7 +525,6 @@ async def get_netuids_for_hotkey( :param hotkey_ss58: The ``SS58`` address of the neuron's hotkey. :param block_hash: The hash of the blockchain block number at which to perform the query. - :param reuse_block: Whether to reuse the last-used block hash when retrieving info. :return: A list of netuids where the neuron is a member. """ @@ -561,7 +534,6 @@ async def get_netuids_for_hotkey( storage_function="IsNetworkMember", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, fully_exhaust=True, page_size=200, ) @@ -575,14 +547,12 @@ async def is_subnet_active( self, netuid: int, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> bool: """Verify if subnet with provided netuid is active. Args: netuid (int): The unique identifier of the subnet. block_hash (Optional[str]): The blockchain block_hash representation of block id. - reuse_block (bool): Whether to reuse the last-used block hash. Returns: True if subnet is active, False otherwise. @@ -593,20 +563,18 @@ async def is_subnet_active( module="SubtensorModule", storage_function="FirstEmissionBlockNumber", block_hash=block_hash, - reuse_block_hash=reuse_block, params=[netuid], ) return True if query and query > 0 else False async def subnet_exists( - self, netuid: int, block_hash: Optional[str] = None, reuse_block: bool = False + self, netuid: int, block_hash: Optional[str] = None ) -> bool: """ Checks if a subnet with the specified unique identifier (netuid) exists within the Bittensor network. :param netuid: The unique identifier of the subnet. :param block_hash: The hash of the blockchain block number at which to check the subnet existence. - :param reuse_block: Whether to reuse the last-used block hash. :return: `True` if the subnet exists, `False` otherwise. @@ -618,18 +586,14 @@ async def subnet_exists( storage_function="NetworksAdded", params=[netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return result - async def total_networks( - self, block_hash: Optional[str] = None, reuse_block: bool = False - ) -> int: + async def total_networks(self, block_hash: Optional[str] = None) -> int: """ Returns the total number of subnets in the Bittensor network. :param block_hash: The hash of the blockchain block number at which to check the subnet existence. - :param reuse_block: Whether to reuse the last-used block hash. :return: The total number of subnets in the network. """ @@ -638,7 +602,6 @@ async def total_networks( storage_function="TotalNetworks", params=[], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return result @@ -670,7 +633,6 @@ async def get_hyperparameter( param_name: str, netuid: int, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> Optional[Any]: """ Retrieves a specified hyperparameter for a specific subnet. @@ -678,7 +640,6 @@ async def get_hyperparameter( :param param_name: The name of the hyperparameter to retrieve. :param netuid: The unique identifier of the subnet. :param block_hash: The hash of blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used block hash. :return: The value of the specified hyperparameter if the subnet exists, or None """ @@ -691,7 +652,6 @@ async def get_hyperparameter( storage_function=param_name, params=[netuid], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if result is None: @@ -705,7 +665,6 @@ async def filter_netuids_by_registered_hotkeys( filter_for_netuids: Iterable[int], all_hotkeys: Iterable[Wallet], block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> list[int]: """ Filters a given list of all netuids for certain specified netuids and hotkeys @@ -714,7 +673,6 @@ async def filter_netuids_by_registered_hotkeys( :param filter_for_netuids: A subset of all_netuids to filter from the main list :param all_hotkeys: Hotkeys to filter from the main list :param block_hash: hash of the blockchain block number at which to perform the query. - :param reuse_block: whether to reuse the last-used blockchain hash when retrieving info. :return: the filtered list of netuids. """ @@ -724,7 +682,6 @@ async def filter_netuids_by_registered_hotkeys( *[ self.get_netuids_for_hotkey( get_hotkey_pub_ss58(wallet), - reuse_block=reuse_block, block_hash=block_hash, ) for wallet in all_hotkeys @@ -753,7 +710,7 @@ async def filter_netuids_by_registered_hotkeys( return list(set(all_netuids)) async def get_existential_deposit( - self, block_hash: Optional[str] = None, reuse_block: bool = False + self, block_hash: Optional[str] = None ) -> Balance: """ Retrieves the existential deposit amount for the Bittensor blockchain. The existential deposit @@ -761,7 +718,6 @@ async def get_existential_deposit( balances below this threshold can be reaped to conserve network resources. :param block_hash: Block hash at which to query the deposit amount. If `None`, the current block is used. - :param reuse_block: Whether to reuse the last-used blockchain block hash. :return: The existential deposit amount @@ -773,7 +729,6 @@ async def get_existential_deposit( module_name="Balances", constant_name="ExistentialDeposit", block_hash=block_hash, - reuse_block_hash=reuse_block, ), "value", None, @@ -819,7 +774,7 @@ async def neurons( return neurons async def neurons_lite( - self, netuid: int, block_hash: Optional[str] = None, reuse_block: bool = False + self, netuid: int, block_hash: Optional[str] = None ) -> list[NeuronInfoLite]: """ Retrieves a list of neurons in a 'lite' format from a specific subnet of the Bittensor network. @@ -828,7 +783,6 @@ async def neurons_lite( :param netuid: The unique identifier of the subnet. :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used blockchain block hash. :return: A list of simplified neuron information for the subnet. @@ -840,7 +794,6 @@ async def neurons_lite( method="get_neurons_lite", params=[netuid], block_hash=block_hash, - reuse_block=reuse_block, ) if result is None: @@ -888,7 +841,6 @@ async def get_delegated( self, coldkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> list[tuple[DelegateInfo, Balance]]: """ Retrieves a list of delegates and their associated stakes for a given coldkey. This function @@ -896,19 +848,12 @@ async def get_delegated( :param coldkey_ss58: The `SS58` address of the account's coldkey. :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used blockchain block hash. :return: A list of tuples, each containing a delegate's information and staked amount. This function is important for account holders to understand their stake allocations and their involvement in the network's delegation and consensus mechanisms. """ - - block_hash = ( - block_hash - if block_hash - else (self.substrate.last_block_hash if reuse_block else None) - ) result = await self.query_runtime_api( runtime_api="DelegateInfoRuntimeApi", method="get_delegated", @@ -924,13 +869,11 @@ async def get_delegated( async def query_all_identities( self, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict[str, dict]: """ Queries all identities on the Bittensor blockchain. :param block_hash: The hash of the blockchain block number at which to perform the query. - :param reuse_block: Whether to reuse the last-used blockchain block hash. :return: A dictionary mapping addresses to their decoded identity data. """ @@ -939,7 +882,6 @@ async def query_all_identities( module="SubtensorModule", storage_function="IdentitiesV2", block_hash=block_hash, - reuse_block_hash=reuse_block, fully_exhaust=True, ) all_identities = { @@ -952,7 +894,6 @@ async def query_identity( self, key: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict: """ Queries the identity of a neuron on the Bittensor blockchain using the given key. This function retrieves @@ -965,7 +906,6 @@ async def query_identity( :param key: The key used to query the neuron's identity, typically the neuron's SS58 address. :param block_hash: The hash of the blockchain block number at which to perform the query. - :param reuse_block: Whether to reuse the last-used blockchain block hash. :return: An object containing the identity information of the neuron if found, ``None`` otherwise. @@ -977,7 +917,6 @@ async def query_identity( storage_function="IdentitiesV2", params=[key], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if not identity_info: return {} @@ -1087,14 +1026,12 @@ async def does_hotkey_exist( self, hotkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> bool: """ Returns true if the hotkey is known by the chain and there are accounts. :param hotkey_ss58: The SS58 address of the hotkey. :param block_hash: The hash of the block number to check the hotkey against. - :param reuse_block: Whether to reuse the last-used blockchain hash. :return: `True` if the hotkey is known by the chain and there are accounts, `False` otherwise. """ @@ -1103,7 +1040,6 @@ async def does_hotkey_exist( storage_function="Owner", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return_val = result != GENESIS_ADDRESS return return_val @@ -1473,7 +1409,6 @@ async def get_vote_data( self, proposal_hash: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> Optional["ProposalVoteData"]: """ Retrieves the voting data for a specific proposal on the Bittensor blockchain. This data includes @@ -1481,7 +1416,6 @@ async def get_vote_data( :param proposal_hash: The hash of the proposal for which voting data is requested. :param block_hash: The hash of the blockchain block number to query the voting data. - :param reuse_block: Whether to reuse the last-used blockchain block hash. :return: An object containing the proposal's voting data, or `None` if not found. @@ -1493,7 +1427,6 @@ async def get_vote_data( storage_function="Voting", params=[proposal_hash], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if vote_data is None: return None @@ -1686,14 +1619,12 @@ async def get_owned_hotkeys( self, coldkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> list[str]: """ Retrieves all hotkeys owned by a specific coldkey address. :param coldkey_ss58: The SS58 address of the coldkey to query. :param block_hash: The hash of the blockchain block number for the query. - :param reuse_block: Whether to reuse the last-used blockchain block hash. :return: A list of hotkey SS58 addresses owned by the coldkey. """ @@ -1702,7 +1633,6 @@ async def get_owned_hotkeys( storage_function="OwnedHotkeys", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return owned_hotkeys @@ -1805,13 +1735,11 @@ async def sim_swap( async def get_coldkey_swap_announcements( self, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> list[ColdkeySwapAnnouncementInfo]: """Fetches all pending coldkey swap announcements. Args: block_hash: Block hash at which to perform query. - reuse_block: Whether to reuse the last-used block hash. Returns: A list of ColdkeySwapAnnouncementInfo for all pending announcements. @@ -1820,7 +1748,6 @@ async def get_coldkey_swap_announcements( module="SubtensorModule", storage_function="ColdkeySwapAnnouncements", block_hash=block_hash, - reuse_block_hash=reuse_block, fully_exhaust=True, page_size=200, ) @@ -1836,14 +1763,12 @@ async def get_coldkey_swap_announcement( self, coldkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> Optional[ColdkeySwapAnnouncementInfo]: """Fetches a pending coldkey swap announcement for a specific coldkey. Args: coldkey_ss58: The SS58 address of the coldkey to query. block_hash: Block hash at which to perform query. - reuse_block: Whether to reuse the last-used block hash. Returns: ColdkeySwapAnnouncementInfo if an announcement exists, None otherwise. @@ -1853,7 +1778,6 @@ async def get_coldkey_swap_announcement( storage_function="ColdkeySwapAnnouncements", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if result is None: @@ -1864,13 +1788,11 @@ async def get_coldkey_swap_announcement( async def get_coldkey_swap_disputes( self, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> list[tuple[str, int]]: """Fetch all coldkey swap disputes. Args: block_hash: Optional block hash at which to query storage. - reuse_block: Whether to reuse the last-used block hash. Returns: list[tuple[str, int]]: Tuples of `(coldkey_ss58, disputed_block)`. @@ -1879,7 +1801,6 @@ async def get_coldkey_swap_disputes( module="SubtensorModule", storage_function="ColdkeySwapDisputes", block_hash=block_hash, - reuse_block_hash=reuse_block, fully_exhaust=True, page_size=200, ) @@ -1893,14 +1814,12 @@ async def get_coldkey_swap_dispute( self, coldkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> Optional[int]: """Fetch the disputed block for a given coldkey swap. Args: coldkey_ss58: Coldkey SS58 address. block_hash: Optional block hash at which to query storage. - reuse_block: Whether to reuse the last-used block hash. Returns: int | None: Block number when disputed, or None if no dispute exists. @@ -1910,7 +1829,6 @@ async def get_coldkey_swap_dispute( storage_function="ColdkeySwapDisputes", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if result is None: @@ -2052,7 +1970,6 @@ async def get_crowdloan_contributors( async def get_coldkey_swap_announcement_delay( self, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> int: """Retrieves the delay (in blocks) before a coldkey swap can be executed. @@ -2061,7 +1978,6 @@ async def get_coldkey_swap_announcement_delay( Args: block_hash: The hash of the blockchain block number for the query. - reuse_block: Whether to reuse the last-used blockchain block hash. Returns: The number of blocks to wait after announcement. @@ -2071,7 +1987,6 @@ async def get_coldkey_swap_announcement_delay( storage_function="ColdkeySwapAnnouncementDelay", params=[], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return result @@ -2079,7 +1994,6 @@ async def get_coldkey_swap_announcement_delay( async def get_coldkey_swap_reannouncement_delay( self, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> int: """Retrieves the delay (in blocks) before the user can reannounce a coldkey swap. @@ -2088,7 +2002,6 @@ async def get_coldkey_swap_reannouncement_delay( Args: block_hash: The hash of the blockchain block number for the query. - reuse_block: Whether to reuse the last-used blockchain block hash. Returns: The number of blocks to wait before reannouncing. @@ -2098,7 +2011,6 @@ async def get_coldkey_swap_reannouncement_delay( storage_function="ColdkeySwapReannouncementDelay", params=[], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return result @@ -2106,13 +2018,11 @@ async def get_coldkey_swap_reannouncement_delay( async def get_coldkey_swap_cost( self, block_hash: Optional[str] = None, - reuse_block: bool = False, - ) -> Balance: + ) -> Optional[Balance]: """Retrieves the fee required to announce a coldkey swap. Args: block_hash: Block hash at which to query the constant. - reuse_block: Whether to reuse the last-used block hash. Returns: The swap cost as a Balance object. Returns 0 TAO if constant not found. @@ -2121,7 +2031,6 @@ async def get_coldkey_swap_cost( module_name="SubtensorModule", constant_name="KeySwapCost", block_hash=block_hash, - reuse_block_hash=reuse_block, ) if swap_cost is None: return None @@ -2131,7 +2040,6 @@ async def get_coldkey_claim_type( self, coldkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict: """ Retrieves the root claim type for a specific coldkey. @@ -2144,7 +2052,6 @@ async def get_coldkey_claim_type( Args: coldkey_ss58: The SS58 address of the coldkey to query. block_hash: The hash of the blockchain block number for the query. - reuse_block: Whether to reuse the last-used blockchain block hash. Returns: dict: Claim type information in one of these formats: @@ -2157,7 +2064,6 @@ async def get_coldkey_claim_type( storage_function="RootClaimType", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if result is None: @@ -2175,14 +2081,12 @@ async def get_coldkey_claim_type( async def get_all_coldkeys_claim_type( self, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict[str, dict[str, str | list[int]]]: """ Retrieves all root claim types for all coldkeys in the network. Args: block_hash: The hash of the blockchain block number for the query. - reuse_block: Whether to reuse the last-used blockchain block hash. Returns: dict[str, dict]: Mapping of coldkey SS58 addresses to claim type dicts @@ -2192,7 +2096,6 @@ async def get_all_coldkeys_claim_type( storage_function="RootClaimType", params=[], block_hash=block_hash, - reuse_block_hash=reuse_block, fully_exhaust=True, page_size=1_000, ) @@ -2226,14 +2129,12 @@ async def get_staking_hotkeys( self, coldkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> list[str]: """Retrieves all hotkeys that a coldkey is staking to. Args: coldkey_ss58: The SS58 address of the coldkey. block_hash: The hash of the blockchain block for the query. - reuse_block: Whether to reuse the last-used blockchain block hash. Returns: list[str]: A list of hotkey SS58 addresses that the coldkey has staked to. @@ -2243,7 +2144,6 @@ async def get_staking_hotkeys( storage_function="StakingHotkeys", params=[coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return result @@ -2253,7 +2153,6 @@ async def get_claimed_amount( hotkey_ss58: str, netuid: int, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> Balance: """Retrieves the root claimed Alpha shares for coldkey from hotkey in provided subnet. @@ -2262,7 +2161,6 @@ async def get_claimed_amount( hotkey_ss58: The SS58 address of the root validator. netuid: The unique identifier of the subnet. block_hash: The blockchain block hash for the query. - reuse_block: Whether to reuse the last-used blockchain block hash. Returns: Balance: The number of Alpha stake claimed from the root validator. @@ -2272,7 +2170,6 @@ async def get_claimed_amount( storage_function="RootClaimed", params=[netuid, hotkey_ss58, coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) return Balance.from_rao(query).set_unit(netuid=netuid) @@ -2281,7 +2178,6 @@ async def get_claimed_amount_all_netuids( coldkey_ss58: str, hotkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict[int, Balance]: """Retrieves the root claimed Alpha shares for coldkey from hotkey in all subnets. @@ -2289,7 +2185,6 @@ async def get_claimed_amount_all_netuids( coldkey_ss58: The SS58 address of the staker. hotkey_ss58: The SS58 address of the root validator. block_hash: The blockchain block hash for the query. - reuse_block: Whether to reuse the last-used blockchain block hash. Returns: dict[int, Balance]: Dictionary mapping netuid to claimed stake. @@ -2299,7 +2194,6 @@ async def get_claimed_amount_all_netuids( storage_function="RootClaimed", params=[hotkey_ss58, coldkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, fully_exhaust=True, page_size=200, ) @@ -2312,14 +2206,12 @@ async def get_claimable_rate_all_netuids( self, hotkey_ss58: str, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> dict[int, float]: """Retrieves all root claimable rates from a given hotkey address for all subnets with this validator. Args: hotkey_ss58: The SS58 address of the root validator. block_hash: The blockchain block hash for the query. - reuse_block: Whether to reuse the last-used blockchain block hash. Returns: dict[int, float]: Dictionary mapping netuid to claimable rate. @@ -2329,7 +2221,6 @@ async def get_claimable_rate_all_netuids( storage_function="RootClaimable", params=[hotkey_ss58], block_hash=block_hash, - reuse_block_hash=reuse_block, ) if not query: @@ -2343,7 +2234,6 @@ async def get_claimable_rate_netuid( hotkey_ss58: str, netuid: int, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> float: """Retrieves the root claimable rate from a given hotkey address for provided netuid. @@ -2351,7 +2241,6 @@ async def get_claimable_rate_netuid( hotkey_ss58: The SS58 address of the root validator. netuid: The unique identifier of the subnet to get the rate. block_hash: The blockchain block hash for the query. - reuse_block: Whether to reuse the last-used blockchain block hash. Returns: float: The rate of claimable stake from validator's hotkey for provided subnet. @@ -2359,7 +2248,6 @@ async def get_claimable_rate_netuid( all_rates = await self.get_claimable_rate_all_netuids( hotkey_ss58=hotkey_ss58, block_hash=block_hash, - reuse_block=reuse_block, ) return all_rates.get(netuid, 0.0) @@ -2369,7 +2257,6 @@ async def get_claimable_stake_for_netuid( hotkey_ss58: str, netuid: int, block_hash: Optional[str] = None, - reuse_block: bool = False, ) -> Balance: """Retrieves the root claimable stake for a given coldkey address. @@ -2378,7 +2265,6 @@ async def get_claimable_stake_for_netuid( hotkey_ss58: The root validator hotkey SS58 address. netuid: Delegate's netuid where stake will be claimed. block_hash: The blockchain block hash for the query. - reuse_block: Whether to reuse the last-used blockchain block hash. Returns: Balance: Available for claiming root stake. @@ -2397,14 +2283,12 @@ async def get_claimable_stake_for_netuid( hotkey_ss58=hotkey_ss58, netuid=netuid, block_hash=block_hash, - reuse_block=reuse_block, ), self.get_claimed_amount( coldkey_ss58=coldkey_ss58, hotkey_ss58=hotkey_ss58, netuid=netuid, block_hash=block_hash, - reuse_block=reuse_block, ), ) diff --git a/bittensor_cli/src/commands/stake/auto_staking.py b/bittensor_cli/src/commands/stake/auto_staking.py index 39896294f..86d3bc4d4 100644 --- a/bittensor_cli/src/commands/stake/auto_staking.py +++ b/bittensor_cli/src/commands/stake/auto_staking.py @@ -52,7 +52,6 @@ async def show_auto_stake_destinations( subtensor.get_auto_stake_destinations( coldkey_ss58=coldkey_ss58, block_hash=chain_head, - reuse_block=True, ), subtensor.fetch_coldkey_hotkey_identities(block_hash=chain_head), ) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 6137ce29a..7bb3da769 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -1376,7 +1376,7 @@ async def senate_vote( return False console.print(f"Fetching proposals in [dark_orange]network: {subtensor.network}") - vote_data = await subtensor.get_vote_data(proposal_hash, reuse_block=True) + vote_data = await subtensor.get_vote_data(proposal_hash) if not vote_data: print_error("Failed: Proposal not found.") return False diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 5c16b9733..ea5a539cb 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1055,7 +1055,7 @@ async def overview( neurons: dict[str, list[NeuronInfoLite]] = {} netuids = await subtensor.filter_netuids_by_registered_hotkeys( - all_netuids, netuids_filter, all_hotkeys, reuse_block=True + all_netuids, netuids_filter, all_hotkeys ) for netuid in netuids: diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 3fa0135c3..c453255d5 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -97,7 +97,6 @@ async def set_weights_extrinsic(self) -> tuple[bool, str, Optional[str]]: await self.subtensor.get_hyperparameter( param_name="get_commit_reveal_weights_enabled", netuid=self.netuid, - reuse_block=False, ) ): return await self._commit_reveal( @@ -162,7 +161,6 @@ async def _commit_reveal( await self.subtensor.get_hyperparameter( param_name="get_commit_reveal_period", netuid=self.netuid, - reuse_block=False, ) ) From d17ef4992040a72ea579147468e3807c5382d4ff Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 12:26:09 +0200 Subject: [PATCH 33/72] Import cleanup and added TODOs --- bittensor_cli/__init__.py | 2 +- bittensor_cli/src/bittensor/chain_data.py | 5 ++--- .../src/bittensor/extrinsics/registration.py | 1 - .../src/bittensor/extrinsics/transfer.py | 1 - bittensor_cli/src/bittensor/utils.py | 18 ------------------ .../src/commands/stake/children_hotkeys.py | 1 - bittensor_cli/src/commands/stake/list.py | 3 ++- bittensor_cli/src/commands/stake/move.py | 1 - bittensor_cli/src/commands/stake/remove.py | 1 - 9 files changed, 5 insertions(+), 28 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 656f68a42..c3f0e0102 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -19,4 +19,4 @@ from .version import __version__, __version_as_int__ -__all__ = ["CLIManager"] +__all__ = ["CLIManager", "__version__", "__version_as_int__"] diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 7fd1e2049..6f18cc919 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1,8 +1,7 @@ from abc import abstractmethod from dataclasses import dataclass -from collections.abc import Sequence from enum import Enum -from typing import Optional, Any, Union, Callable, Hashable +from typing import Optional, Any, Union import netaddr from scalecodec.utils.ss58 import ss58_encode @@ -780,7 +779,7 @@ def tao_to_alpha_with_slippage( if self.is_dynamic: new_tao_in = self.tao_in + tao if new_tao_in == 0: - return tao, Balance.from_rao(0) + return tao, Balance.from_rao(0), 0.0 new_alpha_in = self.k / new_tao_in # Amount of alpha given to the staker diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index e4cc4d183..0193d6702 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -38,7 +38,6 @@ millify, get_human_readable, print_verbose, - print_error, unlock_key, hex_to_bytes, get_hotkey_pub_ss58, diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 2d2369475..383d1f74f 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -15,7 +15,6 @@ print_success, print_verbose, is_valid_bittensor_address_or_public_key, - print_error, unlock_key, ) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index d0a54b115..7cd17abbb 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -27,7 +27,6 @@ from rich.prompt import Confirm, Prompt from rich.table import Table from scalecodec import GenericCall -from scalecodec.utils.ss58 import ss58_encode, ss58_decode import typer @@ -633,23 +632,6 @@ def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool return False -def encode_account_id(ss58_address: str) -> bytes: - return bytes.fromhex(ss58_decode(ss58_address, SS58_FORMAT)) - - -def ss58_to_vec_u8(ss58_address: str) -> list[int]: - """ - Converts an SS58 address to a list of integers (vector of u8). - - :param ss58_address: The SS58 address to be converted. - - :return: A list of integers representing the byte values of the SS58 address. - """ - ss58_bytes: bytes = encode_account_id(ss58_address) - encoded_address: list[int] = [int(byte) for byte in ss58_bytes] - return encoded_address - - def get_explorer_root_url_by_network_from_map( network: str, network_map: dict[str, dict[str, str]] ) -> dict[str, str]: diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 1bf036aff..11115cf39 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -26,7 +26,6 @@ json_console, get_hotkey_pub_ss58, print_extrinsic_id, - err_console, ) diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index 61fc611e2..92cd6f5bb 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -194,6 +194,7 @@ def create_table( tao_value_ = pool.alpha_to_tao(substake_.stake) total_swapped_tao_value_ += tao_value_ + # TODO why is nothing done with `swap_value`? if netuid == 0: swap_value = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]" else: @@ -396,7 +397,7 @@ def format_cell( precision=4, millify=True if not verbose else False, ) - + # TODO why is nothing done with swap_cell if netuid != 0: swap_cell = format_cell( swapped_tao_value_.tao, diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index f618641f7..74bf5bbf1 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -15,7 +15,6 @@ confirm_action, console, create_table, - is_valid_ss58_address, print_error, group_subnets, get_subnet_name, diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 555974504..7cd9b1d0f 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -23,7 +23,6 @@ print_error, get_hotkey_wallets_for_wallet, is_valid_ss58_address, - format_error_message, group_subnets, unlock_key, json_console, From 08a01866e6cc0c78eb0f34922afe99415a4a4a2f Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 13:14:17 +0200 Subject: [PATCH 34/72] Import cleanup --- .github/workflows/ruff-formatter.yml | 4 ++-- tests/unit_tests/test_proxy_address_resolution.py | 1 - tests/unit_tests/test_transfer_extrinsic.py | 1 - tests/unit_tests/test_unstake_helpers.py | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ruff-formatter.yml b/.github/workflows/ruff-formatter.yml index b8e5fbd32..6bc5c582d 100644 --- a/.github/workflows/ruff-formatter.yml +++ b/.github/workflows/ruff-formatter.yml @@ -9,7 +9,7 @@ permissions: on: pull_request: - types: [opened, synchronize, reopened] + types: [ opened, synchronize, reopened ] jobs: ruff: @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v6 - name: Ruff format check - uses: astral-sh/ruff-action@v3 + uses: astral-sh/ruff-action@v4.0.0 with: version: "0.11.5" args: "format --diff" diff --git a/tests/unit_tests/test_proxy_address_resolution.py b/tests/unit_tests/test_proxy_address_resolution.py index 5ca7e97a2..a96bf632d 100644 --- a/tests/unit_tests/test_proxy_address_resolution.py +++ b/tests/unit_tests/test_proxy_address_resolution.py @@ -8,7 +8,6 @@ from contextlib import contextmanager from unittest.mock import AsyncMock, MagicMock, patch -from bittensor_cli.src.bittensor.balances import Balance from tests.unit_tests.conftest import ( PROXY_SS58, HOTKEY_SS58, diff --git a/tests/unit_tests/test_transfer_extrinsic.py b/tests/unit_tests/test_transfer_extrinsic.py index 93c09fdf4..d82e4c95d 100644 --- a/tests/unit_tests/test_transfer_extrinsic.py +++ b/tests/unit_tests/test_transfer_extrinsic.py @@ -5,7 +5,6 @@ and mock_subtensor fixtures from conftest.py. """ -import pytest from unittest.mock import AsyncMock, MagicMock, patch from bittensor_cli.src.bittensor.balances import Balance diff --git a/tests/unit_tests/test_unstake_helpers.py b/tests/unit_tests/test_unstake_helpers.py index 81f0fc5f2..1240c7044 100644 --- a/tests/unit_tests/test_unstake_helpers.py +++ b/tests/unit_tests/test_unstake_helpers.py @@ -9,7 +9,6 @@ - _print_table_and_slippage """ -import pytest from types import SimpleNamespace from unittest.mock import MagicMock, patch From 697e7c708c6b3bdedd6647834ca14637d769330b Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 13:14:55 +0200 Subject: [PATCH 35/72] Add TODO --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e9f2f190a..08f20c63f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "9.20.1" description = "Bittensor CLI" readme = "README.md" authors = [ - {name = "bittensor.com"} + { name = "bittensor.com" } ] license = "MIT" scripts = { btcli = "bittensor_cli.cli:main" } @@ -29,6 +29,7 @@ classifiers = [ ] dependencies = [ "wheel", + # TODO this will need to be changed to asi 2.0-3.0 when merged "async-substrate-interface @ git+https://github.com/opentensor/async-substrate-interface.git@release/2.0.0", "aiohttp~=3.13", "backoff~=2.2.1", From 171b00497e5c19082326d110f49b839490732c2b Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 16:47:36 +0200 Subject: [PATCH 36/72] Debugging --- bittensor_cli/src/bittensor/utils.py | 43 +++++------------------- tests/e2e_tests/test_children_hotkeys.py | 6 +++- tests/unit_tests/test_utils.py | 12 +++---- 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 7cd17abbb..06340568f 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -799,48 +799,21 @@ def decode_hex_identity_dict(info_dictionary) -> dict[str, Any]: Examples: input_dict = { "name": {"value": "0x6a6f686e"}, - "additional": [ - {"data1": "0x64617461"}, - ("data2", "0x64617461") - ] + "additional": "0x64617461" } decode_hex_identity_dict(input_dict) - {'name': 'john', 'additional': [('data1', 'data'), ('data2', 'data')]} + {'name': 'john', 'additional': "data"]} """ - def get_decoded(data: Optional[str]) -> str: - """Decodes a hex-encoded string.""" - if data is None: - return "" - try: - return hex_to_bytes(data).decode() - except (UnicodeDecodeError, ValueError): - print_error(f"Could not decode: {key}: {item}") - raise ValueError - for key, value in info_dictionary.items(): if isinstance(value, dict): item = list(value.values())[0] - if isinstance(item, str) and item.startswith("0x"): - try: - info_dictionary[key] = get_decoded(item) - except UnicodeDecodeError: - print_error(f"Could not decode: {key}: {item}") - else: - info_dictionary[key] = item - if key == "additional": - additional = [] - for item in value: - if isinstance(item, dict): - for k, v in item.items(): - additional.append((k, get_decoded(v))) - else: - if isinstance(item, (tuple, list)) and len(item) == 2: - k_, v = item - k = k_ if k_ is not None else "" - additional.append((k, get_decoded(v))) - info_dictionary[key] = additional - + else: + item = value + if isinstance(item, str) and item.startswith("0x"): + info_dictionary[key] = hex_to_bytes(item.removeprefix("0x")).decode() + else: + info_dictionary[key] = item return info_dictionary diff --git a/tests/e2e_tests/test_children_hotkeys.py b/tests/e2e_tests/test_children_hotkeys.py index 2b1d80681..80a4b2f64 100644 --- a/tests/e2e_tests/test_children_hotkeys.py +++ b/tests/e2e_tests/test_children_hotkeys.py @@ -215,7 +215,11 @@ def test_children_hotkeys(local_chain, wallet_setup): "--json-output", ], ) - set_take_result_json = json.loads(set_take_result.stdout) + try: + set_take_result_json = json.loads(set_take_result.stdout) + except json.decoder.JSONDecodeError: + print(set_take_result.stdout, set_take_result.stderr) + raise Exception assert set_take_result_json[str(netuid)]["success"] is True, ( f"Take not set:\n{set_take_result.stdout}\n{set_take_result.stderr}" ) diff --git a/tests/unit_tests/test_utils.py b/tests/unit_tests/test_utils.py index 6a3774d0f..636fde981 100644 --- a/tests/unit_tests/test_utils.py +++ b/tests/unit_tests/test_utils.py @@ -14,20 +14,20 @@ ( { "name": {"value": "0x6a6f686e"}, - "additional": [{"data1": "0x64617461"}, ("data2", "0x64617461")], + "additional": "0x64617461", }, - {"name": "john", "additional": [("data1", "data"), ("data2", "data")]}, + {"name": "john", "additional": "data"}, ), ( - {"name": {"value": "0x6a6f686e"}, "additional": [("data2", "0x64617461")]}, - {"name": "john", "additional": [("data2", "data")]}, + {"name": {"value": "0x6a6f686e"}, "additional": "0x64617461"}, + {"name": "john", "additional": "data"}, ), ( { "name": {"value": "0x6a6f686e"}, - "additional": [(None, None)], + "additional": None, }, - {"name": "john", "additional": [("", "")]}, + {"name": "john", "additional": None}, ), ], ) From 82fdb23462a759751164afffe350e1b3f7848840 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 14 Apr 2026 16:57:52 +0200 Subject: [PATCH 37/72] Bump cyscale --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 08f20c63f..e3d067792 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0", "rich>=13.7,<15.0", - "cyscale==0.1.11", + "cyscale==0.2.0", "typer>=0.16", "typing_extensions>4.0.0; python_version<'3.11'", "bittensor-wallet==4.0.1", From dce83ba4132b943a496fc29d3e939523ef5726ad Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 17:40:04 +0200 Subject: [PATCH 38/72] Bump cyscale --- pyproject.toml | 2 +- tests/unit_tests/test_subnets_register.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3d067792..d39d3c84a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0", "rich>=13.7,<15.0", - "cyscale==0.2.0", + "cyscale==0.2.2", "typer>=0.16", "typing_extensions>4.0.0; python_version<'3.11'", "bittensor-wallet==4.0.1", diff --git a/tests/unit_tests/test_subnets_register.py b/tests/unit_tests/test_subnets_register.py index b766d8739..45355f1fc 100644 --- a/tests/unit_tests/test_subnets_register.py +++ b/tests/unit_tests/test_subnets_register.py @@ -2,7 +2,6 @@ Unit tests for subnets register command. """ -from asyncio import Future import pytest from unittest.mock import AsyncMock, patch From 0dbf9721c7e88cf75bfc4bdf476c3976f5dd688b Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 19:30:35 +0200 Subject: [PATCH 39/72] Clean up logic --- .../src/bittensor/subtensor_interface.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 04c750415..9f9efa5dd 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -13,7 +13,7 @@ from bittensor_wallet import Wallet from bittensor_wallet.bittensor_wallet import Keypair from bittensor_wallet.utils import SS58_FORMAT -from scalecodec import GenericCall, ScaleBytes +from scalecodec import GenericCall, ScaleBytes, ScaleValue import typer import websockets @@ -144,9 +144,9 @@ async def query( block_hash: Optional[str] = None, raw_storage_key: Optional[bytes] = None, subscription_handler=None, - ) -> Any: + ) -> ScaleValue: """ - Pass-through to substrate.query which automatically returns the .value if it's a ScaleType + Pass-through to substrate.query which automatically returns the .value """ result: Optional[ScaleType] = await self.substrate.query( module, @@ -156,7 +156,7 @@ async def query( raw_storage_key, subscription_handler, ) - return getattr(result, "value", result) + return result.value async def _decode_inline_call( self, @@ -308,7 +308,7 @@ async def query_runtime_api( method: str, params: Optional[Union[list, dict]] = None, block_hash: Optional[str] = None, - ) -> Optional[Any]: + ) -> ScaleValue: """ Queries the runtime API of the Bittensor blockchain, providing a way to interact with the underlying runtime and retrieve data encoded in Scale Bytes format. This function is essential for advanced users @@ -2059,7 +2059,7 @@ async def get_coldkey_claim_type( - {"type": "Keep"} - {"type": "KeepSubnets", "subnets": [1, 5, 10, ...]} """ - result = await self.query( + result: Optional[str | dict] = await self.query( module="SubtensorModule", storage_function="RootClaimType", params=[coldkey_ss58], @@ -2069,14 +2069,13 @@ async def get_coldkey_claim_type( if result is None: return {"type": "Swap"} - claim_type_key = next(iter(result.keys())) - - if claim_type_key == "KeepSubnets": - subnets_data = result["KeepSubnets"]["subnets"] - subnet_list = sorted([subnet for subnet in subnets_data[0]]) - return {"type": "KeepSubnets", "subnets": subnet_list} + if isinstance(result, str): + return {"type": result} else: - return {"type": claim_type_key} + claim_type = next(iter(result.keys())) + subnets_data = result[claim_type]["subnets"] + subnet_list = sorted(subnets_data) + return {"type": claim_type, "subnets": subnet_list} async def get_all_coldkeys_claim_type( self, @@ -2504,7 +2503,7 @@ async def get_subnet_ema_tao_inflow( Returns: Balance(EMA TAO inflow). """ - value = await self.substrate.query( + value = await self.query( module="SubtensorModule", storage_function="SubnetEmaTaoFlow", params=[netuid], From 20e1f3f19e4620702576dfcebda84a0d14cb6436 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 19:33:01 +0200 Subject: [PATCH 40/72] Clean up --- bittensor_cli/src/bittensor/subtensor_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 9f9efa5dd..3d6545af9 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2114,7 +2114,7 @@ async def get_all_coldkeys_claim_type( claim_type_dict = claim_type_data if claim_type_key == "KeepSubnets": subnets_data = claim_type_dict["KeepSubnets"]["subnets"] - subnet_list = sorted([subnet for subnet in subnets_data[0]]) + subnet_list = sorted([subnet for subnet in subnets_data]) root_claim_types[coldkey_ss58] = { "type": "KeepSubnets", "subnets": subnet_list, From 737033395a3750c49d9a7bff39637a0e01f52de8 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 20:09:35 +0200 Subject: [PATCH 41/72] Debug --- bittensor_cli/cli.py | 1 - tests/e2e_tests/test_children_hotkeys.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b2dc910f9..040afef53 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7740,7 +7740,6 @@ def subnets_show( selected_mechanism_id = self.ask_subnet_mechanism( mechanism_id, mechanism_count, netuid ) - return self._run_command( subnets.show( subtensor=subtensor, diff --git a/tests/e2e_tests/test_children_hotkeys.py b/tests/e2e_tests/test_children_hotkeys.py index 80a4b2f64..c7f3f2982 100644 --- a/tests/e2e_tests/test_children_hotkeys.py +++ b/tests/e2e_tests/test_children_hotkeys.py @@ -218,7 +218,7 @@ def test_children_hotkeys(local_chain, wallet_setup): try: set_take_result_json = json.loads(set_take_result.stdout) except json.decoder.JSONDecodeError: - print(set_take_result.stdout, set_take_result.stderr) + print("DEBUG221", set_take_result.stdout, set_take_result.stderr) raise Exception assert set_take_result_json[str(netuid)]["success"] is True, ( f"Take not set:\n{set_take_result.stdout}\n{set_take_result.stderr}" From 1bace1e24ae6c7c5023ef0bff8ad0f3d569b4e18 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 20:11:22 +0200 Subject: [PATCH 42/72] Ruff --- tests/unit_tests/test_subnets_register.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit_tests/test_subnets_register.py b/tests/unit_tests/test_subnets_register.py index 45355f1fc..6019be767 100644 --- a/tests/unit_tests/test_subnets_register.py +++ b/tests/unit_tests/test_subnets_register.py @@ -2,7 +2,6 @@ Unit tests for subnets register command. """ - import pytest from unittest.mock import AsyncMock, patch From 78e5d4536f723a280a51c2a4611f835f9260cf74 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 20:23:40 +0200 Subject: [PATCH 43/72] Test fix --- bittensor_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 040afef53..cc228f159 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6563,7 +6563,7 @@ def stake_childkey_take( wallet_name, wallet_path, wallet_hotkey, - ask_for=[WO.NAME, WO.HOTKEY], + ask_for=[WO.NAME], validate=WV.WALLET_AND_HOTKEY, ) if all_netuids and netuid: From 628e7e30afe75a16b82e6a83e41122a43f5af9d3 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 16 Apr 2026 20:37:28 +0200 Subject: [PATCH 44/72] Bug fix --- bittensor_cli/src/bittensor/balances.py | 16 ++-------------- .../src/commands/liquidity/liquidity.py | 15 ++++++--------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index 34711f46b..9ec33e2df 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.py @@ -19,6 +19,7 @@ from typing import Union from bittensor_cli.src import UNITS +from scalecodec.utils.math import fixed_to_float class Balance: @@ -297,17 +298,4 @@ def set_unit(self, netuid: int): return self -def fixed_to_float(fixed, frac_bits: int = 64, total_bits: int = 128) -> float: - # By default, this is a U64F64 - # which is 64 bits of integer and 64 bits of fractional - - data: int = fixed["bits"] - - # Logical and to get the fractional part; remaining is the integer part - fractional_part = data & (2**frac_bits - 1) - # Shift to get the integer part from the remaining bits - integer_part = data >> (total_bits - frac_bits) - - frac_float = fractional_part / (2**frac_bits) - - return integer_part + frac_float +__all__ = ["Balance", "UNITS", "fixed_to_float"] diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index 7997afc5f..32a3844ca 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -378,10 +378,9 @@ async def get_liquidity_list( preprocessed_positions = [] positions_futures = [] - async for _, p in positions_response: - position = p.value - tick_index_low = position.get("tick_low")[0] - tick_index_high = position.get("tick_high")[0] + async for _, position in positions_response: + tick_index_low = position.get("tick_low") + tick_index_high = position.get("tick_high") preprocessed_positions.append((position, tick_index_low, tick_index_high)) # Get ticks for the position (for below/above fees) @@ -460,12 +459,10 @@ async def get_liquidity_list( lp = LiquidityPosition( **{ - "id": position.get("id")[0], - "price_low": Balance.from_tao( - tick_to_price(position.get("tick_low")[0]) - ), + "id": position.get("id"), + "price_low": Balance.from_tao(tick_to_price(position.get("tick_low"))), "price_high": Balance.from_tao( - tick_to_price(position.get("tick_high")[0]) + tick_to_price(position.get("tick_high")) ), "liquidity": Balance.from_rao(position.get("liquidity")), "fees_tao": fees_tao, From 144e84a7a63d741f69251d19d841702110f38b63 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 17 Apr 2026 13:21:52 +0200 Subject: [PATCH 45/72] Decodes --- bittensor_cli/src/bittensor/chain_data.py | 14 +++++++++++--- bittensor_cli/src/commands/subnets/subnets.py | 1 - pyproject.toml | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 6f18cc919..d8a143fff 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -644,8 +644,10 @@ class SubnetIdentity(InfoBase): @classmethod def _fix_decoded(cls, decoded: dict) -> "SubnetIdentity": + if isinstance(subnet_name := decoded["subnet_name"], list): + subnet_name = bytes(subnet_name).decode("utf-8") return cls( - subnet_name=decoded["subnet_name"], + subnet_name=subnet_name, github_repo=decoded["github_repo"], subnet_contact=decoded["subnet_contact"], subnet_url=decoded["subnet_url"], @@ -688,8 +690,14 @@ def _fix_decoded(cls, decoded: Any) -> "DynamicInfo": """Returns a DynamicInfo object from a decoded DynamicInfo dictionary.""" netuid = int(decoded.get("netuid")) - symbol = bytes([int(b) for b in decoded.get("token_symbol")]).decode() - subnet_name = bytes([int(b) for b in decoded.get("subnet_name")]).decode() + if isinstance(token_symbol := decoded.get("token_symbol", ""), list): + symbol = bytes(token_symbol).decode("utf-8") + else: + symbol = token_symbol + if isinstance(sn_name := decoded.get("subnet_name", ""), list): + subnet_name = bytes(sn_name).decode("utf-8") + else: + subnet_name = sn_name is_dynamic = True if netuid > 0 else False # Patching for netuid 0 owner_hotkey = decoded.get("owner_hotkey") diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 900887aec..e858a57ff 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1379,7 +1379,6 @@ async def show_subnet( if tao_stake.tao > 0: claim_type_info = root_claim_types.get(coldkey_ss58, {"type": "Swap"}) claim_type = format_claim_type_for_subnet(claim_type_info, netuid_) - rows.append( ( str(idx), # UID diff --git a/pyproject.toml b/pyproject.toml index d39d3c84a..576357b74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0", "rich>=13.7,<15.0", - "cyscale==0.2.2", + "cyscale==0.3.0", "typer>=0.16", "typing_extensions>4.0.0; python_version<'3.11'", "bittensor-wallet==4.0.1", From 03e140119ca2f8b1e34cb9b8800d0d02c22bf834 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 17 Apr 2026 19:48:14 +0200 Subject: [PATCH 46/72] trigger ci From c7e71514d7284a9f02bf25fc6a3dd32f42d6909c Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 20 Apr 2026 19:55:42 +0200 Subject: [PATCH 47/72] Adds registration limit --- bittensor_cli/cli.py | 11 + .../src/bittensor/extrinsics/registration.py | 18 +- bittensor_cli/src/commands/subnets/subnets.py | 2 + tests/e2e_tests/test_registration.py | 266 ++++++++++++++++++ 4 files changed, 291 insertions(+), 6 deletions(-) create mode 100644 tests/e2e_tests/test_registration.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index cc228f159..89f5603eb 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8184,6 +8184,11 @@ def subnets_register( help="Length (in blocks) for which the transaction should be valid. Note that it is possible that if you " "use an era for this transaction that you may pay a different fee to register than the one stated.", ), + limit: Optional[float] = typer.Option( + None, + "--limit", + help="Maximum burn price limit, in Tao, for the registration", + ), proxy: Optional[str] = Options.proxy, json_output: bool = Options.json_output, prompt: bool = Options.prompt, @@ -8201,6 +8206,11 @@ def subnets_register( [green]$[/green] btcli subnets register --netuid 1 """ + if limit is not None and netuid == 0: + raise typer.BadParameter( + "Cannot specify both `--limit` and `--netuid 0`, " + "as the limit does not apply for root registrations." + ) self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( @@ -8222,6 +8232,7 @@ def subnets_register( json_output=json_output, prompt=prompt, proxy=proxy, + limit=limit, ) ) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 0193d6702..7068a8320 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -682,6 +682,7 @@ async def burned_register_extrinsic( wait_for_finalization: bool = True, era: Optional[int] = None, proxy: Optional[str] = None, + limit: Optional[float] = None, ) -> tuple[bool, str, Optional[str]]: """Registers the wallet to chain by recycling TAO. @@ -752,13 +753,18 @@ async def burned_register_extrinsic( with console.status( ":satellite: Recycling TAO for Registration...", spinner="aesthetic" ): + call_data = { + "call_module": "SubtensorModule", + "call_params": {"netuid": netuid, "hotkey": get_hotkey_pub_ss58(wallet)}, + "block_hash": block_hash, + } + if limit is not None: + call_data["call_params"]["limit_price"] = Balance.from_tao(limit).rao + call_data["call_function"] = "register_limit" + else: + call_data["call_function"] = "burned_register" call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="burned_register", - call_params={ - "netuid": netuid, - "hotkey": get_hotkey_pub_ss58(wallet), - }, + **call_data, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index e858a57ff..852136d0e 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1787,6 +1787,7 @@ async def register( decline: bool = False, quiet: bool = False, proxy: Optional[str] = None, + limit: Optional[float] = None, ): """Register neuron by recycling some TAO.""" @@ -1908,6 +1909,7 @@ async def _storage_key(storage_fn: str) -> StorageKey: old_balance=balance, era=era, proxy=proxy, + limit=limit, ) if not success: print_error(f"Failure: {msg}") diff --git a/tests/e2e_tests/test_registration.py b/tests/e2e_tests/test_registration.py new file mode 100644 index 000000000..cd8e5fe45 --- /dev/null +++ b/tests/e2e_tests/test_registration.py @@ -0,0 +1,266 @@ +import asyncio + +from .utils import set_storage_extrinsic + + +# @pytest.mark.parametrize("local_chain", [False], indirect=True) +def test_registration(local_chain, wallet_setup): + """ + Test registration both with and without limits + + Steps: + 1. Create wallets for Alice and Bob + 2. Create 2 subnets with Alice + 3. Register Bob in one subnet with no limit + 4. Register Bob in one subnet with limit + """ + print("Testing registration scenarios 🧪") + + # Create wallets for Alice and Bob + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + + # Setup Alice's wallet + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + + # Setup Bob's wallet + keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup( + wallet_path_bob + ) + + # # Call to make Alice root owner + items = [ + ( + bytes.fromhex( + "658faa385070e074c85bf6b568cf055536e3e82152c8758267395fe524fbbd160000" + ), + bytes.fromhex( + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ), + ) + ] + asyncio.run( + set_storage_extrinsic( + local_chain, + wallet=wallet_alice, + items=items, + ) + ) + + # Create first subnet (netuid = 2) + result = exec_command_alice( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--subnet-name", + "Test Subnet 2", + "--repo", + "https://github.com/username/repo", + "--contact", + "test@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "test#1234", + "--description", + "A test subnet for e2e testing", + "--additional-info", + "Test subnet", + "--logo-url", + "https://testsubnet.com/logo.png", + "--no-prompt", + "--no-mev-protection", + ], + ) + assert "✅ Registered subnetwork with netuid: 2" in result.stdout, result.stderr + assert "Your extrinsic has been included" in result.stdout, result.stdout + + # Create second subnet (netuid = 3) + result = exec_command_alice( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--subnet-name", + "Test Subnet 3", + "--repo", + "https://github.com/username/repo", + "--contact", + "test@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "test#1234", + "--description", + "A test subnet for e2e testing", + "--additional-info", + "Test subnet", + "--logo-url", + "https://testsubnet.com/logo.png", + "--no-prompt", + "--no-mev-protection", + ], + ) + assert "✅ Registered subnetwork with netuid: 3" in result.stdout, result.stderr + assert "Your extrinsic has been included" in result.stdout, result.stdout + + # Start emission schedule for subnets + start_call_netuid_0 = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + "0", + "--wallet-name", + wallet_alice.name, + "--no-prompt", + "--chain", + "ws://127.0.0.1:9945", + "--wallet-path", + wallet_path_alice, + ], + ) + assert ( + "Successfully started subnet 0's emission schedule." + in start_call_netuid_0.stdout + ) + assert "Your extrinsic has been included" in start_call_netuid_0.stdout, ( + start_call_netuid_0.stdout + ) + start_call_netuid_2 = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + "2", + "--wallet-name", + wallet_alice.name, + "--no-prompt", + "--chain", + "ws://127.0.0.1:9945", + "--wallet-path", + wallet_path_alice, + ], + ) + assert ( + "Successfully started subnet 2's emission schedule." + in start_call_netuid_2.stdout + ) + assert "Your extrinsic has been included" in start_call_netuid_2.stdout + + start_call_netuid_3 = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + "3", + "--wallet-name", + wallet_alice.name, + "--no-prompt", + "--chain", + "ws://127.0.0.1:9945", + "--wallet-path", + wallet_path_alice, + ], + ) + assert ( + "Successfully started subnet 3's emission schedule." + in start_call_netuid_3.stdout + ) + assert "Your extrinsic has been included" in start_call_netuid_3.stdout + # Register Bob in one subnet (no limit) + register_result = exec_command_bob( + command="subnets", + sub_command="register", + extra_args=[ + "--netuid", + "2", + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--era", + "30", + ], + ) + assert "✅ Registered" in register_result.stdout, register_result.stderr + assert "Your extrinsic has been included" in register_result.stdout, ( + register_result.stdout + ) + + assert "Your extrinsic has been included" in start_call_netuid_3.stdout + # Should fail bc it's never going to be so tiny + register_result = exec_command_bob( + command="subnets", + sub_command="register", + extra_args=[ + "--netuid", + "3", + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--era", + "30", + "--limit", + "0.0005", + "--verbose", + ], + ) + assert all( + x in register_result.stderr + for x in ("Failed", "RegistrationPriceLimitExceeded") + ), register_result.stdout + + register_result = exec_command_bob( + command="subnets", + sub_command="register", + extra_args=[ + "--netuid", + "3", + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--era", + "30", + "--limit", + "10_000.0", + ], + ) + assert "✅ Registered" in register_result.stdout, register_result.stderr + assert "Your extrinsic has been included" in register_result.stdout, ( + register_result.stdout + ) From 6cae547125edbb07c00fabbcfe030f07e9d42d77 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 20 Apr 2026 20:37:13 +0200 Subject: [PATCH 48/72] Uses rate-tolerance instead of direct limit --- bittensor_cli/cli.py | 8 ++--- bittensor_cli/src/commands/subnets/subnets.py | 17 ++++++++-- tests/e2e_tests/test_registration.py | 34 ++++--------------- 3 files changed, 23 insertions(+), 36 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 89f5603eb..333ef6aa2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8184,11 +8184,7 @@ def subnets_register( help="Length (in blocks) for which the transaction should be valid. Note that it is possible that if you " "use an era for this transaction that you may pay a different fee to register than the one stated.", ), - limit: Optional[float] = typer.Option( - None, - "--limit", - help="Maximum burn price limit, in Tao, for the registration", - ), + limit: Optional[float] = Options.rate_tolerance, proxy: Optional[str] = Options.proxy, json_output: bool = Options.json_output, prompt: bool = Options.prompt, @@ -8208,7 +8204,7 @@ def subnets_register( """ if limit is not None and netuid == 0: raise typer.BadParameter( - "Cannot specify both `--limit` and `--netuid 0`, " + "Cannot specify both `--tolerance` and `--netuid 0`, " "as the limit does not apply for root registrations." ) self.verbosity_handler(quiet, verbose, json_output, prompt) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 852136d0e..a17897c6d 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1840,6 +1840,19 @@ async def _storage_key(storage_fn: str) -> StorageKey: ) return + if limit is not None: + with_limit = current_recycle.tao * (1 + limit) + extra_text = ( + f" You have declared a limit of " + f"up to [{COLOR_PALETTE.G.COST}]{Balance.from_tao(with_limit)}[/{COLOR_PALETTE.G.COST}]" + ) + else: + with_limit = None + extra_text = "" + + print_verbose(f"Recycle price: {current_recycle}") + print_verbose(extra_text) + if prompt and not json_output: # Show creation table. table = create_table( @@ -1888,7 +1901,7 @@ async def _storage_key(storage_fn: str) -> StorageKey: confirm_action( f"Your balance is: [{COLOR_PALETTE.G.BAL}]{balance}[/{COLOR_PALETTE.G.BAL}]\n" f"The cost to register by recycle is " - f"[{COLOR_PALETTE.G.COST}]{current_recycle}[/{COLOR_PALETTE.G.COST}]\n" + f"[{COLOR_PALETTE.G.COST}]{current_recycle}.{extra_text}\n" f"Do you want to continue?", default=False, decline=decline, @@ -1909,7 +1922,7 @@ async def _storage_key(storage_fn: str) -> StorageKey: old_balance=balance, era=era, proxy=proxy, - limit=limit, + limit=with_limit, ) if not success: print_error(f"Failure: {msg}") diff --git a/tests/e2e_tests/test_registration.py b/tests/e2e_tests/test_registration.py index cd8e5fe45..863362cdf 100644 --- a/tests/e2e_tests/test_registration.py +++ b/tests/e2e_tests/test_registration.py @@ -211,7 +211,8 @@ def test_registration(local_chain, wallet_setup): ) assert "Your extrinsic has been included" in start_call_netuid_3.stdout - # Should fail bc it's never going to be so tiny + + # use limit of 10% register_result = exec_command_bob( command="subnets", sub_command="register", @@ -229,36 +230,13 @@ def test_registration(local_chain, wallet_setup): "--no-prompt", "--era", "30", - "--limit", - "0.0005", + "--rate-tolerance", + "0.10", "--verbose", ], ) - assert all( - x in register_result.stderr - for x in ("Failed", "RegistrationPriceLimitExceeded") - ), register_result.stdout - - register_result = exec_command_bob( - command="subnets", - sub_command="register", - extra_args=[ - "--netuid", - "3", - "--wallet-path", - wallet_path_bob, - "--wallet-name", - wallet_bob.name, - "--hotkey", - wallet_bob.hotkey_str, - "--chain", - "ws://127.0.0.1:9945", - "--no-prompt", - "--era", - "30", - "--limit", - "10_000.0", - ], + assert "You have declared a limit of up to" in register_result.stdout, ( + register_result.stderr ) assert "✅ Registered" in register_result.stdout, register_result.stderr assert "Your extrinsic has been included" in register_result.stdout, ( From 2512ee7ebc7674d6363b400789605b19febf2339 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 21 Apr 2026 18:58:54 +0200 Subject: [PATCH 49/72] Adjust ASI req --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 576357b74..db4b1c87c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,8 +29,7 @@ classifiers = [ ] dependencies = [ "wheel", - # TODO this will need to be changed to asi 2.0-3.0 when merged - "async-substrate-interface @ git+https://github.com/opentensor/async-substrate-interface.git@release/2.0.0", + "async-substrate-interface==2.0.0", "aiohttp~=3.13", "backoff~=2.2.1", "bittensor-drand>=1.3.0", From 1d89569677c13c893bbb161f4b8a0ebb4d07e7e6 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 23 Apr 2026 16:13:32 +0200 Subject: [PATCH 50/72] Do not unstake from hotkeys that do not exist on chain. --- .../src/bittensor/subtensor_interface.py | 30 +++++++++++++++++++ bittensor_cli/src/commands/stake/remove.py | 9 +++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 3d6545af9..d54aad48d 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1022,6 +1022,36 @@ async def bonds( return b_map + async def do_hotkeys_exist( + self, hotkeys_ss58: Iterable[str], block_hash: Optional[str] = None + ) -> dict[str, bool]: + """ + Checks whether a hotkey exists is known by the chain and there are accounts. + + Args: + hotkeys_ss58: list of hotkey ss58s + block_hash: hash of the block to query + + Returns: + {hotkey ss58: exists (True/False)} + """ + if block_hash is None: + block_hash = await self.substrate.get_chain_head() + keys = [ + await self.substrate.create_storage_key( + "SubtensorModule", "Owner", [ss58], block_hash=block_hash + ) + for ss58 in hotkeys_ss58 + ] + query = await self.substrate.query_multi( + storage_keys=keys, + block_hash=block_hash, + ) + output: dict[str, bool] = {} + for key, value in query: + output[key.params[0]] = value != GENESIS_ADDRESS + return output + async def does_hotkey_exist( self, hotkey_ss58: str, diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 7cd9b1d0f..24ec638ac 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -124,6 +124,13 @@ async def unstake( identities=ck_hk_identities, ) + hotkeys_existence = await subtensor.do_hotkeys_exist( + [x[1] for x in hotkeys_to_unstake_from], block_hash=chain_head + ) + hotkeys_to_unstake_from = [ + x for x in hotkeys_to_unstake_from if hotkeys_existence[x[1]] is True + ] + with console.status( f"Retrieving stake data from {subtensor.network}...", spinner="earth", @@ -334,7 +341,7 @@ async def unstake( with console.status( f"\n:satellite: Batching {total_ops} unstake operations..." ) as status: - batch_block_hash = await subtensor.substrate.get_chain_head() + batch_block_hash = chain_head current_balance = await subtensor.get_balance( coldkey_ss58, block_hash=batch_block_hash ) From 993a8d161d1042080773242ab6b1d3057ba2345c Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 23 Apr 2026 16:29:12 +0200 Subject: [PATCH 51/72] Better display to user. --- bittensor_cli/src/commands/stake/remove.py | 28 +++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 24ec638ac..12fa21475 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -127,9 +127,35 @@ async def unstake( hotkeys_existence = await subtensor.do_hotkeys_exist( [x[1] for x in hotkeys_to_unstake_from], block_hash=chain_head ) - hotkeys_to_unstake_from = [ + hotkeys_to_unstake_from_that_exist = [ x for x in hotkeys_to_unstake_from if hotkeys_existence[x[1]] is True ] + if not hotkeys_to_unstake_from_that_exist: + err_msg = "No keys existing on chain from which to unstake" + if json_output: + json_console.print_json(data={"success": False, "error": err_msg}) + else: + print_error(err_msg) + return False + + if hotkeys_to_unstake_from_that_exist != hotkeys_to_unstake_from: + difference = set(x[1] for x in hotkeys_to_unstake_from).symmetric_difference( + set(x[1] for x in hotkeys_to_unstake_from_that_exist) + ) + sames = set(x[1] for x in hotkeys_to_unstake_from).intersection( + set(x[1] for x in hotkeys_to_unstake_from_that_exist) + ) + msg = ( + "Some hotkeys attempting to unstake are not present: " + + ", ".join(difference) + + ". Using hotkeys:\n" + + "\n".join(sames) + ) + console.print(msg) + if prompt: + if not confirm_action("Do you want to continue?"): + return False + hotkeys_to_unstake_from = hotkeys_to_unstake_from_that_exist with console.status( f"Retrieving stake data from {subtensor.network}...", From 3fd7616c1e4433f5eae794f6daf0d46e62a77d44 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 23 Apr 2026 18:07:57 +0200 Subject: [PATCH 52/72] Apply to unstake all as well. --- bittensor_cli/src/commands/stake/remove.py | 239 ++++++++++++--------- 1 file changed, 136 insertions(+), 103 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 12fa21475..9b85e7218 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -544,6 +544,7 @@ async def unstake_all( include_hotkeys = include_hotkeys or [] exclude_hotkeys = exclude_hotkeys or [] coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address + block_hash = await subtensor.substrate.get_chain_head() with console.status( f"Retrieving stake information & identities from {subtensor.network}...", spinner="earth", @@ -554,10 +555,10 @@ async def unstake_all( all_sn_dynamic_info_, current_wallet_balance, ) = await asyncio.gather( - subtensor.get_stake_for_coldkey(coldkey_ss58), - subtensor.fetch_coldkey_hotkey_identities(), - subtensor.all_subnets(), - subtensor.get_balance(coldkey_ss58), + subtensor.get_stake_for_coldkey(coldkey_ss58, block_hash=block_hash), + subtensor.fetch_coldkey_hotkey_identities(block_hash=block_hash), + subtensor.all_subnets(block_hash=block_hash), + subtensor.get_balance(coldkey_ss58, block_hash=block_hash), ) if all_hotkeys: @@ -575,115 +576,147 @@ async def unstake_all( else: hotkeys = [(None, hotkey_ss58_address, None)] - hotkey_names = {ss58: name for name, ss58, _ in hotkeys if name is not None} - hotkey_ss58s = [item[1] for item in hotkeys] - stake_info = [ - stake for stake in stake_info if stake.hotkey_ss58 in hotkey_ss58s - ] - - if unstake_all_alpha: - stake_info = [stake for stake in stake_info if stake.netuid != 0] - - if not stake_info: - console.print("[red]No stakes found to unstake[/red]") - return + hotkeys_existence = await subtensor.do_hotkeys_exist( + [x[1] for x in hotkeys], block_hash=block_hash + ) - all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} + hotkeys_to_unstake_from_that_exist = [ + x for x in hotkeys if hotkeys_existence[x[1]] is True + ] + if not hotkeys_to_unstake_from_that_exist: + err_msg = "No keys existing on chain from which to unstake" + if json_output: + json_console.print_json(data={"success": False, "error": err_msg}) + else: + print_error(err_msg) + return None - # Create table for unstaking all - table_title = ( - "Unstaking Summary - All Stakes" - if not unstake_all_alpha - else "Unstaking Summary - All Alpha Stakes" - ) - table = create_table( - title=( - f"\n[{COLOR_PALETTE.G.HEADER}]{table_title}[/{COLOR_PALETTE.G.HEADER}]\n" - f"Wallet: [{COLOR_PALETTE.G.COLDKEY}]{wallet.name}[/{COLOR_PALETTE.G.COLDKEY}], " - f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{coldkey_ss58}[/{COLOR_PALETTE.G.CK}]\n" - f"Network: [{COLOR_PALETTE.G.HEADER}]{subtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n" - ), - ) - table.add_column("Netuid", justify="center", style="grey89") - table.add_column( - "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] - ) - table.add_column( - f"Current Stake ({Balance.get_unit(1)})", - justify="center", - style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], - ) - table.add_column( - f"Rate ({Balance.unit}/{Balance.get_unit(1)})", - justify="center", - style=COLOR_PALETTE["POOLS"]["RATE"], - ) - table.add_column( - f"Fee ({Balance.get_unit(1)})", - justify="center", - style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], + if hotkeys_to_unstake_from_that_exist != hotkeys: + difference = set(x[1] for x in hotkeys).symmetric_difference( + set(x[1] for x in hotkeys_to_unstake_from_that_exist) ) - table.add_column( - "Extrinsic Fee (τ)", - justify="center", - style=COLOR_PALETTE.STAKE.TAO, + sames = set(x[1] for x in hotkeys).intersection( + set(x[1] for x in hotkeys_to_unstake_from_that_exist) ) - table.add_column( - f"Received ({Balance.unit})", - justify="center", - style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + msg = ( + "Some hotkeys attempting to unstake are not present: " + + ", ".join(difference) + + ". Using hotkeys:\n" + + "\n".join(sames) ) - # table.add_column( - # "Slippage", - # justify="center", - # style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], - # ) - - # Calculate total received - total_received_value = Balance(0) - for stake in stake_info: - if stake.stake.rao == 0: - continue + console.print(msg) + if prompt: + if not confirm_action("Do you want to continue?"): + return None + hotkeys = hotkeys_to_unstake_from_that_exist - hotkey_display = hotkey_names.get(stake.hotkey_ss58, stake.hotkey_ss58) - subnet_info = all_sn_dynamic_info.get(stake.netuid) - stake_amount = stake.stake + hotkey_names = {ss58: name for name, ss58, _ in hotkeys if name is not None} + hotkey_ss58s = [item[1] for item in hotkeys] + stake_info = [stake for stake in stake_info if stake.hotkey_ss58 in hotkey_ss58s] - try: - _ = 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, - proxy=proxy, - ) - sim_swap = await subtensor.sim_swap(stake.netuid, 0, stake_amount.rao) - received_amount = sim_swap.tao_amount - if not proxy: - received_amount -= extrinsic_fee + if unstake_all_alpha: + stake_info = [stake for stake in stake_info if stake.netuid != 0] - if received_amount < Balance.from_tao(0): - print_error("Not enough Alpha to pay the transaction fee.") - continue - except (AttributeError, ValueError): - continue + if not stake_info: + console.print("[red]No stakes found to unstake[/red]") + return + + all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} + + # Create table for unstaking all + table_title = ( + "Unstaking Summary - All Stakes" + if not unstake_all_alpha + else "Unstaking Summary - All Alpha Stakes" + ) + table = create_table( + title=( + f"\n[{COLOR_PALETTE.G.HEADER}]{table_title}[/{COLOR_PALETTE.G.HEADER}]\n" + f"Wallet: [{COLOR_PALETTE.G.COLDKEY}]{wallet.name}[/{COLOR_PALETTE.G.COLDKEY}], " + f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{coldkey_ss58}[/{COLOR_PALETTE.G.CK}]\n" + f"Network: [{COLOR_PALETTE.G.HEADER}]{subtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n" + ), + ) + table.add_column("Netuid", justify="center", style="grey89") + table.add_column( + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Current Stake ({Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + ) + table.add_column( + f"Rate ({Balance.unit}/{Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + 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", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + # table.add_column( + # "Slippage", + # justify="center", + # style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + # ) - total_received_value += received_amount - - table.add_row( - str(stake.netuid), - hotkey_display, - str(stake_amount), - f"{float(subnet_info.price):.6f}" - + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})", - str(sim_swap.alpha_fee), - str(extrinsic_fee), - str(received_amount), + # Calculate total received + total_received_value = Balance(0) + for stake in stake_info: + if stake.stake.rao == 0: + continue + + hotkey_display = hotkey_names.get(stake.hotkey_ss58, stake.hotkey_ss58) + subnet_info = all_sn_dynamic_info.get(stake.netuid) + stake_amount = stake.stake + + try: + _ = 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, + proxy=proxy, ) + sim_swap = await subtensor.sim_swap(stake.netuid, 0, stake_amount.rao) + received_amount = sim_swap.tao_amount + if not proxy: + received_amount -= extrinsic_fee + + if received_amount < Balance.from_tao(0): + print_error("Not enough Alpha to pay the transaction fee.") + continue + except (AttributeError, ValueError): + continue + + total_received_value += received_amount + + table.add_row( + str(stake.netuid), + hotkey_display, + str(stake_amount), + f"{float(subnet_info.price):.6f}" + + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})", + str(sim_swap.alpha_fee), + str(extrinsic_fee), + str(received_amount), + ) console.print(table) console.print( From 0dd3b3e6901cf69868e1b85216ca81cae9f063d8 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 23 Apr 2026 18:08:02 +0200 Subject: [PATCH 53/72] Add test --- tests/e2e_tests/test_unstaking.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 8f17341a0..03bdb70ae 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -1,5 +1,6 @@ import asyncio import json +import os.path import re import pytest from bittensor_cli.src.bittensor.balances import Balance @@ -89,8 +90,8 @@ def test_unstaking(local_chain, wallet_setup): "--no-prompt", ], ) - assert "✅ Registered subnetwork with netuid: 2" in result.stdout - assert "Your extrinsic has been included" in result.stdout, result.stdout + assert "✅ Registered subnetwork with netuid: 2" in result.stdout, result.stderr + assert "Your extrinsic has been included" in result.stdout, result.stderr # Create second subnet (netuid = 3) result = exec_command_alice( @@ -422,6 +423,17 @@ def test_unstaking(local_chain, wallet_setup): assert "✅ Finalized" in stake_result.stdout assert "Your extrinsic has been included" in stake_result.stdout + # add a dummy wallet to bob (hotkey and hotkeypub) + dummy_hk_pub = '{"ss58Address":"5GWN73LyFw8u5L38CHKh1EogaC4rbYUMPffrmU8PzPfK4oH3","accountId":"0xc482d28f38382438ea64652a6d26c1a6309cd8a91ef8e16af9c307238bea1b57","publicKey":"0xc482d28f38382438ea64652a6d26c1a6309cd8a91ef8e16af9c307238bea1b57"}' + with open( + os.path.join(wallet_path_bob, wallet_bob.name, "hotkeys", "dummypub.txt"), "w+" + ) as f: + f.write(dummy_hk_pub) + with open( + os.path.join(wallet_path_bob, wallet_bob.name, "hotkeys", "dummy"), "w+" + ) as f: + f.write(dummy_hk_pub) + # Remove all stakes unstake_all = exec_command_bob( command="stake", @@ -436,11 +448,17 @@ def test_unstaking(local_chain, wallet_setup): "--chain", "ws://127.0.0.1:9945", "--all", + "--all-hotkeys", "--no-prompt", "--era", "144", ], ) + print(unstake_all.stdout, unstake_all.stderr) + assert ( + "Some hotkeys attempting to unstake are not present: 5GWN73LyFw8u5L38CHKh1EogaC4rbYUMPffrmU8PzPfK4oH3." + in unstake_all.stdout + ) assert "✅ Included: Successfully unstaked all stakes from" in unstake_all.stdout assert "Your extrinsic has been included" in unstake_all.stdout, unstake_all.stdout print("Passed unstaking tests 🎉") From 4166c2951e4899331a01e8dcdb2ee16a8d17c325 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 23 Apr 2026 19:41:25 +0200 Subject: [PATCH 54/72] Applied the same logic for stake add --- bittensor_cli/src/commands/stake/add.py | 46 ++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index f165bf3d6..9f374f96c 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -315,14 +315,58 @@ async def stake_extrinsic( # Get subnet data and stake information for coldkey chain_head = await subtensor.substrate.get_chain_head() - _all_subnets, _stake_info, current_wallet_balance = await asyncio.gather( + ( + _all_subnets, + _stake_info, + current_wallet_balance, + hotkey_existence, + ) = await asyncio.gather( subtensor.all_subnets(block_hash=chain_head), subtensor.get_stake_for_coldkey( coldkey_ss58=coldkey_ss58, block_hash=chain_head, ), subtensor.get_balance(coldkey_ss58, block_hash=chain_head), + subtensor.do_hotkeys_exist( + [x[1] for x in hotkeys_to_stake_to], + block_hash=chain_head, + ), ) + hotkeys_that_exists = [ + x for x in hotkeys_to_stake_to if hotkey_existence[x[1]] is True + ] + if not hotkeys_that_exists: + err_msg = "No keys existing on chain that can be staked to." + if json_output: + json_console.print_json( + data={ + "staking_success": False, + "error_messages": [err_msg], + "extrinsic_ids": None, + } + ) + else: + print_error(err_msg) + return None + + if hotkeys_that_exists != hotkeys_to_stake_to: + difference = set(x[1] for x in hotkeys_to_stake_to).symmetric_difference( + set(x[1] for x in hotkeys_that_exists) + ) + sames = set(x[1] for x in hotkeys_to_stake_to).intersection( + set(x[1] for x in hotkeys_that_exists) + ) + msg = ( + "Some hotkeys attempting to stake to are not present: " + + ", ".join(difference) + + ". Using hotkeys:\n" + + "\n".join(sames) + ) + console.print(msg) + if prompt: + if not confirm_action("Do you want to continue?"): + return None + hotkeys_to_stake_to = hotkeys_that_exists all_subnets = {di.netuid: di for di in _all_subnets} # Map current stake balances for hotkeys From 9baa0a820b4056befc3d36747a05ff457f2e77bf Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 23 Apr 2026 19:46:11 +0200 Subject: [PATCH 55/72] Test fix --- tests/unit_tests/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 04bc2d259..4416d44dd 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -190,4 +190,10 @@ def mock_subtensor() -> MagicMock: return_value={"hotkeys": {}, "coldkeys": {}} ) st.get_all_subnet_netuids = AsyncMock(return_value=[0, 1]) + + async def _do_hotkeys_exist(hotkeys_ss58, block_hash=None): + del block_hash + return {ss58: True for ss58 in hotkeys_ss58} + + st.do_hotkeys_exist = AsyncMock(side_effect=_do_hotkeys_exist) return st From c3410628a9ba65892ae1204dd6f95f3a988f2429 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Thu, 23 Apr 2026 21:30:42 +0200 Subject: [PATCH 56/72] Bumps ASI req --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index db4b1c87c..31fba5e6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] dependencies = [ "wheel", - "async-substrate-interface==2.0.0", + "async-substrate-interface==2.0.1", "aiohttp~=3.13", "backoff~=2.2.1", "bittensor-drand>=1.3.0", From 4cbc1cf011871241713d4ebded58058129f78db6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 23 Apr 2026 14:03:03 -0700 Subject: [PATCH 57/72] remove pow register --- bittensor_cli/cli.py | 92 -------------------------------------------- 1 file changed, 92 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 333ef6aa2..f51679c9c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1182,9 +1182,6 @@ def __init__(self): self.subnets_app.command( "create", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"] )(self.subnets_create) - self.subnets_app.command( - "pow-register", rich_help_panel=HELP_PANELS["SUBNETS"]["REGISTER"] - )(self.subnets_pow_register) self.subnets_app.command( "register", rich_help_panel=HELP_PANELS["SUBNETS"]["REGISTER"] )(self.subnets_register) @@ -1290,7 +1287,6 @@ def __init__(self): # Subnets self.subnets_app.command("burn_cost", hidden=True)(self.subnets_burn_cost) - self.subnets_app.command("pow_register", hidden=True)(self.subnets_pow_register) self.subnets_app.command("set_identity", hidden=True)(self.subnets_set_identity) self.subnets_app.command("get_identity", hidden=True)(self.subnets_get_identity) self.subnets_app.command("check_start", hidden=True)(self.subnets_check_start) @@ -8079,94 +8075,6 @@ def subnets_set_identity( json.dumps({"success": success, "extrinsic_identifier": ext_id}) ) - def subnets_pow_register( - self, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - netuid: int = Options.netuid, - # TODO add the following to config - processors: Optional[int] = typer.Option( - defaults.pow_register.num_processes, - "--processors", - help="Number of processors to use for POW registration.", - ), - update_interval: Optional[int] = typer.Option( - defaults.pow_register.update_interval, - "--update-interval", - "-u", - help="The number of nonces to process before checking for the next block during registration", - ), - output_in_place: Optional[bool] = typer.Option( - defaults.pow_register.output_in_place, - help="Whether to output the registration statistics in-place.", - ), - verbose: Optional[bool] = typer.Option( # TODO verbosity here - defaults.pow_register.verbose, - "--verbose", - "-v", - help="Whether to output the registration statistics verbosely.", - ), - use_cuda: Optional[bool] = typer.Option( - defaults.pow_register.cuda.use_cuda, - "--use-cuda/--no-use-cuda", - "--cuda/--no-cuda", - help="Set the flag to use CUDA for POW registration.", - ), - dev_id: Optional[int] = typer.Option( - defaults.pow_register.cuda.dev_id, - "--dev-id", - "-d", - help="Set the CUDA device id(s), in the order of the device speed (0 is the fastest).", - ), - threads_per_block: Optional[int] = typer.Option( - defaults.pow_register.cuda.tpb, - "--threads-per-block", - "-tbp", - help="Set the number of threads per block for CUDA.", - ), - prompt: bool = Options.prompt, - ): - """ - Register a neuron (a subnet validator or a subnet miner) using Proof of Work (POW). - - This method is an alternative registration process that uses computational work for securing a neuron's place on the subnet. - - The command starts by verifying the existence of the specified subnet. If the subnet does not exist, it terminates with an error message. On successful verification, the POW registration process is initiated, which requires solving computational puzzles. - - The command also supports additional wallet and subtensor arguments, enabling further customization of the registration process. - - EXAMPLE - - [green]$[/green] btcli pow_register --netuid 1 --num_processes 4 --cuda - - [blue bold]Note[/blue bold]: This command is suitable for users with adequate computational resources to participate in POW registration. - It requires a sound understanding of the network's operations and POW mechanics. Users should ensure their systems meet the necessary hardware and software requirements, particularly when opting for CUDA-based GPU acceleration. - - This command may be disabled by the subnet owner. For example, on netuid 1 this is permanently disabled. - """ - return self._run_command( - subnets.pow_register( - self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ), - self.initialize_chain(network), - netuid, - processors, - update_interval, - output_in_place, - verbose, - use_cuda, - dev_id, - threads_per_block, - prompt=prompt, - ) - ) def subnets_register( self, From ad31d864cdca0d4628ddb55c5d3d6c96fb886a2c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 23 Apr 2026 14:03:40 -0700 Subject: [PATCH 58/72] rm pow register extrinsic --- .../src/bittensor/extrinsics/registration.py | 222 ------------------ 1 file changed, 222 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 7068a8320..70ae9079f 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -451,228 +451,6 @@ async def is_hotkey_registered( else: return False - -async def register_extrinsic( - subtensor: "SubtensorInterface", - wallet: Wallet, - netuid: int, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, - decline: bool = False, - quiet: bool = False, - max_allowed_attempts: int = 3, - output_in_place: bool = True, - cuda: bool = False, - dev_id: typing.Union[list[int], int] = 0, - tpb: int = 256, - num_processes: Optional[int] = None, - update_interval: Optional[int] = None, - log_verbose: bool = False, -) -> bool: - """Registers the wallet to the chain. - - :param subtensor: initialized SubtensorInterface object to use for chain interactions - :param wallet: Bittensor wallet object. - :param netuid: The ``netuid`` of the subnet to register on. - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - :param max_allowed_attempts: Maximum number of attempts to register the wallet. - :param output_in_place: Whether the POW solving should be outputted to the console as it goes along. - :param cuda: If `True`, the wallet should be registered using CUDA device(s). - :param dev_id: The CUDA device id to use, or a list of device ids. - :param tpb: The number of threads per block (CUDA). - :param num_processes: The number of processes to use to register. - :param update_interval: The number of nonces to solve between updates. - :param log_verbose: If `True`, the registration process will log more information. - - :return: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, - the response is `True`. - """ - - async def get_neuron_for_pubkey_and_subnet(): - uid = await subtensor.query( - "SubtensorModule", "Uids", [netuid, get_hotkey_pub_ss58(wallet)] - ) - if uid is None: - return NeuronInfo.get_null_neuron() - - result = await subtensor.neuron_for_uid( - uid=uid, - netuid=netuid, - block_hash=subtensor.substrate.last_block_hash, - ) - return result - - print_verbose("Checking subnet status") - if not await subtensor.subnet_exists(netuid): - print_error( - f"Failed: error: [bold white]subnet:{netuid}[/bold white] does not exist." - ) - return False - - with console.status( - f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", - spinner="aesthetic", - ) as status: - neuron = await get_neuron_for_pubkey_and_subnet() - if not neuron.is_null: - print_error( - f"Wallet {wallet} is already registered on subnet {neuron.netuid} with uid {neuron.uid}", - status, - ) - return True - - if prompt: - if not confirm_action( - f"Continue Registration?\n" - f" hotkey [{COLOR_PALETTE.G.HK}]({wallet.hotkey_str})[/{COLOR_PALETTE.G.HK}]:" - f"\t[{COLOR_PALETTE.G.HK}]{get_hotkey_pub_ss58(wallet)}[/{COLOR_PALETTE.G.HK}]\n" - f" coldkey [{COLOR_PALETTE.G.CK}]({wallet.name})[/{COLOR_PALETTE.G.CK}]:" - f"\t[{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n" - f" network:\t\t[{COLOR_PALETTE.G.LINKS}]{subtensor.network}[/{COLOR_PALETTE.G.LINKS}]\n", - decline=decline, - quiet=quiet, - ): - return False - - if not torch: - log_no_torch_error() - return False - - # Attempt rolling registration. - attempts = 1 - pow_result: Optional[POWSolution] - while True: - console.print( - ":satellite: Registering...({}/{})".format(attempts, max_allowed_attempts) - ) - # Solve latest POW. - if cuda: - if not torch.cuda.is_available(): - if prompt: - console.print("CUDA is not available.") - return False - pow_result = await create_pow( - subtensor, - wallet, - netuid, - output_in_place, - cuda=cuda, - dev_id=dev_id, - tpb=tpb, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - else: - pow_result = await create_pow( - subtensor, - wallet, - netuid, - output_in_place, - cuda=cuda, - num_processes=num_processes, - update_interval=update_interval, - log_verbose=log_verbose, - ) - - # pow failed - if not pow_result: - # might be registered already on this subnet - is_registered = await is_hotkey_registered( - subtensor, netuid=netuid, hotkey_ss58=get_hotkey_pub_ss58(wallet) - ) - if is_registered: - print_success( - f"[dark_sea_green3]Already registered on netuid:{netuid}[/dark_sea_green3]" - ) - return True - - # pow successful, proceed to submit pow to chain for registration - else: - with console.status(":satellite: Submitting POW..."): - # check if pow result is still valid - while not await pow_result.is_stale(subtensor=subtensor): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="register", - call_params={ - "netuid": netuid, - "block_number": pow_result.block_number, - "nonce": pow_result.nonce, - "work": [int(byte_) for byte_ in pow_result.seal], - "hotkey": get_hotkey_pub_ss58(wallet), - "coldkey": wallet.coldkeypub.ss58_address, - }, - ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.hotkey - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - if not wait_for_finalization and not wait_for_inclusion: - success, err_msg = True, "" - else: - success = await response.is_success - if not success: - success, err_msg = ( - False, - format_error_message(await response.error_message), - ) - # Look error here - # https://github.com/opentensor/subtensor/blob/development/pallets/subtensor/src/errors.rs - - if "HotKeyAlreadyRegisteredInSubNet" in err_msg: - print_success( - f"[dark_sea_green3]Already Registered on " - f"[bold]subnet:{netuid}[/bold][/dark_sea_green3]" - ) - return True - print_error(f"Failed: {err_msg}") - await asyncio.sleep(0.5) - - # Successful registration, final check for neuron and pubkey - if success: - console.print(":satellite: Checking Registration status...") - is_registered = await is_hotkey_registered( - subtensor, - netuid=netuid, - hotkey_ss58=get_hotkey_pub_ss58(wallet), - ) - if is_registered: - print_success( - "[dark_sea_green3]Registered[/dark_sea_green3]" - ) - return True - else: - # neuron not found, try again - print_error("Unknown error. Neuron not found.") - continue - else: - # Exited loop because pow is no longer valid. - print_error("POW is stale.") - # Try again. - continue - - if attempts < max_allowed_attempts: - # Failed registration, retry pow - attempts += 1 - print_error( - ":satellite: Failed registration, retrying pow ...({attempts}/{max_allowed_attempts})" - ) - else: - # Failed to register after max attempts. - print_error("No more attempts.") - return False - - async def burned_register_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, From d2ebff09259f69f2bc43eb1258a401a4de0fd5ad Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 23 Apr 2026 14:03:57 -0700 Subject: [PATCH 59/72] remove pow register cmd --- bittensor_cli/src/commands/subnets/subnets.py | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index a17897c6d..8e8a40483 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -15,8 +15,7 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.registration import ( - register_extrinsic, - burned_register_extrinsic, + burned_register_extrinsic ) from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( @@ -1747,36 +1746,6 @@ async def create( ) -async def pow_register( - wallet: Wallet, - subtensor: "SubtensorInterface", - netuid, - processors, - update_interval, - output_in_place, - verbose, - use_cuda, - dev_id, - threads_per_block, - prompt: bool, -): - """Register neuron.""" - - await register_extrinsic( - subtensor, - wallet=wallet, - netuid=netuid, - prompt=prompt, - tpb=threads_per_block, - update_interval=update_interval, - num_processes=processors, - cuda=use_cuda, - dev_id=dev_id, - output_in_place=output_in_place, - log_verbose=verbose, - ) - - async def register( wallet: Wallet, subtensor: "SubtensorInterface", From 35020a7f8624c843957ec9cb299e28fb9c9cd934 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 23 Apr 2026 14:07:29 -0700 Subject: [PATCH 60/72] ruff --- bittensor_cli/cli.py | 1 - bittensor_cli/src/bittensor/extrinsics/registration.py | 1 + bittensor_cli/src/commands/subnets/subnets.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f51679c9c..a61cf5a8a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8075,7 +8075,6 @@ def subnets_set_identity( json.dumps({"success": success, "extrinsic_identifier": ext_id}) ) - def subnets_register( self, wallet_name: str = Options.wallet_name, diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 70ae9079f..3a7b1e089 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -451,6 +451,7 @@ async def is_hotkey_registered( else: return False + async def burned_register_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 8e8a40483..01870df89 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -15,7 +15,7 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.registration import ( - burned_register_extrinsic + burned_register_extrinsic, ) from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( From 9c6dd82ed713b31d7d9de64d5b98be2d0052ca24 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 23 Apr 2026 15:34:14 -0700 Subject: [PATCH 61/72] make register limit the default --- bittensor_cli/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a61cf5a8a..ed6dc2430 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8116,6 +8116,8 @@ def subnets_register( ) self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy, False) + if netuid != 0: + limit = self.ask_rate_tolerance(limit) wallet = self.wallet_ask( wallet_name, wallet_path, From 917d352f4cc1c576bee005b74875daf64ea95e3a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 23 Apr 2026 15:35:39 -0700 Subject: [PATCH 62/72] disable caching --- bittensor_cli/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ed6dc2430..c857d3e9a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1576,6 +1576,9 @@ def main_callback( for k, v in config.items(): if k in self.config.keys(): self.config[k] = v + # Temporarily disable cach for this btcli process. + self.config["disk_cache"] = False + os.environ["DISK_CACHE"] = "0" if self.config.get("use_cache", False): with open(self.debug_file_path, "w+") as f: f.write( From 45ab391335a60477bac0248ea64e5752f0f90bdb Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 23 Apr 2026 15:42:06 -0700 Subject: [PATCH 63/72] improve visibility of limit price --- bittensor_cli/src/commands/subnets/subnets.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 01870df89..d685214e8 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1811,16 +1811,10 @@ async def _storage_key(storage_fn: str) -> StorageKey: if limit is not None: with_limit = current_recycle.tao * (1 + limit) - extra_text = ( - f" You have declared a limit of " - f"up to [{COLOR_PALETTE.G.COST}]{Balance.from_tao(with_limit)}[/{COLOR_PALETTE.G.COST}]" - ) else: with_limit = None - extra_text = "" print_verbose(f"Recycle price: {current_recycle}") - print_verbose(extra_text) if prompt and not json_output: # Show creation table. @@ -1846,6 +1840,13 @@ async def _storage_key(storage_fn: str) -> StorageKey: no_wrap=True, justify="center", ) + if with_limit is not None and limit is not None: + table.add_column( + f"Limit Cost (+{limit * 100:g}%)", + style=COLOR_PALETTE["POOLS"]["TAO"], + no_wrap=True, + justify="center", + ) table.add_column( "Hotkey", style=COLOR_PALETTE["GENERAL"]["HOTKEY"], @@ -1858,19 +1859,26 @@ async def _storage_key(storage_fn: str) -> StorageKey: no_wrap=True, justify="center", ) - table.add_row( + row = [ str(netuid), f"{Balance.get_unit(netuid)}", f"τ {current_recycle.tao:.4f}", - f"{get_hotkey_pub_ss58(wallet)}", - f"{coldkey_ss58}", + ] + if with_limit is not None: + row.append(f"τ {with_limit:,.9f}".rstrip("0").rstrip(".")) + row.extend( + [ + f"{get_hotkey_pub_ss58(wallet)}", + f"{coldkey_ss58}", + ] ) + table.add_row(*row) console.print(table) if not ( confirm_action( f"Your balance is: [{COLOR_PALETTE.G.BAL}]{balance}[/{COLOR_PALETTE.G.BAL}]\n" f"The cost to register by recycle is " - f"[{COLOR_PALETTE.G.COST}]{current_recycle}.{extra_text}\n" + f"[{COLOR_PALETTE.G.COST}]{current_recycle}[/{COLOR_PALETTE.G.COST}].\n" f"Do you want to continue?", default=False, decline=decline, From c6635cb0f5bbd56d11e9f4c49df152168bf376b8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 23 Apr 2026 16:01:16 -0700 Subject: [PATCH 64/72] add safe/unsafe flags --- bittensor_cli/cli.py | 48 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c857d3e9a..56c8e2c4a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -393,6 +393,13 @@ def edit_help(cls, option_name: str, help_text: str): show_default=False, help="Enable or disable safe staking mode [dim](default: enabled)[/dim].", ) + safe_registration = typer.Option( + None, + "--safe-register/--unsafe-register", + "--safe/--unsafe", + show_default=False, + help="Enable or disable safe registration mode [dim](default: enabled)[/dim].", + ) allow_partial_stake = typer.Option( None, "--allow-partial-stake/--no-allow-partial-stake", @@ -2395,6 +2402,36 @@ def ask_partial_stake( logger.debug(f"Partial staking {partial_staking}") return False + def ask_safe_registration( + self, + safe_registration: Optional[bool], + ) -> bool: + """ + Gets safe registration setting from args or default. + + Args: + safe_registration (Optional[bool]): Explicitly provided safe registration value + + Returns: + bool: Safe registration setting + """ + if safe_registration is not None: + enabled = "enabled" if safe_registration else "disabled" + console.print( + f"[dim][blue]Safe registration[/blue]: [bold cyan]{enabled}[/bold cyan]." + ) + logger.debug(f"Safe registration {enabled}") + return safe_registration + else: + console.print( + "[dim][blue]Safe registration[/blue]: " + "[bold cyan]enabled[/bold cyan] " + "by default. Disable this using " + "[dark_sea_green3 italic]`--unsafe`[/dark_sea_green3 italic] flag[/dim]" + ) + logger.debug("Safe registration enabled.") + return True + @staticmethod def ask_subnet_mechanism( mechanism_id: Optional[int], @@ -8094,6 +8131,7 @@ def subnets_register( help="Length (in blocks) for which the transaction should be valid. Note that it is possible that if you " "use an era for this transaction that you may pay a different fee to register than the one stated.", ), + safe_registration: Optional[bool] = Options.safe_registration, limit: Optional[float] = Options.rate_tolerance, proxy: Optional[str] = Options.proxy, json_output: bool = Options.json_output, @@ -8118,9 +8156,15 @@ def subnets_register( "as the limit does not apply for root registrations." ) self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy, False) if netuid != 0: - limit = self.ask_rate_tolerance(limit) + safe_registration = self.ask_safe_registration(safe_registration) + if not safe_registration and limit is not None: + raise typer.BadParameter( + "Cannot specify both `--unsafe`/`--unsafe-register` and `--tolerance`, " + "as tolerance only applies in safe registration mode." + ) + limit = self.ask_rate_tolerance(limit) if safe_registration else None + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) wallet = self.wallet_ask( wallet_name, wallet_path, From 07936358bd043ed9d58f708fb1101a5534b61817 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 23 Apr 2026 16:01:27 -0700 Subject: [PATCH 65/72] update e2e --- tests/e2e_tests/test_registration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_registration.py b/tests/e2e_tests/test_registration.py index 863362cdf..83d5bdbca 100644 --- a/tests/e2e_tests/test_registration.py +++ b/tests/e2e_tests/test_registration.py @@ -235,7 +235,7 @@ def test_registration(local_chain, wallet_setup): "--verbose", ], ) - assert "You have declared a limit of up to" in register_result.stdout, ( + assert "Rate tolerance: 0.1 (10.0%)." in register_result.stdout, ( register_result.stderr ) assert "✅ Registered" in register_result.stdout, register_result.stderr From 9dcb2413e7e31de9d7034a88f423cc04eeaf6474 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 24 Apr 2026 07:47:21 +0200 Subject: [PATCH 66/72] Decode hotkey ss58 if bytes --- bittensor_cli/src/bittensor/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 06340568f..2fc1c7990 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -152,7 +152,10 @@ def get_hotkey_identity_name( """Return a hotkey display name from the V2 identity map, if present.""" hotkey_identity = identities.get("hotkeys", {}).get(hotkey_ss58, {}) identity_data = hotkey_identity.get("identity", {}) - return identity_data.get("name") or identity_data.get("display") or None + if isinstance((id_name := identity_data.get("name")), list): + if len(id_name) != 0: + return bytes(id_name).decode("utf-8") + return identity_data.get("display") or None def get_coldkey_identity_name( From 7c5618b2b6d3564e16474f8265659fce3d6e6df3 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 24 Apr 2026 07:57:14 +0200 Subject: [PATCH 67/72] Fix --- bittensor_cli/src/bittensor/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 2fc1c7990..b01552261 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -155,6 +155,8 @@ def get_hotkey_identity_name( if isinstance((id_name := identity_data.get("name")), list): if len(id_name) != 0: return bytes(id_name).decode("utf-8") + elif isinstance(id_name, str): + return id_name return identity_data.get("display") or None From ccde058e31fbf708feb0f05ea8a0504263198988 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 24 Apr 2026 08:31:49 +0200 Subject: [PATCH 68/72] Ensure the call params are not Balance objects --- bittensor_cli/src/commands/stake/remove.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 9b85e7218..aaf5ed06b 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -1232,7 +1232,7 @@ async def _get_extrinsic_fee( "hotkey": hotkey_ss58, "netuid": netuid, "amount_unstaked": amount.rao, - "limit_price": price_limit, + "limit_price": price_limit.rao if price_limit is not None else None, "allow_partial": allow_partial_stake, }, ), From 340f97307d39eb06819c9d6282870d58b4aeb82d Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 24 Apr 2026 09:41:06 +0200 Subject: [PATCH 69/72] Resolves the issue with the disk cache --- bittensor_cli/cli.py | 3 --- pyproject.toml | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 56c8e2c4a..290abc73a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1583,9 +1583,6 @@ def main_callback( for k, v in config.items(): if k in self.config.keys(): self.config[k] = v - # Temporarily disable cach for this btcli process. - self.config["disk_cache"] = False - os.environ["DISK_CACHE"] = "0" if self.config.get("use_cache", False): with open(self.debug_file_path, "w+") as f: f.write( diff --git a/pyproject.toml b/pyproject.toml index 31fba5e6d..ae98e9e6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] dependencies = [ "wheel", - "async-substrate-interface==2.0.1", + "async-substrate-interface==2.0.2", "aiohttp~=3.13", "backoff~=2.2.1", "bittensor-drand>=1.3.0", @@ -40,7 +40,7 @@ dependencies = [ "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0", "rich>=13.7,<15.0", - "cyscale==0.3.0", + "cyscale==0.3.1", "typer>=0.16", "typing_extensions>4.0.0; python_version<'3.11'", "bittensor-wallet==4.0.1", From dfd608b24b7d80546c5f32d9ff0316e66f649efb Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 24 Apr 2026 09:42:27 +0200 Subject: [PATCH 70/72] Adjust --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ae98e9e6e..301d101ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0", "rich>=13.7,<15.0", - "cyscale==0.3.1", + "cyscale>=0.3.1,<1.0.0", "typer>=0.16", "typing_extensions>4.0.0; python_version<'3.11'", "bittensor-wallet==4.0.1", From c86dbf4e4371796f9ee0785513c22cdd8c5b3a13 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Fri, 24 Apr 2026 09:45:55 +0200 Subject: [PATCH 71/72] Adds CODEOWNERS file --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..8b417b3ef --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @thewhaleking @ibraheem-abe @basfroman \ No newline at end of file From 698bfd5f0662bb0289a1a203f2eb84b0966717c2 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 24 Apr 2026 11:10:01 -0700 Subject: [PATCH 72/72] bump version and changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9c726e2c..7693508ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 9.21.0rc1 /2026-04-24 + +## 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 +* fix(crowdloan): normalize creator checks for proxy actors by @bitloi in https://github.com/latent-to/btcli/pull/908 +* Updates conftest imports by @thewhaleking in https://github.com/latent-to/btcli/pull/911 +* fix: cross-subnet received amount in stake move display by @bitloi in https://github.com/latent-to/btcli/pull/910 +* fix: thread --announce-only through stake/liquidity/crowd submission paths by @bitloi in https://github.com/latent-to/btcli/pull/899 +* fix(stake): keep move_stake MEV wait inside active status by @bitloi in https://github.com/latent-to/btcli/pull/913 +* Adds register limit by @thewhaleking in https://github.com/latent-to/btcli/pull/926 +* Use Cyscale + ASI2.0 by @thewhaleking in https://github.com/latent-to/btcli/pull/895 +* Do not unstake from hotkeys that do not exist on chain by @thewhaleking in https://github.com/latent-to/btcli/pull/929 +* Bumps ASI req by @thewhaleking in https://github.com/latent-to/btcli/pull/930 +* Update/remove pow register by @ibraheem-abe in https://github.com/latent-to/btcli/pull/931 +* Fix/register limit disk cache by @ibraheem-abe in https://github.com/latent-to/btcli/pull/932 +* Decode hotkey ss58 if bytes by @thewhaleking in https://github.com/latent-to/btcli/pull/933 +* Ensure the call params are not Balance objects by @thewhaleking in https://github.com/latent-to/btcli/pull/934 +* Adds CODEOWNERS file by @thewhaleking in https://github.com/latent-to/btcli/pull/936 +* Resolves the issue with the disk cache by @thewhaleking in https://github.com/latent-to/btcli/pull/935 + +**Full Changelog**: https://github.com/latent-to/btcli/compare/v9.20.1...9.30.0 + ## 9.20.1 /2026-04-02 ## What's Changed diff --git a/pyproject.toml b/pyproject.toml index 301d101ed..239d74de5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "bittensor-cli" -version = "9.20.1" +version = "9.21.0rc1" description = "Bittensor CLI" readme = "README.md" authors = [