Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the command btcli root list_delegates_lite to handle the Delegate… #1840

Merged
merged 18 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
61268e9
Add the command btcli root list_delegates_lite to handle the Delegate…
roman-opentensor May 4, 2024
29efad2
Add the command btcli root list_delegates_lite to handle the Delegate…
roman-opentensor May 4, 2024
992e242
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 4, 2024
c733c64
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 4, 2024
4c8257e
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 4, 2024
cf85dab
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 4, 2024
69a8e97
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 4, 2024
15cd287
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 4, 2024
e290c68
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 4, 2024
2c5fd19
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 4, 2024
d568147
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 4, 2024
6fcc113
Add the command btcli root list_delegates_lite to handle the Delegate…
roman-opentensor May 4, 2024
19f1965
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 4, 2024
cde9941
Add the command btcli root list_delegates_lite to handle the Delegate…
roman-opentensor May 4, 2024
41b54d9
Add the command btcli root list_delegates_lite to handle the Delegate…
roman-opentensor May 4, 2024
17d7d03
Merge branch 'refs/heads/staging' into feature/roman/get-delegates-lite
roman-opentensor May 7, 2024
1aec229
Merge branch 'refs/heads/staging' into feature/roman/get-delegates-lite
roman-opentensor May 7, 2024
deb11df
Merge remote-tracking branch 'origin/feature/roman/get-delegates-lite…
roman-opentensor May 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bittensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ def debug(on: bool = True):
NeuronInfoLite,
PrometheusInfo,
DelegateInfo,
DelegateInfoLite,
StakeInfo,
SubnetInfo,
SubnetHyperparameters,
Expand Down
18 changes: 17 additions & 1 deletion bittensor/chain_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,22 @@ def fix_decoded_values(cls, prometheus_info_decoded: Dict) -> "PrometheusInfo":
return cls(**prometheus_info_decoded)


@dataclass
class DelegateInfoLite:
"""Dataclass for DelegateLiteInfo."""

delegate_ss58: str # Hotkey of delegate
take: float # Take of the delegate as a percentage
nominators: int # List of nominators of the delegate and their stake
owner_ss58: str # Coldkey of owner
registrations: list[int] # List of subnets that the delegate is registered on
validator_permits: list[
int
] # List of subnets that the delegate is allowed to validate on
return_per_1000: int # Return per 1000 tao of the delegate over a day
total_daily_return: int # Total daily return of the delegate


