From 3084d048fa3e6e5667c8158e3e38629a036f586f Mon Sep 17 00:00:00 2001 From: leonace924 Date: Tue, 23 Dec 2025 05:43:48 -0500 Subject: [PATCH 1/6] feat: add --all to proxy remove --- bittensor_cli/cli.py | 120 +++++++++++++++++++++++++++++++++---------- 1 file changed, 92 insertions(+), 28 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 16e68c25..2543599a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -9449,13 +9449,18 @@ def proxy_add( def proxy_remove( self, delegate: Annotated[ - str, + Optional[str], typer.Option( callback=is_valid_ss58_address_param, - prompt="Enter the SS58 address of the delegate to remove, e.g. 5dxds...", - help="The SS58 address of the delegate to remove", + prompt=False, + help="The SS58 address of the delegate to remove (required if --all is not used)", ), - ] = "", + ] = None, + all: bool = typer.Option( + False, + "--all", + help="Remove all proxies associated with this account", + ), network: Optional[list[str]] = Options.network, proxy_type: ProxyType = Options.proxy_type, delay: int = typer.Option(0, help="Delay, in number of blocks"), @@ -9487,10 +9492,10 @@ def proxy_remove( [green]$[/green] btcli proxy remove --all """ - # TODO should add a --all flag to call Proxy.remove_proxies ? logger.debug( "args:\n" f"delegate: {delegate}\n" + f"all: {all}\n" f"network: {network}\n" f"proxy_type: {proxy_type}\n" f"delay: {delay}\n" @@ -9498,30 +9503,89 @@ def proxy_remove( f"wait_for_inclusion: {wait_for_inclusion}\n" f"era: {period}\n" ) - self.verbosity_handler(quiet, verbose, json_output, prompt) - wallet = self.wallet_ask( - wallet_name=wallet_name, - wallet_path=wallet_path, - wallet_hotkey=wallet_hotkey, - ask_for=[WO.NAME, WO.PATH], - validate=WV.WALLET, - ) - return self._run_command( - proxy_commands.remove_proxy( - subtensor=self.initialize_chain(network), - wallet=wallet, - delegate=delegate, - proxy_type=proxy_type, - delay=delay, - prompt=prompt, - decline=decline, - quiet=quiet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - period=period, - json_output=json_output, + # Validate that either delegate is provided or --all is used + if not all and not delegate: + if not json_output: + print_error( + "Either --delegate must be provided or --all flag must be used." + ) + else: + json_console.print_json( + data={ + "success": False, + "message": "Either --delegate must be provided or --all flag must be used.", + "extrinsic_identifier": None, + } + ) + return + + # If --all is used, delegate and proxy_type/delay should not be required + if all: + if delegate: + if not json_output: + console.print( + "[yellow]Warning: --delegate is ignored when --all flag is used.[/yellow]" + ) + self.verbosity_handler(quiet, verbose, json_output, prompt) + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + return self._run_command( + proxy_commands.remove_proxies( + subtensor=self.initialize_chain(network), + wallet=wallet, + prompt=prompt, + decline=decline, + quiet=quiet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + json_output=json_output, + ) + ) + else: + # Single proxy removal - delegate is required + if not delegate: + if not json_output: + print_error("--delegate is required when --all flag is not used.") + else: + json_console.print_json( + data={ + "success": False, + "message": "--delegate is required when --all flag is not used.", + "extrinsic_identifier": None, + } + ) + return + + self.verbosity_handler(quiet, verbose, json_output, prompt) + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + return self._run_command( + proxy_commands.remove_proxy( + subtensor=self.initialize_chain(network), + wallet=wallet, + delegate=delegate, + proxy_type=proxy_type, + delay=delay, + prompt=prompt, + decline=decline, + quiet=quiet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + json_output=json_output, + ) ) - ) def proxy_kill( self, From b9b5ff3ba0512179af22b67a60b6740b706b081e Mon Sep 17 00:00:00 2001 From: leonace924 Date: Tue, 23 Dec 2025 05:49:57 -0500 Subject: [PATCH 2/6] feat: add remove_proxies function --- bittensor_cli/src/commands/proxy.py | 53 ++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 8852fedf..adb938ba 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -271,7 +271,7 @@ async def remove_proxy( """ if prompt: if not confirm_action( - f"This will remove a proxy of type {proxy_type.value} for delegate {delegate}." + f"This will remove a proxy of type {proxy_type.value} for delegate {delegate}. " f"Do you want to proceed?", decline=decline, quiet=quiet, @@ -309,6 +309,57 @@ async def remove_proxy( ) +async def remove_proxies( + subtensor: "SubtensorInterface", + wallet: "Wallet", + prompt: bool, + decline: bool, + quiet: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, +) -> None: + """ + Executes the remove_proxies call on the chain to remove all proxies for the account + """ + if prompt: + confirmation = Prompt.ask( + "[red]WARNING:[/red] This will remove ALL proxies associated with this account.\n" + "[red]All proxy relationships will be permanently lost.[/red]\n" + "To proceed, enter [red]REMOVE[/red]" + ) + if confirmation != "REMOVE": + print_error("Invalid input. Operation cancelled.") + return None + if not (ulw := unlock_key(wallet, print_out=not json_output)).success: + if not json_output: + print_error(ulw.message) + else: + json_console.print_json( + data={ + "success": ulw.success, + "message": ulw.message, + "extrinsic_identifier": None, + } + ) + return None + call = await subtensor.substrate.compose_call( + call_module="Proxy", + call_function="remove_proxies", + call_params={}, + ) + return await submit_proxy( + subtensor=subtensor, + wallet=wallet, + call=call, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + json_output=json_output, + ) + + async def add_proxy( subtensor: "SubtensorInterface", wallet: "Wallet", From e6af72b5a8a3f4ba948348cb09c9738867d6ce08 Mon Sep 17 00:00:00 2001 From: leonace924 Date: Tue, 23 Dec 2025 05:53:09 -0500 Subject: [PATCH 3/6] feat: add remove proxy e2e test --- tests/e2e_tests/test_proxy.py | 712 ++++++++++++++++++++++++++++++++++ 1 file changed, 712 insertions(+) diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index e5e76724..63bce215 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -684,3 +684,715 @@ def test_add_proxy(local_chain, wallet_setup): os.environ["BTCLI_PROXIES_PATH"] = "" if os.path.exists(testing_db_loc): os.remove(testing_db_loc) + + +def test_remove_all_proxies(local_chain, wallet_setup): + """ + Tests removing all proxies using the --all flag + + Steps: + 1. Add multiple proxies (Dave and Bob as proxies of Alice) + 2. Verify proxies are added + 3. Remove all proxies using --all flag + 4. Verify all proxies are removed + 5. Attempt to use one of the proxies (should fail) + """ + testing_db_loc = "/tmp/btcli-test.db" + os.environ["BTCLI_PROXIES_PATH"] = testing_db_loc + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + wallet_path_dave = "//Dave" + + # Create wallets for Alice, Bob, and Dave + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup( + wallet_path_bob + ) + keypair_dave, wallet_dave, wallet_path_dave, exec_command_dave = wallet_setup( + wallet_path_dave + ) + proxy_type_1 = "Transfer" + proxy_type_2 = "Staking" + delay = 0 + + try: + # Add Dave as a proxy of Alice (Transfer type) + add_result_1 = exec_command_alice( + command="proxy", + sub_command="add", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + wallet_dave.coldkeypub.ss58_address, + "--proxy-type", + proxy_type_1, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + add_result_1_output = json.loads(add_result_1.stdout) + assert add_result_1_output["success"] is True + assert ( + add_result_1_output["data"]["delegatee"] + == wallet_dave.coldkeypub.ss58_address + ) + assert add_result_1_output["data"]["proxy_type"] == proxy_type_1 + print("Proxy 1 (Dave - Transfer) added successfully") + + # Add Bob as a proxy of Alice (Staking type) + add_result_2 = exec_command_alice( + command="proxy", + sub_command="add", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + wallet_bob.coldkeypub.ss58_address, + "--proxy-type", + proxy_type_2, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + add_result_2_output = json.loads(add_result_2.stdout) + assert add_result_2_output["success"] is True + assert ( + add_result_2_output["data"]["delegatee"] + == wallet_bob.coldkeypub.ss58_address + ) + assert add_result_2_output["data"]["proxy_type"] == proxy_type_2 + print("Proxy 2 (Bob - Staking) added successfully") + + # Remove all proxies using --all flag + # Note: proxy_type is still required by CLI even with --all due to prompt=True + # so we provide it with a default value to avoid prompting + remove_all_result = exec_command_alice( + command="proxy", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--all", + "--proxy-type", + "Any", # Required to avoid prompt even with --all + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + remove_all_result_output = json.loads(remove_all_result.stdout) + assert remove_all_result_output["success"] is True + assert remove_all_result_output["message"] == "" + assert isinstance(remove_all_result_output["extrinsic_identifier"], str) + print("All proxies removed successfully") + + # Verify proxies are removed by attempting to use one (should fail) + # Try to use Dave as proxy (should fail since proxy was removed) + amount_to_transfer_proxy = 100 + transfer_result_proxy = exec_command_dave( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + wallet_alice.coldkeypub.ss58_address, + "--dest", + keypair_bob.ss58_address, + "--amount", + str(amount_to_transfer_proxy), + "--no-prompt", + "--json-output", + ], + ) + transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) + # Should fail because proxy was removed + assert transfer_result_proxy_output["success"] is False + print("Verified proxy removal - transfer via removed proxy failed as expected") + + finally: + os.environ["BTCLI_PROXIES_PATH"] = "" + if os.path.exists(testing_db_loc): + os.remove(testing_db_loc) + + +def test_remove_proxy_validation(local_chain, wallet_setup): + """ + Tests validation for proxy remove command + + Steps: + 1. Attempt to remove proxy without --delegate or --all (should fail) + 2. Attempt to remove with --all flag (should succeed if no proxies exist) + """ + testing_db_loc = "/tmp/btcli-test.db" + os.environ["BTCLI_PROXIES_PATH"] = testing_db_loc + wallet_path_alice = "//Alice" + + # Create wallet for Alice + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + + try: + # Attempt to remove proxy without --delegate or --all (should fail) + # Note: proxy_type must be provided to avoid Typer prompt, but validation + # should still fail because delegate is missing + remove_result = exec_command_alice( + command="proxy", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy-type", + "Any", # Required to avoid prompt, but delegate still missing + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + remove_result_output = json.loads(remove_result.stdout) + assert remove_result_output["success"] is False + assert ( + "Either --delegate must be provided or --all flag must be used" + in remove_result_output["message"] + ) + print("Validation test passed - correctly rejected missing delegate/--all") + + # Attempt to remove all proxies when none exist (should succeed) + # Note: proxy_type is still required by CLI even with --all due to prompt=True + # so we provide it with a default value to avoid prompting + remove_all_result = exec_command_alice( + command="proxy", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--all", + "--proxy-type", + "Any", # Required to avoid prompt even with --all + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + remove_all_result_output = json.loads(remove_all_result.stdout) + # Should succeed even if no proxies exist (remove_proxies extrinsic handles this) + assert remove_all_result_output["success"] is True + print("Validation test passed - --all works even when no proxies exist") + + finally: + os.environ["BTCLI_PROXIES_PATH"] = "" + if os.path.exists(testing_db_loc): + os.remove(testing_db_loc) + + +def test_remove_proxy(local_chain, wallet_setup): + """ + Tests removing a single proxy (without --all flag) + + Steps: + 1. Add Dave as a proxy of Alice + 2. Verify proxy is added and can be used + 3. Remove the specific proxy using --delegate flag + 4. Verify proxy is removed by attempting to use it (should fail) + """ + testing_db_loc = "/tmp/btcli-test.db" + os.environ["BTCLI_PROXIES_PATH"] = testing_db_loc + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + wallet_path_dave = "//Dave" + + # Create wallets for Alice, Bob, and Dave + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup( + wallet_path_bob + ) + keypair_dave, wallet_dave, wallet_path_dave, exec_command_dave = wallet_setup( + wallet_path_dave + ) + proxy_type = "Transfer" + delay = 0 + + try: + # Add Dave as a proxy of Alice + add_result = exec_command_alice( + command="proxy", + sub_command="add", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + wallet_dave.coldkeypub.ss58_address, + "--proxy-type", + proxy_type, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + add_result_output = json.loads(add_result.stdout) + assert add_result_output["success"] is True + assert ( + add_result_output["data"]["delegatee"] + == wallet_dave.coldkeypub.ss58_address + ) + assert ( + add_result_output["data"]["delegator"] + == wallet_alice.coldkeypub.ss58_address + ) + assert add_result_output["data"]["proxy_type"] == proxy_type + assert add_result_output["data"]["delay"] == delay + print("Proxy added successfully") + + # Verify proxy works by checking Bob's initial balance + balance_result = exec_command_bob( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["default"]["coldkey"] + == wallet_bob.coldkeypub.ss58_address + ) + bob_init_balance = balance_result_output["balances"]["default"]["free"] + + # Check Alice's initial balance + balance_result = exec_command_alice( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["default"]["coldkey"] + == wallet_alice.coldkeypub.ss58_address + ) + alice_init_balance = balance_result_output["balances"]["default"]["free"] + + # Use the proxy to transfer from Alice to Bob + amount_to_transfer_proxy = 100 + transfer_result_proxy = exec_command_dave( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + wallet_alice.coldkeypub.ss58_address, + "--dest", + keypair_bob.ss58_address, + "--amount", + str(amount_to_transfer_proxy), + "--no-prompt", + "--json-output", + ], + ) + transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) + assert transfer_result_proxy_output["success"] is True + print("Proxy transfer successful - proxy is working") + + # Verify Bob received the funds + balance_result = exec_command_bob( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["default"]["free"] + == float(amount_to_transfer_proxy) + bob_init_balance + ) + + # Now remove the proxy using single proxy removal (without --all) + remove_result = exec_command_alice( + command="proxy", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + wallet_dave.coldkeypub.ss58_address, + "--proxy-type", + proxy_type, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + remove_result_output = json.loads(remove_result.stdout) + assert remove_result_output["success"] is True + assert remove_result_output["message"] == "" + assert isinstance(remove_result_output["extrinsic_identifier"], str) + print("Single proxy removal successful") + + # Verify proxy is removed by attempting to use it (should fail) + transfer_result_proxy_2 = exec_command_dave( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + wallet_alice.coldkeypub.ss58_address, + "--dest", + keypair_bob.ss58_address, + "--amount", + str(amount_to_transfer_proxy), + "--no-prompt", + "--json-output", + ], + ) + transfer_result_proxy_2_output = json.loads(transfer_result_proxy_2.stdout) + # Should fail because proxy was removed + assert transfer_result_proxy_2_output["success"] is False + print("Verified proxy removal - transfer via removed proxy failed as expected") + + finally: + os.environ["BTCLI_PROXIES_PATH"] = "" + if os.path.exists(testing_db_loc): + os.remove(testing_db_loc) + + +def test_remove_proxy_delegate(local_chain, wallet_setup): + """ + Tests delegation handling when --all is NOT used + + Steps: + 1. Add multiple proxies (Dave and Bob as proxies of Alice) + 2. Test that --delegate is required when --all is not used + 3. Test that invalid delegate address format is rejected + 4. Test that valid delegate address correctly removes only that specific proxy + 5. Verify other proxies remain intact + """ + testing_db_loc = "/tmp/btcli-test.db" + os.environ["BTCLI_PROXIES_PATH"] = testing_db_loc + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + wallet_path_dave = "//Dave" + + # Create wallets for Alice, Bob, and Dave + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup( + wallet_path_bob + ) + keypair_dave, wallet_dave, wallet_path_dave, exec_command_dave = wallet_setup( + wallet_path_dave + ) + proxy_type_dave = "Transfer" + proxy_type_bob = "SmallTransfer" + delay = 0 + + try: + # Add Dave as a proxy of Alice + add_result_dave = exec_command_alice( + command="proxy", + sub_command="add", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + wallet_dave.coldkeypub.ss58_address, + "--proxy-type", + proxy_type_dave, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + add_result_dave_output = json.loads(add_result_dave.stdout) + assert add_result_dave_output["success"] is True + print("Dave added as proxy successfully") + + # Add Bob as a proxy of Alice + add_result_bob = exec_command_alice( + command="proxy", + sub_command="add", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + wallet_bob.coldkeypub.ss58_address, + "--proxy-type", + proxy_type_bob, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + add_result_bob_output = json.loads(add_result_bob.stdout) + assert add_result_bob_output["success"] is True + print("Bob added as proxy successfully") + + # Test 1: Attempt to remove proxy without --delegate (should fail) + remove_result_no_delegate = exec_command_alice( + command="proxy", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy-type", + proxy_type_dave, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + remove_result_no_delegate_output = json.loads(remove_result_no_delegate.stdout) + assert remove_result_no_delegate_output["success"] is False + assert ( + "Either --delegate must be provided or --all flag must be used" + in remove_result_no_delegate_output["message"] + ) + print("Test 1 passed: Correctly rejected missing --delegate") + + # Test 2: Attempt to remove proxy with invalid delegate address format (should fail) + remove_result_invalid_delegate = exec_command_alice( + command="proxy", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + "invalid_address_format", + "--proxy-type", + proxy_type_dave, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + # Invalid SS58 address should be caught by Typer validation before execution + # Check exit_code (Click's Result uses exit_code, not returncode) + assert remove_result_invalid_delegate.exit_code != 0 + print("Test 2 passed: Correctly rejected invalid delegate address format") + + # Test 3: Remove Dave's proxy using --delegate (should succeed) + remove_result_dave = exec_command_alice( + command="proxy", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + wallet_dave.coldkeypub.ss58_address, + "--proxy-type", + proxy_type_dave, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + remove_result_dave_output = json.loads(remove_result_dave.stdout) + assert remove_result_dave_output["success"] is True + assert isinstance(remove_result_dave_output["extrinsic_identifier"], str) + print("Test 3 passed: Successfully removed Dave's proxy using --delegate") + + # Test 4: Verify Dave's proxy is removed but Bob's proxy still works + # Attempt to use Dave's proxy (should fail) + transfer_result_dave_fail = exec_command_dave( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + wallet_alice.coldkeypub.ss58_address, + "--dest", + keypair_bob.ss58_address, + "--amount", + "10", + "--no-prompt", + "--json-output", + ], + ) + transfer_result_dave_fail_output = json.loads(transfer_result_dave_fail.stdout) + assert transfer_result_dave_fail_output["success"] is False + print("Test 4a passed: Dave's proxy correctly removed (transfer failed)") + + # Verify Bob's proxy still works + transfer_result_bob_success = exec_command_bob( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + wallet_alice.coldkeypub.ss58_address, + "--dest", + keypair_bob.ss58_address, + "--amount", + "10", + "--no-prompt", + "--json-output", + ], + ) + transfer_result_bob_success_output = json.loads(transfer_result_bob_success.stdout) + assert transfer_result_bob_success_output["success"] is True + print("Test 4b passed: Bob's proxy still works (other proxies remain intact)") + + # Test 5: Remove Bob's proxy using --delegate (should succeed) + remove_result_bob = exec_command_alice( + command="proxy", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--delegate", + wallet_bob.coldkeypub.ss58_address, + "--proxy-type", + proxy_type_bob, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + remove_result_bob_output = json.loads(remove_result_bob.stdout) + assert remove_result_bob_output["success"] is True + assert isinstance(remove_result_bob_output["extrinsic_identifier"], str) + print("Test 5 passed: Successfully removed Bob's proxy using --delegate") + + print("All delegation handling tests passed!") + + finally: + os.environ["BTCLI_PROXIES_PATH"] = "" + if os.path.exists(testing_db_loc): + os.remove(testing_db_loc) From 86e4aebd3845078208ea56df294d79b78f609158 Mon Sep 17 00:00:00 2001 From: leonace924 Date: Tue, 23 Dec 2025 05:57:57 -0500 Subject: [PATCH 4/6] feat: add unit testing for proxy remove --- tests/unit_tests/test_cli.py | 178 +++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 60cc1070..8a6da61c 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -805,3 +805,181 @@ async def test_set_root_weights_skips_current_weights_without_prompt(): ) mock_get_current.assert_not_called() + + +# ============================================================================ +# Tests for proxy_remove command +# ============================================================================ + + +@patch("bittensor_cli.cli.print_error") +def test_proxy_remove_requires_delegate_or_all(mock_print_error): + """Test that proxy_remove requires either --delegate or --all flag""" + cli_manager = CLIManager() + + # Test without delegate and without --all (should fail) + cli_manager.proxy_remove( + delegate=None, + all=False, + network=None, + proxy_type="Transfer", + delay=0, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + prompt=False, + decline=False, + wait_for_inclusion=False, + wait_for_finalization=False, + period=100, + quiet=True, + verbose=False, + json_output=False, + ) + + # Should print error message + mock_print_error.assert_called_once_with( + "Either --delegate must be provided or --all flag must be used." + ) + + +@patch("bittensor_cli.cli.json_console") +def test_proxy_remove_requires_delegate_or_all_json(mock_json_console): + """Test that proxy_remove requires either --delegate or --all flag (JSON output)""" + cli_manager = CLIManager() + + # Test without delegate and without --all (should fail with JSON output) + cli_manager.proxy_remove( + delegate=None, + all=False, + network=None, + proxy_type="Transfer", + delay=0, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + prompt=False, + decline=False, + wait_for_inclusion=False, + wait_for_finalization=False, + period=100, + quiet=True, + verbose=False, + json_output=True, + ) + + # Should print JSON error + mock_json_console.print_json.assert_called_once() + call_args = mock_json_console.print_json.call_args[1]["data"] + assert call_args["success"] is False + assert "Either --delegate must be provided or --all flag must be used." in call_args["message"] + assert call_args["extrinsic_identifier"] is None + + +@patch("bittensor_cli.cli.console") +@patch("bittensor_cli.cli.proxy_commands") +def test_proxy_remove_with_all_flag(mock_proxy_commands, mock_console): + """Test that proxy_remove with --all flag calls remove_proxies""" + cli_manager = CLIManager() + valid_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + mock_wallet = Mock() + mock_subtensor = Mock() + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask", return_value=mock_wallet), + patch.object(cli_manager, "initialize_chain", return_value=mock_subtensor), + patch.object(cli_manager, "_run_command") as mock_run_command, + ): + cli_manager.proxy_remove( + delegate=valid_ss58, # Should be ignored when --all is True + all=True, + network=None, + proxy_type="Transfer", + delay=0, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + prompt=False, + decline=False, + wait_for_inclusion=False, + wait_for_finalization=False, + period=100, + quiet=True, + verbose=False, + json_output=False, + ) + + # Should show warning that delegate is ignored + mock_console.print.assert_called_once_with( + "[yellow]Warning: --delegate is ignored when --all flag is used.[/yellow]" + ) + + # Should call remove_proxies (not remove_proxy) + mock_proxy_commands.remove_proxies.assert_called_once_with( + subtensor=mock_subtensor, + wallet=mock_wallet, + prompt=False, + decline=False, + quiet=True, + wait_for_inclusion=False, + wait_for_finalization=False, + period=100, + json_output=False, + ) + + # Should call _run_command with the result + mock_run_command.assert_called_once() + + +@patch("bittensor_cli.cli.proxy_commands") +def test_proxy_remove_with_delegate_calls_remove_proxy(mock_proxy_commands): + """Test that proxy_remove with --delegate calls remove_proxy with correct parameters""" + cli_manager = CLIManager() + valid_delegate = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + mock_wallet = Mock() + mock_subtensor = Mock() + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask", return_value=mock_wallet), + patch.object(cli_manager, "initialize_chain", return_value=mock_subtensor), + patch.object(cli_manager, "_run_command") as mock_run_command, + ): + cli_manager.proxy_remove( + delegate=valid_delegate, + all=False, + network=None, + proxy_type="Transfer", + delay=10, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + prompt=False, + decline=False, + wait_for_inclusion=True, + wait_for_finalization=True, + period=100, + quiet=True, + verbose=False, + json_output=False, + ) + + # Should call remove_proxy with correct parameters + mock_proxy_commands.remove_proxy.assert_called_once_with( + subtensor=mock_subtensor, + wallet=mock_wallet, + delegate=valid_delegate, + proxy_type="Transfer", + delay=10, + prompt=False, + decline=False, + quiet=True, + wait_for_inclusion=True, + wait_for_finalization=True, + period=100, + json_output=False, + ) + + # Should call _run_command with the result + mock_run_command.assert_called_once() From 9998d03c52ae367de04622346fe82407d5ceb716 Mon Sep 17 00:00:00 2001 From: leonace924 Date: Tue, 23 Dec 2025 06:06:58 -0500 Subject: [PATCH 5/6] fix: remove the test_remove_proxy_delegate as it's duplicated --- tests/e2e_tests/test_proxy.py | 258 ---------------------------------- 1 file changed, 258 deletions(-) diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index 63bce215..b89c564e 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -1138,261 +1138,3 @@ def test_remove_proxy(local_chain, wallet_setup): os.environ["BTCLI_PROXIES_PATH"] = "" if os.path.exists(testing_db_loc): os.remove(testing_db_loc) - - -def test_remove_proxy_delegate(local_chain, wallet_setup): - """ - Tests delegation handling when --all is NOT used - - Steps: - 1. Add multiple proxies (Dave and Bob as proxies of Alice) - 2. Test that --delegate is required when --all is not used - 3. Test that invalid delegate address format is rejected - 4. Test that valid delegate address correctly removes only that specific proxy - 5. Verify other proxies remain intact - """ - testing_db_loc = "/tmp/btcli-test.db" - os.environ["BTCLI_PROXIES_PATH"] = testing_db_loc - wallet_path_alice = "//Alice" - wallet_path_bob = "//Bob" - wallet_path_dave = "//Dave" - - # Create wallets for Alice, Bob, and Dave - keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( - wallet_path_alice - ) - keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup( - wallet_path_bob - ) - keypair_dave, wallet_dave, wallet_path_dave, exec_command_dave = wallet_setup( - wallet_path_dave - ) - proxy_type_dave = "Transfer" - proxy_type_bob = "SmallTransfer" - delay = 0 - - try: - # Add Dave as a proxy of Alice - add_result_dave = exec_command_alice( - command="proxy", - sub_command="add", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--delegate", - wallet_dave.coldkeypub.ss58_address, - "--proxy-type", - proxy_type_dave, - "--delay", - str(delay), - "--period", - "128", - "--no-prompt", - "--json-output", - ], - ) - add_result_dave_output = json.loads(add_result_dave.stdout) - assert add_result_dave_output["success"] is True - print("Dave added as proxy successfully") - - # Add Bob as a proxy of Alice - add_result_bob = exec_command_alice( - command="proxy", - sub_command="add", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--delegate", - wallet_bob.coldkeypub.ss58_address, - "--proxy-type", - proxy_type_bob, - "--delay", - str(delay), - "--period", - "128", - "--no-prompt", - "--json-output", - ], - ) - add_result_bob_output = json.loads(add_result_bob.stdout) - assert add_result_bob_output["success"] is True - print("Bob added as proxy successfully") - - # Test 1: Attempt to remove proxy without --delegate (should fail) - remove_result_no_delegate = exec_command_alice( - command="proxy", - sub_command="remove", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--proxy-type", - proxy_type_dave, - "--delay", - str(delay), - "--period", - "128", - "--no-prompt", - "--json-output", - ], - ) - remove_result_no_delegate_output = json.loads(remove_result_no_delegate.stdout) - assert remove_result_no_delegate_output["success"] is False - assert ( - "Either --delegate must be provided or --all flag must be used" - in remove_result_no_delegate_output["message"] - ) - print("Test 1 passed: Correctly rejected missing --delegate") - - # Test 2: Attempt to remove proxy with invalid delegate address format (should fail) - remove_result_invalid_delegate = exec_command_alice( - command="proxy", - sub_command="remove", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--delegate", - "invalid_address_format", - "--proxy-type", - proxy_type_dave, - "--delay", - str(delay), - "--period", - "128", - "--no-prompt", - "--json-output", - ], - ) - # Invalid SS58 address should be caught by Typer validation before execution - # Check exit_code (Click's Result uses exit_code, not returncode) - assert remove_result_invalid_delegate.exit_code != 0 - print("Test 2 passed: Correctly rejected invalid delegate address format") - - # Test 3: Remove Dave's proxy using --delegate (should succeed) - remove_result_dave = exec_command_alice( - command="proxy", - sub_command="remove", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--delegate", - wallet_dave.coldkeypub.ss58_address, - "--proxy-type", - proxy_type_dave, - "--delay", - str(delay), - "--period", - "128", - "--no-prompt", - "--json-output", - ], - ) - remove_result_dave_output = json.loads(remove_result_dave.stdout) - assert remove_result_dave_output["success"] is True - assert isinstance(remove_result_dave_output["extrinsic_identifier"], str) - print("Test 3 passed: Successfully removed Dave's proxy using --delegate") - - # Test 4: Verify Dave's proxy is removed but Bob's proxy still works - # Attempt to use Dave's proxy (should fail) - transfer_result_dave_fail = exec_command_dave( - command="wallet", - sub_command="transfer", - extra_args=[ - "--wallet-path", - wallet_path_dave, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--proxy", - wallet_alice.coldkeypub.ss58_address, - "--dest", - keypair_bob.ss58_address, - "--amount", - "10", - "--no-prompt", - "--json-output", - ], - ) - transfer_result_dave_fail_output = json.loads(transfer_result_dave_fail.stdout) - assert transfer_result_dave_fail_output["success"] is False - print("Test 4a passed: Dave's proxy correctly removed (transfer failed)") - - # Verify Bob's proxy still works - transfer_result_bob_success = exec_command_bob( - command="wallet", - sub_command="transfer", - extra_args=[ - "--wallet-path", - wallet_path_bob, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--proxy", - wallet_alice.coldkeypub.ss58_address, - "--dest", - keypair_bob.ss58_address, - "--amount", - "10", - "--no-prompt", - "--json-output", - ], - ) - transfer_result_bob_success_output = json.loads(transfer_result_bob_success.stdout) - assert transfer_result_bob_success_output["success"] is True - print("Test 4b passed: Bob's proxy still works (other proxies remain intact)") - - # Test 5: Remove Bob's proxy using --delegate (should succeed) - remove_result_bob = exec_command_alice( - command="proxy", - sub_command="remove", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--delegate", - wallet_bob.coldkeypub.ss58_address, - "--proxy-type", - proxy_type_bob, - "--delay", - str(delay), - "--period", - "128", - "--no-prompt", - "--json-output", - ], - ) - remove_result_bob_output = json.loads(remove_result_bob.stdout) - assert remove_result_bob_output["success"] is True - assert isinstance(remove_result_bob_output["extrinsic_identifier"], str) - print("Test 5 passed: Successfully removed Bob's proxy using --delegate") - - print("All delegation handling tests passed!") - - finally: - os.environ["BTCLI_PROXIES_PATH"] = "" - if os.path.exists(testing_db_loc): - os.remove(testing_db_loc) From 903d27eedfd4da10bf726432c370c7d0074c9c68 Mon Sep 17 00:00:00 2001 From: leonace924 Date: Tue, 23 Dec 2025 06:08:14 -0500 Subject: [PATCH 6/6] ruff --- tests/unit_tests/test_cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 8a6da61c..0153f21c 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -872,7 +872,10 @@ def test_proxy_remove_requires_delegate_or_all_json(mock_json_console): mock_json_console.print_json.assert_called_once() call_args = mock_json_console.print_json.call_args[1]["data"] assert call_args["success"] is False - assert "Either --delegate must be provided or --all flag must be used." in call_args["message"] + assert ( + "Either --delegate must be provided or --all flag must be used." + in call_args["message"] + ) assert call_args["extrinsic_identifier"] is None