bittensor-cli 8.4.4__py3-none-any.whl → 9.0.0__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 (33) hide show
  1. bittensor_cli/__init__.py +1 -1
  2. bittensor_cli/cli.py +1827 -1394
  3. bittensor_cli/src/__init__.py +623 -168
  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 +129 -23
  7. bittensor_cli/src/bittensor/extrinsics/root.py +3 -3
  8. bittensor_cli/src/bittensor/extrinsics/transfer.py +6 -11
  9. bittensor_cli/src/bittensor/minigraph.py +46 -8
  10. bittensor_cli/src/bittensor/subtensor_interface.py +567 -250
  11. bittensor_cli/src/bittensor/utils.py +370 -25
  12. bittensor_cli/src/commands/stake/__init__.py +154 -0
  13. bittensor_cli/src/commands/stake/add.py +625 -0
  14. bittensor_cli/src/commands/stake/children_hotkeys.py +103 -75
  15. bittensor_cli/src/commands/stake/list.py +687 -0
  16. bittensor_cli/src/commands/stake/move.py +1000 -0
  17. bittensor_cli/src/commands/stake/remove.py +1146 -0
  18. bittensor_cli/src/commands/subnets/__init__.py +0 -0
  19. bittensor_cli/src/commands/subnets/price.py +867 -0
  20. bittensor_cli/src/commands/subnets/subnets.py +2028 -0
  21. bittensor_cli/src/commands/sudo.py +554 -12
  22. bittensor_cli/src/commands/wallets.py +225 -531
  23. bittensor_cli/src/commands/weights.py +2 -2
  24. {bittensor_cli-8.4.4.dist-info → bittensor_cli-9.0.0.dist-info}/METADATA +7 -4
  25. bittensor_cli-9.0.0.dist-info/RECORD +34 -0
  26. bittensor_cli/src/bittensor/async_substrate_interface.py +0 -2748
  27. bittensor_cli/src/commands/root.py +0 -1787
  28. bittensor_cli/src/commands/stake/stake.py +0 -1448
  29. bittensor_cli/src/commands/subnets.py +0 -897
  30. bittensor_cli-8.4.4.dist-info/RECORD +0 -31
  31. {bittensor_cli-8.4.4.dist-info → bittensor_cli-9.0.0.dist-info}/WHEEL +0 -0
  32. {bittensor_cli-8.4.4.dist-info → bittensor_cli-9.0.0.dist-info}/entry_points.txt +0 -0
  33. {bittensor_cli-8.4.4.dist-info → bittensor_cli-9.0.0.dist-info}/top_level.txt +0 -0
@@ -3,10 +3,14 @@ from collections import namedtuple
3
3
  import math
4
4
  import os
5
5
  import sqlite3
6
+ import platform
6
7
  import webbrowser
8
+ import sys
7
9
  from pathlib import Path
8
10
  from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable
9
11
  from urllib.parse import urlparse
12
+ from functools import partial
13
+ import re
10
14
 
11
15
  from bittensor_wallet import Wallet, Keypair
12
16
  from bittensor_wallet.utils import SS58_FORMAT
@@ -17,20 +21,17 @@ from markupsafe import Markup
17
21
  import numpy as np
18
22
  from numpy.typing import NDArray
19
23
  from rich.console import Console
20
- import scalecodec
21
- from scalecodec.base import RuntimeConfiguration
22
- from scalecodec.type_registry import load_type_registry_preset
24
+ from rich.prompt import Prompt
25
+ from scalecodec.utils.ss58 import ss58_encode, ss58_decode
23
26
  import typer
24
27
 
25
28
 
26
29
  from bittensor_cli.src.bittensor.balances import Balance
30
+ from bittensor_cli.src import defaults, Constants
27
31
 
28
32
 
29
33
  if TYPE_CHECKING:
30
34
  from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters
31
- from bittensor_cli.src.bittensor.async_substrate_interface import (
32
- AsyncSubstrateInterface,
33
- )
34
35
 
