Skip to content

Commit adf8647

Browse files
committed
feat(proxy): add list, reject, and remove --all (Closes #742)
New commands: - btcli proxy list: query Proxy.Proxies storage for an account - btcli proxy reject: reject a previously announced proxy call Modified command: - btcli proxy remove --all: remove every proxy at once Implementation: - proxy.py: _parse_proxy_storage, list_proxies, reject_announcement, remove_all_proxies; handles nested substrate response formats - cli.py: register proxy list/reject; extend proxy remove with --all flag (mutually exclusive with --delegate) - Specific exception handling (KeyError/TypeError/ValueError/IndexError) with debug logging in _parse_proxy_storage Note: CI will fail on import due to a pre-existing staging bug where extract_mev_shield_id was removed from mev_shield.py but still imported by sudo.py. This is not related to this PR. Tests: - 16 unit tests for parsing, list, reject, remove --all - 2 E2E tests (test_proxy_list_after_add, test_proxy_remove_all) - 1 E2E test (test_proxy_reject_announced)
1 parent fc22436 commit adf8647

5 files changed

Lines changed: 1378 additions & 18 deletions

File tree

bittensor_cli/cli.py

Lines changed: 213 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,12 @@ def __init__(self):
12431243
"execute",
12441244
rich_help_panel=HELP_PANELS["PROXY"]["MGMT"],
12451245
)(self.proxy_execute_announced)
1246+
self.proxy_app.command("list", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])(
1247+
self.proxy_list
1248+
)
1249+
self.proxy_app.command("reject", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])(
1250+
self.proxy_reject_announced
1251+
)
12461252

