From da7541634eb402f079b15f824537a06a4c7bc009 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 28 Feb 2025 17:27:48 -0800 Subject: [PATCH 1/6] Adds metagraph info --- bittensor_cli/src/bittensor/balances.py | 11 +- bittensor_cli/src/bittensor/chain_data.py | 312 ++++++++++++++++-- .../src/bittensor/subtensor_interface.py | 33 ++ 3 files changed, 325 insertions(+), 31 deletions(-) diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index b8300163..34711f46 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.py @@ -297,17 +297,16 @@ def set_unit(self, netuid: int): return self -def fixed_to_float(fixed: dict) -> float: - # Currently this is stored as a U64F64 +def fixed_to_float(fixed, frac_bits: int = 64, total_bits: int = 128) -> float: + # By default, this is a U64F64 # which is 64 bits of integer and 64 bits of fractional - # uint_bits = 64 - frac_bits = 64 data: int = fixed["bits"] - # Shift bits to extract integer part (assuming 64 bits for integer part) - integer_part = data >> frac_bits + # Logical and to get the fractional part; remaining is the integer part fractional_part = data & (2**frac_bits - 1) + # Shift to get the integer part from the remaining bits + integer_part = data >> (total_bits - frac_bits) frac_float = fractional_part / (2**frac_bits) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 9d71c675..628821ae 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -6,11 +6,12 @@ import netaddr from scalecodec.utils.ss58 import ss58_encode -from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float from bittensor_cli.src.bittensor.networking import int_to_ip from bittensor_cli.src.bittensor.utils import ( SS58_FORMAT, - u16_normalized_float, + u16_normalized_float as u16tf, + u64_normalized_float as u64tf, decode_account_id, ) @@ -57,6 +58,31 @@ def process_stake_data(stake_data, netuid): return decoded_stake_data +def _tbwu(val: int, netuid: Optional[int] = 0) -> Balance: + """Returns a Balance object from a value and unit.""" + return Balance.from_rao(val).set_unit(netuid) + + +def _chr_str(codes: tuple[int]) -> str: + """Converts a tuple of integer Unicode code points into a string.""" + return "".join(map(chr, codes)) + + +def process_nested(data: Union[tuple, dict], chr_transform): + """Processes nested data structures by applying a transformation function to their elements.""" + if isinstance(data, (list, tuple)): + if len(data) > 0 and isinstance(data[0], dict): + return [ + {k: chr_transform(v) for k, v in item.items()} + if item is not None + else None + for item in data + ] + return {} + elif isinstance(data, dict): + return {k: chr_transform(v) for k, v in data.items()} + + @dataclass class AxonInfo: version: int @@ -312,13 +338,13 @@ def _fix_decoded(cls, decoded: Any) -> "NeuronInfo": stake=total_stake, stake_dict=stake_dict, total_stake=total_stake, - rank=u16_normalized_float(decoded.get("rank")), + rank=u16tf(decoded.get("rank")), emission=decoded.get("emission") / 1e9, - incentive=u16_normalized_float(decoded.get("incentive")), - consensus=u16_normalized_float(decoded.get("consensus")), - trust=u16_normalized_float(decoded.get("trust")), - validator_trust=u16_normalized_float(decoded.get("validator_trust")), - dividends=u16_normalized_float(decoded.get("dividends")), + incentive=u16tf(decoded.get("incentive")), + consensus=u16tf(decoded.get("consensus")), + trust=u16tf(decoded.get("trust")), + validator_trust=u16tf(decoded.get("validator_trust")), + dividends=u16tf(decoded.get("dividends")), last_update=decoded.get("last_update"), validator_permit=decoded.get("validator_permit"), weights=[[e[0], e[1]] for e in decoded.get("weights")], @@ -426,22 +452,22 @@ def _fix_decoded(cls, decoded: Union[dict, "NeuronInfoLite"]) -> "NeuronInfoLite coldkey=coldkey, ), coldkey=coldkey, - consensus=u16_normalized_float(consensus), - dividends=u16_normalized_float(dividends), + consensus=u16tf(consensus), + dividends=u16tf(dividends), emission=emission / 1e9, hotkey=hotkey, - incentive=u16_normalized_float(incentive), + incentive=u16tf(incentive), last_update=last_update, netuid=netuid, pruning_score=pruning_score, - rank=u16_normalized_float(rank), + rank=u16tf(rank), stake_dict=stake_dict, stake=stake, total_stake=stake, - trust=u16_normalized_float(trust), + trust=u16tf(trust), uid=uid, validator_permit=validator_permit, - validator_trust=u16_normalized_float(validator_trust), + validator_trust=u16tf(validator_trust), ) return neuron @@ -492,7 +518,7 @@ def _fix_decoded(cls, decoded: "DelegateInfo") -> "DelegateInfo": total_stake=total_stake, nominators=nominators, owner_ss58=owner, - take=u16_normalized_float(decoded.get("take")), + take=u16tf(decoded.get("take")), validator_permits=decoded.get("validator_permits"), registrations=decoded.get("registrations"), return_per_1000=Balance.from_rao(decoded.get("return_per_1000")), @@ -528,7 +554,7 @@ def _fix_decoded(cls, decoded: Any) -> "DelegateInfoLite": if decoded_take == 65535: fixed_take = None else: - fixed_take = u16_normalized_float(decoded_take) + fixed_take = u16tf(decoded_take) return cls( hotkey_ss58=ss58_encode(decoded.get("delegate_ss58"), SS58_FORMAT), @@ -581,7 +607,7 @@ def _fix_decoded(cls, decoded: "SubnetInfo") -> "SubnetInfo": tempo=decoded.get("tempo"), modality=decoded.get("network_modality"), connection_requirements={ - str(int(netuid)): u16_normalized_float(int(req)) + str(int(netuid)): u16tf(int(req)) for (netuid, req) in decoded.get("network_connect") }, emission_value=decoded.get("emission_value"), @@ -844,19 +870,17 @@ def _fix_decoded(cls, decoded: Any) -> "SubnetState": coldkeys=[decode_account_id(val) for val in decoded.get("coldkeys")], active=decoded.get("active"), validator_permit=decoded.get("validator_permit"), - pruning_score=[ - u16_normalized_float(val) for val in decoded.get("pruning_score") - ], + pruning_score=[u16tf(val) for val in decoded.get("pruning_score")], last_update=decoded.get("last_update"), emission=[ Balance.from_rao(val).set_unit(netuid) for val in decoded.get("emission") ], - dividends=[u16_normalized_float(val) for val in decoded.get("dividends")], - incentives=[u16_normalized_float(val) for val in decoded.get("incentives")], - consensus=[u16_normalized_float(val) for val in decoded.get("consensus")], - trust=[u16_normalized_float(val) for val in decoded.get("trust")], - rank=[u16_normalized_float(val) for val in decoded.get("rank")], + dividends=[u16tf(val) for val in decoded.get("dividends")], + incentives=[u16tf(val) for val in decoded.get("incentives")], + consensus=[u16tf(val) for val in decoded.get("consensus")], + trust=[u16tf(val) for val in decoded.get("trust")], + rank=[u16tf(val) for val in decoded.get("rank")], block_at_registration=decoded.get("block_at_registration"), alpha_stake=[ Balance.from_rao(val).set_unit(netuid) @@ -871,3 +895,241 @@ def _fix_decoded(cls, decoded: Any) -> "SubnetState": ], emission_history=decoded.get("emission_history"), ) + + +@dataclass +class ChainIdentity(InfoBase): + """Dataclass for chain identity information.""" + + name: str + url: str + github: str + image: str + discord: str + description: str + additional: str + + @classmethod + def _from_dict(cls, decoded: dict) -> "ChainIdentity": + """Returns a ChainIdentity object from decoded chain data.""" + return cls( + name=decoded["name"], + url=decoded["url"], + github=decoded["github_repo"], + image=decoded["image"], + discord=decoded["discord"], + description=decoded["description"], + additional=decoded["additional"], + ) + + +@dataclass +class MetagraphInfo(InfoBase): + # Subnet index + netuid: int + + # Name and symbol + name: str + symbol: str + identity: Optional[SubnetIdentity] + network_registered_at: int + + # Keys for owner. + owner_hotkey: str # hotkey + owner_coldkey: str # coldkey + + # Tempo terms. + block: int # block at call. + tempo: int # epoch tempo + last_step: int + blocks_since_last_step: int + + # Subnet emission terms + subnet_emission: Balance # subnet emission via tao + alpha_in: Balance # amount of alpha in reserve + alpha_out: Balance # amount of alpha outstanding + tao_in: Balance # amount of tao injected per block + alpha_out_emission: Balance # amount injected in alpha reserves per block + alpha_in_emission: Balance # amount injected outstanding per block + tao_in_emission: Balance # amount of tao injected per block + pending_alpha_emission: Balance # pending alpha to be distributed + pending_root_emission: Balance # pending tao for root divs to be distributed + subnet_volume: Balance # volume of the subnet in TAO + moving_price: Balance # subnet moving price. + + # Hparams for epoch + rho: int # subnet rho param + kappa: float # subnet kappa param + + # Validator params + min_allowed_weights: float # min allowed weights per val + max_weights_limit: float # max allowed weights per val + weights_version: int # allowed weights version + weights_rate_limit: int # rate limit on weights. + activity_cutoff: int # validator weights cut off period in blocks + max_validators: int # max allowed validators. + + # Registration + num_uids: int + max_uids: int + burn: Balance # current burn cost. + difficulty: float # current difficulty. + registration_allowed: bool # allows registrations. + pow_registration_allowed: bool # pow registration enabled. + immunity_period: int # subnet miner immunity period + min_difficulty: float # min pow difficulty + max_difficulty: float # max pow difficulty + min_burn: Balance # min tao burn + max_burn: Balance # max tao burn + adjustment_alpha: float # adjustment speed for registration params. + adjustment_interval: int # pow and burn adjustment interval + target_regs_per_interval: int # target registrations per interval + max_regs_per_block: int # max registrations per block. + serving_rate_limit: int # axon serving rate limit + + # CR + commit_reveal_weights_enabled: bool # Is CR enabled. + commit_reveal_period: int # Commit reveal interval + + # Bonds + liquid_alpha_enabled: bool # Bonds liquid enabled. + alpha_high: float # Alpha param high + alpha_low: float # Alpha param low + bonds_moving_avg: float # Bonds moving avg + + # Metagraph info. + hotkeys: list[str] # hotkey per UID + coldkeys: list[str] # coldkey per UID + identities: list[Optional[ChainIdentity]] # coldkeys identities + axons: list[AxonInfo] # UID axons. + active: list[bool] # Active per UID + validator_permit: list[bool] # Val permit per UID + pruning_score: list[float] # Pruning per UID + last_update: list[int] # Last update per UID + emission: list[Balance] # Emission per UID + dividends: list[float] # Dividends per UID + incentives: list[float] # Mining incentives per UID + consensus: list[float] # Consensus per UID + trust: list[float] # Trust per UID + rank: list[float] # Rank per UID + block_at_registration: list[int] # Reg block per UID + alpha_stake: list[Balance] # Alpha staked per UID + tao_stake: list[Balance] # TAO staked per UID + total_stake: list[Balance] # Total stake per UID + + # Dividend break down. + tao_dividends_per_hotkey: list[ + tuple[str, Balance] + ] # List of dividend payouts in tao via root. + alpha_dividends_per_hotkey: list[ + tuple[str, Balance] + ] # List of dividend payout in alpha via subnet. + + @classmethod + def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo": + """Returns a MetagraphInfo object from decoded chain data.""" + # Subnet index + _netuid = decoded["netuid"] + + # Name and symbol + decoded.update({"name": bytes(decoded.get("name")).decode()}) + decoded.update({"symbol": bytes(decoded.get("symbol")).decode()}) + for key in ["identities", "identity"]: + raw_data = decoded.get(key) + processed = process_nested(raw_data, _chr_str) + decoded.update({key: processed}) + + return cls( + # Subnet index + netuid=_netuid, + # Name and symbol + name=decoded["name"], + symbol=decoded["symbol"], + identity=decoded["identity"], + network_registered_at=decoded["network_registered_at"], + # Keys for owner. + owner_hotkey=decoded["owner_hotkey"], + owner_coldkey=decoded["owner_coldkey"], + # Tempo terms. + block=decoded["block"], + tempo=decoded["tempo"], + last_step=decoded["last_step"], + blocks_since_last_step=decoded["blocks_since_last_step"], + # Subnet emission terms + subnet_emission=_tbwu(decoded["subnet_emission"]), + alpha_in=_tbwu(decoded["alpha_in"], _netuid), + alpha_out=_tbwu(decoded["alpha_out"], _netuid), + tao_in=_tbwu(decoded["tao_in"]), + alpha_out_emission=_tbwu(decoded["alpha_out_emission"], _netuid), + alpha_in_emission=_tbwu(decoded["alpha_in_emission"], _netuid), + tao_in_emission=_tbwu(decoded["tao_in_emission"]), + pending_alpha_emission=_tbwu(decoded["pending_alpha_emission"], _netuid), + pending_root_emission=_tbwu(decoded["pending_root_emission"]), + subnet_volume=_tbwu(decoded["subnet_volume"], _netuid), + moving_price=Balance.from_tao( + fixed_to_float(decoded.get("moving_price"), 32) + ), + # Hparams for epoch + rho=decoded["rho"], + kappa=decoded["kappa"], + # Validator params + min_allowed_weights=u16tf(decoded["min_allowed_weights"]), + max_weights_limit=u16tf(decoded["max_weights_limit"]), + weights_version=decoded["weights_version"], + weights_rate_limit=decoded["weights_rate_limit"], + activity_cutoff=decoded["activity_cutoff"], + max_validators=decoded["max_validators"], + # Registration + num_uids=decoded["num_uids"], + max_uids=decoded["max_uids"], + burn=_tbwu(decoded["burn"]), + difficulty=u64tf(decoded["difficulty"]), + registration_allowed=decoded["registration_allowed"], + pow_registration_allowed=decoded["pow_registration_allowed"], + immunity_period=decoded["immunity_period"], + min_difficulty=u64tf(decoded["min_difficulty"]), + max_difficulty=u64tf(decoded["max_difficulty"]), + min_burn=_tbwu(decoded["min_burn"]), + max_burn=_tbwu(decoded["max_burn"]), + adjustment_alpha=u64tf(decoded["adjustment_alpha"]), + adjustment_interval=decoded["adjustment_interval"], + target_regs_per_interval=decoded["target_regs_per_interval"], + max_regs_per_block=decoded["max_regs_per_block"], + serving_rate_limit=decoded["serving_rate_limit"], + # CR + commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], + commit_reveal_period=decoded["commit_reveal_period"], + # Bonds + liquid_alpha_enabled=decoded["liquid_alpha_enabled"], + alpha_high=u16tf(decoded["alpha_high"]), + alpha_low=u16tf(decoded["alpha_low"]), + bonds_moving_avg=u64tf(decoded["bonds_moving_avg"]), + # Metagraph info. + hotkeys=[decode_account_id(ck) for ck in decoded.get("hotkeys", [])], + coldkeys=[decode_account_id(hk) for hk in decoded.get("coldkeys", [])], + identities=decoded["identities"], + axons=decoded.get("axons", []), + active=decoded["active"], + validator_permit=decoded["validator_permit"], + pruning_score=[u16tf(ps) for ps in decoded.get("pruning_score", [])], + last_update=decoded["last_update"], + emission=[_tbwu(em, _netuid) for em in decoded.get("emission", [])], + dividends=[u16tf(dv) for dv in decoded.get("dividends", [])], + incentives=[u16tf(ic) for ic in decoded.get("incentives", [])], + consensus=[u16tf(cs) for cs in decoded.get("consensus", [])], + trust=[u16tf(tr) for tr in decoded.get("trust", [])], + rank=[u16tf(rk) for rk in decoded.get("rank", [])], + block_at_registration=decoded["block_at_registration"], + alpha_stake=[_tbwu(ast, _netuid) for ast in decoded["alpha_stake"]], + tao_stake=[_tbwu(ts) for ts in decoded["tao_stake"]], + total_stake=[_tbwu(ts, _netuid) for ts in decoded["total_stake"]], + # Dividend break down + tao_dividends_per_hotkey=[ + (decode_account_id(alpha[0]), _tbwu(alpha[1])) + for alpha in decoded["tao_dividends_per_hotkey"] + ], + alpha_dividends_per_hotkey=[ + (decode_account_id(adphk[0]), _tbwu(adphk[1], _netuid)) + for adphk in decoded["alpha_dividends_per_hotkey"] + ], + ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index c72d3d80..57cdabee 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -20,6 +20,7 @@ decode_hex_identity, DynamicInfo, SubnetState, + MetagraphInfo, ) from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float @@ -1252,6 +1253,38 @@ async def get_stake_for_coldkey_and_hotkey_on_netuid( else: return Balance.from_rao(_result).set_unit(int(netuid)) + async def get_metagraph_info( + self, netuid: int, block_hash: Optional[str] = None + ) -> Optional[MetagraphInfo]: + hex_bytes_result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_metagraph", + params=[netuid], + block_hash=block_hash, + ) + + if hex_bytes_result is None: + return None + + try: + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + except ValueError: + bytes_result = bytes.fromhex(hex_bytes_result) + + return MetagraphInfo.from_vec_u8(bytes_result) + + async def get_all_metagraphs_info( + self, block_hash: Optional[str] = None + ) -> list[MetagraphInfo]: + hex_bytes_result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_all_metagraphs", + params=[], + block_hash=block_hash, + ) + + return MetagraphInfo.list_from_any(hex_bytes_result) + async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( self, hotkey_ss58s: list[str], From 4fc18105d4c2b6db447a70f7d058cc1935bc44a8 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 28 Feb 2025 17:29:44 -0800 Subject: [PATCH 2/6] Adds btcli view dashboard --- bittensor_cli/cli.py | 43 +- bittensor_cli/src/__init__.py | 3 + bittensor_cli/src/commands/view.py | 2871 ++++++++++++++++++++++++++++ 3 files changed, 2910 insertions(+), 7 deletions(-) create mode 100644 bittensor_cli/src/commands/view.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 10814682..8bf92aec 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -32,7 +32,7 @@ from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from async_substrate_interface.errors import SubstrateRequestException -from bittensor_cli.src.commands import sudo, wallets +from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.subnets import price, subnets from bittensor_cli.src.commands.stake import ( @@ -193,8 +193,7 @@ class Options: ) wait_for_finalization = typer.Option( True, - help="If `True`, waits until the transaction is finalized " - "on the blockchain.", + help="If `True`, waits until the transaction is finalized on the blockchain.", ) prompt = typer.Option( True, @@ -513,6 +512,7 @@ class CLIManager: subnets_app: typer.Typer weights_app: typer.Typer utils_app = typer.Typer(epilog=_epilog) + view_app: typer.Typer asyncio_runner = asyncio def __init__(self): @@ -562,6 +562,7 @@ def __init__(self): self.sudo_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) self.weights_app = typer.Typer(epilog=_epilog) + self.view_app = typer.Typer(epilog=_epilog) # config alias self.app.add_typer( @@ -639,6 +640,9 @@ def __init__(self): self.utils_app, name="utils", no_args_is_help=True, hidden=True ) + # view app + self.app.add_typer(self.view_app, name="view", short_help="HTML view commands", no_args_is_help=True) + # config commands self.config_app.command("set")(self.set_config) self.config_app.command("get")(self.get_config) @@ -806,6 +810,11 @@ def __init__(self): "commit", rich_help_panel=HELP_PANELS["WEIGHTS"]["COMMIT_REVEAL"] )(self.weights_commit) + # view commands + self.view_app.command( + "dashboard", rich_help_panel=HELP_PANELS["VIEW"]["DASHBOARD"] + )(self.view_dashboard) + # Sub command aliases # Weights self.wallet_app.command( @@ -1336,7 +1345,7 @@ def get_config(self): if value in Constants.networks: value = value + f" ({Constants.network_map[value]})" if key == "rate_tolerance": - value = f"{value} ({value*100}%)" if value is not None else "None" + value = f"{value} ({value * 100}%)" if value is not None else "None" elif key in deprecated_configs: continue @@ -1365,19 +1374,19 @@ def ask_rate_tolerance( """ if rate_tolerance is not None: console.print( - f"[dim][blue]Rate tolerance[/blue]: [bold cyan]{rate_tolerance} ({rate_tolerance*100}%)[/bold cyan]." + f"[dim][blue]Rate tolerance[/blue]: [bold cyan]{rate_tolerance} ({rate_tolerance * 100}%)[/bold cyan]." ) return rate_tolerance elif self.config.get("rate_tolerance") is not None: config_slippage = self.config["rate_tolerance"] console.print( - f"[dim][blue]Rate tolerance[/blue]: [bold cyan]{config_slippage} ({config_slippage*100}%)[/bold cyan] (from config)." + f"[dim][blue]Rate tolerance[/blue]: [bold cyan]{config_slippage} ({config_slippage * 100}%)[/bold cyan] (from config)." ) return config_slippage else: console.print( "[dim][blue]Rate tolerance[/blue]: " - + f"[bold cyan]{defaults.rate_tolerance} ({defaults.rate_tolerance*100}%)[/bold cyan] " + + f"[bold cyan]{defaults.rate_tolerance} ({defaults.rate_tolerance * 100}%)[/bold cyan] " + "by default. Set this using " + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " + "or " @@ -5071,6 +5080,26 @@ def weights_commit( ) ) + def view_dashboard( + self, + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Display html dashboard with subnets list, stake, and neuron information. + """ + self.verbosity_handler(quiet, verbose) + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] + ) + return self._run_command( + view.display_network_dashboard(wallet, self.initialize_chain(network)) + ) + @staticmethod @utils_app.command("convert") def convert( diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index e0156309..d990aada 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -713,6 +713,9 @@ class WalletValidationTypes(Enum): "IDENTITY": "Subnet Identity Management", }, "WEIGHTS": {"COMMIT_REVEAL": "Commit / Reveal"}, + "VIEW": { + "DASHBOARD": "Network Dashboard", + }, } COLOR_PALETTE = { diff --git a/bittensor_cli/src/commands/view.py b/bittensor_cli/src/commands/view.py new file mode 100644 index 00000000..0e568a65 --- /dev/null +++ b/bittensor_cli/src/commands/view.py @@ -0,0 +1,2871 @@ +import asyncio +import json +import netaddr +from dataclasses import asdict, is_dataclass +from typing import Any, Dict, List +from pywry import PyWry + +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.utils import console +from bittensor_wallet import Wallet + +root_symbol_html = f"&#x{ord('τ'):X};" + + +class Encoder(json.JSONEncoder): + """JSON encoder for serializing dataclasses and balances""" + + def default(self, obj): + if is_dataclass(obj): + return asdict(obj) + + elif isinstance(obj, Balance): + return obj.tao + + return super().default(obj) + + +async def display_network_dashboard( + wallet: Wallet, + subtensor: "SubtensorInterface", + prompt: bool = True, +) -> bool: + """ + Generate and display the HTML interface. + """ + try: + _subnet_data = await fetch_subnet_data(wallet, subtensor) + subnet_data = process_subnet_data(_subnet_data) + html_content = generate_full_page(subnet_data) + window = PyWry() + window.send_html( + html=html_content, + title="Bittensor View", + width=1200, + height=800, + ) + window.start() + await asyncio.sleep(10) + try: + while True: + if _has_exited(window): + break + await asyncio.sleep(1) + except KeyboardInterrupt: + console.print("\n[yellow]Closing Bittensor View...[/yellow]") + finally: + if not _has_exited(window): + try: + window.close() + except Exception: + pass + + except Exception as e: + print(f"Error: {e}") + return False + + +def int_to_ip(int_val: int) -> str: + """Maps to an ip string""" + return str(netaddr.IPAddress(int_val)) + + +def get_hotkey_identity( + hotkey_ss58: str, + identities: dict, + old_identities: dict, + trucate_length: int = 4, +) -> str: + """Fetch identity of hotkey from both sources""" + if hk_identity := identities["hotkeys"].get(hotkey_ss58): + return hk_identity.get("identity", {}).get("name", "") or hk_identity.get( + "display", "~" + ) + elif old_identity := old_identities.get(hotkey_ss58): + return old_identity.display + else: + return f"{hotkey_ss58[:trucate_length]}...{hotkey_ss58[-trucate_length:]}" + + +async def fetch_subnet_data( + wallet: Wallet, subtensor: "SubtensorInterface" +) -> Dict[str, Any]: + """ + Fetch subnet data from the network. + """ + block_hash = await subtensor.substrate.get_chain_head() + + ( + balance, + stake_info, + metagraphs_info, + subnets_info, + ck_hk_identities, + old_identities, + block_number, + ) = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), + subtensor.get_stake_for_coldkey( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ), + subtensor.get_all_metagraphs_info(block_hash=block_hash), + subtensor.all_subnets(block_hash=block_hash), + subtensor.fetch_coldkey_hotkey_identities(block_hash=block_hash), + subtensor.get_delegate_identities(block_hash=block_hash), + subtensor.substrate.get_block_number(block_hash=block_hash), + ) + + return { + "balance": balance, + "stake_info": stake_info, + "metagraphs_info": metagraphs_info, + "subnets_info": subnets_info, + "ck_hk_identities": ck_hk_identities, + "old_identities": old_identities, + "wallet": wallet, + "block_number": block_number, + } + + +def process_subnet_data(raw_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Process and prepare subnet data. + """ + balance = raw_data["balance"] + stake_info = raw_data["stake_info"] + metagraphs_info = raw_data["metagraphs_info"] + subnets_info = raw_data["subnets_info"] + ck_hk_identities = raw_data["ck_hk_identities"] + old_identities = raw_data["old_identities"] + wallet = raw_data["wallet"] + block_number = raw_data["block_number"] + + pool_info = {info.netuid: info for info in subnets_info} + + total_ideal_stake_value = Balance.from_tao(0) + total_slippage_value = Balance.from_tao(0) + + # Process stake + stake_dict: Dict[int, List[Dict[str, Any]]] = {} + for stake in stake_info: + if stake.stake.tao > 0: + slippage_value, _, slippage_percentage = pool_info[ + stake.netuid + ].alpha_to_tao_with_slippage(stake.stake) + ideal_value = pool_info[stake.netuid].alpha_to_tao(stake.stake) + total_ideal_stake_value += ideal_value + total_slippage_value += slippage_value + stake_dict.setdefault(stake.netuid, []).append( + { + "hotkey": stake.hotkey_ss58, + "hotkey_identity": get_hotkey_identity( + stake.hotkey_ss58, ck_hk_identities, old_identities + ), + "amount": stake.stake.tao, + "emission": stake.emission.tao, + "is_registered": stake.is_registered, + "tao_emission": stake.tao_emission.tao, + "ideal_value": ideal_value.tao, + "slippage_value": slippage_value.tao, + "slippage_percentage": slippage_percentage, + } + ) + + # Process metagraph + subnets = [] + for meta_info in metagraphs_info: + subnet_stakes = stake_dict.get(meta_info.netuid, []) + metagraph_info = { + "netuid": meta_info.netuid, + "name": meta_info.name, + "symbol": meta_info.symbol, + "alpha_in": 0 if meta_info.netuid == 0 else meta_info.alpha_in.tao, + "alpha_out": meta_info.alpha_out.tao, + "tao_in": 0 if meta_info.netuid == 0 else meta_info.tao_in.tao, + "tao_in_emission": meta_info.tao_in_emission.tao, + "num_uids": meta_info.num_uids, + "max_uids": meta_info.max_uids, + "moving_price": meta_info.moving_price.tao, + "blocks_since_last_step": "~" + if meta_info.netuid == 0 + else meta_info.blocks_since_last_step, + "tempo": "~" if meta_info.netuid == 0 else meta_info.tempo, + "registration_allowed": meta_info.registration_allowed, + "commit_reveal_weights_enabled": meta_info.commit_reveal_weights_enabled, + "hotkeys": meta_info.hotkeys, + "coldkeys": meta_info.coldkeys, + "updated_identities": [], + "processed_axons": [], + "rank": meta_info.rank, + "trust": meta_info.trust, + "consensus": meta_info.consensus, + "incentives": meta_info.incentives, + "dividends": meta_info.dividends, + "active": meta_info.active, + "validator_permit": meta_info.validator_permit, + "pruning_score": meta_info.pruning_score, + "last_update": meta_info.last_update, + "block_at_registration": meta_info.block_at_registration, + } + + # Process axon data and convert IPs + for axon in meta_info.axons: + if axon: + processed_axon = { + "ip": int_to_ip(axon["ip"]) if axon["ip"] else "N/A", + "port": axon["port"], + "ip_type": axon["ip_type"], + } + metagraph_info["processed_axons"].append(processed_axon) + else: + metagraph_info["processed_axons"].append(None) + + # Add identities + for hotkey in meta_info.hotkeys: + identity = get_hotkey_identity( + hotkey, ck_hk_identities, old_identities, trucate_length=2 + ) + metagraph_info["updated_identities"].append(identity) + + # Balance conversion + for field in [ + "emission", + "alpha_stake", + "tao_stake", + "total_stake", + ]: + if hasattr(meta_info, field): + raw_data = getattr(meta_info, field) + if isinstance(raw_data, list): + metagraph_info[field] = [ + x.tao if hasattr(x, "tao") else x for x in raw_data + ] + else: + metagraph_info[field] = raw_data + + # Calculate price + price = ( + 1 + if metagraph_info["netuid"] == 0 + else metagraph_info["tao_in"] / metagraph_info["alpha_in"] + if metagraph_info["alpha_in"] > 0 + else 0 + ) + + # Package it all up + symbol_html = f"&#x{ord(meta_info.symbol):X};" + subnets.append( + { + "netuid": meta_info.netuid, + "name": meta_info.name, + "symbol": symbol_html, + "price": price, + "market_cap": float( + (metagraph_info["alpha_in"] + metagraph_info["alpha_out"]) * price + ) + if price + else 0, + "emission": metagraph_info["tao_in_emission"], + "total_stake": metagraph_info["alpha_out"], + "your_stakes": subnet_stakes, + "metagraph_info": metagraph_info, + } + ) + subnets.sort(key=lambda x: x["market_cap"], reverse=True) + return { + "wallet_info": { + "name": wallet.name, + "balance": balance.tao, + "coldkey": wallet.coldkeypub.ss58_address, + "total_ideal_stake_value": total_ideal_stake_value.tao, + "total_slippage_value": total_slippage_value.tao, + }, + "subnets": subnets, + "block_number": block_number, + } + + +def _has_exited(handler) -> bool: + """Check if PyWry process has cleanly exited with returncode 0.""" + return ( + hasattr(handler, "runner") + and handler.runner is not None + and handler.runner.returncode == 0 + ) + + +def generate_full_page(data: Dict[str, Any]) -> str: + """ + Generate full HTML content for the interface. + """ + serializable_data = { + "wallet_info": data["wallet_info"], + "subnets": data["subnets"], + } + wallet_info_json = json.dumps( + serializable_data["wallet_info"], cls=Encoder + ).replace("'", "'") + subnets_json = json.dumps(serializable_data["subnets"], cls=Encoder).replace( + "'", "'" + ) + + return f""" + + + + Bittensor CLI Interface + + + + +
+
+
+
+
+