35
36
  console = Console()
36
37
  err_console = Console(stderr=True)
@@ -39,6 +40,23 @@ verbose_console = Console(quiet=True)
39
40
  UnlockStatus = namedtuple("UnlockStatus", ["success", "message"])
40
41
 
41
42
 
43
+ class _Hotkey:
44
+ def __init__(self, hotkey_ss58=None):
45
+ self.ss58_address = hotkey_ss58
46
+
47
+
48
+ class WalletLike:
49
+ def __init__(self, name=None, hotkey_ss58=None, hotkey_str=None):
50
+ self.name = name
51
+ self.hotkey_ss58 = hotkey_ss58
52
+ self.hotkey_str = hotkey_str
53
+ self._hotkey = _Hotkey(hotkey_ss58)
54
+
55
+ @property
56
+ def hotkey(self):
57
+ return self._hotkey
58
+
59
+
42
60
  def print_console(message: str, colour: str, title: str, console: Console):
43
61
  console.print(
44
62
  f"[bold {colour}][{title}]:[/bold {colour}] [{colour}]{message}[/{colour}]\n"
@@ -197,13 +215,14 @@ def convert_root_weight_uids_and_vals_to_tensor(
197
215
 
198
216
 
199
217
  def get_hotkey_wallets_for_wallet(
200
- wallet: Wallet, show_nulls: bool = False
218
+ wallet: Wallet, show_nulls: bool = False, show_encrypted: bool = False
201
219
  ) -> list[Optional[Wallet]]:
202
220
  """
203
221
  Returns wallet objects with hotkeys for a single given wallet
204
222
 
205
223
  :param wallet: Wallet object to use for the path
206
224
  :param show_nulls: will add `None` into the output if a hotkey is encrypted or not on the device
225
+ :param show_encrypted: will add some basic info about the encrypted hotkey
207
226
 
208
227
  :return: a list of wallets (with Nones included for cases of a hotkey being encrypted or not on the device, if
209
228
  `show_nulls` is set to `True`)
@@ -219,12 +238,18 @@ def get_hotkey_wallets_for_wallet(
219
238
  hotkey_for_name = Wallet(path=str(wallet_path), name=wallet.name, hotkey=h_name)
220
239
  try:
221
240
  if (
222
- hotkey_for_name.hotkey_file.exists_on_device()
241
+ (exists := hotkey_for_name.hotkey_file.exists_on_device())
223
242
  and not hotkey_for_name.hotkey_file.is_encrypted()
224
243
  # and hotkey_for_name.coldkeypub.ss58_address
225
244
  and hotkey_for_name.hotkey.ss58_address
226
245
  ):
227
246
  hotkey_wallets.append(hotkey_for_name)
247
+ elif (
248
+ show_encrypted and exists and hotkey_for_name.hotkey_file.is_encrypted()
249
+ ):
250
+ hotkey_wallets.append(
251
+ WalletLike(str(wallet_path), "<ENCRYPTED>", h_name)
252
+ )
228
253
  elif show_nulls:
229
254
  hotkey_wallets.append(None)
230
255
  except (
@@ -376,21 +401,15 @@ def is_valid_bittensor_address_or_public_key(address: Union[str, bytes]) -> bool
376
401
  return False
377
402
 
378
403
 
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()
404
+ def decode_account_id(account_id_bytes: Union[tuple[int], tuple[tuple[int]]]):
405
+ if isinstance(account_id_bytes, tuple) and isinstance(account_id_bytes[0], tuple):
406
+ account_id_bytes = account_id_bytes[0]
407
+ # Convert the AccountId bytes to a Base64 string
408
+ return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT)
388
409
 
389
410
 
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)
411
+ def encode_account_id(ss58_address: str) -> bytes:
412
+ return bytes.fromhex(ss58_decode(ss58_address, SS58_FORMAT))
394
413
 
395
414
 
396
415
  def ss58_to_vec_u8(ss58_address: str) -> list[int]:
@@ -401,7 +420,7 @@ def ss58_to_vec_u8(ss58_address: str) -> list[int]:
401
420
 
402
421
  :return: A list of integers representing the byte values of the SS58 address.
403
422
  """
404
- ss58_bytes: bytes = ss58_address_to_bytes(ss58_address)
423
+ ss58_bytes: bytes = encode_account_id(ss58_address)
405
424
  encoded_address: list[int] = [int(byte) for byte in ss58_bytes]
406
425
  return encoded_address
407
426
 
@@ -507,7 +526,10 @@ def format_error_message(error_message: Union[dict, Exception]) -> str:
507
526
 
508
527
  # subtensor custom error marker
509
528
  if err_data.startswith("Custom error:"):
510
- err_description = f"{err_data} | Please consult https://docs.bittensor.com/subtensor-nodes/subtensor-error-messages"
529
+ err_description = (
530
+ f"{err_data} | Please consult "
531
+ f"https://docs.bittensor.com/subtensor-nodes/subtensor-error-messages"
532
+ )
511
533
  else:
512
534
  err_description = err_data
513
535
 
@@ -542,7 +564,8 @@ def decode_hex_identity_dict(info_dictionary) -> dict[str, Any]:
542
564
  """
543
565
  Decodes hex-encoded strings in a dictionary.
544
566
 
545
- This function traverses the given dictionary, identifies hex-encoded strings, and decodes them into readable strings. It handles nested dictionaries and lists within the dictionary.
567
+ This function traverses the given dictionary, identifies hex-encoded strings, and decodes them into readable
568
+ strings. It handles nested dictionaries and lists within the dictionary.
546
569
 
547
570
  Args:
548
571
  info_dictionary (dict): The dictionary containing hex-encoded strings to decode.
@@ -630,6 +653,32 @@ def millify(n: int):
630
653
  return "{:.2f}{}".format(n_ / 10 ** (3 * mill_idx), mill_names[mill_idx])
631
654
 
632
655
 
656
+ def millify_tao(n: float, start_at: str = "K") -> str:
657
+ """
658
+ Dupe of millify, but for ease in converting tao values.
659
+ Allows thresholds to be specified for different suffixes.
660
+ """
661
+ mill_names = ["", "k", "m", "b", "t"]
662
+ thresholds = {"K": 1, "M": 2, "B": 3, "T": 4}
663
+
664
+ if start_at not in thresholds:
665
+ raise ValueError(f"start_at must be one of {list(thresholds.keys())}")
666
+
667
+ n_ = float(n)
668
+ if n_ == 0:
669
+ return "0.00"
670
+
671
+ mill_idx = int(math.floor(math.log10(abs(n_)) / 3))
672
+
673
+ # Number's index is below our threshold, return with commas
674
+ if mill_idx < thresholds[start_at]:
675
+ return f"{n_:,.2f}"
676
+
677
+ mill_idx = max(thresholds[start_at], min(len(mill_names) - 1, mill_idx))
678
+
679
+ return "{:.2f}{}".format(n_ / 10 ** (3 * mill_idx), mill_names[mill_idx])
680
+
681
+
633
682
  def normalize_hyperparameters(
634
683
  subnet: "SubnetHyperparameters",
635
684
  ) -> list[tuple[str, str, str]]:
@@ -942,7 +991,7 @@ def retry_prompt(
942
991
  rejection_text: str,
943
992
  default="",
944
993
  show_default=False,
945
- prompt_type=typer.prompt,
994
+ prompt_type=Prompt.ask,
946
995
  ):
947
996
  """
948
997
  Allows for asking prompts again if they do not meet a certain criteria (as defined in `rejection`)
@@ -965,6 +1014,302 @@ def retry_prompt(
965
1014
  err_console.print(rejection_text)
966
1015
 
967
1016
 
1017
+ def validate_netuid(value: int) -> int:
1018
+ if value is not None and value < 0:
1019
+ raise typer.BadParameter("Negative netuid passed. Please use correct netuid.")
1020
+ return value
1021
+
1022
+
1023
+ def validate_uri(uri: str) -> str:
1024
+ if not uri:
1025
+ return None
1026
+ clean_uri = uri.lstrip("/").lower()
1027
+ if not clean_uri.isalnum():
1028
+ raise typer.BadParameter(
1029
+ f"Invalid URI format: {uri}. URI must contain only alphanumeric characters (e.g. 'alice', 'bob')"
1030
+ )
1031
+ return f"//{clean_uri.capitalize()}"
1032
+
1033
+
1034
+ def get_effective_network(config, network: Optional[list[str]]) -> str:
1035
+ """
1036
+ Determines the effective network to be used, considering the network parameter,
1037
+ the configuration, and the default.
1038
+ """
1039
+ if network:
1040
+ for item in network:
1041
+ if item.startswith("ws"):
1042
+ network_ = item
1043
+ break
1044
+ else:
1045
+ network_ = item
1046
+ return network_
1047
+ elif config.get("network"):
1048
+ return config["network"]
1049
+ else:
1050
+ return defaults.subtensor.network
1051
+
1052
+
1053
+ def is_rao_network(network: str) -> bool:
1054
+ """Check if the given network is 'rao'."""
1055
+ network = network.lower()
1056
+ rao_identifiers = [
1057
+ "rao",
1058
+ Constants.rao_entrypoint,
1059
+ ]
1060
+ return (
1061
+ network == "rao"
1062
+ or network in rao_identifiers
1063
+ or "rao.chain.opentensor.ai" in network
1064
+ )
1065
+
1066
+
1067
+ def prompt_for_identity(
1068
+ current_identity: dict,
1069
+ name: Optional[str],
1070
+ web_url: Optional[str],
1071
+ image_url: Optional[str],
1072
+ discord: Optional[str],
1073
+ description: Optional[str],
1074
+ additional: Optional[str],
1075
+ github_repo: Optional[str],
1076
+ ):
1077
+ """
1078
+ Prompts the user for identity fields with validation.
1079
+ Returns a dictionary with the updated fields.
1080
+ """
1081
+ identity_fields = {}
1082
+
1083
+ fields = [
1084
+ ("name", "[blue]Display name[/blue]", name),
1085
+ ("url", "[blue]Web URL[/blue]", web_url),
1086
+ ("image", "[blue]Image URL[/blue]", image_url),
1087
+ ("discord", "[blue]Discord handle[/blue]", discord),
1088
+ ("description", "[blue]Description[/blue]", description),
1089
+ ("additional", "[blue]Additional information[/blue]", additional),
1090
+ ("github_repo", "[blue]GitHub repository URL[/blue]", github_repo),
1091
+ ]
1092
+
1093
+ text_rejection = partial(
1094
+ retry_prompt,
1095
+ rejection=lambda x: sys.getsizeof(x) > 113,
1096
+ rejection_text="[red]Error:[/red] Identity field must be <= 64 raw bytes.",
1097
+ )
1098
+
1099
+ if not any(
1100
+ [
1101
+ name,
1102
+ web_url,
1103
+ image_url,
1104
+ discord,
1105
+ description,
1106
+ additional,
1107
+ github_repo,
1108
+ ]
1109
+ ):
1110
+ console.print(
1111
+ "\n[yellow]All fields are optional. Press Enter to skip and keep the default/existing value.[/yellow]\n"
1112
+ "[dark_sea_green3]Tip: Entering a space and pressing Enter will clear existing default value.\n"
1113
+ )
1114
+
1115
+ for key, prompt, value in fields:
1116
+ if value:
1117
+ identity_fields[key] = value
1118
+ else:
1119
+ identity_fields[key] = text_rejection(
1120
+ prompt,
1121
+ default=current_identity.get(key, ""),
1122
+ show_default=True,
1123
+ )
1124
+
1125
+ return identity_fields
1126
+
1127
+
1128
+ def prompt_for_subnet_identity(
1129
+ subnet_name: Optional[str],
1130
+ github_repo: Optional[str],
1131
+ subnet_contact: Optional[str],
1132
+ subnet_url: Optional[str],
1133
+ discord: Optional[str],
1134
+ description: Optional[str],
1135
+ additional: Optional[str],
1136
+ ):
1137
+ """
1138
+ Prompts the user for required subnet identity fields with validation.
1139
+ Returns a dictionary with the updated fields.
1140
+
1141
+ Args:
1142
+ subnet_name (Optional[str]): Name of the subnet
1143
+ github_repo (Optional[str]): GitHub repository URL
1144
+ subnet_contact (Optional[str]): Contact information for subnet (email)
1145
+
1146
+ Returns:
1147
+ dict: Dictionary containing the subnet identity fields
1148
+ """
1149
+ identity_fields = {}
1150
+
1151
+ fields = [
1152
+ (
1153
+ "subnet_name",
1154
+ "[blue]Subnet name [dim](optional)[/blue]",
1155
+ subnet_name,
1156
+ lambda x: x and sys.getsizeof(x) > 113,
1157
+ "[red]Error:[/red] Subnet name must be <= 64 raw bytes.",
1158
+ ),
1159
+ (
1160
+ "github_repo",
1161
+ "[blue]GitHub repository URL [dim](optional)[/blue]",
1162
+ github_repo,
1163
+ lambda x: x and not is_valid_github_url(x),
1164
+ "[red]Error:[/red] Please enter a valid GitHub repository URL (e.g., https://github.com/username/repo).",
1165
+ ),
1166
+ (
1167
+ "subnet_contact",
1168
+ "[blue]Contact email [dim](optional)[/blue]",
1169
+ subnet_contact,
1170
+ lambda x: x and not is_valid_contact(x),
1171
+ "[red]Error:[/red] Please enter a valid email address.",
1172
+ ),
1173
+ (
1174
+ "subnet_url",
1175
+ "[blue]Subnet URL [dim](optional)[/blue]",
1176
+ subnet_url,
1177
+ lambda x: x and sys.getsizeof(x) > 113,
1178
+ "[red]Error:[/red] Please enter a valid URL.",
1179
+ ),
1180
+ (
1181
+ "discord",
1182
+ "[blue]Discord handle [dim](optional)[/blue]",
1183
+ discord,
1184
+ lambda x: x and sys.getsizeof(x) > 113,
1185
+ "[red]Error:[/red] Please enter a valid Discord handle.",
1186
+ ),
1187
+ (
1188
+ "description",
1189
+ "[blue]Description [dim](optional)[/blue]",
1190
+ description,
1191
+ lambda x: x and sys.getsizeof(x) > 113,
1192
+ "[red]Error:[/red] Description must be <= 64 raw bytes.",
1193
+ ),
1194
+ (
1195
+ "additional",
1196
+ "[blue]Additional information [dim](optional)[/blue]",
1197
+ additional,
1198
+ lambda x: x and sys.getsizeof(x) > 113,
1199
+ "[red]Error:[/red] Additional information must be <= 64 raw bytes.",
1200
+ ),
1201
+ ]
1202
+
1203
+ for key, prompt, value, rejection_func, rejection_msg in fields:
1204
+ if value:
1205
+ if rejection_func(value):
1206
+ raise ValueError(rejection_msg)
1207
+ identity_fields[key] = value
1208
+ else:
1209
+ identity_fields[key] = retry_prompt(
1210
+ prompt,
1211
+ rejection=rejection_func,
1212
+ rejection_text=rejection_msg,
1213
+ default=None, # Maybe we can add some defaults later
1214
+ show_default=True,
1215
+ )
1216
+
1217
+ return identity_fields
1218
+
1219
+
1220
+ def is_valid_github_url(url: str) -> bool:
1221
+ """
1222
+ Validates if the provided URL is a valid GitHub repository URL.
1223
+
1224
+ Args:
1225
+ url (str): URL to validate
1226
+
1227
+ Returns:
1228
+ bool: True if valid GitHub repo URL, False otherwise
1229
+ """
1230
+ try:
1231
+ parsed = urlparse(url)
1232
+ if parsed.netloc != "github.com":
1233
+ return False
1234
+
1235
+ # Check path follows github.com/user/repo format
1236
+ path_parts = [p for p in parsed.path.split("/") if p]
1237
+ if len(path_parts) < 2: # Need at least username/repo
1238
+ return False
1239
+
1240
+ return True
1241
+ except Exception: # TODO figure out the exceptions that can be raised in here
1242
+ return False
1243
+
1244
+
1245
+ def is_valid_contact(contact: str) -> bool:
1246
+ """
1247
+ Validates if the provided contact is a valid email address.
1248
+
1249
+ Args:
1250
+ contact (str): Contact information to validate
1251
+
1252
+ Returns:
1253
+ bool: True if valid email, False otherwise
1254
+ """
1255
+ email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
1256
+ return bool(re.match(email_pattern, contact))
1257
+
1258
+
1259
+ def get_subnet_name(subnet_info) -> str:
1260
+ """Get the subnet name, prioritizing subnet_identity.subnet_name over subnet.subnet_name.
1261
+
1262
+ Args:
1263
+ subnet: The subnet dynamic info
1264
+
1265
+ Returns:
1266
+ str: The subnet name or empty string if no name is found
1267
+ """
1268
+ return (
1269
+ subnet_info.subnet_identity.subnet_name
1270
+ if hasattr(subnet_info, "subnet_identity")
1271
+ and subnet_info.subnet_identity is not None
1272
+ and subnet_info.subnet_identity.subnet_name is not None
1273
+ else (subnet_info.subnet_name if subnet_info.subnet_name is not None else "")
1274
+ )
1275
+
1276
+
1277
+ def print_linux_dependency_message():
1278
+ """Prints the WebKit dependency message for Linux systems."""
1279
+ console.print("[red]This command requires WebKit dependencies on Linux.[/red]")
1280
+ console.print(
1281
+ "\nPlease install the required packages using one of the following commands based on your distribution:"
1282
+ )
1283
+ console.print("\nArch Linux / Manjaro:")
1284
+ console.print("[green]sudo pacman -S webkit2gtk[/green]")
1285
+ console.print("\nDebian / Ubuntu:")
1286
+ console.print("[green]sudo apt install libwebkit2gtk-4.0-dev[/green]")
1287
+ console.print("\nFedora / CentOS / AlmaLinux:")
1288
+ console.print("[green]sudo dnf install gtk3-devel webkit2gtk3-devel[/green]")
1289
+
1290
+
1291
+ def is_linux():
1292
+ """Returns True if the operating system is Linux."""
1293
+ return platform.system().lower() == "linux"
1294
+
1295
+
1296
+ def validate_rate_tolerance(value: Optional[float]) -> Optional[float]:
1297
+ """Validates rate tolerance input"""
1298
+ if value is not None:
1299
+ if value < 0:
1300
+ raise typer.BadParameter(
1301
+ "Rate tolerance cannot be negative (less than 0%)."
1302
+ )
1303
+ if value > 1:
1304
+ raise typer.BadParameter("Rate tolerance cannot be greater than 1 (100%).")
1305
+ if value > 0.5:
1306
+ console.print(
1307
+ f"[yellow]Warning: High rate tolerance of {value*100}% specified. "
1308
+ "This may result in unfavorable transaction execution.[/yellow]"
1309
+ )
1310
+ return value
1311
+
1312
+
968
1313
  def unlock_key(
969
1314
  wallet: Wallet, unlock_type="cold", print_out: bool = True
970
1315
  ) -> "UnlockStatus":
@@ -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_]