12471253
# Sub command aliases
12481254
# Wallet
@@ -9817,15 +9823,23 @@ def proxy_add(
98179823
def proxy_remove(
98189824
self,
98199825
delegate: Annotated[
9820-
str,
9826+
Optional[str],
98219827
typer.Option(
98229828
callback=is_valid_ss58_address_param,
9823-
prompt="Enter the SS58 address of the delegate to remove, e.g. 5dxds...",
9824-
help="The SS58 address of the delegate to remove",
9829+
help="The SS58 address of the delegate to remove. Mutually exclusive with --all.",
98259830
),
9826-
] = "",
9831+
] = None,
9832+
remove_all: bool = typer.Option(
9833+
False,
9834+
"--all",
9835+
help="Remove all proxies for the account at once.",
9836+
),
98279837
network: Optional[list[str]] = Options.network,
9828-
proxy_type: ProxyType = Options.proxy_type,
9838+
proxy_type: ProxyType = typer.Option(
9839+
ProxyType.Any.value,
9840+
"--proxy-type",
9841+
help="Type of proxy (only used with --delegate, not --all)",
9842+
),
98299843
delay: int = typer.Option(0, help="Delay, in number of blocks"),
98309844
wallet_name: str = Options.wallet_name,
98319845
wallet_path: str = Options.wallet_path,
@@ -9842,24 +9856,21 @@ def proxy_remove(
98429856
"""
98439857
Unregisters a proxy from an account.
98449858
9845-
Revokes proxy permissions previously granted to another account. This prevents the delegate account from executing any further transactions on your behalf.
9846-
9847-
9848-
[bold]Example:[/bold]
9849-
Revoke proxy permissions from a single proxy account
9850-
[green]$[/green] btcli proxy remove --delegate 5GDel... --proxy-type Transfer
9851-
9859+
Revokes proxy permissions previously granted to another account. Use --delegate for a single proxy or --all to remove every proxy.
98529860
"""
9853-
# TODO should add a --all flag to call Proxy.remove_proxies ?
9861+
if remove_all and delegate:
9862+
print_error("Cannot use --all and --delegate together. Choose one.")
9863+
raise typer.Exit(1)
9864+
if not remove_all and not delegate:
9865+
print_error("Either --delegate or --all is required.")
9866+
raise typer.Exit(1)
98549867
logger.debug(
98559868
"args:\n"
98569869
f"delegate: {delegate}\n"
9870+
f"remove_all: {remove_all}\n"
98579871
f"network: {network}\n"
98589872
f"proxy_type: {proxy_type}\n"
98599873
f"delay: {delay}\n"
9860-
f"wait_for_finalization: {wait_for_finalization}\n"
9861-
f"wait_for_inclusion: {wait_for_inclusion}\n"
9862-
f"era: {period}\n"
98639874
)
98649875
self.verbosity_handler(quiet, verbose, json_output, prompt)
98659876
wallet = self.wallet_ask(
@@ -9869,9 +9880,24 @@ def proxy_remove(
98699880
ask_for=[WO.NAME, WO.PATH],
98709881
validate=WV.WALLET,
98719882
)
9883+
subtensor = self.initialize_chain(network)
9884+
if remove_all:
9885+
return self._run_command(
9886+
proxy_commands.remove_all_proxies(
9887+
subtensor=subtensor,
9888+
wallet=wallet,
9889+
prompt=prompt,
9890+
decline=decline,
9891+
quiet=quiet,
9892+
wait_for_inclusion=wait_for_inclusion,
9893+
wait_for_finalization=wait_for_finalization,
9894+
period=period,
9895+
json_output=json_output,
9896+
)
9897+
)
98729898
return self._run_command(
98739899
proxy_commands.remove_proxy(
9874-
subtensor=self.initialize_chain(network),
9900+
subtensor=subtensor,
98759901
wallet=wallet,
98769902
delegate=delegate,
98779903
proxy_type=proxy_type,
@@ -10195,6 +10221,176 @@ def proxy_execute_announced(
1019510221
with ProxyAnnouncements.get_db() as (conn, cursor):
1019610222
ProxyAnnouncements.mark_as_executed(conn, cursor, got_call_from_db)
1019710223

10224+
def proxy_list(
10225+
self,
10226+
address: Optional[str] = typer.Option(
10227+
None,
10228+
"--address",
10229+
help="SS58 address to list proxies for. Defaults to the selected wallet.",
10230+
callback=is_valid_ss58_address_param,
10231+
),
10232+
network: Optional[list[str]] = Options.network,
10233+
wallet_name: Optional[str] = Options.wallet_name,
10234+
wallet_path: Optional[str] = Options.wallet_path,
10235+
wallet_hotkey: Optional[str] = Options.wallet_hotkey,
10236+
quiet: bool = Options.quiet,
10237+
verbose: bool = Options.verbose,
10238+
json_output: bool = Options.json_output,
10239+
):
10240+
"""List proxies configured for an account (chain Proxy.Proxies storage)."""
10241+
self.verbosity_handler(quiet, verbose, json_output, prompt=False)
10242+
if address is None:
10243+
wallet = self.wallet_ask(
10244+
wallet_name,
10245+
wallet_path,
10246+
wallet_hotkey,
10247+
ask_for=[WO.NAME, WO.PATH],
10248+
validate=WV.WALLET,
10249+
)
10250+
address = wallet.coldkeypub.ss58_address
10251+
return self._run_command(
10252+
proxy_commands.list_proxies(
10253+
subtensor=self.initialize_chain(network),
10254+
address=address,
10255+
prompt=False,
10256+
json_output=json_output,
10257+
)
10258+
)
10259+
10260+
def proxy_reject_announced(
10261+
self,
10262+
delegate: Optional[str] = typer.Option(
10263+
None,
10264+
"--delegate",
10265+
help="SS58 address of the delegate who made the announcement. Required with --no-prompt.",
10266+
callback=is_valid_ss58_address_param,
10267+
),
10268+
call_hash: Optional[str] = typer.Option(
10269+
None,
10270+
"--call-hash",
10271+
help="Hash of the announced call to reject. Resolved from ProxyAnnouncements table if available.",
10272+
),
10273+
network: Optional[list[str]] = Options.network,
10274+
wallet_name: str = Options.wallet_name,
10275+
wallet_path: str = Options.wallet_path,
10276+
wallet_hotkey: str = Options.wallet_hotkey,
10277+
prompt: bool = Options.prompt,
10278+
decline: bool = Options.decline,
10279+
wait_for_inclusion: bool = Options.wait_for_inclusion,
10280+
wait_for_finalization: bool = Options.wait_for_finalization,
10281+
period: int = Options.period,
10282+
quiet: bool = Options.quiet,
10283+
verbose: bool = Options.verbose,
10284+
json_output: bool = Options.json_output,
10285+
):
10286+
"""
10287+
Reject a previously announced proxy call (Proxy.reject_announcement).
10288+
10289+
If --call-hash is provided, the command attempts to resolve it from the
10290+
ProxyAnnouncements table and marks it as executed on success (same flow
10291+
as btcli proxy execute).
10292+
"""
10293+
self.verbosity_handler(quiet, verbose, json_output, prompt)
10294+
wallet = self.wallet_ask(
10295+
wallet_name,
10296+
wallet_path,
10297+
wallet_hotkey,
10298+
ask_for=[WO.NAME, WO.PATH],
10299+
validate=WV.WALLET,
10300+
)
10301+
real_address = wallet.coldkeypub.ss58_address
10302+
if delegate is None:
10303+
if not prompt:
10304+
print_error("--delegate is required when using --no-prompt.")
10305+
raise typer.Exit(1)
10306+
while True:
10307+
delegate = Prompt.ask("Enter the delegate SS58 address")
10308+
if is_valid_ss58_address(delegate):
10309+
break
10310+
print_error(f"Invalid SS58 address: {delegate}")
10311+
10312+
got_call_from_db: Optional[int] = None
10313+
if call_hash is not None:
10314+
with ProxyAnnouncements.get_db() as (conn, cursor):
10315+
announcements = ProxyAnnouncements.read_rows(conn, cursor)
10316+
potential_call_matches = []
10317+
for row in announcements:
10318+
(
10319+
id_,
10320+
address,
10321+
epoch_time,
10322+
block_,
10323+
call_hash_,
10324+
call_hex_,
10325+
call_serialized,
10326+
executed_int,
10327+
) = row
10328+
if (
10329+
(call_hash_ == call_hash or f"0x{call_hash_}" == call_hash)
10330+
and not bool(executed_int)
10331+
and address == real_address
10332+
):
10333+
potential_call_matches.append(row)
10334+
if len(potential_call_matches) == 1:
10335+
got_call_from_db = potential_call_matches[0][0]
10336+
elif len(potential_call_matches) > 1:
10337+
if prompt:
10338+
console.print(
10339+
f"The call hash you provided matches {len(potential_call_matches)} possible "
10340+
"entries for this wallet. The results will be iterated for you to select your intended call."
10341+
)
10342+
for row in potential_call_matches:
10343+
(
10344+
id_,
10345+
address,
10346+
epoch_time,
10347+
block_,
10348+
call_hash_,
10349+
call_hex_,
10350+
call_serialized,
10351+
executed_int,
10352+
) = row
10353+
console.print(
10354+
f"Time: {datetime.datetime.fromtimestamp(epoch_time)}\nCall:\n"
10355+
)
10356+
console.print_json(call_serialized)
10357+
if confirm_action(
10358+
"Is this the intended call?",
10359+
decline=decline,
10360+
quiet=quiet,
10361+
):
10362+
got_call_from_db = id_
10363+
break
10364+
else:
10365+
verbose_console.print(
10366+
f"Call hash '{call_hash}' matched {len(potential_call_matches)} unexecuted entries "
10367+
"for this wallet. Skipping local mark_as_executed due to ambiguity."
10368+
)
10369+
else:
10370+
if not prompt:
10371+
print_error("--call-hash is required when using --no-prompt.")
10372+
raise typer.Exit(1)
10373+
call_hash = Prompt.ask("Enter the call hash to reject")
10374+
10375+
success = self._run_command(
10376+
proxy_commands.reject_announcement(
10377+
subtensor=self.initialize_chain(network),
10378+
wallet=wallet,
10379+
delegate=delegate,
10380+
call_hash=call_hash,
10381+
prompt=prompt,
10382+
decline=decline,
10383+
quiet=quiet,
10384+
wait_for_inclusion=wait_for_inclusion,
10385+
wait_for_finalization=wait_for_finalization,
10386+
period=period,
10387+
json_output=json_output,
10388+
)
10389+
)
10390+
if success and got_call_from_db is not None:
10391+
with ProxyAnnouncements.get_db() as (conn, cursor):
10392+
ProxyAnnouncements.mark_as_executed(conn, cursor, got_call_from_db)
10393+
1019810394
@staticmethod
1019910395
def convert(
1020010396
from_rao: Optional[str] = typer.Option(

bittensor_cli/src/bittensor/extrinsics/mev_shield.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,25 @@ async def encrypt_extrinsic(
5252
return encrypted_call
5353

5454

55+
async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[str]:
56+
"""
57+
Extract the MEV Shield wrapper ID from an extrinsic response.
58+
59+
After submitting a MEV Shield encrypted call, the EncryptedSubmitted event
60+
contains the wrapper ID needed to track execution.
61+
62+
Args:
63+
response: The extrinsic receipt from submit_extrinsic.
64+
65+
Returns:
66+
The wrapper ID (hex string) or None if not found.
67+
"""
68+
for event in await response.triggered_events:
69+
if event["event_id"] == "EncryptedSubmitted":
70+
return event["attributes"]["id"]
71+
return None
72+
73+
5574
async def wait_for_extrinsic_by_hash(
5675
subtensor: "SubtensorInterface",
5776
extrinsic_hash: str,

0 commit comments

Comments
 (0)