diff --git a/cli.py b/cli.py index c801a5ab..eeb5f91a 100755 --- a/cli.py +++ b/cli.py @@ -106,6 +106,7 @@ class Options: ) reuse_last = typer.Option( False, + "--reuse-last", help="Reuse the metagraph data you last retrieved. Only use this if you have already retrieved metagraph" "data", ) @@ -2357,6 +2358,8 @@ def stake_show( wallet_name: Optional[str] = Options.wallet_name, wallet_hotkey: Optional[str] = Options.wallet_hotkey, wallet_path: Optional[str] = Options.wallet_path, + reuse_last: bool = Options.reuse_last, + html_output: bool = Options.html_output, ): """ # stake show @@ -2393,9 +2396,14 @@ def stake_show( This command is essential for users who wish to monitor their stake distribution and returns across various accounts on the Bittensor network. It provides a clear and detailed overview of the user's staking activities. """ - wallet = self.wallet_ask(wallet_name, wallet_path, wallet_hotkey) + if not reuse_last: + subtensor = self.initialize_chain(network, chain) + wallet = Wallet() + else: + subtensor = None + wallet = self.wallet_ask(wallet_name, wallet_path, wallet_hotkey) return self._run_command( - stake.show(wallet, self.initialize_chain(network, chain), all_wallets) + stake.show(wallet, subtensor, all_wallets, reuse_last, html_output) ) def stake_add( diff --git a/src/commands/stake.py b/src/commands/stake.py index b5fa2e23..c64e4fe2 100644 --- a/src/commands/stake.py +++ b/src/commands/stake.py @@ -1,5 +1,7 @@ import asyncio import copy +import json +import sqlite3 from contextlib import suppress from math import floor from typing import TYPE_CHECKING, Union, Optional, Sequence, cast @@ -20,6 +22,11 @@ is_valid_ss58_address, float_to_u64, u16_normalized_float, + get_metadata_table, + update_metadata_table, + create_table, + render_table, + render_tree, ) if TYPE_CHECKING: @@ -937,16 +944,14 @@ def normalize_children_and_proportions( # Commands -async def show(wallet: Wallet, subtensor: "SubtensorInterface", all_wallets: bool): +async def show( + wallet: Wallet, + subtensor: Optional["SubtensorInterface"], + all_wallets: bool, + reuse_last: bool, + html_output: bool, +): """Show all stake accounts.""" - if all_wallets: - wallets = get_coldkey_wallets_for_path(wallet.path) - else: - wallets = [wallet] - - registered_delegate_info = await get_delegates_details_from_github( - Constants.delegates_detail_url - ) async def get_stake_accounts( wallet_, block_hash: str @@ -1087,53 +1092,150 @@ async def get_all_wallet_accounts( ) return accounts_ - with console.status(":satellite:Retrieving account data..."): - block_hash_ = await subtensor.substrate.get_chain_head() - accounts = await get_all_wallet_accounts(block_hash=block_hash_) - - total_stake: float = 0.0 - total_balance: float = 0.0 - total_rate: float = 0.0 - for acc in accounts: - total_balance += cast(Balance, acc["balance"]).tao - for key, value in cast(dict, acc["accounts"]).items(): - total_stake += cast(Balance, value["stake"]).tao - total_rate += float(value["rate"]) - table = Table( - Column( - "[overline white]Coldkey", footer_style="overline white", style="bold white" - ), - Column( - "[overline white]Balance", - "\u03c4{:.5f}".format(total_balance), - footer_style="overline white", - style="green", - ), - Column("[overline white]Account", footer_style="overline white", style="blue"), - Column( - "[overline white]Stake", - "\u03c4{:.5f}".format(total_stake), - footer_style="overline white", - style="green", - ), - Column( - "[overline white]Rate", - "\u03c4{:.5f}/d".format(total_rate), - footer_style="overline white", - style="green", - ), - show_footer=True, - pad_edge=False, - box=None, - expand=False, - ) - for acc in accounts: - table.add_row(cast(str, acc["name"]), cast(Balance, acc["balance"]), "", "") - for key, value in cast(dict, acc["accounts"]).items(): - table.add_row( - "", "", value["name"], value["stake"], str(value["rate"]) + "/d" + if not reuse_last: + cast(subtensor, "SubtensorInterface") + if all_wallets: + wallets = get_coldkey_wallets_for_path(wallet.path) + else: + wallets = [wallet] + + registered_delegate_info = await get_delegates_details_from_github( + Constants.delegates_detail_url + ) + + with console.status(":satellite:Retrieving account data..."): + block_hash_ = await subtensor.substrate.get_chain_head() + accounts = await get_all_wallet_accounts(block_hash=block_hash_) + + total_stake: float = 0.0 + total_balance: float = 0.0 + total_rate: float = 0.0 + rows = [] + db_rows = [] + for acc in accounts: + cast(str, acc["name"]) + cast(Balance, acc["balance"]) + rows.append([acc["name"], str(acc["balance"]), "", "", ""]) + db_rows.append([acc["name"], float(acc["balance"]), None, None, None, 0]) + total_balance += cast(Balance, acc["balance"]).tao + for key, value in cast(dict, acc["accounts"]).items(): + rows.append( + [ + "", + "", + value["name"], + str(value["stake"]), + str(value["rate"]) + "/d", + ] + ) + db_rows.append( + [ + acc["name"], + None, + value["name"], + float(value["stake"]), + float(value["rate"]), + 1, + ] + ) + total_stake += cast(Balance, value["stake"]).tao + total_rate += float(value["rate"]) + create_table( + "stakeshow", + [ + ("COLDKEY", "TEXT"), + ("BALANCE", "REAL"), + ("ACCOUNT", "TEXT"), + ("STAKE", "REAL"), + ("RATE", "REAL"), + ("CHILD", "INTEGER"), + ], + db_rows, + ) + metadata = { + "total_stake": "\u03c4{:.5f}".format(total_stake), + "total_balance": "\u03c4{:.5f}".format(total_balance), + "total_rate": "\u03c4{:.5f}/d".format(total_rate), + "rows": json.dumps(rows), + } + update_metadata_table("stakeshow", metadata) + else: + try: + metadata = get_metadata_table("stakeshow") + rows = json.loads(metadata["rows"]) + except sqlite3.OperationalError: + err_console.print( + "[red]Error[/red] Unable to retrieve table data. This is usually caused by attempting to use " + "`--reuse-last` before running the command a first time. In rare cases, this could also be due to " + "a corrupted database. Re-run the command (do not use `--reuse-last`) and see if that resolves your " + "issue." ) - console.print(table) + return + if not html_output: + table = Table( + Column( + "[overline white]Coldkey", + footer_style="overline white", + style="bold white", + ), + Column( + "[overline white]Balance", + metadata["total_balance"], + footer_style="overline white", + style="green", + ), + Column( + "[overline white]Account", footer_style="overline white", style="blue" + ), + Column( + "[overline white]Stake", + metadata["total_stake"], + footer_style="overline white", + style="green", + ), + Column( + "[overline white]Rate", + metadata["total_rate"], + footer_style="overline white", + style="green", + ), + show_footer=True, + pad_edge=False, + box=None, + expand=False, + ) + for row in rows: + table.add_row(*row) + console.print(table) + else: + render_tree( + "stakeshow", + f"Stakes | Total Balance: {metadata['total_balance']} - Total Stake: {metadata['total_stake']} " + f"Total Rate: {metadata['total_rate']}", + [ + {"title": "Coldkey", "field": "COLDKEY"}, + { + "title": "Balance", + "field": "BALANCE", + "formatter": "money", + "formatterParams": {"symbol": "τ", "precision": 5}, + }, + {"title": "Account", "field": "ACCOUNT"}, + { + "title": "Stake", + "field": "STAKE", + "formatter": "money", + "formatterParams": {"symbol": "τ", "precision": 5}, + }, + { + "title": "Daily Rate", + "field": "RATE", + "formatter": "money", + "formatterParams": {"symbol": "τ", "precision": 5}, + }, + ], + 0, + ) async def stake_add( diff --git a/src/commands/subnets.py b/src/commands/subnets.py index cf20bdab..87e046e2 100644 --- a/src/commands/subnets.py +++ b/src/commands/subnets.py @@ -309,14 +309,18 @@ async def _get_all_subnets_info(): "title": "EMISSION", "field": "EMISSION", "formatter": "money", - "formatterParams": {"symbolAfter": "%", "precision": 2}, + "formatterParams": { + "symbolAfter": "p", + "symbol": "%", + "precision": 2, + }, }, {"title": "Tempo", "field": "TEMPO"}, { "title": "Recycle", "field": "RECYCLE", "formatter": "money", - "formatterParams": {"symbol": "", "precision": 5}, + "formatterParams": {"symbol": "τ", "precision": 5}, }, { "title": "Difficulty", diff --git a/src/templates/table.j2 b/src/templates/table.j2 index 0df9fd3b..e2848b6b 100644 --- a/src/templates/table.j2 +++ b/src/templates/table.j2 @@ -108,7 +108,7 @@ delete column.customFormatter; } }); - + const None = null; const table = new Tabulator("#my-table", { columns: columns, @@ -119,6 +119,7 @@ movableColumns: true, paginationCounter: "rows", layout: "fitColumns", + {% if tree %} dataTree:true, {% endif %} } ) //Define variables for input elements diff --git a/src/utils.py b/src/utils.py index 5cc98943..391187cd 100644 --- a/src/utils.py +++ b/src/utils.py @@ -556,16 +556,15 @@ def create_table(title: str, columns: list[tuple[str, str]], rows: list[list]) - cursor.execute(creation_query) cursor.execute(f"DELETE FROM {title};") query = f"INSERT INTO {title} ({', '.join([x[0] for x in columns])}) VALUES ({', '.join(['?'] * len(columns))})" - for row in rows: - cursor.execute(query, row) - # cursor.executemany(query, rows) + cursor.executemany(query, rows) return -def read_table(table_name: str) -> tuple[list, list]: +def read_table(table_name: str, order_by: str = "") -> tuple[list, list]: """ Reads a table from a SQLite database, returning back a column names and rows as a tuple :param table_name: the table name in the database + :param order_by: the order of the columns in the table, optional :return: ([column names], [rows]) """ with DB() as (conn, cursor): @@ -573,7 +572,7 @@ def read_table(table_name: str) -> tuple[list, list]: columns_info = cursor.fetchall() column_names = [info[1] for info in columns_info] column_types = [info[2] for info in columns_info] - cursor.execute(f"SELECT * FROM {table_name}") + cursor.execute(f"SELECT * FROM {table_name} {order_by}") rows = cursor.fetchall() blob_cols = [] for idx, col_type in enumerate(column_types): @@ -649,6 +648,75 @@ def render_table(table_name: str, table_info: str, columns: list[dict], show=Tru rows=Markup([{c: v for (c, v) in zip(db_cols, r)} for r in rows]), column_names=db_cols, table_info=table_info, + tree=False, + ) + output_file = "/tmp/bittensor_table.html" + with open(output_file, "w+") as f: + f.write(rendered) + if show: + webbrowser.open(f"file://{output_file}") + + +def render_tree( + table_name: str, + table_info: str, + columns: list[dict], + parent_column: int = 0, + show=True, +): + """ + Largely the same as render_table, but this renders the table with nested data. + This is done by a table looking like: (FOO ANY, BAR ANY, BAZ ANY, CHILD INTEGER) + where CHILD is 0 or 1, determining if the row should be treated as a child of another row. + The parent and child rows should contain same value for the given parent_column + + E.g. Let's say you have rows as such: + (COLDKEY TEXT, BALANCE REAL, STAKE REAL, CHILD INTEGER) + ("5GTjidas", 1.0, 0.0, 0) + ("5GTjidas", 0.0, 1.0, 1) + ("DJIDSkod", 1.0, 0.0, 0) + + This will be rendered as: + Coldkey | Balance | Stake + 5GTjidas | 1.0 | 0.0 + └ | 0.0 | 1.0 + DJIDSkod | 1.0 | 0.0 + + :param table_name: The table name in the database + :param table_info: Think of this like a subtitle + :param columns: list of dicts that conform to Tabulator's expected columns format + :param parent_column: the index of the column to use as for parent reference + :param show: whether to open a browser window with the rendered table HTML + :return: None + """ + db_cols, rows = read_table(table_name, "ORDER BY CHILD ASC") + template_dir = os.path.join(os.path.dirname(__file__), "templates") + result = [] + parent_dicts = {} + for row in rows: + row_dict = {c: v for (c, v) in zip(db_cols, row)} + child = row_dict["CHILD"] + del row_dict["CHILD"] + if child == 0: + row_dict["_children"] = [] + result.append(row_dict) + parent_dicts[row_dict[db_cols[parent_column]]] = ( + row_dict # Reference to row obj + ) + elif child == 1: + parent_key = row[parent_column] + row_dict[db_cols[parent_column]] = None + if parent_key in parent_dicts: + parent_dicts[parent_key]["_children"].append(row_dict) + with open(os.path.join(template_dir, "table.j2"), "r") as f: + template = Template(f.read()) + rendered = template.render( + title=table_name, + columns=Markup(columns), + rows=Markup(result), + column_names=db_cols, + table_info=table_info, + tree=True, ) output_file = "/tmp/bittensor_table.html" with open(output_file, "w+") as f: