bittensor-cli 8.4.2__py3-none-any.whl → 9.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. bittensor_cli/__init__.py +2 -2
  2. bittensor_cli/cli.py +1503 -1372
  3. bittensor_cli/src/__init__.py +625 -197
  4. bittensor_cli/src/bittensor/balances.py +41 -8
  5. bittensor_cli/src/bittensor/chain_data.py +557 -428
  6. bittensor_cli/src/bittensor/extrinsics/registration.py +161 -47
  7. bittensor_cli/src/bittensor/extrinsics/root.py +14 -8
  8. bittensor_cli/src/bittensor/extrinsics/transfer.py +14 -21
  9. bittensor_cli/src/bittensor/minigraph.py +46 -8
  10. bittensor_cli/src/bittensor/subtensor_interface.py +572 -253
  11. bittensor_cli/src/bittensor/utils.py +326 -75
  12. bittensor_cli/src/commands/stake/__init__.py +154 -0
  13. bittensor_cli/src/commands/stake/children_hotkeys.py +123 -91
  14. bittensor_cli/src/commands/stake/move.py +1000 -0
  15. bittensor_cli/src/commands/stake/stake.py +1637 -1264
  16. bittensor_cli/src/commands/subnets/__init__.py +0 -0
  17. bittensor_cli/src/commands/subnets/price.py +867 -0
  18. bittensor_cli/src/commands/subnets/subnets.py +2043 -0
  19. bittensor_cli/src/commands/sudo.py +529 -26
  20. bittensor_cli/src/commands/wallets.py +231 -535
  21. bittensor_cli/src/commands/weights.py +15 -11
  22. {bittensor_cli-8.4.2.dist-info → bittensor_cli-9.0.0rc1.dist-info}/METADATA +7 -4
  23. bittensor_cli-9.0.0rc1.dist-info/RECORD +32 -0
  24. bittensor_cli/src/bittensor/async_substrate_interface.py +0 -2748
  25. bittensor_cli/src/commands/root.py +0 -1752
  26. bittensor_cli/src/commands/subnets.py +0 -897
  27. bittensor_cli-8.4.2.dist-info/RECORD +0 -31
  28. {bittensor_cli-8.4.2.dist-info → bittensor_cli-9.0.0rc1.dist-info}/WHEEL +0 -0
  29. {bittensor_cli-8.4.2.dist-info → bittensor_cli-9.0.0rc1.dist-info}/entry_points.txt +0 -0
  30. {bittensor_cli-8.4.2.dist-info → bittensor_cli-9.0.0rc1.dist-info}/top_level.txt +0 -0
@@ -1,43 +1,42 @@
1
1
  import ast
2
- from collections import namedtuple
3
2
  import math
4
3
  import os
5
4
  import sqlite3
5
+ import platform
6
6
  import webbrowser
7
+ import sys
7
8
  from pathlib import Path
8
9
  from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable
9
10
  from urllib.parse import urlparse
11
+ from functools import partial
12
+ import re
10
13
 
11
14
  from bittensor_wallet import Wallet, Keypair
12
15
  from bittensor_wallet.utils import SS58_FORMAT
13
- from bittensor_wallet.errors import KeyFileError, PasswordError
16
+ from bittensor_wallet.errors import KeyFileError
14
17
  from bittensor_wallet import utils
15
18
  from jinja2 import Template
16
19
  from markupsafe import Markup
17
20
  import numpy as np
18
21
  from numpy.typing import NDArray
19
22
  from rich.console import Console
20
- import scalecodec
21
- from scalecodec.base import RuntimeConfiguration
22
- from scalecodec.type_registry import load_type_registry_preset
23
+ from rich.prompt import Prompt
24
+ from scalecodec.utils.ss58 import ss58_encode, ss58_decode
23
25
  import typer
24
26
 
25
27
 
26
28
  from bittensor_cli.src.bittensor.balances import Balance
