From 09c37920505781968b968843c859632fd2d22226 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 29 Oct 2025 16:25:57 -0700 Subject: [PATCH 01/49] get_all_root_claim_types --- .../src/bittensor/subtensor_interface.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 2ef90d284..b24403ec6 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1824,6 +1824,37 @@ async def get_coldkey_swap_schedule_duration( return result + async def get_all_root_claim_types( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, str]: + """ + Retrieves all root claim types for all coldkeys in the network. + + Args: + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + dict[str, str]: A dictionary mapping coldkey SS58 addresses to their root claim type ("Keep" or "Swap"). + """ + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="RootClaimType", + params=[], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + root_claim_types = {} + async for coldkey, claim_type in result: + coldkey_ss58 = decode_account_id(coldkey[0]) + claim_type = next(iter(claim_type.value.keys())) + root_claim_types[coldkey_ss58] = claim_type + + return root_claim_types + async def get_subnet_price( self, netuid: int = None, From eafbc2817a4643f1e82c9e860414dbfa9ad496e8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 29 Oct 2025 16:31:10 -0700 Subject: [PATCH 02/49] add root claim to root metagraph --- bittensor_cli/src/commands/subnets/subnets.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 2d093b276..d272bf877 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -907,11 +907,18 @@ async def show_root(): # TODO json_output for this, don't forget block_hash = await subtensor.substrate.get_chain_head() - all_subnets, root_state, identities, old_identities = await asyncio.gather( + ( + all_subnets, + root_state, + identities, + old_identities, + root_claim_types, + ) = await asyncio.gather( subtensor.all_subnets(block_hash=block_hash), subtensor.get_subnet_state(netuid=0, block_hash=block_hash), subtensor.query_all_identities(block_hash=block_hash), subtensor.get_delegate_identities(block_hash=block_hash), + subtensor.get_all_root_claim_types(block_hash=block_hash), ) root_info = next((s for s in all_subnets if s.netuid == 0), None) if root_info is None: @@ -971,6 +978,11 @@ async def show_root(): style=COLOR_PALETTE["GENERAL"]["SYMBOL"], justify="left", ) + table.add_column( + "[bold white]Root Claim", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], + justify="center", + ) sorted_hotkeys = sorted( enumerate(root_state.hotkeys), @@ -1001,6 +1013,9 @@ async def show_root(): else (hotkey_identity.display if hotkey_identity else "") ) + coldkey_ss58 = root_state.coldkeys[idx] + claim_type = root_claim_types.get(coldkey_ss58, "Swap") + sorted_rows.append( ( str((pos + 1)), # Position @@ -1021,6 +1036,7 @@ async def show_root(): if not verbose else f"{root_state.coldkeys[idx]}", # Coldkey validator_identity, # Identity + claim_type, # Root Claim Type ) ) sorted_hks_delegation.append(root_state.hotkeys[idx]) @@ -1072,6 +1088,7 @@ async def show_root(): - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. - Hotkey: The hotkey ss58 address. - Coldkey: The coldkey ss58 address. + - Root Claim: The root claim type for this coldkey. 'Swap' converts Alpha to TAO every epoch. 'Keep' keeps Alpha emissions. """ ) if delegate_selection: From d143f4db4c8c9ee30b101a0ced05dba53798fa90 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 29 Oct 2025 17:32:01 -0700 Subject: [PATCH 03/49] wip --- bittensor_cli/src/commands/root.py | 85 ++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 bittensor_cli/src/commands/root.py diff --git a/bittensor_cli/src/commands/root.py b/bittensor_cli/src/commands/root.py new file mode 100644 index 000000000..7a8da22b3 --- /dev/null +++ b/bittensor_cli/src/commands/root.py @@ -0,0 +1,85 @@ +from typing import TYPE_CHECKING, Optional + +from bittensor_wallet import Wallet +from rich.prompt import Confirm, Prompt + +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + unlock_key, + print_extrinsic_id, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def set_claim_type( + wallet: Wallet, + subtensor: "SubtensorInterface", + prompt: bool = True, +) -> tuple[bool, str, Optional[str]]: + """ + Sets the root claim type for the coldkey. + + Root claim types control how staking emissions are handled on the ROOT network (subnet 0): + - "Swap": Future Root Alpha Emissions are swapped to TAO at claim time and added to root stake + - "Keep": Future Root Alpha Emissions are kept as Alpha tokens + + Args: + wallet: Bittensor wallet object + subtensor: SubtensorInterface object + prompt: Whether to prompt for user confirmation + + Returns: + tuple[bool, str, Optional[str]]: Tuple containing: + - bool: True if successful, False otherwise + - str: Error message if failed + - Optional[str]: Extrinsic identifier if successful + """ + + current_type = await subtensor.get_root_claim_type( + coldkey_ss58=wallet.coldkeypub.ss58_address + ) + console.print( + f"\nCurrent root claim type for coldkey:\n" + f" Coldkey: [cyan]{wallet.coldkeypub.ss58_address}[/cyan]\n" + f" Current type: [yellow]{current_type}[/yellow]\n" + ) + new_type = Prompt.ask( + "Select new root claim type", choices=["Swap", "Keep"], default=current_type + ) + if new_type == current_type: + console.print( + f"[yellow]Root claim type is already set to '{current_type}'. No change needed.[/yellow]" + ) + return ( + True, + "Root claim type is already set to '{current_type}'. No change needed.", + None, + ) + + if prompt: + console.print( + f"\n[bold]You are about to change the root claim type:[/bold]\n" + f" [yellow]{current_type}[/yellow] -> [dark_sea_green3]{new_type}[/dark_sea_green3]\n" + ) + + if new_type == "Swap": + console.print( + "[yellow]Note:[/yellow] With 'Swap', future root alpha emissions will be swapped to TAO and added to root stake." + ) + else: + console.print( + "[yellow]Note:[/yellow] With 'Keep', future root alpha emissions will be kept as Alpha tokens." + ) + + if not Confirm.ask("\nDo you want to proceed?"): + console.print("[yellow]Operation cancelled.[/yellow]") + return False, "Operation cancelled.", None + + if not (unlock := unlock_key(wallet)).success: + err_console.print( + f":cross_mark: [red]Failed to unlock wallet: {unlock.message}[/red]" + ) + return False, f"Failed to unlock wallet: {unlock.message}", None From c499b198949237a42fd2f6438e902576c9a1cf69 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 29 Oct 2025 17:32:11 -0700 Subject: [PATCH 04/49] add set_claim_type --- bittensor_cli/src/commands/root.py | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/bittensor_cli/src/commands/root.py b/bittensor_cli/src/commands/root.py index 7a8da22b3..bb61e9f75 100644 --- a/bittensor_cli/src/commands/root.py +++ b/bittensor_cli/src/commands/root.py @@ -83,3 +83,34 @@ async def set_claim_type( f":cross_mark: [red]Failed to unlock wallet: {unlock.message}[/red]" ) return False, f"Failed to unlock wallet: {unlock.message}", None + + with console.status( + f":satellite: Setting root claim type to '{new_type}'...", spinner="earth" + ): + try: + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_root_claim_type", + call_params={"new_root_claim_type": new_type}, + ) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) + if success: + console.print( + f":white_heavy_check_mark: [green]Successfully set root claim type to '{new_type}'[/green]" + ) + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) + return True, f"Successfully set root claim type to '{new_type}'", ext_id + else: + err_console.print( + f":cross_mark: [red]Failed to set root claim type: {err_msg}[/red]" + ) + return False, f"Failed to set root claim type: {err_msg}", None + + except Exception as e: + err_console.print( + f":cross_mark: [red]Error setting root claim type: {e}[/red]" + ) + return False, f"Error setting root claim type: {e}", None From 8d8bf67d1334e03b87c074904886fb68ef5045d0 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 29 Oct 2025 17:32:35 -0700 Subject: [PATCH 05/49] add get_root_claim_type --- .../src/bittensor/subtensor_interface.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index b24403ec6..b8c72fd93 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1824,6 +1824,39 @@ async def get_coldkey_swap_schedule_duration( return result + async def get_root_claim_type( + self, + coldkey_ss58: str, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> str: + """ + Retrieves the root claim type for a specific coldkey. + + Root claim types control how staking emissions are handled on the ROOT network (subnet 0): + - "Swap": Future Root Alpha Emissions are swapped to TAO at claim time and added to your root stake + - "Keep": Future Root Alpha Emissions are kept as Alpha + + Args: + coldkey_ss58: The SS58 address of the coldkey to query. + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + str: The root claim type for the coldkey ("Swap" or "Keep"). + """ + result = await self.query( + module="SubtensorModule", + storage_function="RootClaimType", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + if result is None: + return "Swap" + return next(iter(result.keys())) + async def get_all_root_claim_types( self, block_hash: Optional[str] = None, From c75b9f8b0813dab999e9f527e8be5f08cb971db4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 29 Oct 2025 17:33:31 -0700 Subject: [PATCH 06/49] add root_set_claim_type command --- bittensor_cli/cli.py | 56 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 3b6096047..0f86f2eb7 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -65,7 +65,7 @@ validate_rate_tolerance, get_hotkey_pub_ss58, ) -from bittensor_cli.src.commands import sudo, wallets, view +from bittensor_cli.src.commands import root, sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.liquidity import liquidity from bittensor_cli.src.commands.crowd import ( @@ -760,6 +760,7 @@ def __init__(self): ) self.wallet_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) + self.root_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) self.subnet_mechanisms_app = typer.Typer(epilog=_epilog) @@ -802,6 +803,14 @@ def __init__(self): ) self.app.add_typer(self.stake_app, name="st", hidden=True, no_args_is_help=True) + # root aliases + self.app.add_typer( + self.root_app, + name="root", + short_help="Root claim commands", + no_args_is_help=True, + ) + # sudo aliases self.app.add_typer( self.sudo_app, @@ -988,6 +997,9 @@ def __init__(self): children_app.command("revoke")(self.stake_revoke_children) children_app.command("take")(self.stake_childkey_take) + # root claim commands + self.root_app.command("set-claim")(self.root_set_claim_type) + # subnet mechanism commands self.subnet_mechanisms_app.command( "count", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"] @@ -7066,6 +7078,48 @@ def view_dashboard( ) ) + def root_set_claim_type( + self, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + network: Optional[list[str]] = Options.network, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Set the root claim type for your coldkey. + + Root claim types control how staking emissions are handled on the ROOT network (subnet 0): + + [bold]Claim Types:[/bold] + • [green]Swap[/green]: Future Root Alpha Emissions are swapped to TAO and added to root stake (default) + • [yellow]Keep[/yellow]: Future Root Alpha Emissions are kept as Alpha tokens + + USAGE: + + [green]$[/green] btcli root set-claim-type + + With specific wallet: + + [green]$[/green] btcli root set-claim-type --wallet-name my_wallet + """ + self.verbosity_handler(quiet, verbose) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.HOTKEY], + ) + return self._run_command( + root.set_claim_type( + wallet=wallet, + subtensor=self.initialize_chain(network), + prompt=prompt, + ) + ) + def liquidity_add( self, network: Optional[list[str]] = Options.network, From 22803d0fd5eeb90da1688fd17dc2ca27c1b645d0 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 30 Oct 2025 21:13:18 +0200 Subject: [PATCH 07/49] Stop running e2e tests on changelog branches --- .github/workflows/e2e-subtensor-tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index f57b9a1f7..77568a1f4 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -27,7 +27,7 @@ jobs: find-tests: runs-on: ubuntu-latest - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && !startsWith(github.head_ref, 'changelog/')) }} outputs: test-files: ${{ steps.get-tests.outputs.test-files }} steps: @@ -43,6 +43,7 @@ jobs: pull-docker-image: runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && !startsWith(github.head_ref, 'changelog/')) }} steps: - name: Log in to GitHub Container Registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin From 64b59ae75915d54a96629b27d19088aa0b4d142f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 12:37:19 -0700 Subject: [PATCH 08/49] change root -> stake --- .../src/commands/{root.py => stake/claim.py} | 232 +++++++++--------- 1 file changed, 116 insertions(+), 116 deletions(-) rename bittensor_cli/src/commands/{root.py => stake/claim.py} (97%) diff --git a/bittensor_cli/src/commands/root.py b/bittensor_cli/src/commands/stake/claim.py similarity index 97% rename from bittensor_cli/src/commands/root.py rename to bittensor_cli/src/commands/stake/claim.py index bb61e9f75..5f9bd0af2 100644 --- a/bittensor_cli/src/commands/root.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -1,116 +1,116 @@ -from typing import TYPE_CHECKING, Optional - -from bittensor_wallet import Wallet -from rich.prompt import Confirm, Prompt - -from bittensor_cli.src.bittensor.utils import ( - console, - err_console, - unlock_key, - print_extrinsic_id, -) - -if TYPE_CHECKING: - from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface - - -async def set_claim_type( - wallet: Wallet, - subtensor: "SubtensorInterface", - prompt: bool = True, -) -> tuple[bool, str, Optional[str]]: - """ - Sets the root claim type for the coldkey. - - Root claim types control how staking emissions are handled on the ROOT network (subnet 0): - - "Swap": Future Root Alpha Emissions are swapped to TAO at claim time and added to root stake - - "Keep": Future Root Alpha Emissions are kept as Alpha tokens - - Args: - wallet: Bittensor wallet object - subtensor: SubtensorInterface object - prompt: Whether to prompt for user confirmation - - Returns: - tuple[bool, str, Optional[str]]: Tuple containing: - - bool: True if successful, False otherwise - - str: Error message if failed - - Optional[str]: Extrinsic identifier if successful - """ - - current_type = await subtensor.get_root_claim_type( - coldkey_ss58=wallet.coldkeypub.ss58_address - ) - console.print( - f"\nCurrent root claim type for coldkey:\n" - f" Coldkey: [cyan]{wallet.coldkeypub.ss58_address}[/cyan]\n" - f" Current type: [yellow]{current_type}[/yellow]\n" - ) - new_type = Prompt.ask( - "Select new root claim type", choices=["Swap", "Keep"], default=current_type - ) - if new_type == current_type: - console.print( - f"[yellow]Root claim type is already set to '{current_type}'. No change needed.[/yellow]" - ) - return ( - True, - "Root claim type is already set to '{current_type}'. No change needed.", - None, - ) - - if prompt: - console.print( - f"\n[bold]You are about to change the root claim type:[/bold]\n" - f" [yellow]{current_type}[/yellow] -> [dark_sea_green3]{new_type}[/dark_sea_green3]\n" - ) - - if new_type == "Swap": - console.print( - "[yellow]Note:[/yellow] With 'Swap', future root alpha emissions will be swapped to TAO and added to root stake." - ) - else: - console.print( - "[yellow]Note:[/yellow] With 'Keep', future root alpha emissions will be kept as Alpha tokens." - ) - - if not Confirm.ask("\nDo you want to proceed?"): - console.print("[yellow]Operation cancelled.[/yellow]") - return False, "Operation cancelled.", None - - if not (unlock := unlock_key(wallet)).success: - err_console.print( - f":cross_mark: [red]Failed to unlock wallet: {unlock.message}[/red]" - ) - return False, f"Failed to unlock wallet: {unlock.message}", None - - with console.status( - f":satellite: Setting root claim type to '{new_type}'...", spinner="earth" - ): - try: - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_root_claim_type", - call_params={"new_root_claim_type": new_type}, - ) - success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet - ) - if success: - console.print( - f":white_heavy_check_mark: [green]Successfully set root claim type to '{new_type}'[/green]" - ) - ext_id = await ext_receipt.get_extrinsic_identifier() - await print_extrinsic_id(ext_receipt) - return True, f"Successfully set root claim type to '{new_type}'", ext_id - else: - err_console.print( - f":cross_mark: [red]Failed to set root claim type: {err_msg}[/red]" - ) - return False, f"Failed to set root claim type: {err_msg}", None - - except Exception as e: - err_console.print( - f":cross_mark: [red]Error setting root claim type: {e}[/red]" - ) - return False, f"Error setting root claim type: {e}", None +from typing import TYPE_CHECKING, Optional + +from bittensor_wallet import Wallet +from rich.prompt import Confirm, Prompt + +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + unlock_key, + print_extrinsic_id, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def set_claim_type( + wallet: Wallet, + subtensor: "SubtensorInterface", + prompt: bool = True, +) -> tuple[bool, str, Optional[str]]: + """ + Sets the root claim type for the coldkey. + + Root claim types control how staking emissions are handled on the ROOT network (subnet 0): + - "Swap": Future Root Alpha Emissions are swapped to TAO at claim time and added to root stake + - "Keep": Future Root Alpha Emissions are kept as Alpha tokens + + Args: + wallet: Bittensor wallet object + subtensor: SubtensorInterface object + prompt: Whether to prompt for user confirmation + + Returns: + tuple[bool, str, Optional[str]]: Tuple containing: + - bool: True if successful, False otherwise + - str: Error message if failed + - Optional[str]: Extrinsic identifier if successful + """ + + current_type = await subtensor.get_root_claim_type( + coldkey_ss58=wallet.coldkeypub.ss58_address + ) + console.print( + f"\nCurrent root claim type for coldkey:\n" + f" Coldkey: [cyan]{wallet.coldkeypub.ss58_address}[/cyan]\n" + f" Current type: [yellow]{current_type}[/yellow]\n" + ) + new_type = Prompt.ask( + "Select new root claim type", choices=["Swap", "Keep"], default=current_type + ) + if new_type == current_type: + console.print( + f"[yellow]Root claim type is already set to '{current_type}'. No change needed.[/yellow]" + ) + return ( + True, + "Root claim type is already set to '{current_type}'. No change needed.", + None, + ) + + if prompt: + console.print( + f"\n[bold]You are about to change the root claim type:[/bold]\n" + f" [yellow]{current_type}[/yellow] -> [dark_sea_green3]{new_type}[/dark_sea_green3]\n" + ) + + if new_type == "Swap": + console.print( + "[yellow]Note:[/yellow] With 'Swap', future root alpha emissions will be swapped to TAO and added to root stake." + ) + else: + console.print( + "[yellow]Note:[/yellow] With 'Keep', future root alpha emissions will be kept as Alpha tokens." + ) + + if not Confirm.ask("\nDo you want to proceed?"): + console.print("[yellow]Operation cancelled.[/yellow]") + return False, "Operation cancelled.", None + + if not (unlock := unlock_key(wallet)).success: + err_console.print( + f":cross_mark: [red]Failed to unlock wallet: {unlock.message}[/red]" + ) + return False, f"Failed to unlock wallet: {unlock.message}", None + + with console.status( + f":satellite: Setting root claim type to '{new_type}'...", spinner="earth" + ): + try: + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_root_claim_type", + call_params={"new_root_claim_type": new_type}, + ) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) + if success: + console.print( + f":white_heavy_check_mark: [green]Successfully set root claim type to '{new_type}'[/green]" + ) + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) + return True, f"Successfully set root claim type to '{new_type}'", ext_id + else: + err_console.print( + f":cross_mark: [red]Failed to set root claim type: {err_msg}[/red]" + ) + return False, f"Failed to set root claim type: {err_msg}", None + + except Exception as e: + err_console.print( + f":cross_mark: [red]Error setting root claim type: {e}[/red]" + ) + return False, f"Error setting root claim type: {e}", None From 10945f5f299aef189539956260784553ed33aadd Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 13:00:39 -0700 Subject: [PATCH 09/49] update cli --- bittensor_cli/cli.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0f86f2eb7..e0d6984b0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -65,7 +65,7 @@ validate_rate_tolerance, get_hotkey_pub_ss58, ) -from bittensor_cli.src.commands import root, sudo, wallets, view +from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.liquidity import liquidity from bittensor_cli.src.commands.crowd import ( @@ -87,6 +87,7 @@ move as move_stake, add as add_stake, remove as remove_stake, + claim as claim_stake, ) from bittensor_cli.src.commands.subnets import ( price, @@ -760,7 +761,6 @@ def __init__(self): ) self.wallet_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) - self.root_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) self.subnet_mechanisms_app = typer.Typer(epilog=_epilog) @@ -803,14 +803,6 @@ def __init__(self): ) self.app.add_typer(self.stake_app, name="st", hidden=True, no_args_is_help=True) - # root aliases - self.app.add_typer( - self.root_app, - name="root", - short_help="Root claim commands", - no_args_is_help=True, - ) - # sudo aliases self.app.add_typer( self.sudo_app, @@ -979,6 +971,7 @@ def __init__(self): self.stake_app.command( "swap", rich_help_panel=HELP_PANELS["STAKE"]["MOVEMENT"] )(self.stake_swap) + self.stake_app.command("set-claim")(self.stake_set_claim_type) # stake-children commands children_app = typer.Typer() @@ -997,9 +990,6 @@ def __init__(self): children_app.command("revoke")(self.stake_revoke_children) children_app.command("take")(self.stake_childkey_take) - # root claim commands - self.root_app.command("set-claim")(self.root_set_claim_type) - # subnet mechanism commands self.subnet_mechanisms_app.command( "count", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"] @@ -7078,7 +7068,7 @@ def view_dashboard( ) ) - def root_set_claim_type( + def stake_set_claim_type( self, wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, @@ -7113,7 +7103,7 @@ def root_set_claim_type( ask_for=[WO.NAME, WO.HOTKEY], ) return self._run_command( - root.set_claim_type( + claim_stake.set_claim_type( wallet=wallet, subtensor=self.initialize_chain(network), prompt=prompt, From 76ab92287ae2db7a7d0aac176eaf002b55818499 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 13:00:50 -0700 Subject: [PATCH 10/49] add table --- bittensor_cli/src/commands/stake/claim.py | 34 +++++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 5f9bd0af2..a13ccf5d5 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -2,7 +2,10 @@ from bittensor_wallet import Wallet from rich.prompt import Confirm, Prompt +from rich.table import Table, Column +from rich import box +from bittensor_cli.src import COLORS from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -41,11 +44,31 @@ async def set_claim_type( current_type = await subtensor.get_root_claim_type( coldkey_ss58=wallet.coldkeypub.ss58_address ) - console.print( - f"\nCurrent root claim type for coldkey:\n" - f" Coldkey: [cyan]{wallet.coldkeypub.ss58_address}[/cyan]\n" - f" Current type: [yellow]{current_type}[/yellow]\n" + + claim_table = Table( + Column( + "[bold white]Coldkey", + style=COLORS.GENERAL.COLDKEY, + justify="left", + ), + Column( + "[bold white]Root Claim Type", + style=COLORS.GENERAL.SUBHEADING, + justify="center", + ), + show_header=True, + show_footer=False, + show_edge=True, + border_style="bright_black", + box=box.SIMPLE, + pad_edge=False, + width=None, + title=f"\n[{COLORS.GENERAL.HEADER}]Current root claim type:[/{COLORS.GENERAL.HEADER}]", + ) + claim_table.add_row( + wallet.coldkeypub.ss58_address, f"[yellow]{current_type}[/yellow]" ) + console.print(claim_table) new_type = Prompt.ask( "Select new root claim type", choices=["Swap", "Keep"], default=current_type ) @@ -61,8 +84,7 @@ async def set_claim_type( if prompt: console.print( - f"\n[bold]You are about to change the root claim type:[/bold]\n" - f" [yellow]{current_type}[/yellow] -> [dark_sea_green3]{new_type}[/dark_sea_green3]\n" + f"\n[bold]Changing root claim type from '{current_type}' -> '{new_type}'[/bold]\n" ) if new_type == "Swap": From 0d27f93c558edb35a75add080885883cff0ee668 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 14:43:39 -0700 Subject: [PATCH 11/49] add get_staking_hotkeys --- .../src/bittensor/subtensor_interface.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index b8c72fd93..f37c52d68 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1888,6 +1888,32 @@ async def get_all_root_claim_types( return root_claim_types + async def get_staking_hotkeys( + self, + coldkey_ss58: str, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> list[str]: + """Retrieves all hotkeys that a coldkey is staking to. + + Args: + coldkey_ss58: The SS58 address of the coldkey. + block_hash: The hash of the blockchain block for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + list[str]: A list of hotkey SS58 addresses that the coldkey has staked to. + """ + result = await self.query( + module="SubtensorModule", + storage_function="StakingHotkeys", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + staked_hotkeys = [decode_account_id(hotkey) for hotkey in result] + return staked_hotkeys + async def get_subnet_price( self, netuid: int = None, From 816c76662417699a8086684960f46a31cfb38fbe Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 14:50:35 -0700 Subject: [PATCH 12/49] add get_root_claimed_netuid --- .../src/bittensor/subtensor_interface.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index f37c52d68..a42634b60 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1914,6 +1914,35 @@ async def get_staking_hotkeys( staked_hotkeys = [decode_account_id(hotkey) for hotkey in result] return staked_hotkeys + async def get_root_claimed_netuid( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """Retrieves the root claimed Alpha shares for coldkey from hotkey in provided subnet. + + Args: + coldkey_ss58: The SS58 address of the staker. + hotkey_ss58: The SS58 address of the root validator. + netuid: The unique identifier of the subnet. + block_hash: The blockchain block hash for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + Balance: The number of Alpha stake claimed from the root validator. + """ + query = await self.query( + module="SubtensorModule", + storage_function="RootClaimed", + params=[hotkey_ss58, coldkey_ss58, netuid], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return Balance.from_rao(query).set_unit(netuid=netuid) + async def get_subnet_price( self, netuid: int = None, From 8b868c2ba403af04e9ebca7fb46c9f41af0f30e0 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 14:53:45 -0700 Subject: [PATCH 13/49] add claimable_rate_all_subnets --- .../src/bittensor/subtensor_interface.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a42634b60..a63468094 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1943,6 +1943,68 @@ async def get_root_claimed_netuid( ) return Balance.from_rao(query).set_unit(netuid=netuid) + async def get_root_claimed_all_netuids( + self, + coldkey_ss58: str, + hotkey_ss58: str, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[int, Balance]: + """Retrieves the root claimed Alpha shares for coldkey from hotkey in all subnets. + + Args: + coldkey_ss58: The SS58 address of the staker. + hotkey_ss58: The SS58 address of the root validator. + block_hash: The blockchain block hash for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + dict[int, Balance]: Dictionary mapping netuid to claimed stake. + """ + query = await self.substrate.query_map( + module="SubtensorModule", + storage_function="RootClaimed", + params=[hotkey_ss58, coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + total_claimed = {} + async for netuid, claimed in query: + total_claimed[netuid] = Balance.from_rao(claimed.value).set_unit( + netuid=netuid + ) + return total_claimed + + async def claimable_rate_all_subnets( + self, + hotkey_ss58: str, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[int, float]: + """Retrieves all root claimable rates from a given hotkey address for all subnets with this validator. + + Args: + hotkey_ss58: The SS58 address of the root validator. + block_hash: The blockchain block hash for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + dict[int, float]: Dictionary mapping netuid to claimable rate. + """ + query = await self.query( + module="SubtensorModule", + storage_function="RootClaimable", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + if not query: + return {} + + bits_list = next(iter(query)) + return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} + async def get_subnet_price( self, netuid: int = None, From 90c6111f37b5bf4995b581d0a961d7d52675c5c1 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 14:54:20 -0700 Subject: [PATCH 14/49] claimable_rate_netuid --- .../src/bittensor/subtensor_interface.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a63468094..0356da9ab 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1975,7 +1975,7 @@ async def get_root_claimed_all_netuids( ) return total_claimed - async def claimable_rate_all_subnets( + async def claimable_rate_all_netuids( self, hotkey_ss58: str, block_hash: Optional[str] = None, @@ -2005,6 +2005,31 @@ async def claimable_rate_all_subnets( bits_list = next(iter(query)) return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} + async def claimable_rate_netuid( + self, + hotkey_ss58: str, + netuid: int, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> float: + """Retrieves the root claimable rate from a given hotkey address for provided netuid. + + Args: + hotkey_ss58: The SS58 address of the root validator. + netuid: The unique identifier of the subnet to get the rate. + block_hash: The blockchain block hash for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + float: The rate of claimable stake from validator's hotkey for provided subnet. + """ + all_rates = await self.claimable_rate_all_netuids( + hotkey_ss58=hotkey_ss58, + block_hash=block_hash, + reuse_block=reuse_block, + ) + return all_rates.get(netuid, 0.0) + async def get_subnet_price( self, netuid: int = None, From afe603bb57afc7d99a298b5b0fda4f8e35367874 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 14:55:10 -0700 Subject: [PATCH 15/49] add get_claimable_stake --- .../src/bittensor/subtensor_interface.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 0356da9ab..877966379 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2030,6 +2030,64 @@ async def claimable_rate_netuid( ) return all_rates.get(netuid, 0.0) + async def get_claimable_stake( + self, + coldkey_ss58: str, + hotkey_ss58: str, + netuid: int, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """Retrieves the root claimable stake for a given coldkey address. + + Args: + coldkey_ss58: Delegate's ColdKey SS58 address. + hotkey_ss58: The root validator hotkey SS58 address. + netuid: Delegate's netuid where stake will be claimed. + block_hash: The blockchain block hash for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + Balance: Available for claiming root stake. + + Note: + After manual claim, claimable (available) stake will be added to subnet stake. + """ + root_stake = await self.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=0, + block_hash=block_hash, + ) + + # Get the claimable rate + root_claimable_rate = await self.claimable_rate_netuid( + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + # Calculate claimable stake + root_claimable_stake = (root_claimable_rate * root_stake).set_unit( + netuid=netuid + ) + + # Get already claimed amount + root_claimed = await self.get_root_claimed_netuid( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ) + + # Return the difference (what's left to claim) + return max( + root_claimable_stake - root_claimed, + Balance.from_rao(0).set_unit(netuid=netuid), + ) + async def get_subnet_price( self, netuid: int = None, From 3b0a3c93f560e1ce6d8f199791b67b2485a93227 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 17:15:34 -0700 Subject: [PATCH 16/49] wip --- .../src/bittensor/subtensor_interface.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 877966379..dc677b569 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2088,6 +2088,72 @@ async def get_claimable_stake( Balance.from_rao(0).set_unit(netuid=netuid), ) + async def get_claimable_stakes_batch( + self, + coldkey_ss58: str, + stakes_info: list["StakeInfo"], + block_hash: Optional[str] = None, + ) -> dict[str, dict[int, "Balance"]]: + """Batch query claimable stakes for multiple hotkey-netuid pairs. + + Args: + coldkey_ss58: The coldkey SS58 address. + stakes_info: List of StakeInfo objects containing stake data. + block_hash: Optional block hash for the query. + + Returns: + dict[str, dict[int, Balance]]: Mapping of hotkey to netuid to claimable Balance. + """ + if not stakes_info: + return {} + + root_stakes = {} + for stake_info in stakes_info: + if stake_info.netuid == 0 and stake_info.stake.rao > 0: + root_stakes[stake_info.hotkey_ss58] = stake_info.stake + + target_pairs = [] + for s in stakes_info: + if s.netuid != 0 and s.stake.rao > 0 and s.hotkey_ss58 in root_stakes: + pair = (s.hotkey_ss58, s.netuid) + target_pairs.append(pair) + + if not target_pairs: + return {} + + unique_hotkeys = list(set(h for h, _ in target_pairs)) + if not unique_hotkeys: + return {} + + batch_claimable_calls = [] + batch_claimed_calls = [] + + # Get the claimable rate + for hotkey in unique_hotkeys: + batch_claimable_calls.append( + await self.substrate.create_storage_key( + "SubtensorModule", "RootClaimable", [hotkey], block_hash=block_hash + ) + ) + + # Get already claimed + claimed_pairs = target_pairs + for hotkey, netuid in claimed_pairs: + batch_claimed_calls.append( + await self.substrate.create_storage_key( + "SubtensorModule", + "RootClaimed", + [hotkey, coldkey_ss58, netuid], + block_hash=block_hash, + ) + ) + + batch_claimable, batch_claimed = await asyncio.gather( + self.substrate.query_multi(batch_claimable_calls, block_hash=block_hash), + self.substrate.query_multi(batch_claimed_calls, block_hash=block_hash), + ) + + async def get_subnet_price( self, netuid: int = None, From 8bfb5aca8adcb01c0ac54a9d960dbdf82fcdf991 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 17:15:44 -0700 Subject: [PATCH 17/49] add get_claimable_stakes_batch --- .../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 dc677b569..8e6ab342b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2153,6 +2153,33 @@ async def get_claimable_stakes_batch( self.substrate.query_multi(batch_claimed_calls, block_hash=block_hash), ) + claimable_rates = {} + claimed_amounts = {} + for idx, (_, result) in enumerate(batch_claimable): + hotkey = unique_hotkeys[idx] + if result: + for netuid, rate in result: + if hotkey not in claimable_rates: + claimable_rates[hotkey] = {} + claimable_rates[hotkey][netuid] = fixed_to_float(rate, frac_bits=32) + + for idx, (_, result) in enumerate(batch_claimed): + hotkey, netuid = claimed_pairs[idx] + value = result or 0 + claimed_amounts[(hotkey, netuid)] = Balance.from_rao(value).set_unit(netuid) + + # Calculate the claimable stake for each pair + results = {} + for hotkey, netuid in target_pairs: + root_stake = root_stakes[hotkey] + rate = claimable_rates[hotkey][netuid] + claimable_stake = rate * root_stake + already_claimed = claimed_amounts[(hotkey, netuid)] + net_claimable = claimable_stake - already_claimed + if hotkey not in results: + results[hotkey] = {} + results[hotkey][netuid] = net_claimable.set_unit(netuid) + return results async def get_subnet_price( self, From daaad91d4f177f3c8d664005f5af89be5d3b4baa Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 17:24:51 -0700 Subject: [PATCH 18/49] add claimable amount to stake list --- bittensor_cli/src/commands/stake/list.py | 43 ++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index 4a8e17145..b8f0a2058 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -49,12 +49,21 @@ async def get_stake_data(block_hash_: str = None): subtensor.get_delegate_identities(block_hash=block_hash_), subtensor.all_subnets(block_hash=block_hash_), ) + + claimable_amounts = {} + if sub_stakes_: + claimable_amounts = await subtensor.get_claimable_stakes_batch( + coldkey_ss58=coldkey_address, + stakes_info=sub_stakes_, + block_hash=block_hash_, + ) # sub_stakes = substakes[coldkey_address] dynamic_info__ = {info.netuid: info for info in _dynamic_info} return ( sub_stakes_, registered_delegate_info_, dynamic_info__, + claimable_amounts, ) def define_table( @@ -135,9 +144,18 @@ def define_table( style=COLOR_PALETTE["POOLS"]["EMISSION"], justify="right", ) + defined_table.add_column( + f"[white]Claimable \n({Balance.get_unit(1)})", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + justify="right", + ) return defined_table - def create_table(hotkey_: str, substakes: list[StakeInfo]): + def create_table( + hotkey_: str, + substakes: list[StakeInfo], + claimable_amounts_: dict[str, dict[int, Balance]], + ): name_ = ( f"{registered_delegate_info[hotkey_].display} ({hotkey_})" if hotkey_ in registered_delegate_info @@ -194,6 +212,23 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): subnet_name = get_subnet_name(dynamic_info[netuid]) subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {subnet_name}" + # Claimable amount cell + claimable_amount = Balance.from_rao(0) + if ( + hotkey_ in claimable_amounts_ + and netuid in claimable_amounts_[hotkey_] + ): + claimable_amount = claimable_amounts_[hotkey_][netuid] + + if claimable_amount.tao > 0.00001: + claimable_cell = ( + f"{claimable_amount.tao:.5f} {symbol}" + if not verbose + else f"{claimable_amount}" + ) + else: + claimable_cell = "-" + rows.append( [ str(netuid), # Number @@ -215,6 +250,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): # if substake_.is_registered # else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A", # Emission(α/block) str(Balance.from_tao(per_block_tao_emission)), + claimable_cell, # Claimable amount ] ) substakes_values.append( @@ -230,6 +266,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): "alpha": per_block_emission, "tao": per_block_tao_emission, }, + "claimable": claimable_amount.tao, } ) created_table = define_table( @@ -420,6 +457,7 @@ def format_cell( sub_stakes, registered_delegate_info, dynamic_info, + claimable_amounts, ), balance, ) = await asyncio.gather( @@ -487,6 +525,7 @@ def format_cell( sub_stakes, registered_delegate_info, dynamic_info_, + claimable_amounts_live, ) = await get_stake_data(block_hash) selected_stakes = [ stake @@ -553,7 +592,7 @@ def format_cell( for hotkey, substakes in hotkeys_to_substakes.items(): counter += 1 tao_value, swapped_tao_value, substake_values_ = create_table( - hotkey, substakes + hotkey, substakes, claimable_amounts ) dict_output["stake_info"][hotkey] = substake_values_ all_hks_tao_value += tao_value From 6de2151f7fea8adae4cd8f22608a27681c59209c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 17:33:12 -0700 Subject: [PATCH 19/49] add supp for live mode --- bittensor_cli/src/commands/stake/list.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index b8f0a2058..baeb1f392 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -281,6 +281,7 @@ def create_live_table( substakes: list, dynamic_info_for_lt: dict, hotkey_name_: str, + claimable_amounts_: dict, previous_data_: Optional[dict] = None, ) -> tuple[Table, dict]: rows = [] @@ -425,6 +426,26 @@ def format_cell( f" {get_subnet_name(dynamic_info_for_lt[netuid])}" ) + # Claimable amount cell + hotkey_ss58 = substake_.hotkey_ss58 + claimable_amount = Balance.from_rao(0) + if ( + hotkey_ss58 in claimable_amounts_ + and netuid in claimable_amounts_[hotkey_ss58] + ): + claimable_amount = claimable_amounts_[hotkey_ss58][netuid] + + current_data_[netuid]["claimable"] = claimable_amount.tao + + claimable_cell = format_cell( + claimable_amount.tao, + prev.get("claimable"), + unit=symbol, + unit_first_=unit_first, + precision=5, + millify=True if not verbose else False, + ) + rows.append( [ str(netuid), # Netuid @@ -438,6 +459,7 @@ def format_cell( else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registration status emission_cell, # Emission rate tao_emission_cell, # TAO emission rate + claimable_cell, # Claimable amount ] ) @@ -547,6 +569,7 @@ def format_cell( selected_stakes, dynamic_info_, hotkey_name, + claimable_amounts_live, previous_data, ) From a5cb3fd599614aa40bf31f8afa5a09f687059886 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 30 Oct 2025 18:17:30 -0700 Subject: [PATCH 20/49] add claim type in metagraph --- bittensor_cli/src/commands/subnets/subnets.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d272bf877..53b7e1d31 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -979,7 +979,7 @@ async def show_root(): justify="left", ) table.add_column( - "[bold white]Root Claim", + "[bold white]Claim Type", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], justify="center", ) @@ -1141,6 +1141,7 @@ async def show_subnet( identities, old_identities, current_burn_cost, + root_claim_types, ) = await asyncio.gather( subtensor.subnet(netuid=netuid_, block_hash=block_hash), subtensor.query_all_identities(block_hash=block_hash), @@ -1148,6 +1149,7 @@ async def show_subnet( subtensor.get_hyperparameter( param_name="Burn", netuid=netuid_, block_hash=block_hash ), + subtensor.get_all_root_claim_types(block_hash=block_hash), ) selected_mechanism_id = mechanism_id or 0 @@ -1254,6 +1256,14 @@ async def show_subnet( # Modify tao stake with TAO_WEIGHT tao_stake = metagraph_info.tao_stake[idx] * TAO_WEIGHT + + # Get claim type for this coldkey if applicable TAO stake + coldkey_ss58 = metagraph_info.coldkeys[idx] + if tao_stake.tao > 0: + claim_type = root_claim_types.get(coldkey_ss58, "Swap") + else: + claim_type = "-" + rows.append( ( str(idx), # UID @@ -1276,6 +1286,7 @@ async def show_subnet( if not verbose else f"{metagraph_info.coldkeys[idx]}", # Coldkey uid_identity, # Identity + claim_type, # Root Claim Type ) ) json_out_rows.append( @@ -1292,6 +1303,7 @@ async def show_subnet( "hotkey": metagraph_info.hotkeys[idx], "coldkey": metagraph_info.coldkeys[idx], "identity": uid_identity, + "claim_type": claim_type, } ) @@ -1357,6 +1369,12 @@ async def show_subnet( no_wrap=True, justify="left", ) + table.add_column( + "Claim Type", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], + no_wrap=True, + justify="center", + ) for pos, row in enumerate(rows, 1): table_row = [] table_row.extend(row) From 329daca23379e26d24293acab2066c630d3ff00a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 12:07:07 -0700 Subject: [PATCH 21/49] improve naming --- .../src/bittensor/subtensor_interface.py | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 8e6ab342b..12e07fdc7 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1824,7 +1824,7 @@ async def get_coldkey_swap_schedule_duration( return result - async def get_root_claim_type( + async def get_coldkey_claim_type( self, coldkey_ss58: str, block_hash: Optional[str] = None, @@ -1857,7 +1857,7 @@ async def get_root_claim_type( return "Swap" return next(iter(result.keys())) - async def get_all_root_claim_types( + async def get_all_coldkeys_claim_type( self, block_hash: Optional[str] = None, reuse_block: bool = False, @@ -1914,7 +1914,7 @@ async def get_staking_hotkeys( staked_hotkeys = [decode_account_id(hotkey) for hotkey in result] return staked_hotkeys - async def get_root_claimed_netuid( + async def get_claimed_amount( self, coldkey_ss58: str, hotkey_ss58: str, @@ -1943,7 +1943,7 @@ async def get_root_claimed_netuid( ) return Balance.from_rao(query).set_unit(netuid=netuid) - async def get_root_claimed_all_netuids( + async def get_claimed_amount_all_netuids( self, coldkey_ss58: str, hotkey_ss58: str, @@ -2053,35 +2053,31 @@ async def get_claimable_stake( Note: After manual claim, claimable (available) stake will be added to subnet stake. """ - root_stake = await self.get_stake_for_coldkey_and_hotkey_on_netuid( - coldkey_ss58=coldkey_ss58, - hotkey_ss58=hotkey_ss58, - netuid=0, - block_hash=block_hash, - ) - - # Get the claimable rate - root_claimable_rate = await self.claimable_rate_netuid( - hotkey_ss58=hotkey_ss58, - netuid=netuid, - block_hash=block_hash, - reuse_block=reuse_block, + root_stake, root_claimable_rate, root_claimed = await asyncio.gather( + self.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=0, + block_hash=block_hash, + ), + self.claimable_rate_netuid( + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ), + self.get_claimed_amount( + coldkey_ss58=coldkey_ss58, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + block_hash=block_hash, + reuse_block=reuse_block, + ), ) - # Calculate claimable stake root_claimable_stake = (root_claimable_rate * root_stake).set_unit( netuid=netuid ) - - # Get already claimed amount - root_claimed = await self.get_root_claimed_netuid( - coldkey_ss58=coldkey_ss58, - hotkey_ss58=hotkey_ss58, - netuid=netuid, - block_hash=block_hash, - reuse_block=reuse_block, - ) - # Return the difference (what's left to claim) return max( root_claimable_stake - root_claimed, From b2bd8e4b551ea370517274990f44d652d1bd19a9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 17:10:17 -0700 Subject: [PATCH 22/49] update var --- bittensor_cli/src/commands/subnets/subnets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 53b7e1d31..3671c4709 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -918,7 +918,7 @@ async def show_root(): subtensor.get_subnet_state(netuid=0, block_hash=block_hash), subtensor.query_all_identities(block_hash=block_hash), subtensor.get_delegate_identities(block_hash=block_hash), - subtensor.get_all_root_claim_types(block_hash=block_hash), + subtensor.get_all_coldkeys_claim_type(block_hash=block_hash), ) root_info = next((s for s in all_subnets if s.netuid == 0), None) if root_info is None: @@ -1149,7 +1149,7 @@ async def show_subnet( subtensor.get_hyperparameter( param_name="Burn", netuid=netuid_, block_hash=block_hash ), - subtensor.get_all_root_claim_types(block_hash=block_hash), + subtensor.get_all_coldkeys_claim_type(block_hash=block_hash), ) selected_mechanism_id = mechanism_id or 0 From 5f5f851785298297dea1eacf17e6038024b6a0e1 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 17:17:47 -0700 Subject: [PATCH 23/49] _print_claimable_table --- bittensor_cli/src/commands/stake/claim.py | 43 ++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index a13ccf5d5..c7e62d629 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -1,3 +1,4 @@ +import asyncio from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet @@ -41,7 +42,7 @@ async def set_claim_type( - Optional[str]: Extrinsic identifier if successful """ - current_type = await subtensor.get_root_claim_type( + current_type = await subtensor.get_coldkey_claim_type( coldkey_ss58=wallet.coldkeypub.ss58_address ) @@ -136,3 +137,43 @@ async def set_claim_type( f":cross_mark: [red]Error setting root claim type: {e}[/red]" ) return False, f"Error setting root claim type: {e}", None + + +def _print_claimable_table(wallet: Wallet, claimable_stake: dict): + """Prints claimable stakes table grouped by netuid""" + + table = Table( + show_header=True, + show_footer=False, + show_edge=True, + border_style="bright_black", + box=box.SIMPLE, + pad_edge=False, + title=f"\n[{COLORS.GENERAL.HEADER}]Claimable emissions for coldkey: {wallet.coldkeypub.ss58_address}", + ) + + table.add_column("Netuid", style=COLORS.GENERAL.NETUID, justify="center") + table.add_column("Current Stake", style=COLORS.GENERAL.SUBHEADING, justify="right") + table.add_column("Claimable", style=COLORS.GENERAL.SUCCESS, justify="right") + table.add_column("Hotkey", style=COLORS.GENERAL.HOTKEY, justify="left") + table.add_column("Identity", style=COLORS.GENERAL.SUBHEADING, justify="left") + + for netuid in sorted(claimable_stake.keys()): + hotkeys_info = claimable_stake[netuid] + first_row = True + + for hotkey, info in hotkeys_info.items(): + stake_display = info["stake"] + claimable_display = info["claimable"] + hotkey_display = f"{hotkey[:8]}...{hotkey[-8:]}" + netuid_display = str(netuid) if first_row else "" + table.add_row( + netuid_display, + f"{stake_display.tao:.4f} {stake_display.unit}", + f"{claimable_display.tao:.4f} {claimable_display.unit}", + hotkey_display, + info.get("identity", "~"), + ) + first_row = False + + console.print(table) From c2e20c7724f17f63ca5bfa883b79abd0a9fbb8d7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 17:18:07 -0700 Subject: [PATCH 24/49] _prompt_claim_selection --- bittensor_cli/src/commands/stake/claim.py | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index c7e62d629..679561232 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -139,6 +139,51 @@ async def set_claim_type( return False, f"Error setting root claim type: {e}", None +def _prompt_claim_selection(claimable_stake: dict) -> Optional[list[int]]: + """Prompts user to select up to 5 netuids to claim from""" + + available_netuids = sorted(claimable_stake.keys()) + while True: + netuid_input = Prompt.ask( + "Enter up to 5 netuids to claim from (comma-separated)", + default=",".join(str(n) for n in available_netuids), + ) + + try: + if "," in netuid_input: + selected = [int(n.strip()) for n in netuid_input.split(",")] + else: + selected = [int(netuid_input.strip())] + except ValueError: + err_console.print( + ":cross_mark: [red]Invalid input. Please enter numbers only.[/red]" + ) + continue + + if len(selected) > 5: + err_console.print( + f":cross_mark: [red]You selected {len(selected)} netuids. Maximum is 5. Please try again.[/red]" + ) + continue + + if len(selected) == 0: + err_console.print( + ":cross_mark: [red]Please select at least one netuid.[/red]" + ) + continue + + invalid_netuids = [n for n in selected if n not in available_netuids] + if invalid_netuids: + err_console.print( + f":cross_mark: [red]Invalid netuids: {', '.join(map(str, invalid_netuids))}[/red]" + ) + continue + + selected = list(dict.fromkeys(selected)) + + return selected + + def _print_claimable_table(wallet: Wallet, claimable_stake: dict): """Prints claimable stakes table grouped by netuid""" From 847751322367207149200793ffce1ea4f6dffa32 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 17:18:22 -0700 Subject: [PATCH 25/49] wip --- bittensor_cli/src/commands/stake/claim.py | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 679561232..d86c4e313 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -139,6 +139,71 @@ async def set_claim_type( return False, f"Error setting root claim type: {e}", None +async def process_pending_claims( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuids: Optional[list[int]] = None, + prompt: bool = True, +) -> tuple[bool, str, Optional[str]]: + """Claims root network emissions for the coldkey across specified subnets""" + + with console.status(":satellite: Discovering claimable emissions..."): + all_stakes, identities = await asyncio.gather( + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address + ), + subtensor.query_all_identities(), + ) + if not all_stakes: + console.print("[yellow]No stakes found for this coldkey[/yellow]") + return True, "No stakes found", None + + current_stakes = { + (stake.hotkey_ss58, stake.netuid): stake for stake in all_stakes + } + claimable_by_hotkey = await subtensor.get_claimable_stakes_batch( + coldkey_ss58=wallet.coldkeypub.ss58_address, + stakes_info=all_stakes, + ) + hotkey_owner_tasks = [ + subtensor.get_hotkey_owner(hotkey, check_exists=False) + for hotkey in claimable_by_hotkey.keys() + ] + hotkey_owners = await asyncio.gather(*hotkey_owner_tasks) + hotkey_to_owner = dict(zip(claimable_by_hotkey.keys(), hotkey_owners)) + + # Consolidate data + claimable_stake = {} + for vali_hotkey, claimable_stakes in claimable_by_hotkey.items(): + vali_coldkey = hotkey_to_owner.get(vali_hotkey, "~") + vali_identity = identities.get(vali_coldkey, {}).get("name", "~") + for netuid, stake in claimable_stakes.items(): + if stake.rao > 0: + if netuid not in claimable_stake: + claimable_stake[netuid] = {} + current_stake = current_stakes.get((vali_hotkey, netuid), None) + claimable_stake[netuid][vali_hotkey] = { + "claimable": stake, + "stake": current_stake, + "coldkey": vali_coldkey, + "identity": vali_identity, + } + + if netuids: + claimable_stake = { + netuid: hotkeys_info + for netuid, hotkeys_info in claimable_stake.items() + if netuid in netuids + } + + if not claimable_stake: + console.print("[yellow]No claimable emissions found[/yellow]") + return True, "No claimable emissions found", None + + _print_claimable_table(wallet, claimable_stake) + selected_netuids = netuids if netuids else _prompt_claim_selection(claimable_stake) + + def _prompt_claim_selection(claimable_stake: dict) -> Optional[list[int]]: """Prompts user to select up to 5 netuids to claim from""" From b199c1d3c055ff1ba40b1921480b6ea9da814ce2 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 17:18:37 -0700 Subject: [PATCH 26/49] add cmd process_pending_claims --- bittensor_cli/src/commands/stake/claim.py | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index d86c4e313..9a46b4ad5 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -203,6 +203,49 @@ async def process_pending_claims( _print_claimable_table(wallet, claimable_stake) selected_netuids = netuids if netuids else _prompt_claim_selection(claimable_stake) + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="claim_root", + call_params={"subnets": selected_netuids}, + ) + extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + console.print(f"\n[dim]Estimated extrinsic fee: {extrinsic_fee.tao:.9f} τ[/dim]") + + if prompt: + if not Confirm.ask("Do you want to proceed?"): + console.print("[yellow]Operation cancelled.[/yellow]") + return False, "Operation cancelled by user", None + + if not (unlock := unlock_key(wallet)).success: + err_console.print( + f":cross_mark: [red]Failed to unlock wallet: {unlock.message}[/red]" + ) + return False, f"Failed to unlock wallet: {unlock.message}", None + + with console.status( + f":satellite: Claiming root emissions for {len(selected_netuids)} subnet(s)...", + spinner="earth", + ): + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) + if success: + console.print( + f"[dark_sea_green3]Successfully claimed root emissions for {len(selected_netuids)} subnet(s)[/dark_sea_green3]" + ) + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) + return ( + True, + f"Successfully claimed root emissions for {len(selected_netuids)} subnet(s)", + ext_id, + ) + else: + err_console.print( + f":cross_mark: [red]Failed to claim root emissions: {err_msg}[/red]" + ) + return False, f"Failed to claim root emissions: {err_msg}", None + def _prompt_claim_selection(claimable_stake: dict) -> Optional[list[int]]: """Prompts user to select up to 5 netuids to claim from""" From 20020c83591f4ad4992a5f48a6d07e60315552da Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 17:21:07 -0700 Subject: [PATCH 27/49] adds cli cmd --- bittensor_cli/cli.py | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e0d6984b0..958c13c40 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -972,6 +972,7 @@ def __init__(self): "swap", rich_help_panel=HELP_PANELS["STAKE"]["MOVEMENT"] )(self.stake_swap) self.stake_app.command("set-claim")(self.stake_set_claim_type) + self.stake_app.command("process-claim")(self.stake_process_claim) # stake-children commands children_app = typer.Typer() @@ -7110,6 +7111,68 @@ def stake_set_claim_type( ) ) + def stake_process_claim( + self, + netuids: Optional[str] = Options.netuids, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + network: Optional[list[str]] = Options.network, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Manually claim accumulated root network emissions for your coldkey. + + [bold]Note:[/bold] The network will eventually process your pending emissions automatically. + However, you can choose to manually claim your emissions with a small extrinsic fee. + + A maximum of 5 netuids can be processed in one call. + + USAGE: + + [green]$[/green] btcli stake process-claim + + Claim from specific netuids: + + [green]$[/green] btcli stake process-claim --netuids 1,2,3 + + Claim with specific wallet: + + [green]$[/green] btcli stake process-claim --netuids 1,2 --wallet-name my_wallet + + """ + self.verbosity_handler(quiet, verbose) + + parsed_netuids = None + if netuids: + parsed_netuids = parse_to_list( + netuids, + int, + "Netuids must be a comma-separated list of ints, e.g., `--netuids 1,2,3`.", + ) + + if len(parsed_netuids) > 5: + print_error("Maximum 5 netuids allowed per claim") + return + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + ) + + return self._run_command( + claim_stake.process_pending_claims( + wallet=wallet, + subtensor=self.initialize_chain(network), + netuids=parsed_netuids, + prompt=prompt, + ) + ) + def liquidity_add( self, network: Optional[list[str]] = Options.network, From df213efb8d3bc1be0bcd0299e635d79865f8d15c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 17:21:21 -0700 Subject: [PATCH 28/49] add help text --- bittensor_cli/cli.py | 8 ++++++-- bittensor_cli/src/__init__.py | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 958c13c40..836703ca8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -971,8 +971,12 @@ def __init__(self): self.stake_app.command( "swap", rich_help_panel=HELP_PANELS["STAKE"]["MOVEMENT"] )(self.stake_swap) - self.stake_app.command("set-claim")(self.stake_set_claim_type) - self.stake_app.command("process-claim")(self.stake_process_claim) + self.stake_app.command( + "set-claim", rich_help_panel=HELP_PANELS["STAKE"]["CLAIM"] + )(self.stake_set_claim_type) + self.stake_app.command( + "process-claim", rich_help_panel=HELP_PANELS["STAKE"]["CLAIM"] + )(self.stake_process_claim) # stake-children commands children_app = typer.Typer() diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 598f97167..04e7de999 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -709,6 +709,7 @@ class RootSudoOnly(Enum): "STAKE_MGMT": "Stake Management", "CHILD": "Child Hotkeys", "MOVEMENT": "Stake Movement", + "CLAIM": "Root Claim Management", }, "SUDO": { "CONFIG": "Subnet Configuration", From 02892cb4da7b6173e50117d0a74e7a437c794593 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 17:53:02 -0700 Subject: [PATCH 29/49] fix stake assignment --- bittensor_cli/src/commands/stake/claim.py | 33 ++++++++++++++--------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 9a46b4ad5..7d0936554 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -7,6 +7,7 @@ from rich import box from bittensor_cli.src import COLORS +from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -173,35 +174,41 @@ async def process_pending_claims( hotkey_to_owner = dict(zip(claimable_by_hotkey.keys(), hotkey_owners)) # Consolidate data - claimable_stake = {} + claimable_stake_info = {} for vali_hotkey, claimable_stakes in claimable_by_hotkey.items(): vali_coldkey = hotkey_to_owner.get(vali_hotkey, "~") vali_identity = identities.get(vali_coldkey, {}).get("name", "~") - for netuid, stake in claimable_stakes.items(): - if stake.rao > 0: - if netuid not in claimable_stake: - claimable_stake[netuid] = {} - current_stake = current_stakes.get((vali_hotkey, netuid), None) - claimable_stake[netuid][vali_hotkey] = { - "claimable": stake, + for netuid, claimable_stake in claimable_stakes.items(): + if claimable_stake.rao > 0: + if netuid not in claimable_stake_info: + claimable_stake_info[netuid] = {} + current_stake = ( + stake_info.stake + if (stake_info := current_stakes.get((vali_hotkey, netuid))) + else Balance.from_rao(0).set_unit(netuid) + ) + claimable_stake_info[netuid][vali_hotkey] = { + "claimable": claimable_stake, "stake": current_stake, "coldkey": vali_coldkey, "identity": vali_identity, } if netuids: - claimable_stake = { + claimable_stake_info = { netuid: hotkeys_info - for netuid, hotkeys_info in claimable_stake.items() + for netuid, hotkeys_info in claimable_stake_info.items() if netuid in netuids } - if not claimable_stake: + if not claimable_stake_info: console.print("[yellow]No claimable emissions found[/yellow]") return True, "No claimable emissions found", None - _print_claimable_table(wallet, claimable_stake) - selected_netuids = netuids if netuids else _prompt_claim_selection(claimable_stake) + _print_claimable_table(wallet, claimable_stake_info) + selected_netuids = ( + netuids if netuids else _prompt_claim_selection(claimable_stake_info) + ) call = await subtensor.substrate.compose_call( call_module="SubtensorModule", From 88781ebe7f5b8db3abc0d585c021c6f932d77033 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 17:56:22 -0700 Subject: [PATCH 30/49] add json output - set_claim_type --- bittensor_cli/src/commands/stake/claim.py | 129 +++++++++++++++------- 1 file changed, 90 insertions(+), 39 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 7d0936554..76888344d 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -1,4 +1,5 @@ import asyncio +import json from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet @@ -13,6 +14,7 @@ err_console, unlock_key, print_extrinsic_id, + json_console, ) if TYPE_CHECKING: @@ -23,6 +25,7 @@ async def set_claim_type( wallet: Wallet, subtensor: "SubtensorInterface", prompt: bool = True, + json_output: bool = False, ) -> tuple[bool, str, Optional[str]]: """ Sets the root claim type for the coldkey. @@ -35,6 +38,7 @@ async def set_claim_type( wallet: Bittensor wallet object subtensor: SubtensorInterface object prompt: Whether to prompt for user confirmation + json_output: Whether to output JSON Returns: tuple[bool, str, Optional[str]]: Tuple containing: @@ -75,14 +79,21 @@ async def set_claim_type( "Select new root claim type", choices=["Swap", "Keep"], default=current_type ) if new_type == current_type: - console.print( - f"[yellow]Root claim type is already set to '{current_type}'. No change needed.[/yellow]" - ) - return ( - True, - "Root claim type is already set to '{current_type}'. No change needed.", - None, - ) + msg = f"Root claim type is already set to '{current_type}'. No change needed." + console.print(f"[yellow]{msg}[/yellow]") + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": msg, + "extrinsic_identifier": None, + "old_type": current_type, + "new_type": current_type, + } + ) + ) + return True, msg, None if prompt: console.print( @@ -99,45 +110,85 @@ async def set_claim_type( ) if not Confirm.ask("\nDo you want to proceed?"): - console.print("[yellow]Operation cancelled.[/yellow]") - return False, "Operation cancelled.", None + msg = "Operation cancelled." + console.print(f"[yellow]{msg}[/yellow]") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "message": msg, + "extrinsic_identifier": None, + "old_type": current_type, + "new_type": new_type, + } + ) + ) + return False, msg, None if not (unlock := unlock_key(wallet)).success: - err_console.print( - f":cross_mark: [red]Failed to unlock wallet: {unlock.message}[/red]" - ) - return False, f"Failed to unlock wallet: {unlock.message}", None + msg = f"Failed to unlock wallet: {unlock.message}" + err_console.print(f":cross_mark: [red]{msg}[/red]") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "message": msg, + "extrinsic_identifier": None, + "old_type": current_type, + "new_type": new_type, + } + ) + ) + return False, msg, None with console.status( f":satellite: Setting root claim type to '{new_type}'...", spinner="earth" ): - try: - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="set_root_claim_type", - call_params={"new_root_claim_type": new_type}, - ) - success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet - ) - if success: - console.print( - f":white_heavy_check_mark: [green]Successfully set root claim type to '{new_type}'[/green]" - ) - ext_id = await ext_receipt.get_extrinsic_identifier() - await print_extrinsic_id(ext_receipt) - return True, f"Successfully set root claim type to '{new_type}'", ext_id - else: - err_console.print( - f":cross_mark: [red]Failed to set root claim type: {err_msg}[/red]" + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_root_claim_type", + call_params={"new_root_claim_type": new_type}, + ) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) + if success: + ext_id = await ext_receipt.get_extrinsic_identifier() + msg = f"Successfully set root claim type to '{new_type}'" + console.print(f":white_heavy_check_mark: [green]{msg}[/green]") + await print_extrinsic_id(ext_receipt) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": msg, + "extrinsic_identifier": ext_id, + "old_type": current_type, + "new_type": new_type, + } + ) ) - return False, f"Failed to set root claim type: {err_msg}", None + return True, msg, ext_id - except Exception as e: - err_console.print( - f":cross_mark: [red]Error setting root claim type: {e}[/red]" - ) - return False, f"Error setting root claim type: {e}", None + else: + msg = f"Failed to set root claim type: {err_msg}" + err_console.print(f":cross_mark: [red]{msg}[/red]") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "message": msg, + "extrinsic_identifier": None, + "old_type": current_type, + "new_type": new_type, + } + ) + ) + return False, msg, None async def process_pending_claims( From 6473aee79a4a5e163666f9baad5ad7522e418f76 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 17:56:57 -0700 Subject: [PATCH 31/49] update console status --- bittensor_cli/src/commands/stake/claim.py | 65 ++++++++++++----------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 76888344d..9ca558d3e 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -154,41 +154,42 @@ async def set_claim_type( success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet ) - if success: - ext_id = await ext_receipt.get_extrinsic_identifier() - msg = f"Successfully set root claim type to '{new_type}'" - console.print(f":white_heavy_check_mark: [green]{msg}[/green]") - await print_extrinsic_id(ext_receipt) - if json_output: - json_console.print( - json.dumps( - { - "success": True, - "message": msg, - "extrinsic_identifier": ext_id, - "old_type": current_type, - "new_type": new_type, - } - ) + + if success: + ext_id = await ext_receipt.get_extrinsic_identifier() + msg = f"Successfully set root claim type to '{new_type}'" + console.print(f":white_heavy_check_mark: [green]{msg}[/green]") + await print_extrinsic_id(ext_receipt) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": msg, + "extrinsic_identifier": ext_id, + "old_type": current_type, + "new_type": new_type, + } ) - return True, msg, ext_id + ) + return True, msg, ext_id - else: - msg = f"Failed to set root claim type: {err_msg}" - err_console.print(f":cross_mark: [red]{msg}[/red]") - if json_output: - json_console.print( - json.dumps( - { - "success": False, - "message": msg, - "extrinsic_identifier": None, - "old_type": current_type, - "new_type": new_type, - } - ) + else: + msg = f"Failed to set root claim type: {err_msg}" + err_console.print(f":cross_mark: [red]{msg}[/red]") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "message": msg, + "extrinsic_identifier": None, + "old_type": current_type, + "new_type": new_type, + } ) - return False, msg, None + ) + return False, msg, None async def process_pending_claims( From 26f36bb5d57574030bed6b48f4c3da73809ed0b6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 18:06:40 -0700 Subject: [PATCH 32/49] add json process_pending_claims --- bittensor_cli/src/commands/stake/claim.py | 107 +++++++++++++++++----- 1 file changed, 85 insertions(+), 22 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 9ca558d3e..38119deec 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -197,6 +197,7 @@ async def process_pending_claims( subtensor: "SubtensorInterface", netuids: Optional[list[int]] = None, prompt: bool = True, + json_output: bool = False, ) -> tuple[bool, str, Optional[str]]: """Claims root network emissions for the coldkey across specified subnets""" @@ -208,8 +209,20 @@ async def process_pending_claims( subtensor.query_all_identities(), ) if not all_stakes: - console.print("[yellow]No stakes found for this coldkey[/yellow]") - return True, "No stakes found", None + msg = "No stakes found for this coldkey" + console.print(f"[yellow]{msg}[/yellow]") + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": msg, + "extrinsic_identifier": None, + "netuids": [], + } + ) + ) + return True, msg, None current_stakes = { (stake.hotkey_ss58, stake.netuid): stake for stake in all_stakes @@ -254,8 +267,20 @@ async def process_pending_claims( } if not claimable_stake_info: - console.print("[yellow]No claimable emissions found[/yellow]") - return True, "No claimable emissions found", None + msg = "No claimable emissions found" + console.print(f"[yellow]{msg}[/yellow]") + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": msg, + "extrinsic_identifier": None, + "netuids": [], + } + ) + ) + return True, msg, None _print_claimable_table(wallet, claimable_stake_info) selected_netuids = ( @@ -272,14 +297,36 @@ async def process_pending_claims( if prompt: if not Confirm.ask("Do you want to proceed?"): - console.print("[yellow]Operation cancelled.[/yellow]") - return False, "Operation cancelled by user", None + msg = "Operation cancelled by user" + console.print(f"[yellow]{msg}[/yellow]") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "message": msg, + "extrinsic_identifier": None, + "netuids": selected_netuids, + } + ) + ) + return False, msg, None if not (unlock := unlock_key(wallet)).success: - err_console.print( - f":cross_mark: [red]Failed to unlock wallet: {unlock.message}[/red]" - ) - return False, f"Failed to unlock wallet: {unlock.message}", None + msg = f"Failed to unlock wallet: {unlock.message}" + err_console.print(f":cross_mark: [red]{msg}[/red]") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "message": msg, + "extrinsic_identifier": None, + "netuids": selected_netuids, + } + ) + ) + return False, msg, None with console.status( f":satellite: Claiming root emissions for {len(selected_netuids)} subnet(s)...", @@ -289,21 +336,37 @@ async def process_pending_claims( call, wallet ) if success: - console.print( - f"[dark_sea_green3]Successfully claimed root emissions for {len(selected_netuids)} subnet(s)[/dark_sea_green3]" - ) ext_id = await ext_receipt.get_extrinsic_identifier() + msg = f"Successfully claimed root emissions for {len(selected_netuids)} subnet(s)" + console.print(f"[dark_sea_green3]{msg}[/dark_sea_green3]") await print_extrinsic_id(ext_receipt) - return ( - True, - f"Successfully claimed root emissions for {len(selected_netuids)} subnet(s)", - ext_id, - ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": msg, + "extrinsic_identifier": ext_id, + "netuids": selected_netuids, + } + ) + ) + return True, msg, ext_id else: - err_console.print( - f":cross_mark: [red]Failed to claim root emissions: {err_msg}[/red]" - ) - return False, f"Failed to claim root emissions: {err_msg}", None + msg = f"Failed to claim root emissions: {err_msg}" + err_console.print(f":cross_mark: [red]{msg}[/red]") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "message": msg, + "extrinsic_identifier": None, + "netuids": selected_netuids, + } + ) + ) + return False, msg, None def _prompt_claim_selection(claimable_stake: dict) -> Optional[list[int]]: From 0f57d32da53ca07d155f38ba3d6a44b3a3f02338 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 18:06:59 -0700 Subject: [PATCH 33/49] json_output update to cli --- bittensor_cli/cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 836703ca8..2a13fb179 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7082,6 +7082,7 @@ def stake_set_claim_type( prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Set the root claim type for your coldkey. @@ -7100,7 +7101,7 @@ def stake_set_claim_type( [green]$[/green] btcli root set-claim-type --wallet-name my_wallet """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -7112,6 +7113,7 @@ def stake_set_claim_type( wallet=wallet, subtensor=self.initialize_chain(network), prompt=prompt, + json_output=json_output, ) ) @@ -7125,6 +7127,7 @@ def stake_process_claim( prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Manually claim accumulated root network emissions for your coldkey. @@ -7147,7 +7150,7 @@ def stake_process_claim( [green]$[/green] btcli stake process-claim --netuids 1,2 --wallet-name my_wallet """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) parsed_netuids = None if netuids: @@ -7174,6 +7177,7 @@ def stake_process_claim( subtensor=self.initialize_chain(network), netuids=parsed_netuids, prompt=prompt, + json_output=json_output, ) ) From 37c62e84455c25564d591a848d9dd70a5b0b0415 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 18:18:19 -0700 Subject: [PATCH 34/49] add verbose functionality --- bittensor_cli/cli.py | 1 + bittensor_cli/src/commands/stake/claim.py | 28 ++++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2a13fb179..162145d91 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7178,6 +7178,7 @@ def stake_process_claim( netuids=parsed_netuids, prompt=prompt, json_output=json_output, + verbose=verbose, ) ) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 38119deec..81af06f0a 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -15,6 +15,7 @@ unlock_key, print_extrinsic_id, json_console, + millify_tao, ) if TYPE_CHECKING: @@ -198,6 +199,7 @@ async def process_pending_claims( netuids: Optional[list[int]] = None, prompt: bool = True, json_output: bool = False, + verbose: bool = False, ) -> tuple[bool, str, Optional[str]]: """Claims root network emissions for the coldkey across specified subnets""" @@ -282,7 +284,7 @@ async def process_pending_claims( ) return True, msg, None - _print_claimable_table(wallet, claimable_stake_info) + _print_claimable_table(wallet, claimable_stake_info, verbose) selected_netuids = ( netuids if netuids else _prompt_claim_selection(claimable_stake_info) ) @@ -414,7 +416,9 @@ def _prompt_claim_selection(claimable_stake: dict) -> Optional[list[int]]: return selected -def _print_claimable_table(wallet: Wallet, claimable_stake: dict): +def _print_claimable_table( + wallet: Wallet, claimable_stake: dict, verbose: bool = False +): """Prints claimable stakes table grouped by netuid""" table = Table( @@ -438,14 +442,26 @@ def _print_claimable_table(wallet: Wallet, claimable_stake: dict): first_row = True for hotkey, info in hotkeys_info.items(): + hotkey_display = hotkey if verbose else f"{hotkey[:8]}...{hotkey[-8:]}" + netuid_display = str(netuid) if first_row else "" + stake_display = info["stake"] + stake_formatted = ( + f"{stake_display.tao:.4f} {stake_display.unit}" + if verbose + else f"{millify_tao(stake_display.tao)} {stake_display.unit}" + ) + claimable_display = info["claimable"] - hotkey_display = f"{hotkey[:8]}...{hotkey[-8:]}" - netuid_display = str(netuid) if first_row else "" + claimable_formatted = ( + f"{claimable_display.tao:.4f} {claimable_display.unit}" + if verbose + else f"{millify_tao(claimable_display.tao)} {claimable_display.unit}" + ) table.add_row( netuid_display, - f"{stake_display.tao:.4f} {stake_display.unit}", - f"{claimable_display.tao:.4f} {claimable_display.unit}", + stake_formatted, + claimable_formatted, hotkey_display, info.get("identity", "~"), ) From 07e9c4266ecdddd2342e4213646e2ffd6cc505a5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 18:23:50 -0700 Subject: [PATCH 35/49] update methods --- bittensor_cli/src/bittensor/subtensor_interface.py | 13 +++++++------ bittensor_cli/src/commands/stake/claim.py | 2 +- bittensor_cli/src/commands/stake/list.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 12e07fdc7..d10703029 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1975,7 +1975,8 @@ async def get_claimed_amount_all_netuids( ) return total_claimed - async def claimable_rate_all_netuids( + + async def get_claimable_rate_all_netuids( self, hotkey_ss58: str, block_hash: Optional[str] = None, @@ -2005,7 +2006,7 @@ async def claimable_rate_all_netuids( bits_list = next(iter(query)) return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list} - async def claimable_rate_netuid( + async def get_claimable_rate_netuid( self, hotkey_ss58: str, netuid: int, @@ -2023,14 +2024,14 @@ async def claimable_rate_netuid( Returns: float: The rate of claimable stake from validator's hotkey for provided subnet. """ - all_rates = await self.claimable_rate_all_netuids( + all_rates = await self.get_claimable_rate_all_netuids( hotkey_ss58=hotkey_ss58, block_hash=block_hash, reuse_block=reuse_block, ) return all_rates.get(netuid, 0.0) - async def get_claimable_stake( + async def get_claimable_stake_for_netuid( self, coldkey_ss58: str, hotkey_ss58: str, @@ -2060,7 +2061,7 @@ async def get_claimable_stake( netuid=0, block_hash=block_hash, ), - self.claimable_rate_netuid( + self.get_claimable_rate_netuid( hotkey_ss58=hotkey_ss58, netuid=netuid, block_hash=block_hash, @@ -2084,7 +2085,7 @@ async def get_claimable_stake( Balance.from_rao(0).set_unit(netuid=netuid), ) - async def get_claimable_stakes_batch( + async def get_claimable_stakes_for_coldkey( self, coldkey_ss58: str, stakes_info: list["StakeInfo"], diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 81af06f0a..02ca313b3 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -229,7 +229,7 @@ async def process_pending_claims( current_stakes = { (stake.hotkey_ss58, stake.netuid): stake for stake in all_stakes } - claimable_by_hotkey = await subtensor.get_claimable_stakes_batch( + claimable_by_hotkey = await subtensor.get_claimable_stakes_for_coldkey( coldkey_ss58=wallet.coldkeypub.ss58_address, stakes_info=all_stakes, ) diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index baeb1f392..148da59d7 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -52,7 +52,7 @@ async def get_stake_data(block_hash_: str = None): claimable_amounts = {} if sub_stakes_: - claimable_amounts = await subtensor.get_claimable_stakes_batch( + claimable_amounts = await subtensor.get_claimable_stakes_for_coldkey( coldkey_ss58=coldkey_address, stakes_info=sub_stakes_, block_hash=block_hash_, From 6ccfcbce0b694cde4dc2e2ab315d12967ee60a73 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 18:28:10 -0700 Subject: [PATCH 36/49] final touches --- bittensor_cli/src/commands/stake/claim.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 02ca313b3..30f2052bc 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -204,11 +204,12 @@ async def process_pending_claims( """Claims root network emissions for the coldkey across specified subnets""" 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 + coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=block_hash ), - subtensor.query_all_identities(), + subtensor.query_all_identities(block_hash=block_hash), ) if not all_stakes: msg = "No stakes found for this coldkey" @@ -232,9 +233,12 @@ async def process_pending_claims( claimable_by_hotkey = await subtensor.get_claimable_stakes_for_coldkey( coldkey_ss58=wallet.coldkeypub.ss58_address, stakes_info=all_stakes, + block_hash=block_hash, ) hotkey_owner_tasks = [ - subtensor.get_hotkey_owner(hotkey, check_exists=False) + subtensor.get_hotkey_owner( + hotkey, check_exists=False, block_hash=block_hash + ) for hotkey in claimable_by_hotkey.keys() ] hotkey_owners = await asyncio.gather(*hotkey_owner_tasks) From 5fa68e31746319cf478b21435ffeb947636d76c8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 18:31:39 -0700 Subject: [PATCH 37/49] add aliases --- bittensor_cli/cli.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 162145d91..ff8785336 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1151,6 +1151,16 @@ def __init__(self): self.sudo_app.command("get_take", hidden=True)(self.sudo_get_take) self.sudo_app.command("set_take", hidden=True)(self.sudo_set_take) + # Stake + self.stake_app.command( + "claim", + hidden=True, + )(self.stake_set_claim_type) + self.stake_app.command( + "unclaim", + hidden=True, + )(self.stake_set_claim_type) + # Crowdloan self.app.add_typer( self.crowd_app, From 718e1ca82d84cc6662fe6edc35750e3b6f1ab264 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 18:54:58 -0700 Subject: [PATCH 38/49] add console status --- bittensor_cli/src/commands/stake/claim.py | 2 +- bittensor_cli/src/commands/subnets/subnets.py | 78 +++++++++---------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 30f2052bc..996453301 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -282,7 +282,7 @@ async def process_pending_claims( "success": True, "message": msg, "extrinsic_identifier": None, - "netuids": [], + "netuids": netuids, } ) ) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 3671c4709..c00467985 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -905,21 +905,21 @@ async def show( ) -> Optional[str]: async def show_root(): # TODO json_output for this, don't forget - block_hash = await subtensor.substrate.get_chain_head() - - ( - all_subnets, - root_state, - identities, - old_identities, - root_claim_types, - ) = await asyncio.gather( - subtensor.all_subnets(block_hash=block_hash), - subtensor.get_subnet_state(netuid=0, block_hash=block_hash), - subtensor.query_all_identities(block_hash=block_hash), - subtensor.get_delegate_identities(block_hash=block_hash), - subtensor.get_all_coldkeys_claim_type(block_hash=block_hash), - ) + with console.status(":satellite: Retrieving root network information..."): + block_hash = await subtensor.substrate.get_chain_head() + ( + all_subnets, + root_state, + identities, + old_identities, + root_claim_types, + ) = await asyncio.gather( + subtensor.all_subnets(block_hash=block_hash), + subtensor.get_subnet_state(netuid=0, block_hash=block_hash), + subtensor.query_all_identities(block_hash=block_hash), + subtensor.get_delegate_identities(block_hash=block_hash), + subtensor.get_all_coldkeys_claim_type(block_hash=block_hash), + ) root_info = next((s for s in all_subnets if s.netuid == 0), None) if root_info is None: print_error("The root subnet does not exist") @@ -1131,32 +1131,32 @@ async def show_subnet( mechanism_id: Optional[int], mechanism_count: Optional[int], ): - if not await subtensor.subnet_exists(netuid=netuid): - err_console.print(f"[red]Subnet {netuid} does not exist[/red]") - return False - - block_hash = await subtensor.substrate.get_chain_head() - ( - subnet_info, - identities, - old_identities, - current_burn_cost, - root_claim_types, - ) = await asyncio.gather( - subtensor.subnet(netuid=netuid_, block_hash=block_hash), - subtensor.query_all_identities(block_hash=block_hash), - subtensor.get_delegate_identities(block_hash=block_hash), - subtensor.get_hyperparameter( - param_name="Burn", netuid=netuid_, block_hash=block_hash - ), - subtensor.get_all_coldkeys_claim_type(block_hash=block_hash), - ) + with console.status(":satellite: Retrieving subnet information..."): + block_hash = await subtensor.substrate.get_chain_head() + if not await subtensor.subnet_exists(netuid=netuid_, block_hash=block_hash): + err_console.print(f"[red]Subnet {netuid_} does not exist[/red]") + return False + ( + subnet_info, + identities, + old_identities, + current_burn_cost, + root_claim_types, + ) = await asyncio.gather( + subtensor.subnet(netuid=netuid_, block_hash=block_hash), + subtensor.query_all_identities(block_hash=block_hash), + subtensor.get_delegate_identities(block_hash=block_hash), + subtensor.get_hyperparameter( + param_name="Burn", netuid=netuid_, block_hash=block_hash + ), + subtensor.get_all_coldkeys_claim_type(block_hash=block_hash), + ) - selected_mechanism_id = mechanism_id or 0 + selected_mechanism_id = mechanism_id or 0 - metagraph_info = await subtensor.get_mechagraph_info( - netuid_, selected_mechanism_id, block_hash=block_hash - ) + metagraph_info = await subtensor.get_mechagraph_info( + netuid_, selected_mechanism_id, block_hash=block_hash + ) if metagraph_info is None: print_error( From 27683fa446688ffe0af9cdcd1f22991378704a52 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 18:57:19 -0700 Subject: [PATCH 39/49] dont prompt for HK --- 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 ff8785336..5c29d6dc9 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7116,7 +7116,7 @@ def stake_set_claim_type( wallet_name, wallet_path, wallet_hotkey, - ask_for=[WO.NAME, WO.HOTKEY], + ask_for=[WO.NAME], ) return self._run_command( claim_stake.set_claim_type( From 3bd237a754a5598e2229108af8e2324c43205075 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 19:00:11 -0700 Subject: [PATCH 40/49] ruff --- bittensor_cli/src/bittensor/subtensor_interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index d10703029..3bab9ebf4 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1975,7 +1975,6 @@ async def get_claimed_amount_all_netuids( ) return total_claimed - async def get_claimable_rate_all_netuids( self, hotkey_ss58: str, From 8d151445689a6c06e8ab3601e10cbb5df5b7b71e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 31 Oct 2025 19:59:51 -0700 Subject: [PATCH 41/49] add guardrails --- bittensor_cli/src/bittensor/subtensor_interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 3bab9ebf4..11c15ce5f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2168,10 +2168,10 @@ async def get_claimable_stakes_for_coldkey( results = {} for hotkey, netuid in target_pairs: root_stake = root_stakes[hotkey] - rate = claimable_rates[hotkey][netuid] + rate = claimable_rates[hotkey].get(netuid, 0) claimable_stake = rate * root_stake - already_claimed = claimed_amounts[(hotkey, netuid)] - net_claimable = claimable_stake - already_claimed + already_claimed = claimed_amounts.get((hotkey, netuid), 0) + net_claimable = max(claimable_stake - already_claimed, 0) if hotkey not in results: results[hotkey] = {} results[hotkey][netuid] = net_claimable.set_unit(netuid) From af1d5716a246baf22d084515fe7852ac639d705f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 4 Nov 2025 15:03:02 -0800 Subject: [PATCH 42/49] update ordering of RootClaimed --- bittensor_cli/src/bittensor/subtensor_interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 11c15ce5f..792dc8c4d 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -39,6 +39,7 @@ console, err_console, decode_hex_identity_dict, + i64f64_to_float, validate_chain_endpoint, u16_normalized_float, U16_MAX, @@ -1937,7 +1938,7 @@ async def get_claimed_amount( query = await self.query( module="SubtensorModule", storage_function="RootClaimed", - params=[hotkey_ss58, coldkey_ss58, netuid], + params=[netuid, hotkey_ss58, coldkey_ss58], block_hash=block_hash, reuse_block_hash=reuse_block, ) @@ -2139,7 +2140,7 @@ async def get_claimable_stakes_for_coldkey( await self.substrate.create_storage_key( "SubtensorModule", "RootClaimed", - [hotkey, coldkey_ss58, netuid], + [netuid, hotkey, coldkey_ss58], block_hash=block_hash, ) ) From 9e740c29d3ce95462e05d3d351bce1343403c0be Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 4 Nov 2025 15:04:22 -0800 Subject: [PATCH 43/49] add get_all_subnet_ema_tao_inflow --- .../src/bittensor/subtensor_interface.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 792dc8c4d..75534cc1f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2228,6 +2228,37 @@ async def get_subnet_prices( return map_ + async def get_all_subnet_ema_tao_inflow( + self, + block_hash: Optional[str] = None, + page_size: int = 100, + ) -> dict[int, tuple[int, float]]: + """ + Query EMA TAO inflow for all subnets. + + This represents the exponential moving average of TAO flowing + into or out of a subnet. Negative values indicate net outflow. + + Args: + block_hash: Optional block hash to query at. + page_size: The page size for batch queries (default: 100). + + Returns: + Dict mapping netuid -> (block_number, Balance(EMA TAO inflow)). + """ + query = await self.substrate.query_map( + module="SubtensorModule", + storage_function="SubnetEmaTaoFlow", + page_size=page_size, + block_hash=block_hash, + ) + tao_inflow_ema = {} + async for netuid, value in query: + block_updated, _tao_ema = value + ema_value = fixed_to_float(_tao_ema) + tao_inflow_ema[netuid] = (block_updated, Balance.from_rao(ema_value)) + return tao_inflow_ema + async def best_connection(networks: list[str]): """ From 43180061d021fdbf8e1baebf9579d6e8f8173d74 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 4 Nov 2025 15:06:40 -0800 Subject: [PATCH 44/49] add tao ema inflow in subnets list --- bittensor_cli/src/commands/subnets/subnets.py | 99 +++++++++++++++++-- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index c00467985..f5982d43e 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -223,10 +223,11 @@ async def subnets_list( async def fetch_subnet_data(): block_hash = await subtensor.substrate.get_chain_head() - subnets_, mechanisms, block_number_ = await asyncio.gather( + subnets_, mechanisms, block_number_, ema_flows = await asyncio.gather( subtensor.all_subnets(block_hash=block_hash), subtensor.get_all_subnet_mechanisms(block_hash=block_hash), subtensor.substrate.get_block_number(block_hash=block_hash), + subtensor.get_all_subnet_ema_tao_inflow(block_hash=block_hash), ) # Sort subnets by market cap, keeping the root subnet in the first position @@ -237,7 +238,7 @@ async def fetch_subnet_data(): reverse=True, ) sorted_subnets = [root_subnet] + other_subnets - return sorted_subnets, block_number_, mechanisms + return sorted_subnets, block_number_, mechanisms, ema_flows def calculate_emission_stats( subnets_: list, block_number_: int @@ -259,6 +260,38 @@ def calculate_emission_stats( ) return total_tao_emitted, percentage_string + def format_ema_tao_value(value: float, verbose: bool = False) -> str: + """ + Format EMA TAO inflow value with adaptive precision. + """ + import math + + abs_value = abs(value) + if abs_value == 0: + return "0.00" + elif abs_value >= 1.0: + # Large values: 2-4 decimal places + return f"{abs_value:,.4f}" if verbose else f"{abs_value:,.2f}" + elif abs_value >= 0.01: + # Medium values: 4 decimal places + return f"{abs_value:.4f}" + else: + # Low values: upto 12 decimal places + decimal_places = -int(math.floor(math.log10(abs_value))) + 2 + decimal_places = min(decimal_places, 12) + return f"{abs_value:.{decimal_places}f}" + + def format_ema_flow_cell(ema_value: float) -> tuple[str, str]: + """ + Format EMA TAO inflow value with adaptive precision. + """ + if ema_value > 0: + return "pale_green3", "+" + elif ema_value < 0: + return "hot_pink3", "-" + else: + return "blue", "" + def define_table( total_emissions: float, total_rate: float, @@ -302,6 +335,11 @@ def define_table( justify="left", footer=f"τ {total_emissions}", ) + defined_table.add_column( + f"[bold white]Net Inflow EMA ({Balance.get_unit(0)})", + style=COLOR_PALETTE["POOLS"]["ALPHA_OUT"], + justify="left", + ) defined_table.add_column( f"[bold white]P ({Balance.get_unit(0)}_in, {Balance.get_unit(1)}_in)", style=COLOR_PALETTE["STAKE"]["TAO"], @@ -333,7 +371,7 @@ def define_table( return defined_table # Non-live mode - def _create_table(subnets_, block_number_, mechanisms): + def _create_table(subnets_, block_number_, mechanisms, ema_flows): rows = [] _, percentage_string = calculate_emission_stats(subnets_, block_number_) @@ -399,6 +437,19 @@ def _create_table(subnets_, block_number_, mechanisms): f" {get_subnet_name(subnet)}" ) emission_cell = f"τ {emission_tao:,.4f}" + + # TAO Inflow EMA cell + if netuid in ema_flows: + _, _ema_value = ema_flows[netuid] + ema_value = _ema_value.tao + ema_color, ema_sign = format_ema_flow_cell(ema_value) + ema_formatted = format_ema_tao_value(ema_value, verbose) + ema_flow_cell = ( + f"[{ema_color}]{ema_sign}τ {ema_formatted}[/{ema_color}]" + ) + else: + ema_flow_cell = "-" + price_cell = f"{price_value} τ/{symbol}" alpha_out_cell = ( f"{alpha_out_value} {symbol}" @@ -422,6 +473,7 @@ def _create_table(subnets_, block_number_, mechanisms): price_cell, # Rate τ_in/α_in market_cap_cell, # Market Cap emission_cell, # Emission (τ) + ema_flow_cell, # TAO Flow EMA liquidity_cell, # Liquidity (t_in, a_in) alpha_out_cell, # Stake α_out supply_cell, # Supply @@ -448,7 +500,7 @@ def _create_table(subnets_, block_number_, mechanisms): defined_table.add_row(*row) return defined_table - def dict_table(subnets_, block_number_, mechanisms) -> dict: + def dict_table(subnets_, block_number_, mechanisms, ema_flows) -> dict: subnet_rows = {} total_tao_emitted, _ = calculate_emission_stats(subnets_, block_number_) total_emissions = 0.0 @@ -478,12 +530,18 @@ def dict_table(subnets_, block_number_, mechanisms) -> dict: ), "sn_tempo": (subnet.tempo if netuid != 0 else None), } + tao_flow_ema = None + if netuid in ema_flows: + _, ema_value = ema_flows[netuid] + tao_flow_ema = ema_value.tao + subnet_rows[netuid] = { "netuid": netuid, "subnet_name": subnet_name, "price": price_value, "market_cap": market_cap, "emission": emission_tao, + "tao_flow_ema": tao_flow_ema, "liquidity": {"tao_in": tao_in, "alpha_in": alpha_in}, "alpha_out": alpha_out, "supply": supply, @@ -501,7 +559,9 @@ def dict_table(subnets_, block_number_, mechanisms) -> dict: return output # Live mode - def create_table_live(subnets_, previous_data_, block_number_, mechanisms): + def create_table_live( + subnets_, previous_data_, block_number_, mechanisms, ema_flows + ): def format_cell( value, previous_value, unit="", unit_first=False, precision=4, millify=False ): @@ -619,10 +679,16 @@ def format_liquidity_cell( market_cap = (subnet.alpha_in.tao + subnet.alpha_out.tao) * subnet.price.tao supply = subnet.alpha_in.tao + subnet.alpha_out.tao + ema_value = 0 + if netuid in ema_flows: + _, ema_value = ema_flows[netuid] + ema_value = ema_value.tao + # Store current values for comparison current_data[netuid] = { "market_cap": market_cap, "emission_tao": emission_tao, + "tao_flow_ema": ema_value, "alpha_out": subnet.alpha_out.tao, "tao_in": subnet.tao_in.tao, "alpha_in": subnet.alpha_in.tao, @@ -650,6 +716,13 @@ def format_liquidity_cell( unit_first=True, precision=4, ) + + ema_flow_cell = format_cell( + ema_value, + prev.get("tao_flow_ema"), + unit="τ", + precision=6, + ) price_cell = format_cell( subnet.price.tao, prev.get("price"), @@ -733,6 +806,7 @@ def format_liquidity_cell( price_cell, # Rate τ_in/α_in market_cap_cell, # Market Cap emission_cell, # Emission (τ) + ema_flow_cell, # TAO Flow EMA liquidity_cell, # Liquidity (t_in, a_in) alpha_out_cell, # Stake α_out supply_cell, # Supply @@ -784,7 +858,12 @@ def format_liquidity_cell( with Live(console=console, screen=True, auto_refresh=True) as live: try: while True: - subnets, block_number, mechanisms = await fetch_subnet_data() + ( + subnets, + block_number, + mechanisms, + ema_flows, + ) = await fetch_subnet_data() # Update block numbers previous_block = current_block @@ -796,7 +875,7 @@ def format_liquidity_cell( ) table, current_data = create_table_live( - subnets, previous_data, block_number, mechanisms + subnets, previous_data, block_number, mechanisms, ema_flows ) previous_data = current_data progress.reset(progress_task) @@ -822,13 +901,13 @@ def format_liquidity_cell( pass # Ctrl + C else: # Non-live mode - subnets, block_number, mechanisms = await fetch_subnet_data() + subnets, block_number, mechanisms, ema_flows = await fetch_subnet_data() if json_output: json_console.print( - json.dumps(dict_table(subnets, block_number, mechanisms)) + json.dumps(dict_table(subnets, block_number, mechanisms, ema_flows)) ) else: - table = _create_table(subnets, block_number, mechanisms) + table = _create_table(subnets, block_number, mechanisms, ema_flows) console.print(table) return From bfccca5722e2b0c84069186c15943c7a201a0129 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 4 Nov 2025 15:08:59 -0800 Subject: [PATCH 45/49] fix import --- bittensor_cli/src/bittensor/subtensor_interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 75534cc1f..14cecd34f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -39,7 +39,6 @@ console, err_console, decode_hex_identity_dict, - i64f64_to_float, validate_chain_endpoint, u16_normalized_float, U16_MAX, From d327842eb6296da8985b1e5dbb7e45ac8514a4a7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 4 Nov 2025 15:18:02 -0800 Subject: [PATCH 46/49] Add get_subnet_ema_tao_inflow --- .../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 14cecd34f..423516c92 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2258,6 +2258,34 @@ async def get_all_subnet_ema_tao_inflow( tao_inflow_ema[netuid] = (block_updated, Balance.from_rao(ema_value)) return tao_inflow_ema + async def get_subnet_ema_tao_inflow( + self, + netuid: int, + block_hash: Optional[str] = None, + ) -> Balance: + """ + Query EMA TAO inflow for a specific subnet. + + This represents the exponential moving average of TAO flowing + into or out of a subnet. Negative values indicate net outflow. + + Args: + netuid: The unique identifier of the subnet. + block_hash: Optional block hash to query at. + + Returns: + Balance(EMA TAO inflow). + """ + value = await self.substrate.query( + module="SubtensorModule", + storage_function="SubnetEmaTaoFlow", + params=[netuid], + block_hash=block_hash, + ) + _, raw_ema_value = value + ema_value = fixed_to_float(raw_ema_value) + return Balance.from_rao(ema_value) + async def best_connection(networks: list[str]): """ From 693b1878a1d6ac4781c7ac10813ffbe5d06f230b Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 4 Nov 2025 15:19:17 -0800 Subject: [PATCH 47/49] adds inflow ema to subnets metagraph --- bittensor_cli/src/commands/subnets/subnets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index f5982d43e..6255692b7 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1221,6 +1221,7 @@ async def show_subnet( old_identities, current_burn_cost, root_claim_types, + ema_tao_inflow, ) = await asyncio.gather( subtensor.subnet(netuid=netuid_, block_hash=block_hash), subtensor.query_all_identities(block_hash=block_hash), @@ -1229,6 +1230,9 @@ async def show_subnet( param_name="Burn", netuid=netuid_, block_hash=block_hash ), subtensor.get_all_coldkeys_claim_type(block_hash=block_hash), + subtensor.get_subnet_ema_tao_inflow( + netuid=netuid_, block_hash=block_hash + ), ) selected_mechanism_id = mechanism_id or 0 @@ -1527,6 +1531,7 @@ async def show_subnet( f"{total_mech_line}" f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner_coldkey}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n EMA TAO Inflow: [{COLOR_PALETTE['STAKE']['TAO']}]τ {ema_tao_inflow.tao}[/{COLOR_PALETTE['STAKE']['TAO']}]" f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {tao_pool}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{alpha_pool} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" From f33ca1ba41c8fa547a7ebac165a96b555b12fb03 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 4 Nov 2025 15:37:12 -0800 Subject: [PATCH 48/49] bumps version and changelog --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecb08197f..76eb628ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ # Changelog + +## 9.15.0 /2025-11-04 + +* Stop running e2e tests on changelog branches by @thewhaleking in https://github.com/opentensor/btcli/pull/691 +* Feat/root claim by @ibraheem-abe in https://github.com/opentensor/btcli/pull/692 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.14.3...v9.15.0 + ## 9.14.3 /2025-10-30 * Allows for installing on Py 3.14 by @thewhaleking in https://github.com/opentensor/btcli/pull/688 * corrects `--name` param in `wallet set-identity` and `subnets set-identity` which was a duplicate param alias of `--wallet-name` diff --git a/pyproject.toml b/pyproject.toml index 5945a9a38..476479fe9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.14.3" +version = "9.15.0" description = "Bittensor CLI" readme = "README.md" authors = [ From 53822cbe4dfe1be2e2085e25f22d1ca9eb4ed72f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 4 Nov 2025 15:52:04 -0800 Subject: [PATCH 49/49] add none check --- bittensor_cli/src/bittensor/subtensor_interface.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 423516c92..e4fd4d921 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2282,6 +2282,8 @@ async def get_subnet_ema_tao_inflow( params=[netuid], block_hash=block_hash, ) + if not value: + return Balance.from_rao(0) _, raw_ema_value = value ema_value = fixed_to_float(raw_ema_value) return Balance.from_rao(ema_value)