Btcli View

+ Beta +
+
+
+ + +
+ {generate_main_header(data["wallet_info"], data["block_number"])} + {generate_main_filters()} + {generate_subnets_table(data["subnets"])} +
+ + + + + + + + """ + + +def generate_subnet_details_header() -> str: + """ + Generates the header section for the subnet details page, + including the back button, toggle controls, title, and network visualization. + """ + return """ +
+
+ +
+ + +
+
+ +
+
+

+
+
+
+
+ +
+
+
+
+
+
Moving Price
+
+
+
+
Registration
+
+
+
+
CR Weights
+
+
+
+
Neurons
+
+
+
+
Blocks Since Step
+
+
+
+
+ """ + + +def generate_subnet_metrics() -> str: + """ + Generates the metrics section for the subnet details page, + including market metrics and the stakes table. + """ + return """ +
+
+
+
Market Cap
+
+
+
+
Total Stake
+
+
+
+
Alpha Reserves
+
+
+
+
Tao Reserves
+
+
+
+
Emission
+
+
+
+ +
+
+

Metagraph

+
+ + +
+
+ +
+ + + + + + + + + + + + + + + +
HotkeyAmountValueValue (w/ slippage)Alpha emissionTao emissionRegisteredActions
+
+
+
+ """ + + +def generate_neuron_details() -> str: + """ + Generates the neuron detail container, which is hidden by default. + This section shows detailed information for a selected neuron. + """ + return """ + + """ + + +def generate_main_header(wallet_info: Dict[str, Any], block_number: int) -> str: + truncated_coldkey = f"{wallet_info['coldkey'][:6]}...{wallet_info['coldkey'][-6:]}" + + # Calculate slippage percentage + ideal_value = wallet_info["total_ideal_stake_value"] + slippage_value = wallet_info["total_slippage_value"] + slippage_percentage = ( + ((ideal_value - slippage_value) / ideal_value * 100) if ideal_value > 0 else 0 + ) + + return f""" +
+
+ {wallet_info["name"]} +
+ {truncated_coldkey} + Copy +
+
+
+
+ Block + {block_number} +
+
+ Balance + {wallet_info["balance"]:.4f} {root_symbol_html} +
+
+ Total Stake Value + {wallet_info["total_ideal_stake_value"]:.4f} {root_symbol_html} +
+
+ Slippage Impact + + {slippage_percentage:.2f}% ({wallet_info["total_slippage_value"]:.4f} {root_symbol_html}) + +
+
+
+ """ + + +def generate_main_filters() -> str: + return """ +
+ +
+ + + + +
+
+
+ """ + + +def generate_subnets_table(subnets: List[Dict[str, Any]]) -> str: + rows = [] + for subnet in subnets: + total_your_stake = sum(stake["amount"] for stake in subnet["your_stakes"]) + stake_status = ( + 'Staked' + if total_your_stake > 0 + else 'Not Staked' + ) + rows.append(f""" + + {subnet["netuid"]} - {subnet["name"]} + + + + + {stake_status} + + """) + return f""" +
+ + + + + + + + + + + + + {"".join(rows)} + +
SubnetPriceMarket CapYour StakeEmissionStatus
+
+ """ + + +def generate_subnet_details_html() -> str: + return """ + + """ + + +def get_css_styles() -> str: + """Get CSS styles for the interface.""" + return """ + /* ===================== Base Styles & Typography ===================== */ + @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Noto+Sans:wght@400;500;600&display=swap'); + + body { + font-family: 'Inter', 'Noto Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Arial Unicode MS', sans-serif; + margin: 0; + padding: 24px; + background: #000000; + color: #ffffff; + } + + input, button, select { + font-family: inherit; + font-feature-settings: normal; + } + + /* ===================== Main Page Header ===================== */ + .header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 24px; + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; + margin-bottom: 24px; + backdrop-filter: blur(10px); + } + + .wallet-info { + display: flex; + flex-direction: column; + gap: 4px; + } + + .wallet-name { + font-size: 1.1em; + font-weight: 500; + color: #FF9900; + } + + .wallet-address-container { + position: relative; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + } + + .wallet-address { + font-size: 0.9em; + color: rgba(255, 255, 255, 0.5); + font-family: monospace; + transition: color 0.2s ease; + } + + .wallet-address-container:hover .wallet-address { + color: rgba(255, 255, 255, 0.8); + } + + .copy-indicator { + background: rgba(255, 153, 0, 0.1); + color: rgba(255, 153, 0, 0.8); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.7em; + transition: all 0.2s ease; + opacity: 0; + } + + .wallet-address-container:hover .copy-indicator { + opacity: 1; + background: rgba(255, 153, 0, 0.2); + } + + .wallet-address-container.copied .copy-indicator { + opacity: 1; + background: rgba(255, 153, 0, 0.3); + color: #FF9900; + } + + .stake-metrics { + display: flex; + gap: 24px; + align-items: center; + } + + .stake-metric { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + position: relative; + padding: 8px 16px; + border-radius: 8px; + transition: all 0.2s ease; + } + + .stake-metric:hover { + background: rgba(255, 153, 0, 0.05); + } + + .stake-metric .metric-label { + font-size: 0.8em; + color: rgba(255, 255, 255, 0.6); + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .stake-metric .metric-value { + font-size: 1.1em; + font-weight: 500; + color: #FF9900; + font-feature-settings: "tnum"; + font-variant-numeric: tabular-nums; + } + + .slippage-value { + display: flex; + align-items: center; + gap: 6px; + } + + .slippage-detail { + font-size: 0.8em; + color: rgba(255, 255, 255, 0.5); + } + + /* ===================== Main Page Filters ===================== */ + .filters-section { + display: flex; + justify-content: space-between; + align-items: center; + margin: 24px 0; + gap: 16px; + } + + .search-box input { + padding: 10px 16px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.03); + color: rgba(255, 255, 255, 0.7); + width: 240px; + font-size: 0.9em; + transition: all 0.2s ease; + } + .search-box input::placeholder { + color: rgba(255, 255, 255, 0.4); + } + + .search-box input:focus { + outline: none; + border-color: rgba(255, 153, 0, 0.5); + background: rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.9); + } + + .filter-toggles { + display: flex; + gap: 16px; + } + + .filter-toggles label { + display: flex; + align-items: center; + gap: 8px; + color: rgba(255, 255, 255, 0.7); + font-size: 0.9em; + cursor: pointer; + user-select: none; + } + + /* Checkbox styling for both main page and subnet page */ + .filter-toggles input[type="checkbox"], + .toggle-label input[type="checkbox"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border: 2px solid rgba(255, 153, 0, 0.3); + border-radius: 4px; + background: rgba(0, 0, 0, 0.2); + cursor: pointer; + position: relative; + transition: all 0.2s ease; + } + + .filter-toggles input[type="checkbox"]:hover, + .toggle-label input[type="checkbox"]:hover { + border-color: #FF9900; + } + + .filter-toggles input[type="checkbox"]:checked, + .toggle-label input[type="checkbox"]:checked { + background: #FF9900; + border-color: #FF9900; + } + + .filter-toggles input[type="checkbox"]:checked::after, + .toggle-label input[type="checkbox"]:checked::after { + content: ''; + position: absolute; + left: 5px; + top: 2px; + width: 4px; + height: 8px; + border: solid #000; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + + .filter-toggles label:hover, + .toggle-label:hover { + color: rgba(255, 255, 255, 0.9); + } + .disabled-label { + opacity: 0.5; + cursor: not-allowed; + } + .add-stake-button { + padding: 10px 20px; + font-size: 0.8rem; + } + .export-csv-button { + padding: 10px 20px; + font-size: 0.8rem; + } + .button-group { + display: flex; + gap: 8px; + } + + /* ===================== Main Page Subnet Table ===================== */ + .subnets-table-container { + background: rgba(255, 255, 255, 0.02); + border-radius: 12px; + overflow: hidden; + } + + .subnets-table { + width: 100%; + border-collapse: collapse; + font-size: 0.95em; + } + + .subnets-table th { + background: rgba(255, 255, 255, 0.05); + font-weight: 500; + text-align: left; + padding: 16px; + color: rgba(255, 255, 255, 0.7); + } + + .subnets-table td { + padding: 14px 16px; + border-top: 1px solid rgba(255, 255, 255, 0.05); + } + + .subnet-row { + cursor: pointer; + transition: background-color 0.2s ease; + } + + .subnet-row:hover { + background: rgba(255, 255, 255, 0.05); + } + + .subnet-name { + color: #ffffff; + font-weight: 500; + font-size: 0.95em; + } + + .price, .market-cap, .your-stake, .emission { + font-family: 'Inter', monospace; + font-size: 1.0em; + font-feature-settings: "tnum"; + font-variant-numeric: tabular-nums; + letter-spacing: 0.01em; + white-space: nowrap; + } + + .stake-status { + font-size: 0.85em; + padding: 4px 8px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.05); + } + + .stake-status.staked { + background: rgba(255, 153, 0, 0.1); + color: #FF9900; + } + + .subnets-table th.sortable { + cursor: pointer; + position: relative; + padding-right: 20px; + } + + .subnets-table th.sortable:hover { + color: #FF9900; + } + + .subnets-table th[data-sort] { + color: #FF9900; + } + + /* ===================== Subnet Tiles View ===================== */ + .subnet-tiles-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1rem; + padding: 1rem; + } + + .subnet-tile { + width: clamp(75px, 6vw, 600px); + height: clamp(75px, 6vw, 600px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + position: relative; + cursor: pointer; + transition: all 0.2s ease; + overflow: hidden; + font-size: clamp(0.6rem, 1vw, 1.4rem); + } + + .tile-netuid { + position: absolute; + top: 0.4em; + left: 0.4em; + font-size: 0.7em; + color: rgba(255, 255, 255, 0.6); + } + + .tile-symbol { + font-size: 1.6em; + margin-bottom: 0.4em; + color: #FF9900; + } + + .tile-name { + display: block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 1em; + text-align: center; + color: rgba(255, 255, 255, 0.9); + margin: 0 0.4em; + } + + .tile-market-cap { + font-size: 0.9em; + color: rgba(255, 255, 255, 0.5); + margin-top: 2px; + } + + .subnet-tile:hover { + transform: translateY(-2px); + box-shadow: + 0 0 12px rgba(255, 153, 0, 0.6), + 0 0 24px rgba(255, 153, 0, 0.3); + background: rgba(255, 255, 255, 0.08); + } + + .subnet-tile.staked { + border: 1px solid rgba(255, 153, 0, 0.3); + } + + .subnet-tile.staked::before { + content: ''; + position: absolute; + top: 0.4em; + right: 0.4em; + width: 0.5em; + height: 0.5em; + border-radius: 50%; + background: #FF9900; + } + + /* ===================== Subnet Detail Page Header ===================== */ + .subnet-header { + padding: 16px; + border-radius: 12px; + margin-bottom: 0px; + } + + .subnet-header h2 { + margin: 0; + font-size: 1.3em; + } + + .subnet-price { + font-size: 1.3em; + color: #FF9900; + } + + .subnet-title-row { + display: grid; + grid-template-columns: 300px 1fr 300px; + align-items: start; + margin: 0; + position: relative; + min-height: 60px; + } + + .title-price { + grid-column: 1; + padding-top: 0; + margin-top: -10px; + } + + .header-row { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + margin-bottom: 16px; + } + + .toggle-group { + display: flex; + flex-direction: column; + gap: 8px; + align-items: flex-end; + } + + .toggle-label { + display: flex; + align-items: center; + gap: 8px; + color: rgba(255, 255, 255, 0.7); + font-size: 0.9em; + cursor: pointer; + user-select: none; + } + + .back-button { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.8); + padding: 8px 16px; + border-radius: 8px; + cursor: pointer; + font-size: 0.9em; + transition: all 0.2s ease; + margin-bottom: 16px; + } + + .back-button:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); + } + + /* ===================== Network Visualization ===================== */ + .network-visualization-container { + position: absolute; + left: 50%; + transform: translateX(-50%); + top: -50px; + width: 700px; + height: 80px; + z-index: 1; + } + + .network-visualization { + width: 700px; + height: 80px; + position: relative; + } + + #network-canvas { + background: transparent; + position: relative; + z-index: 1; + } + + /* Gradient behind visualization */ + .network-visualization::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + background: linear-gradient(to bottom, rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0.8) 100%); + z-index: 0; + pointer-events: none; + } + + /* ===================== Subnet Detail Metrics ===================== */ + .network-metrics { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; + margin: 0; + margin-top: 16px; + } + + /* Base card styles - applied to both network and metric cards */ + .network-card, .metric-card { + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + padding: 12px 16px; + min-height: 50px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 4px; + } + + /* Separate styling for moving price value */ + #network-moving-price { + color: #FF9900; + } + + .metrics-section { + margin-top: 0px; + margin-bottom: 16px; + } + + .metrics-group { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; + margin: 0; + margin-top: 2px; + } + + .market-metrics .metric-card { + background: rgba(255, 255, 255, 0.05); + min-height: 70px; + } + + .metric-label { + font-size: 0.85em; + color: rgba(255, 255, 255, 0.7); + margin: 0; + } + + .metric-value { + font-size: 1.2em; + line-height: 1.3; + margin: 0; + } + + /* Add status colors */ + .registration-status { + color: #2ECC71; + } + + .registration-status.closed { + color: #ff4444; /* Red color for closed status */ + } + + .cr-status { + color: #2ECC71; + } + + .cr-status.disabled { + color: #ff4444; /* Red color for disabled status */ + } + + /* ===================== Stakes Table ===================== */ + .stakes-container { + margin-top: 24px; + padding: 0 24px; + } + + .stakes-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + + .stakes-header h3 { + font-size: 1.2em; + color: #ffffff; + margin: 0; + } + + .stakes-table-container { + background: rgba(255, 255, 255, 0.02); + border-radius: 12px; + overflow: hidden; + margin-bottom: 24px; + width: 100%; + } + + .stakes-table { + width: 100%; + border-collapse: collapse; + } + + .stakes-table th { + background: rgba(255, 255, 255, 0.05); + padding: 16px; + text-align: left; + font-weight: 500; + color: rgba(255, 255, 255, 0.7); + } + + .stakes-table td { + padding: 16px; + border-top: 1px solid rgba(255, 255, 255, 0.05); + } + + .stakes-table tr { + transition: background-color 0.2s ease; + } + + .stakes-table tr:nth-child(even) { + background: rgba(255, 255, 255, 0.02); + } + + .stakes-table tr:hover { + background: transparent; + } + + .no-stakes-row td { + text-align: center; + padding: 32px; + color: rgba(255, 255, 255, 0.5); + } + + /* Table styles consistency */ + .stakes-table th, .network-table th { + background: rgba(255, 255, 255, 0.05); + padding: 16px; + text-align: left; + font-weight: 500; + color: rgba(255, 255, 255, 0.7); + transition: color 0.2s ease; + } + + /* Sortable columns */ + .stakes-table th.sortable, .network-table th.sortable { + cursor: pointer; + } + + /* Active sort column - only change color */ + .stakes-table th.sortable[data-sort], .network-table th.sortable[data-sort] { + color: #FF9900; + } + + /* Hover effects - only change color */ + .stakes-table th.sortable:hover, .network-table th.sortable:hover { + color: #FF9900; + } + + /* Remove hover background from table rows */ + .stakes-table tr:hover { + background: transparent; + } + + /* ===================== Network Table ===================== */ + .network-table-container { + margin-top: 60px; + position: relative; + z-index: 2; + background: rgba(0, 0, 0, 0.8); + } + + .network-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + } + + .network-table th { + background: rgba(255, 255, 255, 0.05); + padding: 16px; + text-align: left; + font-weight: 500; + color: rgba(255, 255, 255, 0.7); + } + + .network-table td { + padding: 16px; + border-top: 1px solid rgba(255, 255, 255, 0.05); + } + + .network-table tr { + cursor: pointer; + transition: background-color 0.2s ease; + } + + .network-table tr:hover { + background-color: rgba(255, 255, 255, 0.05); + } + + .network-table tr:nth-child(even) { + background-color: rgba(255, 255, 255, 0.02); + } + + .network-table tr:nth-child(even):hover { + background-color: rgba(255, 255, 255, 0.05); + } + + .network-search-container { + display: flex; + align-items: center; + margin-bottom: 16px; + padding: 0 16px; + } + + .network-search { + width: 100%; + padding: 12px 16px; + border: 1px solid rgba(255, 153, 0, 0.2); + border-radius: 8px; + background: rgba(0, 0, 0, 0.2); + color: #ffffff; + font-size: 0.95em; + transition: all 0.2s ease; + } + + .network-search:focus { + outline: none; + border-color: rgba(255, 153, 0, 0.5); + background: rgba(0, 0, 0, 0.3); + caret-color: #FF9900; + } + + .network-search::placeholder { + color: rgba(255, 255, 255, 0.3); + } + + /* ===================== Cell Styles & Formatting ===================== */ + .hotkey-cell { + max-width: 200px; + position: relative; + } + + .hotkey-container { + position: relative; + display: inline-block; + max-width: 100%; + } + + .hotkey-identity, .truncated-address { + color: rgba(255, 255, 255, 0.8); + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } + + .copy-button { + position: absolute; + top: -20px; /* Position above the text */ + right: 0; + background: rgba(255, 153, 0, 0.1); + color: rgba(255, 255, 255, 0.6); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.7em; + cursor: pointer; + opacity: 0; + transition: all 0.2s ease; + transform: translateY(5px); + } + + .hotkey-container:hover .copy-button { + opacity: 1; + transform: translateY(0); + } + + .copy-button:hover { + background: rgba(255, 153, 0, 0.2); + color: #FF9900; + } + + .address-cell { + max-width: 150px; + position: relative; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .address-container { + display: flex; + align-items: center; + cursor: pointer; + position: relative; + } + + .address-container:hover::after { + content: 'Click to copy'; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + background: rgba(255, 153, 0, 0.1); + color: #FF9900; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.8em; + opacity: 0.8; + } + + .truncated-address { + font-family: monospace; + color: rgba(255, 255, 255, 0.8); + overflow: hidden; + text-overflow: ellipsis; + } + + .truncated-address:hover { + color: #FF9900; + } + + .registered-yes { + color: #FF9900; + font-weight: 500; + display: flex; + align-items: center; + gap: 4px; + } + + .registered-no { + color: #ff4444; + font-weight: 500; + display: flex; + align-items: center; + gap: 4px; + } + + .manage-button { + background: rgba(255, 153, 0, 0.1); + border: 1px solid rgba(255, 153, 0, 0.2); + color: #FF9900; + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + } + + .manage-button:hover { + background: rgba(255, 153, 0, 0.2); + transform: translateY(-1px); + } + + .hotkey-identity { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + color: #FF9900; + } + + .identity-cell { + max-width: 700px; + font-size: 0.90em; + letter-spacing: -0.2px; + color: #FF9900; + } + + .per-day { + font-size: 0.75em; + opacity: 0.7; + margin-left: 4px; + } + + /* ===================== Neuron Detail Panel ===================== */ + #neuron-detail-container { + background: rgba(255, 255, 255, 0.02); + border-radius: 12px; + padding: 16px; + margin-top: 16px; + } + + .neuron-detail-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 16px; + } + + .neuron-detail-content { + display: flex; + flex-direction: column; + gap: 16px; + } + + .neuron-info-top { + display: flex; + flex-direction: column; + gap: 8px; + } + + .neuron-keys { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 0.9em; + color: rgba(255, 255, 255, 0.6); + font-size: 1em; + color: rgba(255, 255, 255, 0.7); + } + + .neuron-cards-container { + display: flex; + flex-direction: column; + gap: 12px; + } + + .neuron-metrics-row { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 12px; + margin: 0; + } + + .neuron-metrics-row.last-row { + grid-template-columns: repeat(3, 1fr); + } + + /* IP Info styling */ + #neuron-ipinfo { + font-size: 0.85em; + line-height: 1.4; + white-space: nowrap; + } + + #neuron-ipinfo .no-connection { + color: #ff4444; + font-weight: 500; + } + + /* Adjust metric card for IP info to accommodate multiple lines */ + .neuron-cards-container .metric-card:has(#neuron-ipinfo) { + min-height: 85px; + } + + /* ===================== Subnet Page Color Overrides ===================== */ + /* Subnet page specific style */ + .subnet-page .metric-card-title, + .subnet-page .network-card-title { + color: rgba(255, 255, 255, 0.7); + } + + .subnet-page .metric-card .metric-value, + .subnet-page .metric-value { + color: white; + } + + /* Green values */ + .subnet-page .validator-true, + .subnet-page .active-yes, + .subnet-page .registration-open, + .subnet-page .cr-enabled, + .subnet-page .ip-info { + color: #FF9900; + } + + /* Red values */ + .subnet-page .validator-false, + .subnet-page .active-no, + .subnet-page .registration-closed, + .subnet-page .cr-disabled, + .subnet-page .ip-na { + color: #ff4444; + } + + /* Keep symbols green in subnet page */ + .subnet-page .symbol { + color: #FF9900; + } + + /* ===================== Responsive Styles ===================== */ + @media (max-width: 1200px) { + .stakes-table { + display: block; + overflow-x: auto; + } + + .network-metrics { + grid-template-columns: repeat(3, 1fr); + } + } + + @media (min-width: 1201px) { + .network-metrics { + grid-template-columns: repeat(5, 1fr); + } + } + /* ===== Splash Screen ===== */ + #splash-screen { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: #000000; + display: flex; + align-items: center; + justify-content: center; + z-index: 999999; + opacity: 1; + transition: opacity 1s ease; + } + + #splash-screen.fade-out { + opacity: 0; + } + + .splash-content { + text-align: center; + color: #FF9900; + opacity: 0; + animation: fadeIn 1.2s ease forwards; + } + @keyframes fadeIn { + 0% { + opacity: 0; + transform: scale(0.97); + } + 100% { + opacity: 1; + transform: scale(1); + } + } + + /* Title & text styling */ + .title-row { + display: flex; + align-items: baseline; + gap: 1rem; + } + + .splash-title { + font-size: 2.4rem; + margin: 0; + padding: 0; + font-weight: 600; + color: #FF9900; + } + + .beta-text { + font-size: 0.9rem; + color: #FF9900; + background: rgba(255, 153, 0, 0.1); + padding: 2px 6px; + border-radius: 4px; + font-weight: 500; + } + + + """ + + +def get_javascript() -> str: + return """ + /* ===================== Global Variables ===================== */ + const root_symbol_html = 'τ'; + let verboseNumbers = false; + + /* ===================== Clipboard Functions ===================== */ + /** + * Copies text to clipboard and shows visual feedback + * @param {string} text The text to copy + * @param {HTMLElement} element Optional element to show feedback on + */ + function copyToClipboard(text, element) { + navigator.clipboard.writeText(text) + .then(() => { + const targetElement = element || (event && event.target); + + if (targetElement) { + const copyIndicator = targetElement.querySelector('.copy-indicator'); + + if (copyIndicator) { + const originalText = copyIndicator.textContent; + copyIndicator.textContent = 'Copied!'; + copyIndicator.style.color = '#FF9900'; + + setTimeout(() => { + copyIndicator.textContent = originalText; + copyIndicator.style.color = ''; + }, 1000); + } else { + const originalText = targetElement.textContent; + targetElement.textContent = 'Copied!'; + targetElement.style.color = '#FF9900'; + + setTimeout(() => { + targetElement.textContent = originalText; + targetElement.style.color = ''; + }, 1000); + } + } + }) + .catch(err => { + console.error('Failed to copy:', err); + }); + } + + + /* ===================== Initialization and DOMContentLoaded Handler ===================== */ + document.addEventListener('DOMContentLoaded', function() { + try { + const initialDataElement = document.getElementById('initial-data'); + if (!initialDataElement) { + throw new Error('Initial data element (#initial-data) not found.'); + } + window.initialData = { + wallet_info: JSON.parse(initialDataElement.getAttribute('data-wallet-info')), + subnets: JSON.parse(initialDataElement.getAttribute('data-subnets')) + }; + } catch (error) { + console.error('Error loading initial data:', error); + } + + // Return to the main list of subnets. + const backButton = document.querySelector('.back-button'); + if (backButton) { + backButton.addEventListener('click', function() { + // First check if neuron details are visible and close them if needed + const neuronDetails = document.getElementById('neuron-detail-container'); + if (neuronDetails && neuronDetails.style.display !== 'none') { + closeNeuronDetails(); + return; // Stop here, don't go back to main page yet + } + + // Otherwise go back to main subnet list + document.getElementById('main-content').style.display = 'block'; + document.getElementById('subnet-page').style.display = 'none'; + }); + } + + + // Splash screen logic + const splash = document.getElementById('splash-screen'); + const mainContent = document.getElementById('main-content'); + mainContent.style.display = 'none'; + + setTimeout(() => { + splash.classList.add('fade-out'); + splash.addEventListener('transitionend', () => { + splash.style.display = 'none'; + mainContent.style.display = 'block'; + }, { once: true }); + }, 2000); + + initializeFormattedNumbers(); + + // Keep main page's "verbose" checkbox and the Subnet page's "verbose" checkbox in sync + const mainVerboseCheckbox = document.getElementById('show-verbose'); + const subnetVerboseCheckbox = document.getElementById('verbose-toggle'); + if (mainVerboseCheckbox && subnetVerboseCheckbox) { + mainVerboseCheckbox.addEventListener('change', function() { + subnetVerboseCheckbox.checked = this.checked; + toggleVerboseNumbers(); + }); + subnetVerboseCheckbox.addEventListener('change', function() { + mainVerboseCheckbox.checked = this.checked; + toggleVerboseNumbers(); + }); + } + + // Initialize tile view as default + const tilesContainer = document.getElementById('subnet-tiles-container'); + const tableContainer = document.querySelector('.subnets-table-container'); + + // Generate and show tiles + generateSubnetTiles(); + tilesContainer.style.display = 'flex'; + tableContainer.style.display = 'none'; + }); + + /* ===================== Main Page Functions ===================== */ + /** + * Sort the main Subnets table by the specified column index. + * Toggles ascending/descending on each click. + * @param {number} columnIndex Index of the column to sort. + */ + function sortMainTable(columnIndex) { + const table = document.querySelector('.subnets-table'); + const headers = table.querySelectorAll('th'); + const header = headers[columnIndex]; + + // Determine new sort direction + let isDescending = header.getAttribute('data-sort') !== 'desc'; + + // Clear sort markers on all columns, then set the new one + headers.forEach(th => { th.removeAttribute('data-sort'); }); + header.setAttribute('data-sort', isDescending ? 'desc' : 'asc'); + + // Sort rows based on numeric value (or netuid in col 0) + const tbody = table.querySelector('tbody'); + const rows = Array.from(tbody.querySelectorAll('tr')); + rows.sort((rowA, rowB) => { + const cellA = rowA.cells[columnIndex]; + const cellB = rowB.cells[columnIndex]; + + // Special handling for the first column with netuid in data-value + if (columnIndex === 0) { + const netuidA = parseInt(cellA.getAttribute('data-value'), 10); + const netuidB = parseInt(cellB.getAttribute('data-value'), 10); + return isDescending ? (netuidB - netuidA) : (netuidA - netuidB); + } + + // Otherwise parse float from data-value + const valueA = parseFloat(cellA.getAttribute('data-value')) || 0; + const valueB = parseFloat(cellB.getAttribute('data-value')) || 0; + return isDescending ? (valueB - valueA) : (valueA - valueB); + }); + + // Re-inject rows in sorted order + tbody.innerHTML = ''; + rows.forEach(row => tbody.appendChild(row)); + } + + /** + * Filters the main Subnets table rows based on user search and "Show Only Staked" checkbox. + */ + function filterSubnets() { + const searchText = document.getElementById('subnet-search').value.toLowerCase(); + const showStaked = document.getElementById('show-staked').checked; + const showTiles = document.getElementById('show-tiles').checked; + + // Filter table rows + const rows = document.querySelectorAll('.subnet-row'); + rows.forEach(row => { + const name = row.querySelector('.subnet-name').textContent.toLowerCase(); + const stakeStatus = row.querySelector('.stake-status').textContent; // "Staked" or "Not Staked" + + let isVisible = name.includes(searchText); + if (showStaked) { + // If "Show only Staked" is checked, the row must have "Staked" to be visible + isVisible = isVisible && (stakeStatus === 'Staked'); + } + row.style.display = isVisible ? '' : 'none'; + }); + + // Filter tiles if they're being shown + if (showTiles) { + const tiles = document.querySelectorAll('.subnet-tile'); + tiles.forEach(tile => { + const name = tile.querySelector('.tile-name').textContent.toLowerCase(); + const netuid = tile.querySelector('.tile-netuid').textContent; + const isStaked = tile.classList.contains('staked'); + + let isVisible = name.includes(searchText) || netuid.includes(searchText); + if (showStaked) { + isVisible = isVisible && isStaked; + } + tile.style.display = isVisible ? '' : 'none'; + }); + } + } + + + /* ===================== Subnet Detail Page Functions ===================== */ + /** + * Displays the Subnet page (detailed view) for the selected netuid. + * Hides the main content and populates all the metrics / stakes / network table. + * @param {number} netuid The netuid of the subnet to show in detail. + */ + function showSubnetPage(netuid) { + try { + window.currentSubnet = netuid; + window.scrollTo(0, 0); + + const subnet = window.initialData.subnets.find(s => s.netuid === parseInt(netuid, 10)); + if (!subnet) { + throw new Error(`Subnet not found for netuid: ${netuid}`); + } + window.currentSubnetSymbol = subnet.symbol; + + // Insert the "metagraph" table beneath the "stakes" table in the hidden container + const networkTableHTML = ` + + `; + + // Show/hide main content vs. subnet detail + document.getElementById('main-content').style.display = 'none'; + document.getElementById('subnet-page').style.display = 'block'; + + document.querySelector('#subnet-title').textContent = `${subnet.netuid} - ${subnet.name}`; + document.querySelector('#subnet-price').innerHTML = formatNumber(subnet.price, subnet.symbol); + document.querySelector('#subnet-market-cap').innerHTML = formatNumber(subnet.market_cap, root_symbol_html); + document.querySelector('#subnet-total-stake').innerHTML= formatNumber(subnet.total_stake, subnet.symbol); + document.querySelector('#subnet-emission').innerHTML = formatNumber(subnet.emission, root_symbol_html); + + + const metagraphInfo = subnet.metagraph_info; + document.querySelector('#network-alpha-in').innerHTML = formatNumber(metagraphInfo.alpha_in, subnet.symbol); + document.querySelector('#network-tau-in').innerHTML = formatNumber(metagraphInfo.tao_in, root_symbol_html); + document.querySelector('#network-moving-price').innerHTML = formatNumber(metagraphInfo.moving_price, subnet.symbol); + + // Registration status + const registrationElement = document.querySelector('#network-registration'); + registrationElement.textContent = metagraphInfo.registration_allowed ? 'Open' : 'Closed'; + registrationElement.classList.toggle('closed', !metagraphInfo.registration_allowed); + + // Commit-Reveal Weight status + const crElement = document.querySelector('#network-cr'); + crElement.textContent = metagraphInfo.commit_reveal_weights_enabled ? 'Enabled' : 'Disabled'; + crElement.classList.toggle('disabled', !metagraphInfo.commit_reveal_weights_enabled); + + // Blocks since last step, out of tempo + document.querySelector('#network-blocks-since-step').innerHTML = + `${metagraphInfo.blocks_since_last_step}/${metagraphInfo.tempo}`; + + // Number of neurons vs. max + document.querySelector('#network-neurons').innerHTML = + `${metagraphInfo.num_uids}/${metagraphInfo.max_uids}`; + + // Update "Your Stakes" table + const stakesTableBody = document.querySelector('#stakes-table-body'); + stakesTableBody.innerHTML = ''; + if (subnet.your_stakes && subnet.your_stakes.length > 0) { + subnet.your_stakes.forEach(stake => { + const row = document.createElement('tr'); + row.innerHTML = ` + +
+ ${stake.hotkey_identity} + + copy +
+ + ${formatNumber(stake.amount, subnet.symbol)} + ${formatNumber(stake.ideal_value, root_symbol_html)} + ${formatNumber(stake.slippage_value, root_symbol_html)} (${stake.slippage_percentage.toFixed(2)}%) + ${formatNumber(stake.emission, subnet.symbol + '/day')} + ${formatNumber(stake.tao_emission, root_symbol_html + '/day')} + + + ${stake.is_registered ? 'Yes' : 'No'} + + + + + + `; + stakesTableBody.appendChild(row); + }); + } else { + // If no user stake in this subnet + stakesTableBody.innerHTML = ` + + No stakes found for this subnet + + `; + } + + // Remove any previously injected network table then add the new one + const existingNetworkTable = document.querySelector('.network-table-container'); + if (existingNetworkTable) { + existingNetworkTable.remove(); + } + document.querySelector('.stakes-table-container').insertAdjacentHTML('afterend', networkTableHTML); + + // Format the new numbers + initializeFormattedNumbers(); + + // Initialize connectivity visualization (the dots / lines "animation") + setTimeout(() => { initNetworkVisualization(); }, 100); + + // Toggle whether we are showing the "Your Stakes" or "Metagraph" table + toggleStakeView(); + + // Initialize sorting on newly injected table columns + initializeSorting(); + + // Auto-sort by Stake descending on the network table for convenience + setTimeout(() => { + const networkTable = document.querySelector('.network-table'); + if (networkTable) { + const stakeColumn = networkTable.querySelector('th:nth-child(2)'); + if (stakeColumn) { + sortTable(networkTable, 1, stakeColumn, true); + stakeColumn.setAttribute('data-sort', 'desc'); + } + } + }, 100); + + console.log('Subnet page updated successfully'); + } catch (error) { + console.error('Error updating subnet page:', error); + } + } + + /** + * Generates the rows for the "Neurons" table (shown when the user unchecks "Show Stakes"). + * Each row, when clicked, calls showNeuronDetails(i). + * @param {Object} metagraphInfo The "metagraph_info" of the subnet that holds hotkeys, etc. + */ + function generateNetworkTableRows(metagraphInfo) { + const rows = []; + console.log('Generating network table rows with data:', metagraphInfo); + + for (let i = 0; i < metagraphInfo.hotkeys.length; i++) { + // Subnet symbol is used to show token vs. root stake + const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet); + const subnetSymbol = subnet ? subnet.symbol : ''; + + // Possibly show hotkey/coldkey truncated for readability + const truncatedHotkey = truncateAddress(metagraphInfo.hotkeys[i]); + const truncatedColdkey = truncateAddress(metagraphInfo.coldkeys[i]); + const identityName = metagraphInfo.updated_identities[i] || '~'; + + // Root stake is being scaled by 0.18 arbitrarily here + const adjustedRootStake = metagraphInfo.tao_stake[i] * 0.18; + + rows.push(` + + ${identityName} + + + + + + + + + + + + + + + + + + + +
+ ${truncatedHotkey} + copy +
+ + +
+ ${truncatedColdkey} + copy +
+ + + `); + } + return rows.join(''); + } + + /** + * Handles toggling between the "Your Stakes" view and the "Neurons" view on the Subnet page. + * The "Show Stakes" checkbox (#stake-toggle) controls which table is visible. + */ + function toggleStakeView() { + const showStakes = document.getElementById('stake-toggle').checked; + const stakesTable = document.querySelector('.stakes-table-container'); + const networkTable = document.querySelector('.network-table-container'); + const sectionHeader = document.querySelector('.view-header'); + const neuronDetails = document.getElementById('neuron-detail-container'); + const addStakeButton = document.querySelector('.add-stake-button'); + const exportCsvButton = document.querySelector('.export-csv-button'); + const stakesHeader = document.querySelector('.stakes-header'); + + // First, close neuron details if they're open + if (neuronDetails && neuronDetails.style.display !== 'none') { + neuronDetails.style.display = 'none'; + } + + // Always show the section header and stakes header when toggling views + if (sectionHeader) sectionHeader.style.display = 'block'; + if (stakesHeader) stakesHeader.style.display = 'flex'; + + if (showStakes) { + // Show the Stakes table, hide the Neurons table + stakesTable.style.display = 'block'; + networkTable.style.display = 'none'; + sectionHeader.textContent = 'Your Stakes'; + if (addStakeButton) { + addStakeButton.style.display = 'none'; + } + if (exportCsvButton) { + exportCsvButton.style.display = 'none'; + } + } else { + // Show the Neurons table, hide the Stakes table + stakesTable.style.display = 'none'; + networkTable.style.display = 'block'; + sectionHeader.textContent = 'Metagraph'; + if (addStakeButton) { + addStakeButton.style.display = 'block'; + } + if (exportCsvButton) { + exportCsvButton.style.display = 'block'; + } + } + } + + /** + * Called when you click a row in the "Neurons" table, to display more detail about that neuron. + * This hides the "Neurons" table and shows the #neuron-detail-container. + * @param {number} rowIndex The index of the neuron in the arrays (hotkeys, coldkeys, etc.) + */ + function showNeuronDetails(rowIndex) { + try { + // Hide the network table & stakes table + const networkTable = document.querySelector('.network-table-container'); + if (networkTable) networkTable.style.display = 'none'; + const stakesTable = document.querySelector('.stakes-table-container'); + if (stakesTable) stakesTable.style.display = 'none'; + + // Hide the stakes header with the action buttons + const stakesHeader = document.querySelector('.stakes-header'); + if (stakesHeader) stakesHeader.style.display = 'none'; + + // Hide the view header that says "Neurons" + const viewHeader = document.querySelector('.view-header'); + if (viewHeader) viewHeader.style.display = 'none'; + + // Show the neuron detail panel + const detailContainer = document.getElementById('neuron-detail-container'); + if (detailContainer) detailContainer.style.display = 'block'; + + // Pull out the current subnet + const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet); + if (!subnet) { + console.error('No subnet data for netuid:', window.currentSubnet); + return; + } + + const metagraphInfo = subnet.metagraph_info; + const subnetSymbol = subnet.symbol || ''; + + // Pull axon data, for IP info + const axonData = metagraphInfo.processed_axons ? metagraphInfo.processed_axons[rowIndex] : null; + let ipInfoString; + + // Update IP info card - hide header if IP info is present + const ipInfoCard = document.getElementById('neuron-ipinfo').closest('.metric-card'); + if (axonData && axonData.ip !== 'N/A') { + // If we have valid IP info, hide the "IP Info" label + if (ipInfoCard && ipInfoCard.querySelector('.metric-label')) { + ipInfoCard.querySelector('.metric-label').style.display = 'none'; + } + // Format IP info with green labels + ipInfoString = `IP: ${axonData.ip}
` + + `Port: ${axonData.port}
` + + `Type: ${axonData.ip_type}`; + } else { + // If no IP info, show the label + if (ipInfoCard && ipInfoCard.querySelector('.metric-label')) { + ipInfoCard.querySelector('.metric-label').style.display = 'block'; + } + ipInfoString = 'N/A'; + } + + // Basic identity and hotkey/coldkey info + const name = metagraphInfo.updated_identities[rowIndex] || '~'; + const hotkey = metagraphInfo.hotkeys[rowIndex]; + const coldkey = metagraphInfo.coldkeys[rowIndex]; + const rank = metagraphInfo.rank ? metagraphInfo.rank[rowIndex] : 0; + const trust = metagraphInfo.trust ? metagraphInfo.trust[rowIndex] : 0; + const pruning = metagraphInfo.pruning_score ? metagraphInfo.pruning_score[rowIndex] : 0; + const vPermit = metagraphInfo.validator_permit ? metagraphInfo.validator_permit[rowIndex] : false; + const lastUpd = metagraphInfo.last_update ? metagraphInfo.last_update[rowIndex] : 0; + const consensus = metagraphInfo.consensus ? metagraphInfo.consensus[rowIndex] : 0; + const regBlock = metagraphInfo.block_at_registration ? metagraphInfo.block_at_registration[rowIndex] : 0; + const active = metagraphInfo.active ? metagraphInfo.active[rowIndex] : false; + + // Update UI fields + document.getElementById('neuron-name').textContent = name; + document.getElementById('neuron-name').style.color = '#FF9900'; + + document.getElementById('neuron-hotkey').textContent = hotkey; + document.getElementById('neuron-coldkey').textContent = coldkey; + document.getElementById('neuron-trust').textContent = trust.toFixed(4); + document.getElementById('neuron-pruning-score').textContent = pruning.toFixed(4); + + // Validator + const validatorElem = document.getElementById('neuron-validator-permit'); + if (vPermit) { + validatorElem.style.color = '#2ECC71'; + validatorElem.textContent = 'True'; + } else { + validatorElem.style.color = '#ff4444'; + validatorElem.textContent = 'False'; + } + + document.getElementById('neuron-last-update').textContent = lastUpd; + document.getElementById('neuron-consensus').textContent = consensus.toFixed(4); + document.getElementById('neuron-reg-block').textContent = regBlock; + document.getElementById('neuron-ipinfo').innerHTML = ipInfoString; + + const activeElem = document.getElementById('neuron-active'); + if (active) { + activeElem.style.color = '#2ECC71'; + activeElem.textContent = 'Yes'; + } else { + activeElem.style.color = '#ff4444'; + activeElem.textContent = 'No'; + } + + // Add stake data ("total_stake", "alpha_stake", "tao_stake") + document.getElementById('neuron-stake-total').setAttribute( + 'data-value', metagraphInfo.total_stake[rowIndex] + ); + document.getElementById('neuron-stake-total').setAttribute( + 'data-symbol', subnetSymbol + ); + + document.getElementById('neuron-stake-token').setAttribute( + 'data-value', metagraphInfo.alpha_stake[rowIndex] + ); + document.getElementById('neuron-stake-token').setAttribute( + 'data-symbol', subnetSymbol + ); + + // Multiply tao_stake by 0.18 + const originalStakeRoot = metagraphInfo.tao_stake[rowIndex]; + const calculatedStakeRoot = originalStakeRoot * 0.18; + + document.getElementById('neuron-stake-root').setAttribute( + 'data-value', calculatedStakeRoot + ); + document.getElementById('neuron-stake-root').setAttribute( + 'data-symbol', root_symbol_html + ); + // Also set the inner text right away, so we show a correct format on load + document.getElementById('neuron-stake-root').innerHTML = + formatNumber(calculatedStakeRoot, root_symbol_html); + + // Dividends, Incentive + document.getElementById('neuron-dividends').setAttribute( + 'data-value', metagraphInfo.dividends[rowIndex] + ); + document.getElementById('neuron-dividends').setAttribute('data-symbol', ''); + + document.getElementById('neuron-incentive').setAttribute( + 'data-value', metagraphInfo.incentives[rowIndex] + ); + document.getElementById('neuron-incentive').setAttribute('data-symbol', ''); + + // Emissions + document.getElementById('neuron-emissions').setAttribute( + 'data-value', metagraphInfo.emission[rowIndex] + ); + document.getElementById('neuron-emissions').setAttribute('data-symbol', subnetSymbol); + + // Rank + document.getElementById('neuron-rank').textContent = rank.toFixed(4); + + // Re-run formatting so the newly updated data-values appear in numeric form + initializeFormattedNumbers(); + } catch (err) { + console.error('Error showing neuron details:', err); + } + } + + /** + * Closes the neuron detail panel and goes back to whichever table was selected ("Stakes" or "Metagraph"). + */ + function closeNeuronDetails() { + // Hide neuron details + const detailContainer = document.getElementById('neuron-detail-container'); + if (detailContainer) detailContainer.style.display = 'none'; + + // Show the stakes header with action buttons + const stakesHeader = document.querySelector('.stakes-header'); + if (stakesHeader) stakesHeader.style.display = 'flex'; + + // Show the view header again + const viewHeader = document.querySelector('.view-header'); + if (viewHeader) viewHeader.style.display = 'block'; + + // Show the appropriate table based on toggle state + const showStakes = document.getElementById('stake-toggle').checked; + const stakesTable = document.querySelector('.stakes-table-container'); + const networkTable = document.querySelector('.network-table-container'); + + if (showStakes) { + stakesTable.style.display = 'block'; + networkTable.style.display = 'none'; + + // Hide action buttons when showing stakes + const addStakeButton = document.querySelector('.add-stake-button'); + const exportCsvButton = document.querySelector('.export-csv-button'); + if (addStakeButton) addStakeButton.style.display = 'none'; + if (exportCsvButton) exportCsvButton.style.display = 'none'; + } else { + stakesTable.style.display = 'none'; + networkTable.style.display = 'block'; + + // Show action buttons when showing metagraph + const addStakeButton = document.querySelector('.add-stake-button'); + const exportCsvButton = document.querySelector('.export-csv-button'); + if (addStakeButton) addStakeButton.style.display = 'block'; + if (exportCsvButton) exportCsvButton.style.display = 'block'; + } + } + + + /* ===================== Number Formatting Functions ===================== */ + /** + * Toggles the numeric display between "verbose" and "short" notations + * across all .formatted-number elements on the page. + */ + function toggleVerboseNumbers() { + // We read from the main or subnet checkboxes + verboseNumbers = + document.getElementById('verbose-toggle')?.checked || + document.getElementById('show-verbose')?.checked || + false; + + // Reformat all visible .formatted-number elements + document.querySelectorAll('.formatted-number').forEach(element => { + const value = parseFloat(element.dataset.value); + const symbol = element.dataset.symbol; + element.innerHTML = formatNumber(value, symbol); + }); + + // If we're currently on the Subnet detail page, update those numbers too + if (document.getElementById('subnet-page').style.display !== 'none') { + updateAllNumbers(); + } + } + + /** + * Scans all .formatted-number elements and replaces their text with + * the properly formatted version (short or verbose). + */ + function initializeFormattedNumbers() { + document.querySelectorAll('.formatted-number').forEach(element => { + const value = parseFloat(element.dataset.value); + const symbol = element.dataset.symbol; + element.innerHTML = formatNumber(value, symbol); + }); + } + + /** + * Called by toggleVerboseNumbers() to reformat key metrics on the Subnet page + * that might not be directly wrapped in .formatted-number but need to be updated anyway. + */ + function updateAllNumbers() { + try { + const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet); + if (!subnet) { + console.error('Could not find subnet data for netuid:', window.currentSubnet); + return; + } + // Reformat a few items in the Subnet detail header + document.querySelector('#subnet-market-cap').innerHTML = + formatNumber(subnet.market_cap, root_symbol_html); + document.querySelector('#subnet-total-stake').innerHTML = + formatNumber(subnet.total_stake, subnet.symbol); + document.querySelector('#subnet-emission').innerHTML = + formatNumber(subnet.emission, root_symbol_html); + + // Reformat the Metagraph table data + const netinfo = subnet.metagraph_info; + document.querySelector('#network-alpha-in').innerHTML = + formatNumber(netinfo.alpha_in, subnet.symbol); + document.querySelector('#network-tau-in').innerHTML = + formatNumber(netinfo.tao_in, root_symbol_html); + + // Reformat items in "Your Stakes" table + document.querySelectorAll('#stakes-table-body .formatted-number').forEach(element => { + const value = parseFloat(element.dataset.value); + const symbol = element.dataset.symbol; + element.innerHTML = formatNumber(value, symbol); + }); + } catch (error) { + console.error('Error updating numbers:', error); + } + } + + /** + * Format a numeric value into either: + * - a short format (e.g. 1.23k, 3.45m) if verboseNumbers==false + * - a more precise format (1,234.5678) if verboseNumbers==true + * @param {number} num The numeric value to format. + * @param {string} symbol A short suffix or currency symbol (e.g. 'τ') that we append. + */ + function formatNumber(num, symbol = '') { + if (num === undefined || num === null || isNaN(num)) { + return '0.00 ' + `${symbol}`; + } + num = parseFloat(num); + if (num === 0) { + return '0.00 ' + `${symbol}`; + } + + // If user requested verbose + if (verboseNumbers) { + return num.toLocaleString('en-US', { + minimumFractionDigits: 4, + maximumFractionDigits: 4 + }) + ' ' + `${symbol}`; + } + + // Otherwise show short scale for large numbers + const absNum = Math.abs(num); + if (absNum >= 1000) { + const suffixes = ['', 'k', 'm', 'b', 't']; + const magnitude = Math.min(4, Math.floor(Math.log10(absNum) / 3)); + const scaledNum = num / Math.pow(10, magnitude * 3); + return scaledNum.toFixed(2) + suffixes[magnitude] + ' ' + + `${symbol}`; + } else { + // For small numbers <1000, just show 4 decimals + return num.toFixed(4) + ' ' + `${symbol}`; + } + } + + /** + * Truncates a string address into the format "ABC..XYZ" for a bit more readability + * @param {string} address + * @returns {string} truncated address form + */ + function truncateAddress(address) { + if (!address || address.length <= 7) { + return address; // no need to truncate if very short + } + return `${address.substring(0, 3)}..${address.substring(address.length - 3)}`; + } + + /** + * Format a number in compact notation (K, M, B) for tile display + */ + function formatTileNumbers(num) { + if (num >= 1000000000) { + return (num / 1000000000).toFixed(1) + 'B'; + } else if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M'; + } else if (num >= 1000) { + return (num / 1000).toFixed(1) + 'K'; + } else { + return num.toFixed(1); + } + } + + + /* ===================== Table Sorting and Filtering Functions ===================== */ + /** + * Switches the Metagraph or Stakes table from sorting ascending to descending on a column, and vice versa. + * @param {HTMLTableElement} table The table element itself + * @param {number} columnIndex The column index to sort by + * @param {HTMLTableHeaderCellElement} header The element clicked + * @param {boolean} forceDescending If true and no existing sort marker, will do a descending sort by default + */ + function sortTable(table, columnIndex, header, forceDescending = false) { + const tbody = table.querySelector('tbody'); + const rows = Array.from(tbody.querySelectorAll('tr')); + + // If forcing descending and the header has no 'data-sort', default to 'desc' + let isDescending; + if (forceDescending && !header.hasAttribute('data-sort')) { + isDescending = true; + } else { + isDescending = header.getAttribute('data-sort') !== 'desc'; + } + + // Clear data-sort from all headers in the table + table.querySelectorAll('th').forEach(th => { + th.removeAttribute('data-sort'); + }); + // Mark the clicked header with new direction + header.setAttribute('data-sort', isDescending ? 'desc' : 'asc'); + + // Sort numerically + rows.sort((rowA, rowB) => { + const cellA = rowA.cells[columnIndex]; + const cellB = rowB.cells[columnIndex]; + + // Attempt to parse float from data-value or fallback to textContent + let valueA = parseFloat(cellA.getAttribute('data-value')) || + parseFloat(cellA.textContent.replace(/[^\\d.-]/g, '')) || + 0; + let valueB = parseFloat(cellB.getAttribute('data-value')) || + parseFloat(cellB.textContent.replace(/[^\\d.-]/g, '')) || + 0; + + return isDescending ? (valueB - valueA) : (valueA - valueB); + }); + + // Reinsert sorted rows + tbody.innerHTML = ''; + rows.forEach(row => tbody.appendChild(row)); + } + + /** + * Adds sortable behavior to certain columns in the "stakes-table" or "network-table". + * Called after these tables are created in showSubnetPage(). + */ + function initializeSorting() { + const networkTable = document.querySelector('.network-table'); + if (networkTable) { + initializeTableSorting(networkTable); + } + const stakesTable = document.querySelector('.stakes-table'); + if (stakesTable) { + initializeTableSorting(stakesTable); + } + } + + /** + * Helper function that attaches sort handlers to appropriate columns in a table. + * @param {HTMLTableElement} table The table element to set up sorting for. + */ + function initializeTableSorting(table) { + const headers = table.querySelectorAll('th'); + headers.forEach((header, index) => { + // We only want some columns to be sortable, as in original code + if (table.classList.contains('stakes-table') && index >= 1 && index <= 5) { + header.classList.add('sortable'); + header.addEventListener('click', () => { + sortTable(table, index, header, true); + }); + } else if (table.classList.contains('network-table') && index < 6) { + header.classList.add('sortable'); + header.addEventListener('click', () => { + sortTable(table, index, header, true); + }); + } + }); + } + + /** + * Filters rows in the Metagraph table by name, hotkey, or coldkey. + * Invoked by the oninput event of the #network-search field. + * @param {string} searchValue The substring typed by the user. + */ + function filterNetworkTable(searchValue) { + const searchTerm = searchValue.toLowerCase().trim(); + const rows = document.querySelectorAll('.network-table tbody tr'); + + rows.forEach(row => { + const nameCell = row.querySelector('.identity-cell'); + const hotkeyContainer = row.querySelector('.hotkey-container[data-full-address]'); + const coldkeyContainer = row.querySelectorAll('.hotkey-container[data-full-address]')[1]; + + const name = nameCell ? nameCell.textContent.toLowerCase() : ''; + const hotkey = hotkeyContainer ? hotkeyContainer.getAttribute('data-full-address').toLowerCase() : ''; + const coldkey= coldkeyContainer ? coldkeyContainer.getAttribute('data-full-address').toLowerCase() : ''; + + const matches = (name.includes(searchTerm) || hotkey.includes(searchTerm) || coldkey.includes(searchTerm)); + row.style.display = matches ? '' : 'none'; + }); + } + + + /* ===================== Network Visualization Functions ===================== */ + /** + * Initializes the network visualization on the canvas element. + */ + function initNetworkVisualization() { + try { + const canvas = document.getElementById('network-canvas'); + if (!canvas) { + console.error('Canvas element (#network-canvas) not found'); + return; + } + const ctx = canvas.getContext('2d'); + + const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet); + if (!subnet) { + console.error('Could not find subnet data for netuid:', window.currentSubnet); + return; + } + const numNeurons = subnet.metagraph_info.num_uids; + const nodes = []; + + // Randomly place nodes, each with a small velocity + for (let i = 0; i < numNeurons; i++) { + nodes.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + radius: 2, + vx: (Math.random() - 0.5) * 0.5, + vy: (Math.random() - 0.5) * 0.5 + }); + } + + // Animation loop + function animate() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + ctx.beginPath(); + ctx.strokeStyle = 'rgba(255, 153, 0, 0.2)'; + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const dx = nodes[i].x - nodes[j].x; + const dy = nodes[i].y - nodes[j].y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < 30) { + ctx.moveTo(nodes[i].x, nodes[i].y); + ctx.lineTo(nodes[j].x, nodes[j].y); + } + } + } + ctx.stroke(); + + nodes.forEach(node => { + node.x += node.vx; + node.y += node.vy; + + // Bounce them off the edges + if (node.x <= 0 || node.x >= canvas.width) node.vx *= -1; + if (node.y <= 0 || node.y >= canvas.height) node.vy *= -1; + + ctx.beginPath(); + ctx.fillStyle = '#FF9900'; + ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2); + ctx.fill(); + }); + + requestAnimationFrame(animate); + } + animate(); + } catch (error) { + console.error('Error in network visualization:', error); + } + } + + + /* ===================== Tile View Functions ===================== */ + /** + * Toggles between the tile view and table view of subnets. + */ + function toggleTileView() { + const showTiles = document.getElementById('show-tiles').checked; + const tilesContainer = document.getElementById('subnet-tiles-container'); + const tableContainer = document.querySelector('.subnets-table-container'); + + if (showTiles) { + // Show tiles, hide table + tilesContainer.style.display = 'flex'; + tableContainer.style.display = 'none'; + + // Generate tiles if they don't exist yet + if (tilesContainer.children.length === 0) { + generateSubnetTiles(); + } + + // Apply current filters to the tiles + filterSubnets(); + } else { + // Show table, hide tiles + tilesContainer.style.display = 'none'; + tableContainer.style.display = 'block'; + } + } + + /** + * Generates the subnet tiles based on the initialData. + */ + function generateSubnetTiles() { + const tilesContainer = document.getElementById('subnet-tiles-container'); + tilesContainer.innerHTML = ''; // Clear existing tiles + + // Sort subnets by market cap (descending) + const sortedSubnets = [...window.initialData.subnets].sort((a, b) => b.market_cap - a.market_cap); + + sortedSubnets.forEach(subnet => { + const isStaked = subnet.your_stakes && subnet.your_stakes.length > 0; + const marketCapFormatted = formatTileNumbers(subnet.market_cap); + + const tile = document.createElement('div'); + tile.className = `subnet-tile ${isStaked ? 'staked' : ''}`; + tile.onclick = () => showSubnetPage(subnet.netuid); + + // Calculate background intensity based on market cap relative to max + const maxMarketCap = sortedSubnets[0].market_cap; + const intensity = Math.max(5, Math.min(15, 5 + (subnet.market_cap / maxMarketCap) * 10)); + + tile.innerHTML = ` + ${subnet.netuid} + ${subnet.symbol} + ${subnet.name} + ${marketCapFormatted} ${root_symbol_html} + `; + + // Set background intensity + tile.style.background = `rgba(255, 255, 255, 0.0${intensity.toFixed(0)})`; + + tilesContainer.appendChild(tile); + }); + } + """ From 7964abc3daf5762b511184cbaba6e87cae080024 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 28 Feb 2025 17:40:24 -0800 Subject: [PATCH 3/6] Adds deps msg + loading animation --- bittensor_cli/cli.py | 9 ++++++++- bittensor_cli/src/commands/view.py | 11 ++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8bf92aec..6619e948 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -641,7 +641,12 @@ def __init__(self): ) # view app - self.app.add_typer(self.view_app, name="view", short_help="HTML view commands", no_args_is_help=True) + self.app.add_typer( + self.view_app, + name="view", + short_help="HTML view commands", + no_args_is_help=True, + ) # config commands self.config_app.command("set")(self.set_config) @@ -5093,6 +5098,8 @@ def view_dashboard( Display html dashboard with subnets list, stake, and neuron information. """ self.verbosity_handler(quiet, verbose) + if is_linux(): + print_linux_dependency_message() wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) diff --git a/bittensor_cli/src/commands/view.py b/bittensor_cli/src/commands/view.py index 0e568a65..d6eb0263 100644 --- a/bittensor_cli/src/commands/view.py +++ b/bittensor_cli/src/commands/view.py @@ -35,9 +35,14 @@ async def display_network_dashboard( Generate and display the HTML interface. """ try: - _subnet_data = await fetch_subnet_data(wallet, subtensor) - subnet_data = process_subnet_data(_subnet_data) - html_content = generate_full_page(subnet_data) + with console.status("[dark_sea_green3]Fetching data...", spinner="earth"): + _subnet_data = await fetch_subnet_data(wallet, subtensor) + subnet_data = process_subnet_data(_subnet_data) + html_content = generate_full_page(subnet_data) + + console.print( + "[dark_sea_green3]Opening dashboard in a window. Press Ctrl+C to close.[/dark_sea_green3]" + ) window = PyWry() window.send_html( html=html_content, From 41e03b0c2bf569926fe3c9b873ffdcfcd8f19693 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 28 Feb 2025 17:43:45 -0800 Subject: [PATCH 4/6] Updates subtensor call --- bittensor_cli/src/bittensor/subtensor_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 57cdabee..59dd2fcb 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1271,7 +1271,7 @@ async def get_metagraph_info( except ValueError: bytes_result = bytes.fromhex(hex_bytes_result) - return MetagraphInfo.from_vec_u8(bytes_result) + return MetagraphInfo.from_any(bytes_result) async def get_all_metagraphs_info( self, block_hash: Optional[str] = None From c2847a9f25c399fe5b968f95e85596461870b9dc Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 28 Feb 2025 18:33:45 -0800 Subject: [PATCH 5/6] Updates deps msg --- bittensor_cli/src/bittensor/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index a1a4f1ae..abcc7a6d 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1279,7 +1279,7 @@ def print_linux_dependency_message(): """Prints the WebKit dependency message for Linux systems.""" console.print("[red]This command requires WebKit dependencies on Linux.[/red]") console.print( - "\nPlease install the required packages using one of the following commands based on your distribution:" + "\nPlease make sure these packages are installed on your system for PyWry to work:" ) console.print("\nArch Linux / Manjaro:") console.print("[green]sudo pacman -S webkit2gtk[/green]") From 325ffa7548c3598ce3b37aa39980f4f4be58d9ec Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 28 Feb 2025 19:07:02 -0800 Subject: [PATCH 6/6] Adds extra info for deps msg --- bittensor_cli/src/bittensor/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index abcc7a6d..04a4bafa 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1285,8 +1285,13 @@ def print_linux_dependency_message(): console.print("[green]sudo pacman -S webkit2gtk[/green]") console.print("\nDebian / Ubuntu:") console.print("[green]sudo apt install libwebkit2gtk-4.0-dev[/green]") + console.print("\nNote for Ubuntu 24.04+ & Debian 13+:") + console.print( + "You may need to add the following line to your `/etc/apt/sources.list` file:" + ) + console.print("[green]http://gb.archive.ubuntu.com/ubuntu jammy main[/green]") console.print("\nFedora / CentOS / AlmaLinux:") - console.print("[green]sudo dnf install gtk3-devel webkit2gtk3-devel[/green]") + console.print("[green]sudo dnf install gtk3-devel webkit2gtk3-devel[/green]\n\n") def is_linux():