From c32f7e75b5808bf1d9d4578eb1fdc4b81f131920 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 27 Oct 2025 23:15:12 +0200 Subject: [PATCH 001/113] WIP --- bittensor_cli/cli.py | 3 +-- bittensor_cli/src/bittensor/subtensor_interface.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 3b6096047..a1179199d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -13,7 +13,7 @@ import warnings from dataclasses import fields from pathlib import Path -from typing import Coroutine, Optional, Union, Literal +from typing import Coroutine, Optional, Union import numpy as np import rich @@ -93,7 +93,6 @@ subnets, mechanisms as subnet_mechanisms, ) -from bittensor_cli.src.commands.wallets import SortByBalance from bittensor_cli.version import __version__, __version_as_int__ try: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 2ef90d284..63368755b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,4 +1,5 @@ import asyncio +import logging import os import time from typing import Optional, Any, Union, TypedDict, Iterable @@ -45,6 +46,8 @@ get_hotkey_pub_ss58, ) +logger = logging.getLogger("btcli") + class ParamWithTypes(TypedDict): name: str # Name of the parameter. @@ -113,6 +116,7 @@ def __init__(self, network, use_disk_cache: bool = False): if (use_disk_cache or os.getenv("DISK_CACHE", "0") == "1") else AsyncSubstrateInterface ) + logger.debug(f"Using substrate class {substrate_class.__name__}") self.substrate = substrate_class( url=self.chain_endpoint, ss58_format=SS58_FORMAT, @@ -438,11 +442,13 @@ async def get_total_stake_for_coldkey( :return: {address: Balance objects} """ - sub_stakes = await self.get_stake_for_coldkeys( + sub_stakes, dynamic_info = await asyncio.gather( + self.get_stake_for_coldkeys( list(ss58_addresses), block_hash=block_hash + ), + # Token pricing info + self.all_subnets(block_hash=block_hash), ) - # Token pricing info - dynamic_info = await self.all_subnets() results = {} for ss58, stake_info_list in sub_stakes.items(): From 90331569f6969879e6a3282adbb2a1e3dba7b703 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 28 Oct 2025 16:32:26 +0200 Subject: [PATCH 002/113] WIP --- bittensor_cli/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a1179199d..ca369180f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1338,10 +1338,13 @@ async def _run(): ): # temporarily to handle multiple run commands in one session if self.subtensor: try: - await self.subtensor.substrate.close() + # TODO for some reason having the close here tries to close it twice. + print(1341, "not closing connection") + # await self.subtensor.substrate.close() except Exception as e: # ensures we always exit cleanly if not isinstance(e, (typer.Exit, RuntimeError)): err_console.print(f"An unknown error has occurred: {e}") + traceback.print_exc() if exception_occurred: raise typer.Exit() From f45e98eba395f5519c60104034ff5ba40c698516 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 28 Oct 2025 16:33:21 +0200 Subject: [PATCH 003/113] Nvm --- bittensor_cli/cli.py | 4 +--- bittensor_cli/src/commands/wallets.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ca369180f..669459ddc 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1338,9 +1338,7 @@ async def _run(): ): # temporarily to handle multiple run commands in one session if self.subtensor: try: - # TODO for some reason having the close here tries to close it twice. - print(1341, "not closing connection") - # await self.subtensor.substrate.close() + await self.subtensor.substrate.close() except Exception as e: # ensures we always exit cleanly if not isinstance(e, (typer.Exit, RuntimeError)): err_console.print(f"An unknown error has occurred: {e}") diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 6473f2c69..25fb9787e 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -697,7 +697,6 @@ async def wallet_balance( str(total_free_balance + total_staked_balance), ) console.print(Padding(table, (0, 0, 0, 4))) - await subtensor.substrate.close() if json_output: output_balances = { key: { From 68a2d73471e0b62e8f643662ac98bee3a68ce48a Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 28 Oct 2025 16:54:57 +0200 Subject: [PATCH 004/113] Better switching to archive --- bittensor_cli/src/commands/wallets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 25fb9787e..607c71962 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2174,7 +2174,10 @@ async def find_coldkey_swap_extrinsic( ): console.print("Querying archive node for coldkey swap events...") await subtensor.substrate.close() - subtensor = SubtensorInterface("archive") + subtensor.substrate.chain_endpoint = Constants.archive_entrypoint + subtensor.substrate.url = Constants.archive_entrypoint + subtensor.substrate.initialized = False + await subtensor.substrate.initialize() block_hashes = await asyncio.gather( *[ From 2f17683b72839b32613b878798904bc0478fbcae Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 28 Oct 2025 16:55:25 +0200 Subject: [PATCH 005/113] Ruff --- bittensor_cli/src/bittensor/subtensor_interface.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 63368755b..12b0b6139 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -443,9 +443,7 @@ async def get_total_stake_for_coldkey( :return: {address: Balance objects} """ sub_stakes, dynamic_info = await asyncio.gather( - self.get_stake_for_coldkeys( - list(ss58_addresses), block_hash=block_hash - ), + self.get_stake_for_coldkeys(list(ss58_addresses), block_hash=block_hash), # Token pricing info self.all_subnets(block_hash=block_hash), ) From 0777571b3cfc5fe503c351af9c7c5790f03d04b8 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 19 Dec 2025 08:37:17 +0100 Subject: [PATCH 006/113] fix: replace broad exception catches with specific exception types Replace overly broad `except Exception` catches with specific exception types to improve error handling clarity and debugging: - subtensor_interface.py: Handle both TimeoutError and asyncio.TimeoutError for substrate initialization timeout (resolves TODO comment) - utils.py (is_valid_github_url): Catch ValueError, TypeError, AttributeError for URL parsing exceptions (resolves TODO comment) - utils.py (normalize_hyperparameters): Catch KeyError, ValueError, TypeError, AttributeError for parameter normalization - wallets.py (new_hotkey): Catch ValueError, TypeError for Keypair.create_from_uri - wallets.py (new_coldkey): Catch ValueError, TypeError for Keypair.create_from_uri - wallets.py (wallet_create): Catch ValueError, TypeError, KeyFileError for keypair and wallet creation This change improves code quality by making exception handling more explicit and easier to debug while maintaining the same error recovery behavior. --- bittensor_cli/src/bittensor/subtensor_interface.py | 2 +- bittensor_cli/src/bittensor/utils.py | 4 ++-- bittensor_cli/src/commands/wallets.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index abdcfa961..7338b70b0 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -135,7 +135,7 @@ async def __aenter__(self): try: await self.substrate.initialize() return self - except TimeoutError: # TODO verify + except (TimeoutError, asyncio.TimeoutError): err_console.print( "\n[red]Error[/red]: Timeout occurred connecting to substrate. " f"Verify your chain and network settings: {self}" diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index c15b8f287..7f50ed5dc 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -850,7 +850,7 @@ def normalize_hyperparameters( norm_value = norm_value.to_dict() else: norm_value = value - except Exception: + except (KeyError, ValueError, TypeError, AttributeError): # bittensor.logging.warning(f"Error normalizing parameter '{param}': {e}") norm_value = "-" if not json_output: @@ -1728,7 +1728,7 @@ def is_valid_github_url(url: str) -> bool: return False return True - except Exception: # TODO figure out the exceptions that can be raised in here + except (ValueError, TypeError, AttributeError): return False diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index fd205237a..645799b87 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -377,7 +377,7 @@ async def new_hotkey( if uri: try: keypair = Keypair.create_from_uri(uri) - except Exception as e: + except (ValueError, TypeError) as e: print_error(f"Failed to create keypair from URI {uri}: {str(e)}") return wallet.set_hotkey(keypair=keypair, encrypt=use_password) @@ -428,7 +428,7 @@ async def new_coldkey( if uri: try: keypair = Keypair.create_from_uri(uri) - except Exception as e: + except (ValueError, TypeError) as e: print_error(f"Failed to create keypair from URI {uri}: {str(e)}") wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) @@ -501,7 +501,7 @@ async def wallet_create( "hotkey_ss58": wallet.hotkeypub.ss58_address, "coldkey_ss58": wallet.coldkeypub.ss58_address, } - except Exception as e: + except (ValueError, TypeError, KeyFileError) as e: err = f"Failed to create keypair from URI: {str(e)}" print_error(err) output_dict["error"] = err From 8e8043d862fc8f4bd9d6265d6f6fa404fec46399 Mon Sep 17 00:00:00 2001 From: leonace924 Date: Mon, 22 Dec 2025 20:32:34 -0500 Subject: [PATCH 007/113] feat: add the resuable create_table for subnets --- bittensor_cli/src/bittensor/utils.py | 58 +++++++++++++++++ bittensor_cli/src/commands/subnets/subnets.py | 63 +++---------------- 2 files changed, 65 insertions(+), 56 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index d63759271..26bd49e2e 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -25,6 +25,7 @@ from numpy.typing import NDArray from rich.console import Console 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 @@ -89,6 +90,63 @@ def confirm_action( return Confirm.ask(message, default=default) +def create_table(*columns, title: str = "", **overrides) -> Table: + """ + Creates a Rich Table with consistent CLI styling. + + Default styling: no edge borders, bold white headers, bright black borders, + footer enabled, center-aligned title, and no lines between rows. + + Args: + *columns: Optional Column objects to add to the table upfront. + title: Table title with rich markup support. + **overrides: Any Table() parameter to override defaults (e.g., show_footer, + border_style, box, expand). + + Returns: + Configured Rich Table ready for adding columns/rows. + + Examples: + Basic usage (add columns later): + >>> table = create_table(title="My Subnets") + >>> table.add_column("Netuid", justify="center") + >>> table.add_row("1") + + With Column objects upfront: + >>> from rich.table import Column + >>> table = create_table( + ... Column("Name", justify="left"), + ... Column("Value", justify="right"), + ... title="Settings" + ... ) + >>> table.add_row("Timeout", "30s") + + Custom styling: + >>> from rich import box + >>> table = create_table( + ... title="Custom", + ... border_style="blue", + ... box=box.ROUNDED + ... ) + """ + defaults = { + "title": title, + "show_footer": True, + "show_edge": False, + "header_style": "bold white", + "border_style": "bright_black", + "style": "bold", + "title_justify": "center", + "show_lines": False, + "pad_edge": True, + } + + # Merge overrides into defaults + config = {**defaults, **overrides} + + return Table(*columns, **config) + + jinja_env = Environment( loader=PackageLoader("bittensor_cli", "src/bittensor/templates"), autoescape=select_autoescape(), diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 610606ba3..ece49f57d 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -30,6 +30,7 @@ confirm_action, console, create_and_populate_table, + create_table, print_success, print_verbose, print_error, @@ -364,17 +365,9 @@ def define_table( tao_emission_percentage: str, total_tao_flow_ema: float, ): - defined_table = Table( + defined_table = create_table( title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets" f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) defined_table.add_column( @@ -1095,17 +1088,9 @@ async def show_root(): tao_sum = sum(root_state.tao_stake).tao - table = Table( + table = create_table( title=f"[{COLOR_PALETTE.G.HEADER}]Root Network\n[{COLOR_PALETTE.G.SUBHEAD}]" f"Network: {subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) table.add_column("[bold white]Position", style="white", justify="center") @@ -1340,18 +1325,10 @@ async def show_subnet( # Define table properties mechanism_label = f"Mechanism {selected_mechanism_id}" - table = Table( + table = create_table( title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}" f"{': ' + get_subnet_name(subnet_info)}" f"\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Network: {subtensor.network} • {mechanism_label}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) # For table footers @@ -1892,22 +1869,13 @@ async def _storage_key(storage_fn: str) -> StorageKey: return if prompt and not json_output: - # TODO make this a reusable function, also used in subnets list # Show creation table. - table = Table( + table = create_table( title=( f"\n[{COLOR_PALETTE.G.HEADER}]" f"Register to [{COLOR_PALETTE.G.SUBHEAD}]netuid: {netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" f"\nNetwork: [{COLOR_PALETTE.G.SUBHEAD}]{subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n" ), - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) table.add_column( "Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center" @@ -2476,16 +2444,8 @@ async def metagraph_cmd( table_cols_indices.append(idx) table_cols.append(v) - table = Table( + table = create_table( *table_cols, - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_style="bold white", - title_justify="center", - show_lines=False, expand=True, title=( f"[underline dark_orange]Metagraph[/underline dark_orange]\n\n" @@ -2496,7 +2456,6 @@ async def metagraph_cmd( f"Issuance: [bright_blue]{metadata_info['issuance']}[/bright_blue], " f"Difficulty: [bright_cyan]{metadata_info['difficulty']}[/bright_cyan]\n" ), - pad_edge=True, ) if all(x is False for x in display_cols.values()): @@ -2521,7 +2480,7 @@ def create_identity_table(title: str = None): if not title: title = "Subnet Identity" - table = Table( + table = create_table( Column( "Item", justify="right", @@ -2530,14 +2489,6 @@ def create_identity_table(title: str = None): ), Column("Value", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]), title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{title}\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) return table From 123de68090a5a91b89b642ce454b5969b90bd1c5 Mon Sep 17 00:00:00 2001 From: leonace924 Date: Mon, 22 Dec 2025 20:32:43 -0500 Subject: [PATCH 008/113] feat: add create table unit test --- tests/unit_tests/test_utils.py | 171 +++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/tests/unit_tests/test_utils.py b/tests/unit_tests/test_utils.py index 6a3774d0f..22e4135dc 100644 --- a/tests/unit_tests/test_utils.py +++ b/tests/unit_tests/test_utils.py @@ -5,7 +5,10 @@ from bittensor_cli.src.bittensor.utils import ( check_img_mimetype, confirm_action, + create_table, ) +from rich.table import Column, Table +from bittensor_cli.src import COLOR_PALETTE @pytest.mark.parametrize( @@ -131,3 +134,171 @@ def test_confirm_action_default_values(self): result = confirm_action("Do you want to proceed?") assert result is True mock_ask.assert_called_once() + + +class TestCreateTable: + """Tests for the create_table utility function.""" + + def test_simple_table_creation(self): + """Test creating a simple table with default styling.""" + table = create_table(title="My Subnets") + + # Verify it returns a Table instance + assert isinstance(table, Table) + assert table.title == "My Subnets" + + # Verify default styling is applied + assert table.show_footer is True + assert table.show_edge is False + assert table.header_style == "bold white" + assert table.border_style == "bright_black" + assert table.title_justify == "center" + assert table.show_lines is False + assert table.pad_edge is True + + def test_table_with_columns_added_later(self): + """Test adding columns after table creation.""" + table = create_table(title="Test Table") + + # Add columns dynamically + table.add_column("Column1", justify="center") + table.add_column("Column2", justify="left") + + assert len(table.columns) == 2 + assert table.columns[0].header == "Column1" + assert table.columns[1].header == "Column2" + + # Add rows + table.add_row("Value1", "Value2") + assert len(table.rows) == 1 + + def test_table_with_column_objects(self): + """Test creating table with Column objects upfront (identity table pattern).""" + table = create_table( + Column( + "Item", + justify="right", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], + no_wrap=True, + ), + Column("Value", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]), + title="Identity", + ) + + # Verify columns were added + assert len(table.columns) == 2 + assert table.columns[0].header == "Item" + assert table.columns[1].header == "Value" + assert table.columns[0].justify == "right" + assert table.columns[0].no_wrap is True + + # Verify default styling still applied + assert table.show_footer is True + assert table.show_edge is False + + def test_custom_overrides(self): + """Test overriding default parameters.""" + table = create_table( + title="Custom Table", + show_footer=False, + border_style="blue", + show_lines=True, + ) + + # Verify overrides applied + assert table.show_footer is False + assert table.border_style == "blue" + assert table.show_lines is True + + # Verify non-overridden defaults preserved + assert table.show_edge is False + assert table.header_style == "bold white" + + def test_subnets_list_pattern(self): + """Test actual pattern from subnets_list() function.""" + table = create_table( + title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets\n" + f"Network: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]finney\n", + ) + + # Add columns as in actual code + table.add_column("[bold white]Netuid", style="grey89", justify="center") + table.add_column("[bold white]Name", style="cyan", justify="left") + table.add_column("[bold white]Price", style="dark_sea_green2", justify="left") + + assert len(table.columns) == 3 + + # Add sample row + table.add_row("1", "Alpha", "0.0025") + assert len(table.rows) == 1 + + def test_registration_pattern(self): + """Test registration confirmation table pattern.""" + table = create_table( + title=( + f"[{COLOR_PALETTE.G.HEADER}]" + f"Register to [{COLOR_PALETTE.G.SUBHEAD}]netuid: 1[/{COLOR_PALETTE.G.SUBHEAD}]\n" + f"Network: [{COLOR_PALETTE.G.SUBHEAD}]finney[/{COLOR_PALETTE.G.SUBHEAD}]\n" + ), + ) + + table.add_column("Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center") + table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"], no_wrap=True) + table.add_column("Cost (τ)", style=COLOR_PALETTE["POOLS"]["TAO"], justify="center") + + assert len(table.columns) == 3 + + # Add sample row + table.add_row("1", "α", "τ 0.5000") + assert len(table.rows) == 1 + + def test_advanced_rich_features(self): + """Test advanced Rich features with custom box and expand.""" + from rich import box + + table = create_table( + Column("Command", overflow="fold", ratio=2), + Column("Description", overflow="fold", ratio=3), + title="Commands", + box=box.ROUNDED, + expand=True, + padding=(0, 1), + ) + + assert table.box == box.ROUNDED + assert table.expand is True + assert len(table.columns) == 2 + assert table.columns[0].ratio == 2 + assert table.columns[1].ratio == 3 + + def test_empty_table_minimal_config(self): + """Test creating empty table with minimal configuration.""" + table = create_table() + + assert isinstance(table, Table) + assert table.title == "" + assert table.show_footer is True + assert len(table.columns) == 0 + + def test_multiple_column_objects_with_styling(self): + """Test multiple Column objects with various styling options.""" + table = create_table( + Column("Col1", style="cyan", justify="left"), + Column("Col2", style="green", justify="center", no_wrap=True), + Column("Col3", style="yellow", justify="right", overflow="fold"), + title="Multi-Column Test", + ) + + assert len(table.columns) == 3 + assert table.columns[0].style == "cyan" + assert table.columns[1].justify == "center" + assert table.columns[2].overflow == "fold" + + def test_rich_markup_in_title(self): + """Test that rich markup in title is preserved.""" + table = create_table( + title="[bold cyan]Test[/bold cyan] [dim]subtitle[/dim]" + ) + + assert "[bold cyan]Test[/bold cyan]" in table.title + assert "[dim]subtitle[/dim]" in table.title From 99b519bef012a551b7dd2a3fb8e48d4cda68cade Mon Sep 17 00:00:00 2001 From: leonace924 Date: Mon, 22 Dec 2025 20:35:41 -0500 Subject: [PATCH 009/113] ruff --- tests/unit_tests/test_utils.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/unit_tests/test_utils.py b/tests/unit_tests/test_utils.py index 22e4135dc..5d60ac651 100644 --- a/tests/unit_tests/test_utils.py +++ b/tests/unit_tests/test_utils.py @@ -242,9 +242,15 @@ def test_registration_pattern(self): ), ) - table.add_column("Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center") - table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"], no_wrap=True) - table.add_column("Cost (τ)", style=COLOR_PALETTE["POOLS"]["TAO"], justify="center") + table.add_column( + "Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center" + ) + table.add_column( + "Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"], no_wrap=True + ) + table.add_column( + "Cost (τ)", style=COLOR_PALETTE["POOLS"]["TAO"], justify="center" + ) assert len(table.columns) == 3 @@ -296,9 +302,7 @@ def test_multiple_column_objects_with_styling(self): def test_rich_markup_in_title(self): """Test that rich markup in title is preserved.""" - table = create_table( - title="[bold cyan]Test[/bold cyan] [dim]subtitle[/dim]" - ) + table = create_table(title="[bold cyan]Test[/bold cyan] [dim]subtitle[/dim]") assert "[bold cyan]Test[/bold cyan]" in table.title assert "[dim]subtitle[/dim]" in table.title From 6046aa55c4002b0f21303e0e6cd36c11c6ae1053 Mon Sep 17 00:00:00 2001 From: leonace924 Date: Mon, 22 Dec 2025 20:50:24 -0500 Subject: [PATCH 010/113] feat: refactor move.py and remove.py with create_table --- bittensor_cli/src/commands/stake/move.py | 21 ++--------- bittensor_cli/src/commands/stake/remove.py | 41 +++------------------- 2 files changed, 8 insertions(+), 54 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index f28d689f2..52b232264 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -16,6 +16,7 @@ from bittensor_cli.src.bittensor.utils import ( confirm_action, console, + create_table, print_error, group_subnets, get_subnet_name, @@ -167,7 +168,7 @@ async def display_stake_movement_cross_subnets( ) # Create and display table - table = Table( + table = create_table( title=( f"\n[{COLOR_PALETTE.G.HEADER}]" f"Moving stake from: " @@ -178,14 +179,6 @@ async def display_stake_movement_cross_subnets( f"[/{COLOR_PALETTE.G.SUBHEAD}]\nNetwork: {subtensor.network}\n" f"[/{COLOR_PALETTE.G.HEADER}]" ), - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) table.add_column( @@ -352,16 +345,8 @@ async def stake_move_transfer_selection( raise ValueError # Display hotkeys with stakes - table = Table( + table = create_table( title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) table.add_column("Index", justify="right") table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index eb930a60e..d1490e9fd 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -18,6 +18,7 @@ from bittensor_cli.src.bittensor.utils import ( confirm_action, console, + create_table, print_success, print_verbose, print_error, @@ -450,21 +451,13 @@ async def unstake_all( if not unstake_all_alpha else "Unstaking Summary - All Alpha Stakes" ) - table = Table( + 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" ), - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) table.add_column("Netuid", justify="center", style="grey89") table.add_column( @@ -1055,16 +1048,8 @@ async def _unstake_selection( # Display existing hotkeys, id, and staked netuids. subnet_filter = f" for Subnet {netuid}" if netuid is not None else "" - table = Table( + table = create_table( title=f"\n[{COLOR_PALETTE.G.HEADER}]Hotkeys with Stakes{subnet_filter}\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) table.add_column("Index", justify="right") table.add_column("Identity", style=COLOR_PALETTE.G.SUBHEAD) @@ -1092,18 +1077,10 @@ async def _unstake_selection( netuid_stakes = hotkey_stakes[selected_hotkey_ss58] # Display hotkey's staked netuids with amount. - table = Table( + table = create_table( title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Stakes for hotkey \n" f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey_name}\n" f"{selected_hotkey_ss58}\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) table.add_column("Subnet", justify="right") table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]) @@ -1341,16 +1318,8 @@ def _create_unstake_table( f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet_coldkey_ss58}[/{COLOR_PALETTE.G.CK}]\n" f"Network: {network}[/{COLOR_PALETTE.G.HEADER}]\n" ) - table = Table( + table = create_table( title=title, - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) table.add_column("Netuid", justify="center", style="grey89") From ae7378d1eb70903e9e651b07e130013bc162044c Mon Sep 17 00:00:00 2001 From: leonace924 Date: Mon, 22 Dec 2025 21:11:38 -0500 Subject: [PATCH 011/113] feat: refactor Table to create_table --- bittensor_cli/src/commands/stake/add.py | 11 ++-------- .../src/commands/stake/auto_staking.py | 20 +++---------------- bittensor_cli/src/commands/stake/move.py | 19 ++++++++---------- bittensor_cli/src/commands/stake/wizard.py | 11 +++++----- 4 files changed, 18 insertions(+), 43 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 275049be4..b9bff970c 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -17,6 +17,7 @@ from bittensor_cli.src.bittensor.utils import ( confirm_action, console, + create_table, get_hotkey_wallets_for_wallet, is_valid_ss58_address, print_error, @@ -651,19 +652,11 @@ def _define_stake_table( Returns: Table: An initialized rich Table object with appropriate columns """ - table = Table( + table = create_table( title=f"\n[{COLOR_PALETTE.G.HEADER}]Staking to:\n" f"Wallet: [{COLOR_PALETTE.G.CK}]{wallet.name}[/{COLOR_PALETTE.G.CK}], " f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n" f"Network: {subtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, ) table.add_column("Netuid", justify="center", style="grey89") diff --git a/bittensor_cli/src/commands/stake/auto_staking.py b/bittensor_cli/src/commands/stake/auto_staking.py index 3d7888321..0afee7bce 100644 --- a/bittensor_cli/src/commands/stake/auto_staking.py +++ b/bittensor_cli/src/commands/stake/auto_staking.py @@ -4,12 +4,12 @@ from bittensor_wallet import Wallet from rich import box -from rich.table import Table from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.utils import ( confirm_action, console, + create_table, json_console, print_success, get_subnet_name, @@ -127,7 +127,7 @@ def resolve_identity(hotkey: str) -> Optional[str]: json_console.print(json.dumps(data_output)) return data_output - table = Table( + table = create_table( title=( f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Auto Stake Destinations" f" for [bold]{coldkey_display}[/bold]\n" @@ -135,13 +135,6 @@ def resolve_identity(hotkey: str) -> Optional[str]: f"Coldkey: {coldkey_ss58}\n" f"[/{COLOR_PALETTE['GENERAL']['HEADER']}]" ), - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, box=box.SIMPLE_HEAD, ) @@ -214,18 +207,11 @@ async def set_auto_stake_destination( hotkey_identity = delegate_info.display if prompt_user and not json_output: - table = Table( + table = create_table( title=( f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Confirm Auto-Stake Destination" f"[/{COLOR_PALETTE['GENERAL']['HEADER']}]" ), - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, box=box.SIMPLE_HEAD, ) table.add_column( diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 52b232264..69934f53c 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet -from rich.table import Table from rich.prompt import Prompt from bittensor_cli.src import COLOR_PALETTE @@ -390,13 +389,12 @@ async def stake_move_transfer_selection( origin_hotkey_ss58 = origin_hotkey_info["hotkey_ss58"] # Display available netuids for selected hotkey - table = Table( + table = create_table( title=f"\n[{COLOR_PALETTE.G.HEADER}]Available Stakes for Hotkey\n[/{COLOR_PALETTE.G.HEADER}]" f"[{COLOR_PALETTE.G.HK}]{origin_hotkey_ss58}[/{COLOR_PALETTE.G.HK}]\n", - show_edge=False, - header_style="bold white", - border_style="bright_black", - title_justify="center", + show_footer=False, + show_lines=True, + pad_edge=False, width=len(origin_hotkey_ss58) + 20, ) table.add_column("Netuid", style="cyan") @@ -468,13 +466,12 @@ async def stake_swap_selection( raise ValueError # Display available stakes - table = Table( + table = create_table( title=f"\n[{COLOR_PALETTE.G.HEADER}]Available Stakes for Hotkey\n[/{COLOR_PALETTE.G.HEADER}]" f"[{COLOR_PALETTE.G.HK}]{wallet.hotkey_str}: {hotkey_ss58}[/{COLOR_PALETTE.G.HK}]\n", - show_edge=False, - header_style="bold white", - border_style="bright_black", - title_justify="center", + show_footer=False, + show_lines=True, + pad_edge=False, width=len(hotkey_ss58) + 20, ) diff --git a/bittensor_cli/src/commands/stake/wizard.py b/bittensor_cli/src/commands/stake/wizard.py index f1886f65e..1b11f93c3 100644 --- a/bittensor_cli/src/commands/stake/wizard.py +++ b/bittensor_cli/src/commands/stake/wizard.py @@ -10,12 +10,12 @@ from bittensor_wallet import Wallet from rich.prompt import Prompt -from rich.table import Table from rich.panel import Panel from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.utils import ( console, + create_table, print_error, is_valid_ss58_address, get_hotkey_pub_ss58, @@ -159,12 +159,11 @@ def get_identity(hotkey_ss58_: str) -> str: return old_identity.display return "~" - table = Table( + table = create_table( title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Your Available Stakes[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", - show_edge=False, - header_style="bold white", - border_style="bright_black", - title_justify="center", + show_footer=False, + show_lines=True, + pad_edge=False, ) table.add_column("Hotkey Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) From bb4956d9beb058b04e9a4a89418fdfc1ac421655 Mon Sep 17 00:00:00 2001 From: Angel98518 Date: Tue, 23 Dec 2025 16:44:33 +0100 Subject: [PATCH 012/113] fix: remove ValueError from Keypair.create_from_uri exception handling Per reviewer feedback: bittensor_wallet.Keypair.create_from_uri() only raises TypeError, not ValueError. Removed ValueError from exception tuples on lines 380 and 431. --- bittensor_cli/src/commands/wallets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index b614ae94b..50b66a5a6 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -377,7 +377,7 @@ async def new_hotkey( if uri: try: keypair = Keypair.create_from_uri(uri) - except (ValueError, TypeError) as e: + except TypeError as e: print_error(f"Failed to create keypair from URI {uri}: {str(e)}") return wallet.set_hotkey(keypair=keypair, encrypt=use_password) @@ -428,7 +428,7 @@ async def new_coldkey( if uri: try: keypair = Keypair.create_from_uri(uri) - except (ValueError, TypeError) as e: + except TypeError as e: print_error(f"Failed to create keypair from URI {uri}: {str(e)}") wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) From 9059da9b3aceaf467e6e9b18a46a3870a92979d6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 16:44:01 -0800 Subject: [PATCH 013/113] add ColdkeySwapAnnouncementInfo --- bittensor_cli/src/bittensor/chain_data.py | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index cfcc699f5..dc8436c38 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -901,6 +901,38 @@ def _fix_decoded(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": ) +@dataclass +class ColdkeySwapAnnouncementInfo(InfoBase): + """ + Information about a coldkey swap announcement. + + Contains information about a pending coldkey swap announcement when a coldkey + wants to declare its intent to swap to a new coldkey address. + The announcement is made before the actual swap can be executed, + allowing time for verification and security checks. + + The destination coldkey address is stored as a hash. + This is to prevent the actual coldkey address from being exposed + to the network. The hash is computed using the BlakeTwo256 hashing algorithm. + """ + + coldkey: str + execution_block: int + new_coldkey_hash: str + + @classmethod + 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, + ) + + @dataclass class SubnetState(InfoBase): netuid: int From 20e14b9542f0ef469f483701fb57baa375afac32 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 16:44:35 -0800 Subject: [PATCH 014/113] removes deprecated ScheduledColdkeySwapInfo --- bittensor_cli/src/bittensor/chain_data.py | 24 +++-------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index dc8436c38..718dbd694 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -883,35 +883,17 @@ def alpha_to_tao_with_slippage( return tao_returned, slippage, slippage_pct_float -@dataclass -class ScheduledColdkeySwapInfo(InfoBase): - """Dataclass for scheduled coldkey swap information.""" - - old_coldkey: str - new_coldkey: str - arbitration_block: int - - @classmethod - def _fix_decoded(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": - """Fixes the decoded values.""" - return cls( - old_coldkey=decode_account_id(decoded.get("old_coldkey")), - new_coldkey=decode_account_id(decoded.get("new_coldkey")), - arbitration_block=decoded.get("arbitration_block"), - ) - - @dataclass class ColdkeySwapAnnouncementInfo(InfoBase): """ Information about a coldkey swap announcement. Contains information about a pending coldkey swap announcement when a coldkey - wants to declare its intent to swap to a new coldkey address. - The announcement is made before the actual swap can be executed, + wants to declare its intent to swap to a new coldkey address. + The announcement is made before the actual swap can be executed, allowing time for verification and security checks. - The destination coldkey address is stored as a hash. + The destination coldkey address is stored as a hash. This is to prevent the actual coldkey address from being exposed to the network. The hash is computed using the BlakeTwo256 hashing algorithm. """ From 52d3d142dbd1b67826a90472a4a4d7e47264a67c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 16:48:25 -0800 Subject: [PATCH 015/113] update wallet_check_ck_swap --- bittensor_cli/cli.py | 63 +++++++++++++++----------------------------- 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 16e68c254..e06c9c9b5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3519,62 +3519,58 @@ def wallet_check_ck_swap( wallet_ss58_address: Optional[str] = Options.wallet_ss58_address, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, - scheduled_block: Optional[int] = typer.Option( - None, - "--block", - help="Block number where the swap was scheduled", - ), show_all: bool = typer.Option( False, "--all", "-a", - help="Show all pending coldkey swaps", + help="Show all pending coldkey swap announcements", ), network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): """ - Check the status of scheduled coldkey swaps. + Check the status of pending coldkey swap announcements. + + Coldkey swaps use a two-step announcement system. Use this command + to check if you have any pending announcements and when they become executable. USAGE - This command can be used in three ways: - 1. Show all pending swaps (--all) - 2. Check status of a specific wallet's swap or SS58 address - 3. Check detailed swap status with block number (--block) + This command can be used in two ways: + + 1. Show all pending announcements (--all) + + 2. Check status of a specific wallet or SS58 address EXAMPLES - Show all pending swaps: + 1. Show all pending swap announcements: + [green]$[/green] btcli wallet swap-check --all - Check specific wallet's swap: + 2. Check specific wallet's announcement: + [green]$[/green] btcli wallet swap-check --wallet-name my_wallet - Check swap using SS58 address: - [green]$[/green] btcli wallet swap-check --ss58 5DkQ4... + 3. Check announcement using SS58 address: - Check swap details with block number: - [green]$[/green] btcli wallet swap-check --wallet-name my_wallet --block 12345 + [green]$[/green] btcli wallet swap-check --ss58 5DkQ4... """ - # TODO add json_output if this ever gets used again (doubtful) self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) self.initialize_chain(network) if show_all: - return self._run_command( - wallets.check_swap_status(self.subtensor, None, None) - ) + return self._run_command(wallets.check_swap_status(self.subtensor, None)) if not wallet_ss58_address: wallet_ss58_address = Prompt.ask( "Enter [blue]wallet name[/blue] or [blue]SS58 address[/blue] [dim]" - "(leave blank to show all pending swaps)[/dim]" + "(leave blank to show all pending announcements)[/dim]" ) if not wallet_ss58_address: return self._run_command( - wallets.check_swap_status(self.subtensor, None, None) + wallets.check_swap_status(self.subtensor, None) ) if is_valid_ss58_address(wallet_ss58_address): @@ -3589,26 +3585,9 @@ def wallet_check_ck_swap( ) ss58_address = wallet.coldkeypub.ss58_address - if not scheduled_block: - block_input = Prompt.ask( - "[blue]Enter the block number[/blue] where the swap was scheduled " - "[dim](optional, press enter to skip)[/dim]", - default="", - ) - if block_input: - try: - scheduled_block = int(block_input) - except ValueError: - print_error("Invalid block number") - raise typer.Exit() - logger.debug( - "args:\n" - f"scheduled_block {scheduled_block}\n" - f"ss58_address {ss58_address}\n" - f"network {network}\n" - ) + logger.debug(f"args:\nss58_address {ss58_address}\nnetwork {network}\n") return self._run_command( - wallets.check_swap_status(self.subtensor, ss58_address, scheduled_block) + wallets.check_swap_status(self.subtensor, ss58_address) ) def wallet_create_wallet( From 52618ce603fb5e4a87ed9b3ec9f8d081f24ad5ef Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 17:24:47 -0800 Subject: [PATCH 016/113] updates check_swap_status --- bittensor_cli/src/commands/wallets.py | 129 +++++++++++++------------- 1 file changed, 62 insertions(+), 67 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 216d7bf10..6d76650a8 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2225,85 +2225,80 @@ async def find_coldkey_swap_extrinsic( async def check_swap_status( subtensor: SubtensorInterface, origin_ss58: Optional[str] = None, - expected_block_number: Optional[int] = None, ) -> None: - """ - Check the status of a coldkey swap. + """Retrieves and displays the status of coldkey swap announcements. Args: - subtensor: Connection to the network - origin_ss58: The SS58 address of the original coldkey - expected_block_number: Optional block number where the swap was scheduled - + subtensor: Connection to the network. + origin_ss58: The SS58 address of the coldkey to check. If None, shows all pending announcements. """ - - if not origin_ss58: - scheduled_swaps = await subtensor.get_scheduled_coldkey_swap() - if not scheduled_swaps: - console.print("[yellow]No pending coldkey swaps found.[/yellow]") - return - - table = Table( - Column( - "Original Coldkey", - justify="Left", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], - no_wrap=True, - ), - Column("Status", style="dark_sea_green3"), - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Pending Coldkey Swaps\n", - show_header=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, + block_hash = await subtensor.substrate.get_chain_head() + if origin_ss58: + announcement, current_block = await asyncio.gather( + subtensor.get_coldkey_swap_announcement(origin_ss58, block_hash=block_hash), + subtensor.substrate.get_block_number(block_hash=block_hash), ) + if not announcement: + console.print( + f"[yellow]No pending swap announcement found for coldkey:[/yellow] " + f"[{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + ) + return + announcements = [announcement] - for coldkey in scheduled_swaps: - table.add_row(coldkey, "Pending") - - console.print(table) - console.print( - "\n[dim]Tip: Check specific swap details by providing the original coldkey " - "SS58 address and the block number.[/dim]" - ) - return - chain_reported_completion_block, destination_address = await subtensor.query( - "SubtensorModule", "ColdkeySwapScheduled", [origin_ss58] - ) - destination_address = decode_account_id(destination_address[0]) - if chain_reported_completion_block != 0 and destination_address != GENESIS_ADDRESS: - is_pending = True else: - is_pending = False - - if not is_pending: - console.print( - f"[red]No pending swap found for coldkey:[/red] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + announcements, current_block = await asyncio.gather( + subtensor.get_coldkey_swap_announcements(block_hash=block_hash), + subtensor.substrate.get_block_number(block_hash=block_hash), ) - return + if not announcements: + console.print( + "[yellow]No pending coldkey swap announcements found.[/yellow]" + ) + return - console.print( - f"[green]Found pending swap for coldkey:[/green] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + table = Table( + Column( + "Coldkey", + justify="left", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], + no_wrap=True, + ), + Column("New Coldkey Hash", justify="left", style="dim", no_wrap=True), + Column("Execution Block", justify="right", style="dark_sea_green3"), + Column("Time Remaining", justify="right", style="yellow"), + Column("Status", justify="center", style="green"), + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Pending Coldkey Swap Announcements\nCurrent Block: {current_block}\n", + show_header=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, ) - if expected_block_number is None: - expected_block_number = chain_reported_completion_block - - current_block = await subtensor.substrate.get_block_number() - remaining_blocks = expected_block_number - current_block + for announcement in announcements: + remaining_blocks = announcement.execution_block - current_block + if remaining_blocks <= 0: + status = "Ready" + time_str = "[green]Ready[/green]" + else: + status = "Pending" + time_str = blocks_to_duration(remaining_blocks) + hash_display = f"{announcement.new_coldkey_hash[:12]}...{announcement.new_coldkey_hash[-6:]}" - if remaining_blocks <= 0: - console.print("[green]Swap period has completed![/green]") - return + table.add_row( + announcement.coldkey, + hash_display, + str(announcement.execution_block), + time_str, + status, + ) + console.print(table) console.print( - "\n[green]Coldkey swap details:[/green]" - f"\nOriginal address: [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" - f"\nDestination address: [{COLORS.G.CK}]{destination_address}[/{COLORS.G.CK}]" - f"\nCompletion block: {chain_reported_completion_block}" - f"\nTime remaining: {blocks_to_duration(remaining_blocks)}" + "\n[dim]To execute a ready swap:[/dim] " + "[green]btcli wallet swap-coldkey execute[/green]" ) From 4b8c7df3077211216f0c85f897ed94cd3504e1d9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 17:28:34 -0800 Subject: [PATCH 017/113] add get_coldkey_swap_announcements --- .../src/bittensor/subtensor_interface.py | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index bf2f91a23..bd5220018 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -30,6 +30,7 @@ MetagraphInfo, SimSwapResult, CrowdloanData, + ColdkeySwapAnnouncementInfo, ) from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float @@ -1805,30 +1806,63 @@ async def sim_swap( destination_netuid, ) - async def get_scheduled_coldkey_swap( + async def get_coldkey_swap_announcements( self, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[list[str]]: - """ - Queries the chain to fetch the list of coldkeys that are scheduled for a swap. + ) -> list[ColdkeySwapAnnouncementInfo]: + """Fetches all pending coldkey swap announcements. - :param block_hash: Block hash at which to perform query. - :param reuse_block: Whether to reuse the last-used block hash. + Args: + block_hash: Block hash at which to perform query. + reuse_block: Whether to reuse the last-used block hash. - :return: A list of SS58 addresses of the coldkeys that are scheduled for a coldkey swap. + Returns: + A list of ColdkeySwapAnnouncementInfo for all pending announcements. """ result = await self.substrate.query_map( module="SubtensorModule", - storage_function="ColdkeySwapScheduled", + storage_function="ColdkeySwapAnnouncements", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + announcements = [] + async for ss58, data in result: + coldkey = decode_account_id(ss58) + announcements.append( + ColdkeySwapAnnouncementInfo._fix_decoded(coldkey, data) + ) + return announcements + + 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. + """ + result = await self.query( + module="SubtensorModule", + storage_function="ColdkeySwapAnnouncements", + params=[coldkey_ss58], block_hash=block_hash, reuse_block_hash=reuse_block, ) - keys_pending_swap = [] - async for ss58, _ in result: - keys_pending_swap.append(decode_account_id(ss58)) - return keys_pending_swap + if result is None: + return None + + return ColdkeySwapAnnouncementInfo._fix_decoded(coldkey_ss58, result) async def get_crowdloans( self, block_hash: Optional[str] = None From 476778440eea8bf594fd892ec87b539085397b08 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 19:37:15 -0800 Subject: [PATCH 018/113] update wallet_swap_coldkey cmd --- bittensor_cli/cli.py | 99 ++++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e06c9c9b5..925806f51 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4105,6 +4105,10 @@ def wallet_verify( def wallet_swap_coldkey( self, + action: str = typer.Argument( + None, + help="Action to perform: 'announce' to announce intent, 'execute' to complete swap after delay.", + ), wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, @@ -4114,38 +4118,58 @@ def wallet_swap_coldkey( "--new-coldkey-ss58", "--new-wallet", "--new", - help="SS58 address of the new coldkey that will replace the current one.", + help="SS58 address or wallet name of the new coldkey.", ), network: Optional[list[str]] = Options.network, - proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, quiet: bool = Options.quiet, verbose: bool = Options.verbose, - force_swap: bool = typer.Option( - False, - "--force", - "-f", - "--force-swap", - help="Force the swap even if the new coldkey is already scheduled for a swap.", - ), + decline: bool = Options.decline, ): """ - Schedule a coldkey swap for a wallet. + Swap your coldkey to a new address using a two-step announcement process. + + Coldkey swaps require two steps for security: + + 1. [bold]Announce[/bold]: Declare your intent to swap. This pays the swap fee and starts a delay period. - This command allows you to schedule a coldkey swap for a wallet. You can either provide a new wallet name, or SS58 address. + 2. [bold]Execute[/bold]: After the delay (typically 5 days), complete the swap. EXAMPLES - [green]$[/green] btcli wallet schedule-coldkey-swap --new-wallet my_new_wallet + Step 1 - Announce your intent to swap: + + [green]$[/green] btcli wallet swap-coldkey announce --new-coldkey 5Dk...X3q + + Step 2 - After the delay period, execute the swap: - [green]$[/green] btcli wallet schedule-coldkey-swap --new-coldkey-ss58 5Dk...X3q + [green]$[/green] btcli wallet swap-coldkey execute --new-coldkey 5Dk...X3q + + Check status of pending swaps: + + [green]$[/green] btcli wallet swap-check """ self.verbosity_handler(quiet, verbose, prompt=False, json_output=False) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + + if not action: + console.print( + "\n[bold][blue]Coldkey Swap Actions:[/blue][/bold]\n" + " [dark_sea_green3]announce[/dark_sea_green3] - Start the swap process (pays fee, starts delay timer)\n" + " [dark_sea_green3]execute[/dark_sea_green3] - Complete the swap (after delay period)\n\n" + " [dim]You can check the current status of your swap with 'btcli wallet swap-check'.[/dim]\n" + ) + action = Prompt.ask( + "Select action", + choices=["announce", "execute"], + default="announce", + ) + + if action.lower() not in ("announce", "execute"): + print_error(f"Invalid action: {action}. Must be 'announce' or 'execute'.") + raise typer.Exit(1) if not wallet_name: wallet_name = Prompt.ask( - "Enter the [blue]wallet name[/blue] which you want to swap the coldkey for", + "Enter the [blue]wallet name[/blue] of the coldkey to swap", default=self.config.get("wallet_name") or defaults.wallet.name, ) wallet = self.wallet_ask( @@ -4156,8 +4180,7 @@ def wallet_swap_coldkey( validate=WV.WALLET, ) console.print( - f"\nWallet selected to swap the [blue]coldkey[/blue] from: \n" - f"[dark_sea_green3]{wallet}[/dark_sea_green3]\n" + f"\nWallet selected: [dark_sea_green3]{wallet}[/dark_sea_green3]\n" ) if not new_wallet_or_ss58: @@ -4177,25 +4200,37 @@ def wallet_swap_coldkey( validate=WV.WALLET, ) console.print( - f"\nNew wallet to swap the [blue]coldkey[/blue] to: \n" - f"[dark_sea_green3]{new_wallet}[/dark_sea_green3]\n" + f"\nNew coldkey wallet: [dark_sea_green3]{new_wallet}[/dark_sea_green3]\n" ) new_wallet_coldkey_ss58 = new_wallet.coldkeypub.ss58_address + logger.debug( - "args:\n" - f"network {network}\n" - f"new_coldkey_ss58 {new_wallet_coldkey_ss58}\n" - f"force_swap {force_swap}" + f"args:\n" + f"action: {action}\n" + f"network: {network}\n" + f"new_coldkey_ss58: {new_wallet_coldkey_ss58}" ) - return self._run_command( - wallets.schedule_coldkey_swap( - wallet=wallet, - subtensor=self.initialize_chain(network), - new_coldkey_ss58=new_wallet_coldkey_ss58, - force_swap=force_swap, - proxy=proxy, + + if action == "announce": + return self._run_command( + wallets.announce_coldkey_swap( + wallet=wallet, + subtensor=self.initialize_chain(network), + new_coldkey_ss58=new_wallet_coldkey_ss58, + decline=decline, + quiet=quiet, + ) + ) + else: + return self._run_command( + wallets.execute_coldkey_swap( + wallet=wallet, + subtensor=self.initialize_chain(network), + new_coldkey_ss58=new_wallet_coldkey_ss58, + decline=decline, + quiet=quiet, + ) ) - ) def axon_reset( self, From 7474832ca9fb8d4862b6e65812017416a37418d8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 19:44:45 -0800 Subject: [PATCH 019/113] add get_coldkey_swap_cost --- .../src/bittensor/subtensor_interface.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index bd5220018..4a435ff7b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1984,6 +1984,30 @@ async def get_coldkey_swap_schedule_duration( return result + async def get_coldkey_swap_cost( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> 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. + """ + swap_cost = await self.substrate.get_constant( + module_name="SubtensorModule", + constant_name="KeySwapCost", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + if swap_cost is None: + return None + return Balance.from_rao(swap_cost.value) + async def get_coldkey_claim_type( self, coldkey_ss58: str, From 423ae03f10108865d4a8ff4a8bf520f2fe8c05ac Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 19:45:22 -0800 Subject: [PATCH 020/113] add get_coldkey_swap_reannouncement_delay --- bittensor_cli/src/bittensor/subtensor_interface.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 4a435ff7b..7e39459ff 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1959,24 +1959,26 @@ async def get_crowdloan_contribution( return Balance.from_rao(contribution) return None - async def get_coldkey_swap_schedule_duration( + async def get_coldkey_swap_reannouncement_delay( self, block_hash: Optional[str] = None, reuse_block: bool = False, ) -> int: - """ - Retrieves the duration (in blocks) required for a coldkey swap to be executed. + """Retrieves the delay (in blocks) before the user can reannounce a coldkey swap. + + If the user has already announced a swap, they must wait this many blocks + after the original execution block before they can announce a new swap. 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: - int: The number of blocks required for the coldkey swap schedule duration. + The number of blocks to wait before reannouncing. """ result = await self.query( module="SubtensorModule", - storage_function="ColdkeySwapScheduleDuration", + storage_function="ColdkeySwapReannouncementDelay", params=[], block_hash=block_hash, reuse_block_hash=reuse_block, From c7563b0a4525795f145ba77821dad526fd6f9356 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 19:45:49 -0800 Subject: [PATCH 021/113] add get_coldkey_swap_announcement_delay --- .../src/bittensor/subtensor_interface.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 7e39459ff..5daec09b9 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1959,6 +1959,33 @@ async def get_crowdloan_contribution( return Balance.from_rao(contribution) return None + 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. + + This is the time the user must wait after announcing a coldkey swap + before they can execute the swap. + + 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. + """ + result = await self.query( + module="SubtensorModule", + storage_function="ColdkeySwapAnnouncementDelay", + params=[], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + return result + async def get_coldkey_swap_reannouncement_delay( self, block_hash: Optional[str] = None, From c45dd04c74e557729ca1df4bcd8d7c35c3cc56c7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 12:34:12 -0800 Subject: [PATCH 022/113] add reusable table --- bittensor_cli/src/commands/wallets.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 6d76650a8..208df6aba 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1,4 +1,5 @@ import asyncio +import hashlib import itertools import json import os @@ -1767,11 +1768,16 @@ async def swap_hotkey( return result -def create_identity_table(title: str = None): - if not title: - title = "On-Chain Identity" +def create_key_value_table(title: str = "Details") -> Table: + """Creates a key-value table for displaying information for various cmds. - table = Table( + Args: + title: The title shown above the table. + + Returns: + A Rich Table for key-value display. + """ + return Table( Column( "Item", justify="right", @@ -1789,7 +1795,6 @@ def create_identity_table(title: str = None): show_lines=False, pad_edge=True, ) - return table async def set_id( @@ -1846,7 +1851,7 @@ async def set_id( output_dict["success"] = True identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) - table = create_identity_table(title="New on-chain Identity") + table = create_key_value_table(title="New on-chain Identity\n") table.add_row("Address", wallet.coldkeypub.ss58_address) for key, value in identity.items(): table.add_row(key, str(value) if value else "~") @@ -1880,7 +1885,7 @@ async def get_id( json_console.print("{}") return {} - table = create_identity_table(title) + table = create_key_value_table(title) table.add_row("Address", ss58_address) for key, value in identity.items(): table.add_row(key, str(value) if value else "~") From 4b41956bf44660447eafc3105241707758e05816 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 12:35:02 -0800 Subject: [PATCH 023/113] add compute_coldkey_hash --- bittensor_cli/src/commands/wallets.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 208df6aba..7f87280d5 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2046,6 +2046,26 @@ async def verify( return is_valid +def compute_coldkey_hash(ss58_address: str) -> str: + """ + Compute Blake2b-256 hash of a coldkey AccountId. + + Args: + ss58_address: SS58 address of the coldkey. + + Returns: + str: 0x-prefixed hex hash. + + Notes: + Hashes AccountId bytes (not the SS58). Used by coldkey-swap announcements. + """ + keypair = Keypair(ss58_address=ss58_address) + public_key_bytes = keypair.public_key + + hash_result = hashlib.blake2b(public_key_bytes, digest_size=32) + return "0x" + hash_result.hexdigest() + + async def schedule_coldkey_swap( wallet: Wallet, subtensor: SubtensorInterface, From a2d56e0c4198ae97f75781db8d8f2effe45319b9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 12:36:17 -0800 Subject: [PATCH 024/113] add announce_coldkey_swap --- bittensor_cli/src/commands/wallets.py | 202 ++++++++++++++++++-------- 1 file changed, 138 insertions(+), 64 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 7f87280d5..1b0f8fdce 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2066,66 +2066,139 @@ def compute_coldkey_hash(ss58_address: str) -> str: return "0x" + hash_result.hexdigest() -async def schedule_coldkey_swap( +async def announce_coldkey_swap( wallet: Wallet, subtensor: SubtensorInterface, new_coldkey_ss58: str, - force_swap: bool = False, decline: bool = False, quiet: bool = False, proxy: Optional[str] = None, ) -> bool: - """Schedules a coldkey swap operation to be executed at a future block. + """Announces intent to swap a coldkey to a new address. + + This is the first step of a two-step coldkey swap process. After announcing, + the user must wait for the announcement delay period to pass before executing + the swap with execute_coldkey_swap. Args: - wallet (Wallet): The wallet initiating the coldkey swap - subtensor (SubtensorInterface): Connection to the Bittensor network - new_coldkey_ss58 (str): SS58 address of the new coldkey - force_swap (bool, optional): Whether to force the swap even if the new coldkey is already scheduled for a swap. Defaults to False. + wallet: The wallet initiating the coldkey swap. + subtensor: Connection to the Bittensor network. + new_coldkey_ss58: SS58 address of the new coldkey. + decline: If True, skip confirmation prompt and decline. + quiet: If True, skip confirmation prompt and proceed. + proxy: Optional proxy SS58 address for the transaction. + Returns: - bool: True if the swap was scheduled successfully, False otherwise + True if the announcement was successful, False otherwise. """ if not is_valid_ss58_address(new_coldkey_ss58): print_error(f"Invalid SS58 address format: {new_coldkey_ss58}") return False - scheduled_coldkey_swap = await subtensor.get_scheduled_coldkey_swap() - if wallet.coldkeypub.ss58_address in scheduled_coldkey_swap: - print_error( - f"Coldkey {wallet.coldkeypub.ss58_address} is already scheduled for a swap." - ) - console.print("[dim]Use the force_swap (--force) flag to override this.[/dim]") - if not force_swap: + # Check for existing announcement + block_hash = await subtensor.substrate.get_chain_head() + new_coldkey_hash = compute_coldkey_hash(new_coldkey_ss58) + + existing = await subtensor.get_coldkey_swap_announcement( + wallet.coldkeypub.ss58_address, + block_hash=block_hash, + ) + if existing: + current_block, reannounce_delay, announce_delay = await asyncio.gather( + subtensor.substrate.get_block_number(block_hash=block_hash), + subtensor.get_coldkey_swap_reannouncement_delay(block_hash=block_hash), + subtensor.get_coldkey_swap_announcement_delay(block_hash=block_hash), + ) + remaining = existing.execution_block - current_block + reannounce_block = existing.execution_block + reannounce_delay + same_hash = new_coldkey_hash.lower() == str(existing.new_coldkey_hash).lower() + + # Show existing announcement info + table = create_key_value_table("Existing Coldkey Swap Announcement") + table.add_row("Execution Block", str(existing.execution_block)) + if remaining > 0: + table.add_row( + "Time Remaining", f"[yellow]{blocks_to_duration(remaining)}[/yellow]" + ) + table.add_row("Status", "[yellow]Pending[/yellow]") + else: + table.add_row("Status", "[green]Ready to Execute[/green]") + table.add_row("Announced Hash", f"[dim]{existing.new_coldkey_hash}[/dim]") + table.add_row("Requested Hash", f"[dim]{new_coldkey_hash}[/dim]") + table.add_row("Match", "[green]Yes[/green]" if same_hash else "[red]No[/red]") + console.print(table) + + # Check if reannouncement allowed + if current_block < reannounce_block: + time_until_reannounce = blocks_to_duration(reannounce_block - current_block) + console.print( + f"\n[dim]You can reannounce after block {reannounce_block} ({time_until_reannounce} from now).[/dim]", + f"Current block: {current_block}", + ) return False + + # Reannouncement allowed + if same_hash: + console.print( + "\n[yellow]You already have an announcement for this coldkey.[/yellow] " + "You can execute the existing swap without reannouncing." + ) + if not confirm_action( + "Do you still want to reannounce the same hash (the period to wait before executing the swap will be reset)?", + decline=decline, + quiet=quiet, + ): + return False else: console.print( - "[yellow]Continuing with the swap due to force_swap flag.[/yellow]\n" + f"\n[dim]Reannouncing with a different coldkey will reset the waiting period " + f"to {blocks_to_duration(announce_delay)} from now.[/dim]" ) + if not confirm_action( + "Proceed with reannouncement and reset the waiting period?", + decline=decline, + quiet=quiet, + ): + return False - prompt_msg = ( - "You are [red]swapping[/red] your [blue]coldkey[/blue] to a new address.\n" - f"Current ss58: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]\n" - f"New ss58: [{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]\n" - "Are you sure you want to continue?" + # Proceed with the announcement + swap_cost, delay = await asyncio.gather( + subtensor.get_coldkey_swap_cost(block_hash=block_hash), + subtensor.get_coldkey_swap_announcement_delay(block_hash=block_hash), + ) + + table = create_key_value_table("Announcing Coldkey Swap\n") + table.add_row( + "Current Coldkey", + f"[{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]", + ) + table.add_row("New Coldkey", f"[{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]") + table.add_row( + "New Coldkey Hash", + f"[dim]{new_coldkey_hash}[/dim]", + ) + table.add_row("Swap Cost", f"[green]{swap_cost}[/green]") + table.add_row( + "Delay Before Execution", f"[yellow]{blocks_to_duration(delay)}[/yellow]" ) - if not confirm_action(prompt_msg, decline=decline, quiet=quiet): + console.print(table) + + if not confirm_action( + "Are you sure you want to continue?", decline=decline, quiet=quiet + ): return False if not unlock_key(wallet).success: return False - block_pre_call, call = await asyncio.gather( - subtensor.substrate.get_block_number(), - subtensor.substrate.compose_call( + with console.status(":satellite: Announcing coldkey swap on-chain..."): + call = await subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="schedule_swap_coldkey", + call_function="announce_coldkey_swap", call_params={ - "new_coldkey": new_coldkey_ss58, + "new_coldkey_hash": new_coldkey_hash, }, - ), - ) - swap_info = None - with console.status(":satellite: Scheduling coldkey swap on-chain..."): + ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, @@ -2133,53 +2206,54 @@ async def schedule_coldkey_swap( wait_for_finalization=True, proxy=proxy, ) - block_post_call = await subtensor.substrate.get_block_number() if not success: - print_error(f"Failed to schedule coldkey swap: {err_msg}") + print_error(f"Failed to announce coldkey swap: {err_msg}") return False console.print( - ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" + ":white_heavy_check_mark: [green]Successfully announced coldkey swap[/green]" ) await print_extrinsic_id(ext_receipt) - for event in await ext_receipt.triggered_events: - if ( - event.get("event", {}).get("module_id") == "SubtensorModule" - and event.get("event", {}).get("event_id") == "ColdkeySwapScheduled" - ): - attributes = event["event"].get("attributes", {}) - old_coldkey = decode_account_id(attributes["old_coldkey"][0]) - if old_coldkey == wallet.coldkeypub.ss58_address: - swap_info = { - "block_num": block_pre_call, - "dest_coldkey": decode_account_id(attributes["new_coldkey"][0]), - "execution_block": attributes["execution_block"], - } + # Post-success information + new_block_hash = await subtensor.substrate.get_chain_head() + announcement = await subtensor.get_coldkey_swap_announcement( + wallet.coldkeypub.ss58_address, + block_hash=new_block_hash, + ) + if announcement: + current_block = await subtensor.substrate.get_block_number( + block_hash=new_block_hash + ) + remaining = announcement.execution_block - current_block - if not swap_info: - swap_info = await find_coldkey_swap_extrinsic( - subtensor=subtensor, - start_block=block_pre_call, - end_block=block_post_call, - wallet_ss58=wallet.coldkeypub.ss58_address, + details_table = create_key_value_table("Coldkey Swap Announced\n") + details_table.add_row( + "Original Coldkey", + f"[{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]", + ) + details_table.add_row( + "New Coldkey", f"[{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]" + ) + details_table.add_row( + "New Coldkey Hash", + f"[dim]{new_coldkey_hash}[/dim]", + ) + details_table.add_row( + "Execution Block", f"[green]{announcement.execution_block}[/green]" + ) + details_table.add_row( + "Time Until Executable", f"[yellow]{blocks_to_duration(remaining)}[/yellow]" ) - if not swap_info: + console.print(details_table) console.print( - "[yellow]Warning: Could not find the swap extrinsic in recent blocks" + f"\n[dim]After the delay, run:" + f"\n[green]btcli wallet swap-coldkey execute --new-coldkey {new_coldkey_ss58}[/green]" ) - return True - console.print( - "\n[green]Coldkey swap details:[/green]" - f"\nBlock number: {swap_info['block_num']}" - f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" - f"\nDestination address: [{COLORS.G.CK}]{swap_info['dest_coldkey']}[/{COLORS.G.CK}]" - f"\nThe swap will be completed at block: [green]{swap_info['execution_block']}[/green]" - f"\n[dim]You can provide the block number to `btcli wallet swap-check`[/dim]" - ) + return True async def find_coldkey_swap_extrinsic( From 78b7c9e9d1517a0706c6bb367dd70f989ddd85c3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 12:36:46 -0800 Subject: [PATCH 025/113] add execute_coldkey_swap --- bittensor_cli/src/commands/wallets.py | 165 ++++++++++++++++++-------- 1 file changed, 114 insertions(+), 51 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 1b0f8fdce..353c085b4 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2256,69 +2256,132 @@ async def announce_coldkey_swap( return True -async def find_coldkey_swap_extrinsic( +async def execute_coldkey_swap( + wallet: Wallet, subtensor: SubtensorInterface, - start_block: int, - end_block: int, - wallet_ss58: str, -) -> dict: - """Search for a coldkey swap event in a range of blocks. + new_coldkey_ss58: str, + decline: bool = False, + quiet: bool = False, + proxy: Optional[str] = None, +) -> bool: + """Executes a previously announced coldkey swap. + + This is the second step of a two-step coldkey swap process. You must have + previously called announce_coldkey_swap and waited for the delay period. Args: - subtensor: SubtensorInterface for chain queries - start_block: Starting block number to search - end_block: Ending block number to search (inclusive) - wallet_ss58: SS58 address of the signing wallet + wallet: The wallet executing the coldkey swap. + subtensor: Connection to the Bittensor network. + new_coldkey_ss58: SS58 address of the new coldkey (must match announcement). + decline: If True, skip confirmation prompt and decline. + quiet: If True, skip confirmation prompt and proceed. + proxy: Optional proxy SS58 address for the transaction. Returns: - dict: Contains the following keys if found: - - block_num: Block number where swap was scheduled - - dest_coldkey: SS58 address of destination coldkey - - execution_block: Block number when swap will execute - Empty dict if not found + True if the swap was executed successfully, False otherwise. """ + if not is_valid_ss58_address(new_coldkey_ss58): + print_error(f"Invalid SS58 address format: {new_coldkey_ss58}") + return False - current_block, genesis_block = await asyncio.gather( - subtensor.substrate.get_block_number(), subtensor.substrate.get_block_hash(0) + block_hash = await subtensor.substrate.get_chain_head() + announcement = await subtensor.get_coldkey_swap_announcement( + wallet.coldkeypub.ss58_address, + block_hash=block_hash, ) - if ( - current_block - start_block > 300 - and genesis_block == Constants.genesis_block_hash_map["finney"] - ): - console.print("Querying archive node for coldkey swap events...") - await subtensor.substrate.close() - subtensor = SubtensorInterface("archive") - - block_hashes = await asyncio.gather( - *[ - subtensor.substrate.get_block_hash(block_num) - for block_num in range(start_block, end_block + 1) - ] + if not announcement: + print_error( + f"No pending coldkey swap announcement found for {wallet.coldkeypub.ss58_address}.\n" + "You must first announce your swap with 'btcli wallet swap-coldkey announce'." + ) + return False + + expected_hash = compute_coldkey_hash(new_coldkey_ss58) + if announcement.new_coldkey_hash != expected_hash: + table = create_key_value_table("Coldkey Hash Mismatch\n") + table.add_row("Announced Hash", f"[dim]{announcement.new_coldkey_hash}[/dim]") + table.add_row("Provided Hash", f"[dim]{expected_hash}[/dim]") + console.print(table) + print_error( + "The provided coldkey does not match the announced hash.\n" + "Make sure you're using the same coldkey you announced." + ) + return False + + current_block = await subtensor.substrate.get_block_number(block_hash=block_hash) + if current_block < announcement.execution_block: + remaining = announcement.execution_block - current_block + table = create_key_value_table("Coldkey Swap Not Ready") + table.add_row("Current Block", str(current_block)) + table.add_row("Execution Block", str(announcement.execution_block)) + table.add_row( + "Time Remaining", f"[yellow]{blocks_to_duration(remaining)}[/yellow]" + ) + console.print(table) + print_error( + "Coldkey swap cannot be executed yet. Please wait for the delay period." + ) + return False + + # Display confirmation table + table = create_key_value_table("Executing Coldkey Swap\n") + table.add_row( + "Current Coldkey", + f"[{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]", ) - block_events = await asyncio.gather( - *[ - subtensor.substrate.get_events(block_hash=block_hash) - for block_hash in block_hashes - ] + table.add_row("New Coldkey", f"[{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]") + + console.print(table) + console.print( + "\n[bold red]WARNING:[/bold red] This action is irreversible. All assets will be transferred.\n" ) - for block_num, events in zip(range(start_block, end_block + 1), block_events): - for event in events: - if ( - event.get("event", {}).get("module_id") == "SubtensorModule" - and event.get("event", {}).get("event_id") == "ColdkeySwapScheduled" - ): - attributes = event["event"].get("attributes", {}) - old_coldkey = decode_account_id(attributes["old_coldkey"][0]) - - if old_coldkey == wallet_ss58: - return { - "block_num": block_num, - "dest_coldkey": decode_account_id(attributes["new_coldkey"][0]), - "execution_block": attributes["execution_block"], - } + if not confirm_action( + "Are you sure you want to continue?", decline=decline, quiet=quiet + ): + return False + + if not unlock_key(wallet).success: + return False + + with console.status(":satellite: Executing coldkey swap on-chain..."): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="swap_coldkey_announced", + call_params={ + "new_coldkey": new_coldkey_ss58, + }, + ) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + proxy=proxy, + ) + + if not success: + print_error(f"Failed to execute coldkey swap: {err_msg}") + return False + + console.print( + ":white_heavy_check_mark: [green]Successfully executed coldkey swap![/green]" + ) + await print_extrinsic_id(ext_receipt) - return {} + # Success details table + success_table = create_key_value_table("Coldkey Swap Completed\n") + success_table.add_row( + "Old Coldkey", + f"[{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]", + ) + success_table.add_row( + "New Coldkey", f"[{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]" + ) + console.print(success_table) + console.print("\n[dim]All assets have been transferred to the new coldkey.[/dim]") + + return True async def check_swap_status( From 8ce14a640f0553dbf7e7533cd8798eb2203551db Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 12:44:40 -0800 Subject: [PATCH 026/113] remove proxy in ck swaps --- bittensor_cli/src/commands/wallets.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 353c085b4..c622a5647 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2072,7 +2072,6 @@ async def announce_coldkey_swap( new_coldkey_ss58: str, decline: bool = False, quiet: bool = False, - proxy: Optional[str] = None, ) -> bool: """Announces intent to swap a coldkey to a new address. @@ -2086,7 +2085,6 @@ async def announce_coldkey_swap( new_coldkey_ss58: SS58 address of the new coldkey. decline: If True, skip confirmation prompt and decline. quiet: If True, skip confirmation prompt and proceed. - proxy: Optional proxy SS58 address for the transaction. Returns: True if the announcement was successful, False otherwise. @@ -2204,7 +2202,6 @@ async def announce_coldkey_swap( wallet, wait_for_inclusion=True, wait_for_finalization=True, - proxy=proxy, ) if not success: @@ -2262,7 +2259,6 @@ async def execute_coldkey_swap( new_coldkey_ss58: str, decline: bool = False, quiet: bool = False, - proxy: Optional[str] = None, ) -> bool: """Executes a previously announced coldkey swap. @@ -2275,7 +2271,6 @@ async def execute_coldkey_swap( new_coldkey_ss58: SS58 address of the new coldkey (must match announcement). decline: If True, skip confirmation prompt and decline. quiet: If True, skip confirmation prompt and proceed. - proxy: Optional proxy SS58 address for the transaction. Returns: True if the swap was executed successfully, False otherwise. @@ -2357,7 +2352,6 @@ async def execute_coldkey_swap( wallet, wait_for_inclusion=True, wait_for_finalization=True, - proxy=proxy, ) if not success: From 266b1dff8796055eaecad3718ac66a4f49f789a3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 13:38:46 -0800 Subject: [PATCH 027/113] adds mev_protection and warning --- bittensor_cli/cli.py | 3 +++ bittensor_cli/src/commands/wallets.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 925806f51..bb9e10e13 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4120,6 +4120,7 @@ def wallet_swap_coldkey( "--new", help="SS58 address or wallet name of the new coldkey.", ), + mev_protection: bool = Options.mev_protection, network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4219,6 +4220,7 @@ def wallet_swap_coldkey( new_coldkey_ss58=new_wallet_coldkey_ss58, decline=decline, quiet=quiet, + mev_protection=mev_protection, ) ) else: @@ -4229,6 +4231,7 @@ def wallet_swap_coldkey( new_coldkey_ss58=new_wallet_coldkey_ss58, decline=decline, quiet=quiet, + mev_protection=mev_protection, ) ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index c622a5647..b76c94deb 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2072,6 +2072,7 @@ async def announce_coldkey_swap( new_coldkey_ss58: str, decline: bool = False, quiet: bool = False, + mev_protection: bool = False, ) -> bool: """Announces intent to swap a coldkey to a new address. @@ -2085,6 +2086,7 @@ async def announce_coldkey_swap( new_coldkey_ss58: SS58 address of the new coldkey. decline: If True, skip confirmation prompt and decline. quiet: If True, skip confirmation prompt and proceed. + mev_protection: If True, encrypt the extrinsic with MEV protection. Returns: True if the announcement was successful, False otherwise. @@ -2202,6 +2204,7 @@ async def announce_coldkey_swap( wallet, wait_for_inclusion=True, wait_for_finalization=True, + mev_protection=mev_protection, ) if not success: @@ -2259,6 +2262,7 @@ async def execute_coldkey_swap( new_coldkey_ss58: str, decline: bool = False, quiet: bool = False, + mev_protection: bool = True, ) -> bool: """Executes a previously announced coldkey swap. @@ -2271,6 +2275,7 @@ async def execute_coldkey_swap( new_coldkey_ss58: SS58 address of the new coldkey (must match announcement). decline: If True, skip confirmation prompt and decline. quiet: If True, skip confirmation prompt and proceed. + mev_protection: If True, encrypt the extrinsic with MEV protection. Returns: True if the swap was executed successfully, False otherwise. @@ -2279,6 +2284,12 @@ async def execute_coldkey_swap( print_error(f"Invalid SS58 address format: {new_coldkey_ss58}") return False + if not mev_protection: + console.print( + "[yellow]WARNING: MEV protection is disabled.\n" + "This transaction is not protected & will expose the new coldkey.[/yellow]" + ) + block_hash = await subtensor.substrate.get_chain_head() announcement = await subtensor.get_coldkey_swap_announcement( wallet.coldkeypub.ss58_address, @@ -2352,6 +2363,7 @@ async def execute_coldkey_swap( wallet, wait_for_inclusion=True, wait_for_finalization=True, + mev_protection=mev_protection, ) if not success: From e6c70507ef09644e5d2cdd3071cf1d5d6795bca4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 13:45:47 -0800 Subject: [PATCH 028/113] cleanup --- bittensor_cli/src/commands/wallets.py | 55 +++------------------------ 1 file changed, 5 insertions(+), 50 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index b76c94deb..acbd67ab4 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -16,7 +16,7 @@ from rich.table import Column, Table from rich.tree import Tree from rich.padding import Padding -from bittensor_cli.src import COLOR_PALETTE, COLORS, Constants +from bittensor_cli.src import COLOR_PALETTE, COLORS from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import ( @@ -29,18 +29,15 @@ ) from bittensor_cli.src.bittensor.extrinsics.transfer import transfer_extrinsic from bittensor_cli.src.bittensor.networking import int_to_ip -from bittensor_cli.src.bittensor.subtensor_interface import ( - SubtensorInterface, - GENESIS_ADDRESS, -) +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( RAO_PER_TAO, confirm_action, console, - convert_blocks_to_time, json_console, print_error, print_verbose, + print_success, get_all_wallets_for_path, get_hotkey_wallets_for_wallet, is_valid_ss58_address, @@ -50,7 +47,6 @@ unlock_key, WalletLike, blocks_to_duration, - decode_account_id, get_hotkey_pub_ss58, print_extrinsic_id, ) @@ -1896,43 +1892,6 @@ async def get_id( return identity -async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): - arbitration_check = len( # TODO verify this works - ( - await subtensor.query( - module="SubtensorModule", - storage_function="ColdkeySwapDestinations", - params=[wallet.coldkeypub.ss58_address], - ) - ) - ) - if arbitration_check == 0: - console.print( - "[green]There has been no previous key swap initiated for your coldkey.[/green]" - ) - elif arbitration_check == 1: - arbitration_block = await subtensor.query( - module="SubtensorModule", - storage_function="ColdkeyArbitrationBlock", - params=[wallet.coldkeypub.ss58_address], - ) - arbitration_remaining = ( - arbitration_block.value - await subtensor.substrate.get_block_number(None) - ) - - hours, minutes, seconds = convert_blocks_to_time(arbitration_remaining) - console.print( - "[yellow]There has been 1 swap request made for this coldkey already." - " By adding another swap request, the key will enter arbitration." - f" Your key swap is scheduled for {hours} hours, {minutes} minutes, {seconds} seconds" - " from now.[/yellow]" - ) - elif arbitration_check > 1: - console.print( - f"[red]This coldkey is currently in arbitration with a total swaps of {arbitration_check}.[/red]" - ) - - async def sign( wallet: Wallet, message: str, use_hotkey: bool, json_output: bool = False ): @@ -2211,9 +2170,7 @@ async def announce_coldkey_swap( print_error(f"Failed to announce coldkey swap: {err_msg}") return False - console.print( - ":white_heavy_check_mark: [green]Successfully announced coldkey swap[/green]" - ) + print_success("[dark_sea_green3]Successfully announced coldkey swap") await print_extrinsic_id(ext_receipt) # Post-success information @@ -2370,9 +2327,7 @@ async def execute_coldkey_swap( print_error(f"Failed to execute coldkey swap: {err_msg}") return False - console.print( - ":white_heavy_check_mark: [green]Successfully executed coldkey swap![/green]" - ) + print_success("[dark_sea_green3]Successfully executed coldkey swap!") await print_extrinsic_id(ext_receipt) # Success details table From 5fb8a746ca038d223eaf9743ae9dd39924873b4f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 14:09:48 -0800 Subject: [PATCH 029/113] adds confirmation for mev_protection --- bittensor_cli/src/commands/wallets.py | 40 +++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index acbd67ab4..536b42438 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -23,6 +23,10 @@ DelegateInfo, NeuronInfoLite, ) +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + extract_mev_shield_id, + wait_for_extrinsic_by_hash, +) from bittensor_cli.src.bittensor.extrinsics.registration import ( run_faucet_extrinsic, swap_hotkey_extrinsic, @@ -2150,7 +2154,7 @@ async def announce_coldkey_swap( if not unlock_key(wallet).success: return False - with console.status(":satellite: Announcing coldkey swap on-chain..."): + with console.status(":satellite: Announcing coldkey swap on-chain...") as status: call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="announce_coldkey_swap", @@ -2170,6 +2174,22 @@ async def announce_coldkey_swap( print_error(f"Failed to announce coldkey swap: {err_msg}") return False + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(ext_receipt) + mev_success, mev_error, ext_receipt = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=ext_receipt.block_hash, + status=status, + ) + if not mev_success: + print_error( + f"Failed to announce coldkey swap: {mev_error}", status=status + ) + return False + print_success("[dark_sea_green3]Successfully announced coldkey swap") await print_extrinsic_id(ext_receipt) @@ -2307,7 +2327,7 @@ async def execute_coldkey_swap( if not unlock_key(wallet).success: return False - with console.status(":satellite: Executing coldkey swap on-chain..."): + with console.status(":satellite: Executing coldkey swap on-chain...") as status: call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="swap_coldkey_announced", @@ -2327,6 +2347,22 @@ async def execute_coldkey_swap( print_error(f"Failed to execute coldkey swap: {err_msg}") return False + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(ext_receipt) + mev_success, mev_error, ext_receipt = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=ext_receipt.block_hash, + status=status, + ) + if not mev_success: + print_error( + f"Failed to execute coldkey swap: {mev_error}", status=status + ) + return False + print_success("[dark_sea_green3]Successfully executed coldkey swap!") await print_extrinsic_id(ext_receipt) From bac7f09eca87d10174f6693167c2955ad739085f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 14:23:07 -0800 Subject: [PATCH 030/113] remove proxy unit test for swap-ck - not applicable anymore --- tests/unit_tests/test_cli.py | 40 ------------------------------------ 1 file changed, 40 deletions(-) diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 60cc10708..a793ac0c0 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -551,46 +551,6 @@ def test_wallet_set_id_calls_proxy_validation(): mock_proxy_validation.assert_called_once_with(valid_proxy, False) -def test_wallet_swap_coldkey_calls_proxy_validation(): - """Test that wallet_swap_coldkey calls is_valid_proxy_name_or_ss58""" - cli_manager = CLIManager() - valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - new_coldkey = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - - with ( - patch.object(cli_manager, "verbosity_handler"), - patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, - patch.object(cli_manager, "initialize_chain"), - patch.object(cli_manager, "_run_command"), - patch("bittensor_cli.cli.is_valid_ss58_address", return_value=True), - patch.object( - cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy - ) as mock_proxy_validation, - ): - mock_wallet = Mock() - mock_wallet.coldkeypub = Mock() - mock_wallet.coldkeypub.ss58_address = ( - "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - ) - mock_wallet_ask.return_value = mock_wallet - - cli_manager.wallet_swap_coldkey( - wallet_name="test_wallet", - wallet_path="/tmp/test", - wallet_hotkey="test_hotkey", - new_wallet_or_ss58=new_coldkey, - network=None, - proxy=valid_proxy, - announce_only=False, - quiet=True, - verbose=False, - force_swap=False, - ) - - # Assert that proxy validation was called - mock_proxy_validation.assert_called_once_with(valid_proxy, False) - - def test_stake_move_calls_proxy_validation(): """Test that stake_move calls is_valid_proxy_name_or_ss58""" cli_manager = CLIManager() From dfdc6620824f463a05dd8cc92482b6e31fb79046 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 14:39:50 -0800 Subject: [PATCH 031/113] update tests --- tests/e2e_tests/test_wallet_interactions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 7ed705b65..02797b978 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -447,17 +447,17 @@ def test_wallet_identities(local_chain, wallet_setup): assert "Your extrinsic has been included as" in set_id_output[1] - assert alice_identity["name"] in set_id_output[7] - assert alice_identity["url"] in set_id_output[8] - assert alice_identity["github_repo"] in set_id_output[9] - assert alice_identity["image"] in set_id_output[10] - assert alice_identity["discord"] in set_id_output[11] - assert alice_identity["description"] in set_id_output[12] - assert alice_identity["additional"] in set_id_output[13] + assert alice_identity["name"] in set_id_output[8] + assert alice_identity["url"] in set_id_output[9] + assert alice_identity["github_repo"] in set_id_output[10] + assert alice_identity["image"] in set_id_output[11] + assert alice_identity["discord"] in set_id_output[12] + assert alice_identity["description"] in set_id_output[13] + assert alice_identity["additional"] in set_id_output[14] # TODO: Currently coldkey + hotkey are the same for test wallets. # Maybe we can add a new key to help in distinguishing - assert wallet_alice.coldkeypub.ss58_address in set_id_output[6] + assert wallet_alice.coldkeypub.ss58_address in set_id_output[7] # Execute btcli get-identity using hotkey get_identity = exec_command_alice( From d43f8d85686252d1a4dd9aef9bb94e7e1f258333 Mon Sep 17 00:00:00 2001 From: leonace924 Date: Tue, 23 Dec 2025 19:11:07 -0500 Subject: [PATCH 032/113] fix: revert the test of table --- tests/unit_tests/test_utils.py | 175 --------------------------------- 1 file changed, 175 deletions(-) diff --git a/tests/unit_tests/test_utils.py b/tests/unit_tests/test_utils.py index 5d60ac651..6a3774d0f 100644 --- a/tests/unit_tests/test_utils.py +++ b/tests/unit_tests/test_utils.py @@ -5,10 +5,7 @@ from bittensor_cli.src.bittensor.utils import ( check_img_mimetype, confirm_action, - create_table, ) -from rich.table import Column, Table -from bittensor_cli.src import COLOR_PALETTE @pytest.mark.parametrize( @@ -134,175 +131,3 @@ def test_confirm_action_default_values(self): result = confirm_action("Do you want to proceed?") assert result is True mock_ask.assert_called_once() - - -class TestCreateTable: - """Tests for the create_table utility function.""" - - def test_simple_table_creation(self): - """Test creating a simple table with default styling.""" - table = create_table(title="My Subnets") - - # Verify it returns a Table instance - assert isinstance(table, Table) - assert table.title == "My Subnets" - - # Verify default styling is applied - assert table.show_footer is True - assert table.show_edge is False - assert table.header_style == "bold white" - assert table.border_style == "bright_black" - assert table.title_justify == "center" - assert table.show_lines is False - assert table.pad_edge is True - - def test_table_with_columns_added_later(self): - """Test adding columns after table creation.""" - table = create_table(title="Test Table") - - # Add columns dynamically - table.add_column("Column1", justify="center") - table.add_column("Column2", justify="left") - - assert len(table.columns) == 2 - assert table.columns[0].header == "Column1" - assert table.columns[1].header == "Column2" - - # Add rows - table.add_row("Value1", "Value2") - assert len(table.rows) == 1 - - def test_table_with_column_objects(self): - """Test creating table with Column objects upfront (identity table pattern).""" - table = create_table( - Column( - "Item", - justify="right", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], - no_wrap=True, - ), - Column("Value", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]), - title="Identity", - ) - - # Verify columns were added - assert len(table.columns) == 2 - assert table.columns[0].header == "Item" - assert table.columns[1].header == "Value" - assert table.columns[0].justify == "right" - assert table.columns[0].no_wrap is True - - # Verify default styling still applied - assert table.show_footer is True - assert table.show_edge is False - - def test_custom_overrides(self): - """Test overriding default parameters.""" - table = create_table( - title="Custom Table", - show_footer=False, - border_style="blue", - show_lines=True, - ) - - # Verify overrides applied - assert table.show_footer is False - assert table.border_style == "blue" - assert table.show_lines is True - - # Verify non-overridden defaults preserved - assert table.show_edge is False - assert table.header_style == "bold white" - - def test_subnets_list_pattern(self): - """Test actual pattern from subnets_list() function.""" - table = create_table( - title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets\n" - f"Network: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]finney\n", - ) - - # Add columns as in actual code - table.add_column("[bold white]Netuid", style="grey89", justify="center") - table.add_column("[bold white]Name", style="cyan", justify="left") - table.add_column("[bold white]Price", style="dark_sea_green2", justify="left") - - assert len(table.columns) == 3 - - # Add sample row - table.add_row("1", "Alpha", "0.0025") - assert len(table.rows) == 1 - - def test_registration_pattern(self): - """Test registration confirmation table pattern.""" - table = create_table( - title=( - f"[{COLOR_PALETTE.G.HEADER}]" - f"Register to [{COLOR_PALETTE.G.SUBHEAD}]netuid: 1[/{COLOR_PALETTE.G.SUBHEAD}]\n" - f"Network: [{COLOR_PALETTE.G.SUBHEAD}]finney[/{COLOR_PALETTE.G.SUBHEAD}]\n" - ), - ) - - table.add_column( - "Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center" - ) - table.add_column( - "Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"], no_wrap=True - ) - table.add_column( - "Cost (τ)", style=COLOR_PALETTE["POOLS"]["TAO"], justify="center" - ) - - assert len(table.columns) == 3 - - # Add sample row - table.add_row("1", "α", "τ 0.5000") - assert len(table.rows) == 1 - - def test_advanced_rich_features(self): - """Test advanced Rich features with custom box and expand.""" - from rich import box - - table = create_table( - Column("Command", overflow="fold", ratio=2), - Column("Description", overflow="fold", ratio=3), - title="Commands", - box=box.ROUNDED, - expand=True, - padding=(0, 1), - ) - - assert table.box == box.ROUNDED - assert table.expand is True - assert len(table.columns) == 2 - assert table.columns[0].ratio == 2 - assert table.columns[1].ratio == 3 - - def test_empty_table_minimal_config(self): - """Test creating empty table with minimal configuration.""" - table = create_table() - - assert isinstance(table, Table) - assert table.title == "" - assert table.show_footer is True - assert len(table.columns) == 0 - - def test_multiple_column_objects_with_styling(self): - """Test multiple Column objects with various styling options.""" - table = create_table( - Column("Col1", style="cyan", justify="left"), - Column("Col2", style="green", justify="center", no_wrap=True), - Column("Col3", style="yellow", justify="right", overflow="fold"), - title="Multi-Column Test", - ) - - assert len(table.columns) == 3 - assert table.columns[0].style == "cyan" - assert table.columns[1].justify == "center" - assert table.columns[2].overflow == "fold" - - def test_rich_markup_in_title(self): - """Test that rich markup in title is preserved.""" - table = create_table(title="[bold cyan]Test[/bold cyan] [dim]subtitle[/dim]") - - assert "[bold cyan]Test[/bold cyan]" in table.title - assert "[dim]subtitle[/dim]" in table.title From 28aed1f99a41f3e9a985f6f6dbb58e67f9f23085 Mon Sep 17 00:00:00 2001 From: GlobalStar117 Date: Thu, 15 Jan 2026 18:51:14 +1100 Subject: [PATCH 033/113] fix: JSON output empty for subnets list command Fixes #635 ## Problem Running `btcli subnets list --json-out` produced no output. ## Root Cause In `dict_table()` function, `tao_flow_ema` was already converted to a float via `.tao`, but line 604 tried to call `.tao` again, causing an `AttributeError: 'float' object has no attribute 'tao'`. This error was silently swallowed, resulting in empty output. ## Changes 1. `subnets.py`: Fixed double `.tao` call on `tao_flow_ema` 2. `utils.py`: Changed `json_console` to use `force_terminal=True` to ensure output in all environments ## Testing - Verified `btcli subnets list --json-out` now produces valid JSON output - Tested with Python 3.12, BTCLI 9.17.0, finney network --- bittensor_cli/src/bittensor/utils.py | 2 +- bittensor_cli/src/commands/subnets/subnets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index a5c1896d3..3043cfc26 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -52,7 +52,7 @@ _force_terminal = False if (_is_pytest or not sys.stdout.isatty()) else None console = Console(no_color=_no_color, force_terminal=_force_terminal) json_console = Console( - markup=False, highlight=False, force_terminal=False, no_color=True + markup=False, highlight=False, force_terminal=True, no_color=True ) err_console = Console(stderr=True, no_color=_no_color, force_terminal=_force_terminal) verbose_console = Console( diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index ad0404b4f..45af8d72d 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -601,7 +601,7 @@ def dict_table(subnets_, block_number_, mechanisms, ema_tao_inflow) -> dict: tao_flow_ema = None if netuid in ema_tao_inflow: tao_flow_ema = ema_tao_inflow[netuid].tao - total_tao_flow_ema += tao_flow_ema.tao + total_tao_flow_ema += tao_flow_ema subnet_rows[netuid] = { "netuid": netuid, "subnet_name": subnet_name, From 58b179191c135555cd89b91e5d9bda8f1f8cbfc7 Mon Sep 17 00:00:00 2001 From: Ibraheem <165814940+ibraheem-abe@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:47:15 -0700 Subject: [PATCH 034/113] force_terminal as False --- bittensor_cli/src/bittensor/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 3043cfc26..a5c1896d3 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -52,7 +52,7 @@ _force_terminal = False if (_is_pytest or not sys.stdout.isatty()) else None console = Console(no_color=_no_color, force_terminal=_force_terminal) json_console = Console( - markup=False, highlight=False, force_terminal=True, no_color=True + markup=False, highlight=False, force_terminal=False, no_color=True ) err_console = Console(stderr=True, no_color=_no_color, force_terminal=_force_terminal) verbose_console = Console( From 78ca077dd0f8ed62cbed939e4c7be30c1c86c29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9?= Date: Thu, 22 Jan 2026 08:49:12 +0100 Subject: [PATCH 035/113] fix: disable wallet history command due to external API deprecation This command is disabled as it used external APIs which are no longer feasible; meanwhile a chain native data fetching solution is being investigated. Fixes #235 Related to #302 --- bittensor_cli/cli.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 72faeb5e9..f431c2617 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3841,8 +3841,10 @@ def wallet_history( # if self.config.get("network") != "finney": # console.print(no_use_config_str) - # For Rao games - print_error("This command is disabled on the 'rao' network.") + print_error( + "This command is currently disabled as it used external APIs which are no longer " + "feasible; meanwhile a chain native data fetching solution is being investigated." + ) raise typer.Exit() self.verbosity_handler(quiet, verbose, False, False) From 79a8d651b000f48cff839dadb64c7cfcb1cfea15 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 23 Jan 2026 18:06:40 -0800 Subject: [PATCH 036/113] add json output --- bittensor_cli/cli.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index bb9e10e13..1e034dc92 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3528,6 +3528,7 @@ def wallet_check_ck_swap( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Check the status of pending coldkey swap announcements. @@ -3557,11 +3558,13 @@ def wallet_check_ck_swap( [green]$[/green] btcli wallet swap-check --ss58 5DkQ4... """ - self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) + self.verbosity_handler(quiet, verbose, json_output=json_output, prompt=False) self.initialize_chain(network) if show_all: - return self._run_command(wallets.check_swap_status(self.subtensor, None)) + return self._run_command( + wallets.check_swap_status(self.subtensor, None, json_output=json_output) + ) if not wallet_ss58_address: wallet_ss58_address = Prompt.ask( @@ -3570,7 +3573,9 @@ def wallet_check_ck_swap( ) if not wallet_ss58_address: return self._run_command( - wallets.check_swap_status(self.subtensor, None) + wallets.check_swap_status( + self.subtensor, None, json_output=json_output + ) ) if is_valid_ss58_address(wallet_ss58_address): @@ -3587,7 +3592,9 @@ def wallet_check_ck_swap( logger.debug(f"args:\nss58_address {ss58_address}\nnetwork {network}\n") return self._run_command( - wallets.check_swap_status(self.subtensor, ss58_address) + wallets.check_swap_status( + self.subtensor, ss58_address, json_output=json_output + ) ) def wallet_create_wallet( From 141c314f54e612fe31c62450fc8db47e09de86bd Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 23 Jan 2026 18:06:57 -0800 Subject: [PATCH 037/113] add new dispute cmd --- bittensor_cli/cli.py | 70 +++++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 1e034dc92..ec2c73173 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4114,7 +4114,7 @@ def wallet_swap_coldkey( self, action: str = typer.Argument( None, - help="Action to perform: 'announce' to announce intent, 'execute' to complete swap after delay.", + help="Action to perform: 'announce' to announce intent, 'execute' to complete swap after delay, 'dispute' to freeze the swap.", ), wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, @@ -4142,6 +4142,9 @@ def wallet_swap_coldkey( 2. [bold]Execute[/bold]: After the delay (typically 5 days), complete the swap. + If you suspect compromise, you can [bold]Dispute[/bold] an active announcement to freeze + all activity for the coldkey until the triumvirate can intervene. + EXAMPLES Step 1 - Announce your intent to swap: @@ -4152,6 +4155,10 @@ def wallet_swap_coldkey( [green]$[/green] btcli wallet swap-coldkey execute --new-coldkey 5Dk...X3q + Dispute an active swap (freezes the swap process): + + [green]$[/green] btcli wallet swap-coldkey dispute + Check status of pending swaps: [green]$[/green] btcli wallet swap-check @@ -4162,17 +4169,20 @@ def wallet_swap_coldkey( console.print( "\n[bold][blue]Coldkey Swap Actions:[/blue][/bold]\n" " [dark_sea_green3]announce[/dark_sea_green3] - Start the swap process (pays fee, starts delay timer)\n" - " [dark_sea_green3]execute[/dark_sea_green3] - Complete the swap (after delay period)\n\n" + " [dark_sea_green3]execute[/dark_sea_green3] - Complete the swap (after delay period)\n" + " [dark_sea_green3]dispute[/dark_sea_green3] - Freeze the swap process if you suspect compromise\n\n" " [dim]You can check the current status of your swap with 'btcli wallet swap-check'.[/dim]\n" ) action = Prompt.ask( "Select action", - choices=["announce", "execute"], + choices=["announce", "execute", "dispute"], default="announce", ) - if action.lower() not in ("announce", "execute"): - print_error(f"Invalid action: {action}. Must be 'announce' or 'execute'.") + if action.lower() not in ("announce", "execute", "dispute"): + print_error( + f"Invalid action: {action}. Must be 'announce', 'execute', or 'dispute'." + ) raise typer.Exit(1) if not wallet_name: @@ -4191,26 +4201,28 @@ def wallet_swap_coldkey( f"\nWallet selected: [dark_sea_green3]{wallet}[/dark_sea_green3]\n" ) - if not new_wallet_or_ss58: - new_wallet_or_ss58 = Prompt.ask( - "Enter the [blue]new wallet name[/blue] or [blue]SS58 address[/blue] of the new coldkey", - ) + new_wallet_coldkey_ss58 = None + if action != "dispute": + if not new_wallet_or_ss58: + new_wallet_or_ss58 = Prompt.ask( + "Enter the [blue]new wallet name[/blue] or [blue]SS58 address[/blue] of the new coldkey", + ) - if is_valid_ss58_address(new_wallet_or_ss58): - new_wallet_coldkey_ss58 = new_wallet_or_ss58 - else: - new_wallet_name = new_wallet_or_ss58 - new_wallet = self.wallet_ask( - new_wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME], - validate=WV.WALLET, - ) - console.print( - f"\nNew coldkey wallet: [dark_sea_green3]{new_wallet}[/dark_sea_green3]\n" - ) - new_wallet_coldkey_ss58 = new_wallet.coldkeypub.ss58_address + if is_valid_ss58_address(new_wallet_or_ss58): + new_wallet_coldkey_ss58 = new_wallet_or_ss58 + else: + new_wallet_name = new_wallet_or_ss58 + new_wallet = self.wallet_ask( + new_wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + validate=WV.WALLET, + ) + console.print( + f"\nNew coldkey wallet: [dark_sea_green3]{new_wallet}[/dark_sea_green3]\n" + ) + new_wallet_coldkey_ss58 = new_wallet.coldkeypub.ss58_address logger.debug( f"args:\n" @@ -4230,6 +4242,16 @@ def wallet_swap_coldkey( mev_protection=mev_protection, ) ) + elif action == "dispute": + return self._run_command( + wallets.dispute_coldkey_swap( + wallet=wallet, + subtensor=self.initialize_chain(network), + decline=decline, + quiet=quiet, + mev_protection=mev_protection, + ) + ) else: return self._run_command( wallets.execute_coldkey_swap( From 3cb6ae97eb0c4096b305cdf9ad55f25d257d0f31 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 23 Jan 2026 18:08:16 -0800 Subject: [PATCH 038/113] add dispute_coldkey_swap --- bittensor_cli/src/commands/wallets.py | 100 ++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 536b42438..c4eda574a 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2233,6 +2233,106 @@ async def announce_coldkey_swap( return True +async def dispute_coldkey_swap( + wallet: Wallet, + subtensor: SubtensorInterface, + decline: bool = False, + quiet: bool = False, + mev_protection: bool = False, +) -> bool: + """Dispute a pending coldkey swap for the calling coldkey. + + Disputing freezes the current swap process until the triumvirate can intervene. + + Args: + wallet: Wallet initiating the dispute (must be the announcing coldkey). + subtensor: Connection to the Bittensor network. + decline: If True, default to declining at confirmation prompt. + quiet: If True, skip confirmation prompts and proceed. + mev_protection: If True, encrypt the extrinsic with MEV protection. + + Returns: + bool: True if the dispute extrinsic was included successfully, else False. + """ + block_hash = await subtensor.substrate.get_chain_head() + announcement, dispute = await asyncio.gather( + subtensor.get_coldkey_swap_announcement( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ), + subtensor.get_coldkey_swap_dispute( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ), + ) + + if not announcement: + print_error( + f"No coldkey swap announcement found for {wallet.coldkeypub.ss58_address}.\n" + "You can only dispute an active announcement." + ) + return False + + if dispute is not None: + console.print( + f"[yellow]Swap already disputed at block {dispute}.[/yellow] " + "Account remains frozen until a root reset." + ) + return False + + current_block = await subtensor.substrate.get_block_number(block_hash=block_hash) + info = create_key_value_table("Dispute Coldkey Swap\n") + info.add_row( + "Coldkey", f"[{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" + ) + info.add_row("Execution Block", str(announcement.execution_block)) + info.add_row( + "Status", + "[yellow]Pending[/yellow]" + if current_block < announcement.execution_block + else "[green]Ready[/green]", + ) + info.add_row( + "Warning", + "[red]Disputing freezes all transactions from this coldkey until the triumvirate can intervene.[/red]", + ) + console.print(info) + + if not confirm_action( + "Proceed with dispute? You will be blocked from all transactions until the triumvirate can intervene.", + decline=decline, + quiet=quiet, + ): + return False + + if not unlock_key(wallet).success: + return False + + with console.status(":satellite: Disputing coldkey swap on-chain..."): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="dispute_coldkey_swap", + call_params={}, + ) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + mev_protection=mev_protection, + ) + + if not success: + print_error(f"Failed to dispute coldkey swap: {err_msg}") + return False + + print_success("[dark_sea_green3]Coldkey swap disputed.") + await print_extrinsic_id(ext_receipt) + + console.print( + "\n[dim]Your coldkey is now frozen. The triumvirate will need to intervene to clear the dispute.[/dim]" + ) + return True + + async def execute_coldkey_swap( wallet: Wallet, subtensor: SubtensorInterface, From 6e14f7229031c72676f81f6e2285c586f55aba04 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 23 Jan 2026 18:08:33 -0800 Subject: [PATCH 039/113] add json output and dispute data --- bittensor_cli/src/commands/wallets.py | 51 +++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index c4eda574a..dbf36289d 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2484,17 +2484,20 @@ async def execute_coldkey_swap( async def check_swap_status( subtensor: SubtensorInterface, origin_ss58: Optional[str] = None, + json_output: bool = False, ) -> None: """Retrieves and displays the status of coldkey swap announcements. Args: subtensor: Connection to the network. origin_ss58: The SS58 address of the coldkey to check. If None, shows all pending announcements. + json_output: If True, print JSON payload instead of rich table. """ block_hash = await subtensor.substrate.get_chain_head() if origin_ss58: - announcement, current_block = await asyncio.gather( + announcement, dispute, current_block = await asyncio.gather( subtensor.get_coldkey_swap_announcement(origin_ss58, block_hash=block_hash), + subtensor.get_coldkey_swap_dispute(origin_ss58, block_hash=block_hash), subtensor.substrate.get_block_number(block_hash=block_hash), ) if not announcement: @@ -2504,18 +2507,38 @@ async def check_swap_status( ) return announcements = [announcement] + disputes = [(origin_ss58, dispute)] if dispute is not None else [] else: - announcements, current_block = await asyncio.gather( + announcements, disputes, current_block = await asyncio.gather( subtensor.get_coldkey_swap_announcements(block_hash=block_hash), + subtensor.get_coldkey_swap_disputes(block_hash=block_hash), subtensor.substrate.get_block_number(block_hash=block_hash), ) if not announcements: console.print( "[yellow]No pending coldkey swap announcements found.[/yellow]" ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "current_block": current_block, + "announcements": [], + } + ) + ) return + dispute_map = {coldkey: block for coldkey, block in disputes if coldkey and block is not None} + + payload = { + "success": True, + "current_block": current_block, + "announcements": [], + } + table = Table( Column( "Coldkey", @@ -2539,13 +2562,20 @@ async def check_swap_status( ) for announcement in announcements: + dispute_block = dispute_map.get(announcement.coldkey) remaining_blocks = announcement.execution_block - current_block - if remaining_blocks <= 0: + if dispute_block is not None: + status = "[red]Disputed[/red]" + time_str = f"Disputed @ {dispute_block}" + status_label = "disputed" + elif remaining_blocks <= 0: status = "Ready" time_str = "[green]Ready[/green]" + status_label = "ready" else: status = "Pending" time_str = blocks_to_duration(remaining_blocks) + status_label = "pending" hash_display = f"{announcement.new_coldkey_hash[:12]}...{announcement.new_coldkey_hash[-6:]}" table.add_row( @@ -2556,6 +2586,21 @@ async def check_swap_status( status, ) + payload["announcements"].append( + { + "coldkey": announcement.coldkey, + "new_coldkey_hash": announcement.new_coldkey_hash, + "execution_block": announcement.execution_block, + "status": status_label, + "time_remaining_blocks": max(0, remaining_blocks), + "disputed_block": dispute_block, + } + ) + + if json_output: + json_console.print(json.dumps(payload)) + return + console.print(table) console.print( "\n[dim]To execute a ready swap:[/dim] " From 38cde889f9120f9af34276faaa25beffdd3ab6c4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 23 Jan 2026 18:08:59 -0800 Subject: [PATCH 040/113] get_coldkey_swap_disputes --- .../src/bittensor/subtensor_interface.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 5daec09b9..ef69ed4a3 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1864,6 +1864,34 @@ async def get_coldkey_swap_announcement( return ColdkeySwapAnnouncementInfo._fix_decoded(coldkey_ss58, result) + 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)`. + """ + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="ColdkeySwapDisputes", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + disputes: list[tuple[str, int]] = [] + async for ss58, data in result: + coldkey = decode_account_id(ss58) + disputes.append((coldkey, int(data))) + return disputes + + async def get_crowdloans( self, block_hash: Optional[str] = None ) -> list[CrowdloanData]: From c473ded67db6b8c7f4b4eaaba02dbf13ad7d92c9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 23 Jan 2026 18:09:10 -0800 Subject: [PATCH 041/113] get single get_coldkey_swap_dispute --- .../src/bittensor/subtensor_interface.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index ef69ed4a3..3a615e09d 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1891,6 +1891,34 @@ async def get_coldkey_swap_disputes( disputes.append((coldkey, int(data))) return disputes + 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. + """ + result = await self.query( + module="SubtensorModule", + storage_function="ColdkeySwapDisputes", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + if result is None: + return None + + return int(result) async def get_crowdloans( self, block_hash: Optional[str] = None From 084e9af54476648842dcbd934fd1e7610300d9c9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 23 Jan 2026 18:13:06 -0800 Subject: [PATCH 042/113] improve wording --- bittensor_cli/src/commands/wallets.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index dbf36289d..41cf7d296 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2292,12 +2292,12 @@ async def dispute_coldkey_swap( ) info.add_row( "Warning", - "[red]Disputing freezes all transactions from this coldkey until the triumvirate can intervene.[/red]", + "[red]Disputing freezes the current swap process until the triumvirate can intervene.[/red]", ) console.print(info) if not confirm_action( - "Proceed with dispute? You will be blocked from all transactions until the triumvirate can intervene.", + "Proceed with dispute? Your swap process will be frozen until the triumvirate can intervene.", decline=decline, quiet=quiet, ): @@ -2531,7 +2531,9 @@ async def check_swap_status( ) return - dispute_map = {coldkey: block for coldkey, block in disputes if coldkey and block is not None} + dispute_map = { + coldkey: block for coldkey, block in disputes if coldkey and block is not None + } payload = { "success": True, From 9de25916c5f3d050de3920f2d7f6227fb59e788f Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 26 Jan 2026 21:31:26 +0200 Subject: [PATCH 043/113] Make disk cache default on --- README.md | 2 +- bittensor_cli/cli.py | 4 ++-- bittensor_cli/src/bittensor/subtensor_interface.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e3d8d38ad..a6ba1ea79 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ The default location of the config file is: `~/.bittensor/config.yml`. An exampl network: local use_cache: true dashboard_path: null -disk_cache: false +disk_cache: true rate_tolerance: null safe_staking: true wallet_hotkey: default diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f431c2617..a83447568 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -797,7 +797,7 @@ def __init__(self): "wallet_hotkey": None, "network": None, "use_cache": True, - "disk_cache": False, + "disk_cache": True, "rate_tolerance": None, "safe_staking": True, "allow_partial_stake": False, @@ -1431,7 +1431,7 @@ def initialize_chain( "Verify this is intended.", ) if not self.subtensor: - use_disk_cache = self.config.get("disk_cache", False) + use_disk_cache = self.config.get("disk_cache", True) if network: network_ = None for item in network: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 2d01f914e..376318282 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -85,7 +85,7 @@ class SubtensorInterface: Thin layer for interacting with Substrate Interface. Mostly a collection of frequently-used calls. """ - def __init__(self, network, use_disk_cache: bool = False): + def __init__(self, network, use_disk_cache: bool = True): if network in Constants.network_map: self.chain_endpoint = Constants.network_map[network] self.network = network @@ -117,7 +117,7 @@ def __init__(self, network, use_disk_cache: bool = False): self.network = defaults.subtensor.network substrate_class = ( DiskCachedAsyncSubstrateInterface - if (use_disk_cache or os.getenv("DISK_CACHE", "0") == "1") + if (use_disk_cache or os.getenv("DISK_CACHE", "1") == "1") else AsyncSubstrateInterface ) logger.debug(f"Using substrate class {substrate_class.__name__}") From 05116c6a8cebb85f9b12dfc95ad0bb958461fb49 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 26 Jan 2026 21:50:33 +0200 Subject: [PATCH 044/113] Bump ASI --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3ec9af78e..767c365c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ ] dependencies = [ "wheel", - "async-substrate-interface>=1.5.14", + "async-substrate-interface>=1.6.0", "aiohttp~=3.13", "backoff~=2.2.1", "bittensor-drand>=1.2.0", From 02101f173c2ae9ce96a6ed4b4336836af060acfc Mon Sep 17 00:00:00 2001 From: BD Himes Date: Mon, 26 Jan 2026 21:51:58 +0200 Subject: [PATCH 045/113] Remove debug logging --- bittensor_cli/src/bittensor/subtensor_interface.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 376318282..57b4a627e 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,5 +1,4 @@ import asyncio -import logging import os import time from typing import Optional, Any, Union, TypedDict, Iterable, Literal @@ -50,8 +49,6 @@ GENESIS_ADDRESS = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" -logger = logging.getLogger("btcli") - class ParamWithTypes(TypedDict): name: str # Name of the parameter. @@ -120,7 +117,6 @@ def __init__(self, network, use_disk_cache: bool = True): if (use_disk_cache or os.getenv("DISK_CACHE", "1") == "1") else AsyncSubstrateInterface ) - logger.debug(f"Using substrate class {substrate_class.__name__}") self.substrate = substrate_class( url=self.chain_endpoint, ss58_format=SS58_FORMAT, From c5e1b08bb13a77eeddd7f5bd9a582dd8d3e2b0a0 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 26 Jan 2026 18:58:18 -0800 Subject: [PATCH 046/113] update announce_coldkey_swap --- bittensor_cli/src/commands/wallets.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 41cf7d296..78a6c39a9 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2035,6 +2035,7 @@ async def announce_coldkey_swap( new_coldkey_ss58: str, decline: bool = False, quiet: bool = False, + prompt: bool = True, mev_protection: bool = False, ) -> bool: """Announces intent to swap a coldkey to a new address. @@ -2106,7 +2107,7 @@ async def announce_coldkey_swap( "\n[yellow]You already have an announcement for this coldkey.[/yellow] " "You can execute the existing swap without reannouncing." ) - if not confirm_action( + if prompt and not confirm_action( "Do you still want to reannounce the same hash (the period to wait before executing the swap will be reset)?", decline=decline, quiet=quiet, @@ -2117,7 +2118,7 @@ async def announce_coldkey_swap( f"\n[dim]Reannouncing with a different coldkey will reset the waiting period " f"to {blocks_to_duration(announce_delay)} from now.[/dim]" ) - if not confirm_action( + if prompt and not confirm_action( "Proceed with reannouncement and reset the waiting period?", decline=decline, quiet=quiet, @@ -2146,7 +2147,7 @@ async def announce_coldkey_swap( ) console.print(table) - if not confirm_action( + if prompt and not confirm_action( "Are you sure you want to continue?", decline=decline, quiet=quiet ): return False From cebe676a79879d04083c4a58ad5fa92923de3f81 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 26 Jan 2026 18:58:34 -0800 Subject: [PATCH 047/113] update dispute and execute ck swap --- bittensor_cli/src/commands/wallets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 78a6c39a9..73dc8288d 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2239,6 +2239,7 @@ async def dispute_coldkey_swap( subtensor: SubtensorInterface, decline: bool = False, quiet: bool = False, + prompt: bool = True, mev_protection: bool = False, ) -> bool: """Dispute a pending coldkey swap for the calling coldkey. @@ -2297,7 +2298,7 @@ async def dispute_coldkey_swap( ) console.print(info) - if not confirm_action( + if prompt and not confirm_action( "Proceed with dispute? Your swap process will be frozen until the triumvirate can intervene.", decline=decline, quiet=quiet, @@ -2340,6 +2341,7 @@ async def execute_coldkey_swap( new_coldkey_ss58: str, decline: bool = False, quiet: bool = False, + prompt: bool = True, mev_protection: bool = True, ) -> bool: """Executes a previously announced coldkey swap. @@ -2420,7 +2422,7 @@ async def execute_coldkey_swap( "\n[bold red]WARNING:[/bold red] This action is irreversible. All assets will be transferred.\n" ) - if not confirm_action( + if prompt and not confirm_action( "Are you sure you want to continue?", decline=decline, quiet=quiet ): return False From 1cad597b9bcbeedb7faeeda1214d9e644ab50adc Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 26 Jan 2026 18:58:46 -0800 Subject: [PATCH 048/113] add prompt args --- bittensor_cli/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ec2c73173..22cda21bc 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4132,6 +4132,7 @@ def wallet_swap_coldkey( quiet: bool = Options.quiet, verbose: bool = Options.verbose, decline: bool = Options.decline, + prompt: bool = Options.prompt, ): """ Swap your coldkey to a new address using a two-step announcement process. @@ -4239,6 +4240,7 @@ def wallet_swap_coldkey( new_coldkey_ss58=new_wallet_coldkey_ss58, decline=decline, quiet=quiet, + prompt=prompt, mev_protection=mev_protection, ) ) @@ -4249,6 +4251,7 @@ def wallet_swap_coldkey( subtensor=self.initialize_chain(network), decline=decline, quiet=quiet, + prompt=prompt, mev_protection=mev_protection, ) ) @@ -4260,6 +4263,7 @@ def wallet_swap_coldkey( new_coldkey_ss58=new_wallet_coldkey_ss58, decline=decline, quiet=quiet, + prompt=prompt, mev_protection=mev_protection, ) ) From 0a95ba3efef0cb8c26c0af0be6ef6578ee6232ff Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 26 Jan 2026 18:59:32 -0800 Subject: [PATCH 049/113] add test_coldkey_swap_happy_path --- tests/e2e_tests/test_coldkey_swap.py | 225 +++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 tests/e2e_tests/test_coldkey_swap.py diff --git a/tests/e2e_tests/test_coldkey_swap.py b/tests/e2e_tests/test_coldkey_swap.py new file mode 100644 index 000000000..dcfc32010 --- /dev/null +++ b/tests/e2e_tests/test_coldkey_swap.py @@ -0,0 +1,225 @@ +import asyncio +import json + +from .utils import ( + find_stake_entries, +) + + +def _wait_until_block(substrate, target_block: int): + async def _wait(): + while True: + head = await substrate.get_chain_head() + current = await substrate.get_block_number(block_hash=head) + if current >= target_block: + return current + await asyncio.sleep(1) + + return asyncio.run(_wait()) + + +def test_coldkey_swap_happy_path(local_chain, wallet_setup): + """ + Green path coldkey swap: + 0. Bob registers on root and adds stake. + 1. Bob announces coldkey swap. + 2. Status shows pending. + 3. Wait until execution block. + 4. Execute swap. + 5. Status clear and root stake moves to new coldkey. + """ + print("Testing coldkey swap happy path 🧪") + wallet_path_bob = "//Bob" + wallet_path_new = "//Charlie" + + _, wallet_bob, path_bob, exec_command_bob = wallet_setup(wallet_path_bob) + _, wallet_new, path_new, _ = wallet_setup(wallet_path_new) + netuid = 2 + + # Create a new subnet by Bob + create_sn = exec_command_bob( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--wallet-hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--subnet-name", + "Test Subnet CK Swap", + "--repo", + "https://github.com/opentensor/subnet-repo", + "--contact", + "bob@opentensor.dev", + "--url", + "https://subnet.example.com", + "--discord", + "bob#1234", + "--description", + "Subnet for coldkey swap e2e", + "--logo-url", + "https://subnet.example.com/logo.png", + "--additional-info", + "Created for e2e coldkey swap test", + "--no-prompt", + "--json-output", + "--no-mev-protection", + ], + ) + create_payload = json.loads(create_sn.stdout) + assert create_payload["success"] is True + + # Start emission schedule + start_sn = exec_command_bob( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + str(2), + "--wallet-name", + wallet_bob.name, + "--wallet-path", + path_bob, + "--network", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "Successfully started subnet" in start_sn.stdout, start_sn.stdout + + # Add stake to the new subnet + stake_add = exec_command_bob( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "5", + "--unsafe", + "--no-prompt", + "--no-mev-protection", + ], + ) + assert "✅ Finalized" in stake_add.stdout, stake_add.stdout + + # Announce swap + announce = exec_command_bob( + command="wallet", + sub_command="swap-coldkey", + extra_args=[ + "announce", + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--network", + "ws://127.0.0.1:9945", + "--new-coldkey", + wallet_new.coldkeypub.ss58_address, + "--no-prompt", + "--no-mev-protection", + ], + ) + assert "Successfully announced coldkey swap" in announce.stdout, announce.stdout + + # Fetch announcement and wait until executable + status_json = exec_command_bob( + command="wallet", + sub_command="swap-check", + extra_args=[ + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--network", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + status_payload = json.loads(status_json.stdout) + assert status_payload["announcements"], status_payload + when = status_payload["announcements"][0]["execution_block"] + _wait_until_block(local_chain, when) + + # Execute swap + execute = exec_command_bob( + command="wallet", + sub_command="swap-coldkey", + extra_args=[ + "execute", + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--network", + "ws://127.0.0.1:9945", + "--new-coldkey", + wallet_new.coldkeypub.ss58_address, + "--no-prompt", + "--no-mev-protection", + ], + ) + assert "Successfully executed coldkey swap" in execute.stdout, execute.stdout + + # Status should clear + status = exec_command_bob( + command="wallet", + sub_command="swap-check", + extra_args=[ + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--network", + "ws://127.0.0.1:9945", + ], + ) + assert "No pending swap announcement" in status.stdout, status.stdout + + # Stake should now be on the new coldkey + stake_new = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--coldkey-ss58", + wallet_new.coldkeypub.ss58_address, + "--network", + "ws://127.0.0.1:9945", + "--json-output", + "--no-prompt", + ], + ) + payload_new = json.loads(stake_new.stdout) + new_entries = find_stake_entries( + payload_new, netuid=netuid, hotkey_ss58=wallet_bob.hotkey.ss58_address + ) + assert len(new_entries) > 0, "Stake not found on new coldkey" + assert float(new_entries[0].get("value", 0)) > 0 + + # Old coldkey should have no stake + stake_old = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--coldkey-ss58", + wallet_bob.coldkeypub.ss58_address, + "--network", + "ws://127.0.0.1:9945", + "--json-output", + "--no-prompt", + ], + ) + assert not stake_old.stdout, "Old coldkey still has stake" From 42cfc7aa16b8677dc77eaa793bfd76f608ddcd92 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 26 Jan 2026 23:14:17 -0800 Subject: [PATCH 050/113] fix get_coldkey_swap_disputes --- 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 3a615e09d..999fa4833 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1888,7 +1888,7 @@ async def get_coldkey_swap_disputes( disputes: list[tuple[str, int]] = [] async for ss58, data in result: coldkey = decode_account_id(ss58) - disputes.append((coldkey, int(data))) + disputes.append((coldkey, data.value)) return disputes async def get_coldkey_swap_dispute( From f2e881e27d0f09b735d6079e15e3c74634fd9dbb Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 26 Jan 2026 23:31:27 -0800 Subject: [PATCH 051/113] add handling for error 21 --- bittensor_cli/src/commands/wallets.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 73dc8288d..42211424f 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2447,7 +2447,14 @@ async def execute_coldkey_swap( ) if not success: - print_error(f"Failed to execute coldkey swap: {err_msg}") + if "Custom error: 21" in err_msg: + print_error( + "Failed to execute coldkey swap: The swap has been disputed.\n" + "The account is frozen until the triumvirate resolves the dispute.\n", + status=status, + ) + else: + print_error(f"Failed to execute coldkey swap: {err_msg}", status=status) return False if mev_protection: From ab28bfbd9b47cc99c365ad0fd14ffa7501c91169 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 26 Jan 2026 23:35:56 -0800 Subject: [PATCH 052/113] add test_coldkey_swap_dispute --- tests/e2e_tests/test_coldkey_swap.py | 208 ++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 4 deletions(-) diff --git a/tests/e2e_tests/test_coldkey_swap.py b/tests/e2e_tests/test_coldkey_swap.py index dcfc32010..6b5c954af 100644 --- a/tests/e2e_tests/test_coldkey_swap.py +++ b/tests/e2e_tests/test_coldkey_swap.py @@ -18,9 +18,9 @@ async def _wait(): return asyncio.run(_wait()) -def test_coldkey_swap_happy_path(local_chain, wallet_setup): +def test_coldkey_swap_with_stake(local_chain, wallet_setup): """ - Green path coldkey swap: + Coldkey swap with stake: 0. Bob registers on root and adds stake. 1. Bob announces coldkey swap. 2. Status shows pending. @@ -28,7 +28,7 @@ def test_coldkey_swap_happy_path(local_chain, wallet_setup): 4. Execute swap. 5. Status clear and root stake moves to new coldkey. """ - print("Testing coldkey swap happy path 🧪") + print("Testing coldkey swap with stake 🧪") wallet_path_bob = "//Bob" wallet_path_new = "//Charlie" @@ -135,7 +135,7 @@ def test_coldkey_swap_happy_path(local_chain, wallet_setup): ) assert "Successfully announced coldkey swap" in announce.stdout, announce.stdout - # Fetch announcement and wait until executable + # Fetch announcement and wait for execution block status_json = exec_command_bob( command="wallet", sub_command="swap-check", @@ -223,3 +223,203 @@ def test_coldkey_swap_happy_path(local_chain, wallet_setup): ], ) assert not stake_old.stdout, "Old coldkey still has stake" + + +def test_coldkey_swap_dispute(local_chain, wallet_setup): + """ + Dispute path: + 1. Bob announces swap. + 2. Status pending. + 3. Bob disputes immediately. + 4. Execute attempt fails and status shows Disputed. + """ + print("Testing coldkey swap dispute path 🧪") + wallet_path_bob = "//Bob" + wallet_path_new = "//Dave" + + _, wallet_bob, path_bob, exec_command_bob = wallet_setup(wallet_path_bob) + _, wallet_new, _, _ = wallet_setup(wallet_path_new) + + # Create subnet, start, and stake on it + create_sn = exec_command_bob( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--wallet-hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--subnet-name", + "Test Subnet CK Swap Dispute", + "--repo", + "https://github.com/opentensor/subnet-repo", + "--contact", + "bob@opentensor.dev", + "--url", + "https://subnet.example.com", + "--discord", + "bob#1234", + "--description", + "Subnet for coldkey swap dispute test", + "--logo-url", + "https://subnet.example.com/logo.png", + "--additional-info", + "Created for e2e coldkey swap dispute test", + "--no-prompt", + "--json-output", + ], + ) + create_payload = json.loads(create_sn.stdout) + assert create_payload.get("success") is True, create_payload + netuid = create_payload["netuid"] + + start_sn = exec_command_bob( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-name", + wallet_bob.name, + "--wallet-path", + path_bob, + "--network", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "Successfully started subnet" in start_sn.stdout, start_sn.stdout + + stake_add = exec_command_bob( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "2", + "--unsafe", + "--no-prompt", + "--no-mev-protection", + ], + ) + assert "✅ Finalized" in stake_add.stdout, stake_add.stdout + + announce = exec_command_bob( + command="wallet", + sub_command="swap-coldkey", + extra_args=[ + "announce", + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--network", + "ws://127.0.0.1:9945", + "--new-coldkey", + wallet_new.coldkeypub.ss58_address, + "--no-prompt", + "--no-mev-protection", + ], + ) + assert "Successfully announced coldkey swap" in announce.stdout, announce.stdout + + status = exec_command_bob( + command="wallet", + sub_command="swap-check", + extra_args=[ + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--network", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + status_payload = json.loads(status.stdout) + assert status_payload["announcements"], status_payload + assert status_payload["announcements"][0]["status"] == "pending" + + dispute = exec_command_bob( + command="wallet", + sub_command="swap-coldkey", + extra_args=[ + "dispute", + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--network", + "ws://127.0.0.1:9945", + "--no-prompt", + "--no-mev-protection", + ], + ) + assert "Coldkey swap disputed" in dispute.stdout, dispute.stdout + + status_after_dispute = exec_command_bob( + command="wallet", + sub_command="swap-check", + extra_args=[ + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--network", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + status_payload_after_dispute = json.loads(status_after_dispute.stdout) + if status_payload_after_dispute["announcements"]: + when = status_payload_after_dispute["announcements"][0]["execution_block"] + _wait_until_block(local_chain, when) + + execute = exec_command_bob( + command="wallet", + sub_command="swap-coldkey", + extra_args=[ + "execute", + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--network", + "ws://127.0.0.1:9945", + "--new-coldkey", + wallet_new.coldkeypub.ss58_address, + "--no-prompt", + "--no-mev-protection", + ], + ) + assert "The account is frozen" in execute.stderr, execute.stderr + + status_after = exec_command_bob( + command="wallet", + sub_command="swap-check", + extra_args=[ + "--wallet-path", + path_bob, + "--wallet-name", + wallet_bob.name, + "--network", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + status_after_payload = json.loads(status_after.stdout) + assert status_after_payload["announcements"], status_after_payload + assert status_after_payload["announcements"][0]["status"] == "disputed" From 906c4bb8083b2658813eb95ac8c67bce03ba5f51 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 28 Jan 2026 10:12:43 -0800 Subject: [PATCH 053/113] remove mev prot --- tests/e2e_tests/test_coldkey_swap.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e_tests/test_coldkey_swap.py b/tests/e2e_tests/test_coldkey_swap.py index 6b5c954af..dfa423788 100644 --- a/tests/e2e_tests/test_coldkey_swap.py +++ b/tests/e2e_tests/test_coldkey_swap.py @@ -402,7 +402,6 @@ def test_coldkey_swap_dispute(local_chain, wallet_setup): "--new-coldkey", wallet_new.coldkeypub.ss58_address, "--no-prompt", - "--no-mev-protection", ], ) assert "The account is frozen" in execute.stderr, execute.stderr From 8c22bb1d6b41a3a2564b744337edce6af27ff609 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 28 Jan 2026 16:24:24 -0800 Subject: [PATCH 054/113] update get_subnet_prices --- .../src/bittensor/subtensor_interface.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 8b393c884..4885f136b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2546,20 +2546,28 @@ async def get_subnet_prices( :return: A dictionary mapping netuid to the current Alpha price in TAO units. """ - query = await self.substrate.query_map( - module="Swap", - storage_function="AlphaSqrtPrice", - page_size=page_size, - block_hash=block_hash, - ) + all_netuids = await self.get_all_subnet_netuids(block_hash=block_hash) - map_ = {} - async for netuid_, current_sqrt_price in query: - current_sqrt_price_ = fixed_to_float(current_sqrt_price.value) - current_price = current_sqrt_price_**2 - map_[netuid_] = Balance.from_rao(int(current_price * 1e9)) + price_tasks = [ + self.query_runtime_api( + "SwapRuntimeApi", + "current_alpha_price", + params={"netuid": netuid}, + block_hash=block_hash, + ) + for netuid in all_netuids + if netuid != 0 + ] - return map_ + prices = await asyncio.gather(*price_tasks, return_exceptions=True) + + result = {0: Balance.from_tao(1.0)} + netuids_to_query = [netuid for netuid in all_netuids if netuid != 0] + for netuid, current_price in zip(netuids_to_query, prices): + if not isinstance(current_price, Exception): + result[netuid] = Balance.from_rao(current_price) + + return result async def get_all_subnet_ema_tao_inflow( self, From 9a873ea39d7c6a442f3a52af7c72ae994f55c06d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 28 Jan 2026 16:24:57 -0800 Subject: [PATCH 055/113] get_subnet_price updated --- .../src/bittensor/subtensor_interface.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 4885f136b..c3d15d645 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2510,6 +2510,7 @@ async def get_claimable_stakes_for_coldkey( results[hotkey][netuid] = net_claimable.set_unit(netuid) return results + async def get_subnet_price( self, netuid: int = None, @@ -2523,17 +2524,16 @@ async def get_subnet_price( :return: The current Alpha price in TAO units for the specified subnet. """ - # TODO update this to use the runtime call SwapRuntimeAPI.current_alpha_price - current_sqrt_price = await self.query( - module="Swap", - storage_function="AlphaSqrtPrice", - params=[netuid], + if netuid == 0: + return Balance.from_tao(1.0) + + current_price = await self.query_runtime_api( + "SwapRuntimeApi", + "current_alpha_price", + params={"netuid": netuid}, block_hash=block_hash, ) - - current_sqrt_price = fixed_to_float(current_sqrt_price) - current_price = current_sqrt_price * current_sqrt_price - return Balance.from_rao(int(current_price * 1e9)) + return Balance.from_rao(current_price) async def get_subnet_prices( self, block_hash: Optional[str] = None, page_size: int = 100 From 2e54aa912862835150f7f5bad9a4a760c73bf27c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 14:02:59 -0800 Subject: [PATCH 056/113] remove deprecated methods --- bittensor_cli/src/bittensor/chain_data.py | 81 ----------------------- 1 file changed, 81 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 718dbd694..eac92304c 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -800,87 +800,6 @@ def tao_to_alpha(self, tao: Balance) -> Balance: def alpha_to_tao(self, alpha: Balance) -> Balance: return Balance.from_tao(alpha.tao * self.price.tao) - def tao_to_alpha_with_slippage( - self, tao: Balance - ) -> tuple[Balance, Balance, float]: - """ - Returns an estimate of how much Alpha a staker would receive if they stake their tao using the current pool - state. - - Args: - tao: Amount of TAO to stake. - Returns: - Tuple of balances where the first part is the amount of Alpha received, and the - second part (slippage) is the difference between the estimated amount and ideal - amount as if there was no slippage - """ - if self.is_dynamic: - new_tao_in = self.tao_in + tao - if new_tao_in == 0: - return tao, Balance.from_rao(0) - new_alpha_in = self.k / new_tao_in - - # Amount of alpha given to the staker - alpha_returned = Balance.from_rao( - self.alpha_in.rao - new_alpha_in.rao - ).set_unit(self.netuid) - - # Ideal conversion as if there is no slippage, just price - alpha_ideal = self.tao_to_alpha(tao) - - if alpha_ideal.tao > alpha_returned.tao: - slippage = Balance.from_tao( - alpha_ideal.tao - alpha_returned.tao - ).set_unit(self.netuid) - else: - slippage = Balance.from_tao(0) - else: - alpha_returned = tao.set_unit(self.netuid) - slippage = Balance.from_tao(0) - - slippage_pct_float = ( - 100 * float(slippage) / float(slippage + alpha_returned) - if slippage + alpha_returned != 0 - else 0 - ) - return alpha_returned, slippage, slippage_pct_float - - def alpha_to_tao_with_slippage( - self, alpha: Balance - ) -> tuple[Balance, Balance, float]: - """ - Returns an estimate of how much TAO a staker would receive if they unstake their alpha using the current pool - state. - - Args: - alpha: Amount of Alpha to stake. - Returns: - Tuple of balances where the first part is the amount of TAO received, and the - second part (slippage) is the difference between the estimated amount and ideal - amount as if there was no slippage - """ - if self.is_dynamic: - new_alpha_in = self.alpha_in + alpha - new_tao_reserve = self.k / new_alpha_in - # Amount of TAO given to the unstaker - tao_returned = Balance.from_rao(self.tao_in - new_tao_reserve) - - # Ideal conversion as if there is no slippage, just price - tao_ideal = self.alpha_to_tao(alpha) - - if tao_ideal > tao_returned: - slippage = Balance.from_tao(tao_ideal.tao - tao_returned.tao) - else: - slippage = Balance.from_tao(0) - else: - tao_returned = alpha.set_unit(0) - slippage = Balance.from_tao(0) - slippage_pct_float = ( - 100 * float(slippage) / float(slippage + tao_returned) - if slippage + tao_returned != 0 - else 0 - ) - return tao_returned, slippage, slippage_pct_float @dataclass From 67b1e33e7059044c5785ed75671db042cdbeb728 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 15:14:08 -0800 Subject: [PATCH 057/113] add tao_to_alpha_slippage --- bittensor_cli/src/bittensor/chain_data.py | 40 +++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index eac92304c..eca6c1f98 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1,7 +1,7 @@ from abc import abstractmethod from dataclasses import dataclass from enum import Enum -from typing import Optional, Any, Union +from typing import Optional, Any, Union, Literal import netaddr from scalecodec.utils.ss58 import ss58_encode @@ -801,7 +801,6 @@ def alpha_to_tao(self, alpha: Balance) -> Balance: return Balance.from_tao(alpha.tao * self.price.tao) - @dataclass class ColdkeySwapAnnouncementInfo(InfoBase): """ @@ -1147,6 +1146,43 @@ def from_dict(cls, d: dict, netuid: int) -> "SimSwapResult": alpha_fee=Balance.from_rao(d["alpha_fee"]).set_unit(netuid), ) + def tao_to_alpha_slippage( + self, + tao_amount: Balance, + current_price: float, + netuid: int, + ) -> tuple[Balance, Balance, float]: + """ + Calculate slippage for a TAO -> Alpha swap. + + Args: + tao_amount: Amount of TAO provided as input. + current_price: Current alpha price in TAO (TAO per 1 alpha). + netuid: Target subnet netuid (used for unit tagging). + + Returns: + A tuple of: + received_alpha (Balance): Simulated alpha received. + slippage_alpha (Balance): Shortfall vs ideal at current_price. + slippage_pct_float (float): Slippage percentage (0 - 100). + """ + if current_price <= 0: + zero = Balance.from_tao(0).set_unit(netuid) + return zero, zero, 0.0 + + ideal_amount = Balance.from_tao(tao_amount.tao / current_price).set_unit(netuid) + received_amount = self.alpha_amount + + if ideal_amount.tao == 0: + zero = Balance.from_tao(0).set_unit(netuid) + return received_amount, zero, 0.0 + + slippage_amount = max(ideal_amount.tao - received_amount.tao, 0) + slippage_amount_balance = Balance.from_tao(slippage_amount).set_unit(netuid) + slippage_pct = 100 * slippage_amount / ideal_amount.tao + + return received_amount, slippage_amount_balance, slippage_pct + @dataclass class CrowdloanData(InfoBase): From 5bf8bc376e6d6beca821c7291453a3194def61a3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 15:14:21 -0800 Subject: [PATCH 058/113] add alpha_to_tao_slippage --- bittensor_cli/src/bittensor/chain_data.py | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index eca6c1f98..4b144a445 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1183,6 +1183,41 @@ def tao_to_alpha_slippage( return received_amount, slippage_amount_balance, slippage_pct + def alpha_to_tao_slippage( + self, + alpha_amount: Balance, + current_price: float, + ) -> tuple[Balance, Balance, float]: + """ + Calculate slippage for an Alpha -> TAO swap. + + Args: + alpha_amount: Amount of Alpha provided as input. + current_price: Current alpha price in TAO (TAO per 1 alpha). + + Returns: + A tuple of: + received_tao (Balance): Simulated TAO received. + slippage_tao (Balance): Shortfall vs ideal at current_price. + slippage_pct_float (float): Slippage percentage (0 - 100). + """ + if current_price <= 0: + zero = Balance.from_tao(0).set_unit(0) + return zero, zero, 0.0 + + ideal_amount = Balance.from_tao(alpha_amount.tao * current_price).set_unit(0) + received_amount = self.tao_amount + + if ideal_amount.tao == 0: + zero = Balance.from_tao(0).set_unit(0) + return received_amount, zero, 0.0 + + slippage_amount = max(ideal_amount.tao - received_amount.tao, 0) + slippage_amount_balance = Balance.from_tao(slippage_amount).set_unit(0) + slippage_pct = 100 * slippage_amount / ideal_amount.tao + + return received_amount, slippage_amount_balance, slippage_pct + @dataclass class CrowdloanData(InfoBase): From f36dcb82ed076c7be289c9067217fc7359267da0 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 15:50:56 -0800 Subject: [PATCH 059/113] improve get_subnet_prices --- .../src/bittensor/subtensor_interface.py | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index c3d15d645..d97bf2e93 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1654,7 +1654,7 @@ async def all_subnets(self, block_hash: Optional[str] = None) -> list[DynamicInf "get_all_dynamic_info", block_hash=block_hash, ), - self.get_subnet_prices(block_hash=block_hash, page_size=129), + self.get_subnet_prices(block_hash=block_hash), ) sns: list[DynamicInfo] = DynamicInfo.list_from_any(result) for sn in sns: @@ -2510,7 +2510,6 @@ async def get_claimable_stakes_for_coldkey( results[hotkey][netuid] = net_claimable.set_unit(netuid) return results - async def get_subnet_price( self, netuid: int = None, @@ -2536,36 +2535,32 @@ async def get_subnet_price( return Balance.from_rao(current_price) async def get_subnet_prices( - self, block_hash: Optional[str] = None, page_size: int = 100 + self, block_hash: Optional[str] = None ) -> dict[int, Balance]: """ Gets the current Alpha prices in TAO for all subnets. :param block_hash: The hash of the block to retrieve prices from. - :param page_size: The page size for batch queries (default: 100). :return: A dictionary mapping netuid to the current Alpha price in TAO units. """ all_netuids = await self.get_all_subnet_netuids(block_hash=block_hash) - price_tasks = [ - self.query_runtime_api( - "SwapRuntimeApi", - "current_alpha_price", - params={"netuid": netuid}, - block_hash=block_hash, - ) - for netuid in all_netuids - if netuid != 0 - ] - - prices = await asyncio.gather(*price_tasks, return_exceptions=True) - result = {0: Balance.from_tao(1.0)} netuids_to_query = [netuid for netuid in all_netuids if netuid != 0] + prices = await asyncio.gather( + *[ + self.query_runtime_api( + "SwapRuntimeApi", + "current_alpha_price", + params={"netuid": netuid}, + block_hash=block_hash, + ) + for netuid in netuids_to_query + ], + ) for netuid, current_price in zip(netuids_to_query, prices): - if not isinstance(current_price, Exception): - result[netuid] = Balance.from_rao(current_price) + result[netuid] = Balance.from_rao(current_price) return result From 71fd52a9d99899615e7188351244dd19721ba45e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 16:12:21 -0800 Subject: [PATCH 060/113] update get_total_stake_for_coldkey --- .../src/bittensor/subtensor_interface.py | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index d97bf2e93..6719daf79 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -444,40 +444,52 @@ async def get_total_stake_for_coldkey( :return: {address: Balance objects} """ - sub_stakes, dynamic_info = await asyncio.gather( + sub_stakes, price_map = await asyncio.gather( self.get_stake_for_coldkeys(list(ss58_addresses), block_hash=block_hash), - # Token pricing info - self.all_subnets(block_hash=block_hash), + self.get_subnet_prices(block_hash=block_hash), ) - results = {} + results: dict[str, tuple[Balance, Balance]] = {} + dynamic_stakes: list[tuple[str, "StakeInfo"]] = [] + for ss58, stake_info_list in sub_stakes.items(): - total_tao_value = Balance(0) - total_swapped_tao_value = Balance(0) + total_tao_value, total_swapped_tao_value = Balance(0), Balance(0) for sub_stake in stake_info_list: if sub_stake.stake.rao == 0: continue - netuid = sub_stake.netuid - pool = dynamic_info[netuid] - alpha_value = Balance.from_rao(int(sub_stake.stake.rao)).set_unit( - netuid + netuid = sub_stake.netuid + price = price_map[netuid] + ideal_tao = Balance.from_tao(sub_stake.stake.tao * price.tao).set_unit( + 0 ) + total_tao_value += ideal_tao - # Without slippage - tao_value = pool.alpha_to_tao(alpha_value) - total_tao_value += tao_value - - # With slippage if netuid == 0: - swapped_tao_value = tao_value + total_swapped_tao_value += ideal_tao else: - swapped_tao_value, _, _ = pool.alpha_to_tao_with_slippage( - sub_stake.stake - ) - total_swapped_tao_value += swapped_tao_value + dynamic_stakes.append((ss58, sub_stake)) results[ss58] = (total_tao_value, total_swapped_tao_value) + + if dynamic_stakes: + sim_results = await asyncio.gather( + *[ + self.sim_swap( + origin_netuid=sub_stake.netuid, + destination_netuid=0, + amount=sub_stake.stake.rao, + block_hash=block_hash, + ) + for _, sub_stake in dynamic_stakes + ] + ) + + for (ss58, sub_stake), sim_result in zip(dynamic_stakes, sim_results): + total_tao_value, total_swapped_tao_value = results[ss58] + total_swapped_tao_value += sim_result.tao_amount + results[ss58] = (total_tao_value, total_swapped_tao_value) + return results async def get_total_stake_for_hotkey( From 971b31e1c1ca7b3cccebea33acda62f242e45ec6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 17:24:57 -0800 Subject: [PATCH 061/113] update remove stake slippage --- bittensor_cli/src/commands/stake/remove.py | 36 ++++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index d1490e9fd..7d9724d5d 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -250,6 +250,13 @@ async def unstake( received_amount = sim_swap.tao_amount if not proxy: received_amount -= extrinsic_fee + + _, _, slippage_pct_float = sim_swap.alpha_to_tao_slippage( + alpha_amount=amount_to_unstake_as_balance, + current_price=current_price, + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + max_float_slippage = max(slippage_pct_float, max_float_slippage) except ValueError: continue total_received_amount += received_amount @@ -275,7 +282,7 @@ async def unstake( str(sim_swap.alpha_fee), # Fee str(extrinsic_fee), # Extrinsic fee str(received_amount), # Received Amount - # slippage_pct, # Slippage Percent + slippage_pct, # Slippage Percent ] # Additional fields for safe unstaking @@ -444,6 +451,7 @@ async def unstake_all( return all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} + max_float_slippage = 0.0 # Create table for unstaking all table_title = ( @@ -488,11 +496,11 @@ async def unstake_all( justify="center", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) - # table.add_column( - # "Slippage", - # justify="center", - # style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], - # ) + table.add_column( + "Slippage", + justify="center", + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + ) # Calculate total received total_received_value = Balance(0) @@ -524,6 +532,13 @@ async def unstake_all( if received_amount < Balance.from_tao(0): print_error("Not enough Alpha to pay the transaction fee.") continue + + _, _, slippage_pct_float = sim_swap.alpha_to_tao_slippage( + alpha_amount=stake_amount, + current_price=current_price, + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + max_float_slippage = max(slippage_pct_float, max_float_slippage) except (AttributeError, ValueError): continue @@ -538,8 +553,9 @@ async def unstake_all( str(sim_swap.alpha_fee), str(extrinsic_fee), str(received_amount), + slippage_pct, ) - console.print(table) + _print_table_and_slippage(table, max_float_slippage, False) console.print( f"Total expected return: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}" @@ -1350,9 +1366,9 @@ def _create_unstake_table( style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], footer=str(total_received_amount), ) - # table.add_column( - # "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] - # ) + table.add_column( + "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) if safe_staking: table.add_column( f"Rate with tolerance: [blue]({rate_tolerance * 100}%)[/blue]", From 6813d5b0a35f7e14b7e7241845850a594c401d49 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 18:01:48 -0800 Subject: [PATCH 062/113] update slippage calc in add stake --- bittensor_cli/src/commands/stake/add.py | 79 +++++-------------------- 1 file changed, 16 insertions(+), 63 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index b9bff970c..340f336ed 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -382,18 +382,7 @@ async def stake_extrinsic( 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 + # Calculate rate current_price_float = float(subnet_info.price.tao) rate = 1.0 / current_price_float @@ -443,6 +432,15 @@ async def stake_extrinsic( amount=amount_minus_fee.rao, ) received_amount = sim_swap.alpha_amount + + _, _, slippage_pct_float = sim_swap.tao_to_alpha_slippage( + tao_amount=amount_minus_fee, + current_price=current_price_float, + netuid=netuid, + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + max_slippage = max(slippage_pct_float, max_slippage) + # Add rows for the table base_row = [ str(netuid), # netuid @@ -453,7 +451,7 @@ async def stake_extrinsic( str(received_amount.set_unit(netuid)), # received str(sim_swap.tao_fee), # fee str(extrinsic_fee), - # str(slippage_pct), # slippage + str(slippage_pct), # slippage ] + row_extension rows.append(tuple(base_row)) @@ -688,10 +686,9 @@ def _define_stake_table( justify="center", style=COLOR_PALETTE.STAKE.TAO, ) - # TODO: Uncomment when slippage is reimplemented for v3 - # table.add_column( - # "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] - # ) + table.add_column( + "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) if safe_staking: table.add_column( @@ -736,8 +733,8 @@ def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: b - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to. - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. - - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage.""" - # - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root).""" + - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. + - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root).""" safe_staking_description = """ - [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate exceeds this tolerance, the transaction will be limited or rejected. @@ -745,47 +742,3 @@ def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: b console.print(base_description + (safe_staking_description if safe_staking else "")) - -def _calculate_slippage( - subnet_info, amount: Balance, stake_fee: Balance -) -> tuple[Balance, str, float, str]: - """Calculate slippage when adding stake. - - Args: - subnet_info: Subnet dynamic info - amount: Amount being staked - stake_fee: Transaction fee for the stake operation - - Returns: - tuple containing: - - received_amount: Amount received after slippage and fees - - slippage_str: Formatted slippage percentage string - - slippage_float: Raw slippage percentage value - - rate: Exchange rate string - - TODO: Update to v3. This method only works for protocol-liquidity-only - mode (user liquidity disabled) - """ - amount_after_fee = amount - stake_fee - - if amount_after_fee < 0: - print_error("You don't have enough balance to cover the stake fee.") - raise ValueError() - - received_amount, _, _ = subnet_info.tao_to_alpha_with_slippage(amount_after_fee) - - if subnet_info.is_dynamic: - ideal_amount = subnet_info.tao_to_alpha(amount) - total_slippage = ideal_amount - received_amount - slippage_pct_float = 100 * (total_slippage.tao / ideal_amount.tao) - slippage_str = f"{slippage_pct_float:.4f} %" - rate = f"{(1 / subnet_info.price.tao or 1):.4f}" - else: - # TODO: Fix this. Slippage is always zero for static networks. - slippage_pct_float = ( - 100 * float(stake_fee.tao) / float(amount.tao) if amount.tao != 0 else 0 - ) - slippage_str = f"{slippage_pct_float:.4f} %" - rate = "1" - - return received_amount, slippage_str, slippage_pct_float, rate From 7b86d41d7acdadd83264790e0e22a8405fc8e7a2 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 19:08:00 -0800 Subject: [PATCH 063/113] add disabled cmds --- bittensor_cli/src/commands/view.py | 74 +++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/commands/view.py b/bittensor_cli/src/commands/view.py index 26bb3eb95..0f959fc9e 100644 --- a/bittensor_cli/src/commands/view.py +++ b/bittensor_cli/src/commands/view.py @@ -31,7 +31,10 @@ async def display_network_dashboard( try: with console.status("[dark_sea_green3]Fetching data...", spinner="earth"): _subnet_data = await fetch_subnet_data(wallet, subtensor) - subnet_data = process_subnet_data(_subnet_data) + # Add block_hash to raw_data for RPC calls + if "block_hash" not in _subnet_data: + _subnet_data["block_hash"] = await subtensor.substrate.get_chain_head() + subnet_data = await process_subnet_data(_subnet_data, subtensor) html_content = generate_full_page(subnet_data) if use_wry: @@ -154,10 +157,13 @@ async def fetch_subnet_data( "old_identities": old_identities, "wallet": wallet, "block_number": block_number, + "block_hash": block_hash, } -def process_subnet_data(raw_data: dict[str, Any]) -> dict[str, Any]: +async def process_subnet_data( + raw_data: dict[str, Any], subtensor: "SubtensorInterface" +) -> dict[str, Any]: """ Process and prepare subnet data. """ @@ -169,20 +175,74 @@ def process_subnet_data(raw_data: dict[str, Any]) -> dict[str, Any]: old_identities = raw_data["old_identities"] wallet = raw_data["wallet"] block_number = raw_data["block_number"] + block_hash = raw_data.get("block_hash") pool_info = {info.netuid: info for info in subnets_info} total_ideal_stake_value = Balance.from_tao(0) total_slippage_value = Balance.from_tao(0) + # Batch calculate slippage using RPC for accurate Balancer calculations + slippage_tasks = [] + stake_list = [] + for stake in stake_info: + if stake.stake.tao > 0: + if stake.netuid == 0: + # Root subnet - no slippage, skip RPC call + stake_list.append((stake, None)) + else: + slippage_tasks.append( + subtensor.sim_swap( + origin_netuid=stake.netuid, + destination_netuid=0, + amount=stake.stake.rao, + block_hash=block_hash, + ) + ) + stake_list.append((stake, len(slippage_tasks) - 1)) + + slippage_results = ( + await asyncio.gather(*slippage_tasks, return_exceptions=True) + if slippage_tasks + else [] + ) + # Process stake stake_dict: dict[int, list[dict[str, Any]]] = {} - for stake in stake_info: + for stake, slippage_idx in stake_list: if stake.stake.tao > 0: - slippage_value, _, slippage_percentage = pool_info[ - stake.netuid - ].alpha_to_tao_with_slippage(stake.stake) - ideal_value = pool_info[stake.netuid].alpha_to_tao(stake.stake) + if stake.netuid == 0: + # Root subnet - no slippage + slippage_value = stake.stake.set_unit(0) + slippage_percentage = 0.0 + current_price = 1.0 + else: + if slippage_idx is not None and slippage_idx < len(slippage_results): + slippage_result = slippage_results[slippage_idx] + if isinstance(slippage_result, Exception): + # On RPC error, skip slippage calculation for this stake + console.print( + f"[yellow]Warning:[/yellow] Could not calculate slippage " + f"for stake on netuid {stake.netuid}. Skipping slippage display." + ) + slippage_value = Balance.from_rao(0).set_unit(0) + slippage_percentage = 0.0 + else: + current_price = pool_info[stake.netuid].price.tao + slippage_value, _, slippage_percentage = ( + slippage_result.alpha_to_tao_slippage( + alpha_amount=stake.stake, current_price=current_price + ) + ) + else: + # This should not happen, but handle gracefully + slippage_value = Balance.from_rao(0).set_unit(0) + slippage_percentage = 0.0 + # Ideal TAO value at current price (runtime price) + current_price = ( + pool_info[stake.netuid].price.tao if stake.netuid in pool_info else 0.0 + ) + ideal_value = Balance.from_tao(stake.stake.tao * current_price).set_unit(0) total_ideal_stake_value += ideal_value total_slippage_value += slippage_value stake_dict.setdefault(stake.netuid, []).append( From 8057d82039a1eaed614f1ed3ac823da9cc8f975e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 19:10:07 -0800 Subject: [PATCH 064/113] wip --- bittensor_cli/src/commands/view.py | 74 +++--------------------------- 1 file changed, 7 insertions(+), 67 deletions(-) diff --git a/bittensor_cli/src/commands/view.py b/bittensor_cli/src/commands/view.py index 0f959fc9e..26bb3eb95 100644 --- a/bittensor_cli/src/commands/view.py +++ b/bittensor_cli/src/commands/view.py @@ -31,10 +31,7 @@ async def display_network_dashboard( try: with console.status("[dark_sea_green3]Fetching data...", spinner="earth"): _subnet_data = await fetch_subnet_data(wallet, subtensor) - # Add block_hash to raw_data for RPC calls - if "block_hash" not in _subnet_data: - _subnet_data["block_hash"] = await subtensor.substrate.get_chain_head() - subnet_data = await process_subnet_data(_subnet_data, subtensor) + subnet_data = process_subnet_data(_subnet_data) html_content = generate_full_page(subnet_data) if use_wry: @@ -157,13 +154,10 @@ async def fetch_subnet_data( "old_identities": old_identities, "wallet": wallet, "block_number": block_number, - "block_hash": block_hash, } -async def process_subnet_data( - raw_data: dict[str, Any], subtensor: "SubtensorInterface" -) -> dict[str, Any]: +def process_subnet_data(raw_data: dict[str, Any]) -> dict[str, Any]: """ Process and prepare subnet data. """ @@ -175,74 +169,20 @@ async def process_subnet_data( old_identities = raw_data["old_identities"] wallet = raw_data["wallet"] block_number = raw_data["block_number"] - block_hash = raw_data.get("block_hash") pool_info = {info.netuid: info for info in subnets_info} total_ideal_stake_value = Balance.from_tao(0) total_slippage_value = Balance.from_tao(0) - # Batch calculate slippage using RPC for accurate Balancer calculations - slippage_tasks = [] - stake_list = [] - for stake in stake_info: - if stake.stake.tao > 0: - if stake.netuid == 0: - # Root subnet - no slippage, skip RPC call - stake_list.append((stake, None)) - else: - slippage_tasks.append( - subtensor.sim_swap( - origin_netuid=stake.netuid, - destination_netuid=0, - amount=stake.stake.rao, - block_hash=block_hash, - ) - ) - stake_list.append((stake, len(slippage_tasks) - 1)) - - slippage_results = ( - await asyncio.gather(*slippage_tasks, return_exceptions=True) - if slippage_tasks - else [] - ) - # Process stake stake_dict: dict[int, list[dict[str, Any]]] = {} - for stake, slippage_idx in stake_list: + for stake in stake_info: if stake.stake.tao > 0: - if stake.netuid == 0: - # Root subnet - no slippage - slippage_value = stake.stake.set_unit(0) - slippage_percentage = 0.0 - current_price = 1.0 - else: - if slippage_idx is not None and slippage_idx < len(slippage_results): - slippage_result = slippage_results[slippage_idx] - if isinstance(slippage_result, Exception): - # On RPC error, skip slippage calculation for this stake - console.print( - f"[yellow]Warning:[/yellow] Could not calculate slippage " - f"for stake on netuid {stake.netuid}. Skipping slippage display." - ) - slippage_value = Balance.from_rao(0).set_unit(0) - slippage_percentage = 0.0 - else: - current_price = pool_info[stake.netuid].price.tao - slippage_value, _, slippage_percentage = ( - slippage_result.alpha_to_tao_slippage( - alpha_amount=stake.stake, current_price=current_price - ) - ) - else: - # This should not happen, but handle gracefully - slippage_value = Balance.from_rao(0).set_unit(0) - slippage_percentage = 0.0 - # Ideal TAO value at current price (runtime price) - current_price = ( - pool_info[stake.netuid].price.tao if stake.netuid in pool_info else 0.0 - ) - ideal_value = Balance.from_tao(stake.stake.tao * current_price).set_unit(0) + slippage_value, _, slippage_percentage = pool_info[ + stake.netuid + ].alpha_to_tao_with_slippage(stake.stake) + ideal_value = pool_info[stake.netuid].alpha_to_tao(stake.stake) total_ideal_stake_value += ideal_value total_slippage_value += slippage_value stake_dict.setdefault(stake.netuid, []).append( From 491581a52ed3e53e277ed020d5db7f24ec3d07e1 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 19:10:25 -0800 Subject: [PATCH 065/113] add disabled warnings --- bittensor_cli/cli.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index dadf57eb9..768eef890 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8557,6 +8557,9 @@ def liquidity_add( json_output: bool = Options.json_output, ): """Add liquidity to the swap (as a combination of TAO + Alpha).""" + console.print_error("User liquidity is currently disabled on Bittensor.") + raise typer.Exit() + self.verbosity_handler(quiet, verbose, json_output, prompt, decline) proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not netuid: @@ -8634,6 +8637,9 @@ def liquidity_list( json_output: bool = Options.json_output, ): """Displays liquidity positions in given subnet.""" + console.print_error("User liquidity is currently disabled on Bittensor.") + raise typer.Exit() + self.verbosity_handler(quiet, verbose, json_output, prompt=False) if not netuid: netuid = IntPrompt.ask( @@ -8687,6 +8693,8 @@ def liquidity_remove( ): """Remove liquidity from the swap (as a combination of TAO + Alpha).""" + console.print_error("User liquidity is currently disabled on Bittensor.") + raise typer.Exit() self.verbosity_handler(quiet, verbose, json_output, prompt, decline) proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if all_liquidity_ids and position_id: @@ -8763,6 +8771,8 @@ def liquidity_modify( json_output: bool = Options.json_output, ): """Modifies the liquidity position for the given subnet.""" + console.print_error("User liquidity is currently disabled on Bittensor.") + raise typer.Exit() self.verbosity_handler(quiet, verbose, json_output, prompt, decline) proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not netuid: From 263f732e90e0f570bfda9d995d4d6cd834fd080b Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 19:10:44 -0800 Subject: [PATCH 066/113] disable liquidity cmd --- 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 768eef890..c56926a36 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -866,7 +866,7 @@ def __init__(self): self.subnet_mechanisms_app = typer.Typer(epilog=_epilog) self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) - self.liquidity_app = typer.Typer(epilog=_epilog) + self.liquidity_app = typer.Typer(epilog=_epilog, hidden=True) self.crowd_app = typer.Typer(epilog=_epilog) self.utils_app = typer.Typer(epilog=_epilog) self.axon_app = typer.Typer(epilog=_epilog) From f6e9af74b701bf14d599b951ac5fdb8c6ebabbb7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 19:11:30 -0800 Subject: [PATCH 067/113] cleanup --- bittensor_cli/src/bittensor/chain_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 4b144a445..af2b4711b 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1,7 +1,7 @@ from abc import abstractmethod from dataclasses import dataclass from enum import Enum -from typing import Optional, Any, Union, Literal +from typing import Optional, Any, Union import netaddr from scalecodec.utils.ss58 import ss58_encode From 86973fabd5bc492a5cb0b4168e4db78e3e6d9593 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 30 Jan 2026 19:13:48 -0800 Subject: [PATCH 068/113] ruff --- bittensor_cli/src/commands/stake/add.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 340f336ed..a024d5607 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -741,4 +741,3 @@ def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: b - [bold white]Partial staking[/bold white]: If True, allows staking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.\n""" console.print(base_description + (safe_staking_description if safe_staking else "")) - From 91e038460e0915edba99b4edc33f27f0bf90a046 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 3 Feb 2026 13:57:22 +0200 Subject: [PATCH 069/113] Error message handled properly --- bittensor_cli/src/commands/stake/remove.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index d1490e9fd..608f627c3 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -806,9 +806,7 @@ async def _safe_unstake_extrinsic( status=status, ) else: - err_out( - f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" - ) + err_out(f"\n{failure_prelude} with error: {err_msg}") return False, None From bbc0d642e37433a8a8da8f6eb48589dd90e5d639 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Tue, 3 Feb 2026 21:44:55 +0200 Subject: [PATCH 070/113] Adds more to the debug section of the readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index a6ba1ea79..37b4bc488 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,12 @@ calls (such as block number to block hash mapping), which can speed up subsequen --- ## Debugging +If a command is failing with an odd error, you can usually rerun the command with `--verbose` for a more detailed output +of any errors/exceptions that occur. This should be done prior to reporting the issue, as it helps us substantially in +determining the root cause of issues. + +Additionally, you can pull a debug log. + BTCLI will store a debug log for every command run. This file is overwritten for each new command run. The default location of this file is `~/.bittensor/debug.txt` and can be set with the `BTCLI_DEBUG_FILE` env var (see above section). @@ -191,6 +197,12 @@ and set the save file location. We recommend doing this first before anything, a us on our [Discord](https://discord.gg/bittensor), or by opening an issue on [GitHub](https://github.com/opentensor/btcli/issues/new) (where you can also upload your debug file). + +Steps: +1. Re-run the command with `--verbose` at the end, e.g. `btcli st remove --verbose` +2. Run `btcli --debug` to save the debug log +3. Report the issue on GitHub or Discord + --- ## License From 7ea545b3c6b65d5878dde2f78d64c43d17a6c629 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:30:31 -0800 Subject: [PATCH 071/113] add sudo_buyback cmd --- bittensor_cli/cli.py | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index dadf57eb9..e158f0665 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1164,6 +1164,9 @@ def __init__(self): self.sudo_app.command("trim", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( self.sudo_trim ) + self.sudo_app.command("buyback", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( + self.sudo_buyback + ) # subnets commands self.subnets_app.command( @@ -7382,6 +7385,98 @@ def sudo_trim( ) ) + def sudo_buyback( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, + netuid: int = Options.netuid, + amount: float = typer.Option( + None, + "--amount", + "-a", + help="Amount of TAO to buyback", + ), + proxy: Optional[str] = Options.proxy, + rate_tolerance: Optional[float] = Options.rate_tolerance, + safe_staking: Optional[bool] = Options.safe_staking, + mev_protection: bool = Options.mev_protection, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + prompt: bool = Options.prompt, + decline: bool = Options.decline, + period: int = Options.period, + ): + """ + Allows subnet owners to buy back alpha on their subnet by staking TAO and immediately burning the acquired alpha. + + [bold]Examples:[/bold] + 1. Buyback 10 TAO on subnet 14: + [green]$[/green] btcli sudo buyback --netuid 14 --amount 10 + 2. Buyback 10 TAO on subnet 14 with safe staking and 5% rate tolerance: + [green]$[/green] btcli sudo buyback --netuid 14 --amount 10 --tolerance 0.05 + 3. Buyback 10 TAO on subnet 14 with a specific hotkey: + [green]$[/green] btcli sudo buyback --netuid 14 --amount 10 --wallet-hotkey + """ + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only=False) + safe_staking = self.ask_safe_staking(safe_staking) + if safe_staking: + rate_tolerance = self.ask_rate_tolerance(rate_tolerance) + + print_protection_warnings( + mev_protection=mev_protection, + safe_staking=safe_staking, + command_name="sudo buyback", + ) + + if not wallet_hotkey: + wallet_hotkey = Prompt.ask( + "Enter the [blue]hotkey[/blue] name or " + "[blue]hotkey ss58 address[/blue] [dim](to use for the buyback)[/dim]", + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + ) + + if wallet_hotkey and is_valid_ss58_address(wallet_hotkey): + hotkey_ss58 = wallet_hotkey + wallet = self.wallet_ask( + wallet_name, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + else: + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + hotkey_ss58 = get_hotkey_pub_ss58(wallet) + + self._run_command( + sudo.buyback( + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + amount=amount, + hotkey_ss58=hotkey_ss58, + safe_staking=safe_staking, + proxy=proxy, + rate_tolerance=rate_tolerance, + mev_protection=mev_protection, + json_output=json_output, + prompt=prompt, + decline=decline, + quiet=quiet, + period=period, + ) + ) + # Subnets def subnets_list( From 596a962103d1b925b88fa92699b11fc9a1684a74 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:31:31 -0800 Subject: [PATCH 072/113] add table definition --- bittensor_cli/src/commands/sudo.py | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index ec6d1461c..bb7af6140 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -18,10 +18,16 @@ DelegatesDetails, COLOR_PALETTE, ) +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + extract_mev_shield_id, + 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, + create_table, print_error, print_success, print_verbose, @@ -94,6 +100,56 @@ def string_to_bool(val) -> Union[bool, Type[ValueError]]: return ValueError +def _define_buyback_table( + wallet: Wallet, + subtensor: "SubtensorInterface", + safe_staking: bool, + rate_tolerance: float, +) -> Table: + table = create_table( + title=f"\n[{COLOR_PALETTE.G.HEADER}]Subnet Buyback:\n" + f"Wallet: [{COLOR_PALETTE.G.CK}]{wallet.name}[/{COLOR_PALETTE.G.CK}], " + f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n" + f"Network: {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( + "Amount (τ)", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO"], + ) + table.add_column( + "Rate (per τ)", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + "Est. Burned", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + table.add_column( + "Fee (τ)", + justify="center", + style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], + ) + table.add_column( + "Extrinsic Fee (τ)", + justify="center", + style=COLOR_PALETTE.STAKE.TAO, + ) + if safe_staking: + table.add_column( + f"Rate with tolerance: [blue]({rate_tolerance * 100}%)[/blue]", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + return table + + def search_metadata( param_name: str, value: Union[str, bool, float, list[float]], From b7424fec4e3638f96600c997e568b1eaa702a6ca Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:32:07 -0800 Subject: [PATCH 073/113] wip --- bittensor_cli/src/commands/sudo.py | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index bb7af6140..d63db5e54 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -100,6 +100,53 @@ def string_to_bool(val) -> Union[bool, Type[ValueError]]: return ValueError +async def buyback( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + amount: float, + hotkey_ss58: Optional[str], + safe_staking: bool, + proxy: Optional[str], + rate_tolerance: Optional[float], + mev_protection: bool, + json_output: bool, + prompt: bool, + decline: bool, + quiet: bool, + period: int, +) -> bool: + """ + Perform a subnet buyback (owner-only). Stakes TAO into the subnet and immediately burns the acquired alpha. + """ + subnet_owner = await subtensor.query( + module="SubtensorModule", + storage_function="SubnetOwner", + params=[netuid], + ) + if subnet_owner != wallet.coldkeypub.ss58_address: + err_msg = ( + f"Coldkey {wallet.coldkeypub.ss58_address} does not own subnet {netuid}." + ) + if json_output: + json_console.print_json( + data={ + "success": False, + "message": err_msg, + "extrinsic_identifier": None, + } + ) + else: + print_error(err_msg) + return False + + subnet_info = await subtensor.subnet(netuid=netuid) + buyback_amount = Balance.from_tao(amount) + rate_tolerance = rate_tolerance if rate_tolerance is not None else 0.0 + + + + def _define_buyback_table( wallet: Wallet, subtensor: "SubtensorInterface", From 7314d75bc07bf9c49cc2280413aa8fb6b5068043 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:32:38 -0800 Subject: [PATCH 074/113] safe staking + table output --- bittensor_cli/src/commands/sudo.py | 70 +++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index d63db5e54..40a1584d3 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -144,7 +144,75 @@ async def buyback( buyback_amount = Balance.from_tao(amount) rate_tolerance = rate_tolerance if rate_tolerance is not None else 0.0 - + price_limit: Optional[Balance] = None + if safe_staking: + price_limit = Balance.from_tao(subnet_info.price.tao * (1 + rate_tolerance)) + + call_params = { + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount": buyback_amount.rao, + "limit": price_limit.rao if price_limit else None, + } + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="subnet_buyback", + call_params=call_params, + ) + + if not json_output: + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) + amount_minus_fee = buyback_amount - extrinsic_fee + sim_swap = await subtensor.sim_swap( + origin_netuid=0, + destination_netuid=netuid, + amount=amount_minus_fee.rao, + ) + received_amount = sim_swap.alpha_amount + + current_price_float = float(subnet_info.price.tao) + rate = 1.0 / current_price_float + + table = _define_buyback_table( + wallet=wallet, + subtensor=subtensor, + safe_staking=safe_staking, + rate_tolerance=rate_tolerance, + ) + row = [ + str(netuid), + hotkey_ss58, + str(buyback_amount), + str(rate) + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", + str(received_amount.set_unit(netuid)), + str(sim_swap.tao_fee), + str(extrinsic_fee), + ] + if safe_staking: + price_with_tolerance = current_price_float * (1 + rate_tolerance) + rate_with_tolerance = 1.0 / price_with_tolerance + rate_with_tolerance_str = ( + f"{rate_with_tolerance:.4f} " + f"{Balance.get_unit(netuid)}/{Balance.get_unit(0)} " + ) + row.append(rate_with_tolerance_str) + + table.add_row(*row) + console.print(table) + + if prompt and not confirm_action( + "Would you like to continue?", decline=decline, quiet=quiet + ): + print_error("User aborted.") + return False + + if not unlock_key(wallet).success: + return False + + def _define_buyback_table( From 1cacf1b5621e2c4fe6837908eed96b980b66e503 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:32:56 -0800 Subject: [PATCH 075/113] execute buyback call --- bittensor_cli/src/commands/sudo.py | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 40a1584d3..511a84f03 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -212,7 +212,71 @@ async def buyback( if not unlock_key(wallet).success: return False + with console.status( + f":satellite: Performing subnet buyback on [bold]{netuid}[/bold]...", + spinner="earth", + ) as status: + next_nonce = await subtensor.substrate.get_account_next_index( + wallet.coldkeypub.ss58_address + ) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + nonce=next_nonce, + era={"period": period}, + proxy=proxy, + mev_protection=mev_protection, + ) + if not success: + if json_output: + json_console.print_json( + data={ + "success": False, + "message": err_msg, + "extrinsic_identifier": None, + } + ) + else: + print_error(err_msg, status=status) + return False + + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(ext_receipt) + mev_success, mev_error, ext_receipt = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=ext_receipt.block_hash, + status=status, + ) + if not mev_success: + status.stop() + if json_output: + json_console.print_json( + data={ + "success": False, + "message": mev_error, + "extrinsic_identifier": None, + } + ) + else: + print_error(mev_error, status=status) + return False + + ext_id = await ext_receipt.get_extrinsic_identifier() + + msg = f"Subnet buyback succeeded on SN{netuid}." + if json_output: + json_console.print_json( + data={"success": True, "message": msg, "extrinsic_identifier": ext_id} + ) + else: + await print_extrinsic_id(ext_receipt) + print_success(msg) + + return True def _define_buyback_table( From 3630f35f636fbe884fa35902c686d08a2136fbe8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:34:14 -0800 Subject: [PATCH 076/113] add guardrails --- bittensor_cli/cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e158f0665..c94fec425 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7458,6 +7458,14 @@ def sudo_buyback( ) hotkey_ss58 = get_hotkey_pub_ss58(wallet) + if amount <= 0: + print_error(f"You entered an incorrect buyback amount: {amount}") + raise typer.Exit() + + if netuid == 0: + print_error("Cannot buyback on the root subnet.") + raise typer.Exit() + self._run_command( sudo.buyback( subtensor=self.initialize_chain(network), From 13570936ae70f04903f12a9cd23396d79a1e43e9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:34:44 -0800 Subject: [PATCH 077/113] wip e2e test --- tests/e2e_tests/test_buyback.py | 120 ++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/e2e_tests/test_buyback.py diff --git a/tests/e2e_tests/test_buyback.py b/tests/e2e_tests/test_buyback.py new file mode 100644 index 000000000..25a0339de --- /dev/null +++ b/tests/e2e_tests/test_buyback.py @@ -0,0 +1,120 @@ +import json +import time + +import pytest + +from .utils import extract_coldkey_balance + + +@pytest.mark.parametrize("local_chain", [False], indirect=True) +def test_subnet_buyback(local_chain, wallet_setup): + """ + Test subnet buyback + 1. Create a subnet + 2. Start the subnet's emission schedule + 3. Buyback the subnet + 3. Check the balance before and after the buyback upon success + 4. Try to buyback again and expect it to fail due to rate limit + """ + + _, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup("//Alice") + time.sleep(2) + 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", + "--repo", + "https://github.com/username/repo", + "--contact", + "test@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "test#1234", + "--description", + "A test subnet for e2e testing", + "--logo-url", + "https://testsubnet.com/logo.png", + "--additional-info", + "Test subnet", + "--no-prompt", + "--no-mev-protection", + ], + ) + assert "✅ Registered subnetwork with netuid: 2" in result.stdout + + # Start the subnet's emission schedule + start_call_netuid_2 = 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 ( + "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 + time.sleep(2) + + # Balance before buyback + _balance_before = exec_command_alice( + "wallet", + "balance", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--network", + "ws://127.0.0.1:9945", + ], + ) + balance_before = extract_coldkey_balance( + _balance_before.stdout, wallet_alice.name, wallet_alice.coldkey.ss58_address + )["free_balance"] + + # First buyback + amount_tao = 5.0 + buyback_result = exec_command_alice( + "sudo", + "buyback", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--network", + "ws://127.0.0.1:9945", + "--netuid", + str(netuid), + "--amount", + str(amount_tao), + "--no-prompt", + "--json-output", + ], + ) + buyback_ok_out = json.loads(buyback_result.stdout) + assert buyback_ok_out["success"] is True, buyback_result.stdout From 298f0b3642920a22a6c27e5dab4a5b09eafef495 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:40:09 -0800 Subject: [PATCH 078/113] test_subnet_buyback test --- tests/e2e_tests/test_buyback.py | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/e2e_tests/test_buyback.py b/tests/e2e_tests/test_buyback.py index 25a0339de..7112899ce 100644 --- a/tests/e2e_tests/test_buyback.py +++ b/tests/e2e_tests/test_buyback.py @@ -118,3 +118,46 @@ def test_subnet_buyback(local_chain, wallet_setup): ) buyback_ok_out = json.loads(buyback_result.stdout) assert buyback_ok_out["success"] is True, buyback_result.stdout + + # Balance after buyback + _balance_after = exec_command_alice( + "wallet", + "balance", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--network", + "ws://127.0.0.1:9945", + ], + ) + balance_after = extract_coldkey_balance( + _balance_after.stdout, wallet_alice.name, wallet_alice.coldkey.ss58_address + )["free_balance"] + assert balance_after < balance_before, (balance_before, balance_after) + + # Should fail due to rate limit + buyback_ratelimited_result = exec_command_alice( + "sudo", + "buyback", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--network", + "ws://127.0.0.1:9945", + "--netuid", + str(netuid), + "--amount", + str(amount_tao), + "--no-prompt", + "--json-output", + ], + ) + buyback_ratelimited = json.loads(buyback_ratelimited_result.stdout) + assert buyback_ratelimited["success"] is False, buyback_ratelimited_result.stdout + assert "SubnetBuybackRateLimitExceeded" in buyback_ratelimited["message"] From 824a43edba0242bf0ce7f6d1203f8bc44bc5aed5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:40:51 -0800 Subject: [PATCH 079/113] rename func --- bittensor_cli/cli.py | 2 +- bittensor_cli/src/commands/sudo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c94fec425..0df7479d3 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7467,7 +7467,7 @@ def sudo_buyback( raise typer.Exit() self._run_command( - sudo.buyback( + sudo.subnet_buyback( subtensor=self.initialize_chain(network), wallet=wallet, netuid=netuid, diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 511a84f03..be58b3bc1 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -100,7 +100,7 @@ def string_to_bool(val) -> Union[bool, Type[ValueError]]: return ValueError -async def buyback( +async def subnet_buyback( wallet: Wallet, subtensor: "SubtensorInterface", netuid: int, From 5ddd93b9b23e754515727a4a0ad377da05c09b46 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:41:21 -0800 Subject: [PATCH 080/113] cleanup --- bittensor_cli/cli.py | 4 ++-- bittensor_cli/src/commands/sudo.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0df7479d3..592b56b22 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1165,7 +1165,7 @@ def __init__(self): self.sudo_trim ) self.sudo_app.command("buyback", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( - self.sudo_buyback + self.sudo_subnet_buyback ) # subnets commands @@ -7385,7 +7385,7 @@ def sudo_trim( ) ) - def sudo_buyback( + def sudo_subnet_buyback( self, network: Optional[list[str]] = Options.network, wallet_name: Optional[str] = Options.wallet_name, diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index be58b3bc1..4813150ef 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -117,7 +117,8 @@ async def subnet_buyback( period: int, ) -> bool: """ - Perform a subnet buyback (owner-only). Stakes TAO into the subnet and immediately burns the acquired alpha. + Perform a subnet buyback (owner-only). + Stakes TAO into the subnet and immediately burns the acquired alpha. """ subnet_owner = await subtensor.query( module="SubtensorModule", From 82eeb3ae46c565ab09a38c5ebc502123b51ae3dd Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:58:50 -0800 Subject: [PATCH 081/113] update ck_swap test + improvs --- bittensor_cli/cli.py | 1 + bittensor_cli/src/commands/sudo.py | 4 ++-- tests/e2e_tests/test_coldkey_swap.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 592b56b22..7a83e25a9 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7397,6 +7397,7 @@ def sudo_subnet_buyback( "--amount", "-a", help="Amount of TAO to buyback", + prompt="Enter the amount of TAO to buyback", ), proxy: Optional[str] = Options.proxy, rate_tolerance: Optional[float] = Options.rate_tolerance, diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 4813150ef..c10327c4f 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -172,9 +172,9 @@ async def subnet_buyback( destination_netuid=netuid, amount=amount_minus_fee.rao, ) + received_amount = sim_swap.alpha_amount - - current_price_float = float(subnet_info.price.tao) + current_price_float = subnet_info.price.tao rate = 1.0 / current_price_float table = _define_buyback_table( diff --git a/tests/e2e_tests/test_coldkey_swap.py b/tests/e2e_tests/test_coldkey_swap.py index dfa423788..a528bf862 100644 --- a/tests/e2e_tests/test_coldkey_swap.py +++ b/tests/e2e_tests/test_coldkey_swap.py @@ -1,6 +1,6 @@ import asyncio import json - +import time from .utils import ( find_stake_entries, ) @@ -35,7 +35,7 @@ def test_coldkey_swap_with_stake(local_chain, wallet_setup): _, wallet_bob, path_bob, exec_command_bob = wallet_setup(wallet_path_bob) _, wallet_new, path_new, _ = wallet_setup(wallet_path_new) netuid = 2 - + time.sleep(5) # Create a new subnet by Bob create_sn = exec_command_bob( command="subnets", From 7f677b8034d7c107427dbc180dca6c24116b3cbd Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 16:59:14 -0800 Subject: [PATCH 082/113] ruff --- bittensor_cli/src/commands/sudo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index c10327c4f..e2f3c028a 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -117,7 +117,7 @@ async def subnet_buyback( period: int, ) -> bool: """ - Perform a subnet buyback (owner-only). + Perform a subnet buyback (owner-only). Stakes TAO into the subnet and immediately burns the acquired alpha. """ subnet_owner = await subtensor.query( @@ -172,7 +172,7 @@ async def subnet_buyback( destination_netuid=netuid, amount=amount_minus_fee.rao, ) - + received_amount = sim_swap.alpha_amount current_price_float = subnet_info.price.tao rate = 1.0 / current_price_float From 13de2e85ed0917b9e4c6981b7dee0ced0d7adcb2 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 17:11:20 -0800 Subject: [PATCH 083/113] fix test_coldkey_swap_dispute --- tests/e2e_tests/test_coldkey_swap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_tests/test_coldkey_swap.py b/tests/e2e_tests/test_coldkey_swap.py index a528bf862..7d56d3049 100644 --- a/tests/e2e_tests/test_coldkey_swap.py +++ b/tests/e2e_tests/test_coldkey_swap.py @@ -240,6 +240,7 @@ def test_coldkey_swap_dispute(local_chain, wallet_setup): _, wallet_bob, path_bob, exec_command_bob = wallet_setup(wallet_path_bob) _, wallet_new, _, _ = wallet_setup(wallet_path_new) + time.sleep(5) # Create subnet, start, and stake on it create_sn = exec_command_bob( command="subnets", From 7a41a098461d903383d2186cfe73d2296454d25a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 17:28:08 -0800 Subject: [PATCH 084/113] add waits --- tests/e2e_tests/test_coldkey_swap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_coldkey_swap.py b/tests/e2e_tests/test_coldkey_swap.py index 7d56d3049..0f7f977cd 100644 --- a/tests/e2e_tests/test_coldkey_swap.py +++ b/tests/e2e_tests/test_coldkey_swap.py @@ -35,7 +35,7 @@ def test_coldkey_swap_with_stake(local_chain, wallet_setup): _, wallet_bob, path_bob, exec_command_bob = wallet_setup(wallet_path_bob) _, wallet_new, path_new, _ = wallet_setup(wallet_path_new) netuid = 2 - time.sleep(5) + time.sleep(12) # Create a new subnet by Bob create_sn = exec_command_bob( command="subnets", @@ -240,7 +240,7 @@ def test_coldkey_swap_dispute(local_chain, wallet_setup): _, wallet_bob, path_bob, exec_command_bob = wallet_setup(wallet_path_bob) _, wallet_new, _, _ = wallet_setup(wallet_path_new) - time.sleep(5) + time.sleep(12) # Create subnet, start, and stake on it create_sn = exec_command_bob( command="subnets", From 7a94fda63faea6cb43b20b076f2fdf12afaaaf4e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 17:47:04 -0800 Subject: [PATCH 085/113] update nonce handling in add --- bittensor_cli/src/commands/stake/add.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index b9bff970c..424c44858 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -131,7 +131,7 @@ async def safe_stake_extrinsic( ) current_balance, next_nonce, call = await asyncio.gather( subtensor.get_balance(coldkey_ss58), - subtensor.substrate.get_account_next_index(coldkey_ss58), + subtensor.substrate.get_account_next_index(signer_ss58), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake_limit", @@ -229,7 +229,7 @@ async def stake_extrinsic( block_hash = await subtensor.substrate.get_chain_head() current_balance, next_nonce, call = await asyncio.gather( subtensor.get_balance(coldkey_ss58, block_hash=block_hash), - subtensor.substrate.get_account_next_index(coldkey_ss58), + subtensor.substrate.get_account_next_index(signer_ss58), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake", @@ -309,6 +309,7 @@ async def stake_extrinsic( netuids if netuids is not None else await subtensor.get_all_subnet_netuids() ) coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address + signer_ss58 = wallet.coldkeypub.ss58_address hotkeys_to_stake_to = _get_hotkeys_to_stake_to( wallet=wallet, From 9e55e54b73f544de8c99383ad46f9ee99e204dab Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 17:52:13 -0800 Subject: [PATCH 086/113] update nonce handling in remove cmds --- bittensor_cli/src/commands/stake/remove.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 608f627c3..a06dbf8bb 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -609,6 +609,7 @@ async def _unstake_extrinsic( f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}" ) coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address + signer_ss58 = wallet.coldkeypub.ss58_address if status: status.update( @@ -617,7 +618,7 @@ async def _unstake_extrinsic( current_balance, next_nonce, call = await asyncio.gather( subtensor.get_balance(coldkey_ss58), - subtensor.substrate.get_account_next_index(coldkey_ss58), + subtensor.substrate.get_account_next_index(signer_ss58), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake", @@ -714,6 +715,7 @@ async def _safe_unstake_extrinsic( f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}" ) coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address + signer_ss58 = wallet.coldkeypub.ss58_address if status: status.update( @@ -724,7 +726,7 @@ async def _safe_unstake_extrinsic( current_balance, next_nonce, current_stake, call = await asyncio.gather( subtensor.get_balance(coldkey_ss58, block_hash), - subtensor.substrate.get_account_next_index(coldkey_ss58), + subtensor.substrate.get_account_next_index(signer_ss58), subtensor.get_stake( hotkey_ss58=hotkey_ss58, coldkey_ss58=coldkey_ss58, @@ -836,6 +838,7 @@ async def _unstake_all_extrinsic( f":cross_mark: [red]Failed[/red] to unstake all from {hotkey_name}" ) coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address + signer_ss58 = wallet.coldkeypub.ss58_address if status: status.update( @@ -866,7 +869,7 @@ async def _unstake_all_extrinsic( call_function=call_function, call_params={"hotkey": hotkey_ss58}, ), - subtensor.substrate.get_account_next_index(coldkey_ss58), + subtensor.substrate.get_account_next_index(signer_ss58), ) try: success_, err_msg, response = await subtensor.sign_and_send_extrinsic( From 49c1651b958b890843682af8e700be9c1dbe651d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 17:54:14 -0800 Subject: [PATCH 087/113] update nonce in movement --- bittensor_cli/src/commands/stake/move.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 69934f53c..2e83e78cd 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -864,9 +864,7 @@ async def transfer_stake( amount=amount_to_transfer.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), - subtensor.substrate.get_account_next_index( - proxy or wallet.coldkeypub.ss58_address - ), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) # Display stake movement details @@ -1094,9 +1092,7 @@ async def swap_stake( amount=amount_to_swap.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), - subtensor.substrate.get_account_next_index( - proxy or wallet.coldkeypub.ss58_address - ), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) # Display stake movement details From 85704678d9652ab4d76b1b3de6c7c164dd8907f0 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 3 Feb 2026 18:04:43 -0800 Subject: [PATCH 088/113] handle proxy acc when querying balance in st add --- bittensor_cli/cli.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index dadf57eb9..fef389710 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4909,12 +4909,21 @@ def stake_add( # TODO: Ask amount for each subnet explicitly if more than one if not stake_all and not amount: - free_balance = self._run_command( - wallets.wallet_balance( - wallet, self.initialize_chain(network), False, None - ), - exit_early=False, - ) + staker_ss58 = proxy or wallet.coldkeypub.ss58_address + if proxy: + free_balance = self._run_command( + wallets.wallet_balance( + None, self.initialize_chain(network), False, [staker_ss58] + ), + exit_early=False, + ) + else: + free_balance = self._run_command( + wallets.wallet_balance( + wallet, self.initialize_chain(network), False, None + ), + exit_early=False, + ) logger.debug(f"Free balance: {free_balance}") if free_balance == Balance.from_tao(0): print_error("You dont have any balance to stake.") From aad35fe7c0b4d26dbc30b5fdf743cfcc6aa7ec52 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 4 Feb 2026 10:05:33 -0800 Subject: [PATCH 089/113] use signer's coldkey in registering subnets --- bittensor_cli/src/commands/subnets/subnets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index dbf7a264a..f627de7fb 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -239,7 +239,7 @@ async def _find_event_attributes_in_extrinsic_receipt( if not unlock_key(wallet).success: return False, None, None - coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address + coldkey_ss58 = wallet.coldkeypub.ss58_address with console.status(":satellite: Registering subnet...", spinner="earth") as status: substrate = subtensor.substrate From cf3f56b87bc3bbdee62a50596cc6957b8da0a014 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 4 Feb 2026 10:05:46 -0800 Subject: [PATCH 090/113] update wording --- bittensor_cli/src/commands/crowd/create.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 2ce1dc9df..2195ecda9 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -424,7 +424,7 @@ async def create_crowdloan( table.add_row( "Estimated fee", f"[{COLORS.P.TAO}]{extrinsic_fee}[/{COLORS.P.TAO}]" - + (" (paid by real account)" if proxy else ""), + + (" (paid by signer account)" if proxy else ""), ) console.print(table) @@ -678,7 +678,7 @@ async def finalize_crowdloan( table.add_row( "Transaction Fee", f"[{COLORS.S.TAO}]{extrinsic_fee.tao}[/{COLORS.S.TAO}]" - + (" (paid by real account)" if proxy else ""), + + (" (paid by signer account)" if proxy else ""), ) table.add_section() From ef4ccaecfa7711f641a15680713a3e814c683a4d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 4 Feb 2026 10:06:14 -0800 Subject: [PATCH 091/113] fetch stakes for proxied account --- bittensor_cli/src/commands/stake/claim.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 2f225f135..860a52dd7 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -244,11 +244,12 @@ async def process_pending_claims( ) -> tuple[bool, str, Optional[str]]: """Claims root network emissions for the coldkey across specified subnets""" + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address with console.status(":satellite: Discovering claimable emissions..."): block_hash = await subtensor.substrate.get_chain_head() all_stakes, identities = await asyncio.gather( subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=block_hash + coldkey_ss58=coldkey_ss58, block_hash=block_hash ), subtensor.query_all_identities(block_hash=block_hash), ) @@ -272,7 +273,7 @@ async def process_pending_claims( (stake.hotkey_ss58, stake.netuid): stake for stake in all_stakes } claimable_by_hotkey = await subtensor.get_claimable_stakes_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, stakes_info=all_stakes, block_hash=block_hash, ) @@ -344,7 +345,7 @@ async def process_pending_claims( ) console.print( f"\n[dim]Estimated extrinsic fee: {extrinsic_fee.tao:.9f} τ" - + (" (paid by real account)" if proxy else "") + + (" (paid by signer account)" if proxy else "") ) if prompt: From fa265d3de9d722545fe93c4eabac2128297acb13 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 4 Feb 2026 15:38:21 -0800 Subject: [PATCH 092/113] adds help aliases --- bittensor_cli/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index fef389710..a78c05b8e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -852,6 +852,7 @@ def __init__(self): callback=self.main_callback, epilog=_epilog, no_args_is_help=True, + context_settings={"help_option_names": ["-h", "--help"]}, ) self.config_app = typer.Typer( epilog=_epilog, From b7f512fb55888973a867371f82fea51d0761d58a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 9 Feb 2026 14:02:41 -0800 Subject: [PATCH 093/113] update cmd -> stake-burn --- bittensor_cli/cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7a83e25a9..26b9fc876 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1164,8 +1164,8 @@ def __init__(self): self.sudo_app.command("trim", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( self.sudo_trim ) - self.sudo_app.command("buyback", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( - self.sudo_subnet_buyback + self.sudo_app.command("stake-burn", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( + self.sudo_stake_burn ) # subnets commands @@ -1301,6 +1301,7 @@ def __init__(self): self.sudo_app.command("senate_vote", hidden=True)(self.sudo_senate_vote) self.sudo_app.command("get_take", hidden=True)(self.sudo_get_take) self.sudo_app.command("set_take", hidden=True)(self.sudo_set_take) + self.sudo_app.command("buyback", hidden=True)(self.sudo_stake_burn) # Stake self.stake_app.command( @@ -7385,7 +7386,7 @@ def sudo_trim( ) ) - def sudo_subnet_buyback( + def sudo_stake_burn( self, network: Optional[list[str]] = Options.network, wallet_name: Optional[str] = Options.wallet_name, @@ -7468,7 +7469,7 @@ def sudo_subnet_buyback( raise typer.Exit() self._run_command( - sudo.subnet_buyback( + sudo.stake_burn( subtensor=self.initialize_chain(network), wallet=wallet, netuid=netuid, From f77b0d9a1eec1c0953cb0e039c30224bd93bb68f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 9 Feb 2026 14:03:22 -0800 Subject: [PATCH 094/113] rename sudo cmd --- bittensor_cli/src/commands/sudo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index e2f3c028a..3f1c04e54 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -100,7 +100,7 @@ def string_to_bool(val) -> Union[bool, Type[ValueError]]: return ValueError -async def subnet_buyback( +async def stake_burn( wallet: Wallet, subtensor: "SubtensorInterface", netuid: int, @@ -117,7 +117,7 @@ async def subnet_buyback( period: int, ) -> bool: """ - Perform a subnet buyback (owner-only). + Perform a stake burn (owner-only). Stakes TAO into the subnet and immediately burns the acquired alpha. """ subnet_owner = await subtensor.query( @@ -158,7 +158,7 @@ async def subnet_buyback( call = await subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="subnet_buyback", + call_function="add_stake_burn", call_params=call_params, ) From d52d5ee6db01d9b98e0ea6eed2b7050050f09db8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 9 Feb 2026 14:03:45 -0800 Subject: [PATCH 095/113] update e2e --- .../{test_buyback.py => test_stake_burn.py} | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) rename tests/e2e_tests/{test_buyback.py => test_stake_burn.py} (85%) diff --git a/tests/e2e_tests/test_buyback.py b/tests/e2e_tests/test_stake_burn.py similarity index 85% rename from tests/e2e_tests/test_buyback.py rename to tests/e2e_tests/test_stake_burn.py index 7112899ce..2e2858e16 100644 --- a/tests/e2e_tests/test_buyback.py +++ b/tests/e2e_tests/test_stake_burn.py @@ -7,18 +7,18 @@ @pytest.mark.parametrize("local_chain", [False], indirect=True) -def test_subnet_buyback(local_chain, wallet_setup): +def test_stake_burn(local_chain, wallet_setup): """ - Test subnet buyback + Test stake burn 1. Create a subnet 2. Start the subnet's emission schedule - 3. Buyback the subnet + 3. Buyback the subnet (stake burn) 3. Check the balance before and after the buyback upon success 4. Try to buyback again and expect it to fail due to rate limit """ _, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup("//Alice") - time.sleep(2) + time.sleep(12) netuid = 2 result = exec_command_alice( command="subnets", @@ -94,11 +94,11 @@ def test_subnet_buyback(local_chain, wallet_setup): _balance_before.stdout, wallet_alice.name, wallet_alice.coldkey.ss58_address )["free_balance"] - # First buyback + # First stake burn amount_tao = 5.0 - buyback_result = exec_command_alice( + stake_burn_result = exec_command_alice( "sudo", - "buyback", + "stake-burn", extra_args=[ "--wallet-path", wallet_path_alice, @@ -116,10 +116,10 @@ def test_subnet_buyback(local_chain, wallet_setup): "--json-output", ], ) - buyback_ok_out = json.loads(buyback_result.stdout) - assert buyback_ok_out["success"] is True, buyback_result.stdout + stale_burn_ok_out = json.loads(stake_burn_result.stdout) + assert stale_burn_ok_out["success"] is True, stake_burn_result.stdout - # Balance after buyback + # Balance after stake burn _balance_after = exec_command_alice( "wallet", "balance", @@ -138,7 +138,7 @@ def test_subnet_buyback(local_chain, wallet_setup): assert balance_after < balance_before, (balance_before, balance_after) # Should fail due to rate limit - buyback_ratelimited_result = exec_command_alice( + stake_burn_ratelimited_result = exec_command_alice( "sudo", "buyback", extra_args=[ @@ -158,6 +158,6 @@ def test_subnet_buyback(local_chain, wallet_setup): "--json-output", ], ) - buyback_ratelimited = json.loads(buyback_ratelimited_result.stdout) - assert buyback_ratelimited["success"] is False, buyback_ratelimited_result.stdout - assert "SubnetBuybackRateLimitExceeded" in buyback_ratelimited["message"] + stake_burn_ratelimited = json.loads(stake_burn_ratelimited_result.stdout) + assert stake_burn_ratelimited["success"] is False, stake_burn_ratelimited_result.stdout + assert "AddStakeBurnRateLimitExceeded" in stake_burn_ratelimited["message"] From be7e9032471d775e7c6ae6d581af121643c07baf Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 9 Feb 2026 14:10:28 -0800 Subject: [PATCH 096/113] update help text --- bittensor_cli/cli.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 26b9fc876..39f5e3121 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1164,9 +1164,9 @@ def __init__(self): self.sudo_app.command("trim", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( self.sudo_trim ) - self.sudo_app.command("stake-burn", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( - self.sudo_stake_burn - ) + self.sudo_app.command( + "stake-burn", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"] + )(self.sudo_stake_burn) # subnets commands self.subnets_app.command( @@ -7397,8 +7397,8 @@ def sudo_stake_burn( None, "--amount", "-a", - help="Amount of TAO to buyback", - prompt="Enter the amount of TAO to buyback", + help="Amount of TAO to stake and burn", + prompt="Enter the amount of TAO to stake and burn", ), proxy: Optional[str] = Options.proxy, rate_tolerance: Optional[float] = Options.rate_tolerance, @@ -7415,12 +7415,12 @@ def sudo_stake_burn( Allows subnet owners to buy back alpha on their subnet by staking TAO and immediately burning the acquired alpha. [bold]Examples:[/bold] - 1. Buyback 10 TAO on subnet 14: - [green]$[/green] btcli sudo buyback --netuid 14 --amount 10 - 2. Buyback 10 TAO on subnet 14 with safe staking and 5% rate tolerance: - [green]$[/green] btcli sudo buyback --netuid 14 --amount 10 --tolerance 0.05 - 3. Buyback 10 TAO on subnet 14 with a specific hotkey: - [green]$[/green] btcli sudo buyback --netuid 14 --amount 10 --wallet-hotkey + 1. Stake and burn 10 TAO on subnet 14: + [green]$[/green] btcli sudo stake-burn --netuid 14 --amount 10 + 2. Stake and burn 10 TAO on subnet 14 with safe staking and 5% rate tolerance: + [green]$[/green] btcli sudo stake-burn --netuid 14 --amount 10 --tolerance 0.05 + 3. Stake and burn 10 TAO on subnet 14 with a specific hotkey: + [green]$[/green] btcli sudo stake-burn --netuid 14 --amount 10 --wallet-hotkey """ self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only=False) @@ -7431,13 +7431,13 @@ def sudo_stake_burn( print_protection_warnings( mev_protection=mev_protection, safe_staking=safe_staking, - command_name="sudo buyback", + command_name="sudo stake-burn", ) if not wallet_hotkey: wallet_hotkey = Prompt.ask( "Enter the [blue]hotkey[/blue] name or " - "[blue]hotkey ss58 address[/blue] [dim](to use for the buyback)[/dim]", + "[blue]hotkey ss58 address[/blue] [dim](to use for the stake burn)[/dim]", default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, ) @@ -7461,11 +7461,11 @@ def sudo_stake_burn( hotkey_ss58 = get_hotkey_pub_ss58(wallet) if amount <= 0: - print_error(f"You entered an incorrect buyback amount: {amount}") + print_error(f"You entered an incorrect stake and burn amount: {amount}") raise typer.Exit() if netuid == 0: - print_error("Cannot buyback on the root subnet.") + print_error("Cannot stake and burn on the root subnet.") raise typer.Exit() self._run_command( From c3dbf896381ca19103c6eb3ecd18935718bc88ae Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 9 Feb 2026 14:10:46 -0800 Subject: [PATCH 097/113] update cmd feedbacks --- bittensor_cli/src/commands/sudo.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 3f1c04e54..2ea78c137 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -142,7 +142,7 @@ async def stake_burn( return False subnet_info = await subtensor.subnet(netuid=netuid) - buyback_amount = Balance.from_tao(amount) + stake_burn_amount = Balance.from_tao(amount) rate_tolerance = rate_tolerance if rate_tolerance is not None else 0.0 price_limit: Optional[Balance] = None @@ -152,7 +152,7 @@ async def stake_burn( call_params = { "hotkey": hotkey_ss58, "netuid": netuid, - "amount": buyback_amount.rao, + "amount": stake_burn_amount.rao, "limit": price_limit.rao if price_limit else None, } @@ -166,7 +166,7 @@ async def stake_burn( extrinsic_fee = await subtensor.get_extrinsic_fee( call, wallet.coldkeypub, proxy=proxy ) - amount_minus_fee = buyback_amount - extrinsic_fee + amount_minus_fee = stake_burn_amount - extrinsic_fee sim_swap = await subtensor.sim_swap( origin_netuid=0, destination_netuid=netuid, @@ -177,7 +177,7 @@ async def stake_burn( current_price_float = subnet_info.price.tao rate = 1.0 / current_price_float - table = _define_buyback_table( + table = _define_stake_burn_table( wallet=wallet, subtensor=subtensor, safe_staking=safe_staking, @@ -186,7 +186,7 @@ async def stake_burn( row = [ str(netuid), hotkey_ss58, - str(buyback_amount), + str(stake_burn_amount), str(rate) + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", str(received_amount.set_unit(netuid)), str(sim_swap.tao_fee), @@ -214,7 +214,7 @@ async def stake_burn( return False with console.status( - f":satellite: Performing subnet buyback on [bold]{netuid}[/bold]...", + f":satellite: Performing subnet stake burn on [bold]{netuid}[/bold]...", spinner="earth", ) as status: next_nonce = await subtensor.substrate.get_account_next_index( @@ -268,7 +268,7 @@ async def stake_burn( ext_id = await ext_receipt.get_extrinsic_identifier() - msg = f"Subnet buyback succeeded on SN{netuid}." + msg = f"Subnet stake burn succeeded on SN{netuid}." if json_output: json_console.print_json( data={"success": True, "message": msg, "extrinsic_identifier": ext_id} @@ -280,7 +280,7 @@ async def stake_burn( return True -def _define_buyback_table( +def _define_stake_burn_table( wallet: Wallet, subtensor: "SubtensorInterface", safe_staking: bool, From fb837e2a0112700ef3784c4dfde2caf739023b2c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 9 Feb 2026 14:12:17 -0800 Subject: [PATCH 098/113] ruff --- tests/e2e_tests/test_stake_burn.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_stake_burn.py b/tests/e2e_tests/test_stake_burn.py index 2e2858e16..c6694fa5d 100644 --- a/tests/e2e_tests/test_stake_burn.py +++ b/tests/e2e_tests/test_stake_burn.py @@ -159,5 +159,7 @@ def test_stake_burn(local_chain, wallet_setup): ], ) stake_burn_ratelimited = json.loads(stake_burn_ratelimited_result.stdout) - assert stake_burn_ratelimited["success"] is False, stake_burn_ratelimited_result.stdout + assert stake_burn_ratelimited["success"] is False, ( + stake_burn_ratelimited_result.stdout + ) assert "AddStakeBurnRateLimitExceeded" in stake_burn_ratelimited["message"] From 790e180cb35d1b9aa9c40b850ad51726c179b486 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 9 Feb 2026 15:01:23 -0800 Subject: [PATCH 099/113] add tao & alpha slippage --- bittensor_cli/src/bittensor/chain_data.py | 84 ++++------------------- 1 file changed, 14 insertions(+), 70 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index af2b4711b..e33b141ea 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1136,6 +1136,8 @@ class SimSwapResult: alpha_amount: Balance tao_fee: Balance alpha_fee: Balance + tao_slippage: Balance + alpha_slippage: Balance @classmethod def from_dict(cls, d: dict, netuid: int) -> "SimSwapResult": @@ -1144,79 +1146,21 @@ def from_dict(cls, d: dict, netuid: int) -> "SimSwapResult": alpha_amount=Balance.from_rao(d["alpha_amount"]).set_unit(netuid), tao_fee=Balance.from_rao(d["tao_fee"]).set_unit(0), alpha_fee=Balance.from_rao(d["alpha_fee"]).set_unit(netuid), + tao_slippage=Balance.from_rao(d["tao_slippage"]).set_unit(0), + alpha_slippage=Balance.from_rao(d["alpha_slippage"]).set_unit(netuid), ) - def tao_to_alpha_slippage( - self, - tao_amount: Balance, - current_price: float, - netuid: int, - ) -> tuple[Balance, Balance, float]: - """ - Calculate slippage for a TAO -> Alpha swap. - - Args: - tao_amount: Amount of TAO provided as input. - current_price: Current alpha price in TAO (TAO per 1 alpha). - netuid: Target subnet netuid (used for unit tagging). - - Returns: - A tuple of: - received_alpha (Balance): Simulated alpha received. - slippage_alpha (Balance): Shortfall vs ideal at current_price. - slippage_pct_float (float): Slippage percentage (0 - 100). - """ - if current_price <= 0: - zero = Balance.from_tao(0).set_unit(netuid) - return zero, zero, 0.0 - - ideal_amount = Balance.from_tao(tao_amount.tao / current_price).set_unit(netuid) - received_amount = self.alpha_amount - - if ideal_amount.tao == 0: - zero = Balance.from_tao(0).set_unit(netuid) - return received_amount, zero, 0.0 - - slippage_amount = max(ideal_amount.tao - received_amount.tao, 0) - slippage_amount_balance = Balance.from_tao(slippage_amount).set_unit(netuid) - slippage_pct = 100 * slippage_amount / ideal_amount.tao - - return received_amount, slippage_amount_balance, slippage_pct - - def alpha_to_tao_slippage( - self, - alpha_amount: Balance, - current_price: float, - ) -> tuple[Balance, Balance, float]: - """ - Calculate slippage for an Alpha -> TAO swap. - - Args: - alpha_amount: Amount of Alpha provided as input. - current_price: Current alpha price in TAO (TAO per 1 alpha). - - Returns: - A tuple of: - received_tao (Balance): Simulated TAO received. - slippage_tao (Balance): Shortfall vs ideal at current_price. - slippage_pct_float (float): Slippage percentage (0 - 100). - """ - if current_price <= 0: - zero = Balance.from_tao(0).set_unit(0) - return zero, zero, 0.0 - - ideal_amount = Balance.from_tao(alpha_amount.tao * current_price).set_unit(0) - received_amount = self.tao_amount - - if ideal_amount.tao == 0: - zero = Balance.from_tao(0).set_unit(0) - return received_amount, zero, 0.0 - - slippage_amount = max(ideal_amount.tao - received_amount.tao, 0) - slippage_amount_balance = Balance.from_tao(slippage_amount).set_unit(0) - slippage_pct = 100 * slippage_amount / ideal_amount.tao + @property + def tao_slippage_pct(self) -> float: + """Slippage percentage for alpha->tao swaps.""" + ideal = self.tao_amount.tao + self.tao_slippage.tao + return (100.0 * self.tao_slippage.tao / ideal) if ideal > 0 else 0.0 - return received_amount, slippage_amount_balance, slippage_pct + @property + def alpha_slippage_pct(self) -> float: + """Slippage percentage for tao->alpha swaps.""" + ideal = self.alpha_amount.tao + self.alpha_slippage.tao + return (100.0 * self.alpha_slippage.tao / ideal) if ideal > 0 else 0.0 @dataclass From 3000801bc6245ddd788ebfe004abdf5b4420a00e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 9 Feb 2026 17:19:10 -0800 Subject: [PATCH 100/113] update get_subnet_prices --- .../src/bittensor/subtensor_interface.py | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 6719daf79..7eb93aa7c 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1794,6 +1794,7 @@ async def sim_swap( ) secondary_fee = (result.tao_fee / sn_price.tao).set_unit(origin_netuid) result.alpha_fee = result.alpha_fee + secondary_fee + result.tao_slippage = intermediate_result.tao_slippage return result elif origin_netuid > 0: # dynamic to tao @@ -2556,24 +2557,14 @@ async def get_subnet_prices( :return: A dictionary mapping netuid to the current Alpha price in TAO units. """ - all_netuids = await self.get_all_subnet_netuids(block_hash=block_hash) - - result = {0: Balance.from_tao(1.0)} - netuids_to_query = [netuid for netuid in all_netuids if netuid != 0] - prices = await asyncio.gather( - *[ - self.query_runtime_api( - "SwapRuntimeApi", - "current_alpha_price", - params={"netuid": netuid}, - block_hash=block_hash, - ) - for netuid in netuids_to_query - ], + all_prices = await self.query_runtime_api( + "SwapRuntimeApi", + "current_alpha_price_all", + block_hash=block_hash, ) - for netuid, current_price in zip(netuids_to_query, prices): - result[netuid] = Balance.from_rao(current_price) - + result = {} + for entry in all_prices: + result[entry["netuid"]] = Balance.from_rao(entry["price"]) return result async def get_all_subnet_ema_tao_inflow( From 958e04fa2516bca7028487671dddecc7e532b2dc Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 9 Feb 2026 17:19:30 -0800 Subject: [PATCH 101/113] update slippage % calcs --- bittensor_cli/src/commands/stake/add.py | 6 +----- bittensor_cli/src/commands/stake/remove.py | 10 ++-------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index a024d5607..e57b3924b 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -433,11 +433,7 @@ async def stake_extrinsic( ) received_amount = sim_swap.alpha_amount - _, _, slippage_pct_float = sim_swap.tao_to_alpha_slippage( - tao_amount=amount_minus_fee, - current_price=current_price_float, - netuid=netuid, - ) + slippage_pct_float = sim_swap.alpha_slippage_pct slippage_pct = f"{slippage_pct_float:.4f} %" max_slippage = max(slippage_pct_float, max_slippage) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 7d9724d5d..bc1e31c1a 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -251,10 +251,7 @@ async def unstake( if not proxy: received_amount -= extrinsic_fee - _, _, slippage_pct_float = sim_swap.alpha_to_tao_slippage( - alpha_amount=amount_to_unstake_as_balance, - current_price=current_price, - ) + slippage_pct_float = sim_swap.tao_slippage_pct slippage_pct = f"{slippage_pct_float:.4f} %" max_float_slippage = max(slippage_pct_float, max_float_slippage) except ValueError: @@ -533,10 +530,7 @@ async def unstake_all( print_error("Not enough Alpha to pay the transaction fee.") continue - _, _, slippage_pct_float = sim_swap.alpha_to_tao_slippage( - alpha_amount=stake_amount, - current_price=current_price, - ) + slippage_pct_float = sim_swap.tao_slippage_pct slippage_pct = f"{slippage_pct_float:.4f} %" max_float_slippage = max(slippage_pct_float, max_float_slippage) except (AttributeError, ValueError): From d3ddbed10e72b2118ee553e25b13f0897875fbb9 Mon Sep 17 00:00:00 2001 From: Roman Chkhaidze Date: Wed, 11 Feb 2026 14:22:53 -0800 Subject: [PATCH 102/113] avoid to use `typing_extensions` replace with `typing` --- 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 39f5e3121..704c139da 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -32,7 +32,7 @@ from rich.prompt import FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table from rich.tree import Tree -from typing_extensions import Annotated +from typing import Annotated from yaml import safe_dump, safe_load from bittensor_cli.src import ( From f8f78c89681a361953aaad5bb7e0bd7b578161b3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 12 Feb 2026 10:08:08 -0800 Subject: [PATCH 103/113] update e2e --- tests/e2e_tests/test_staking_sudo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index e76ff1627..0e0cc65a4 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -297,8 +297,8 @@ def test_staking(local_chain, wallet_setup): assert str(netuid) in get_s_price_output.keys() stats = get_s_price_output[str(netuid)]["stats"] assert stats["name"] == sn_name - assert stats["current_price"] == 0.0 - assert stats["market_cap"] == 0.0 + assert stats["current_price"] == 1 + assert stats["market_cap"] == 1_000 # Start emissions on SNs for netuid_ in multiple_netuids: From 4b4ff4052f788ee3cb16abf3f45769892f63654c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 13 Feb 2026 10:56:00 -0800 Subject: [PATCH 104/113] replace typing_extensions -> typing --- 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 c56926a36..6b9c3aa6d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -32,7 +32,7 @@ from rich.prompt import FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table from rich.tree import Tree -from typing_extensions import Annotated +from typing import Annotated from yaml import safe_dump, safe_load from bittensor_cli.src import ( From df789e3a489c12684f4600250ecb2025803d0781 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Wed, 18 Feb 2026 14:20:51 +0200 Subject: [PATCH 105/113] Handle `MechId` type in call crafter, and raise appropriate error if type not handleable --- bittensor_cli/src/commands/sudo.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index ec6d1461c..deb2aa04b 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -126,8 +126,19 @@ def type_converter_with_retry(type_, val, arg_name): return arg_types[type_](val) except ValueError: return type_converter_with_retry(type_, None, arg_name) + except KeyError: + print_error( + f"Type {type_} is not recognized. " + "You will be unable to set this parameter via this command.\n" + "Some hyperparams must be set by their dedicated command, such as `btcli subnets mech`" + ) - arg_types = {"bool": string_to_bool, "u16": string_to_u16, "u64": string_to_u64} + arg_types = { + "bool": string_to_bool, + "u16": string_to_u16, + "u64": string_to_u64, + "MechId": int, + } arg_type_output = {"bool": "bool", "u16": "float", "u64": "float"} call_crafter = {"netuid": netuid} From 2908de68ffac6b2b62c516d5a751a38e371b96fe Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 19 Feb 2026 17:07:33 -0800 Subject: [PATCH 106/113] remove 3.9 support --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0cfe2953b..3b1ad3b62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,12 +12,11 @@ authors = [ ] license = "MIT" scripts = { btcli = "bittensor_cli.cli:main" } -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: End Users/Desktop", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", From 2bf2941db343aa1a1ec8d4a503a9e80833256baf Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 19 Feb 2026 17:07:48 -0800 Subject: [PATCH 107/113] update workflows --- .github/workflows/e2e-subtensor-tests.yml | 2 +- .github/workflows/ruff-formatter.yml | 2 +- .github/workflows/unit-tests.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index fcd7ddf53..b1527f857 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -163,7 +163,7 @@ jobs: os: - ubuntu-latest test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Check-out repository uses: actions/checkout@v4 diff --git a/.github/workflows/ruff-formatter.yml b/.github/workflows/ruff-formatter.yml index 93166c304..20fe10625 100644 --- a/.github/workflows/ruff-formatter.yml +++ b/.github/workflows/ruff-formatter.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9.13"] + python-version: ["3.10.0"] steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 4e5d34e41..4e96f29b6 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -27,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Check-out repository uses: actions/checkout@v4 From e69c70a4598ce3fca57e1f1a81db7601f2ff5d51 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 19 Feb 2026 17:10:14 -0800 Subject: [PATCH 108/113] update ruff workflow --- .github/workflows/ruff-formatter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruff-formatter.yml b/.github/workflows/ruff-formatter.yml index 20fe10625..52e7e3591 100644 --- a/.github/workflows/ruff-formatter.yml +++ b/.github/workflows/ruff-formatter.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10.0"] + python-version: ["3.10"] steps: - name: Checkout repository uses: actions/checkout@v4 From 75653d3c79bc1a9c1e2475f15d4274f6ffee0f77 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Sun, 22 Feb 2026 12:09:58 +0200 Subject: [PATCH 109/113] Optimises the workflow for ruff. --- .github/workflows/ruff-formatter.yml | 37 +++++++++++----------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ruff-formatter.yml b/.github/workflows/ruff-formatter.yml index 52e7e3591..1b9935113 100644 --- a/.github/workflows/ruff-formatter.yml +++ b/.github/workflows/ruff-formatter.yml @@ -1,42 +1,33 @@ name: Ruff Formatter Check + +concurrency: + group: ruff-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + permissions: contents: read on: pull_request: - types: [opened, synchronize, reopened, edited] + types: [opened, synchronize, reopened] jobs: ruff: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10"] + timeout-minutes: 10 steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Install uv + uses: astral-sh/setup-uv@v4 with: - python-version: ${{ matrix.python-version }} + python-version: "3.13" - - name: Set up caching for Ruff virtual environment - id: cache-ruff - uses: actions/cache@v4 - with: - path: .venv - key: v2-pypi-py-ruff-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} - restore-keys: | - v2-pypi-py-ruff-${{ matrix.python-version }}- - - - name: Set up Ruff virtual environment if cache is missed - if: steps.cache-ruff.outputs.cache-hit != 'true' - run: | - python -m venv .venv - .venv/bin/python -m pip install ruff==0.11.5 + - name: Install dependencies + run: uv sync --all-extras - name: Ruff format check run: | - .venv/bin/ruff format --diff bittensor_cli - .venv/bin/ruff format --diff tests + uv run ruff format --diff bittensor_cli + uv run ruff format --diff tests From 251cf73421e855aebcf0f1a36ba6e8c57b0c8db2 Mon Sep 17 00:00:00 2001 From: BD Himes Date: Sun, 22 Feb 2026 13:17:00 +0200 Subject: [PATCH 110/113] Optimises the workflow for ruff. --- .github/workflows/ruff-formatter.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ruff-formatter.yml b/.github/workflows/ruff-formatter.yml index 1b9935113..92d7c30c4 100644 --- a/.github/workflows/ruff-formatter.yml +++ b/.github/workflows/ruff-formatter.yml @@ -19,15 +19,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - python-version: "3.13" - - - name: Install dependencies - run: uv sync --all-extras - - name: Ruff format check - run: | - uv run ruff format --diff bittensor_cli - uv run ruff format --diff tests + uses: astral-sh/ruff-action@v3 + with: + version: "0.11.5" + args: "format --diff" + src: "bittensor_cli tests" From 4d8398127301f25fdf1d93a59b1659249eb3e078 Mon Sep 17 00:00:00 2001 From: MkDev11 <94194147+MkDev11@users.noreply.github.com> Date: Mon, 23 Feb 2026 03:58:19 -0800 Subject: [PATCH 111/113] Merge pull request #827 from MkDev11/feat/add-hyperparams-826 feat: Add hyperparams: sudo_set_sn_owner_hotkey, sudo_set_subnet_owner_hotkey, sudo_set_recycle_or_burn --- bittensor_cli/src/__init__.py | 21 +++++++++++++++ tests/unit_tests/test_cli.py | 35 ++++++++++++++++++++++++ tests/unit_tests/test_hyperparams.py | 40 ++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 tests/unit_tests/test_hyperparams.py diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index e4de4da53..bbbbb3151 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -683,6 +683,9 @@ class RootSudoOnly(Enum): "bonds_reset_enabled": ("sudo_set_bonds_reset_enabled", RootSudoOnly.FALSE), "transfers_enabled": ("sudo_set_toggle_transfer", RootSudoOnly.FALSE), "min_allowed_uids": ("sudo_set_min_allowed_uids", RootSudoOnly.TRUE), + "sn_owner_hotkey": ("sudo_set_sn_owner_hotkey", RootSudoOnly.FALSE), + "subnet_owner_hotkey": ("sudo_set_sn_owner_hotkey", RootSudoOnly.FALSE), + "recycle_or_burn": ("sudo_set_recycle_or_burn", RootSudoOnly.FALSE), # Note: These are displayed but not directly settable via HYPERPARAMS # They are derived or set via other mechanisms "alpha_high": ("", RootSudoOnly.FALSE), # Derived from alpha_values @@ -895,6 +898,24 @@ class RootSudoOnly(Enum): "owner_settable": False, "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters#minalloweduids", }, + "sn_owner_hotkey": { + "description": "Set the subnet owner hotkey.", + "side_effects": "Changes which hotkey is authorized as subnet owner for the given subnet.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters", + }, + "subnet_owner_hotkey": { + "description": "Alias for sn_owner_hotkey; sets the subnet owner hotkey.", + "side_effects": "Same as sn_owner_hotkey.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters", + }, + "recycle_or_burn": { + "description": "Set whether subnet TAO is recycled or burned.", + "side_effects": "Controls whether unstaked TAO is recycled back into the subnet or burned.", + "owner_settable": True, + "docs_link": "docs.learnbittensor.org/subnets/subnet-hyperparameters", + }, # Additional hyperparameters that appear in chain data but aren't directly settable via HYPERPARAMS "alpha_high": { "description": "High bound of the alpha range for stake calculations.", diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index a793ac0c0..fbd4130db 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -4,6 +4,7 @@ from async_substrate_interface import AsyncSubstrateInterface from bittensor_cli.cli import parse_mnemonic, CLIManager +from bittensor_cli.src import HYPERPARAMS, HYPERPARAMS_METADATA, RootSudoOnly from bittensor_cli.src.bittensor.extrinsics.root import ( get_current_weights_for_uid, set_root_weights_extrinsic, @@ -765,3 +766,37 @@ async def test_set_root_weights_skips_current_weights_without_prompt(): ) mock_get_current.assert_not_called() + + +# HYPERPARAMS / HYPERPARAMS_METADATA (issue #826) +NEW_HYPERPARAMS_826 = {"sn_owner_hotkey", "subnet_owner_hotkey", "recycle_or_burn"} + + +def test_new_hyperparams_in_hyperparams(): + for key in NEW_HYPERPARAMS_826: + assert key in HYPERPARAMS, f"{key} should be in HYPERPARAMS" + extrinsic, root_only = HYPERPARAMS[key] + assert extrinsic, f"{key} must have non-empty extrinsic name" + assert root_only is RootSudoOnly.FALSE + + +def test_subnet_owner_hotkey_alias_maps_to_same_extrinsic(): + ext_sn, _ = HYPERPARAMS["sn_owner_hotkey"] + ext_subnet, _ = HYPERPARAMS["subnet_owner_hotkey"] + assert ext_sn == ext_subnet == "sudo_set_sn_owner_hotkey" + + +def test_new_hyperparams_have_metadata(): + required = {"description", "side_effects", "owner_settable", "docs_link"} + for key in NEW_HYPERPARAMS_826: + assert key in HYPERPARAMS_METADATA, f"{key} should be in HYPERPARAMS_METADATA" + meta = HYPERPARAMS_METADATA[key] + for field in required: + assert field in meta, f"{key} metadata missing '{field}'" + assert isinstance(meta["description"], str) + assert isinstance(meta["owner_settable"], bool) + + +def test_new_hyperparams_owner_settable_true(): + for key in NEW_HYPERPARAMS_826: + assert HYPERPARAMS_METADATA[key]["owner_settable"] is True diff --git a/tests/unit_tests/test_hyperparams.py b/tests/unit_tests/test_hyperparams.py new file mode 100644 index 000000000..8f6b42647 --- /dev/null +++ b/tests/unit_tests/test_hyperparams.py @@ -0,0 +1,40 @@ +"""Unit tests for HYPERPARAMS and HYPERPARAMS_METADATA (issue #826).""" + +from bittensor_cli.src import HYPERPARAMS, HYPERPARAMS_METADATA, RootSudoOnly + + +NEW_HYPERPARAMS_826 = { + "sn_owner_hotkey", + "subnet_owner_hotkey", + "recycle_or_burn", +} + + +def test_new_hyperparams_in_hyperparams(): + for key in NEW_HYPERPARAMS_826: + assert key in HYPERPARAMS, f"{key} should be in HYPERPARAMS" + extrinsic, root_only = HYPERPARAMS[key] + assert extrinsic, f"{key} must have non-empty extrinsic name" + assert root_only is RootSudoOnly.FALSE + + +def test_subnet_owner_hotkey_alias_maps_to_same_extrinsic(): + ext_sn, _ = HYPERPARAMS["sn_owner_hotkey"] + ext_subnet, _ = HYPERPARAMS["subnet_owner_hotkey"] + assert ext_sn == ext_subnet == "sudo_set_sn_owner_hotkey" + + +def test_new_hyperparams_have_metadata(): + required = {"description", "side_effects", "owner_settable", "docs_link"} + for key in NEW_HYPERPARAMS_826: + assert key in HYPERPARAMS_METADATA, f"{key} should be in HYPERPARAMS_METADATA" + meta = HYPERPARAMS_METADATA[key] + for field in required: + assert field in meta, f"{key} metadata missing '{field}'" + assert isinstance(meta["description"], str) + assert isinstance(meta["owner_settable"], bool) + + +def test_new_hyperparams_owner_settable_true(): + for key in NEW_HYPERPARAMS_826: + assert HYPERPARAMS_METADATA[key]["owner_settable"] is True From 5e39873e84ebf1348367304a6942317e75588766 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Feb 2026 07:50:14 -0800 Subject: [PATCH 112/113] bump rc version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3b1ad3b62..9388281e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "bittensor-cli" -version = "9.18.1" +version = "9.19.0rc1" description = "Bittensor CLI" readme = "README.md" authors = [ From f25741ce1d256c331e46c852012745d8f27833e9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 24 Feb 2026 07:56:26 -0800 Subject: [PATCH 113/113] reduce amount of stake burn --- tests/e2e_tests/test_stake_burn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_stake_burn.py b/tests/e2e_tests/test_stake_burn.py index c6694fa5d..a85f24137 100644 --- a/tests/e2e_tests/test_stake_burn.py +++ b/tests/e2e_tests/test_stake_burn.py @@ -95,7 +95,7 @@ def test_stake_burn(local_chain, wallet_setup): )["free_balance"] # First stake burn - amount_tao = 5.0 + amount_tao = 1.0 stake_burn_result = exec_command_alice( "sudo", "stake-burn",