bittensor-cli 8.4.3__py3-none-any.whl → 9.0.0rc2__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.
- bittensor_cli/__init__.py +2 -2
- bittensor_cli/cli.py +1508 -1385
- bittensor_cli/src/__init__.py +627 -197
- bittensor_cli/src/bittensor/balances.py +41 -8
- bittensor_cli/src/bittensor/chain_data.py +557 -428
- bittensor_cli/src/bittensor/extrinsics/registration.py +161 -47
- bittensor_cli/src/bittensor/extrinsics/root.py +14 -8
- bittensor_cli/src/bittensor/extrinsics/transfer.py +14 -21
- bittensor_cli/src/bittensor/minigraph.py +46 -8
- bittensor_cli/src/bittensor/subtensor_interface.py +572 -253
- bittensor_cli/src/bittensor/utils.py +326 -75
- bittensor_cli/src/commands/stake/__init__.py +154 -0
- bittensor_cli/src/commands/stake/children_hotkeys.py +121 -87
- bittensor_cli/src/commands/stake/move.py +1000 -0
- bittensor_cli/src/commands/stake/stake.py +1637 -1264
- bittensor_cli/src/commands/subnets/__init__.py +0 -0
- bittensor_cli/src/commands/subnets/price.py +867 -0
- bittensor_cli/src/commands/subnets/subnets.py +2055 -0
- bittensor_cli/src/commands/sudo.py +529 -26
- bittensor_cli/src/commands/wallets.py +234 -544
- bittensor_cli/src/commands/weights.py +15 -11
- {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0rc2.dist-info}/METADATA +7 -4
- bittensor_cli-9.0.0rc2.dist-info/RECORD +32 -0
- bittensor_cli/src/bittensor/async_substrate_interface.py +0 -2748
- bittensor_cli/src/commands/root.py +0 -1752
- bittensor_cli/src/commands/subnets.py +0 -897
- bittensor_cli-8.4.3.dist-info/RECORD +0 -31
- {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0rc2.dist-info}/WHEEL +0 -0
- {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0rc2.dist-info}/entry_points.txt +0 -0
- {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0rc2.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
|
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
|
21
|
-
from scalecodec.
|
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
|
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
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
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
|
391
|
-
|
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 =
|
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(
|
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
|
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=
|
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
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
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
|
982
|
-
|
983
|
-
|
984
|
-
|
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
|
-
|
987
|
-
|
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
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
return
|
1002
|
-
|
1003
|
-
|
1004
|
-
def
|
1005
|
-
"""
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
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_]
|