From cb18fba35cac8e9765933612d35672e678b3c6ae Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 6 Jun 2025 18:17:56 +0200 Subject: [PATCH 01/48] Better checks the swap status --- bittensor_cli/src/commands/wallets.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 40ea61218..ed2d71052 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2002,9 +2002,9 @@ async def check_swap_status( origin_ss58: The SS58 address of the original coldkey block_number: Optional block number where the swap was scheduled """ - scheduled_swaps = await subtensor.get_scheduled_coldkey_swap() if not origin_ss58: + scheduled_swaps = await subtensor.get_scheduled_coldkey_swap() if not scheduled_swaps: console.print("[yellow]No pending coldkey swaps found.[/yellow]") return @@ -2036,8 +2036,16 @@ async def check_swap_status( "\n[dim]Tip: Check specific swap details by providing the original coldkey SS58 address and the block number.[/dim]" ) return - - is_pending = origin_ss58 in scheduled_swaps + chain_reported_completion_block, destination_address = await subtensor.query( + "SubtensorModule", "ColdkeySwapScheduled", [origin_ss58] + ) + if ( + chain_reported_completion_block != 0 + and destination_address != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + ): + is_pending = True + else: + is_pending = False if not is_pending: console.print( @@ -2050,7 +2058,7 @@ async def check_swap_status( ) if expected_block_number is None: - return + expected_block_number = chain_reported_completion_block swap_info = await find_coldkey_swap_extrinsic( subtensor=subtensor, From aed5a9cd11c4006b014b872589d573c6f7e39f71 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 11 Jun 2025 23:09:25 +0200 Subject: [PATCH 02/48] Ensures network local is used if forgotten in e2e tests --- tests/e2e_tests/utils.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 07f2fbd93..2917ab59f 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -31,6 +31,19 @@ def exec_command( inputs: list[str] = None, ): extra_args = extra_args or [] + # Ensure if we forget to add `--network ws://127.0.0.1:9945` that it will run still using the local chain + if not any( + ( + x in extra_args + for x in ( + "--network", + "--chain", + "--subtensor.network", + "--subtensor.chain_endpoint", + ) + ) + ): + extra_args.extend(["--network", "ws://127.0.0.1:9945"]) cli_manager = CLIManager() # Capture stderr separately from stdout runner = CliRunner(mix_stderr=False) From df661ba6f6cc7cae69e0ef3cbb9d337836ba5855 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 12 Jun 2025 17:29:49 +0200 Subject: [PATCH 03/48] Ensures only adds network local to commands which utilise that arg. --- tests/e2e_tests/utils.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 2917ab59f..7a3c0993f 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -1,3 +1,4 @@ +import inspect import os import re import shutil @@ -31,20 +32,28 @@ def exec_command( inputs: list[str] = None, ): extra_args = extra_args or [] - # Ensure if we forget to add `--network ws://127.0.0.1:9945` that it will run still using the local chain - if not any( - ( - x in extra_args - for x in ( - "--network", - "--chain", - "--subtensor.network", - "--subtensor.chain_endpoint", - ) - ) - ): - extra_args.extend(["--network", "ws://127.0.0.1:9945"]) cli_manager = CLIManager() + for group in cli_manager.app.registered_groups: + if group.name == command: + for command_ in group.typer_instance.registered_commands: + if command_.name == sub_command: + if "network" in inspect.getcallargs( + command_.callback + ).keys() and not any( + ( + x in extra_args + for x in ( + "--network", + "--chain", + "--subtensor.network", + "--subtensor.chain_endpoint", + ) + ) + ): + # Ensure if we forget to add `--network ws://127.0.0.1:9945` that it will run still + # using the local chain + extra_args.extend(["--network", "ws://127.0.0.1:9945"]) + # Capture stderr separately from stdout runner = CliRunner(mix_stderr=False) # Prepare the command arguments From 1216a2bac86ad9ecfdce895c10c26b61e5a884bb Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 08:04:36 -0700 Subject: [PATCH 04/48] adds netuid support in swap-hotkey --- bittensor_cli/cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 60d8f4926..ecb936e7d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1897,6 +1897,8 @@ def wallet_swap_hotkey( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, + netuid: Optional[int] = Options.netuid_not_req, + all_netuids: bool = Options.all_netuids, network: Optional[list[str]] = Options.network, destination_hotkey_name: Optional[str] = typer.Argument( None, help="Destination hotkey name." @@ -1917,12 +1919,14 @@ def wallet_swap_hotkey( - Make sure that your original key pair (coldkeyA, hotkeyA) is already registered. - Make sure that you use a newly created hotkeyB in this command. A hotkeyB that is already registered cannot be used in this command. + - You can specify the netuid for which you want to swap the hotkey for. If it is not defined, the swap will be initiated for all subnets. - Finally, note that this command requires a fee of 1 TAO for recycling and this fee is taken from your wallet (coldkeyA). EXAMPLE - [green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey + [green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey --netuid 1 """ + netuid = get_optional_netuid(netuid, all_netuids) self.verbosity_handler(quiet, verbose, json_output) original_wallet = self.wallet_ask( wallet_name, @@ -1946,7 +1950,7 @@ def wallet_swap_hotkey( self.initialize_chain(network) return self._run_command( wallets.swap_hotkey( - original_wallet, new_wallet, self.subtensor, prompt, json_output + original_wallet, new_wallet, self.subtensor, netuid, prompt, json_output ) ) From 0f68766fed66f54a0f758c54284ce4a4d1988e61 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 08:05:24 -0700 Subject: [PATCH 05/48] updates swap extrinsic --- .../src/bittensor/extrinsics/registration.py | 69 +++++++++++++++---- bittensor_cli/src/commands/wallets.py | 3 + 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index c230f1134..1e3328964 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -1611,7 +1611,8 @@ def _update_curr_block( """ Update the current block data with the provided block information and difficulty. - This function updates the current block and its difficulty in a thread-safe manner. It sets the current block + This function updates the current block + and its difficulty in a thread-safe manner. It sets the current block number, hashes the block with the hotkey, updates the current block bytes, and packs the difficulty. :param curr_diff: Shared array to store the current difficulty. @@ -1745,6 +1746,7 @@ async def swap_hotkey_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, new_wallet: Wallet, + netuid: Optional[int] = None, prompt: bool = False, ) -> bool: """ @@ -1756,37 +1758,76 @@ async def swap_hotkey_extrinsic( netuids_registered = await subtensor.get_netuids_for_hotkey( wallet.hotkey.ss58_address, block_hash=block_hash ) - if not len(netuids_registered) > 0: + netuids_registered_new_hotkey = await subtensor.get_netuids_for_hotkey( + new_wallet.hotkey.ss58_address, block_hash=block_hash + ) + + if netuid is not None and netuid not in netuids_registered: + err_console.print( + f":cross_mark: [red]Failed[/red]: Original hotkey {wallet.hotkey.ss58_address} is not registered on subnet {netuid}" + ) + return False + + elif not len(netuids_registered) > 0: err_console.print( - f"Destination hotkey [dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] is not registered. " + f"Original hotkey [dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] is not registered on any subnet. " f"Please register and try again" ) return False + if netuid is not None: + if netuid in netuids_registered_new_hotkey: + err_console.print( + f":cross_mark: [red]Failed[/red]: New hotkey {new_wallet.hotkey.ss58_address} " + f"is already registered on subnet {netuid}" + ) + return False + else: + if len(netuids_registered_new_hotkey) > 0: + err_console.print( + f":cross_mark: [red]Failed[/red]: New hotkey {new_wallet.hotkey.ss58_address} " + f"is already registered on subnet(s) {netuids_registered_new_hotkey}" + ) + return False + if not unlock_key(wallet).success: return False if prompt: # Prompt user for confirmation. - if not Confirm.ask( - f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t" - f"[dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] with hotkey \n\t" - f"[dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange]\n" - "This operation will cost [bold cyan]1 TAO t (recycled)[/bold cyan]" - ): + if netuid is not None: + confirm_message = ( + f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t" + f"[dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] with hotkey \n\t" + f"[dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] on subnet {netuid}\n" + "This operation will cost [bold cyan]1 TAO (recycled)[/bold cyan]" + ) + else: + confirm_message = ( + f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t" + f"[dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] with hotkey \n\t" + f"[dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] on all subnets\n" + "This operation will cost [bold cyan]1 TAO (recycled)[/bold cyan]" + ) + + if not Confirm.ask(confirm_message): return False print_verbose( f"Swapping {wallet.name}'s hotkey ({wallet.hotkey.ss58_address}) with " - f"{new_wallet.name}s hotkey ({new_wallet.hotkey.ss58_address})" + f"{new_wallet.name}'s hotkey ({new_wallet.hotkey.ss58_address})" ) with console.status(":satellite: Swapping hotkeys...", spinner="aesthetic"): + call_params = { + "hotkey": wallet.hotkey.ss58_address, + "new_hotkey": new_wallet.hotkey.ss58_address, + } + if netuid is not None: + call_params["netuid"] = netuid + call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="swap_hotkey", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "new_hotkey": new_wallet.hotkey.ss58_address, - }, + call_params=call_params, ) success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 40ea61218..252fa6aab 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1632,6 +1632,7 @@ async def swap_hotkey( original_wallet: Wallet, new_wallet: Wallet, subtensor: SubtensorInterface, + netuid: Optional[int], prompt: bool, json_output: bool, ): @@ -1640,7 +1641,9 @@ async def swap_hotkey( subtensor, original_wallet, new_wallet, + netuid=netuid, prompt=prompt, + ) if json_output: json_console.print(json.dumps({"success": result})) From 0fc391cc0fe0f48f7098b19799e7ea55780bd7e6 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 08:06:47 -0700 Subject: [PATCH 06/48] ruff --- bittensor_cli/src/commands/wallets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 252fa6aab..c1c4f4261 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1643,7 +1643,6 @@ async def swap_hotkey( new_wallet, netuid=netuid, prompt=prompt, - ) if json_output: json_console.print(json.dumps({"success": result})) From 3d3657e53b13863696bf07b147c320abb536ee0e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 12:24:39 -0700 Subject: [PATCH 07/48] logo_url params --- bittensor_cli/cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ecb936e7d..18cf5db6d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5025,6 +5025,9 @@ def subnets_create( description: Optional[str] = typer.Option( None, "--description", help="Description" ), + logo_url: Optional[str] = typer.Option( + None, "--logo-url", help="Logo URL" + ), additional_info: Optional[str] = typer.Option( None, "--additional-info", help="Additional information" ), @@ -5067,6 +5070,7 @@ def subnets_create( subnet_url=subnet_url, discord=discord, description=description, + logo_url=logo_url, additional=additional_info, ) self._run_command( @@ -5190,6 +5194,9 @@ def subnets_set_identity( description: Optional[str] = typer.Option( None, "--description", help="Description" ), + logo_url: Optional[str] = typer.Option( + None, "--logo-url", help="Logo URL" + ), additional_info: Optional[str] = typer.Option( None, "--additional-info", help="Additional information" ), @@ -5241,6 +5248,7 @@ def subnets_set_identity( subnet_url=subnet_url, discord=discord, description=description, + logo_url=logo_url, additional=additional_info, ) From 1fa0491afe60f294eb7a737f5328f3b3d7a41119 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 12:28:27 -0700 Subject: [PATCH 08/48] adds logo_url support --- bittensor_cli/src/bittensor/chain_data.py | 2 ++ bittensor_cli/src/bittensor/utils.py | 8 ++++++++ bittensor_cli/src/commands/subnets/subnets.py | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 1f9401ce4..5749ada6f 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -630,6 +630,7 @@ class SubnetIdentity(InfoBase): subnet_url: str discord: str description: str + logo_url: str additional: str @classmethod @@ -641,6 +642,7 @@ def _fix_decoded(cls, decoded: dict) -> "SubnetIdentity": subnet_url=bytes(decoded["subnet_url"]).decode(), discord=bytes(decoded["discord"]).decode(), description=bytes(decoded["description"]).decode(), + logo_url=bytes(decoded["logo_url"]).decode(), additional=bytes(decoded["additional"]).decode(), ) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 31ca5ea61..d7e8da805 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1167,6 +1167,7 @@ def prompt_for_subnet_identity( subnet_url: Optional[str], discord: Optional[str], description: Optional[str], + logo_url: Optional[str], additional: Optional[str], ): """ @@ -1227,6 +1228,13 @@ def prompt_for_subnet_identity( lambda x: x and len(x.encode("utf-8")) > 1024, "[red]Error:[/red] Description must be <= 1024 bytes.", ), + ( + "logo_url", + "[blue]Logo URL [dim](optional)[/blue]", + logo_url, + lambda x: x and len(x.encode("utf-8")) > 1024, + "[red]Error:[/red] Logo URL must be <= 1024 bytes.", + ), ( "additional", "[blue]Additional information [dim](optional)[/blue]", diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 99a619d8e..e94402953 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -140,6 +140,9 @@ async def _find_event_attributes_in_extrinsic_receipt( "description": subnet_identity["description"].encode() if subnet_identity.get("description") else b"", + "logo_url": subnet_identity["logo_url"].encode() + if subnet_identity.get("logo_url") + else b"", "additional": subnet_identity["additional"].encode() if subnet_identity.get("additional") else b"", @@ -2207,6 +2210,7 @@ async def set_identity( "subnet_url": subnet_identity.get("subnet_url", ""), "discord": subnet_identity.get("discord", ""), "description": subnet_identity.get("description", ""), + "logo_url": subnet_identity.get("logo_url", ""), "additional": subnet_identity.get("additional", ""), } @@ -2252,6 +2256,7 @@ async def set_identity( "subnet_url", "discord", "description", + "logo_url", "additional", ]: value = getattr(identity, key, None) @@ -2301,6 +2306,7 @@ async def get_identity( "subnet_url", "discord", "description", + "logo_url", "additional", ]: value = getattr(identity, key, None) From 7dcf37bdd0e37bba9e5a9df2435ede15ce64a6d1 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 12:28:56 -0700 Subject: [PATCH 09/48] updates tests --- tests/e2e_tests/test_staking_sudo.py | 13 ++++++++++--- tests/e2e_tests/test_unstaking.py | 4 ++++ tests/e2e_tests/test_wallet_interactions.py | 6 ++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 0a0984488..5315788ac 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -86,6 +86,8 @@ def test_staking(local_chain, wallet_setup): "A test subnet for e2e testing", "--additional-info", "Created by Alice", + "--logo-url", + "https://testsubnet.com/logo.png", "--no-prompt", "--json-output", ], @@ -121,6 +123,8 @@ def test_staking(local_chain, wallet_setup): "A test subnet for e2e testing", "--additional-info", "Created by Alice", + "--logo-url", + "https://testsubnet.com/logo.png", "--no-prompt", "--json-output", ], @@ -198,6 +202,8 @@ def test_staking(local_chain, wallet_setup): sn_discord := "alice#1234", "--description", sn_description := "A test subnet for e2e testing", + "--logo-url", + sn_logo_url := "https://testsubnet.com/logo.png", "--additional-info", sn_add_info := "Created by Alice", "--json-output", @@ -225,6 +231,7 @@ def test_staking(local_chain, wallet_setup): assert get_identity_output["subnet_url"] == sn_url assert get_identity_output["discord"] == sn_discord assert get_identity_output["description"] == sn_description + assert get_identity_output["logo_url"] == sn_logo_url assert get_identity_output["additional"] == sn_add_info # Start emissions on SNs @@ -508,9 +515,9 @@ def test_staking(local_chain, wallet_setup): ], ) change_yuma3_hyperparam_json = json.loads(change_yuma3_hyperparam.stdout) - assert change_yuma3_hyperparam_json["success"] is True, ( - change_yuma3_hyperparam.stdout - ) + assert ( + change_yuma3_hyperparam_json["success"] is True + ), change_yuma3_hyperparam.stdout changed_yuma3_hyperparam = exec_command_alice( command="sudo", diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index bfbb77e85..e28c54df6 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -83,6 +83,8 @@ def test_unstaking(local_chain, wallet_setup): "A test subnet for e2e testing", "--additional-info", "Test subnet", + "--logo-url", + "https://testsubnet.com/logo.png", "--no-prompt", ], ) @@ -115,6 +117,8 @@ def test_unstaking(local_chain, wallet_setup): "A test subnet for e2e testing", "--additional-info", "Test subnet", + "--logo-url", + "https://testsubnet.com/logo.png", "--no-prompt", ], ) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 08b2c73c4..cdd22c775 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -69,6 +69,8 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): "test#1234", "--description", "A test subnet for e2e testing", + "--logo-url", + "https://testsubnet.com/logo.png", "--additional-info", "Test subnet", "--no-prompt", @@ -388,6 +390,8 @@ def test_wallet_identities(local_chain, wallet_setup): "A test subnet for e2e testing", "--additional-info", "Created by Alice", + "--logo-url", + "https://testsubnet.com/logo.png", "--no-prompt", ], ) @@ -427,6 +431,8 @@ def test_wallet_identities(local_chain, wallet_setup): alice_identity["discord"], "--description", alice_identity["description"], + # "--logo-url", + # alice_identity["logo_url"], "--additional", alice_identity["additional"], "--github", From 4077b98e2a2760fa1a49edd20b2a0f3a2b6842ca Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 12:34:49 -0700 Subject: [PATCH 10/48] cleanup --- tests/e2e_tests/test_wallet_interactions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index cdd22c775..e6a4bb22d 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -431,8 +431,6 @@ def test_wallet_identities(local_chain, wallet_setup): alice_identity["discord"], "--description", alice_identity["description"], - # "--logo-url", - # alice_identity["logo_url"], "--additional", alice_identity["additional"], "--github", From 0611641cbbe79bce424f8d4cef47edbcccbe165a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 12:36:23 -0700 Subject: [PATCH 11/48] ruff --- bittensor_cli/cli.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 18cf5db6d..c92ae2a18 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5025,9 +5025,7 @@ def subnets_create( description: Optional[str] = typer.Option( None, "--description", help="Description" ), - logo_url: Optional[str] = typer.Option( - None, "--logo-url", help="Logo URL" - ), + logo_url: Optional[str] = typer.Option(None, "--logo-url", help="Logo URL"), additional_info: Optional[str] = typer.Option( None, "--additional-info", help="Additional information" ), @@ -5194,9 +5192,7 @@ def subnets_set_identity( description: Optional[str] = typer.Option( None, "--description", help="Description" ), - logo_url: Optional[str] = typer.Option( - None, "--logo-url", help="Logo URL" - ), + logo_url: Optional[str] = typer.Option(None, "--logo-url", help="Logo URL"), additional_info: Optional[str] = typer.Option( None, "--additional-info", help="Additional information" ), From 35b2d44e0171306add7b363f4760aab1f795a581 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 12:40:20 -0700 Subject: [PATCH 12/48] ruff (again) --- tests/e2e_tests/test_staking_sudo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 5315788ac..c880fee4b 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -515,9 +515,9 @@ def test_staking(local_chain, wallet_setup): ], ) change_yuma3_hyperparam_json = json.loads(change_yuma3_hyperparam.stdout) - assert ( - change_yuma3_hyperparam_json["success"] is True - ), change_yuma3_hyperparam.stdout + assert change_yuma3_hyperparam_json["success"] is True, ( + change_yuma3_hyperparam.stdout + ) changed_yuma3_hyperparam = exec_command_alice( command="sudo", From d7a70146c018ffa2ad1e9643dea584fec0acd518 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 12:57:54 -0700 Subject: [PATCH 13/48] update creation test --- tests/e2e_tests/test_wallet_creations.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index 7b573dbba..019cad6b5 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -587,7 +587,13 @@ def test_wallet_balance_all(local_chain, wallet_setup, capfd): result = exec_command( command="wallet", sub_command="balance", - extra_args=["--wallet-path", wallet_path, "--all"], + extra_args=[ + "--wallet-path", + wallet_path, + "--all", + "--chain", + "ws://127.0.0.1:9945", + ], ) output = result.stdout @@ -600,7 +606,14 @@ def test_wallet_balance_all(local_chain, wallet_setup, capfd): json_results = exec_command( "wallet", "balance", - extra_args=["--wallet-path", wallet_path, "--all", "--json-output"], + extra_args=[ + "--wallet-path", + wallet_path, + "--all", + "--json-output", + "--chain", + "ws://127.0.0.1:9945", + ], ) json_results_output = json.loads(json_results.stdout) for wallet_name in wallet_names: From 06153d74bf52c29b9853ed4c57362863729b5422 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 14:35:16 -0700 Subject: [PATCH 14/48] bumps version and changelog --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6413ce04f..4f646dc19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 9.7.0/2025-06-16 + +## What's Changed +* Add `SKIP_PULL` variable for conftest.py by @basfroman in https://github.com/opentensor/btcli/pull/502 +* Feat: Adds netuid support in swap_hotkeys by @ibraheem-abe in https://github.com/opentensor/btcli/pull/505 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.6.0...v9.7.0 + ## 9.6.0/2025-06-12 ## What's Changed diff --git a/pyproject.toml b/pyproject.toml index e680c1e57..fdafdd92f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.6.0" +version = "9.7.0" description = "Bittensor CLI" readme = "README.md" authors = [ From 07884c48e44713f275b0cd9fab6f85685fd3f7d4 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 08:05:24 -0700 Subject: [PATCH 15/48] updates swap extrinsic --- bittensor_cli/src/commands/wallets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index c1c4f4261..252fa6aab 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1643,6 +1643,7 @@ async def swap_hotkey( new_wallet, netuid=netuid, prompt=prompt, + ) if json_output: json_console.print(json.dumps({"success": result})) From f6a642479efa269c45bd58bf1069f7053125b91d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 16 Jun 2025 15:02:50 -0700 Subject: [PATCH 16/48] pip --- bittensor_cli/src/commands/wallets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 252fa6aab..c1c4f4261 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1643,7 +1643,6 @@ async def swap_hotkey( new_wallet, netuid=netuid, prompt=prompt, - ) if json_output: json_console.print(json.dumps({"success": result})) From 608a7914022356e195f475104f5647b98458ce9b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 23 Jun 2025 18:50:29 +0200 Subject: [PATCH 17/48] Our inputs are always strings --- bittensor_cli/src/bittensor/utils.py | 10 ++++++++++ bittensor_cli/src/commands/sudo.py | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index d7e8da805..3a53c946b 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -122,6 +122,11 @@ def u64_normalized_float(x: int) -> float: return float(x) / float(U64_MAX) +def string_to_u64(value: str) -> int: + """Converts a string to u64""" + return float_to_u64(float(value)) + + def float_to_u64(value: float) -> int: """Converts a float to a u64 int""" # Ensure the input is within the expected range @@ -142,6 +147,11 @@ def u64_to_float(value: int) -> float: return min(value / u64_max, 1.0) # Ensure the result is never greater than 1.0 +def string_to_u16(value: str) -> int: + """Converts a string to a u16 int""" + return float_to_u16(float(value)) + + def float_to_u16(value: float) -> int: # Ensure the input is within the expected range if not (0 <= value <= 1): diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 0f919659b..f1f116b5e 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -18,9 +18,9 @@ normalize_hyperparameters, unlock_key, blocks_to_duration, - float_to_u64, - float_to_u16, json_console, + string_to_u16, + string_to_u64, ) if TYPE_CHECKING: @@ -108,7 +108,7 @@ def type_converter_with_retry(type_, val, arg_name): except ValueError: return type_converter_with_retry(type_, None, arg_name) - arg_types = {"bool": string_to_bool, "u16": float_to_u16, "u64": float_to_u64} + arg_types = {"bool": string_to_bool, "u16": string_to_u16, "u64": string_to_u64} arg_type_output = {"bool": "bool", "u16": "float", "u64": "float"} call_crafter = {"netuid": netuid} From 7e24c04c581fa0c1c071090a94b8452dd5bb8b12 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 23 Jun 2025 19:03:28 +0200 Subject: [PATCH 18/48] Added e2e test --- tests/e2e_tests/test_staking_sudo.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index c880fee4b..e56f0e32a 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -540,3 +540,31 @@ def test_staking(local_chain, wallet_setup): assert yuma3_val["value"] is True assert yuma3_val["normalized_value"] is True print("✅ Passed staking and sudo commands") + + change_arbitrary_hyperparam = exec_command_alice( + command="sudo", + sub_command="set", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--param", + "sudo_set_bonds_penalty", # arbitrary hyperparam + "--value", + "0", # int/float value + "--no-prompt", + "--json-output", + ], + ) + change_arbitrary_hyperparam_json = json.loads(change_arbitrary_hyperparam.stdout) + assert change_arbitrary_hyperparam_json["success"] is True, ( + change_arbitrary_hyperparam.stdout, + change_arbitrary_hyperparam.stderr, + ) From 5ed8e84cc7b03db8837effc865dd5425f5933d3e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 24 Jun 2025 16:35:24 +0200 Subject: [PATCH 19/48] Ensure that non_netuid_fields are strings, instead of `'>` --- bittensor_cli/src/commands/sudo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index f1f116b5e..de02993bb 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -234,9 +234,9 @@ async def set_hyperparameter_extrinsic( if isinstance(value, list): # Ensure that there are enough values for all non-netuid parameters non_netuid_fields = [ - param["name"] + pn_str for param in extrinsic_params["fields"] - if "netuid" not in param["name"] + if "netuid" not in (pn_str := str(param["name"])) ] if len(value) < len(non_netuid_fields): @@ -246,7 +246,7 @@ async def set_hyperparameter_extrinsic( return False call_params.update( - {str(name): val for name, val in zip(non_netuid_fields, value)} + {name: val for name, val in zip(non_netuid_fields, value)} ) else: From 79e4b34a66c8384b92f962b40d8fb58e783e0a52 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Jul 2025 20:18:22 +0200 Subject: [PATCH 20/48] Adds snake_case aliases for all commands and options. --- bittensor_cli/cli.py | 45 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c92ae2a18..35352f222 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -11,7 +11,7 @@ import traceback import warnings from pathlib import Path -from typing import Coroutine, Optional, Union +from typing import Coroutine, Optional from dataclasses import fields import rich @@ -169,15 +169,29 @@ def edit_help(cls, option_name: str, help_text: str): help="Path to a JSON file containing the encrypted key backup. For example, a JSON file from PolkadotJS.", ) json_password = typer.Option( - None, "--json-password", help="Password to decrypt the JSON file." + None, + "--json-password", + "--json_password", + help="Password to decrypt the JSON file.", ) use_password = typer.Option( True, + "--use-password", + "--use_password", help="Set this to `True` to protect the generated Bittensor key with a password.", ) - public_hex_key = typer.Option(None, help="The public key in hex format.") + public_hex_key = typer.Option( + None, + "--public-hex-key", + "--public_hex_key", + help="The public key in hex format.", + ) ss58_address = typer.Option( - None, "--ss58", "--ss58-address", help="The SS58 address of the coldkey." + None, + "--ss58", + "--ss58-address", + "--ss58_address", + help="The SS58 address of the coldkey.", ) overwrite = typer.Option( False, @@ -208,11 +222,14 @@ def edit_help(cls, option_name: str, help_text: str): ) netuid_not_req = typer.Option( None, + "--netuid", help="The netuid of the subnet in the network, (e.g. 1).", prompt=False, ) all_netuids = typer.Option( False, + "--all-netuids/--not-all-netuids", + "--all_netuids/--not_all_netuids", help="Use all netuids", prompt=False, ) @@ -225,6 +242,7 @@ def edit_help(cls, option_name: str, help_text: str): reuse_last = typer.Option( False, "--reuse-last", + "--reuse_last", help="Reuse the metagraph data you last retrieved." "Use this option only if you have already retrieved the metagraph." "data", @@ -235,10 +253,15 @@ def edit_help(cls, option_name: str, help_text: str): help="Display the table as HTML in the browser.", ) wait_for_inclusion = typer.Option( - True, help="If `True`, waits until the transaction is included in a block." + True, + "--wait-for-inclusion", + "--wait_for_inclusion", + help="If `True`, waits until the transaction is included in a block.", ) wait_for_finalization = typer.Option( True, + "--wait-for-finalization", + "--wait_for_finalization", help="If `True`, waits until the transaction is finalized on the blockchain.", ) prompt = typer.Option( @@ -276,6 +299,7 @@ def edit_help(cls, option_name: str, help_text: str): "--slippage-tolerance", "--tolerance", "--rate-tolerance", + "--rate_tolerance", help="Set the rate tolerance percentage for transactions (default: 0.05 for 5%).", callback=validate_rate_tolerance, ) @@ -283,6 +307,7 @@ def edit_help(cls, option_name: str, help_text: str): None, "--safe-staking/--no-safe-staking", "--safe/--unsafe", + "--safe_staking/--no_safe_staking", show_default=False, help="Enable or disable safe staking mode [dim](default: enabled)[/dim].", ) @@ -292,6 +317,7 @@ def edit_help(cls, option_name: str, help_text: str): "--partial/--no-partial", "--allow/--not-allow", "--allow-partial/--not-partial", + "--allow_partial_stake/--no_allow_partial_stake", show_default=False, help="Enable or disable partial stake mode [dim](default: disabled)[/dim].", ) @@ -308,6 +334,7 @@ def edit_help(cls, option_name: str, help_text: str): False, "--json-output", "--json-out", + "--json_output", help="Outputs the result of the command as JSON.", ) period: int = typer.Option( @@ -929,11 +956,13 @@ def __init__(self): )(self.view_dashboard) # Sub command aliases - # Weights + # Wallet self.wallet_app.command( "swap_hotkey", hidden=True, )(self.wallet_swap_hotkey) + self.wallet_app.command("swap_coldkey", hidden=True)(self.wallet_swap_coldkey) + self.wallet_app.command("swap_check", hidden=True)(self.wallet_check_ck_swap) self.wallet_app.command( "regen_coldkey", hidden=True, @@ -962,10 +991,14 @@ def __init__(self): "get_identity", hidden=True, )(self.wallet_get_id) + self.wallet_app.command("associate_hotkey")(self.wallet_associate_hotkey) # Subnets self.subnets_app.command("burn_cost", hidden=True)(self.subnets_burn_cost) self.subnets_app.command("pow_register", hidden=True)(self.subnets_pow_register) + self.subnets_app.command("set_identity", hidden=True)(self.subnets_set_identity) + self.subnets_app.command("get_identity", hidden=True)(self.subnets_get_identity) + self.subnets_app.command("check_start", hidden=True)(self.subnets_check_start) # Sudo self.sudo_app.command("senate_vote", hidden=True)(self.sudo_senate_vote) From 302a331023cb9ba8678dba9b4f621e4e80fca1e9 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Jul 2025 20:19:57 +0200 Subject: [PATCH 21/48] Updates help --- bittensor_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 35352f222..9e994691c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5123,7 +5123,7 @@ def subnets_check_start( This command verifies if a subnet's emission schedule can be started based on the subnet's registration block. Example: - [green]$[/green] btcli subnets check_start --netuid 1 + [green]$[/green] btcli subnets check-start --netuid 1 """ self.verbosity_handler(quiet, verbose) return self._run_command( From 82748489091be365561ad2745de24ee23a60a44a Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Jul 2025 20:25:51 +0200 Subject: [PATCH 22/48] Revert aliases in Options. --- bittensor_cli/cli.py | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9e994691c..08201fa47 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -169,29 +169,15 @@ def edit_help(cls, option_name: str, help_text: str): help="Path to a JSON file containing the encrypted key backup. For example, a JSON file from PolkadotJS.", ) json_password = typer.Option( - None, - "--json-password", - "--json_password", - help="Password to decrypt the JSON file.", + None, "--json-password", help="Password to decrypt the JSON file." ) use_password = typer.Option( True, - "--use-password", - "--use_password", help="Set this to `True` to protect the generated Bittensor key with a password.", ) - public_hex_key = typer.Option( - None, - "--public-hex-key", - "--public_hex_key", - help="The public key in hex format.", - ) + public_hex_key = typer.Option(None, help="The public key in hex format.") ss58_address = typer.Option( - None, - "--ss58", - "--ss58-address", - "--ss58_address", - help="The SS58 address of the coldkey.", + None, "--ss58", "--ss58-address", help="The SS58 address of the coldkey." ) overwrite = typer.Option( False, @@ -222,14 +208,11 @@ def edit_help(cls, option_name: str, help_text: str): ) netuid_not_req = typer.Option( None, - "--netuid", help="The netuid of the subnet in the network, (e.g. 1).", prompt=False, ) all_netuids = typer.Option( False, - "--all-netuids/--not-all-netuids", - "--all_netuids/--not_all_netuids", help="Use all netuids", prompt=False, ) @@ -242,7 +225,6 @@ def edit_help(cls, option_name: str, help_text: str): reuse_last = typer.Option( False, "--reuse-last", - "--reuse_last", help="Reuse the metagraph data you last retrieved." "Use this option only if you have already retrieved the metagraph." "data", @@ -253,15 +235,10 @@ def edit_help(cls, option_name: str, help_text: str): help="Display the table as HTML in the browser.", ) wait_for_inclusion = typer.Option( - True, - "--wait-for-inclusion", - "--wait_for_inclusion", - help="If `True`, waits until the transaction is included in a block.", + True, help="If `True`, waits until the transaction is included in a block." ) wait_for_finalization = typer.Option( True, - "--wait-for-finalization", - "--wait_for_finalization", help="If `True`, waits until the transaction is finalized on the blockchain.", ) prompt = typer.Option( @@ -299,7 +276,6 @@ def edit_help(cls, option_name: str, help_text: str): "--slippage-tolerance", "--tolerance", "--rate-tolerance", - "--rate_tolerance", help="Set the rate tolerance percentage for transactions (default: 0.05 for 5%).", callback=validate_rate_tolerance, ) @@ -307,7 +283,6 @@ def edit_help(cls, option_name: str, help_text: str): None, "--safe-staking/--no-safe-staking", "--safe/--unsafe", - "--safe_staking/--no_safe_staking", show_default=False, help="Enable or disable safe staking mode [dim](default: enabled)[/dim].", ) @@ -317,7 +292,6 @@ def edit_help(cls, option_name: str, help_text: str): "--partial/--no-partial", "--allow/--not-allow", "--allow-partial/--not-partial", - "--allow_partial_stake/--no_allow_partial_stake", show_default=False, help="Enable or disable partial stake mode [dim](default: disabled)[/dim].", ) @@ -334,7 +308,6 @@ def edit_help(cls, option_name: str, help_text: str): False, "--json-output", "--json-out", - "--json_output", help="Outputs the result of the command as JSON.", ) period: int = typer.Option( From c83f7985017107a5046ffa0a435effc807468a8e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Jul 2025 20:46:56 +0200 Subject: [PATCH 23/48] Removed extrinsic parsing in favour of using the reported queries. --- bittensor_cli/src/commands/wallets.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 7fc639e1b..1c2de6a3e 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2002,7 +2002,8 @@ async def check_swap_status( Args: subtensor: Connection to the network origin_ss58: The SS58 address of the original coldkey - block_number: Optional block number where the swap was scheduled + expected_block_number: Optional block number where the swap was scheduled + """ if not origin_ss58: @@ -2035,7 +2036,8 @@ async def check_swap_status( console.print(table) console.print( - "\n[dim]Tip: Check specific swap details by providing the original coldkey SS58 address and the block number.[/dim]" + "\n[dim]Tip: Check specific swap details by providing the original coldkey " + "SS58 address and the block number.[/dim]" ) return chain_reported_completion_block, destination_address = await subtensor.query( @@ -2062,21 +2064,8 @@ async def check_swap_status( if expected_block_number is None: expected_block_number = chain_reported_completion_block - swap_info = await find_coldkey_swap_extrinsic( - subtensor=subtensor, - start_block=expected_block_number, - end_block=expected_block_number, - wallet_ss58=origin_ss58, - ) - - if not swap_info: - console.print( - f"[yellow]Warning: Could not find swap extrinsic at block {expected_block_number}[/yellow]" - ) - return - current_block = await subtensor.substrate.get_block_number() - remaining_blocks = swap_info["execution_block"] - current_block + remaining_blocks = expected_block_number - current_block if remaining_blocks <= 0: console.print("[green]Swap period has completed![/green]") @@ -2084,9 +2073,8 @@ async def check_swap_status( console.print( "\n[green]Coldkey swap details:[/green]" - f"\nScheduled at block: {swap_info['block_num']}" f"\nOriginal address: [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" - f"\nDestination address: [{COLORS.G.CK}]{swap_info['dest_coldkey']}[/{COLORS.G.CK}]" - f"\nCompletion block: {swap_info['execution_block']}" + f"\nDestination address: [{COLORS.G.CK}]{destination_address}[/{COLORS.G.CK}]" + f"\nCompletion block: {chain_reported_completion_block}" f"\nTime remaining: {blocks_to_duration(remaining_blocks)}" ) From 883e4f8c13468940ac3ffe214f96086f73350b71 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Jul 2025 17:45:53 -0700 Subject: [PATCH 24/48] add liquidity utils with math --- bittensor_cli/src/commands/liquidity/utils.py | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 bittensor_cli/src/commands/liquidity/utils.py diff --git a/bittensor_cli/src/commands/liquidity/utils.py b/bittensor_cli/src/commands/liquidity/utils.py new file mode 100644 index 000000000..c4507060c --- /dev/null +++ b/bittensor_cli/src/commands/liquidity/utils.py @@ -0,0 +1,202 @@ +""" +This module provides utilities for managing liquidity positions and price conversions in the Bittensor network. The +module handles conversions between TAO and Alpha tokens while maintaining precise calculations for liquidity +provisioning and fee distribution. +""" + +import math +from dataclasses import dataclass +from typing import Any + +from rich.prompt import Prompt + +from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float +from bittensor_cli.src.bittensor.utils import ( + console, +) + +# These three constants are unchangeable at the level of Uniswap math +MIN_TICK = -887272 +MAX_TICK = 887272 +PRICE_STEP = 1.0001 + + +@dataclass +class LiquidityPosition: + id: int + price_low: Balance # RAO + price_high: Balance # RAO + liquidity: Balance # TAO + ALPHA (sqrt by TAO balance * Alpha Balance -> math under the hood) + fees_tao: Balance # RAO + fees_alpha: Balance # RAO + netuid: int + + def to_token_amounts( + self, current_subnet_price: Balance + ) -> tuple[Balance, Balance]: + """Convert a position to token amounts. + + Arguments: + current_subnet_price: current subnet price in Alpha. + + Returns: + tuple[int, int]: + Amount of Alpha in liquidity + Amount of TAO in liquidity + + Liquidity is a combination of TAO and Alpha depending on the price of the subnet at the moment. + """ + sqrt_price_low = math.sqrt(self.price_low) + sqrt_price_high = math.sqrt(self.price_high) + sqrt_current_subnet_price = math.sqrt(current_subnet_price) + + if sqrt_current_subnet_price < sqrt_price_low: + amount_alpha = self.liquidity * (1 / sqrt_price_low - 1 / sqrt_price_high) + amount_tao = 0 + elif sqrt_current_subnet_price > sqrt_price_high: + amount_alpha = 0 + amount_tao = self.liquidity * (sqrt_price_high - sqrt_price_low) + else: + amount_alpha = self.liquidity * ( + 1 / sqrt_current_subnet_price - 1 / sqrt_price_high + ) + amount_tao = self.liquidity * (sqrt_current_subnet_price - sqrt_price_low) + return Balance.from_rao(int(amount_alpha)).set_unit( + self.netuid + ), Balance.from_rao(int(amount_tao)) + + +def price_to_tick(price: float) -> int: + """Converts a float price to the nearest Uniswap V3 tick index.""" + if price <= 0: + raise ValueError(f"Price must be positive, got `{price}`.") + + tick = int(math.log(price) / math.log(PRICE_STEP)) + + if not (MIN_TICK <= tick <= MAX_TICK): + raise ValueError( + f"Resulting tick {tick} is out of allowed range ({MIN_TICK} to {MAX_TICK})" + ) + return tick + + +def tick_to_price(tick: int) -> float: + """Convert an integer Uniswap V3 tick index to float price.""" + if not (MIN_TICK <= tick <= MAX_TICK): + raise ValueError("Tick is out of allowed range") + return PRICE_STEP**tick + + +def get_fees( + current_tick: int, + tick: dict, + tick_index: int, + quote: bool, + global_fees_tao: float, + global_fees_alpha: float, + above: bool, +) -> float: + """Returns the liquidity fee.""" + tick_fee_key = "fees_out_tao" if quote else "fees_out_alpha" + tick_fee_value = fixed_to_float(tick.get(tick_fee_key)) + global_fee_value = global_fees_tao if quote else global_fees_alpha + + if above: + return ( + global_fee_value - tick_fee_value + if tick_index <= current_tick + else tick_fee_value + ) + return ( + tick_fee_value + if tick_index <= current_tick + else global_fee_value - tick_fee_value + ) + + +def get_fees_in_range( + quote: bool, + global_fees_tao: float, + global_fees_alpha: float, + fees_below_low: float, + fees_above_high: float, +) -> float: + """Returns the liquidity fee value in a range.""" + global_fees = global_fees_tao if quote else global_fees_alpha + return global_fees - fees_below_low - fees_above_high + + +# Calculate fees for a position +def calculate_fees( + position: dict[str, Any], + global_fees_tao: float, + global_fees_alpha: float, + tao_fees_below_low: float, + tao_fees_above_high: float, + alpha_fees_below_low: float, + alpha_fees_above_high: float, + netuid: int, +) -> tuple[Balance, Balance]: + fee_tao_agg = get_fees_in_range( + quote=True, + global_fees_tao=global_fees_tao, + global_fees_alpha=global_fees_alpha, + fees_below_low=tao_fees_below_low, + fees_above_high=tao_fees_above_high, + ) + + fee_alpha_agg = get_fees_in_range( + quote=False, + global_fees_tao=global_fees_tao, + global_fees_alpha=global_fees_alpha, + fees_below_low=alpha_fees_below_low, + fees_above_high=alpha_fees_above_high, + ) + + fee_tao = fee_tao_agg - fixed_to_float(position["fees_tao"]) + fee_alpha = fee_alpha_agg - fixed_to_float(position["fees_alpha"]) + liquidity_frac = position["liquidity"] + + fee_tao = liquidity_frac * fee_tao + fee_alpha = liquidity_frac * fee_alpha + + return Balance.from_rao(int(fee_tao)), Balance.from_rao(int(fee_alpha)).set_unit( + netuid + ) + + +def prompt_liquidity(prompt: str, negative_allowed: bool = False) -> Balance: + """Prompt the user for the amount of liquidity. + + Arguments: + prompt: Prompt to display to the user. + negative_allowed: Whether negative amounts are allowed. + + Returns: + Balance converted from input to TAO. + """ + while True: + input_ = Prompt.ask(prompt) + try: + amount = float(input_) + if amount <= 0 and not negative_allowed: + console.print("[red]Amount must be greater than 0[/red].") + continue + return Balance.from_tao(amount) + except ValueError: + console.print("[red]Please enter a valid number[/red].") + + +def prompt_position_id() -> int: + """Ask the user for the ID of the liquidity position to remove.""" + while True: + position_id = Prompt.ask(f"Enter the [blue]liquidity position ID[/blue]") + + try: + position_id = int(position_id) + if position_id <= 1: + console.print("[red]Position ID must be greater than 1[/red].") + continue + return position_id + except ValueError: + console.print("[red]Please enter a valid number[/red].") From 38ec6310edeb01d08621e4591c472cae58148da1 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Jul 2025 17:46:21 -0700 Subject: [PATCH 25/48] add liquidity extrinsics --- .../src/commands/liquidity/__init__.py | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 bittensor_cli/src/commands/liquidity/__init__.py diff --git a/bittensor_cli/src/commands/liquidity/__init__.py b/bittensor_cli/src/commands/liquidity/__init__.py new file mode 100644 index 000000000..091699995 --- /dev/null +++ b/bittensor_cli/src/commands/liquidity/__init__.py @@ -0,0 +1,330 @@ +import asyncio +from typing import TYPE_CHECKING, Optional + +from rich.prompt import Confirm + +from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + unlock_key, +) +from bittensor_cli.src.commands.liquidity.liquidity import ( + add_liquidity_extrinsic, + modify_liquidity_extrinsic, + remove_liquidity_extrinsic, + toggle_user_liquidity_extrinsic, +) +from .utils import ( + LiquidityPosition, + calculate_fees, + get_fees, + price_to_tick, + prompt_position_id, + prompt_liquidity, + tick_to_price, +) + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +__all__ = [ + "add_liquidity", + "get_liquidity_list", + "modify_liquidity", + "remove_liquidity", +] + + +# Command +async def add_liquidity( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: Optional[int], + liquidity: Optional[float], + price_low: Optional[float], + price_high: Optional[float], + prompt: bool, + json_output: bool, +): + """Add liquidity position to provided subnet.""" + # Check wallet access + if not unlock_key(wallet).success: + return False + + # Check that the subnet exists. + if not await subtensor.subnet_exists(netuid=netuid): + return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." + + # Determine the liquidity amount. + if liquidity: + liquidity = Balance.from_tao(liquidity) + else: + liquidity = prompt_liquidity("Enter the amount of liquidity") + + # Determine price range + if price_low: + price_low = Balance.from_tao(price_low) + else: + price_low = prompt_liquidity("Enter liquidity position low price") + + if price_high: + price_high = Balance.from_tao(price_high) + else: + price_high = prompt_liquidity( + "Enter liquidity position high price (must be greater than low price)" + ) + + if price_low >= price_high: + err_console.print(f"The low price must be lower than the high price.") + return False + + if prompt: + console.print("You are about to add a LiquidityPosition with:") + console.print(f"\tliquidity: {liquidity}") + console.print(f"\tprice low: {price_low}") + console.print(f"\tprice high: {price_high}") + console.print(f"\tto SN: {netuid}") + console.print(f"\tusing wallet with name: {wallet.name}") + + if not Confirm.ask("Would you like to continue?"): + return False, "User cancelled operation." + + return await add_liquidity_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + liquidity=liquidity, + price_low=price_low, + price_high=price_high, + ) + + +async def get_liquidity_list( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: Optional[int], + json_output: bool, +): + """ + Args: + wallet: wallet object + subtensor: SubtensorInterface object + netuid: the netuid to stake to (None indicates all subnets) + json_output: whether to output stake info in JSON format + + Returns: + bool: True if add_liquidity operation is successful, False otherwise + """ + + if not await subtensor.subnet_exists(netuid=netuid): + return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." + + if not await subtensor.is_subnet_active(netuid=netuid): + return False, f"Subnet with netuid: {netuid} is not active in {subtensor}." + + ( + positions_response, + fee_global_tao, + fee_global_alpha, + current_sqrt_price, + ) = await asyncio.gather( + subtensor.substrate.query_map( + module="Swap", + storage_function="Positions", + params=[netuid, wallet.coldkeypub.ss58_address], + ), + subtensor.substrate.query( + module="Swap", + storage_function="FeeGlobalTao", + params=[netuid], + ), + subtensor.substrate.query( + module="Swap", + storage_function="FeeGlobalAlpha", + params=[netuid], + ), + subtensor.substrate.query( + module="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + ), + ) + + current_sqrt_price = fixed_to_float(current_sqrt_price) + fee_global_tao = fixed_to_float(fee_global_tao) + fee_global_alpha = fixed_to_float(fee_global_alpha) + + current_price = current_sqrt_price * current_sqrt_price + current_tick = price_to_tick(current_price) + + positions = [] + + async for _, p in positions_response: + position = p.value + tick_index_low = position.get("tick_low")[0] + tick_index_high = position.get("tick_high")[0] + + # Get ticks for the position (for below/above fees) + ( + tick_low, + tick_high, + ) = await asyncio.gather( + subtensor.substrate.query( + module="Swap", + storage_function="Ticks", + params=[netuid, tick_index_low], + ), + subtensor.substrate.query( + module="Swap", + storage_function="Ticks", + params=[netuid, tick_index_high], + ), + ) + + tao_fees_below_low = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_index_low, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + tao_fees_above_high = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_index_high, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + alpha_fees_below_low = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_index_low, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + alpha_fees_above_high = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_index_high, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + + # Get position accrued fees + fees_tao, fees_alpha = calculate_fees( + position=position, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + tao_fees_below_low=tao_fees_below_low, + tao_fees_above_high=tao_fees_above_high, + alpha_fees_below_low=alpha_fees_below_low, + alpha_fees_above_high=alpha_fees_above_high, + netuid=netuid, + ) + + lp = LiquidityPosition( + **{ + "id": position.get("id")[0], + "price_low": Balance.from_tao( + tick_to_price(position.get("tick_low")[0]) + ), + "price_high": Balance.from_tao( + tick_to_price(position.get("tick_high")[0]) + ), + "liquidity": Balance.from_rao(position.get("liquidity")), + "fees_tao": fees_tao, + "fees_alpha": fees_alpha, + "netuid": position.get("netuid"), + } + ) + positions.append(lp) + + return positions + + +async def remove_liquidity( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + position_id: Optional[int] = None, + prompt: Optional[bool] = None, + all_liquidity_ids: Optional[bool] = None, + json_output: bool = False, +) -> tuple[bool, str]: + """Remove liquidity position from provided subnet.""" + if not await subtensor.subnet_exists(netuid=netuid): + return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." + + if not position_id: + position_id = prompt_position_id() + + if prompt and not all_liquidity_ids: + console.print("You are about to remove a LiquidityPosition with:") + console.print(f"\tSubnet: {netuid}") + console.print(f"\tPosition id: {position_id}") + console.print(f"\tWallet name: {wallet.name}") + + if not Confirm.ask("Would you like to continue?"): + return False, "User cancelled operation." + + return await remove_liquidity_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + position_id=position_id, + ) + + +async def modify_liquidity( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Optional[float], + prompt: Optional[bool] = None, + json_output: bool = False, +): + """Modify liquidity position in provided subnet.""" + if not await subtensor.subnet_exists(netuid=netuid): + return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." + + if not position_id: + position_id = prompt_position_id() + + if liquidity_delta: + liquidity_delta = Balance.from_tao(liquidity_delta) + else: + liquidity_delta = prompt_liquidity( + f"Enter the [blue]liquidity_delta[/blue] for modify position with id " + f"[blue]{position_id}[/blue] (could be positive or negative)", + negative_allowed=True, + ) + + if prompt: + console.print("You are about to modify a LiquidityPosition with:") + console.print(f"\tSubnet: {netuid}") + console.print(f"\tPosition id: {position_id}") + console.print(f"\tWallet name: {wallet.name}") + console.print(f"\tLiquidity delta: {liquidity_delta}") + + if not Confirm.ask("Would you like to continue?"): + return False, "User cancelled operation." + + return await modify_liquidity_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=liquidity_delta, + ) From e05e6cdcb2102c7c8ed430019c2ab6c30df2dcde Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Jul 2025 17:46:49 -0700 Subject: [PATCH 26/48] update HELP_PANELS --- bittensor_cli/src/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 76041ec71..d3da0211c 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -699,6 +699,9 @@ class WalletValidationTypes(Enum): "VIEW": { "DASHBOARD": "Network Dashboard", }, + "LIQUIDITY": { + "LIQUIDITY_MGMT": "Liquidity Management", + }, } From 7488f71554569d3317660d87d267f6acdbad7aa0 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Jul 2025 17:47:12 -0700 Subject: [PATCH 27/48] add middleware calls --- .../src/commands/liquidity/liquidity.py | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 bittensor_cli/src/commands/liquidity/liquidity.py diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py new file mode 100644 index 000000000..d67457c71 --- /dev/null +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -0,0 +1,203 @@ +from typing import TYPE_CHECKING + +from bittensor_cli.src.bittensor.utils import unlock_key +from bittensor_cli.src.bittensor.balances import Balance +from .utils import price_to_tick + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def add_liquidity_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + liquidity: Balance, + price_low: Balance, + price_high: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """ + Adds liquidity to the specified price range. + + Arguments: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + liquidity: The amount of liquidity to be added. + price_low: The lower bound of the price tick range. + price_high: The upper bound of the price tick range. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call + `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + return False, unlock.message + + tick_low = price_to_tick(price_low.tao) + tick_high = price_to_tick(price_high.tao) + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="add_liquidity", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "tick_low": tick_low, + "tick_high": tick_high, + "liquidity": liquidity.rao, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +async def modify_liquidity_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """Modifies liquidity in liquidity position by adding or removing liquidity from it. + + Arguments: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Modifying is allowed even when user liquidity is enabled in specified subnet. + Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="modify_position", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "position_id": position_id, + "liquidity_delta": liquidity_delta.rao, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +async def remove_liquidity_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + position_id: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """Remove liquidity and credit balances back to wallet's hotkey stake. + + Arguments: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. + Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="remove_liquidity", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "netuid": netuid, + "position_id": position_id, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +async def toggle_user_liquidity_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + enable: bool, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str]: + """Allow to toggle user liquidity for specified subnet. + + Arguments: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + enable: Boolean indicating whether to enable user liquidity. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + if not (unlock := unlock_key(wallet)).success: + return False, unlock.message + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="toggle_user_liquidity", + call_params={"netuid": netuid, "enable": enable}, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) From 61874c1e5973518fd5a8163daad444e39610ad81 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Jul 2025 17:47:24 -0700 Subject: [PATCH 28/48] add is_subnet_active --- .../src/bittensor/subtensor_interface.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 7b632af31..d3a87c188 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -531,6 +531,33 @@ async def get_netuids_for_hotkey( res.append(record[0]) return res + async def is_subnet_active( + self, + netuid: int, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """Verify if subnet with provided netuid is active. + + Args: + netuid (int): The unique identifier of the subnet. + block_hash (Optional[str]): The blockchain block_hash representation of block id. + reuse_block (bool): Whether to reuse the last-used block hash. + + Returns: + True if subnet is active, False otherwise. + + This means whether the `start_call` was initiated or not. + """ + query = await self.substrate.query( + module="SubtensorModule", + storage_function="FirstEmissionBlockNumber", + block_hash=block_hash, + reuse_block_hash=reuse_block, + params=[netuid], + ) + return True if query and query.value > 0 else False + async def subnet_exists( self, netuid: int, block_hash: Optional[str] = None, reuse_block: bool = False ) -> bool: From 8e38753ad778ff4428f91b8117589d26cf3df59f Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Jul 2025 17:47:33 -0700 Subject: [PATCH 29/48] add commands --- bittensor_cli/cli.py | 373 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 356 insertions(+), 17 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 08201fa47..bb5564602 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import asyncio -import curses import copy +import curses import importlib import json import os.path @@ -10,13 +10,13 @@ import sys import traceback import warnings +from dataclasses import fields from pathlib import Path from typing import Coroutine, Optional -from dataclasses import fields +import numpy as np import rich import typer -import numpy as np from async_substrate_interface.errors import ( SubstrateRequestException, ConnectionClosed, @@ -30,6 +30,7 @@ from typing_extensions import Annotated from yaml import safe_dump, safe_load +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src import ( defaults, HELP_PANELS, @@ -39,21 +40,10 @@ COLORS, HYPERPARAMS, ) -from bittensor_cli.version import __version__, __version_as_int__ from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.commands import sudo, wallets, view -from bittensor_cli.src.commands import weights as weights_cmds -from bittensor_cli.src.commands.subnets import price, subnets -from bittensor_cli.src.commands.stake import ( - children_hotkeys, - list as list_stake, - move as move_stake, - add as add_stake, - remove as remove_stake, -) -from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -70,6 +60,23 @@ prompt_for_subnet_identity, validate_rate_tolerance, ) +from bittensor_cli.src.commands import sudo, wallets, view +from bittensor_cli.src.commands import weights as weights_cmds +from bittensor_cli.src.commands.liquidity import ( + add_liquidity, + get_liquidity_list, + modify_liquidity, + remove_liquidity, +) +from bittensor_cli.src.commands.stake import ( + children_hotkeys, + list as list_stake, + move as move_stake, + add as add_stake, + remove as remove_stake, +) +from bittensor_cli.src.commands.subnets import price, subnets +from bittensor_cli.version import __version__, __version_as_int__ try: from git import Repo, GitError @@ -116,7 +123,7 @@ def edit_help(cls, option_name: str, help_text: str): help="Name of the wallet.", ) wallet_path = typer.Option( - None, + "default", "--wallet-path", "-p", "--wallet_path", @@ -124,7 +131,7 @@ def edit_help(cls, option_name: str, help_text: str): help="Path where the wallets are located. For example: `/Users/btuser/.bittensor/wallets`.", ) wallet_hotkey = typer.Option( - None, + "default", "--hotkey", "-H", "--wallet_hotkey", @@ -656,6 +663,7 @@ def __init__(self): self.subnets_app = typer.Typer(epilog=_epilog) self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) + self.liquidity_app = typer.Typer(epilog=_epilog) # config alias self.app.add_typer( @@ -978,6 +986,30 @@ def __init__(self): self.sudo_app.command("get_take", hidden=True)(self.sudo_get_take) self.sudo_app.command("set_take", hidden=True)(self.sudo_set_take) + # Liquidity + self.app.add_typer( + self.utils_app, + name="liquidity", + short_help="liquidity commands, aliases: `l`", + no_args_is_help=True, + ) + self.app.add_typer( + self.liquidity_app, name="l", hidden=True, no_args_is_help=True + ) + # liquidity commands + self.liquidity_app.command( + "add", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] + )(self.liquidity_add) + self.liquidity_app.command( + "list", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] + )(self.liquidity_list) + self.liquidity_app.command( + "modify", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] + )(self.liquidity_modify) + self.liquidity_app.command( + "remove", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] + )(self.liquidity_remove) + def generate_command_tree(self) -> Tree: """ Generates a rich.Tree of the commands, subcommands, and groups of this app @@ -5762,6 +5794,313 @@ def view_dashboard( ) ) + def liquidity_add( + self, + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: Optional[int] = Options.netuid, + liquidity: Optional[float] = typer.Option( + None, + "--liquidity", + help="Liquidity amount for", + ), + price_low: Optional[float] = typer.Option( + None, + "--price-low", + "--price_low", + "--liquidity-price-low", + "--liquidity_price_low", + help="Low price for the adding liquidity position.", + ), + price_high: Optional[float] = typer.Option( + None, + "--price-high", + "--price_high", + "--liquidity-price-high", + "--liquidity_price_high", + help="High price for the adding liquidity position.", + ), + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """Add liquidity to the swap (as a combination of TAO + Alpha).""" + self.verbosity_handler(quiet, verbose, json_output) + if not netuid: + netuid = Prompt.ask( + f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", + default=None, + show_default=False, + ) + + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], + validate=WV.WALLET_AND_HOTKEY, + ) + + success, message = self._run_command( + add_liquidity( + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + liquidity=liquidity, + price_low=price_low, + price_high=price_high, + prompt=prompt, + json_output=json_output, + ) + ) + + if success: + console.print( + "[green]LiquidityPosition has beed successfully added.[/green]" + ) + else: + console.print(f"[red]{message}[/red]") + + def liquidity_list( + self, + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: Optional[int] = Options.netuid, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """Displays liquidity positions in given subnet.""" + self.verbosity_handler(quiet, verbose, json_output) + if not netuid: + netuid = Prompt.ask( + f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", + default=None, + show_default=False, + ) + + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], + validate=WV.WALLET_AND_HOTKEY, + ) + + positions = self._run_command( + get_liquidity_list( + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + json_output=json_output, + ) + ) + + liquidity_table = Table( + Column("ID", justify="center"), + Column("Liquidity", justify="center"), + Column("Price low", justify="center"), + Column("Price high", justify="center"), + Column("Fee TAO", justify="center"), + Column("Fee Alpha", justify="center"), + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{'Liquidity Positions of '}{wallet.name} wallet in SN #{netuid}\n", + show_footer=False, + show_edge=True, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + + for lp in positions: + liquidity_table.add_row( + str(lp.id), + str(lp.liquidity), + str(lp.price_low), + str(lp.price_high), + str(lp.fees_tao), + str(lp.fees_alpha), + ) + + console.print(liquidity_table) + + def liquidity_remove( + self, + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: Optional[int] = Options.netuid, + position_id: Optional[int] = typer.Option( + None, + "--position-id", + "--position_id", + help="Position ID for modification or removing.", + ), + all_liquidity_ids: Optional[bool] = typer.Option( + False, + "--all", + "--a", + help="Whether to remove all liquidity positions for given subnet.", + ), + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """Remove liquidity from the swap (as a combination of TAO + Alpha).""" + + if all_liquidity_ids and position_id: + print_error("Cannot specify both --all and --position-id.") + return + + self.verbosity_handler(quiet, verbose, json_output) + if not netuid: + netuid = Prompt.ask( + f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", + default=None, + show_default=False, + ) + + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], + validate=WV.WALLET_AND_HOTKEY, + ) + + position_ids = [position_id] + if all_liquidity_ids: + positions = self._run_command( + get_liquidity_list( + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + json_output=json_output, + ) + ) + position_ids = [p.id for p in positions] + + successes = [] + message = "" + for position_id in position_ids: + success, message = self._run_command( + remove_liquidity( + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + position_id=position_id, + prompt=prompt, + all_liquidity_ids=all_liquidity_ids, + json_output=json_output, + ) + ) + successes.append(success) + + if all(successes): + if all_liquidity_ids: + console.print( + f"[green]All liquidity positions for subnet {netuid} have been successfully removed.[/green]" + ) + else: + console.print( + "[green]LiquidityPosition has beed successfully deleted.[/green]" + ) + else: + console.print(f"[red]{message}[/red]") + + def liquidity_modify( + self, + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: Optional[int] = Options.netuid, + position_id: Optional[int] = typer.Option( + None, + "--position-id", + "--position_id", + help="Position ID for modification or removing.", + ), + liquidity_delta: Optional[float] = typer.Option( + None, + "--liquidity-delta", + "--liquidity_delta", + help="Liquidity amount for modification.", + ), + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """Modifies the liquidity position for the given subnet.""" + self.verbosity_handler(quiet, verbose, json_output) + if not netuid: + netuid = Prompt.ask( + f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", + default=None, + show_default=False, + ) + + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], + validate=WV.WALLET_AND_HOTKEY, + ) + + success, message = self._run_command( + modify_liquidity( + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=liquidity_delta, + prompt=prompt, + json_output=json_output, + ) + ) + + if success: + console.print( + "[green]LiquidityPosition has beed successfully modified.[/green]" + ) + else: + console.print(f"[red]{message}[/red]") + @staticmethod @utils_app.command("convert") def convert( From d986616936e3b35b9b0070034466fb6c9be11af7 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Jul 2025 19:11:43 -0700 Subject: [PATCH 30/48] update help for flag --- bittensor_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index bb5564602..4526c5a56 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5804,7 +5804,7 @@ def liquidity_add( liquidity: Optional[float] = typer.Option( None, "--liquidity", - help="Liquidity amount for", + help="Amount of liquidity to add to the subnet.", ), price_low: Optional[float] = typer.Option( None, From fc536d66ac6f1c15e99bb8e360dff6d8df9ad6b7 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 2 Jul 2025 20:12:55 -0700 Subject: [PATCH 31/48] =?UTF-8?q?=F0=9F=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/e2e_tests/test_staking_sudo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index e56f0e32a..57cbe0e06 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -276,7 +276,7 @@ def test_staking(local_chain, wallet_setup): "--amount", "100", "--tolerance", - "0.1", + "0.2", "--partial", "--no-prompt", "--era", From 3027b60a42d2f64d751a8064a157fbc48f1e03b6 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 3 Jul 2025 13:23:45 -0700 Subject: [PATCH 32/48] add `HYPERPARAMS_MODULE`, update `SubnetHyperparameters`, improve `get_subnet_hyperparameters`, add `DEFAULT_PALLET` for new logic --- bittensor_cli/src/__init__.py | 5 + bittensor_cli/src/bittensor/chain_data.py | 112 ++++++++++++------ .../src/bittensor/subtensor_interface.py | 21 +--- bittensor_cli/src/commands/sudo.py | 31 +++-- 4 files changed, 112 insertions(+), 57 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index d3da0211c..d7b1a9f70 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -660,6 +660,11 @@ class WalletValidationTypes(Enum): ), "yuma3_enabled": ("sudo_set_yuma3_enabled", False), "alpha_sigmoid_steepness": ("sudo_set_alpha_sigmoid_steepness", True), + "user_liquidity_enabled": ("toggle_user_liquidity", True), +} + +HYPERPARAMS_MODULE = { + "user_liquidity_enabled": "Swap", } # Help Panels for cli help diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 5749ada6f..6fe7b3da7 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -148,13 +148,49 @@ def get(self, item, default=None): @dataclass class SubnetHyperparameters(InfoBase): - """Dataclass for subnet hyperparameters.""" + """ + This class represents the hyperparameters for a subnet. + Attributes: + rho (int): The rate of decay of some value. + kappa (int): A constant multiplier used in calculations. + immunity_period (int): The period during which immunity is active. + min_allowed_weights (int): Minimum allowed weights. + max_weight_limit (float): Maximum weight limit. + tempo (int): The tempo or rate of operation. + min_difficulty (int): Minimum difficulty for some operations. + max_difficulty (int): Maximum difficulty for some operations. + weights_version (int): The version number of the weights used. + weights_rate_limit (int): Rate limit for processing weights. + adjustment_interval (int): Interval at which adjustments are made. + activity_cutoff (int): Activity cutoff threshold. + registration_allowed (bool): Indicates if registration is allowed. + target_regs_per_interval (int): Target number of registrations per interval. + min_burn (int): Minimum burn value. + max_burn (int): Maximum burn value. + bonds_moving_avg (int): Moving average of bonds. + max_regs_per_block (int): Maximum number of registrations per block. + serving_rate_limit (int): Limit on the rate of service. + max_validators (int): Maximum number of validators. + adjustment_alpha (int): Alpha value for adjustments. + difficulty (int): Difficulty level. + commit_reveal_period (int): Interval for commit-reveal weights. + commit_reveal_weights_enabled (bool): Flag indicating if commit-reveal weights are enabled. + alpha_high (int): High value of alpha. + alpha_low (int): Low value of alpha. + liquid_alpha_enabled (bool): Flag indicating if liquid alpha is enabled. + alpha_sigmoid_steepness (float): + yuma_version (int): Version of yuma. + subnet_is_active (bool): Indicates if subnet is active after START CALL. + transfers_enabled (bool): Flag indicating if transfers are enabled. + bonds_reset_enabled (bool): Flag indicating if bonds are reset enabled. + user_liquidity_enabled (bool): Flag indicating if user liquidity is enabled. + """ rho: int kappa: int immunity_period: int min_allowed_weights: int - max_weights_limit: float + max_weight_limit: float tempo: int min_difficulty: int max_difficulty: int @@ -177,43 +213,53 @@ class SubnetHyperparameters(InfoBase): alpha_high: int alpha_low: int liquid_alpha_enabled: bool - yuma3_enabled: bool - alpha_sigmoid_steepness: int + alpha_sigmoid_steepness: float + yuma_version: int + subnet_is_active: bool + transfers_enabled: bool + bonds_reset_enabled: bool + user_liquidity_enabled: bool @classmethod def _fix_decoded( cls, decoded: Union[dict, "SubnetHyperparameters"] ) -> "SubnetHyperparameters": return cls( - rho=decoded.get("rho"), - kappa=decoded.get("kappa"), - immunity_period=decoded.get("immunity_period"), - min_allowed_weights=decoded.get("min_allowed_weights"), - max_weights_limit=decoded.get("max_weights_limit"), - tempo=decoded.get("tempo"), - min_difficulty=decoded.get("min_difficulty"), - max_difficulty=decoded.get("max_difficulty"), - weights_version=decoded.get("weights_version"), - weights_rate_limit=decoded.get("weights_rate_limit"), - adjustment_interval=decoded.get("adjustment_interval"), - activity_cutoff=decoded.get("activity_cutoff"), - registration_allowed=decoded.get("registration_allowed"), - target_regs_per_interval=decoded.get("target_regs_per_interval"), - min_burn=decoded.get("min_burn"), - max_burn=decoded.get("max_burn"), - bonds_moving_avg=decoded.get("bonds_moving_avg"), - max_regs_per_block=decoded.get("max_regs_per_block"), - serving_rate_limit=decoded.get("serving_rate_limit"), - max_validators=decoded.get("max_validators"), - adjustment_alpha=decoded.get("adjustment_alpha"), - difficulty=decoded.get("difficulty"), - commit_reveal_period=decoded.get("commit_reveal_period"), - commit_reveal_weights_enabled=decoded.get("commit_reveal_weights_enabled"), - alpha_high=decoded.get("alpha_high"), - alpha_low=decoded.get("alpha_low"), - liquid_alpha_enabled=decoded.get("liquid_alpha_enabled"), - yuma3_enabled=decoded.get("yuma3_enabled"), - alpha_sigmoid_steepness=decoded.get("alpha_sigmoid_steepness"), + activity_cutoff=decoded["activity_cutoff"], + adjustment_alpha=decoded["adjustment_alpha"], + adjustment_interval=decoded["adjustment_interval"], + alpha_high=decoded["alpha_high"], + alpha_low=decoded["alpha_low"], + alpha_sigmoid_steepness=fixed_to_float( + decoded["alpha_sigmoid_steepness"], frac_bits=32 + ), + bonds_moving_avg=decoded["bonds_moving_avg"], + bonds_reset_enabled=decoded["bonds_reset_enabled"], + commit_reveal_weights_enabled=decoded["commit_reveal_weights_enabled"], + commit_reveal_period=decoded["commit_reveal_period"], + difficulty=decoded["difficulty"], + immunity_period=decoded["immunity_period"], + kappa=decoded["kappa"], + liquid_alpha_enabled=decoded["liquid_alpha_enabled"], + max_burn=decoded["max_burn"], + max_difficulty=decoded["max_difficulty"], + max_regs_per_block=decoded["max_regs_per_block"], + max_validators=decoded["max_validators"], + max_weight_limit=decoded["max_weights_limit"], + min_allowed_weights=decoded["min_allowed_weights"], + min_burn=decoded["min_burn"], + min_difficulty=decoded["min_difficulty"], + registration_allowed=decoded["registration_allowed"], + rho=decoded["rho"], + serving_rate_limit=decoded["serving_rate_limit"], + subnet_is_active=decoded["subnet_is_active"], + target_regs_per_interval=decoded["target_regs_per_interval"], + tempo=decoded["tempo"], + transfers_enabled=decoded["transfers_enabled"], + user_liquidity_enabled=decoded["user_liquidity_enabled"], + weights_rate_limit=decoded["weights_rate_limit"], + weights_version=decoded["weights_version"], + yuma_version=decoded["yuma_version"], ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index d3a87c188..c274246ff 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1155,22 +1155,13 @@ async def get_subnet_hyperparameters( Understanding the hyperparameters is crucial for comprehending how subnets are configured and managed, and how they interact with the network's consensus and incentive mechanisms. """ - main_result, yuma3_result, sigmoid_steepness = await asyncio.gather( - self.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", - method="get_subnet_hyperparams", - params=[netuid], - block_hash=block_hash, - ), - self.query("SubtensorModule", "Yuma3On", [netuid]), - self.query("SubtensorModule", "AlphaSigmoidSteepness", [netuid]), + result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_hyperparams_v2", + params=[netuid], + block_hash=block_hash, ) - result = { - **main_result, - **{"yuma3_enabled": yuma3_result}, - **{"alpha_sigmoid_steepness": sigmoid_steepness}, - } - if not main_result: + if not result: return [] return SubnetHyperparameters.from_any(result) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index de02993bb..8d7950392 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -8,7 +8,12 @@ from rich.prompt import Confirm from scalecodec import GenericCall -from bittensor_cli.src import HYPERPARAMS, DelegatesDetails, COLOR_PALETTE +from bittensor_cli.src import ( + HYPERPARAMS, + HYPERPARAMS_MODULE, + DelegatesDetails, + COLOR_PALETTE, +) from bittensor_cli.src.bittensor.chain_data import decode_account_id from bittensor_cli.src.bittensor.utils import ( console, @@ -31,6 +36,7 @@ # helpers and extrinsics +DEFAULT_PALLET = "AdminUtils" def allowed_value( @@ -73,7 +79,7 @@ def allowed_value( return True, value -def string_to_bool(val) -> bool: +def string_to_bool(val) -> bool | type[ValueError]: try: return {"true": True, "1": True, "0": False, "false": False}[val.lower()] except KeyError: @@ -81,7 +87,11 @@ def string_to_bool(val) -> bool: def search_metadata( - param_name: str, value: Union[str, bool, float, list[float]], netuid: int, metadata + param_name: str, + value: Union[str, bool, float, list[float]], + netuid: int, + metadata, + pallet: str = DEFAULT_PALLET, ) -> tuple[bool, Optional[dict]]: """ Searches the substrate metadata AdminUtils pallet for a given parameter name. Crafts a response dict to be used @@ -92,6 +102,7 @@ def search_metadata( value: the value to set the hyperparameter netuid: the specified netuid metadata: the subtensor.substrate.metadata + pallet: the name of the module to use for the query. If not set, the default value is DEFAULT_PALLET Returns: (success, dict of call params) @@ -113,7 +124,7 @@ def type_converter_with_retry(type_, val, arg_name): call_crafter = {"netuid": netuid} - pallet = metadata.get_metadata_pallet("AdminUtils") + pallet = metadata.get_metadata_pallet(pallet) for call in pallet.calls: if call.name == param_name: if "netuid" not in [x.name for x in call.args]: @@ -135,11 +146,11 @@ def type_converter_with_retry(type_, val, arg_name): return False, None -def requires_bool(metadata, param_name) -> bool: +def requires_bool(metadata, param_name, pallet: str = DEFAULT_PALLET) -> bool: """ Determines whether a given hyperparam takes a single arg (besides netuid) that is of bool type. """ - pallet = metadata.get_metadata_pallet("AdminUtils") + pallet = metadata.get_metadata_pallet(pallet) for call in pallet.calls: if call.name == param_name: if "netuid" not in [x.name for x in call.args]: @@ -218,6 +229,8 @@ async def set_hyperparameter_extrinsic( substrate = subtensor.substrate msg_value = value if not arbitrary_extrinsic else call_params + pallet = HYPERPARAMS_MODULE.get(parameter) or DEFAULT_PALLET + with console.status( f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}" f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{msg_value}" @@ -227,7 +240,7 @@ async def set_hyperparameter_extrinsic( ): if not arbitrary_extrinsic: extrinsic_params = await substrate.get_metadata_call_function( - "AdminUtils", extrinsic + module_name=pallet, call_function_name=extrinsic ) # if input value is a list, iterate through the list and assign values @@ -251,7 +264,7 @@ async def set_hyperparameter_extrinsic( else: if requires_bool( - substrate.metadata, param_name=extrinsic + substrate.metadata, param_name=extrinsic, pallet=pallet ) and isinstance(value, str): value = string_to_bool(value) value_argument = extrinsic_params["fields"][ @@ -261,7 +274,7 @@ async def set_hyperparameter_extrinsic( # create extrinsic call call_ = await substrate.compose_call( - call_module="AdminUtils", + call_module=pallet, call_function=extrinsic, call_params=call_params, ) From e637f17302a63548398099fa036e69d3dae7f0f2 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 3 Jul 2025 13:43:39 -0700 Subject: [PATCH 33/48] fix test --- tests/e2e_tests/test_staking_sudo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 57cbe0e06..152cf7591 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -533,12 +533,12 @@ def test_staking(local_chain, wallet_setup): yuma3_val = next( filter( - lambda x: x["hyperparameter"] == "yuma3_enabled", + lambda x: x["hyperparameter"] == "yuma_version", json.loads(changed_yuma3_hyperparam.stdout), ) ) - assert yuma3_val["value"] is True - assert yuma3_val["normalized_value"] is True + assert yuma3_val["value"] is 3 + assert yuma3_val["normalized_value"] is 3 print("✅ Passed staking and sudo commands") change_arbitrary_hyperparam = exec_command_alice( From a5df4ac924a0da135801f39c2659a6812fef9917 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 3 Jul 2025 14:06:39 -0700 Subject: [PATCH 34/48] update list table --- bittensor_cli/cli.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4526c5a56..58c66faab 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5915,7 +5915,7 @@ def liquidity_list( liquidity_table = Table( Column("ID", justify="center"), - Column("Liquidity", justify="center"), + Column("Liquidity (Alpha and TAO part)", justify="center"), Column("Price low", justify="center"), Column("Price high", justify="center"), Column("Fee TAO", justify="center"), @@ -5931,10 +5931,13 @@ def liquidity_list( pad_edge=True, ) + current_price = self._run_command(self.subtensor.subnet(netuid=netuid)).price + for lp in positions: + alpha, tao = lp.to_token_amounts(current_price) liquidity_table.add_row( str(lp.id), - str(lp.liquidity), + f"{alpha} {tao}", str(lp.price_low), str(lp.price_high), str(lp.fees_tao), From 658a1f139c48b2a32091efafa5a1fcc8f65bc974 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Jul 2025 16:50:32 +0200 Subject: [PATCH 35/48] Brought the PR into line with the rest of the btcli codebase, added JSON output for all liquidity options, fixed edited test. --- bittensor_cli/cli.py | 191 +++----- .../src/commands/liquidity/__init__.py | 330 -------------- .../src/commands/liquidity/liquidity.py | 415 +++++++++++++++++- bittensor_cli/src/commands/liquidity/utils.py | 8 +- tests/e2e_tests/test_staking_sudo.py | 4 +- 5 files changed, 481 insertions(+), 467 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 58c66faab..65295d71e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -30,7 +30,6 @@ from typing_extensions import Annotated from yaml import safe_dump, safe_load -from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src import ( defaults, HELP_PANELS, @@ -62,11 +61,10 @@ ) from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds -from bittensor_cli.src.commands.liquidity import ( - add_liquidity, - get_liquidity_list, - modify_liquidity, - remove_liquidity, +from bittensor_cli.src.commands.liquidity import liquidity +from bittensor_cli.src.commands.liquidity.utils import ( + prompt_liquidity, + prompt_position_id, ) from bittensor_cli.src.commands.stake import ( children_hotkeys, @@ -5801,7 +5799,7 @@ def liquidity_add( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, - liquidity: Optional[float] = typer.Option( + liquidity_: Optional[float] = typer.Option( None, "--liquidity", help="Amount of liquidity to add to the subnet.", @@ -5849,13 +5847,35 @@ def liquidity_add( ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], validate=WV.WALLET_AND_HOTKEY, ) + # Determine the liquidity amount. + if liquidity_: + liquidity_ = Balance.from_tao(liquidity_) + else: + liquidity_ = prompt_liquidity("Enter the amount of liquidity") + + # Determine price range + if price_low: + price_low = Balance.from_tao(price_low) + else: + price_low = prompt_liquidity("Enter liquidity position low price") + + if price_high: + price_high = Balance.from_tao(price_high) + else: + price_high = prompt_liquidity( + "Enter liquidity position high price (must be greater than low price)" + ) - success, message = self._run_command( - add_liquidity( + if price_low >= price_high: + err_console.print("The low price must be lower than the high price.") + return False + + return self._run_command( + liquidity.add_liquidity( subtensor=self.initialize_chain(network), wallet=wallet, netuid=netuid, - liquidity=liquidity, + liquidity=liquidity_, price_low=price_low, price_high=price_high, prompt=prompt, @@ -5863,13 +5883,6 @@ def liquidity_add( ) ) - if success: - console.print( - "[green]LiquidityPosition has beed successfully added.[/green]" - ) - else: - console.print(f"[red]{message}[/red]") - def liquidity_list( self, network: Optional[list[str]] = Options.network, @@ -5884,18 +5897,12 @@ def liquidity_list( """Displays liquidity positions in given subnet.""" self.verbosity_handler(quiet, verbose, json_output) if not netuid: - netuid = Prompt.ask( + netuid = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", default=None, show_default=False, ) - if not wallet_name: - wallet_name = Prompt.ask( - "Enter the [blue]wallet name[/blue]", - default=self.config.get("wallet_name") or defaults.wallet.name, - ) - wallet = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, @@ -5903,9 +5910,8 @@ def liquidity_list( ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], validate=WV.WALLET_AND_HOTKEY, ) - - positions = self._run_command( - get_liquidity_list( + self._run_command( + liquidity.show_liquidity_list( subtensor=self.initialize_chain(network), wallet=wallet, netuid=netuid, @@ -5913,39 +5919,6 @@ def liquidity_list( ) ) - liquidity_table = Table( - Column("ID", justify="center"), - Column("Liquidity (Alpha and TAO part)", justify="center"), - Column("Price low", justify="center"), - Column("Price high", justify="center"), - Column("Fee TAO", justify="center"), - Column("Fee Alpha", justify="center"), - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{'Liquidity Positions of '}{wallet.name} wallet in SN #{netuid}\n", - show_footer=False, - show_edge=True, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, - ) - - current_price = self._run_command(self.subtensor.subnet(netuid=netuid)).price - - for lp in positions: - alpha, tao = lp.to_token_amounts(current_price) - liquidity_table.add_row( - str(lp.id), - f"{alpha} {tao}", - str(lp.price_low), - str(lp.price_high), - str(lp.fees_tao), - str(lp.fees_alpha), - ) - - console.print(liquidity_table) - def liquidity_remove( self, network: Optional[list[str]] = Options.network, @@ -5957,7 +5930,7 @@ def liquidity_remove( None, "--position-id", "--position_id", - help="Position ID for modification or removing.", + help="Position ID for modification or removal.", ), all_liquidity_ids: Optional[bool] = typer.Option( False, @@ -5972,24 +5945,22 @@ def liquidity_remove( ): """Remove liquidity from the swap (as a combination of TAO + Alpha).""" + self.verbosity_handler(quiet, verbose, json_output) + if all_liquidity_ids and position_id: print_error("Cannot specify both --all and --position-id.") return - self.verbosity_handler(quiet, verbose, json_output) + if not position_id and not all_liquidity_ids: + position_id = prompt_position_id() + if not netuid: - netuid = Prompt.ask( + netuid = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", default=None, show_default=False, ) - if not wallet_name: - wallet_name = Prompt.ask( - "Enter the [blue]wallet name[/blue]", - default=self.config.get("wallet_name") or defaults.wallet.name, - ) - wallet = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, @@ -5997,46 +5968,17 @@ def liquidity_remove( ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], validate=WV.WALLET_AND_HOTKEY, ) - - position_ids = [position_id] - if all_liquidity_ids: - positions = self._run_command( - get_liquidity_list( - subtensor=self.initialize_chain(network), - wallet=wallet, - netuid=netuid, - json_output=json_output, - ) - ) - position_ids = [p.id for p in positions] - - successes = [] - message = "" - for position_id in position_ids: - success, message = self._run_command( - remove_liquidity( - subtensor=self.initialize_chain(network), - wallet=wallet, - netuid=netuid, - position_id=position_id, - prompt=prompt, - all_liquidity_ids=all_liquidity_ids, - json_output=json_output, - ) + return self._run_command( + liquidity.remove_liquidity( + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + position_id=position_id, + prompt=prompt, + all_liquidity_ids=all_liquidity_ids, + json_output=json_output, ) - successes.append(success) - - if all(successes): - if all_liquidity_ids: - console.print( - f"[green]All liquidity positions for subnet {netuid} have been successfully removed.[/green]" - ) - else: - console.print( - "[green]LiquidityPosition has beed successfully deleted.[/green]" - ) - else: - console.print(f"[red]{message}[/red]") + ) def liquidity_modify( self, @@ -6065,16 +6007,8 @@ def liquidity_modify( """Modifies the liquidity position for the given subnet.""" self.verbosity_handler(quiet, verbose, json_output) if not netuid: - netuid = Prompt.ask( + netuid = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", - default=None, - show_default=False, - ) - - if not wallet_name: - wallet_name = Prompt.ask( - "Enter the [blue]wallet name[/blue]", - default=self.config.get("wallet_name") or defaults.wallet.name, ) wallet = self.wallet_ask( @@ -6085,8 +6019,20 @@ def liquidity_modify( validate=WV.WALLET_AND_HOTKEY, ) - success, message = self._run_command( - modify_liquidity( + if not position_id: + position_id = prompt_position_id() + + if liquidity_delta: + liquidity_delta = Balance.from_tao(liquidity_delta) + else: + liquidity_delta = prompt_liquidity( + f"Enter the [blue]liquidity delta[/blue] to modify position with id " + f"[blue]{position_id}[/blue] (can be positive or negative)", + negative_allowed=True, + ) + + return self._run_command( + liquidity.modify_liquidity( subtensor=self.initialize_chain(network), wallet=wallet, netuid=netuid, @@ -6097,13 +6043,6 @@ def liquidity_modify( ) ) - if success: - console.print( - "[green]LiquidityPosition has beed successfully modified.[/green]" - ) - else: - console.print(f"[red]{message}[/red]") - @staticmethod @utils_app.command("convert") def convert( diff --git a/bittensor_cli/src/commands/liquidity/__init__.py b/bittensor_cli/src/commands/liquidity/__init__.py index 091699995..e69de29bb 100644 --- a/bittensor_cli/src/commands/liquidity/__init__.py +++ b/bittensor_cli/src/commands/liquidity/__init__.py @@ -1,330 +0,0 @@ -import asyncio -from typing import TYPE_CHECKING, Optional - -from rich.prompt import Confirm - -from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float -from bittensor_cli.src.bittensor.utils import ( - console, - err_console, - unlock_key, -) -from bittensor_cli.src.commands.liquidity.liquidity import ( - add_liquidity_extrinsic, - modify_liquidity_extrinsic, - remove_liquidity_extrinsic, - toggle_user_liquidity_extrinsic, -) -from .utils import ( - LiquidityPosition, - calculate_fees, - get_fees, - price_to_tick, - prompt_position_id, - prompt_liquidity, - tick_to_price, -) - -if TYPE_CHECKING: - from bittensor_wallet import Wallet - from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface - - -__all__ = [ - "add_liquidity", - "get_liquidity_list", - "modify_liquidity", - "remove_liquidity", -] - - -# Command -async def add_liquidity( - subtensor: "SubtensorInterface", - wallet: "Wallet", - netuid: Optional[int], - liquidity: Optional[float], - price_low: Optional[float], - price_high: Optional[float], - prompt: bool, - json_output: bool, -): - """Add liquidity position to provided subnet.""" - # Check wallet access - if not unlock_key(wallet).success: - return False - - # Check that the subnet exists. - if not await subtensor.subnet_exists(netuid=netuid): - return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." - - # Determine the liquidity amount. - if liquidity: - liquidity = Balance.from_tao(liquidity) - else: - liquidity = prompt_liquidity("Enter the amount of liquidity") - - # Determine price range - if price_low: - price_low = Balance.from_tao(price_low) - else: - price_low = prompt_liquidity("Enter liquidity position low price") - - if price_high: - price_high = Balance.from_tao(price_high) - else: - price_high = prompt_liquidity( - "Enter liquidity position high price (must be greater than low price)" - ) - - if price_low >= price_high: - err_console.print(f"The low price must be lower than the high price.") - return False - - if prompt: - console.print("You are about to add a LiquidityPosition with:") - console.print(f"\tliquidity: {liquidity}") - console.print(f"\tprice low: {price_low}") - console.print(f"\tprice high: {price_high}") - console.print(f"\tto SN: {netuid}") - console.print(f"\tusing wallet with name: {wallet.name}") - - if not Confirm.ask("Would you like to continue?"): - return False, "User cancelled operation." - - return await add_liquidity_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - liquidity=liquidity, - price_low=price_low, - price_high=price_high, - ) - - -async def get_liquidity_list( - subtensor: "SubtensorInterface", - wallet: "Wallet", - netuid: Optional[int], - json_output: bool, -): - """ - Args: - wallet: wallet object - subtensor: SubtensorInterface object - netuid: the netuid to stake to (None indicates all subnets) - json_output: whether to output stake info in JSON format - - Returns: - bool: True if add_liquidity operation is successful, False otherwise - """ - - if not await subtensor.subnet_exists(netuid=netuid): - return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." - - if not await subtensor.is_subnet_active(netuid=netuid): - return False, f"Subnet with netuid: {netuid} is not active in {subtensor}." - - ( - positions_response, - fee_global_tao, - fee_global_alpha, - current_sqrt_price, - ) = await asyncio.gather( - subtensor.substrate.query_map( - module="Swap", - storage_function="Positions", - params=[netuid, wallet.coldkeypub.ss58_address], - ), - subtensor.substrate.query( - module="Swap", - storage_function="FeeGlobalTao", - params=[netuid], - ), - subtensor.substrate.query( - module="Swap", - storage_function="FeeGlobalAlpha", - params=[netuid], - ), - subtensor.substrate.query( - module="Swap", - storage_function="AlphaSqrtPrice", - params=[netuid], - ), - ) - - current_sqrt_price = fixed_to_float(current_sqrt_price) - fee_global_tao = fixed_to_float(fee_global_tao) - fee_global_alpha = fixed_to_float(fee_global_alpha) - - current_price = current_sqrt_price * current_sqrt_price - current_tick = price_to_tick(current_price) - - positions = [] - - async for _, p in positions_response: - position = p.value - tick_index_low = position.get("tick_low")[0] - tick_index_high = position.get("tick_high")[0] - - # Get ticks for the position (for below/above fees) - ( - tick_low, - tick_high, - ) = await asyncio.gather( - subtensor.substrate.query( - module="Swap", - storage_function="Ticks", - params=[netuid, tick_index_low], - ), - subtensor.substrate.query( - module="Swap", - storage_function="Ticks", - params=[netuid, tick_index_high], - ), - ) - - tao_fees_below_low = get_fees( - current_tick=current_tick, - tick=tick_low, - tick_index=tick_index_low, - quote=True, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=False, - ) - tao_fees_above_high = get_fees( - current_tick=current_tick, - tick=tick_high, - tick_index=tick_index_high, - quote=True, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=True, - ) - alpha_fees_below_low = get_fees( - current_tick=current_tick, - tick=tick_low, - tick_index=tick_index_low, - quote=False, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=False, - ) - alpha_fees_above_high = get_fees( - current_tick=current_tick, - tick=tick_high, - tick_index=tick_index_high, - quote=False, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - above=True, - ) - - # Get position accrued fees - fees_tao, fees_alpha = calculate_fees( - position=position, - global_fees_tao=fee_global_tao, - global_fees_alpha=fee_global_alpha, - tao_fees_below_low=tao_fees_below_low, - tao_fees_above_high=tao_fees_above_high, - alpha_fees_below_low=alpha_fees_below_low, - alpha_fees_above_high=alpha_fees_above_high, - netuid=netuid, - ) - - lp = LiquidityPosition( - **{ - "id": position.get("id")[0], - "price_low": Balance.from_tao( - tick_to_price(position.get("tick_low")[0]) - ), - "price_high": Balance.from_tao( - tick_to_price(position.get("tick_high")[0]) - ), - "liquidity": Balance.from_rao(position.get("liquidity")), - "fees_tao": fees_tao, - "fees_alpha": fees_alpha, - "netuid": position.get("netuid"), - } - ) - positions.append(lp) - - return positions - - -async def remove_liquidity( - subtensor: "SubtensorInterface", - wallet: "Wallet", - netuid: int, - position_id: Optional[int] = None, - prompt: Optional[bool] = None, - all_liquidity_ids: Optional[bool] = None, - json_output: bool = False, -) -> tuple[bool, str]: - """Remove liquidity position from provided subnet.""" - if not await subtensor.subnet_exists(netuid=netuid): - return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." - - if not position_id: - position_id = prompt_position_id() - - if prompt and not all_liquidity_ids: - console.print("You are about to remove a LiquidityPosition with:") - console.print(f"\tSubnet: {netuid}") - console.print(f"\tPosition id: {position_id}") - console.print(f"\tWallet name: {wallet.name}") - - if not Confirm.ask("Would you like to continue?"): - return False, "User cancelled operation." - - return await remove_liquidity_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - position_id=position_id, - ) - - -async def modify_liquidity( - subtensor: "SubtensorInterface", - wallet: "Wallet", - netuid: int, - position_id: int, - liquidity_delta: Optional[float], - prompt: Optional[bool] = None, - json_output: bool = False, -): - """Modify liquidity position in provided subnet.""" - if not await subtensor.subnet_exists(netuid=netuid): - return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." - - if not position_id: - position_id = prompt_position_id() - - if liquidity_delta: - liquidity_delta = Balance.from_tao(liquidity_delta) - else: - liquidity_delta = prompt_liquidity( - f"Enter the [blue]liquidity_delta[/blue] for modify position with id " - f"[blue]{position_id}[/blue] (could be positive or negative)", - negative_allowed=True, - ) - - if prompt: - console.print("You are about to modify a LiquidityPosition with:") - console.print(f"\tSubnet: {netuid}") - console.print(f"\tPosition id: {position_id}") - console.print(f"\tWallet name: {wallet.name}") - console.print(f"\tLiquidity delta: {liquidity_delta}") - - if not Confirm.ask("Would you like to continue?"): - return False, "User cancelled operation." - - return await modify_liquidity_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - position_id=position_id, - liquidity_delta=liquidity_delta, - ) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index d67457c71..ba31f1c17 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -1,8 +1,25 @@ -from typing import TYPE_CHECKING +import asyncio +import json +from typing import TYPE_CHECKING, Optional -from bittensor_cli.src.bittensor.utils import unlock_key -from bittensor_cli.src.bittensor.balances import Balance -from .utils import price_to_tick +from rich.prompt import Confirm +from rich.table import Column, Table + +from bittensor_cli.src import COLORS +from bittensor_cli.src.bittensor.utils import ( + unlock_key, + console, + err_console, + json_console, +) +from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float +from bittensor_cli.src.commands.liquidity.utils import ( + LiquidityPosition, + calculate_fees, + get_fees, + price_to_tick, + tick_to_price, +) if TYPE_CHECKING: from bittensor_wallet import Wallet @@ -201,3 +218,393 @@ async def toggle_user_liquidity_extrinsic( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + + +# Command +async def add_liquidity( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: Optional[int], + liquidity: Optional[float], + price_low: Optional[float], + price_high: Optional[float], + prompt: bool, + json_output: bool, +) -> tuple[bool, str]: + """Add liquidity position to provided subnet.""" + # Check wallet access + if not unlock_key(wallet).success: + return False + + # Check that the subnet exists. + if not await subtensor.subnet_exists(netuid=netuid): + return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." + + if prompt: + console.print( + "You are about to add a LiquidityPosition with:\n" + f"\tliquidity: {liquidity}\n" + f"\tprice low: {price_low}\n" + f"\tprice high: {price_high}\n" + f"\tto SN: {netuid}\n" + f"\tusing wallet with name: {wallet.name}" + ) + + if not Confirm.ask("Would you like to continue?"): + return False, "User cancelled operation." + + success, message = await add_liquidity_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + liquidity=liquidity, + price_low=price_low, + price_high=price_high, + ) + if json_output: + json_console.print(json.dumps({"success": success, "message": message})) + else: + if success: + console.print( + "[green]LiquidityPosition has been successfully added.[/green]" + ) + else: + err_console.print(f"[red]Error: {message}[/red]") + + +async def get_liquidity_list( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: Optional[int], +) -> tuple[bool, str, list]: + """ + Args: + wallet: wallet object + subtensor: SubtensorInterface object + netuid: the netuid to stake to (None indicates all subnets) + + Returns: + Tuple of (success, error message, liquidity list) + """ + + if not await subtensor.subnet_exists(netuid=netuid): + return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}.", [] + + if not await subtensor.is_subnet_active(netuid=netuid): + return False, f"Subnet with netuid: {netuid} is not active in {subtensor}.", [] + + block_hash = await subtensor.substrate.get_chain_head() + ( + positions_response, + fee_global_tao, + fee_global_alpha, + current_sqrt_price, + ) = await asyncio.gather( + subtensor.substrate.query_map( + module="Swap", + storage_function="Positions", + params=[netuid, wallet.coldkeypub.ss58_address], + block_hash=block_hash, + ), + subtensor.query( + module="Swap", + storage_function="FeeGlobalTao", + params=[netuid], + block_hash=block_hash, + ), + subtensor.query( + module="Swap", + storage_function="FeeGlobalAlpha", + params=[netuid], + block_hash=block_hash, + ), + subtensor.query( + module="Swap", + storage_function="AlphaSqrtPrice", + params=[netuid], + block_hash=block_hash, + ), + ) + + current_sqrt_price = fixed_to_float(current_sqrt_price) + fee_global_tao = fixed_to_float(fee_global_tao) + fee_global_alpha = fixed_to_float(fee_global_alpha) + + current_price = current_sqrt_price * current_sqrt_price + current_tick = price_to_tick(current_price) + + preprocessed_positions = [] + positions_futures = [] + + async for _, p in positions_response: + position = p.value + tick_index_low = position.get("tick_low")[0] + tick_index_high = position.get("tick_high")[0] + preprocessed_positions.append((position, tick_index_low, tick_index_high)) + + # Get ticks for the position (for below/above fees) + positions_futures.append( + asyncio.gather( + subtensor.query( + module="Swap", + storage_function="Ticks", + params=[netuid, tick_index_low], + block_hash=block_hash, + ), + subtensor.query( + module="Swap", + storage_function="Ticks", + params=[netuid, tick_index_high], + block_hash=block_hash, + ), + ) + ) + + awaited_futures = await asyncio.gather(*positions_futures) + + positions = [] + + for (position, tick_index_low, tick_index_high), (tick_low, tick_high) in zip( + preprocessed_positions, awaited_futures + ): + tao_fees_below_low = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_index_low, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + tao_fees_above_high = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_index_high, + quote=True, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + alpha_fees_below_low = get_fees( + current_tick=current_tick, + tick=tick_low, + tick_index=tick_index_low, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=False, + ) + alpha_fees_above_high = get_fees( + current_tick=current_tick, + tick=tick_high, + tick_index=tick_index_high, + quote=False, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + above=True, + ) + + # Get position accrued fees + fees_tao, fees_alpha = calculate_fees( + position=position, + global_fees_tao=fee_global_tao, + global_fees_alpha=fee_global_alpha, + tao_fees_below_low=tao_fees_below_low, + tao_fees_above_high=tao_fees_above_high, + alpha_fees_below_low=alpha_fees_below_low, + alpha_fees_above_high=alpha_fees_above_high, + netuid=netuid, + ) + + lp = LiquidityPosition( + **{ + "id": position.get("id")[0], + "price_low": Balance.from_tao( + tick_to_price(position.get("tick_low")[0]) + ), + "price_high": Balance.from_tao( + tick_to_price(position.get("tick_high")[0]) + ), + "liquidity": Balance.from_rao(position.get("liquidity")), + "fees_tao": fees_tao, + "fees_alpha": fees_alpha, + "netuid": position.get("netuid"), + } + ) + positions.append(lp) + + return True, "", positions + + +async def show_liquidity_list( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + json_output: bool = False, +): + current_price_, (success, err_msg, positions) = await asyncio.gather( + subtensor.subnet(netuid=netuid), get_liquidity_list(subtensor, wallet, netuid) + ) + if not success: + if json_output: + json_console.print( + json.dumps({"success": success, "err_msg": err_msg, "positions": []}) + ) + return False + else: + err_console.print(f"Error: {err_msg}") + return False + liquidity_table = Table( + Column("ID", justify="center"), + Column("Liquidity (Alpha and TAO part)", justify="center"), + Column("Price low", justify="center"), + Column("Price high", justify="center"), + Column("Fee TAO", justify="center"), + Column("Fee Alpha", justify="center"), + title=f"\n[{COLORS.G.HEADER}]{'Liquidity Positions of '}{wallet.name} wallet in SN #{netuid}\n", + show_footer=False, + show_edge=True, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + json_table = [] + current_price = current_price_.price + lp: LiquidityPosition + for lp in positions: + alpha, tao = lp.to_token_amounts(current_price) + liquidity_table.add_row( + str(lp.id), + f"{alpha} {tao}", + str(lp.price_low), + str(lp.price_high), + str(lp.fees_tao), + str(lp.fees_alpha), + ) + json_table.append( + { + "ID": lp.id, + "Alpha Liquidity": alpha.tao, + "Tao Liquidity": tao.tao, + "Price Low": lp.price_low.tao, + "Price High": lp.price_high.tao, + "Fee TAO": lp.fees_tao.tao, + "Fee Alpha": lp.fees_alpha.tao, + } + ) + if not json_output: + console.print(liquidity_table) + else: + json_console.print( + json.dumps({"success": True, "error": "", "positions": json_table}) + ) + + +async def remove_liquidity( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + position_id: Optional[int] = None, + prompt: Optional[bool] = None, + all_liquidity_ids: Optional[bool] = None, + json_output: bool = False, +) -> tuple[bool, str]: + """Remove liquidity position from provided subnet.""" + if not await subtensor.subnet_exists(netuid=netuid): + return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." + + if all_liquidity_ids: + success, msg, positions = await get_liquidity_list(subtensor, wallet, netuid) + if not success: + if json_output: + return json_console.print( + {"success": False, "err_msg": msg, "positions": positions} + ) + else: + return err_console.print(f"Error: {msg}") + else: + position_ids = [p.id for p in positions] + else: + position_ids = [position_id] + + if prompt: + console.print("You are about to remove LiquidityPositions with:") + console.print(f"\tSubnet: {netuid}") + console.print(f"\tWallet name: {wallet.name}") + for pos in position_ids: + console.print(f"\tPosition id: {pos}") + + if not Confirm.ask("Would you like to continue?"): + return False, "User cancelled operation." + + results = await asyncio.gather( + *[ + remove_liquidity_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + position_id=pos_id, + ) + for pos_id in position_ids + ] + ) + if not json_output: + for (success, msg), posid in zip(results, position_ids): + if success: + console.print(f"[green] Position {posid} has been removed.") + else: + err_console.print(f"[red] Error removing {posid}: {msg}") + else: + json_table = {} + for (success, msg), posid in zip(results, position_ids): + json_table[posid] = {"success": success, "err_msg": msg} + json_console.print(json.dumps(json_table)) + + +async def modify_liquidity( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + position_id: int, + liquidity_delta: Optional[float], + prompt: Optional[bool] = None, + json_output: bool = False, +) -> bool: + """Modify liquidity position in provided subnet.""" + if not await subtensor.subnet_exists(netuid=netuid): + err_msg = f"Subnet with netuid: {netuid} does not exist in {subtensor}." + if json_output: + json_console.print(json.dumps({"success": False, "err_msg": err_msg})) + else: + err_console.print(err_msg) + return False + + if prompt: + console.print( + "You are about to modify a LiquidityPosition with:" + f"\tSubnet: {netuid}\n" + f"\tPosition id: {position_id}\n" + f"\tWallet name: {wallet.name}\n" + f"\tLiquidity delta: {liquidity_delta}" + ) + + if not Confirm.ask("Would you like to continue?"): + return False + + success, msg = await modify_liquidity_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + position_id=position_id, + liquidity_delta=liquidity_delta, + ) + if json_output: + json_console.print(json.dumps({"success": success, "err_msg": msg})) + else: + if success: + console.print(f"[green] Position {position_id} has been modified.") + else: + err_console.print(f"[red] Error modifying {position_id}: {msg}") diff --git a/bittensor_cli/src/commands/liquidity/utils.py b/bittensor_cli/src/commands/liquidity/utils.py index c4507060c..76f7ea8a7 100644 --- a/bittensor_cli/src/commands/liquidity/utils.py +++ b/bittensor_cli/src/commands/liquidity/utils.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from typing import Any -from rich.prompt import Prompt +from rich.prompt import IntPrompt, FloatPrompt from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float from bittensor_cli.src.bittensor.utils import ( @@ -176,9 +176,8 @@ def prompt_liquidity(prompt: str, negative_allowed: bool = False) -> Balance: Balance converted from input to TAO. """ while True: - input_ = Prompt.ask(prompt) + amount = FloatPrompt.ask(prompt) try: - amount = float(input_) if amount <= 0 and not negative_allowed: console.print("[red]Amount must be greater than 0[/red].") continue @@ -190,10 +189,9 @@ def prompt_liquidity(prompt: str, negative_allowed: bool = False) -> Balance: def prompt_position_id() -> int: """Ask the user for the ID of the liquidity position to remove.""" while True: - position_id = Prompt.ask(f"Enter the [blue]liquidity position ID[/blue]") + position_id = IntPrompt.ask("Enter the [blue]liquidity position ID[/blue]") try: - position_id = int(position_id) if position_id <= 1: console.print("[red]Position ID must be greater than 1[/red].") continue diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 152cf7591..03913ed82 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -537,8 +537,8 @@ def test_staking(local_chain, wallet_setup): json.loads(changed_yuma3_hyperparam.stdout), ) ) - assert yuma3_val["value"] is 3 - assert yuma3_val["normalized_value"] is 3 + assert yuma3_val["value"] == 3 + assert yuma3_val["normalized_value"] == 3 print("✅ Passed staking and sudo commands") change_arbitrary_hyperparam = exec_command_alice( From b01d9972d4e7f2f3a4802c74bd6300d72cf8d338 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Jul 2025 19:36:05 +0200 Subject: [PATCH 36/48] WIP tests --- bittensor_cli/cli.py | 2 +- .../src/commands/liquidity/liquidity.py | 17 +- tests/e2e_tests/test_liquidity.py | 181 ++++++++++++++++++ tests/e2e_tests/test_staking_sudo.py | 2 +- 4 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 tests/e2e_tests/test_liquidity.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 65295d71e..575b3db4e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -986,7 +986,7 @@ def __init__(self): # Liquidity self.app.add_typer( - self.utils_app, + self.liquidity_app, name="liquidity", short_help="liquidity commands, aliases: `l`", no_args_is_help=True, diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index ba31f1c17..8d2e94d17 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -486,20 +486,21 @@ async def show_liquidity_list( ) json_table.append( { - "ID": lp.id, - "Alpha Liquidity": alpha.tao, - "Tao Liquidity": tao.tao, - "Price Low": lp.price_low.tao, - "Price High": lp.price_high.tao, - "Fee TAO": lp.fees_tao.tao, - "Fee Alpha": lp.fees_alpha.tao, + "id": lp.id, + "alpha_liquidity": alpha.tao, + "tao_liquidity": tao.tao, + "price_low": lp.price_low.tao, + "price_high": lp.price_high.tao, + "fees_tao": lp.fees_tao.tao, + "fees_alpha": lp.fees_alpha.tao, + "netuid": lp.netuid, } ) if not json_output: console.print(liquidity_table) else: json_console.print( - json.dumps({"success": True, "error": "", "positions": json_table}) + json.dumps({"success": True, "err_msg": "", "positions": json_table}) ) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py new file mode 100644 index 000000000..84cea7492 --- /dev/null +++ b/tests/e2e_tests/test_liquidity.py @@ -0,0 +1,181 @@ +import json +import re + +from bittensor_cli.src.bittensor.balances import Balance + +""" +Verify commands: + +* btcli liquidity add +* btcli liquidity list +* btcli liquidity modify +* btcli liquidity remove +""" + + +def test_liquidity(local_chain, wallet_setup): + def liquidity_list(): + return exec_command_alice( + command="liquidity", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--json-output", + ], + ) + + wallet_path_alice = "//Alice" + netuid = 2 + + # Create wallet for Alice + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + + # Register a subnet with sudo as Alice + result = exec_command_alice( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--subnet-name", + "Test Subnet", + "--repo", + "https://github.com/username/repo", + "--contact", + "alice@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "alice#1234", + "--description", + "A test subnet for e2e testing", + "--additional-info", + "Created by Alice", + "--logo-url", + "https://testsubnet.com/logo.png", + "--no-prompt", + "--json-output", + ], + ) + result_output = json.loads(result.stdout) + assert result_output["success"] is True + assert result_output["netuid"] == netuid + + # verify no results for list thus far (subnet not yet started) + liquidity_list_result = liquidity_list() + result_output = json.loads(liquidity_list_result.stdout) + assert result_output["success"] is False + assert f"Subnet with netuid: {netuid} is not active" in result_output["err_msg"] + assert result_output["positions"] == [] + + # start emissions schedule + start_subnet_emissions = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + netuid, + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--network", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert ( + f"Successfully started subnet {netuid}'s emission schedule" + in start_subnet_emissions.stdout + ), start_subnet_emissions.stderr + + liquidity_list_result = liquidity_list() + result_output = json.loads(liquidity_list_result.stdout) + assert result_output["success"] is True + assert result_output["err_msg"] == "" + assert result_output["positions"] == [] + + enable_user_liquidity = exec_command_alice( + command="sudo", + sub_command="set", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--param", + "user_liquidity_enabled", + "--value", + "1", + "--json-output", + "--no-prompt", + ], + ) + enable_user_liquidity_result = json.loads(enable_user_liquidity.stdout) + assert enable_user_liquidity_result["success"] is True + + add_liquidity = exec_command_alice( + command="liquidity", + sub_command="add", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--liquidity", + "1.0", + "--price-low", + "1.7", + "--price-high", + "1.8", + "--no-prompt", + "--json-output", + ], + ) + add_liquidity_result = json.loads(add_liquidity.stdout) + assert add_liquidity_result["success"] is True + assert add_liquidity_result["message"] == "" + + liquidity_list_result = liquidity_list() + liquidity_list_result = json.loads(liquidity_list_result.stdout) + assert liquidity_list_result["success"] is True + assert len(liquidity_list_result["positions"]) == 1 + liquidity_position = liquidity_list_result["positions"][0] + assert liquidity_position["alpha_liquidity"] == 1.0 + assert liquidity_position["id"] == 2 + assert liquidity_position["fees_tao"] == 0.0 + assert liquidity_position["fees_alpha"] == 0.0 + assert liquidity_position["netuid"] == netuid + assert abs(liquidity_position["price_high"] - 1.8) < 0.1 + assert abs(liquidity_position["price_low"] - 1.7) < 0.1 diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 03913ed82..3225b9e6e 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -133,7 +133,7 @@ def test_staking(local_chain, wallet_setup): assert result_output_second["success"] is True assert result_output_second["netuid"] == multiple_netuids[1] - # Register Alice in netuid = 1 using her hotkey + # Register Alice in netuid = 2 using her hotkey register_subnet = exec_command_alice( command="subnets", sub_command="register", From 7c5d9e120241c9013fe83b72843006ec5a0abfad Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Jul 2025 20:53:38 +0200 Subject: [PATCH 37/48] Updated tests + list output --- .../src/commands/liquidity/liquidity.py | 12 ++-- tests/e2e_tests/test_liquidity.py | 63 ++++++++++++++++++- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index 8d2e94d17..a5555fb41 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -456,7 +456,9 @@ async def show_liquidity_list( return False liquidity_table = Table( Column("ID", justify="center"), - Column("Liquidity (Alpha and TAO part)", justify="center"), + Column("Liquidity", justify="center"), + Column("Alpha", justify="center"), + Column("Tao", justify="center"), Column("Price low", justify="center"), Column("Price high", justify="center"), Column("Fee TAO", justify="center"), @@ -478,7 +480,9 @@ async def show_liquidity_list( alpha, tao = lp.to_token_amounts(current_price) liquidity_table.add_row( str(lp.id), - f"{alpha} {tao}", + str(lp.liquidity), + str(alpha), + str(tao), str(lp.price_low), str(lp.price_high), str(lp.fees_tao), @@ -487,8 +491,8 @@ async def show_liquidity_list( json_table.append( { "id": lp.id, - "alpha_liquidity": alpha.tao, - "tao_liquidity": tao.tao, + "liquidity": lp.liquidity.tao, + "token_amounts": {"alpha": alpha.tao, "tao": tao.tao}, "price_low": lp.price_low.tao, "price_high": lp.price_high.tao, "fees_tao": lp.fees_tao.tao, diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 84cea7492..c8a7b7d4c 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -172,10 +172,67 @@ def liquidity_list(): assert liquidity_list_result["success"] is True assert len(liquidity_list_result["positions"]) == 1 liquidity_position = liquidity_list_result["positions"][0] - assert liquidity_position["alpha_liquidity"] == 1.0 + assert liquidity_position["liquidity"] == 1.0 assert liquidity_position["id"] == 2 assert liquidity_position["fees_tao"] == 0.0 assert liquidity_position["fees_alpha"] == 0.0 assert liquidity_position["netuid"] == netuid - assert abs(liquidity_position["price_high"] - 1.8) < 0.1 - assert abs(liquidity_position["price_low"] - 1.7) < 0.1 + assert abs(liquidity_position["price_high"] - 1.8) < 0.0001 + assert abs(liquidity_position["price_low"] - 1.7) < 0.0001 + + modify_liquidity = exec_command_alice( + command="liquidity", + sub_command="modify", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--position-id", + str(liquidity_position["id"]), + "--liquidity-delta", + "20.0", + "--json-output", + "--no-prompt", + ], + ) + modify_liquidity_result = json.loads(modify_liquidity.stdout) + assert modify_liquidity_result["success"] is True + + liquidity_list_result = json.loads(liquidity_list().stdout) + assert len(liquidity_list_result["positions"]) == 1 + liquidity_position = liquidity_list_result["positions"][0] + assert liquidity_position["id"] == 2 + assert liquidity_position["liquidity"] == 21.0 + + removal = exec_command_alice( + command="liquidity", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--all", + "--no-prompt", + "--json-output", + ], + ) + removal_result = json.loads(removal.stdout) + assert removal_result[str(liquidity_position["id"])]["success"] is True + + liquidity_list_result = json.loads(liquidity_list().stdout) + assert liquidity_list_result["success"] is True + assert liquidity_list_result["positions"] == [] From 5cce337482092571e922dfc3bb2172cfe3cf2ece Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Jul 2025 21:01:35 +0200 Subject: [PATCH 38/48] Updated title. --- bittensor_cli/src/commands/liquidity/liquidity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index a5555fb41..758b3da18 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -463,7 +463,8 @@ async def show_liquidity_list( Column("Price high", justify="center"), Column("Fee TAO", justify="center"), Column("Fee Alpha", justify="center"), - title=f"\n[{COLORS.G.HEADER}]{'Liquidity Positions of '}{wallet.name} wallet in SN #{netuid}\n", + title=f"\n[{COLORS.G.HEADER}]{'Liquidity Positions of '}{wallet.name} wallet in SN #{netuid}\n" + "Alpha and Tao columns are respective portions of liquidity.", show_footer=False, show_edge=True, header_style="bold white", From 7ba1ce58a485aded6d93982c9e440e2df0ad9ac3 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 7 Jul 2025 12:16:04 -0700 Subject: [PATCH 39/48] update add_stake_limit --- bittensor_cli/src/commands/stake/add.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 18f3517c1..a8d1ecc59 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -342,14 +342,14 @@ async def stake_extrinsic( # If we are staking safe, add price tolerance if safe_staking: if subnet_info.is_dynamic: - rate = 1 / subnet_info.price.tao or 1 + rate = amount_to_stake.rao / received_amount.rao _rate_with_tolerance = rate * ( 1 + rate_tolerance ) # Rate only for display rate_with_tolerance = f"{_rate_with_tolerance:.4f}" - price_with_tolerance = subnet_info.price.rao * ( - 1 + rate_tolerance - ) # Actual price to pass to extrinsic + price_with_tolerance = Balance.from_tao( + _rate_with_tolerance + ).rao # Actual price to pass to extrinsic else: rate_with_tolerance = "1" price_with_tolerance = Balance.from_rao(1) From 0904784e97168481b4762968ebfae9ab2b53f16d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 7 Jul 2025 12:16:14 -0700 Subject: [PATCH 40/48] update_remove_stake_limit --- bittensor_cli/src/commands/stake/remove.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index c7a72ffed..d562c1e1b 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -248,13 +248,13 @@ async def unstake( # Additional fields for safe unstaking if safe_staking: if subnet_info.is_dynamic: - rate = subnet_info.price.tao or 1 + rate = amount_to_unstake_as_balance.rao / received_amount.rao rate_with_tolerance = rate * ( 1 - rate_tolerance ) # Rate only for display - price_with_tolerance = subnet_info.price.rao * ( - 1 - rate_tolerance - ) # Actual price to pass to extrinsic + price_with_tolerance = Balance.from_tao( + rate_with_tolerance + ).rao # Actual price to pass to extrinsic else: rate_with_tolerance = 1 price_with_tolerance = 1 From f37ecaee1206975ceb9ed9987c2b57205215a7cd Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Jul 2025 21:21:44 +0200 Subject: [PATCH 41/48] Why was this changed? --- bittensor_cli/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 575b3db4e..fd7718c26 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -121,7 +121,7 @@ def edit_help(cls, option_name: str, help_text: str): help="Name of the wallet.", ) wallet_path = typer.Option( - "default", + None, "--wallet-path", "-p", "--wallet_path", @@ -129,7 +129,7 @@ def edit_help(cls, option_name: str, help_text: str): help="Path where the wallets are located. For example: `/Users/btuser/.bittensor/wallets`.", ) wallet_hotkey = typer.Option( - "default", + None, "--hotkey", "-H", "--wallet_hotkey", From 10015571627e15e7742c9408ef34b1f66b58c36e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Jul 2025 22:03:12 +0200 Subject: [PATCH 42/48] Liquidity not measured in tao. --- bittensor_cli/src/commands/liquidity/liquidity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index 758b3da18..36c7f1075 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -481,7 +481,7 @@ async def show_liquidity_list( alpha, tao = lp.to_token_amounts(current_price) liquidity_table.add_row( str(lp.id), - str(lp.liquidity), + str(lp.liquidity.tao), str(alpha), str(tao), str(lp.price_low), From 5ee38d7ae1e46cc199fa3af91e66083d6989a14e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Jul 2025 22:33:51 +0200 Subject: [PATCH 43/48] I'm not proud of this code --- bittensor_cli/cli.py | 62 +++++++++++++------ .../src/commands/liquidity/liquidity.py | 17 ++++- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index fd7718c26..7e15afd15 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -12,7 +12,7 @@ import warnings from dataclasses import fields from pathlib import Path -from typing import Coroutine, Optional +from typing import Coroutine, Optional, Union import numpy as np import rich @@ -1639,7 +1639,8 @@ def wallet_ask( wallet_hotkey: Optional[str], ask_for: Optional[list[str]] = None, validate: WV = WV.WALLET, - ) -> Wallet: + return_wallet_and_hotkey: bool = False, + ) -> Union[Wallet, tuple[Wallet, str]]: """ Generates a wallet object based on supplied values, validating the wallet is valid if flag is set :param wallet_name: name of the wallet @@ -1647,7 +1648,8 @@ def wallet_ask( :param wallet_hotkey: name of the wallet hotkey file :param validate: flag whether to check for the wallet's validity :param ask_for: aspect of the wallet (name, path, hotkey) to prompt the user for - :return: created Wallet object + :param return_wallet_and_hotkey: if specified, will return both the wallet object, and the hotkey SS58 + :return: created Wallet object (or wallet, hotkey ss58) """ ask_for = ask_for or [] # Prompt for missing attributes specified in ask_for @@ -1672,8 +1674,9 @@ def wallet_ask( ) else: wallet_hotkey = Prompt.ask( - "Enter the [blue]wallet hotkey[/blue]" - + " [dark_sea_green3 italic](Hint: You can set this with `btcli config set --wallet-hotkey`)[/dark_sea_green3 italic]", + "Enter the [blue]wallet hotkey[/blue][dark_sea_green3 italic]" + "(Hint: You can set this with `btcli config set --wallet-hotkey`)" + "[/dark_sea_green3 italic]", default=defaults.wallet.hotkey, ) if wallet_path: @@ -1691,7 +1694,8 @@ def wallet_ask( if WO.PATH in ask_for and not wallet_path: wallet_path = Prompt.ask( "Enter the [blue]wallet path[/blue]" - + " [dark_sea_green3 italic](Hint: You can set this with `btcli config set --wallet-path`)[/dark_sea_green3 italic]", + "[dark_sea_green3 italic](Hint: You can set this with `btcli config set --wallet-path`)" + "[/dark_sea_green3 italic]", default=defaults.wallet.path, ) # Create the Wallet object @@ -1715,7 +1719,25 @@ def wallet_ask( f"Please verify your wallet information: {wallet}[/red]" ) raise typer.Exit() - return wallet + if return_wallet_and_hotkey: + valid = utils.is_valid_wallet(wallet) + if valid[1]: + return wallet, wallet.hotkey.ss58_address + else: + hotkey = ( + Prompt.ask( + "Enter the SS58 of the hotkey to use for this transaction." + ) + ).strip() + if not is_valid_ss58_address(hotkey): + err_console.print( + f"[red]Error: {hotkey} is not valid SS58 address." + ) + raise typer.Exit(1) + else: + return wallet, hotkey + else: + return wallet def wallet_list( self, @@ -5834,18 +5856,13 @@ def liquidity_add( show_default=False, ) - if not wallet_name: - wallet_name = Prompt.ask( - "Enter the [blue]wallet name[/blue]", - default=self.config.get("wallet_name") or defaults.wallet.name, - ) - - wallet = self.wallet_ask( + wallet, hotkey = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, wallet_hotkey=wallet_hotkey, ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], - validate=WV.WALLET_AND_HOTKEY, + validate=WV.WALLET, + return_wallet_and_hotkey=True, ) # Determine the liquidity amount. if liquidity_: @@ -5874,6 +5891,7 @@ def liquidity_add( liquidity.add_liquidity( subtensor=self.initialize_chain(network), wallet=wallet, + hotkey_ss58=hotkey, netuid=netuid, liquidity=liquidity_, price_low=price_low, @@ -5908,7 +5926,7 @@ def liquidity_list( wallet_path=wallet_path, wallet_hotkey=wallet_hotkey, ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], - validate=WV.WALLET_AND_HOTKEY, + validate=WV.WALLET, ) self._run_command( liquidity.show_liquidity_list( @@ -5961,17 +5979,19 @@ def liquidity_remove( show_default=False, ) - wallet = self.wallet_ask( + wallet, hotkey = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, wallet_hotkey=wallet_hotkey, ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], - validate=WV.WALLET_AND_HOTKEY, + validate=WV.WALLET, + return_wallet_and_hotkey=True, ) return self._run_command( liquidity.remove_liquidity( subtensor=self.initialize_chain(network), wallet=wallet, + hotkey_ss58=hotkey, netuid=netuid, position_id=position_id, prompt=prompt, @@ -6011,12 +6031,13 @@ def liquidity_modify( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", ) - wallet = self.wallet_ask( + wallet, hotkey = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, wallet_hotkey=wallet_hotkey, ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], - validate=WV.WALLET_AND_HOTKEY, + validate=WV.WALLET, + return_wallet_and_hotkey=True, ) if not position_id: @@ -6035,6 +6056,7 @@ def liquidity_modify( liquidity.modify_liquidity( subtensor=self.initialize_chain(network), wallet=wallet, + hotkey_ss58=hotkey, netuid=netuid, position_id=position_id, liquidity_delta=liquidity_delta, diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index 36c7f1075..5da2259c5 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -29,6 +29,7 @@ async def add_liquidity_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", + hotkey_ss58: str, netuid: int, liquidity: Balance, price_low: Balance, @@ -42,6 +43,7 @@ async def add_liquidity_extrinsic( Arguments: subtensor: The Subtensor client instance used for blockchain interaction. wallet: The wallet used to sign the extrinsic (must be unlocked). + hotkey_ss58: the SS58 of the hotkey to use for this transaction. netuid: The UID of the target subnet for which the call is being initiated. liquidity: The amount of liquidity to be added. price_low: The lower bound of the price tick range. @@ -67,7 +69,7 @@ async def add_liquidity_extrinsic( call_module="Swap", call_function="add_liquidity", call_params={ - "hotkey": wallet.hotkey.ss58_address, + "hotkey": hotkey_ss58, "netuid": netuid, "tick_low": tick_low, "tick_high": tick_high, @@ -86,6 +88,7 @@ async def add_liquidity_extrinsic( async def modify_liquidity_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", + hotkey_ss58: str, netuid: int, position_id: int, liquidity_delta: Balance, @@ -97,6 +100,7 @@ async def modify_liquidity_extrinsic( Arguments: subtensor: The Subtensor client instance used for blockchain interaction. wallet: The wallet used to sign the extrinsic (must be unlocked). + hotkey_ss58: the SS58 of the hotkey to use for this transaction. netuid: The UID of the target subnet for which the call is being initiated. position_id: The id of the position record in the pool. liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). @@ -118,7 +122,7 @@ async def modify_liquidity_extrinsic( call_module="Swap", call_function="modify_position", call_params={ - "hotkey": wallet.hotkey.ss58_address, + "hotkey": hotkey_ss58, "netuid": netuid, "position_id": position_id, "liquidity_delta": liquidity_delta.rao, @@ -136,6 +140,7 @@ async def modify_liquidity_extrinsic( async def remove_liquidity_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", + hotkey_ss58: str, netuid: int, position_id: int, wait_for_inclusion: bool = True, @@ -146,6 +151,7 @@ async def remove_liquidity_extrinsic( Arguments: subtensor: The Subtensor client instance used for blockchain interaction. wallet: The wallet used to sign the extrinsic (must be unlocked). + hotkey_ss58: the SS58 of the hotkey to use for this transaction. netuid: The UID of the target subnet for which the call is being initiated. position_id: The id of the position record in the pool. wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. @@ -166,7 +172,7 @@ async def remove_liquidity_extrinsic( call_module="Swap", call_function="remove_liquidity", call_params={ - "hotkey": wallet.hotkey.ss58_address, + "hotkey": hotkey_ss58, "netuid": netuid, "position_id": position_id, }, @@ -224,6 +230,7 @@ async def toggle_user_liquidity_extrinsic( async def add_liquidity( subtensor: "SubtensorInterface", wallet: "Wallet", + hotkey_ss58: str, netuid: Optional[int], liquidity: Optional[float], price_low: Optional[float], @@ -512,6 +519,7 @@ async def show_liquidity_list( async def remove_liquidity( subtensor: "SubtensorInterface", wallet: "Wallet", + hotkey_ss58: str, netuid: int, position_id: Optional[int] = None, prompt: Optional[bool] = None, @@ -551,6 +559,7 @@ async def remove_liquidity( remove_liquidity_extrinsic( subtensor=subtensor, wallet=wallet, + hotkey_ss58=hotkey_ss58, netuid=netuid, position_id=pos_id, ) @@ -573,6 +582,7 @@ async def remove_liquidity( async def modify_liquidity( subtensor: "SubtensorInterface", wallet: "Wallet", + hotkey_ss58: str, netuid: int, position_id: int, liquidity_delta: Optional[float], @@ -603,6 +613,7 @@ async def modify_liquidity( success, msg = await modify_liquidity_extrinsic( subtensor=subtensor, wallet=wallet, + hotkey_ss58=hotkey_ss58, netuid=netuid, position_id=position_id, liquidity_delta=liquidity_delta, From faa201d43b8c3c2213334962a82934188944c443 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Jul 2025 22:46:09 +0200 Subject: [PATCH 44/48] Missed arg --- bittensor_cli/src/commands/liquidity/liquidity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index 5da2259c5..60f5c6529 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -263,6 +263,7 @@ async def add_liquidity( success, message = await add_liquidity_extrinsic( subtensor=subtensor, wallet=wallet, + hotkey_ss58=hotkey_ss58, netuid=netuid, liquidity=liquidity, price_low=price_low, From 9f011dc9ed33e36a9078c39ec334bde04835fd9c Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Jul 2025 22:46:18 +0200 Subject: [PATCH 45/48] Retry for incorect password --- bittensor_cli/src/bittensor/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 3a53c946b..6ac75cee0 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1379,6 +1379,7 @@ def unlock_key( err_msg = f"The password used to decrypt your {unlock_type.capitalize()}key Keyfile is invalid." if print_out: err_console.print(f":cross_mark: [red]{err_msg}[/red]") + return unlock_key(wallet, unlock_type, print_out) return UnlockStatus(False, err_msg) except KeyFileError: err_msg = f"{unlock_type.capitalize()}key Keyfile is corrupt, non-writable, or non-readable, or non-existent." From 4de77c83a47d1d053ce737d997cdb276c69fe169 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Jul 2025 23:04:52 +0200 Subject: [PATCH 46/48] Small fixes --- bittensor_cli/cli.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7e15afd15..9128f83fe 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1724,18 +1724,21 @@ def wallet_ask( if valid[1]: return wallet, wallet.hotkey.ss58_address else: - hotkey = ( - Prompt.ask( - "Enter the SS58 of the hotkey to use for this transaction." - ) - ).strip() - if not is_valid_ss58_address(hotkey): - err_console.print( - f"[red]Error: {hotkey} is not valid SS58 address." - ) - raise typer.Exit(1) + if wallet_hotkey and is_valid_ss58_address(wallet_hotkey): + return wallet, wallet_hotkey else: - return wallet, hotkey + hotkey = ( + Prompt.ask( + "Enter the SS58 of the hotkey to use for this transaction." + ) + ).strip() + if not is_valid_ss58_address(hotkey): + err_console.print( + f"[red]Error: {hotkey} is not valid SS58 address." + ) + raise typer.Exit(1) + else: + return wallet, hotkey else: return wallet @@ -5925,7 +5928,7 @@ def liquidity_list( wallet_name=wallet_name, wallet_path=wallet_path, wallet_hotkey=wallet_hotkey, - ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], + ask_for=[WO.NAME, WO.PATH], validate=WV.WALLET, ) self._run_command( From 4280071b7cb43bfd40826876eb3dd18af1099426 Mon Sep 17 00:00:00 2001 From: Ibraheem <165814940+ibraheem-abe@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:02:17 -0700 Subject: [PATCH 47/48] Update bittensor_cli/src/commands/stake/remove.py Co-authored-by: Cameron Fairchild --- bittensor_cli/src/commands/stake/remove.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index d562c1e1b..b2ef8b608 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -248,7 +248,7 @@ async def unstake( # Additional fields for safe unstaking if safe_staking: if subnet_info.is_dynamic: - rate = amount_to_unstake_as_balance.rao / received_amount.rao + rate = received_amount.rao / amount_to_unstake_as_balance.rao rate_with_tolerance = rate * ( 1 - rate_tolerance ) # Rate only for display From 7903d8678717a71870e74f8eef35577de22e0b83 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 7 Jul 2025 16:41:50 -0700 Subject: [PATCH 48/48] Updates version and changelog --- CHANGELOG.md | 16 ++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f06cebcbe..cded2eb43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 9.8.0/2025-07-07 + +## What's Changed +* Feat/logo urls in subnet identity by @ibraheem-abe in https://github.com/opentensor/btcli/pull/504 +* Feat/swap hotkey with netuids by @ibraheem-abe in https://github.com/opentensor/btcli/pull/503 +* Backmerge main staging by @ibraheem-abe in https://github.com/opentensor/btcli/pull/508 +* Ensures network local is used if forgotten in e2e tests by @thewhaleking in https://github.com/opentensor/btcli/pull/497 +* Convert hyperparams from strings by @thewhaleking in https://github.com/opentensor/btcli/pull/510 +* Ensure we parse strings for param names by @thewhaleking in https://github.com/opentensor/btcli/pull/511 +* add snake case aliases by @thewhaleking in https://github.com/opentensor/btcli/pull/514 +* Better checks the swap status by @thewhaleking in https://github.com/opentensor/btcli/pull/485 +* Integrate Liquidity Provider feature by @basfroman in https://github.com/opentensor/btcli/pull/515 +* Updates safe staking/unstaking limits by @ibraheem-abe in https://github.com/opentensor/btcli/pull/519 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.7.1...v9.8.0 + ## 9.7.1/2025-06-26 ## What's Changed diff --git a/pyproject.toml b/pyproject.toml index ff7a019e3..d733a105e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.7.1" +version = "9.8.0" description = "Bittensor CLI" readme = "README.md" authors = [