diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2044bae7..30b0c665 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -42,7 +42,10 @@ from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters -from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.subtensor_interface import ( + SubtensorInterface, + best_connection, +) from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -619,7 +622,7 @@ class CLIManager: wallet_app: typer.Typer subnets_app: typer.Typer weights_app: typer.Typer - utils_app = typer.Typer(epilog=_epilog) + utils_app: typer.Typer view_app: typer.Typer asyncio_runner = asyncio @@ -692,6 +695,7 @@ def __init__(self): self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) self.liquidity_app = typer.Typer(epilog=_epilog) + self.utils_app = typer.Typer(epilog=_epilog) # config alias self.app.add_typer( @@ -766,7 +770,7 @@ def __init__(self): # utils app self.app.add_typer( - self.utils_app, name="utils", no_args_is_help=True, hidden=True + self.utils_app, name="utils", no_args_is_help=True, hidden=False ) # view app @@ -1048,6 +1052,10 @@ def __init__(self): "remove", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] )(self.liquidity_remove) + # utils app + self.utils_app.command("convert")(self.convert) + self.utils_app.command("latency")(self.best_connection) + def generate_command_tree(self) -> Tree: """ Generates a rich.Tree of the commands, subcommands, and groups of this app @@ -6296,7 +6304,6 @@ def liquidity_modify( ) @staticmethod - @utils_app.command("convert") def convert( from_rao: Optional[str] = typer.Option( None, "--rao", help="Convert amount from Rao" @@ -6326,6 +6333,66 @@ def convert( f"{Balance.from_tao(tao).rao}{Balance.rao_unit}", ) + def best_connection( + self, + additional_networks: Optional[list[str]] = typer.Option( + None, + "--network", + help="Network(s) to test for the best connection", + ), + ): + """ + This command will give you the latency of all finney-like network in additional to any additional networks you specify via the '--network' flag + + The results are three-fold. One column is the overall time to initialise a connection, send the requests, and wait for the results. The second column measures single ping-pong speed once connected. The third makes a real world call to fetch the chain head. + + EXAMPLE + + [green]$[/green] btcli utils latency --network ws://189.234.12.45 --network wss://mysubtensor.duckdns.org + + """ + additional_networks = additional_networks or [] + if any(not x.startswith("ws") for x in additional_networks): + err_console.print( + "Invalid network endpoint. Ensure you are specifying a valid websocket endpoint" + f" (starting with [{COLORS.G.LINKS}]ws://[/{COLORS.G.LINKS}] or " + f"[{COLORS.G.LINKS}]wss://[/{COLORS.G.LINKS}]).", + ) + return False + results: dict[str, list[float]] = self._run_command( + best_connection(Constants.lite_nodes + additional_networks) + ) + sorted_results = { + k: v for k, v in sorted(results.items(), key=lambda item: item[1][0]) + } + table = Table( + Column("Network"), + Column("End to End Latency", style="cyan"), + Column("Single Request Ping", style="cyan"), + Column("Chain Head Request Latency", style="cyan"), + title="Connection Latencies (seconds)", + caption="lower value is faster", + ) + for n_name, ( + overall_latency, + single_request, + chain_head, + ) in sorted_results.items(): + table.add_row( + n_name, str(overall_latency), str(single_request), str(chain_head) + ) + console.print(table) + fastest = next(iter(sorted_results.keys())) + if conf_net := self.config.get("network", ""): + if not conf_net.startswith("ws") and conf_net in Constants.networks: + conf_net = Constants.network_map[conf_net] + if conf_net != fastest: + console.print( + f"The fastest network is {fastest}. You currently have {conf_net} selected as your default network." + f"\nYou can update this with {arg__(f'btcli config set --network {fastest}')}" + ) + return True + def run(self): self.app() diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index afabc424..67d3b0ec 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -23,6 +23,7 @@ class Constants: dev_entrypoint = "wss://dev.chain.opentensor.ai:443" local_entrypoint = "ws://127.0.0.1:9944" latent_lite_entrypoint = "wss://lite.sub.latent.to:443" + lite_nodes = [finney_entrypoint, subvortex_entrypoint, latent_lite_entrypoint] network_map = { "finney": finney_entrypoint, "test": finney_test_entrypoint, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index ffe8d78e..cb3b295f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,21 +1,22 @@ import asyncio import os +import time from typing import Optional, Any, Union, TypedDict, Iterable import aiohttp +from async_substrate_interface.async_substrate import ( + DiskCachedAsyncSubstrateInterface, + AsyncSubstrateInterface, +) +from async_substrate_interface.errors import SubstrateRequestException from async_substrate_interface.utils.storage import StorageKey from bittensor_wallet import Wallet from bittensor_wallet.bittensor_wallet import Keypair from bittensor_wallet.utils import SS58_FORMAT from scalecodec import GenericCall -from async_substrate_interface.errors import SubstrateRequestException import typer +import websockets - -from async_substrate_interface.async_substrate import ( - DiskCachedAsyncSubstrateInterface, - AsyncSubstrateInterface, -) from bittensor_cli.src.bittensor.chain_data import ( DelegateInfo, StakeInfo, @@ -1654,3 +1655,32 @@ async def get_subnet_prices( map_[netuid_] = Balance.from_rao(int(current_price * 1e9)) return map_ + + +async def best_connection(networks: list[str]): + """ + Basic function to compare the latency of a given list of websocket endpoints + Args: + networks: list of network URIs + + Returns: + {network_name: [end_to_end_latency, single_request_latency, chain_head_request_latency]} + + """ + results = {} + for network in networks: + try: + t1 = time.monotonic() + async with websockets.connect(network) as websocket: + pong = await websocket.ping() + latency = await pong + pt1 = time.monotonic() + await websocket.send( + "{'jsonrpc': '2.0', 'method': 'chain_getHead', 'params': [], 'id': '82'}" + ) + await websocket.recv() + t2 = time.monotonic() + results[network] = [t2 - t1, latency, t2 - pt1] + except Exception as e: + err_console.print(f"Error attempting network {network}: {e}") + return results