@dataclass
class DelegateInfo:
r"""
Expand Down Expand Up @@ -829,7 +845,7 @@ def list_of_tuple_from_vec_u8(
) -> Dict[str, List["StakeInfo"]]:
r"""Returns a list of StakeInfo objects from a ``vec_u8``."""
decoded: Optional[
List[Tuple(str, List[object])]
list[tuple[str, list[object]]]
] = from_scale_encoding_using_type_string(
input=vec_u8, type_string="Vec<(AccountId, Vec<StakeInfo>)>"
)
Expand Down
4 changes: 3 additions & 1 deletion bittensor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
InspectCommand,
ListCommand,
ListDelegatesCommand,
ListDelegatesLiteCommand,
MetagraphCommand,
MyDelegatesCommand,
NewColdkeyCommand,
Expand Down Expand Up @@ -125,6 +126,7 @@
"undelegate": DelegateUnstakeCommand,
"my_delegates": MyDelegatesCommand,
"list_delegates": ListDelegatesCommand,
"list_delegates_lite": ListDelegatesLiteCommand,
"nominate": NominateCommand,
},
},
Expand Down Expand Up @@ -329,7 +331,7 @@ def check_config(config: "bittensor.config"):
command_data = COMMANDS[command]

if isinstance(command_data, dict):
if config["subcommand"] != None:
if config["subcommand"] is not None:
command_data["commands"][config["subcommand"]].check_config(config)
else:
console.print(
Expand Down
1 change: 1 addition & 0 deletions bittensor/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
from .delegates import (
NominateCommand,
ListDelegatesCommand,
ListDelegatesLiteCommand,
DelegateStakeCommand,
DelegateUnstakeCommand,
MyDelegatesCommand,
Expand Down
178 changes: 177 additions & 1 deletion bittensor/commands/delegates.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,79 @@ def _get_coldkey_wallets_for_path(path: str) -> List["bittensor.wallet"]:
console = bittensor.__console__


def show_delegates_lite(
delegates_lite: List["bittensor.DelegateInfoLite"], width: Optional[int] = None
):
"""Outputs a list of lite version delegates to the console."""

registered_delegate_info: Optional[
Dict[str, DelegatesDetails]
] = get_delegates_details(url=bittensor.__delegates_details_url__)
if registered_delegate_info is None:
bittensor.__console__.print(
":warning:[yellow]Could not get delegate info from chain.[/yellow]"
)
registered_delegate_info = {}

table = Table(show_footer=True, width=width, pad_edge=False, box=None, expand=True)
table.add_column(
"[overline white]INDEX",
str(len(delegates_lite)),
footer_style="overline white",
style="bold white",
)
table.add_column(
"[overline white]DELEGATE",
style="rgb(50,163,219)",
no_wrap=True,
justify="left",
)
table.add_column(
"[overline white]SS58",
str(len(delegates_lite)),
footer_style="overline white",
style="bold yellow",
)
table.add_column(
"[overline white]NOMINATORS", justify="center", style="green", no_wrap=True
)
table.add_column("[overline white]VPERMIT", justify="right", no_wrap=False)
table.add_column("[overline white]TAKE", style="white", no_wrap=True)
table.add_column("[overline white]DELEGATE/(24h)", style="green", justify="center")
table.add_column("[overline white]Desc", style="rgb(50,163,219)")

for i, d in enumerate(delegates_lite):
if d.delegate_ss58 in registered_delegate_info:
delegate_name = registered_delegate_info[d.delegate_ss58].name
delegate_url = registered_delegate_info[d.delegate_ss58].url
delegate_description = registered_delegate_info[d.delegate_ss58].description
else:
delegate_name = ""
delegate_url = ""
delegate_description = ""

table.add_row(
# `INDEX` column
str(i),
# `DELEGATE` column
Text(delegate_name, style=f"link {delegate_url}"),
# `SS58` column
f"{d.delegate_ss58:8.8}...",
# `NOMINATORS` column
str(d.nominators),
# `VPERMIT` column
str(d.registrations),
# `TAKE` column
f"{d.take * 100:.1f}%",
# `DELEGATE/(24h)` column
f"τ{bittensor.Balance.from_tao(d.total_daily_return * 0.18) !s:6.6}",
# `Desc` column
str(delegate_description),
end_section=True,
)
bittensor.__console__.print(table)


# Uses rich console to pretty print a table of delegates.
def show_delegates(
delegates: List["bittensor.DelegateInfo"],
Expand Down Expand Up @@ -198,17 +271,29 @@ def show_delegates(
rate_change_in_stake_str = "[grey0]NA[/grey0]"

table.add_row(
# INDEX
str(i),
# DELEGATE
Text(delegate_name, style=f"link {delegate_url}"),
# SS58
f"{delegate.hotkey_ss58:8.8}...",
# NOMINATORS
str(len([nom for nom in delegate.nominators if nom[1].rao > 0])),
# DELEGATE STAKE
f"{owner_stake!s:13.13}",
# TOTAL STAKE
f"{delegate.total_stake!s:13.13}",
# CHANGE/(4h)
rate_change_in_stake_str,
# VPERMIT
str(delegate.registrations),
# TAKE
f"{delegate.take * 100:.1f}%",
# NOMINATOR/(24h)/k
f"{bittensor.Balance.from_tao( delegate.total_daily_return.tao * (1000/ (0.001 + delegate.total_stake.tao)))!s:6.6}",
# DELEGATE/(24h)
f"{bittensor.Balance.from_tao(delegate.total_daily_return.tao * 0.18) !s:6.6}",
# Desc
str(delegate_description),
end_section=True,
)
Expand Down Expand Up @@ -490,6 +575,87 @@ def check_config(config: "bittensor.config"):
config.unstake_all = True


class ListDelegatesLiteCommand:
"""
Displays a formatted table of Bittensor network delegates, providing a comprehensive overview of delegate statistics
and information.

This table helps users make informed decisions on which delegates to allocate their Tao stake.

Optional Arguments:
- ``wallet.name``: The name of the wallet to use for the command.
- ``subtensor.network``: The name of the network to use for the command.

The table columns include:

- INDEX: The delegate's index in the sorted list.
- DELEGATE: The name of the delegate.
- SS58: The delegate's unique SS58 address (truncated for display).
- NOMINATORS: The count of nominators backing the delegate.
- DELEGATE STAKE(τ): The amount of delegate's own stake (not the TAO delegated from any nominators).
- TOTAL STAKE(τ): The delegate's cumulative stake, including self-staked and nominators' stakes.
- CHANGE/(4h): The percentage change in the delegate's stake over the last four hours.
- SUBNETS: The subnets to which the delegate is registered.
- VPERMIT: Indicates the subnets for which the delegate has validator permits.
- NOMINATOR/(24h)/kτ: The earnings per 1000 τ staked by nominators in the last 24 hours.
- DELEGATE/(24h): The total earnings of the delegate in the last 24 hours.
- DESCRIPTION: A brief description of the delegate's purpose and operations.

Sorting is done based on the ``TOTAL STAKE`` column in descending order. Changes in stake are highlighted:
increases in green and decreases in red. Entries with no previous data are marked with ``NA``. Each delegate's name
is a hyperlink to their respective URL, if available.

Example usage::

btcli root list_delegates
btcli root list_delegates --wallet.name my_wallet
btcli root list_delegates --subtensor.network finney # can also be `test` or `local`