29
+ from bittensor_cli.src import defaults, Constants
27
30
 
28
31
 
29
32
  if TYPE_CHECKING:
30
33
  from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters
31
- from bittensor_cli.src.bittensor.async_substrate_interface import (
32
- AsyncSubstrateInterface,
33
- )
34
+ from async_substrate_interface.async_substrate import AsyncSubstrateInterface
34
35
 
35
36
  console = Console()
36
37
  err_console = Console(stderr=True)
37
38
  verbose_console = Console(quiet=True)
38
39
 
39
- UnlockStatus = namedtuple("UnlockStatus", ["success", "message"])
40
-
41
40
 
42
41
  def print_console(message: str, colour: str, title: str, console: Console):
43
42
  console.print(
@@ -241,14 +240,11 @@ def get_hotkey_wallets_for_wallet(
241
240
  def get_coldkey_wallets_for_path(path: str) -> list[Wallet]:
242
241
  """Gets all wallets with coldkeys from a given path"""
243
242
  wallet_path = Path(path).expanduser()
244
- try:
245
- wallets = [
246
- Wallet(name=directory.name, path=path)
247
- for directory in wallet_path.iterdir()
248
- if directory.is_dir()
249
- ]
250
- except FileNotFoundError:
251
- wallets = []
243
+ wallets = [
244
+ Wallet(name=directory.name, path=path)
245
+ for directory in wallet_path.iterdir()
246
+ if directory.is_dir()
247
+ ]
252
248
  return wallets
253
249
 
254
250
 
@@ -376,21 +372,15 @@ def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool
376
372
  return False
377
373
 
378
374
 
379
- def decode_scale_bytes(return_type, scale_bytes, custom_rpc_type_registry):
380
- """Decodes a ScaleBytes object using our type registry and return type"""
381
- rpc_runtime_config = RuntimeConfiguration()
382
- rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy"))
383
- rpc_runtime_config.update_type_registry(custom_rpc_type_registry)
384
- obj = rpc_runtime_config.create_scale_object(return_type, scale_bytes)
385
- if obj.data.to_hex() == "0x0400": # RPC returned None result
386
- return None
387
- return obj.decode()
375
+ def decode_account_id(account_id_bytes: Union[tuple[int], tuple[tuple[int]]]):
376
+ if isinstance(account_id_bytes, tuple) and isinstance(account_id_bytes[0], tuple):
377
+ account_id_bytes = account_id_bytes[0]
378
+ # Convert the AccountId bytes to a Base64 string
379
+ return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT)
388
380
 
389
381
 
390
- def ss58_address_to_bytes(ss58_address: str) -> bytes:
391
- """Converts a ss58 address to a bytes object."""
392
- account_id_hex: str = scalecodec.ss58_decode(ss58_address, SS58_FORMAT)
393
- return bytes.fromhex(account_id_hex)
382
+ def encode_account_id(ss58_address: str) -> bytes:
383
+ return bytes.fromhex(ss58_decode(ss58_address, SS58_FORMAT))
394
384
 
395
385
 
396
386
  def ss58_to_vec_u8(ss58_address: str) -> list[int]:
@@ -401,7 +391,7 @@ def ss58_to_vec_u8(ss58_address: str) -> list[int]:
401
391
 
402
392
  :return: A list of integers representing the byte values of the SS58 address.
403
393
  """
404
- ss58_bytes: bytes = ss58_address_to_bytes(ss58_address)
394
+ ss58_bytes: bytes = encode_account_id(ss58_address)
405
395
  encoded_address: list[int] = [int(byte) for byte in ss58_bytes]
406
396
  return encoded_address
407
397
 
@@ -458,13 +448,16 @@ def get_explorer_url_for_network(
458
448
  return explorer_urls
459
449
 
460
450
 
461
- def format_error_message(error_message: Union[dict, Exception]) -> str:
451
+ def format_error_message(
452
+ error_message: Union[dict, Exception], substrate: "AsyncSubstrateInterface"
453
+ ) -> str:
462
454
  """
463
455
  Formats an error message from the Subtensor error information for use in extrinsics.
464
456
 
465
457
  Args:
466
458
  error_message: A dictionary containing the error information from Subtensor, or a SubstrateRequestException
467
459
  containing dictionary literal args.
460
+ substrate: The initialised SubstrateInterface object to use.
468
461
 
469
462
  Returns:
470
463
  str: A formatted error message string.
@@ -486,7 +479,7 @@ def format_error_message(error_message: Union[dict, Exception]) -> str:
486
479
  elif all(x in d for x in ["code", "message", "data"]):
487
480
  new_error_message = d
488
481
  break
489
- except ValueError:
482
+ except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
490
483
  pass
491
484
  if new_error_message is None:
492
485
  return_val = " ".join(error_message.args)
@@ -564,7 +557,7 @@ def decode_hex_identity_dict(info_dictionary) -> dict[str, Any]:
564
557
  def get_decoded(data: str) -> str:
565
558
  """Decodes a hex-encoded string."""
566
559
  try:
567
- return hex_to_bytes(data).decode()
560
+ return bytes.fromhex(data[2:]).decode()
568
561
  except UnicodeDecodeError:
569
562
  print(f"Could not decode: {key}: {item}")
570
563
 
@@ -630,6 +623,32 @@ def millify(n: int):
630
623
  return "{:.2f}{}".format(n_ / 10 ** (3 * mill_idx), mill_names[mill_idx])
631
624
 
632
625
 
626
+ def millify_tao(n: float, start_at: str = "K") -> str:
627
+ """
628
+ Dupe of millify, but for ease in converting tao values.
629
+ Allows thresholds to be specified for different suffixes.
630
+ """
631
+ mill_names = ["", "k", "m", "b", "t"]
632
+ thresholds = {"K": 1, "M": 2, "B": 3, "T": 4}
633
+
634
+ if start_at not in thresholds:
635
+ raise ValueError(f"start_at must be one of {list(thresholds.keys())}")
636
+
637
+ n_ = float(n)
638
+ if n_ == 0:
639
+ return "0.00"
640
+
641
+ mill_idx = int(math.floor(math.log10(abs(n_)) / 3))
642
+
643
+ # Number's index is below our threshold, return with commas
644
+ if mill_idx < thresholds[start_at]:
645
+ return f"{n_:,.2f}"
646
+
647
+ mill_idx = max(thresholds[start_at], min(len(mill_names) - 1, mill_idx))
648
+
649
+ return "{:.2f}{}".format(n_ / 10 ** (3 * mill_idx), mill_names[mill_idx])
650
+
651
+
633
652
  def normalize_hyperparameters(
634
653
  subnet: "SubnetHyperparameters",
635
654
  ) -> list[tuple[str, str, str]]:
@@ -942,7 +961,7 @@ def retry_prompt(
942
961
  rejection_text: str,
943
962
  default="",
944
963
  show_default=False,
945
- prompt_type=typer.prompt,
964
+ prompt_type=Prompt.ask,
946
965
  ):
947
966
  """
948
967
  Allows for asking prompts again if they do not meet a certain criteria (as defined in `rejection`)
@@ -965,48 +984,280 @@ def retry_prompt(
965
984
  err_console.print(rejection_text)
966
985
 
967
986
 
968
- def unlock_key(
969
- wallet: Wallet, unlock_type="cold", print_out: bool = True
970
- ) -> "UnlockStatus":
971
- """
972
- Attempts to decrypt a wallet's coldkey or hotkey
973
- Args:
974
- wallet: a Wallet object
975
- unlock_type: the key type, 'cold' or 'hot'
976
- print_out: whether to print out the error message to the err_console
987
+ def validate_netuid(value: int) -> int:
988
+ if value is not None and value < 0:
989
+ raise typer.BadParameter("Negative netuid passed. Please use correct netuid.")
990
+ return value
991
+
992
+
993
+ def validate_uri(uri: str) -> str:
994
+ if not uri:
995
+ return None
996
+ clean_uri = uri.lstrip("/").lower()
997
+ if not clean_uri.isalnum():
998
+ raise typer.BadParameter(
999
+ f"Invalid URI format: {uri}. URI must contain only alphanumeric characters (e.g. 'alice', 'bob')"
1000
+ )
1001
+ return f"//{clean_uri.capitalize()}"
977
1002
 
978
- Returns: UnlockStatus for success status of unlock, with error message if unsuccessful
979
1003
 
1004
+ def get_effective_network(config, network: Optional[list[str]]) -> str:
1005
+ """
1006
+ Determines the effective network to be used, considering the network parameter,
1007
+ the configuration, and the default.
980
1008
  """
981
- if unlock_type == "cold":
982
- unlocker = "unlock_coldkey"
983
- elif unlock_type == "hot":
984
- unlocker = "unlock_hotkey"
1009
+ if network:
1010
+ for item in network:
1011
+ if item.startswith("ws"):
1012
+ network_ = item
1013
+ break
1014
+ else:
1015
+ network_ = item
1016
+ return network_
1017
+ elif config.get("network"):
1018
+ return config["network"]
985
1019
  else:
986
- raise ValueError(
987
- f"Invalid unlock type provided: {unlock_type}. Must be 'cold' or 'hot'."
1020
+ return defaults.subtensor.network
1021
+
1022
+
1023
+ def is_rao_network(network: str) -> bool:
1024
+ """Check if the given network is 'rao'."""
1025
+ network = network.lower()
1026
+ rao_identifiers = [
1027
+ "rao",
1028
+ Constants.rao_entrypoint,
1029
+ ]
1030
+ return (
1031
+ network == "rao"
1032
+ or network in rao_identifiers
1033
+ or "rao.chain.opentensor.ai" in network
1034
+ )
1035
+
1036
+
1037
+ def prompt_for_identity(
1038
+ current_identity: dict,
1039
+ name: Optional[str],
1040
+ web_url: Optional[str],
1041
+ image_url: Optional[str],
1042
+ discord: Optional[str],
1043
+ description: Optional[str],
1044
+ additional: Optional[str],
1045
+ github_repo: Optional[str],
1046
+ ):
1047
+ """
1048
+ Prompts the user for identity fields with validation.
1049
+ Returns a dictionary with the updated fields.
1050
+ """
1051
+ identity_fields = {}
1052
+
1053
+ fields = [
1054
+ ("name", "[blue]Display name[/blue]", name),
1055
+ ("url", "[blue]Web URL[/blue]", web_url),
1056
+ ("image", "[blue]Image URL[/blue]", image_url),
1057
+ ("discord", "[blue]Discord handle[/blue]", discord),
1058
+ ("description", "[blue]Description[/blue]", description),
1059
+ ("additional", "[blue]Additional information[/blue]", additional),
1060
+ ("github_repo", "[blue]GitHub repository URL[/blue]", github_repo),
1061
+ ]
1062
+
1063
+ text_rejection = partial(
1064
+ retry_prompt,
1065
+ rejection=lambda x: sys.getsizeof(x) > 113,
1066
+ rejection_text="[red]Error:[/red] Identity field must be <= 64 raw bytes.",
1067
+ )
1068
+
1069
+ if not any(
1070
+ [
1071
+ name,
1072
+ web_url,
1073
+ image_url,
1074
+ discord,
1075
+ description,
1076
+ additional,
1077
+ github_repo,
1078
+ ]
1079
+ ):
1080
+ console.print(
1081
+ "\n[yellow]All fields are optional. Press Enter to skip and keep the default/existing value.[/yellow]\n"
1082
+ "[dark_sea_green3]Tip: Entering a space and pressing Enter will clear existing default value.\n"
988
1083
  )
1084
+
1085
+ for key, prompt, value in fields:
1086
+ if value:
1087
+ identity_fields[key] = value
1088
+ else:
1089
+ identity_fields[key] = text_rejection(
1090
+ prompt,
1091
+ default=current_identity.get(key, ""),
1092
+ show_default=True,
1093
+ )
1094
+
1095
+ return identity_fields
1096
+
1097
+
1098
+ def prompt_for_subnet_identity(
1099
+ subnet_name: Optional[str],
1100
+ github_repo: Optional[str],
1101
+ subnet_contact: Optional[str],
1102
+ subnet_url: Optional[str],
1103
+ discord: Optional[str],
1104
+ description: Optional[str],
1105
+ additional: Optional[str],
1106
+ ):
1107
+ """
1108
+ Prompts the user for required subnet identity fields with validation.
1109
+ Returns a dictionary with the updated fields.
1110
+
1111
+ Args:
1112
+ subnet_name (Optional[str]): Name of the subnet
1113
+ github_repo (Optional[str]): GitHub repository URL
1114
+ subnet_contact (Optional[str]): Contact information for subnet (email)
1115
+
1116
+ Returns:
1117
+ dict: Dictionary containing the subnet identity fields
1118
+ """
1119
+ identity_fields = {}
1120
+
1121
+ fields = [
1122
+ (
1123
+ "subnet_name",
1124
+ "[blue]Subnet name [dim](optional)[/blue]",
1125
+ subnet_name,
1126
+ lambda x: x and sys.getsizeof(x) > 113,
1127
+ "[red]Error:[/red] Subnet name must be <= 64 raw bytes.",
1128
+ ),
1129
+ (
1130
+ "github_repo",
1131
+ "[blue]GitHub repository URL [dim](optional)[/blue]",
1132
+ github_repo,
1133
+ lambda x: x and not is_valid_github_url(x),
1134
+ "[red]Error:[/red] Please enter a valid GitHub repository URL (e.g., https://github.com/username/repo).",
1135
+ ),
1136
+ (
1137
+ "subnet_contact",
1138
+ "[blue]Contact email [dim](optional)[/blue]",
1139
+ subnet_contact,
1140
+ lambda x: x and not is_valid_contact(x),
1141
+ "[red]Error:[/red] Please enter a valid email address.",
1142
+ ),
1143
+ (
1144
+ "subnet_url",
1145
+ "[blue]Subnet URL [dim](optional)[/blue]",
1146
+ subnet_url,
1147
+ lambda x: x and sys.getsizeof(x) > 113,
1148
+ "[red]Error:[/red] Please enter a valid URL.",
1149
+ ),
1150
+ (
1151
+ "discord",
1152
+ "[blue]Discord handle [dim](optional)[/blue]",
1153
+ discord,
1154
+ lambda x: x and sys.getsizeof(x) > 113,
1155
+ "[red]Error:[/red] Please enter a valid Discord handle.",
1156
+ ),
1157
+ (
1158
+ "description",
1159
+ "[blue]Description [dim](optional)[/blue]",
1160
+ description,
1161
+ lambda x: x and sys.getsizeof(x) > 113,
1162
+ "[red]Error:[/red] Description must be <= 64 raw bytes.",
1163
+ ),
1164
+ (
1165
+ "additional",
1166
+ "[blue]Additional information [dim](optional)[/blue]",
1167
+ additional,
1168
+ lambda x: x and sys.getsizeof(x) > 113,
1169
+ "[red]Error:[/red] Additional information must be <= 64 raw bytes.",
1170
+ ),
1171
+ ]
1172
+
1173
+ for key, prompt, value, rejection_func, rejection_msg in fields:
1174
+ if value:
1175
+ if rejection_func(value):
1176
+ raise ValueError(rejection_msg)
1177
+ identity_fields[key] = value
1178
+ else:
1179
+ identity_fields[key] = retry_prompt(
1180
+ prompt,
1181
+ rejection=rejection_func,
1182
+ rejection_text=rejection_msg,
1183
+ default=None, # Maybe we can add some defaults later
1184
+ show_default=True,
1185
+ )
1186
+
1187
+ return identity_fields
1188
+
1189
+
1190
+ def is_valid_github_url(url: str) -> bool:
1191
+ """
1192
+ Validates if the provided URL is a valid GitHub repository URL.
1193
+
1194
+ Args:
1195
+ url (str): URL to validate
1196
+
1197
+ Returns:
1198
+ bool: True if valid GitHub repo URL, False otherwise
1199
+ """
989
1200
  try:
990
- getattr(wallet, unlocker)()
991
- return UnlockStatus(True, "")
992
- except PasswordError:
993
- err_msg = f"The password used to decrypt your {unlock_type.capitalize()}key Keyfile is invalid."
994
- if print_out:
995
- err_console.print(f":cross_mark: [red]{err_msg}[/red]")
996
- return UnlockStatus(False, err_msg)
997
- except KeyFileError:
998
- err_msg = f"{unlock_type.capitalize()}key Keyfile is corrupt, non-writable, or non-readable, or non-existent."
999
- if print_out:
1000
- err_console.print(f":cross_mark: [red]{err_msg}[/red]")
1001
- return UnlockStatus(False, err_msg)
1002
-
1003
-
1004
- def hex_to_bytes(hex_str: str) -> bytes:
1005
- """
1006
- Converts a hex-encoded string into bytes. Handles 0x-prefixed and non-prefixed hex-encoded strings.
1007
- """
1008
- if hex_str.startswith("0x"):
1009
- bytes_result = bytes.fromhex(hex_str[2:])
1010
- else:
1011
- bytes_result = bytes.fromhex(hex_str)
1012
- return bytes_result
1201
+ parsed = urlparse(url)
1202
+ if parsed.netloc != "github.com":
1203
+ return False
1204
+
1205
+ # Check path follows github.com/user/repo format
1206
+ path_parts = [p for p in parsed.path.split("/") if p]
1207
+ if len(path_parts) < 2: # Need at least username/repo
1208
+ return False
1209
+
1210
+ return True
1211
+ except Exception: # TODO figure out the exceptions that can be raised in here
1212
+ return False
1213
+
1214
+
1215
+ def is_valid_contact(contact: str) -> bool:
1216
+ """
1217
+ Validates if the provided contact is a valid email address.
1218
+
1219
+ Args:
1220
+ contact (str): Contact information to validate
1221
+
1222
+ Returns:
1223
+ bool: True if valid email, False otherwise
1224
+ """
1225
+ email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
1226
+ return bool(re.match(email_pattern, contact))
1227
+
1228
+
1229
+ def get_subnet_name(subnet_info) -> str:
1230
+ """Get the subnet name, prioritizing subnet_identity.subnet_name over subnet.subnet_name.
1231
+
1232
+ Args:
1233
+ subnet: The subnet dynamic info
1234
+
1235
+ Returns:
1236
+ str: The subnet name or empty string if no name is found
1237
+ """
1238
+ return (
1239
+ subnet_info.subnet_identity.subnet_name
1240
+ if hasattr(subnet_info, "subnet_identity")
1241
+ and subnet_info.subnet_identity is not None
1242
+ and subnet_info.subnet_identity.subnet_name is not None
1243
+ else (subnet_info.subnet_name if subnet_info.subnet_name is not None else "")
1244
+ )
1245
+
1246
+
1247
+ def print_linux_dependency_message():
1248
+ """Prints the WebKit dependency message for Linux systems."""
1249
+ console.print("[red]This command requires WebKit dependencies on Linux.[/red]")
1250
+ console.print(
1251
+ "\nPlease install the required packages using one of the following commands based on your distribution:"
1252
+ )
1253
+ console.print("\nArch Linux / Manjaro:")
1254
+ console.print("[green]sudo pacman -S webkit2gtk[/green]")
1255
+ console.print("\nDebian / Ubuntu:")
1256
+ console.print("[green]sudo apt install libwebkit2gtk-4.0-dev[/green]")
1257
+ console.print("\nFedora / CentOS / AlmaLinux:")
1258
+ console.print("[green]sudo dnf install gtk3-devel webkit2gtk3-devel[/green]")
1259
+
1260
+
1261
+ def is_linux():
1262
+ """Returns True if the operating system is Linux."""
1263
+ return platform.system().lower() == "linux"
@@ -0,0 +1,154 @@
1
+ from typing import Optional, TYPE_CHECKING
2
+
3
+ import rich.prompt
4
+ from rich.table import Table
5
+
6
+ from bittensor_cli.src.bittensor.chain_data import DelegateInfoLite
7
+ from bittensor_cli.src.bittensor.utils import console
8
+
9
+ if TYPE_CHECKING:
10
+ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
11
+
12
+
13
+ async def select_delegate(subtensor: "SubtensorInterface", netuid: int):
14
+ # Get a list of delegates and sort them by total stake in descending order
15
+ delegates: list[DelegateInfoLite] = (
16
+ await subtensor.get_delegates_by_netuid_light(netuid)
17
+ ).sort(key=lambda x: x.total_stake, reverse=True)
18
+
19
+ # Get registered delegates details.
20
+ registered_delegate_info = await subtensor.get_delegate_identities()
21
+
22
+ # Create a table to display delegate information
23
+ table = Table(
24
+ show_header=True,
25
+ header_style="bold",
26
+ border_style="rgb(7,54,66)",
27
+ style="rgb(0,43,54)",
28
+ )
29
+
30
+ # Add columns to the table with specific styles
31
+ table.add_column("Index", style="rgb(253,246,227)", no_wrap=True)
32
+ table.add_column("Delegate Name", no_wrap=True)
33
+ table.add_column("Hotkey SS58", style="rgb(211,54,130)", no_wrap=True)
34
+ table.add_column("Owner SS58", style="rgb(133,153,0)", no_wrap=True)
35
+ table.add_column("Take", style="rgb(181,137,0)", no_wrap=True)
36
+ table.add_column(
37
+ "Total Stake", style="rgb(38,139,210)", no_wrap=True, justify="right"
38
+ )
39
+ table.add_column(
40
+ "Owner Stake", style="rgb(220,50,47)", no_wrap=True, justify="right"
41
+ )
42
+ # table.add_column("Return per 1000", style="rgb(108,113,196)", no_wrap=True, justify="right")
43
+ # table.add_column("Total Daily Return", style="rgb(42,161,152)", no_wrap=True, justify="right")
44
+
45
+ # List to store visible delegates
46
+ visible_delegates = []
47
+
48
+ def get_user_input() -> str:
49
+ return rich.prompt.Prompt.ask(
50
+ 'Press Enter to scroll, enter a number (1-N) to select, or type "h" for help: ',
51
+ choices=["", "h"] + [str(x) for x in range(1, len(delegates) - 1)],
52
+ show_choices=True,
53
+ )
54
+
55
+ # TODO: Add pagination to handle large number of delegates more efficiently
56
+ # Iterate through delegates and display their information
57
+
58
+ def loop_selections() -> Optional[int]:
59
+ idx = 0
60
+ selected_idx = None
61
+ while idx < len(delegates):
62
+ if idx < len(delegates):
63
+ delegate = delegates[idx]
64
+
65
+ # Add delegate to visible list
66
+ visible_delegates.append(delegate)
67
+
68
+ # Add a row to the table with delegate information
69
+ table.add_row(
70
+ str(idx),
71
+ registered_delegate_info[delegate.hotkey_ss58].name
72
+ if delegate.hotkey_ss58 in registered_delegate_info
73
+ else "",
74
+ delegate.hotkey_ss58[:5]
75
+ + "..."
76
+ + delegate.hotkey_ss58[-5:], # Show truncated hotkey
77
+ delegate.owner_ss58[:5]
78
+ + "..."
79
+ + delegate.owner_ss58[-5:], # Show truncated owner address
80
+ f"{delegate.take:.6f}",
81
+ f"τ{delegate.total_stake.tao:,.4f}",
82
+ f"τ{delegate.owner_stake.tao:,.4f}",
83
+ # f"τ{delegate.return_per_1000.tao:,.4f}",
84
+ # f"τ{delegate.total_daily_return.tao:,.4f}",
85
+ )
86
+
87
+ # Clear console and print updated table
88
+ console.clear()
89
+ console.print(table)
90
+
91
+ # Prompt user for input
92
+ user_input: str = get_user_input()
93
+
94
+ # Add a help option to display information about each column
95
+ if user_input == "h":
96
+ console.print("\nColumn Information:")
97
+ console.print(
98
+ "[rgb(253,246,227)]Index:[/rgb(253,246,227)] Position in the list of delegates"
99
+ )
100
+ console.print(
101
+ "[rgb(211,54,130)]Hotkey SS58:[/rgb(211,54,130)] Truncated public key of the delegate's hotkey"
102
+ )
103
+ console.print(
104
+ "[rgb(133,153,0)]Owner SS58:[/rgb(133,153,0)] Truncated public key of the delegate's owner"
105
+ )
106
+ console.print(
107
+ "[rgb(181,137,0)]Take:[/rgb(181,137,0)] Percentage of rewards the delegate takes"
108
+ )
109
+ console.print(
110
+ "[rgb(38,139,210)]Total Stake:[/rgb(38,139,210)] Total amount staked to this delegate"
111
+ )
112
+ console.print(
113
+ "[rgb(220,50,47)]Owner Stake:[/rgb(220,50,47)] Amount staked by the delegate owner"
114
+ )
115
+ console.print(
116
+ "[rgb(108,113,196)]Return per 1000:[/rgb(108,113,196)] Estimated return for 1000 Tao staked"
117
+ )
118
+ console.print(
119
+ "[rgb(42,161,152)]Total Daily Return:[/rgb(42,161,152)] Estimated total daily return for all stake"
120
+ )
121
+ user_input = get_user_input()
122
+
123
+ # If user presses Enter, continue to next delegate
124
+ if user_input and user_input != "h":
125
+ selected_idx = int(user_input)
126
+ break
127
+
128
+ if idx < len(delegates):
129
+ idx += 1
130
+
131
+ return selected_idx
132
+
133
+ # TODO( const ): uncomment for check
134
+ # Add a confirmation step before returning the selected delegate
135
+ # console.print(f"\nSelected delegate: [rgb(211,54,130)]{visible_delegates[selected_idx].hotkey_ss58}[/rgb(211,54,130)]")
136
+ # console.print(f"Take: [rgb(181,137,0)]{visible_delegates[selected_idx].take:.6f}[/rgb(181,137,0)]")
137
+ # console.print(f"Total Stake: [rgb(38,139,210)]{visible_delegates[selected_idx].total_stake}[/rgb(38,139,210)]")
138
+
139
+ # confirmation = Prompt.ask("Do you want to proceed with this delegate? (y/n)")
140
+ # if confirmation.lower() != 'yes' and confirmation.lower() != 'y':
141
+ # return select_delegate( subtensor, netuid )
142
+
143
+ # Return the selected delegate
144
+ while True:
145
+ selected_idx_ = loop_selections()
146
+ if selected_idx_ is None:
147
+ if not rich.prompt.Confirm.ask(
148
+ "You've reached the end of the list. You must make a selection. Loop through again?"
149
+ ):
150
+ raise IndexError
151
+ else:
152
+ continue
153
+ else:
154
+ return delegates[selected_idx_]