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 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/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7fae9e844..33be9a05b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -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, @@ -970,6 +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", 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() @@ -1144,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, @@ -7066,6 +7083,115 @@ def view_dashboard( ) ) + def stake_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, + json_output: bool = Options.json_output, + ): + """ + 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, json_output) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + ) + return self._run_command( + claim_stake.set_claim_type( + wallet=wallet, + subtensor=self.initialize_chain(network), + prompt=prompt, + json_output=json_output, + ) + ) + + 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, + json_output: bool = Options.json_output, + ): + """ + 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, json_output) + + 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, + json_output=json_output, + verbose=verbose, + ) + ) + def liquidity_add( self, network: Optional[list[str]] = Options.network, 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", diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 2ef90d284..e4fd4d921 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1824,6 +1824,359 @@ async def get_coldkey_swap_schedule_duration( return result + async def get_coldkey_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_coldkeys_claim_type( + 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_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_claimed_amount( + 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=[netuid, hotkey_ss58, coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return Balance.from_rao(query).set_unit(netuid=netuid) + + async def get_claimed_amount_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 get_claimable_rate_all_netuids( + self, + hotkey_ss58: str, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[int, float]: + """Retrieves all root claimable rates from a given hotkey address for all subnets with this validator. + + Args: + hotkey_ss58: The SS58 address of the root validator. + block_hash: The blockchain block hash for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + dict[int, float]: Dictionary mapping netuid to claimable rate. + """ + 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_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.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_for_netuid( + 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, 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.get_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, + ), + ) + + root_claimable_stake = (root_claimable_rate * root_stake).set_unit( + netuid=netuid + ) + # 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_claimable_stakes_for_coldkey( + 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", + [netuid, hotkey, coldkey_ss58], + 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), + ) + + 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].get(netuid, 0) + claimable_stake = rate * root_stake + 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) + return results + async def get_subnet_price( self, netuid: int = None, @@ -1874,6 +2227,67 @@ 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 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, + ) + 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) + async def best_connection(networks: list[str]): """ diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py new file mode 100644 index 000000000..996453301 --- /dev/null +++ b/bittensor_cli/src/commands/stake/claim.py @@ -0,0 +1,474 @@ +import asyncio +import json +from typing import TYPE_CHECKING, Optional + +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.balances import Balance +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + unlock_key, + print_extrinsic_id, + json_console, + millify_tao, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +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. + + 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 + json_output: Whether to output JSON + + 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_coldkey_claim_type( + coldkey_ss58=wallet.coldkeypub.ss58_address + ) + + 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 + ) + if new_type == current_type: + 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( + f"\n[bold]Changing root claim type from '{current_type}' -> '{new_type}'[/bold]\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?"): + 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: + 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" + ): + 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 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, + } + ) + ) + return False, msg, None + + +async def process_pending_claims( + wallet: Wallet, + subtensor: "SubtensorInterface", + 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""" + + with console.status(":satellite: Discovering claimable emissions..."): + block_hash = await subtensor.substrate.get_chain_head() + all_stakes, identities = await asyncio.gather( + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=block_hash + ), + subtensor.query_all_identities(block_hash=block_hash), + ) + if not all_stakes: + 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 + } + 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, block_hash=block_hash + ) + 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_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, 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_info = { + netuid: hotkeys_info + for netuid, hotkeys_info in claimable_stake_info.items() + if netuid in netuids + } + + if not claimable_stake_info: + 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": netuids, + } + ) + ) + return True, msg, None + + _print_claimable_table(wallet, claimable_stake_info, verbose) + selected_netuids = ( + netuids if netuids else _prompt_claim_selection(claimable_stake_info) + ) + + 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?"): + 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: + 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)...", + spinner="earth", + ): + 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 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) + 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: + 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]]: + """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, verbose: bool = False +): + """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(): + 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"] + 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, + stake_formatted, + claimable_formatted, + hotkey_display, + info.get("identity", "~"), + ) + first_row = False + + console.print(table) diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index 4a8e17145..148da59d7 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_for_coldkey( + 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( @@ -244,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 = [] @@ -388,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 @@ -401,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 ] ) @@ -420,6 +479,7 @@ def format_cell( sub_stakes, registered_delegate_info, dynamic_info, + claimable_amounts, ), balance, ) = await asyncio.gather( @@ -487,6 +547,7 @@ def format_cell( sub_stakes, registered_delegate_info, dynamic_info_, + claimable_amounts_live, ) = await get_stake_data(block_hash) selected_stakes = [ stake @@ -508,6 +569,7 @@ def format_cell( selected_stakes, dynamic_info_, hotkey_name, + claimable_amounts_live, previous_data, ) @@ -553,7 +615,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 diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 2d093b276..6255692b7 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 @@ -905,14 +984,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 = 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), - ) + 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") @@ -971,6 +1057,11 @@ async def show_root(): style=COLOR_PALETTE["GENERAL"]["SYMBOL"], justify="left", ) + table.add_column( + "[bold white]Claim Type", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], + justify="center", + ) sorted_hotkeys = sorted( enumerate(root_state.hotkeys), @@ -1001,6 +1092,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 +1115,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 +1167,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: @@ -1114,30 +1210,36 @@ 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, - ) = 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 - ), - ) + 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, + ema_tao_inflow, + ) = 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), + subtensor.get_subnet_ema_tao_inflow( + netuid=netuid_, 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( @@ -1237,6 +1339,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 @@ -1259,6 +1369,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( @@ -1275,6 +1386,7 @@ async def show_subnet( "hotkey": metagraph_info.hotkeys[idx], "coldkey": metagraph_info.coldkeys[idx], "identity": uid_identity, + "claim_type": claim_type, } ) @@ -1340,6 +1452,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) @@ -1413,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']}]" 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 = [