Note:
This function is part of the Bittensor CLI tools and is intended for use within a console application. It prints
directly to the console and does not return any value.
"""

@staticmethod
def run(cli: "bittensor.cli"):
r"""
List all delegates on the network.
"""
try:
subtensor: "bittensor.subtensor" = bittensor.subtensor(
config=cli.config, log_verbose=False
)
ListDelegatesLiteCommand._run(cli, subtensor)
finally:
if "subtensor" in locals():
subtensor.close()
bittensor.logging.debug("closing subtensor connection")

@staticmethod
def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"):
r"""
List all delegates on the network.
"""
cli.config.subtensor.network = "archive"
cli.config.subtensor.chain_endpoint = "wss://archive.chain.opentensor.ai:443"
with bittensor.__console__.status(":satellite: Loading delegates..."):
delegates: list[bittensor.DelegateInfoLite] = subtensor.get_delegates_lite()

show_delegates_lite(delegates, width=cli.config.get("width", None))

@staticmethod
def add_args(parser: argparse.ArgumentParser):
list_delegates_parser = parser.add_parser(
"list_delegates_lite",
help="""List all delegates on the network (lite version).""",
)
bittensor.subtensor.add_args(list_delegates_parser)

@staticmethod
def check_config(config: "bittensor.config"):
pass


class ListDelegatesCommand:
"""
Displays a formatted table of Bittensor network delegates, providing a comprehensive overview of delegate statistics
Expand Down Expand Up @@ -556,9 +722,19 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"):
with bittensor.__console__.status(":satellite: Loading delegates..."):
delegates: list[bittensor.DelegateInfo] = subtensor.get_delegates()

try:
prev_delegates = subtensor.get_delegates(max(0, subtensor.block - 1200))
except SubstrateRequestException:
prev_delegates = None

if prev_delegates is None:
bittensor.__console__.print(
":warning: [yellow]Could not fetch delegates history[/yellow]"
)

show_delegates(
delegates,
prev_delegates=None,
prev_delegates=prev_delegates,
width=cli.config.get("width", None),
)

Expand Down
41 changes: 38 additions & 3 deletions bittensor/subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from .chain_data import (
NeuronInfo,
DelegateInfo,
DelegateInfoLite,
PrometheusInfo,
SubnetInfo,
SubnetHyperparameters,
Expand Down Expand Up @@ -3524,6 +3525,40 @@ def make_substrate_call_with_retry(encoded_hotkey: List[int]):

return DelegateInfo.from_vec_u8(result)

def get_delegates_lite(self, block: Optional[int] = None) -> List[DelegateInfoLite]:
"""
Retrieves a list of all delegate neurons within the Bittensor network. This function provides an
overview of the neurons that are actively involved in the network's delegation system. Lite version.

Args:
block (Optional[int], optional): The blockchain block number for the query.

Returns:
List[DelegateInfoLite]: A list of DelegateInfoLite objects detailing each delegate's characteristics.

Analyzing the delegate population offers insights into the network's governance dynamics and the
distribution of trust and responsibility among participating neurons.
"""

@retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logger)
def make_substrate_call_with_retry():
block_hash = None if block is None else self.substrate.get_block_hash(block)
params = []
if block_hash:
params.extend([block_hash])
return self.substrate.rpc_request(
method="delegateInfo_getDelegatesLite", # custom rpc method
params=params,
)

json_body = make_substrate_call_with_retry()
result = json_body["result"]

if result in (None, []):
return []

return [DelegateInfoLite(**d) for d in result]

def get_delegates(self, block: Optional[int] = None) -> List[DelegateInfo]:
"""
Retrieves a list of all delegate neurons within the Bittensor network. This function provides an
Expand All @@ -3539,12 +3574,12 @@ def get_delegates(self, block: Optional[int] = None) -> List[DelegateInfo]:
distribution of trust and responsibility among participating neurons.
"""

@retry(delay=2, tries=3, backoff=2, max_delay=4, logger=logger)
@retry(delay=1, tries=3, backoff=2, max_delay=4, logger=logger)
def make_substrate_call_with_retry():
block_hash = None if block == None else self.substrate.get_block_hash(block)
block_hash = None if block is None else self.substrate.get_block_hash(block)
params = []
if block_hash:
params = params + [block_hash]
params.extend([block_hash])
return self.substrate.rpc_request(
method="delegateInfo_getDelegates", # custom rpc method
params=params,
Expand Down
6 changes: 6 additions & 0 deletions tests/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

import os
from .helpers import (
_get_mock_coldkey,
_get_mock_hotkey,
Expand All @@ -24,3 +25,8 @@
MockConsole,
__mock_wallet_factory__,
)


def is_running_in_circleci():
"""Checks that tests are running in the app.circleci.com environment."""
return os.getenv("CIRCLECI") == "true"
Loading