From 069883e1a2f26a488949865d9a078cff4f54c187 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 17:05:56 +0200 Subject: [PATCH 001/179] Pure proxy create init --- bittensor_cli/cli.py | 45 ++++++++++++++ bittensor_cli/src/commands/proxy.py | 92 +++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 bittensor_cli/src/commands/proxy.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ba7aa78b9..208457fce 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -80,6 +80,7 @@ prompt_liquidity, prompt_position_id, ) +from bittensor_cli.src.commands.proxy import ProxyType from bittensor_cli.src.commands.stake import ( auto_staking as auto_stake, children_hotkeys, @@ -769,6 +770,7 @@ def __init__(self): self.liquidity_app = typer.Typer(epilog=_epilog) self.crowd_app = typer.Typer(epilog=_epilog) self.utils_app = typer.Typer(epilog=_epilog) + self.proxy_app = typer.Typer(epilog=_epilog) # config alias self.app.add_typer( @@ -867,6 +869,14 @@ def __init__(self): no_args_is_help=True, ) + # proxy app + self.app.add_typer( + self.proxy_app, + name="proxy", + short_help="Proxy commands", + no_args_is_help=True, + ) + # config commands self.config_app.command("set")(self.set_config) self.config_app.command("get")(self.get_config) @@ -1092,6 +1102,11 @@ def __init__(self): "dashboard", rich_help_panel=HELP_PANELS["VIEW"]["DASHBOARD"] )(self.view_dashboard) + # proxy commands + self.proxy_app.command( + "create", # TODO add rich help panel + )(self.proxy_create) + # Sub command aliases # Wallet self.wallet_app.command( @@ -8060,6 +8075,36 @@ def crowd_dissolve( ) ) + def proxy_create( + self, + network: Optional[list[str]] = Options.network, + proxy_type: ProxyType = typer.Option( + ProxyType.Any.value, + "--proxy-type", + help="Type of proxy", + prompt=True, + ), + delay: int = typer.Option(0, help="Delay, in number of blocks"), + idx: int = typer.Option(0, "--index", help="TODO lol"), + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + period: int = Options.period, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + TODO ask someone else to write the docs + + Creates a pure proxy + """ + # TODO add debug logger + self.verbosity_handler(quiet, verbose, json_output) + @staticmethod def convert( from_rao: Optional[str] = typer.Option( diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py new file mode 100644 index 000000000..b6887176c --- /dev/null +++ b/bittensor_cli/src/commands/proxy.py @@ -0,0 +1,92 @@ +import asyncio +from enum import Enum +from typing import TYPE_CHECKING + +from rich.prompt import Confirm + +from bittensor_cli.src.bittensor.utils import ( + print_extrinsic_id, + json_console, + console, + err_console, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + from bittensor_wallet.bittensor_wallet import Wallet + + +class ProxyType(str, Enum): + Any = "Any" + Owner = "Owner" + NonCritical = "NonCritical" + NonTransfer = "NonTransfer" + Senate = "Senate" + NonFungible = "NonFungible" + Triumvirate = "Triumvirate" + Governance = "Governance" + Staking = "Staking" + Registration = "Registration" + Transfer = "Transfer" + SmallTransfer = "SmallTransfer" + RootWeights = "RootWeights" + ChildKeys = "ChildKeys" + SudoUncheckedSetCode = "SudoUncheckedSetCode" + SwapHotkey = "SwapHotkey" + SubnetLeaseBeneficiary = "SubnetLeaseBeneficiary" + RootClaim = "RootClaim" + + +async def create_proxy( + subtensor: "SubtensorInterface", + wallet: "Wallet", + proxy_type: ProxyType, + delay: int, + idx: int, + prompt: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, +) -> None: + if prompt: + if not Confirm.ask( + f"This will create a Pure Proxy of type {proxy_type.value}. Do you want to proceed?", + ): + return + call = await subtensor.substrate.compose_call( + call_module="Proxy", + call_function="create_pure", + call_params={"proxy_type": proxy_type.value, "delay": delay, "index": idx}, + ) + success, msg, receipt = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + era={"period": period}, + ) + if success: + await print_extrinsic_id(receipt) + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_id": await receipt.get_extrinsic_identifier(), + } + ) + else: + console.print("Success!") # TODO add more shit here + + else: + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_id": None, + } + ) + else: + err_console.print(f"Failure: {msg}") # TODO add more shit here From ee4fd78a4ae0c93405cba892ccbb17b0c3f45784 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 17:11:05 +0200 Subject: [PATCH 002/179] Actually run the command --- bittensor_cli/cli.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 208457fce..5490a24ee 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -13,7 +13,7 @@ import warnings from dataclasses import fields from pathlib import Path -from typing import Coroutine, Optional, Union, Literal +from typing import Coroutine, Optional, Union import numpy as np import rich @@ -80,6 +80,7 @@ prompt_liquidity, prompt_position_id, ) +from bittensor_cli.src.commands import proxy as proxy_commands from bittensor_cli.src.commands.proxy import ProxyType from bittensor_cli.src.commands.stake import ( auto_staking as auto_stake, @@ -95,7 +96,6 @@ subnets, mechanisms as subnet_mechanisms, ) -from bittensor_cli.src.commands.wallets import SortByBalance from bittensor_cli.version import __version__, __version_as_int__ try: @@ -8104,6 +8104,28 @@ def proxy_create( """ # TODO add debug logger self.verbosity_handler(quiet, verbose, json_output) + 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.create_proxy( + subtensor=self.initialize_chain(network), + wallet=wallet, + proxy_type=proxy_type, + delay=delay, + idx=idx, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + json_output=json_output, + period=period, + ) + ) @staticmethod def convert( From 8cf030ddba313039c6279ecbd4537bca6e805c5e Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 17:54:45 +0200 Subject: [PATCH 003/179] More proxies --- bittensor_cli/cli.py | 122 ++++++++++++++++++++++++++-- bittensor_cli/src/commands/proxy.py | 120 +++++++++++++++++++++++++++ 2 files changed, 236 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5490a24ee..dd6bb9151 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -358,6 +358,12 @@ def edit_help(cls, option_name: str, help_text: str): "--era", help="Length (in blocks) for which the transaction should be valid.", ) + proxy_type: ProxyType = typer.Option( + ProxyType.Any.value, + "--proxy-type", + help="Type of proxy", + prompt=True, + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -397,6 +403,12 @@ def parse_to_list( raise typer.BadParameter(error_message) +def is_valid_ss58_address_param(address: str) -> str: + if not bittensor_wallet.utils.is_valid_ss58_address(address): + raise typer.BadParameter(f"Invalid SS58 address: {address}") + return address + + def verbosity_console_handler(verbosity_level: int = 1) -> None: """ Sets verbosity level of console output @@ -1106,6 +1118,9 @@ def __init__(self): self.proxy_app.command( "create", # TODO add rich help panel )(self.proxy_create) + self.proxy_app.command( + "remove", # TODO add rich help panel + )(self.proxy_remove) # Sub command aliases # Wallet @@ -8078,12 +8093,7 @@ def crowd_dissolve( def proxy_create( self, network: Optional[list[str]] = Options.network, - proxy_type: ProxyType = typer.Option( - ProxyType.Any.value, - "--proxy-type", - help="Type of proxy", - prompt=True, - ), + proxy_type: ProxyType = Options.proxy_type, delay: int = typer.Option(0, help="Delay, in number of blocks"), idx: int = typer.Option(0, "--index", help="TODO lol"), wallet_name: str = Options.wallet_name, @@ -8127,6 +8137,106 @@ def proxy_create( ) ) + def proxy_add( + self, + delegate: Annotated[ + str, + typer.Option( + callback=is_valid_ss58_address_param, + prompt="Enter the SS58 address of the delegate to add, e.g. 5dxds...", + help="The SS58 address of the delegate to add", + ), + ], + network: Optional[list[str]] = Options.network, + proxy_type: ProxyType = Options.proxy_type, + delay: int = typer.Option(0, help="Delay, in number of blocks"), + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + period: int = Options.period, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + # TODO add debug logger + self.verbosity_handler(quiet, verbose, json_output) + 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, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + json_output=json_output, + ) + ) + + def proxy_remove( + self, + delegate: Annotated[ + 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", + ), + ], + network: Optional[list[str]] = Options.network, + proxy_type: ProxyType = Options.proxy_type, + delay: int = typer.Option(0, help="Delay, in number of blocks"), + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + period: int = Options.period, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + # TODO should add a --all flag to call Proxy.remove_proxies ? + # TODO add debug logger + self.verbosity_handler(quiet, verbose, json_output) + 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, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + period=period, + json_output=json_output, + ) + ) + + def proxy_kill(self): + pass + @staticmethod def convert( from_rao: Optional[str] = typer.Option( diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index b6887176c..d740e6976 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -90,3 +90,123 @@ async def create_proxy( ) else: err_console.print(f"Failure: {msg}") # TODO add more shit here + + +async def remove_proxy( + subtensor: "SubtensorInterface", + wallet: "Wallet", + proxy_type: ProxyType, + delegate: str, + delay: int, + prompt: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, +) -> None: + if prompt: + if not Confirm.ask( + f"This will remove a proxy of type {proxy_type.value} for delegate {delegate}." + f"Do you want to proceed?" + ): + return + call = await subtensor.substrate.compose_call( + call_module="Proxy", + call_function="remove_proxy", + call_params={ + "proxy_type": proxy_type.value, + "delay": delay, + "delegate": delegate, + }, + ) + success, msg, receipt = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + era={"period": period}, + ) + if success: + await print_extrinsic_id(receipt) + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_id": await receipt.get_extrinsic_identifier(), + } + ) + else: + console.print("Success!") # TODO add more shit here + + else: + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_id": None, + } + ) + else: + err_console.print(f"Failure: {msg}") # TODO add more shit here + + +async def add_proxy( + subtensor: "SubtensorInterface", + wallet: "Wallet", + proxy_type: ProxyType, + delegate: str, + delay: int, + prompt: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, +): + if prompt: + if not Confirm.ask( + f"This will add a proxy of type {proxy_type.value} for delegate {delegate}." + f"Do you want to proceed?" + ): + return + call = await subtensor.substrate.compose_call( + call_module="Proxy", + call_function="add_proxy", + call_params={ + "proxy_type": proxy_type.value, + "delay": delay, + "delegate": delegate, + }, + ) + success, msg, receipt = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + era={"period": period}, + ) + if success: + await print_extrinsic_id(receipt) + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_id": await receipt.get_extrinsic_identifier(), + } + ) + else: + console.print("Success!") # TODO add more shit here + + else: + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_id": None, + } + ) + else: + err_console.print(f"Failure: {msg}") # TODO add more shit here From 17b731d0cfdb91207640b525410fd696ff63eedb Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 17:55:13 +0200 Subject: [PATCH 004/179] Import --- bittensor_cli/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index dd6bb9151..023dd63f6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -23,6 +23,7 @@ ConnectionClosed, InvalidHandshake, ) +import bittensor_wallet from bittensor_wallet import Wallet from rich import box from rich.prompt import Confirm, FloatPrompt, Prompt, IntPrompt From db736744800177e319e57e64964141c3f9399514 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 18:14:12 +0200 Subject: [PATCH 005/179] Kill proxy pure --- bittensor_cli/cli.py | 63 ++++++++- bittensor_cli/src/commands/proxy.py | 201 ++++++++++++++++++---------- 2 files changed, 195 insertions(+), 69 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 023dd63f6..f03047a04 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8162,6 +8162,10 @@ def proxy_add( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): + """ + Adds a proxy + + """ # TODO add debug logger self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( @@ -8210,6 +8214,9 @@ def proxy_remove( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): + """ + Removes a proxy + """ # TODO should add a --all flag to call Proxy.remove_proxies ? # TODO add debug logger self.verbosity_handler(quiet, verbose, json_output) @@ -8235,8 +8242,60 @@ def proxy_remove( ) ) - def proxy_kill(self): - pass + def proxy_kill( + self, + height: int = typer.Option( + help="The block number that the proxy was created at", + prompt="Enter the block number at which the proxy was created", + ), + ext_index: int = typer.Option( + help=f"The extrinsic index of the Proxy.PureCreated event" + f" ([{COLORS.G.ARG}]btcli proxy create[/{COLORS.G.ARG}])", + prompt="Enter the extrinsic index of the `btcli proxy create` event.", + ), + network: Optional[list[str]] = Options.network, + proxy_type: ProxyType = Options.proxy_type, + idx: int = typer.Option(0, "--index", help="TODO lol"), + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + period: int = Options.period, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + + Kills a pure proxy + """ + # TODO add debug logger + self.verbosity_handler(quiet, verbose, json_output) + 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.kill_proxy( + subtensor=self.initialize_chain(network), + wallet=wallet, + proxy_type=proxy_type, + height=height, + ext_index=ext_index, + idx=idx, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + json_output=json_output, + period=period, + ) + ) @staticmethod def convert( diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index d740e6976..765eb2023 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -1,14 +1,15 @@ -import asyncio from enum import Enum from typing import TYPE_CHECKING from rich.prompt import Confirm +from scalecodec import GenericCall from bittensor_cli.src.bittensor.utils import ( print_extrinsic_id, json_console, console, err_console, + unlock_key, ) if TYPE_CHECKING: @@ -37,28 +38,15 @@ class ProxyType(str, Enum): RootClaim = "RootClaim" -async def create_proxy( +async def submit_proxy( subtensor: "SubtensorInterface", - wallet: "Wallet", - proxy_type: ProxyType, - delay: int, - idx: int, - prompt: bool, + wallet: Wallet, + call: GenericCall, wait_for_inclusion: bool, wait_for_finalization: bool, period: int, json_output: bool, ) -> None: - if prompt: - if not Confirm.ask( - f"This will create a Pure Proxy of type {proxy_type.value}. Do you want to proceed?", - ): - return - call = await subtensor.substrate.compose_call( - call_module="Proxy", - call_function="create_pure", - call_params={"proxy_type": proxy_type.value, "delay": delay, "index": idx}, - ) success, msg, receipt = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -92,6 +80,51 @@ async def create_proxy( err_console.print(f"Failure: {msg}") # TODO add more shit here +async def create_proxy( + subtensor: "SubtensorInterface", + wallet: "Wallet", + proxy_type: ProxyType, + delay: int, + idx: int, + prompt: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, +) -> None: + if prompt: + if not Confirm.ask( + f"This will create a Pure Proxy of type {proxy_type.value}. Do you want to proceed?", + ): + return None + if not (ulw := unlock_key(wallet, print_out=not json_output)).success: + if not json_output: + err_console.print(ulw.message) + else: + json_console.print_json( + data={ + "success": ulw.success, + "message": ulw.message, + "extrinsic_id": None, + } + ) + return None + call = await subtensor.substrate.compose_call( + call_module="Proxy", + call_function="create_pure", + call_params={"proxy_type": proxy_type.value, "delay": delay, "index": idx}, + ) + 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 remove_proxy( subtensor: "SubtensorInterface", wallet: "Wallet", @@ -109,7 +142,19 @@ async def remove_proxy( f"This will remove a proxy of type {proxy_type.value} for delegate {delegate}." f"Do you want to proceed?" ): - return + return None + if not (ulw := unlock_key(wallet, print_out=not json_output)).success: + if not json_output: + err_console.print(ulw.message) + else: + json_console.print_json( + data={ + "success": ulw.success, + "message": ulw.message, + "extrinsic_id": None, + } + ) + return None call = await subtensor.substrate.compose_call( call_module="Proxy", call_function="remove_proxy", @@ -119,37 +164,15 @@ async def remove_proxy( "delegate": delegate, }, ) - success, msg, receipt = await subtensor.sign_and_send_extrinsic( - call=call, + return await submit_proxy( + subtensor=subtensor, wallet=wallet, + call=call, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - era={"period": period}, + period=period, + json_output=json_output, ) - if success: - await print_extrinsic_id(receipt) - if json_output: - json_console.print_json( - data={ - "success": success, - "message": msg, - "extrinsic_id": await receipt.get_extrinsic_identifier(), - } - ) - else: - console.print("Success!") # TODO add more shit here - - else: - if json_output: - json_console.print_json( - data={ - "success": success, - "message": msg, - "extrinsic_id": None, - } - ) - else: - err_console.print(f"Failure: {msg}") # TODO add more shit here async def add_proxy( @@ -169,7 +192,19 @@ async def add_proxy( f"This will add a proxy of type {proxy_type.value} for delegate {delegate}." f"Do you want to proceed?" ): - return + return None + if not (ulw := unlock_key(wallet, print_out=not json_output)).success: + if not json_output: + err_console.print(ulw.message) + else: + json_console.print_json( + data={ + "success": ulw.success, + "message": ulw.message, + "extrinsic_id": None, + } + ) + return None call = await subtensor.substrate.compose_call( call_module="Proxy", call_function="add_proxy", @@ -179,34 +214,66 @@ async def add_proxy( "delegate": delegate, }, ) - success, msg, receipt = await subtensor.sign_and_send_extrinsic( - call=call, + return await submit_proxy( + subtensor=subtensor, wallet=wallet, + call=call, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - era={"period": period}, + period=period, + json_output=json_output, ) - if success: - await print_extrinsic_id(receipt) - if json_output: - json_console.print_json( - data={ - "success": success, - "message": msg, - "extrinsic_id": await receipt.get_extrinsic_identifier(), - } - ) - else: - console.print("Success!") # TODO add more shit here - else: - if json_output: + +async def kill_proxy( + subtensor: "SubtensorInterface", + wallet: "Wallet", + proxy_type: ProxyType, + spawner: str, + height: int, + ext_index: int, + idx: int, + prompt: bool, + wait_for_inclusion: bool, + wait_for_finalization: bool, + period: int, + json_output: bool, +) -> None: + if prompt: + if not Confirm.ask( + f"This will kill a Pure Proxy of type {proxy_type.value}. Do you want to proceed?", + ): + return None + if not (ulw := unlock_key(wallet, print_out=not json_output)).success: + if not json_output: + err_console.print(ulw.message) + else: json_console.print_json( data={ - "success": success, - "message": msg, + "success": ulw.success, + "message": ulw.message, "extrinsic_id": None, } ) - else: - err_console.print(f"Failure: {msg}") # TODO add more shit here + return None + spawner = wallet.coldkey.ss58_address + call = await subtensor.substrate.compose_call( + call_module="Proxy", + call_function="kill_pure", + call_params={ + "proxy_type": proxy_type.value, + "index": idx, + "height": height, + "ext_index": ext_index, + "spawner": spawner, + }, + ) + 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, + ) From 57624fe77b91baf4912628baa2e60bcad39c6cd4 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 18:31:46 +0200 Subject: [PATCH 006/179] Add proxy support to sign_and_send_extrinsic --- bittensor_cli/src/bittensor/subtensor_interface.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 304ad83e6..ff06e3e08 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1150,6 +1150,7 @@ async def sign_and_send_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, era: Optional[dict[str, int]] = None, + proxy: Optional[str] = None, ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1159,9 +1160,19 @@ async def sign_and_send_extrinsic( :param wait_for_inclusion: whether to wait until the extrinsic call is included on the chain :param wait_for_finalization: whether to wait until the extrinsic call is finalized on the chain :param era: The length (in blocks) for which a transaction should be valid. + :param proxy: The real account used to create the proxy. None if not using a proxy for this call. :return: (success, error message) """ + if proxy is not None: + call = await self.substrate.compose_call( + "Proxy", + "proxy", + { + "real": proxy, + "call": call, + } + ) call_args: dict[str, Union[GenericCall, Keypair, dict[str, int]]] = { "call": call, "keypair": wallet.coldkey, From 458fb701da539210e2ad84e97e77eabc09a1da4a Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 18:43:34 +0200 Subject: [PATCH 007/179] Subnets Register proxy added --- bittensor_cli/cli.py | 40 +++++++++++++------ .../src/bittensor/extrinsics/registration.py | 10 ++++- .../src/bittensor/extrinsics/root.py | 4 +- .../src/bittensor/subtensor_interface.py | 4 +- bittensor_cli/src/commands/proxy.py | 3 +- bittensor_cli/src/commands/subnets/subnets.py | 6 ++- 6 files changed, 46 insertions(+), 21 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f03047a04..95a5257af 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -23,8 +23,10 @@ ConnectionClosed, InvalidHandshake, ) -import bittensor_wallet from bittensor_wallet import Wallet +from bittensor_wallet.utils import ( + is_valid_ss58_address as btwallet_is_valid_ss58_address, +) from rich import box from rich.prompt import Confirm, FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table @@ -121,6 +123,14 @@ def arg__(arg_name: str) -> str: return f"[{COLORS.G.ARG}]{arg_name}[/{COLORS.G.ARG}]" +def is_valid_ss58_address_param(address: Optional[str]) -> Optional[str]: + if address is None: + return None + elif not btwallet_is_valid_ss58_address(address): + raise typer.BadParameter(f"Invalid SS58 address: {address}") + return address + + class Options: """ Re-usable typer args @@ -143,6 +153,14 @@ def edit_help(cls, option_name: str, help_text: str): setattr(copied_attr, "help", help_text) return copied_attr + proxy = Annotated[ + Optional[str], + typer.Option( + callback=is_valid_ss58_address_param, + help="Optional proxy account to use to make this call", + ), + ] + wallet_name = typer.Option( None, "--wallet-name", @@ -404,12 +422,6 @@ def parse_to_list( raise typer.BadParameter(error_message) -def is_valid_ss58_address_param(address: str) -> str: - if not bittensor_wallet.utils.is_valid_ss58_address(address): - raise typer.BadParameter(f"Invalid SS58 address: {address}") - return address - - def verbosity_console_handler(verbosity_level: int = 1) -> None: """ Sets verbosity level of console output @@ -6669,6 +6681,7 @@ def subnets_register( help="Length (in blocks) for which the transaction should be valid. Note that it is possible that if you " "use an era for this transaction that you may pay a different fee to register than the one stated.", ), + proxy: Options.proxy = None, json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -6696,12 +6709,13 @@ def subnets_register( logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\nperiod: {period}\n") return self._run_command( subnets.register( - wallet, - self.initialize_chain(network), - netuid, - period, - json_output, - prompt, + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + era=period, + json_output=json_output, + prompt=prompt, + proxy=proxy, ) ) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index a32bc1c3d..3147f3129 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -681,6 +681,7 @@ async def burned_register_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, era: Optional[int] = None, + proxy: Optional[str] = None, ) -> tuple[bool, str, Optional[str]]: """Registers the wallet to chain by recycling TAO. @@ -693,7 +694,7 @@ async def burned_register_extrinsic( :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. :param era: the period (in blocks) for which the transaction should remain valid. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. + :param proxy: the proxy address to use for the call. :return: (success, msg), where success is `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. @@ -758,7 +759,12 @@ async def burned_register_extrinsic( }, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization, era=era_ + call, + wallet, + wait_for_inclusion, + wait_for_finalization, + era=era_, + proxy=proxy, ) if not success: diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index ea515ed1a..f95d9990e 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -292,6 +292,7 @@ async def root_register_extrinsic( wallet: Wallet, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, + proxy: Optional[str] = None, ) -> tuple[bool, str, Optional[str]]: r"""Registers the wallet to root network. @@ -301,7 +302,7 @@ async def root_register_extrinsic( `False` if the extrinsic fails to enter the block within the timeout. :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. + :param proxy: Optional proxy to use for making the call. :return: (success, msg), with success being `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. @@ -331,6 +332,7 @@ async def root_register_extrinsic( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) if not success: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index ff06e3e08..f0cd6aa5b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1162,7 +1162,7 @@ async def sign_and_send_extrinsic( :param era: The length (in blocks) for which a transaction should be valid. :param proxy: The real account used to create the proxy. None if not using a proxy for this call. - :return: (success, error message) + :return: (success, error message, extrinsic receipt | None) """ if proxy is not None: call = await self.substrate.compose_call( @@ -1171,7 +1171,7 @@ async def sign_and_send_extrinsic( { "real": proxy, "call": call, - } + }, ) call_args: dict[str, Union[GenericCall, Keypair, dict[str, int]]] = { "call": call, diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 765eb2023..b2fc20777 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -40,7 +40,7 @@ class ProxyType(str, Enum): async def submit_proxy( subtensor: "SubtensorInterface", - wallet: Wallet, + wallet: "Wallet", call: GenericCall, wait_for_inclusion: bool, wait_for_finalization: bool, @@ -229,7 +229,6 @@ async def kill_proxy( subtensor: "SubtensorInterface", wallet: "Wallet", proxy_type: ProxyType, - spawner: str, height: int, ext_index: int, idx: int, diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index c2346880b..ed2f4350b 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1718,6 +1718,7 @@ async def register( era: Optional[int], json_output: bool, prompt: bool, + proxy: Optional[str] = None, ): """Register neuron by recycling some TAO.""" @@ -1820,7 +1821,9 @@ async def register( return if netuid == 0: - success, msg, ext_id = await root_register_extrinsic(subtensor, wallet=wallet) + success, msg, ext_id = await root_register_extrinsic( + subtensor, wallet=wallet, proxy=proxy + ) else: success, msg, ext_id = await burned_register_extrinsic( subtensor, @@ -1828,6 +1831,7 @@ async def register( netuid=netuid, old_balance=balance, era=era, + proxy=proxy, ) if json_output: json_console.print( From 4effee267bbb5045cbe8a32018a6ea909b9955f5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 18:46:25 +0200 Subject: [PATCH 008/179] Docstring --- bittensor_cli/cli.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 95a5257af..c1ae1e571 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -124,6 +124,19 @@ def arg__(arg_name: str) -> str: def is_valid_ss58_address_param(address: Optional[str]) -> Optional[str]: + """ + Evaluates whether a non-None address is a valid SS58 address. Used as a callback for + Annotated typer params. + + Args: + address: an SS58 address, or None + + Returns: + the SS58 address (if valid) or None (if None) + + Raises: + typer.BadParameter: if the address is not a valid SS58 address + """ if address is None: return None elif not btwallet_is_valid_ss58_address(address): @@ -157,7 +170,7 @@ def edit_help(cls, option_name: str, help_text: str): Optional[str], typer.Option( callback=is_valid_ss58_address_param, - help="Optional proxy account to use to make this call", + help="Optional proxy account SS58 to use to make this call", ), ] From 8b5a79e2a8230c2acc738346731246873bd448c9 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 20:20:41 +0200 Subject: [PATCH 009/179] Transfer proxy added --- bittensor_cli/cli.py | 10 +++++- .../src/bittensor/extrinsics/transfer.py | 34 +++++++------------ bittensor_cli/src/commands/wallets.py | 2 ++ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c1ae1e571..435f80265 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1144,9 +1144,15 @@ def __init__(self): self.proxy_app.command( "create", # TODO add rich help panel )(self.proxy_create) + self.proxy_app.command( + "add", # TODO add rich help panel + )(self.proxy_add) self.proxy_app.command( "remove", # TODO add rich help panel )(self.proxy_remove) + self.proxy_app.command( + "kill", # TODO add rich help panel + )(self.proxy_kill) # Sub command aliases # Wallet @@ -1948,8 +1954,8 @@ def ask_partial_stake( logger.debug(f"Partial staking {partial_staking}") return False + @staticmethod def ask_subnet_mechanism( - self, mechanism_id: Optional[int], mechanism_count: int, netuid: int, @@ -2269,6 +2275,7 @@ def wallet_transfer( help="Transfer balance even if the resulting balance falls below the existential deposit.", ), period: int = Options.period, + proxy: Options.proxy_type = None, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -2336,6 +2343,7 @@ def wallet_transfer( era=period, prompt=prompt, json_output=json_output, + proxy=proxy, ) ) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 6886fb41a..58eadb92a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -30,6 +30,7 @@ async def transfer_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, + proxy: Optional[str] = None, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Transfers funds from this wallet to the destination public key address. @@ -45,6 +46,8 @@ async def transfer_extrinsic( :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. :param prompt: If `True`, the call waits for confirmation from the user before proceeding. + :param proxy: Optional proxy to use for this call. + :return: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is `True`, regardless of its inclusion. """ @@ -75,7 +78,7 @@ async def get_transfer_fee() -> Balance: return Balance.from_rao(payment_info["partial_fee"]) - async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: + async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt]]: """ Makes transfer from wallet to destination public key address. :return: success, block hash, formatted error message @@ -85,29 +88,16 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: call_function=call_function, call_params=call_params, ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, + success_, error_msg_, receipt_ = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, + proxy=proxy, + era={"period": era}, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, "", "", response - - # Otherwise continue with finalization. - if await response.is_success: - block_hash_ = response.block_hash - return True, block_hash_, "", response - else: - return ( - False, - "", - format_error_message(await response.error_message), - response, - ) + block_hash_ = receipt_.block_hash if receipt_ is not None else "" + return success_, block_hash_, error_msg_, receipt_ # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 6473f2c69..de681584b 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1530,6 +1530,7 @@ async def transfer( era: int, prompt: bool, json_output: bool, + proxy: Optional[str] = None, ): """Transfer token of amount to destination.""" result, ext_receipt = await transfer_extrinsic( @@ -1541,6 +1542,7 @@ async def transfer( allow_death=allow_death, era=era, prompt=prompt, + proxy=proxy, ) ext_id = (await ext_receipt.get_extrinsic_identifier()) if result else None if json_output: From 14163dd0b5f8c477ca9784d10b7360c5be06710d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 22:03:28 +0200 Subject: [PATCH 010/179] Swap hotkey --- bittensor_cli/cli.py | 9 ++++++++- bittensor_cli/src/bittensor/extrinsics/registration.py | 3 ++- bittensor_cli/src/commands/wallets.py | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 435f80265..cc50c4caf 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2362,6 +2362,7 @@ def wallet_swap_hotkey( verbose: bool = Options.verbose, prompt: bool = Options.prompt, json_output: bool = Options.json_output, + proxy: Options.proxy = None, ): """ Swap hotkeys of a given wallet on the blockchain. For a registered key pair, for example, a (coldkeyA, hotkeyA) pair, this command swaps the hotkeyA with a new, unregistered, hotkeyB to move the original registration to the (coldkeyA, hotkeyB) pair. @@ -2441,7 +2442,13 @@ def wallet_swap_hotkey( self.initialize_chain(network) return self._run_command( wallets.swap_hotkey( - original_wallet, new_wallet, self.subtensor, netuid, prompt, json_output + original_wallet=original_wallet, + new_wallet=new_wallet, + subtensor=self.subtensor, + netuid=netuid, + proxy=proxy, + prompt=prompt, + json_output=json_output, ) ) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 3147f3129..0c4e4f585 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -1758,6 +1758,7 @@ async def swap_hotkey_extrinsic( wallet: Wallet, new_wallet: Wallet, netuid: Optional[int] = None, + proxy: Optional[str] = None, prompt: bool = False, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """ @@ -1843,7 +1844,7 @@ async def swap_hotkey_extrinsic( call_params=call_params, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call=call, wallet=wallet, proxy=proxy ) if success: diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index de681584b..6f60bf825 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1736,6 +1736,7 @@ async def swap_hotkey( new_wallet: Wallet, subtensor: SubtensorInterface, netuid: Optional[int], + proxy: Optional[str], prompt: bool, json_output: bool, ): @@ -1746,6 +1747,7 @@ async def swap_hotkey( new_wallet, netuid=netuid, prompt=prompt, + proxy=proxy, ) if result: ext_id = await ext_receipt.get_extrinsic_identifier() From c8e7329bc53a24b1f590454963003c8d3e619066 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 22:07:56 +0200 Subject: [PATCH 011/179] Add spawner to proxy kill --- bittensor_cli/cli.py | 8 ++++++++ bittensor_cli/src/commands/proxy.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index cc50c4caf..451a6c9f6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8295,6 +8295,13 @@ def proxy_kill( f" ([{COLORS.G.ARG}]btcli proxy create[/{COLORS.G.ARG}])", prompt="Enter the extrinsic index of the `btcli proxy create` event.", ), + spawner: Annotated[ + Optional[str], + typer.Option( + callback=is_valid_ss58_address_param, + help="The SS58 of the pure proxy creator account. If omitted, the wallet's coldkeypub is used.", + ), + ] = None, network: Optional[list[str]] = Options.network, proxy_type: ProxyType = Options.proxy_type, idx: int = typer.Option(0, "--index", help="TODO lol"), @@ -8331,6 +8338,7 @@ def proxy_kill( height=height, ext_index=ext_index, idx=idx, + spawner=spawner, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index b2fc20777..09f9b285d 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from rich.prompt import Confirm from scalecodec import GenericCall @@ -231,6 +231,7 @@ async def kill_proxy( proxy_type: ProxyType, height: int, ext_index: int, + spawner: Optional[str], idx: int, prompt: bool, wait_for_inclusion: bool, @@ -255,7 +256,7 @@ async def kill_proxy( } ) return None - spawner = wallet.coldkey.ss58_address + spawner = spawner or wallet.coldkeypub.ss58_address call = await subtensor.substrate.compose_call( call_module="Proxy", call_function="kill_pure", From 1ff4795f6fbe720997704e441db36fcf5d813e36 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 23:49:49 +0200 Subject: [PATCH 012/179] Better proxy stuff --- bittensor_cli/cli.py | 98 ++++++++++++++++--- bittensor_cli/src/__init__.py | 1 + .../src/bittensor/subtensor_interface.py | 5 +- bittensor_cli/src/commands/proxy.py | 51 +++++++++- 4 files changed, 134 insertions(+), 21 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 451a6c9f6..8906e5535 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -166,14 +166,6 @@ def edit_help(cls, option_name: str, help_text: str): setattr(copied_attr, "help", help_text) return copied_attr - proxy = Annotated[ - Optional[str], - typer.Option( - callback=is_valid_ss58_address_param, - help="Optional proxy account SS58 to use to make this call", - ), - ] - wallet_name = typer.Option( None, "--wallet-name", @@ -745,6 +737,7 @@ def __init__(self): "safe_staking": True, "allow_partial_stake": False, "dashboard_path": None, + "proxies": {}, # Commenting this out as this needs to get updated # "metagraph_cols": { # "UID": True, @@ -919,6 +912,8 @@ def __init__(self): self.config_app.command("set")(self.set_config) self.config_app.command("get")(self.get_config) self.config_app.command("clear")(self.del_config) + self.config_app.command("add-proxy")(self.config_add_proxy) + self.config_app.command("proxies")(self.config_get_proxies) # self.config_app.command("metagraph", hidden=True)(self.metagraph_config) # wallet commands @@ -1457,7 +1452,7 @@ def main_callback( # Load or create the config file if os.path.exists(self.config_path): with open(self.config_path, "r") as f: - config = safe_load(f) + config = safe_load(f) or {} else: directory_path = Path(self.config_base_path) directory_path.mkdir(exist_ok=True, parents=True) @@ -1837,6 +1832,82 @@ def get_config(self): console.print(table) + def config_add_proxy( + self, + name: Annotated[ + str, + typer.Option( + help="Name of the proxy", prompt="Enter a name for this proxy" + ), + ], + address: Annotated[ + str, + typer.Option( + callback=is_valid_ss58_address_param, + help="The SS58 address of the pure proxy", + prompt="Enter the SS58 address of the pure proxy", + ), + ], + proxy_type: Annotated[ + ProxyType, + typer.Option( + help="The type of this pure proxy", + prompt="Enter the type of this pure proxy", + ), + ], + ): + proxies = self.config.get("proxies", {}) + proxies[name] = {"proxy_type": proxy_type.value, "address": address} + self.config["proxies"] = proxies + with open(self.config_path, "w") as f: + safe_dump(self.config, f) + self.config_get_proxies() + + def config_get_proxies(self): + proxies = self.config.get("proxies", {}) + table = Table( + Column("[bold white]Name", style=f"{COLORS.G.ARG}"), + Column("[bold white]Address", style="gold1"), + Column("Proxy Type", style="medium_purple"), + box=box.SIMPLE_HEAD, + title=f"[{COLORS.G.HEADER}]BTCLI Config Proxies[/{COLORS.G.HEADER}]: {arg__(self.config_path)}", + ) + for name, keys in proxies.items(): + address = keys.get("address") + proxy_type = keys.get("proxy_type") + table.add_row(name, address, proxy_type) + console.print(table) + + def is_valid_proxy_name_or_ss58(self, address: Optional[str]) -> Optional[str]: + """ + Evaluates whether a non-None address is a valid SS58 address. Used as a callback for + Annotated typer params. + + Args: + address: an SS58 address, proxy name in config, or None + + Returns: + the SS58 address (if valid) or None (if None) + + Raises: + typer.BadParameter: if the address is not a valid SS58 address + """ + if address is None: + return None + config_proxies = self.config.get("proxies", {}) + outer_proxy_from_config = config_proxies.get(address, {}) + proxy_from_config = outer_proxy_from_config.get("address") + if proxy_from_config is not None: + if not btwallet_is_valid_ss58_address(proxy_from_config): + raise typer.BadParameter( + f"Invalid SS58 address: {proxy_from_config} from config {address}" + ) + else: + return proxy_from_config + elif not btwallet_is_valid_ss58_address(address): + raise typer.BadParameter(f"Invalid SS58 address: {address}") + return address + def ask_rate_tolerance( self, rate_tolerance: Optional[float], @@ -2275,7 +2346,7 @@ def wallet_transfer( help="Transfer balance even if the resulting balance falls below the existential deposit.", ), period: int = Options.period, - proxy: Options.proxy_type = None, + proxy: Optional[str] = None, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -2308,6 +2379,7 @@ def wallet_transfer( raise typer.Exit() self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2362,7 +2434,7 @@ def wallet_swap_hotkey( verbose: bool = Options.verbose, prompt: bool = Options.prompt, json_output: bool = Options.json_output, - proxy: Options.proxy = None, + proxy: Optional[str] = None, ): """ Swap hotkeys of a given wallet on the blockchain. For a registered key pair, for example, a (coldkeyA, hotkeyA) pair, this command swaps the hotkeyA with a new, unregistered, hotkeyB to move the original registration to the (coldkeyA, hotkeyB) pair. @@ -2389,6 +2461,7 @@ def wallet_swap_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) + proxy = self.is_valid_proxy_name_or_ss58(proxy) self.verbosity_handler(quiet, verbose, json_output) # Warning for netuid 0 - only swaps on root network, not a full swap @@ -6709,7 +6782,7 @@ def subnets_register( help="Length (in blocks) for which the transaction should be valid. Note that it is possible that if you " "use an era for this transaction that you may pay a different fee to register than the one stated.", ), - proxy: Options.proxy = None, + proxy: Optional[str] = None, json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -6727,6 +6800,7 @@ def subnets_register( [green]$[/green] btcli subnets register --netuid 1 """ self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, wallet_path, diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 04e7de999..0d17ccaf4 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -116,6 +116,7 @@ class config: "HOTKEY": True, "COLDKEY": True, }, + "proxies": {}, } class subtensor: diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index f0cd6aa5b..630cf8f27 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1168,10 +1168,7 @@ async def sign_and_send_extrinsic( call = await self.substrate.compose_call( "Proxy", "proxy", - { - "real": proxy, - "call": call, - }, + {"real": proxy, "call": call, "force_proxy_type": None}, ) call_args: dict[str, Union[GenericCall, Keypair, dict[str, int]]] = { "call": call, diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 09f9b285d..43175782a 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -114,15 +114,56 @@ async def create_proxy( call_function="create_pure", call_params={"proxy_type": proxy_type.value, "delay": delay, "index": idx}, ) - return await submit_proxy( - subtensor=subtensor, - wallet=wallet, + success, msg, receipt = await subtensor.sign_and_send_extrinsic( call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - period=period, - json_output=json_output, + era={"period": period}, ) + if success: + await print_extrinsic_id(receipt) + created_pure = None + created_spawner = None + created_proxy_type = None + for event in await receipt.triggered_events: + if event["event_id"] == "PureCreated": + attrs = event["attributes"] + created_pure = attrs["pure"] + created_spawner = attrs["who"] + created_proxy_type = attrs["proxy_type"] + arg_start = "`" if json_output else "[blue]" + arg_end = "`" if json_output else "[/blue]" + msg = ( + f"Created pure '{created_pure}' from spawner '{created_spawner}' with proxy type '{created_proxy_type}'." + f" You can add this to your config with {arg_start}" + f"btcli config add-proxy " + f"--name --address {created_pure} --proxy-type {created_proxy_type}" + f"{arg_end}" + ) + # TODO add to wallets somehow? + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_id": await receipt.get_extrinsic_identifier(), + } + ) + else: + console.print(f"Success! {msg}") # TODO add more shit here + + else: + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_id": None, + } + ) + else: + err_console.print(f"Failure: {msg}") # TODO add more shit here async def remove_proxy( From b29728d28c6c3dee76dda8c89db043473a0b4c0f Mon Sep 17 00:00:00 2001 From: Dera Okeke Date: Thu, 6 Nov 2025 23:03:36 +0100 Subject: [PATCH 013/179] proxy help text --- bittensor_cli/cli.py | 48 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f03047a04..12ffc0fe7 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8109,9 +8109,19 @@ def proxy_create( json_output: bool = Options.json_output, ): """ - TODO ask someone else to write the docs + Creates a new pure proxy account. The pure proxy account is a keyless account controlled by your wallet. + + [bold]Note[/bold]: The proxy account has no private key and cannot sign transactions independently. + All operations must be initiated and signed by the delegator. + + + [bold]Common Examples:[/bold] + 1. Create a pure proxy account + [green]$[/green] btcli proxy create --proxy-type any + + 2. Create a delayed pure proxy account + [green]$[/green] btcli proxy create --proxy-type any --delay 1000 - Creates a pure proxy """ # TODO add debug logger self.verbosity_handler(quiet, verbose, json_output) @@ -8163,7 +8173,18 @@ def proxy_add( json_output: bool = Options.json_output, ): """ - Adds a proxy + + Registers an existing account as a standard proxy for the delegator. + + Grants an existing account permission to execute transactions on your behalf with + specified restrictions. + + [bold]Common Examples:[/bold] + 1. Create a standard proxy account + [green]$[/green] btcli proxy add --delegate 5GDeleg... --proxy-type small-transfer + + 2. Create a delayed standard proxy account + [green]$[/green] btcli proxy add --delegate 5GDeleg... --proxy-type transfer --delay 500 """ # TODO add debug logger @@ -8215,7 +8236,20 @@ def proxy_remove( json_output: bool = Options.json_output, ): """ - Removes a proxy + Unregisters a proxy from an account. + + Revokes proxy permissions previously granted to another account. This prevents the delegate account from executing any further transactions on your behalf. + + [bold]Note[/bold]: You can specify a delegate to remove a single proxy or use the `--all` flag to remove all existing proxies linked to an account. + + + [bold]Common Examples:[/bold] + 1. Revoke proxy permissions from a single proxy account + [green]$[/green] btcli proxy remove --delegate 5GDel... --proxy-type transfer + + 2. Remove all proxies linked to an account + [green]$[/green] btcli proxy remove --all + """ # TODO should add a --all flag to call Proxy.remove_proxies ? # TODO add debug logger @@ -8269,7 +8303,11 @@ def proxy_kill( ): """ - Kills a pure proxy + Permanently removes a pure proxy account. + + Once killed, the pure proxy account is cleared from chain storage and cannot be recovered. + + Requires the spawner account, proxy type, and creation block details.... TBC """ # TODO add debug logger self.verbosity_handler(quiet, verbose, json_output) From 14b755d4651a6c2eda6ad9d7c8abb44e7547827b Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 00:42:11 +0200 Subject: [PATCH 014/179] Working kill --- bittensor_cli/cli.py | 14 +++++++++++--- bittensor_cli/src/commands/proxy.py | 5 +++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8906e5535..3012c0245 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -388,6 +388,11 @@ def edit_help(cls, option_name: str, help_text: str): help="Type of proxy", prompt=True, ) + proxy: Optional[str] = typer.Option( + None, + "--proxy", + help="Optional proxy to use for the transaction.", + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -2346,7 +2351,7 @@ def wallet_transfer( help="Transfer balance even if the resulting balance falls below the existential deposit.", ), period: int = Options.period, - proxy: Optional[str] = None, + proxy: Optional[str] = Options.proxy, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -2434,7 +2439,7 @@ def wallet_swap_hotkey( verbose: bool = Options.verbose, prompt: bool = Options.prompt, json_output: bool = Options.json_output, - proxy: Optional[str] = None, + proxy: Optional[str] = Options.proxy, ): """ Swap hotkeys of a given wallet on the blockchain. For a registered key pair, for example, a (coldkeyA, hotkeyA) pair, this command swaps the hotkeyA with a new, unregistered, hotkeyB to move the original registration to the (coldkeyA, hotkeyB) pair. @@ -6782,7 +6787,7 @@ def subnets_register( help="Length (in blocks) for which the transaction should be valid. Note that it is possible that if you " "use an era for this transaction that you may pay a different fee to register than the one stated.", ), - proxy: Optional[str] = None, + proxy: Optional[str] = Options.proxy, json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -8378,6 +8383,7 @@ def proxy_kill( ] = None, network: Optional[list[str]] = Options.network, proxy_type: ProxyType = Options.proxy_type, + proxy: Optional[str] = Options.proxy, idx: int = typer.Option(0, "--index", help="TODO lol"), wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, @@ -8396,6 +8402,7 @@ def proxy_kill( """ # TODO add debug logger self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, @@ -8410,6 +8417,7 @@ def proxy_kill( wallet=wallet, proxy_type=proxy_type, height=height, + proxy=proxy, ext_index=ext_index, idx=idx, spawner=spawner, diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 43175782a..52bbd083b 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -46,6 +46,7 @@ async def submit_proxy( wait_for_finalization: bool, period: int, json_output: bool, + proxy: Optional[str] = None, ) -> None: success, msg, receipt = await subtensor.sign_and_send_extrinsic( call=call, @@ -53,6 +54,7 @@ async def submit_proxy( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, era={"period": period}, + proxy=proxy, ) if success: await print_extrinsic_id(receipt) @@ -274,6 +276,7 @@ async def kill_proxy( ext_index: int, spawner: Optional[str], idx: int, + proxy: Optional[str], prompt: bool, wait_for_inclusion: bool, wait_for_finalization: bool, @@ -309,6 +312,7 @@ async def kill_proxy( "spawner": spawner, }, ) + return await submit_proxy( subtensor=subtensor, wallet=wallet, @@ -317,4 +321,5 @@ async def kill_proxy( wait_for_finalization=wait_for_finalization, period=period, json_output=json_output, + proxy=proxy, ) From fe6dee15de2d868cc488f982f4f87fe6f8b483f9 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 11:39:14 +0200 Subject: [PATCH 015/179] More added --- bittensor_cli/cli.py | 73 ++++++----- .../src/bittensor/subtensor_interface.py | 16 ++- bittensor_cli/src/commands/stake/add.py | 114 +++++++++--------- .../src/commands/stake/auto_staking.py | 2 + bittensor_cli/src/commands/wallets.py | 8 +- 5 files changed, 122 insertions(+), 91 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 3012c0245..007fad03b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -391,7 +391,8 @@ def edit_help(cls, option_name: str, help_text: str): proxy: Optional[str] = typer.Option( None, "--proxy", - help="Optional proxy to use for the transaction.", + help="Optional proxy to use for the transaction: either the SS58 or the name of the proxy if you " + f"have added it with {arg__('btcli config add-proxy')}.", ) @@ -3035,6 +3036,7 @@ def wallet_associate_hotkey( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, network: Optional[list[str]] = Options.network, + proxy: Optional[str] = Options.proxy, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3052,6 +3054,7 @@ def wallet_associate_hotkey( [green]$[/green] btcli wallet associate-hotkey --hotkey-ss58 5DkQ4... """ self.verbosity_handler(quiet, verbose) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if not wallet_name: wallet_name = Prompt.ask( "Enter the [blue]wallet name[/blue] [dim](which you want to associate with the hotkey)[/dim]", @@ -3103,6 +3106,7 @@ def wallet_associate_hotkey( hotkey_ss58, hotkey_display, prompt, + proxy=proxy, ) ) @@ -3527,6 +3531,7 @@ def wallet_set_id( "--github", help="The GitHub repository for the identity.", ), + proxy: Optional[str] = Options.proxy, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -3550,6 +3555,7 @@ def wallet_set_id( [bold]Note[/bold]: This command should only be used if the user is willing to incur the a recycle fee associated with setting an identity on the blockchain. It is a high-level command that makes changes to the blockchain state and should not be used programmatically as part of other scripts or applications. """ self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -3589,17 +3595,17 @@ def wallet_set_id( return self._run_command( wallets.set_id( - wallet, - self.initialize_chain(network), - identity["name"], - identity["url"], - identity["image"], - identity["discord"], - identity["description"], - identity["additional"], - identity["github_repo"], - prompt, - json_output, + wallet=wallet, + subtensor=self.initialize_chain(network), + name=identity["name"], + web_url=identity["url"], + image_url=identity["image"], + discord=identity["discord"], + description=identity["description"], + additional=identity["additional"], + github_repo=identity["github_repo"], + json_output=json_output, + proxy=proxy, ) ) @@ -3666,7 +3672,11 @@ def wallet_get_id( coldkey_ss58 = wallet.coldkeypub.ss58_address return self._run_command( - wallets.get_id(self.initialize_chain(network), coldkey_ss58, json_output) + wallets.get_id( + subtensor=self.initialize_chain(network), + ss58_address=coldkey_ss58, + json_output=json_output, + ) ) def wallet_sign( @@ -3786,6 +3796,7 @@ def wallet_swap_coldkey( help="SS58 address of the new coldkey that will replace the current one.", ), network: Optional[list[str]] = Options.network, + proxy: Optional[str] = Options.proxy, quiet: bool = Options.quiet, verbose: bool = Options.verbose, force_swap: bool = typer.Option( @@ -3808,6 +3819,7 @@ def wallet_swap_coldkey( [green]$[/green] btcli wallet schedule-coldkey-swap --new-coldkey-ss58 5Dk...X3q """ self.verbosity_handler(quiet, verbose) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if not wallet_name: wallet_name = Prompt.ask( @@ -3859,6 +3871,7 @@ def wallet_swap_coldkey( subtensor=self.initialize_chain(network), new_coldkey_ss58=new_wallet_coldkey_ss58, force_swap=force_swap, + proxy=proxy, ) ) @@ -3923,6 +3936,7 @@ def set_auto_stake( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, netuid: Optional[int] = Options.netuid_not_req, + proxy: Optional[str] = Options.proxy, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -3933,6 +3947,7 @@ def set_auto_stake( """Set the auto-stake destination hotkey for a coldkey.""" self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, @@ -3985,6 +4000,7 @@ def set_auto_stake( self.initialize_chain(network), netuid, hotkey_ss58, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt_user=prompt, @@ -4113,6 +4129,7 @@ def stake_add( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, network: Optional[list[str]] = Options.network, rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, @@ -4159,6 +4176,7 @@ def stake_add( """ netuids = netuids or [] self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy) safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: rate_tolerance = self.ask_rate_tolerance(rate_tolerance) @@ -4349,20 +4367,21 @@ def stake_add( ) return self._run_command( add_stake.stake_add( - wallet, - self.initialize_chain(network), - netuids, - stake_all, - amount, - prompt, - all_hotkeys, - include_hotkeys, - exclude_hotkeys, - safe_staking, - rate_tolerance, - allow_partial_stake, - json_output, - period, + wallet=wallet, + subtensor=self.initialize_chain(network), + netuids=netuids, + stake_all=stake_all, + amount=amount, + prompt=prompt, + all_hotkeys=all_hotkeys, + include_hotkeys=include_hotkeys, + exclude_hotkeys=exclude_hotkeys, + safe_staking=safe_staking, + rate_tolerance=rate_tolerance, + allow_partial_stake=allow_partial_stake, + json_output=json_output, + era=period, + proxy=proxy, ) ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 630cf8f27..7e5ab92cd 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1151,6 +1151,7 @@ async def sign_and_send_extrinsic( wait_for_finalization: bool = False, era: Optional[dict[str, int]] = None, proxy: Optional[str] = None, + nonce: Optional[str] = None, ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1161,6 +1162,7 @@ async def sign_and_send_extrinsic( :param wait_for_finalization: whether to wait until the extrinsic call is finalized on the chain :param era: The length (in blocks) for which a transaction should be valid. :param proxy: The real account used to create the proxy. None if not using a proxy for this call. + :param nonce: The nonce used to submit this extrinsic call. :return: (success, error message, extrinsic receipt | None) """ @@ -1170,9 +1172,10 @@ async def sign_and_send_extrinsic( "proxy", {"real": proxy, "call": call, "force_proxy_type": None}, ) - call_args: dict[str, Union[GenericCall, Keypair, dict[str, int]]] = { + call_args: dict[str, Union[GenericCall, Keypair, dict[str, int], int]] = { "call": call, "keypair": wallet.coldkey, + "nonce": nonce, } if era is not None: call_args["era"] = era @@ -1600,16 +1603,25 @@ async def get_owned_hotkeys( return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []] - async def get_extrinsic_fee(self, call: GenericCall, keypair: Keypair) -> Balance: + async def get_extrinsic_fee( + self, call: GenericCall, keypair: Keypair, proxy: Optional[str] = None + ) -> Balance: """ Determines the fee for the extrinsic call. Args: call: Created extrinsic call keypair: The keypair that would sign the extrinsic (usually you would just want to use the *pub for this) + proxy: Optional proxy for the extrinsic call Returns: Balance object representing the fee for this extrinsic. """ + if proxy is not None: + call = await self.substrate.compose_call( + "Proxy", + "proxy", + {"real": proxy, "call": call, "force_proxy_type": None}, + ) fee_dict = await self.substrate.get_payment_info(call, keypair) return Balance.from_rao(fee_dict["partial_fee"]) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 5046981da..1b5d93bdb 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -46,6 +46,7 @@ async def stake_add( allow_partial_stake: bool, json_output: bool, era: int, + proxy: Optional[str], ): """ Args: @@ -63,6 +64,7 @@ async def stake_add( allow_partial_stake: whether to allow partial stake json_output: whether to output stake info in JSON format era: Blocks for which the transaction should be valid. + proxy: Optional proxy to use for staking. Returns: bool: True if stake operation is successful, False otherwise @@ -105,7 +107,7 @@ async def get_stake_extrinsic_fee( call_function=call_fn, call_params=call_params, ) - return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy) async def safe_stake_extrinsic( netuid_: int, @@ -134,18 +136,15 @@ async def safe_stake_extrinsic( }, ), ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, - keypair=wallet.coldkey, + wallet=wallet, nonce=next_nonce, era={"period": era}, + proxy=proxy, ) - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - except SubstrateRequestException as e: - if "Custom error: 8" in str(e): + if not success_: + if "Custom error: 8" in err_msg: err_msg = ( f"{failure_prelude}: Price exceeded tolerance limit. " f"Transaction rejected because partial staking is disabled. " @@ -153,13 +152,9 @@ async def safe_stake_extrinsic( ) print_error("\n" + err_msg, status=status) else: - err_msg = f"{failure_prelude} with error: {format_error_message(e)}" + err_msg = f"{failure_prelude} with error: {err_msg}" err_out("\n" + err_msg) return False, err_msg, None - if not await response.is_success: - err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" - err_out("\n" + err_msg) - return False, err_msg, None else: if json_output: # the rest of this checking is not necessary if using json_output @@ -208,8 +203,11 @@ async def stake_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: err_out = partial(print_error, status=status) + block_hash = await subtensor.substrate.get_chain_head() current_balance, next_nonce, call = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ), subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -219,61 +217,57 @@ async def stake_extrinsic( "netuid": netuid_i, "amount_staked": amount_.rao, }, + block_hash=block_hash, ), ) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + nonce=next_nonce, + era={"period": era}, + proxy=proxy, ) - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - except SubstrateRequestException as e: - err_msg = f"{failure_prelude} with error: {format_error_message(e)}" + if not success_: + err_msg = f"{failure_prelude} with error: {err_msg}" err_out("\n" + err_msg) return False, err_msg, None else: - if not await response.is_success: - err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" - err_out("\n" + err_msg) - return False, err_msg, None - else: - if json_output: - # the rest of this is not necessary if using json_output - return True, "", response - await print_extrinsic_id(response) - new_block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=new_block_hash - ), - subtensor.get_stake( - hotkey_ss58=staking_address_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid_i, - block_hash=new_block_hash, - ), - ) - console.print( - f":white_heavy_check_mark: " - f"[dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" - ) - console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " - f"Stake:\n" - f" [blue]{current}[/blue] " - f":arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" - ) + if json_output: + # the rest of this is not necessary if using json_output return True, "", response + await print_extrinsic_id(response) + new_block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=new_block_hash + ), + subtensor.get_stake( + hotkey_ss58=staking_address_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid_i, + block_hash=new_block_hash, + ), + ) + console.print( + f":white_heavy_check_mark: " + f"[dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" + ) + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Stake:\n" + f" [blue]{current}[/blue] " + f":arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + ) + return True, "", response netuids = ( netuids if netuids is not None else await subtensor.get_all_subnet_netuids() diff --git a/bittensor_cli/src/commands/stake/auto_staking.py b/bittensor_cli/src/commands/stake/auto_staking.py index 6e8bf3632..d917cc439 100644 --- a/bittensor_cli/src/commands/stake/auto_staking.py +++ b/bittensor_cli/src/commands/stake/auto_staking.py @@ -174,6 +174,7 @@ async def set_auto_stake_destination( subtensor: "SubtensorInterface", netuid: int, hotkey_ss58: str, + proxy: Optional[str] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt_user: bool = True, @@ -269,6 +270,7 @@ async def set_auto_stake_destination( wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) ext_id = await ext_receipt.get_extrinsic_identifier() if success else None diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 6f60bf825..ce40d15f1 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -81,6 +81,7 @@ async def associate_hotkey( hotkey_ss58: str, hotkey_display: str, prompt: bool = False, + proxy: Optional[str] = None, ): """Associates a hotkey with a wallet""" @@ -126,6 +127,7 @@ async def associate_hotkey( wallet, wait_for_inclusion=True, wait_for_finalization=False, + proxy=proxy, ) if not success: @@ -1797,8 +1799,8 @@ async def set_id( description: str, additional: str, github_repo: str, - prompt: bool, json_output: bool = False, + proxy: Optional[str] = None, ) -> bool: """Create a new or update existing identity on-chain.""" output_dict = {"success": False, "identity": None, "error": ""} @@ -1825,7 +1827,7 @@ async def set_id( " :satellite: [dark_sea_green3]Updating identity on-chain...", spinner="earth" ): success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) if not success: @@ -2041,6 +2043,7 @@ async def schedule_coldkey_swap( subtensor: SubtensorInterface, new_coldkey_ss58: str, force_swap: bool = False, + proxy: Optional[str] = None, ) -> bool: """Schedules a coldkey swap operation to be executed at a future block. @@ -2098,6 +2101,7 @@ async def schedule_coldkey_swap( wallet, wait_for_inclusion=True, wait_for_finalization=True, + proxy=proxy, ) block_post_call = await subtensor.substrate.get_block_number() From ac78ee16c2acd9a5be21e9fb0d1ec8ff3f7e8648 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 11:59:26 +0200 Subject: [PATCH 016/179] Stake Remove --- bittensor_cli/cli.py | 4 ++ bittensor_cli/src/commands/stake/remove.py | 73 +++++++++++----------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 007fad03b..fe8325b29 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4430,6 +4430,7 @@ def stake_remove( help="When set, this command unstakes from all the hotkeys associated with the wallet. Do not use if specifying " "hotkeys in `--include-hotkeys`.", ), + proxy: Optional[str] = Options.proxy, rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, @@ -4476,6 +4477,7 @@ def stake_remove( • [blue]--partial[/blue]: Complete partial unstake if rates exceed tolerance """ self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if not unstake_all and not unstake_all_alpha: safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: @@ -4656,6 +4658,7 @@ def stake_remove( prompt=prompt, json_output=json_output, era=period, + proxy=proxy, ) ) elif ( @@ -4728,6 +4731,7 @@ def stake_remove( allow_partial_stake=allow_partial_stake, json_output=json_output, era=period, + proxy=proxy, ) ) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 5d125cc16..eb602bd1a 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -48,6 +48,7 @@ async def unstake( allow_partial_stake: bool, json_output: bool, era: int, + proxy: Optional[str], ): """Unstake from hotkey(s).""" @@ -223,6 +224,7 @@ async def unstake( netuid=netuid, price_limit=price_limit, allow_partial_stake=allow_partial_stake, + proxy=proxy, ) else: extrinsic_fee = await _get_extrinsic_fee( @@ -232,6 +234,7 @@ async def unstake( hotkey_ss58=staking_address_ss58, netuid=netuid, amount=amount_to_unstake_as_balance, + proxy=proxy, ) sim_swap = await subtensor.sim_swap( netuid, 0, amount_to_unstake_as_balance.rao @@ -370,6 +373,7 @@ async def unstake_all( era: int = 3, prompt: bool = True, json_output: bool = False, + proxy: Optional[str] = None, ) -> None: """Unstakes all stakes from all hotkeys in all subnets.""" include_hotkeys = include_hotkeys or [] @@ -431,10 +435,10 @@ async def unstake_all( ) table = Table( title=( - f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{table_title}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" - f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " - f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f"Network: [{COLOR_PALETTE['GENERAL']['HEADER']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" + f"\n[{COLOR_PALETTE.G.HEADER}]{table_title}[/{COLOR_PALETTE.G.HEADER}]\n" + f"Wallet: [{COLOR_PALETTE.G.COLDKEY}]{wallet.name}[/{COLOR_PALETTE.G.COLDKEY}], " + f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n" + f"Network: [{COLOR_PALETTE.G.HEADER}]{subtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n" ), show_footer=True, show_edge=False, @@ -500,6 +504,7 @@ async def unstake_all( wallet, subtensor, hotkey_ss58=stake.hotkey_ss58, + proxy=proxy, ) sim_swap = await subtensor.sim_swap(stake.netuid, 0, stake_amount.rao) received_amount = sim_swap.tao_amount - extrinsic_fee @@ -546,6 +551,7 @@ async def unstake_all( unstake_all_alpha=unstake_all_alpha, status=status, era=era, + proxy=proxy, ) ext_id = await ext_receipt.get_extrinsic_identifier() if success else None successes[hotkey_ss58] = { @@ -630,11 +636,11 @@ async def _unstake_extrinsic( console.print(":white_heavy_check_mark: [green]Finalized[/green]") console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}" ) console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + f"Subnet: [{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" + f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_stake}" ) return True, response @@ -740,21 +746,21 @@ async def _safe_unstake_extrinsic( console.print(":white_heavy_check_mark: [green]Finalized[/green]") console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}" ) amount_unstaked = current_stake - new_stake if allow_partial_stake and (amount_unstaked != amount): console.print( "Partial unstake transaction. Unstaked:\n" - f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_unstaked.set_unit(netuid=netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f" [{COLOR_PALETTE.S.AMOUNT}]{amount_unstaked.set_unit(netuid=netuid)}[/{COLOR_PALETTE.S.AMOUNT}] " f"instead of " f"[blue]{amount}[/blue]" ) console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " - f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + f"Subnet: [{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] " + f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_stake}" ) return True, response @@ -767,6 +773,7 @@ async def _unstake_all_extrinsic( unstake_all_alpha: bool, status=None, era: int = 3, + proxy: Optional[str] = None, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute an unstake all extrinsic. @@ -813,21 +820,16 @@ async def _unstake_all_extrinsic( call_function=call_function, call_params={"hotkey": hotkey_ss58}, ) - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic=await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ), - wait_for_inclusion=True, - wait_for_finalization=False, + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, ) - if not await response.is_success: - err_out( - f"{failure_prelude} with error: " - f"{format_error_message(await response.error_message)}" - ) + if not success_: + err_out(f"{failure_prelude} with error: {err_msg}") return False, None else: await print_extrinsic_id(response) @@ -852,21 +854,18 @@ async def _unstake_all_extrinsic( ) new_root_stake = None - success_message = ( - ":white_heavy_check_mark: [green]Finalized: Successfully unstaked all stakes[/green]" - if not unstake_all_alpha - else ":white_heavy_check_mark: [green]Finalized: Successfully unstaked all Alpha stakes[/green]" - ) + msg_modifier = "Alpha " if unstake_all_alpha else "" + success_message = f":white_heavy_check_mark: [green]Included: Successfully unstaked all {msg_modifier}stakes[/green]" console.print(f"{success_message} from {hotkey_name}") console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}" ) if unstake_all_alpha: console.print( f"Root Stake for {hotkey_name}:\n " f"[blue]{previous_root_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_root_stake}" + f"[{COLOR_PALETTE.S.AMOUNT}]{new_root_stake}" ) return True, response @@ -884,6 +883,7 @@ async def _get_extrinsic_fee( amount: Optional[Balance] = None, price_limit: Optional[Balance] = None, allow_partial_stake: bool = False, + proxy: Optional[str] = None, ) -> Balance: """ Retrieves the extrinsic fee for a given unstaking call. @@ -929,7 +929,7 @@ async def _get_extrinsic_fee( call_function=call_fn, call_params=call_params, ) - return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy) # Helpers @@ -1066,7 +1066,8 @@ async def _unstake_selection( invalid_netuids = [n for n in netuid_list if n not in netuid_stakes] if invalid_netuids: print_error( - f"The following netuids are invalid or not available: {', '.join(map(str, invalid_netuids))}. Please try again." + f"The following netuids are invalid or not available: " + f"{', '.join(map(str, invalid_netuids))}. Please try again." ) else: selected_netuids = netuid_list @@ -1259,10 +1260,10 @@ def _create_unstake_table( Rich Table object configured for unstake summary """ title = ( - f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking to: \n" - f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet_name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], " - f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet_coldkey_ss58}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f"Network: {network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n" + f"\n[{COLOR_PALETTE.G.HEADER}]Unstaking to: \n" + f"Wallet: [{COLOR_PALETTE.G.CK}]{wallet_name}[/{COLOR_PALETTE.G.CK}], " + f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet_coldkey_ss58}[/{COLOR_PALETTE.G.CK}]\n" + f"Network: {network}[/{COLOR_PALETTE.G.HEADER}]\n" ) table = Table( title=title, From 9e6078b0bb6bc2b607a82aacc824974c7065d5c1 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 12:13:08 +0200 Subject: [PATCH 017/179] Add debug loggers for proxy app --- bittensor_cli/cli.py | 49 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index fe8325b29..e88883e1e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8257,7 +8257,17 @@ def proxy_create( Creates a pure proxy """ - # TODO add debug logger + logger.debug( + "args:\n" + f"network: {network}\n" + f"proxy_type: {proxy_type}\n" + f"delay: {delay}\n" + f"idx: {idx}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + f"era: {period}\n" + f"prompt: {prompt}\n" + ) self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name=wallet_name, @@ -8310,7 +8320,17 @@ def proxy_add( Adds a proxy """ - # TODO add debug logger + logger.debug( + "args:\n" + f"network: {network}\n" + f"delegate: {delegate}\n" + f"proxy_type: {proxy_type}\n" + f"delay: {delay}\n" + f"prompt: {prompt}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"era: {period}\n" + ) self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name=wallet_name, @@ -8362,7 +8382,16 @@ def proxy_remove( Removes a proxy """ # TODO should add a --all flag to call Proxy.remove_proxies ? - # TODO add debug logger + logger.debug( + "args:\n" + f"delegate: {delegate}\n" + f"network: {network}\n" + f"proxy_type: {proxy_type}\n" + f"delay: {delay}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"era: {period}\n" + ) self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name=wallet_name, @@ -8423,7 +8452,19 @@ def proxy_kill( Kills a pure proxy """ - # TODO add debug logger + logger.debug( + "args:\n" + f"height: {height}\n" + f"ext_index: {ext_index}\n" + f"proxy_type: {proxy_type}\n" + f"spawner: {spawner}\n" + f"proxy: {proxy}\n" + f"network: {network}\n" + f"idx: {idx}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + f"era: {period}\n" + ) self.verbosity_handler(quiet, verbose, json_output) proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( From d7744335e65ca99b55541f7cddd230b51e14c28a Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 12:28:21 +0200 Subject: [PATCH 018/179] Move stake --- bittensor_cli/cli.py | 4 ++ bittensor_cli/src/commands/stake/move.py | 55 +++++++++++------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e88883e1e..dd41416bd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4776,6 +4776,7 @@ def stake_move( stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), + proxy: Optional[str] = Options.proxy, period: int = Options.period, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -4803,6 +4804,7 @@ def stake_move( [green]$[/green] btcli stake move """ self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if prompt: if not Confirm.ask( "This transaction will [bold]move stake[/bold] to another hotkey while keeping the same " @@ -4915,6 +4917,7 @@ def stake_move( f"era: {period}\n" f"interactive_selection: {interactive_selection}\n" f"prompt: {prompt}\n" + f"proxy: {proxy}\n" ) result, ext_id = self._run_command( move_stake.move_stake( @@ -4929,6 +4932,7 @@ def stake_move( era=period, interactive_selection=interactive_selection, prompt=prompt, + proxy=proxy, ) ) if json_output: diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 2b5d42a91..92672f996 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -1,6 +1,6 @@ import asyncio -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet from rich.table import Table @@ -439,6 +439,7 @@ async def move_stake( era: int, interactive_selection: bool = False, prompt: bool = True, + proxy: Optional[str] = None, ) -> tuple[bool, str]: if interactive_selection: try: @@ -453,6 +454,7 @@ async def move_stake( # Get the wallet stake balances. block_hash = await subtensor.substrate.get_chain_head() + # TODO should this use `proxy if proxy else wallet.coldkeypub.ss58_address`? origin_stake_balance, destination_stake_balance = await asyncio.gather( subtensor.get_stake( coldkey_ss58=wallet.coldkeypub.ss58_address, @@ -471,23 +473,23 @@ async def move_stake( if origin_stake_balance.tao == 0: print_error( f"Your balance is " - f"[{COLOR_PALETTE['POOLS']['TAO']}]0[/{COLOR_PALETTE['POOLS']['TAO']}] " + f"[{COLOR_PALETTE.POOLS.TAO}]0[/{COLOR_PALETTE.POOLS.TAO}] " f"in Netuid: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"[{COLOR_PALETTE.G.SUBHEAD}]{origin_netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" ) return False, "" console.print( f"\nOrigin Netuid: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], " + f"[{COLOR_PALETTE.G.SUBHEAD}]{origin_netuid}[/{COLOR_PALETTE.G.SUBHEAD}], " f"Origin stake: " - f"[{COLOR_PALETTE['POOLS']['TAO']}]{origin_stake_balance}[/{COLOR_PALETTE['POOLS']['TAO']}]" + f"[{COLOR_PALETTE.POOLS.TAO}]{origin_stake_balance}[/{COLOR_PALETTE.POOLS.TAO}]" ) console.print( f"Destination netuid: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{destination_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], " + f"[{COLOR_PALETTE.G.SUBHEAD}]{destination_netuid}[/{COLOR_PALETTE.G.SUBHEAD}], " f"Destination stake: " - f"[{COLOR_PALETTE['POOLS']['TAO']}]{destination_stake_balance}[/{COLOR_PALETTE['POOLS']['TAO']}]\n" + f"[{COLOR_PALETTE.POOLS.TAO}]{destination_stake_balance}[/{COLOR_PALETTE.POOLS.TAO}]\n" ) # Determine the amount we are moving. @@ -505,10 +507,8 @@ async def move_stake( if amount_to_move_as_balance > origin_stake_balance: err_console.print( f"[red]Not enough stake[/red]:\n" - f" Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f"{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f" < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f"{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" Stake balance: [{COLOR_PALETTE.S.AMOUNT}]{origin_stake_balance}[/{COLOR_PALETTE.S.AMOUNT}]" + f" < Moving amount: [{COLOR_PALETTE.S.AMOUNT}]{amount_to_move_as_balance}[/{COLOR_PALETTE.S.AMOUNT}]" ) return False, "" @@ -529,7 +529,7 @@ async def move_stake( destination_netuid=destination_netuid, amount=amount_to_move_as_balance.rao, ), - subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), ) # Display stake movement details @@ -560,26 +560,20 @@ async def move_stake( f"[blue]{origin_netuid}[/blue] \nto " f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." ): - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, ) - ext_id = await response.get_extrinsic_identifier() - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, ext_id - else: - if not await response.is_success: - err_console.print( - f"\n:cross_mark: [red]Failed[/red] with error:" - f" {format_error_message(await response.error_message)}" - ) - return False, "" + ext_id = await response.get_extrinsic_identifier() if response else "" + if success_: + await print_extrinsic_id(response) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, ext_id else: - await print_extrinsic_id(response) console.print( ":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]" ) @@ -611,6 +605,9 @@ async def move_stake( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" ) return True, ext_id + else: + err_console.print(f"\n:cross_mark: [red]Failed[/red] with error: {err_msg}") + return False, "" async def transfer_stake( From c181fb9b27a667b900f22cba095f14ebd207ef0a Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 12:38:03 +0200 Subject: [PATCH 019/179] Transfer stake --- bittensor_cli/cli.py | 4 + bittensor_cli/src/commands/stake/move.py | 103 ++++++++++++----------- 2 files changed, 58 insertions(+), 49 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index dd41416bd..72f740b25 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4974,6 +4974,7 @@ def stake_transfer( False, "--stake-all", "--all", help="Stake all", prompt=False ), period: int = Options.period, + proxy: Optional[str] = Options.proxy, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5012,6 +5013,7 @@ def stake_transfer( [green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2 """ self.verbosity_handler(quiet, verbose, json_output) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if prompt: if not Confirm.ask( "This transaction will [bold]transfer ownership[/bold] from one coldkey to another, in subnets " @@ -5106,6 +5108,7 @@ def stake_transfer( f"amount: {amount}\n" f"era: {period}\n" f"stake_all: {stake_all}" + f"proxy: {proxy}" ) result, ext_id = self._run_command( move_stake.transfer_stake( @@ -5120,6 +5123,7 @@ def stake_transfer( interactive_selection=interactive_selection, stake_all=stake_all, prompt=prompt, + proxy=proxy, ) ) if json_output: diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 92672f996..277b588ce 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -622,22 +622,28 @@ async def transfer_stake( interactive_selection: bool = False, stake_all: bool = False, prompt: bool = True, + proxy: Optional[str] = None, ) -> tuple[bool, str]: """Transfers stake from one network to another. Args: - wallet (Wallet): Bittensor wallet object. - subtensor (SubtensorInterface): Subtensor interface instance. - amount (float): Amount to transfer. - origin_hotkey (str): The hotkey SS58 to transfer the stake from. - origin_netuid (int): The netuid to transfer stake from. - dest_netuid (int): The netuid to transfer stake to. - dest_coldkey_ss58 (str): The destination coldkey to transfer stake to. - interactive_selection (bool): If true, prompts for selection of origin and destination subnets. - prompt (bool): If true, prompts for confirmation before executing transfer. + wallet: Bittensor wallet object. + subtensor: Subtensor interface instance. + amount: Amount to transfer. + origin_hotkey: The hotkey SS58 to transfer the stake from. + origin_netuid: The netuid to transfer stake from. + dest_netuid: The netuid to transfer stake to. + dest_coldkey_ss58: The destination coldkey to transfer stake to. + interactive_selection: If true, prompts for selection of origin and destination subnets. + prompt: If true, prompts for confirmation before executing transfer. + era: number of blocks for which the extrinsic should be valid + stake_all: If true, transfer all stakes. + proxy: Optional proxy to use for this extrinsic Returns: - bool: True if transfer was successful, False otherwise. + tuple: + bool: True if transfer was successful, False otherwise. + str: error message """ if interactive_selection: selection = await stake_move_transfer_selection(subtensor, wallet) @@ -663,6 +669,7 @@ async def transfer_stake( # Get current stake balances with console.status(f"Retrieving stake data from {subtensor.network}..."): + # TODO should use proxy for these checks? current_stake = await subtensor.get_stake( coldkey_ss58=wallet.coldkeypub.ss58_address, hotkey_ss58=origin_hotkey, @@ -715,7 +722,7 @@ async def transfer_stake( destination_netuid=dest_netuid, amount=amount_to_transfer.rao, ), - subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), ) # Display stake movement details @@ -744,49 +751,47 @@ async def transfer_stake( return False, "" with console.status("\n:satellite: Transferring stake ..."): - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - ext_id = await response.get_extrinsic_identifier() + if success_: + ext_id = await response.get_extrinsic_identifier() + await print_extrinsic_id(response) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, ext_id + else: + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=dest_coldkey_ss58, + hotkey_ss58=origin_hotkey, + netuid=dest_netuid, + ), + ) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, ext_id + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" + ) + return True, ext_id - if not await response.is_success: - err_console.print( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message)}" - ) + else: + err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") return False, "" - await print_extrinsic_id(response) - # Get and display new stake balances - new_stake, new_dest_stake = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=origin_hotkey, - netuid=origin_netuid, - ), - subtensor.get_stake( - coldkey_ss58=dest_coldkey_ss58, - hotkey_ss58=origin_hotkey, - netuid=dest_netuid, - ), - ) - - console.print( - f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" - ) - console.print( - f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}" - ) - return True, ext_id async def swap_stake( From 6384be2b212359015df759f8fda3ee1c9b0cbd9c Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 12:45:31 +0200 Subject: [PATCH 020/179] Add docstring --- bittensor_cli/cli.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 72f740b25..a62962a6a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1862,6 +1862,9 @@ def config_add_proxy( ), ], ): + """ + Adds a new proxy to the address book. + """ proxies = self.config.get("proxies", {}) proxies[name] = {"proxy_type": proxy_type.value, "address": address} self.config["proxies"] = proxies @@ -1870,6 +1873,9 @@ def config_add_proxy( self.config_get_proxies() def config_get_proxies(self): + """ + Displays the current proxies address book + """ proxies = self.config.get("proxies", {}) table = Table( Column("[bold white]Name", style=f"{COLORS.G.ARG}"), From 83408c1917d6fa4a39e7af526e14d882e36b4a69 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 13:12:06 +0200 Subject: [PATCH 021/179] Made verbosity_handler also handle prompt/json-output --- bittensor_cli/cli.py | 180 ++++++++++++++++++++++--------------------- 1 file changed, 93 insertions(+), 87 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a62962a6a..ac18bfd08 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1510,8 +1510,12 @@ def main_callback( logger.addHandler(handler) def verbosity_handler( - self, quiet: bool, verbose: bool, json_output: bool = False + self, quiet: bool, verbose: bool, json_output: bool = False, prompt: bool = True ) -> None: + if json_output and prompt: + raise typer.BadParameter( + f"Cannot specify both {arg__('json-output')} and {arg__('prompt')}" + ) if quiet and verbose: err_console.print("Cannot specify both `--quiet` and `--verbose`") raise typer.Exit() @@ -2207,7 +2211,7 @@ def wallet_list( [bold]NOTE[/bold]: This command is read-only and does not modify the filesystem or the blockchain state. It is intended for use with the Bittensor CLI to provide a quick overview of the user's wallets. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = self.wallet_ask( None, wallet_path, None, ask_for=[WO.PATH], validate=WV.NONE ) @@ -2274,7 +2278,7 @@ def wallet_overview( It provides a quick and comprehensive view of the user's network presence, making it useful for monitoring account status, stake distribution, and overall contribution to the Bittensor network. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if include_hotkeys and exclude_hotkeys: utils.err_console.print( "[red]You have specified both the inclusion and exclusion options. Only one of these options is allowed currently." @@ -2390,7 +2394,7 @@ def wallet_transfer( print_error("You have entered an incorrect ss58 address. Please try again.") raise typer.Exit() - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, @@ -2474,7 +2478,7 @@ def wallet_swap_hotkey( """ netuid = get_optional_netuid(netuid, all_netuids) proxy = self.is_valid_proxy_name_or_ss58(proxy) - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) # Warning for netuid 0 - only swaps on root network, not a full swap if netuid == 0 and prompt: @@ -2588,7 +2592,7 @@ def wallet_inspect( """ print_error("This command is disabled on the 'rao' network.") raise typer.Exit() - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if netuids: netuids = parse_to_list( @@ -2751,7 +2755,7 @@ def wallet_regen_coldkey( [bold]Note[/bold]: This command is critical for users who need to regenerate their coldkey either for recovery or for security reasons. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_path: wallet_path = Prompt.ask( @@ -2812,7 +2816,7 @@ def wallet_regen_coldkey_pub( [bold]Note[/bold]: This command is particularly useful for users who need to regenerate their coldkeypub, perhaps due to file corruption or loss. You will need either ss58 address or public hex key from your old coldkeypub.txt for the wallet. It is a recovery-focused utility that ensures continued access to your wallet functionalities. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_path: wallet_path = Prompt.ask( @@ -2883,7 +2887,7 @@ def wallet_regen_hotkey( [bold]Note[/bold]: This command is essential for users who need to regenerate their hotkey, possibly for security upgrades or key recovery. It should be used with caution to avoid accidental overwriting of existing keys. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2935,7 +2939,7 @@ def wallet_regen_hotkey_pub( [bold]Note[/bold]: This command is particularly useful for users who need to regenerate their hotkeypub, perhaps due to file corruption or loss. You will need either ss58 address or public hex key from your old hotkeypub.txt for the wallet. It is a recovery-focused utility that ensures continued access to your wallet functionalities. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_path: wallet_path = Prompt.ask( @@ -3006,7 +3010,7 @@ def wallet_new_hotkey( [italic]Note[/italic]: This command is useful to create additional hotkeys for different purposes, such as running multiple subnet miners or subnet validators or separating operational roles within the Bittensor network. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_name: wallet_name = Prompt.ask( @@ -3059,7 +3063,7 @@ def wallet_associate_hotkey( [green]$[/green] btcli wallet associate-hotkey --hotkey-name hotkey_name [green]$[/green] btcli wallet associate-hotkey --hotkey-ss58 5DkQ4... """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy) if not wallet_name: wallet_name = Prompt.ask( @@ -3147,7 +3151,7 @@ def wallet_new_coldkey( [bold]Note[/bold]: This command is crucial for users who need to create a new coldkey for enhanced security or as part of setting up a new wallet. It is a foundational step in establishing a secure presence on the Bittensor network. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_path: wallet_path = Prompt.ask( @@ -3221,7 +3225,7 @@ def wallet_check_ck_swap( [green]$[/green] btcli wallet swap-check --wallet-name my_wallet --block 12345 """ # TODO add json_output if this ever gets used again (doubtful) - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) self.initialize_chain(network) if show_all: @@ -3299,7 +3303,7 @@ def wallet_create_wallet( [bold]Note[/bold]: This command is for new users setting up their wallet for the first time, or for those who wish to completely renew their wallet keys. It ensures a fresh start with new keys for secure and effective participation in the Bittensor network. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_path: wallet_path = Prompt.ask( "Enter the path of wallets directory", @@ -3378,7 +3382,7 @@ def wallet_balance( [green]$[/green] btcli w balance --ss58 --ss58 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = None if all_balances: ask_for = [WO.PATH] @@ -3484,7 +3488,7 @@ def wallet_history( print_error("This command is disabled on the 'rao' network.") raise typer.Exit() - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, False, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -3560,7 +3564,7 @@ def wallet_set_id( [bold]Note[/bold]: This command should only be used if the user is willing to incur the a recycle fee associated with setting an identity on the blockchain. It is a high-level command that makes changes to the blockchain state and should not be used programmatically as part of other scripts or applications. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, @@ -3652,7 +3656,7 @@ def wallet_get_id( [bold]Note[/bold]: This command is primarily used for informational purposes and has no side effects on the blockchain network state. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not wallet_name: if coldkey_ss58: if not is_valid_ss58_address(coldkey_ss58): @@ -3714,7 +3718,7 @@ def wallet_sign( [green]$[/green] btcli wallet sign --wallet-name default --wallet-hotkey hotkey --message '{"something": "here", "timestamp": 1719908486}' """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if use_hotkey is None: use_hotkey = Confirm.ask( f"Would you like to sign the transaction using your [{COLORS.G.HK}]hotkey[/{COLORS.G.HK}]?" @@ -3771,7 +3775,7 @@ def wallet_verify( [green]$[/green] btcli wallet verify -m "Test message" -s "0xdef456..." -p "0x1234abcd..." """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) if not public_key_or_ss58: public_key_or_ss58 = Prompt.ask( @@ -3824,7 +3828,7 @@ def wallet_swap_coldkey( [green]$[/green] btcli wallet schedule-coldkey-swap --new-coldkey-ss58 5Dk...X3q """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, prompt=False, json_output=False) proxy = self.is_valid_proxy_name_or_ss58(proxy) if not wallet_name: @@ -3900,7 +3904,7 @@ def get_auto_stake( ): """Display auto-stake destinations for a wallet across all subnets.""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = None if coldkey_ss58: @@ -3952,7 +3956,7 @@ def set_auto_stake( ): """Set the auto-stake destination hotkey for a coldkey.""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( @@ -4054,7 +4058,7 @@ def stake_list( 4. Verbose output with full values: [green]$[/green] btcli stake list --wallet.name my_wallet --verbose """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = None if coldkey_ss58: @@ -4181,7 +4185,7 @@ def stake_add( """ netuids = netuids or [] - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy) safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: @@ -4482,7 +4486,7 @@ def stake_remove( • [blue]--tolerance[/blue]: Max allowed rate change (0.05 = 5%) • [blue]--partial[/blue]: Complete partial unstake if rates exceed tolerance """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy) if not unstake_all and not unstake_all_alpha: safe_staking = self.ask_safe_staking(safe_staking) @@ -4809,7 +4813,7 @@ def stake_move( [green]$[/green] btcli stake move """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy) if prompt: if not Confirm.ask( @@ -5018,7 +5022,7 @@ def stake_transfer( Transfer all available stake from origin hotkey: [green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy) if prompt: if not Confirm.ask( @@ -5198,7 +5202,7 @@ def stake_swap( Swap 100 TAO from subnet 1 to subnet 2: [green]$[/green] btcli stake swap --wallet-name default --wallet-hotkey default --origin-netuid 1 --dest-netuid 2 --amount 100 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) console.print( "[dim]This command moves stake from one subnet to another subnet while keeping " "the same coldkey-hotkey pair.[/dim]" @@ -5292,7 +5296,7 @@ def stake_get_children( [green]$[/green] btcli stake child get --netuid 1 [green]$[/green] btcli stake child get --all-netuids """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5358,7 +5362,7 @@ def stake_set_children( [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 --prop 0.3 --prop 0.7 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) netuid = get_optional_netuid(netuid, all_netuids) children = list_prompt( @@ -5446,7 +5450,7 @@ def stake_revoke_children( [green]$[/green] btcli stake child revoke --hotkey --netuid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5536,7 +5540,7 @@ def stake_childkey_take( [green]$[/green] btcli stake child take --child-hotkey-ss58 --take 0.12 --netuid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5615,7 +5619,7 @@ def mechanism_count_set( """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) if not json_output: @@ -5718,7 +5722,7 @@ def mechanism_count_get( [green]$[/green] btcli subnet mech count --netuid 12 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) subtensor = self.initialize_chain(network) return self._run_command( subnet_mechanisms.count( @@ -5763,7 +5767,7 @@ def mechanism_emission_set( [green]$[/green] btcli subnet mech emissions-split --netuid 12 --split 70,30 --wallet.name my_wallet --wallet.hotkey admin """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) wallet = self.wallet_ask( wallet_name, @@ -5802,7 +5806,7 @@ def mechanism_emission_get( [green]$[/green] btcli subnet mech emissions --netuid 12 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) subtensor = self.initialize_chain(network) return self._run_command( subnet_mechanisms.get_emission_split( @@ -5839,7 +5843,7 @@ def sudo_set( [green]$[/green] btcli sudo set --netuid 1 --param tempo --value 400 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if not param_name or not param_value: hyperparams = self._run_command( @@ -5969,7 +5973,7 @@ def sudo_get( [green]$[/green] btcli sudo get --netuid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) return self._run_command( sudo.get_hyperparameters( self.initialize_chain(network), netuid, json_output @@ -5991,7 +5995,7 @@ def sudo_senate( EXAMPLE [green]$[/green] btcli sudo senate """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, False) return self._run_command( sudo.get_senate(self.initialize_chain(network), json_output) ) @@ -6011,7 +6015,7 @@ def sudo_proposals( EXAMPLE [green]$[/green] btcli sudo proposals """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) return self._run_command( sudo.proposals(self.initialize_chain(network), verbose, json_output) ) @@ -6051,7 +6055,7 @@ def sudo_senate_vote( [green]$[/green] btcli sudo senate_vote --proposal """ # TODO discuss whether this should receive json_output. I don't think it should. - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6088,7 +6092,7 @@ def sudo_set_take( """ max_value = 0.18 min_value = 0.00 - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) wallet = self.wallet_ask( wallet_name, @@ -6140,7 +6144,7 @@ def sudo_get_take( EXAMPLE [green]$[/green] btcli sudo get-take --wallet-name my_wallet --wallet-hotkey my_hotkey """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) wallet = self.wallet_ask( wallet_name, @@ -6185,7 +6189,7 @@ def sudo_trim( EXAMPLE [green]$[/green] btcli sudo trim --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey --max 64 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) wallet = self.wallet_ask( wallet_name, @@ -6243,7 +6247,7 @@ def subnets_list( if json_output and live_mode: print_error("Cannot use `--json-output` and `--live` at the same time.") return - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) subtensor = self.initialize_chain(network) return self._run_command( subnets.subnets_list( @@ -6318,7 +6322,9 @@ def subnets_price( f"Cannot specify both {arg__('--current')} and {arg__('--html')}" ) return - self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) + self.verbosity_handler( + quiet=quiet, verbose=verbose, json_output=json_output, prompt=False + ) subtensor = self.initialize_chain(network) non_archives = ["finney", "latent-lite", "subvortex"] @@ -6401,7 +6407,7 @@ def subnets_show( 2. Pick mechanism 1 explicitly: [green]$[/green] btcli subnets show --netuid 12 --mechid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) if netuid == 0: mechanism_count = 1 @@ -6449,7 +6455,7 @@ def subnets_burn_cost( [green]$[/green] btcli subnets burn_cost """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) return self._run_command( subnets.burn_cost(self.initialize_chain(network), json_output) ) @@ -6505,7 +6511,7 @@ def subnets_create( 2. Create with GitHub repo and contact email: [green]$[/green] btcli subnets create --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6550,7 +6556,7 @@ def subnets_check_start( Example: [green]$[/green] btcli subnets check-start --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) return self._run_command( subnets.get_start_schedule(self.initialize_chain(network), netuid) ) @@ -6575,7 +6581,7 @@ def subnets_start( [green]$[/green] btcli subnets start --netuid 1 [green]$[/green] btcli subnets start --netuid 1 --wallet-name alice """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=prompt) if not wallet_name: wallet_name = Prompt.ask( "Enter the [blue]wallet name[/blue] [dim](which you used to create the subnet)[/dim]", @@ -6615,7 +6621,7 @@ def subnets_get_identity( [green]$[/green] btcli subnets get-identity --netuid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) return self._run_command( subnets.get_identity( self.initialize_chain(network), netuid, json_output=json_output @@ -6673,7 +6679,7 @@ def subnets_set_identity( 2. Set subnet identity with specific values: [green]$[/green] btcli subnets set-identity --netuid 1 --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6841,7 +6847,7 @@ def subnets_register( [green]$[/green] btcli subnets register --netuid 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, @@ -6929,7 +6935,7 @@ def subnets_metagraph( [blue bold]Note[/blue bold]: This command is not intended to be used as a standalone function within user code. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) if (reuse_last or html_output) and self.config.get("use_cache") is False: err_console.print( "Unable to use `--reuse-last` or `--html` when config `no-cache` is set to `True`. " @@ -6991,7 +6997,7 @@ def subnets_set_symbol( If --json-output is used, the output will be in the following schema: [#AFEFFF]{success: [dark_orange]bool[/dark_orange], message: [dark_orange]str[/dark_orange]}[/#AFEFFF] """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if len(symbol) > 1: err_console.print("Your symbol must be a single character.") return False @@ -7047,7 +7053,7 @@ def weights_reveal( [green]$[/green] btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) # TODO think we need to ','.split uids and weights ? uids = list_prompt(uids, int, "UIDs of interest for the specified netuid") weights = list_prompt( @@ -7151,7 +7157,7 @@ def weights_commit( [italic]Note[/italic]: This command is used to commit weights for a specific subnet and requires the user to have the necessary permissions. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if uids: uids = parse_to_list( @@ -7237,7 +7243,7 @@ def view_dashboard( """ Display html dashboard with subnets list, stake, and neuron information. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) if use_wry and save_file: print_error("Cannot save file when using browser output.") @@ -7306,7 +7312,7 @@ def stake_set_claim_type( [green]$[/green] btcli stake claim swap --wallet-name my_wallet """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if claim_type is not None: claim_type_normalized = claim_type.capitalize() @@ -7367,7 +7373,7 @@ def stake_process_claim( [green]$[/green] btcli stake process-claim --netuids 1,2 --wallet-name my_wallet """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) parsed_netuids = None if netuids: @@ -7433,7 +7439,7 @@ def liquidity_add( json_output: bool = Options.json_output, ): """Add liquidity to the swap (as a combination of TAO + Alpha).""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if not netuid: netuid = Prompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -7505,7 +7511,7 @@ def liquidity_list( json_output: bool = Options.json_output, ): """Displays liquidity positions in given subnet.""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) if not netuid: netuid = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -7555,7 +7561,7 @@ def liquidity_remove( ): """Remove liquidity from the swap (as a combination of TAO + Alpha).""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if all_liquidity_ids and position_id: print_error("Cannot specify both --all and --position-id.") @@ -7625,7 +7631,7 @@ def liquidity_modify( json_output: bool = Options.json_output, ): """Modifies the liquidity position for the given subnet.""" - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if not netuid: netuid = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -7695,7 +7701,7 @@ def crowd_list( [green]$[/green] btcli crowd list --verbose """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) return self._run_command( view_crowdloan.list_crowdloans( subtensor=self.initialize_chain(network), @@ -7732,7 +7738,7 @@ def crowd_info( [green]$[/green] btcli crowd info --id 1 --verbose """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt=False) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( @@ -7842,7 +7848,7 @@ def crowd_create( Subnet lease ending at block 500000: [green]$[/green] btcli crowd create --subnet-lease --emissions-share 25 --lease-end-block 500000 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) wallet = self.wallet_ask( wallet_name=wallet_name, @@ -7909,7 +7915,7 @@ def crowd_contribute( [green]$[/green] btcli crowd contribute --id 1 """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( @@ -7965,7 +7971,7 @@ def crowd_withdraw( Non-creators can withdraw their full contribution. Creators can only withdraw amounts above their initial deposit. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( @@ -8020,7 +8026,7 @@ def crowd_finalize( Only the creator can finalize. This will transfer funds to the target address (if specified) and execute any attached call (e.g., subnet creation). """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( @@ -8095,7 +8101,7 @@ def crowd_update( against the chain constants (absolute minimum contribution, block-duration bounds, etc.). """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( @@ -8160,7 +8166,7 @@ def crowd_refund( Contributors can call `btcli crowdloan withdraw` at will. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( @@ -8220,7 +8226,7 @@ def crowd_dissolve( If there are funds still available other than the creator's contribution, you can run `btcli crowd refund` to refund the remaining contributors. """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( @@ -8271,6 +8277,14 @@ def proxy_create( Creates a pure proxy """ + 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, + ) logger.debug( "args:\n" f"network: {network}\n" @@ -8282,14 +8296,6 @@ def proxy_create( f"era: {period}\n" f"prompt: {prompt}\n" ) - self.verbosity_handler(quiet, verbose, json_output) - 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.create_proxy( @@ -8345,7 +8351,7 @@ def proxy_add( f"wait_for_inclusion: {wait_for_inclusion}\n" f"era: {period}\n" ) - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) wallet = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, @@ -8406,7 +8412,7 @@ def proxy_remove( f"wait_for_inclusion: {wait_for_inclusion}\n" f"era: {period}\n" ) - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) wallet = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, @@ -8479,7 +8485,7 @@ def proxy_kill( f"wait_for_finalization: {wait_for_finalization}\n" f"era: {period}\n" ) - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose, json_output, prompt) proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name=wallet_name, From da1e4310657a52d7236c812fd4d630dc027c3eeb Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 13:17:37 +0200 Subject: [PATCH 022/179] Allow for adding created pure proxy to address book. --- bittensor_cli/cli.py | 4 ++- bittensor_cli/src/commands/proxy.py | 55 +++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ac18bfd08..e5caa666c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8297,7 +8297,7 @@ def proxy_create( f"prompt: {prompt}\n" ) - return self._run_command( + should_update, proxy_name, created_pure, created_type = self._run_command( proxy_commands.create_proxy( subtensor=self.initialize_chain(network), wallet=wallet, @@ -8311,6 +8311,8 @@ def proxy_create( period=period, ) ) + if should_update: + self.config_add_proxy(proxy_name, created_pure, created_type) def proxy_add( self, diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 52bbd083b..8a4e05aae 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -1,7 +1,7 @@ from enum import Enum from typing import TYPE_CHECKING, Optional -from rich.prompt import Confirm +from rich.prompt import Confirm, Prompt from scalecodec import GenericCall from bittensor_cli.src.bittensor.utils import ( @@ -93,12 +93,34 @@ async def create_proxy( wait_for_finalization: bool, period: int, json_output: bool, -) -> None: +) -> tuple[bool, str, str, str]: + """ + + Args: + subtensor: + wallet: + proxy_type: + delay: + idx: + prompt: + wait_for_inclusion: + wait_for_finalization: + period: + json_output: + + Returns: + tuple containing the following: + should_update: True if the address book should be updated, False otherwise + name: name of the new pure proxy for the address book + address: SS58 address of the new pure proxy + proxy_type: proxy type of the new pure proxy + + """ if prompt: if not Confirm.ask( f"This will create a Pure Proxy of type {proxy_type.value}. Do you want to proceed?", ): - return None + return False, "", "", "" if not (ulw := unlock_key(wallet, print_out=not json_output)).success: if not json_output: err_console.print(ulw.message) @@ -110,7 +132,7 @@ async def create_proxy( "extrinsic_id": None, } ) - return None + return False, "", "", "" call = await subtensor.substrate.compose_call( call_module="Proxy", call_function="create_pure", @@ -136,14 +158,20 @@ async def create_proxy( created_proxy_type = attrs["proxy_type"] arg_start = "`" if json_output else "[blue]" arg_end = "`" if json_output else "[/blue]" - msg = ( - f"Created pure '{created_pure}' from spawner '{created_spawner}' with proxy type '{created_proxy_type}'." - f" You can add this to your config with {arg_start}" - f"btcli config add-proxy " - f"--name --address {created_pure} --proxy-type {created_proxy_type}" - f"{arg_end}" - ) - # TODO add to wallets somehow? + msg = f"Created pure '{created_pure}' from spawner '{created_spawner}' with proxy type '{created_proxy_type}'." + console.print(msg) + if not prompt: + console.print( + f" You can add this to your config with {arg_start}" + f"btcli config add-proxy " # TODO change after changing to proxy app + f"--name --address {created_pure} --proxy-type {created_proxy_type}" + f"{arg_end}" + ) + else: + if Confirm.ask("Would you like to add this to your address book?"): + proxy_name = Prompt.ask("Name this proxy:") + return True, proxy_name, created_pure, created_proxy_type + if json_output: json_console.print_json( data={ @@ -152,8 +180,6 @@ async def create_proxy( "extrinsic_id": await receipt.get_extrinsic_identifier(), } ) - else: - console.print(f"Success! {msg}") # TODO add more shit here else: if json_output: @@ -166,6 +192,7 @@ async def create_proxy( ) else: err_console.print(f"Failure: {msg}") # TODO add more shit here + return False, "", "", "" async def remove_proxy( From c89ea3d3e364cf47fdc8657f3a827d77e9900eb5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 13:27:34 +0200 Subject: [PATCH 023/179] Updated kill confirmation --- bittensor_cli/src/commands/proxy.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 8a4e05aae..d71b01df0 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -311,9 +311,13 @@ async def kill_proxy( json_output: bool, ) -> None: if prompt: - if not Confirm.ask( - f"This will kill a Pure Proxy of type {proxy_type.value}. Do you want to proceed?", - ): + confirmation = Prompt.ask( + f"This will kill a Pure Proxy account of type {proxy_type.value}.\n" + f"[red]All access to this account will be lost. Any funds held in it will be inaccessible.[/red]" + f"To proceed, enter [red]KILL[/red]" + ) + if confirmation != "KILL": + err_console.print("Invalid input. Exiting.") return None if not (ulw := unlock_key(wallet, print_out=not json_output)).success: if not json_output: From 4be011c20e7cc3d2ac96f245717abc67bb12b174 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 14:08:08 +0200 Subject: [PATCH 024/179] Updated proxies to new file --- bittensor_cli/cli.py | 27 ++++++++++++++++++--------- bittensor_cli/src/__init__.py | 6 +++++- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e5caa666c..7caba2038 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -743,7 +743,6 @@ def __init__(self): "safe_staking": True, "allow_partial_stake": False, "dashboard_path": None, - "proxies": {}, # Commenting this out as this needs to get updated # "metagraph_cols": { # "UID": True, @@ -765,6 +764,7 @@ def __init__(self): # "COLDKEY": True, # }, } + self.proxies = {} self.subtensor = None if sys.version_info < (3, 10): @@ -784,6 +784,9 @@ def __init__(self): self.debug_file_path = os.getenv("BTCLI_DEBUG_FILE") or os.path.expanduser( defaults.config.debug_file_path ) + self.proxies_path = os.getenv("BTCLI_PROXIES_PATH") or os.path.expanduser( + defaults.proxies.path + ) self.app = typer.Typer( rich_markup_mode="rich", @@ -1509,6 +1512,16 @@ def main_callback( asi_logger.addHandler(handler) logger.addHandler(handler) + # load proxies address book + if os.path.exists(self.proxies_path): + with open(self.proxies_path, "r") as f: + proxies = safe_load(f) or {} + else: + proxies = {} + with open(self.proxies_path, "w+") as f: + safe_dump(proxies, f) + self.proxies = proxies + def verbosity_handler( self, quiet: bool, verbose: bool, json_output: bool = False, prompt: bool = True ) -> None: @@ -1869,9 +1882,7 @@ def config_add_proxy( """ Adds a new proxy to the address book. """ - proxies = self.config.get("proxies", {}) - proxies[name] = {"proxy_type": proxy_type.value, "address": address} - self.config["proxies"] = proxies + self.proxies[name] = {"proxy_type": proxy_type.value, "address": address} with open(self.config_path, "w") as f: safe_dump(self.config, f) self.config_get_proxies() @@ -1880,15 +1891,14 @@ def config_get_proxies(self): """ Displays the current proxies address book """ - proxies = self.config.get("proxies", {}) table = Table( Column("[bold white]Name", style=f"{COLORS.G.ARG}"), Column("[bold white]Address", style="gold1"), Column("Proxy Type", style="medium_purple"), box=box.SIMPLE_HEAD, - title=f"[{COLORS.G.HEADER}]BTCLI Config Proxies[/{COLORS.G.HEADER}]: {arg__(self.config_path)}", + title=f"[{COLORS.G.HEADER}]BTCLI Proxies Address Book[/{COLORS.G.HEADER}]: {arg__(self.proxies_path)}", ) - for name, keys in proxies.items(): + for name, keys in self.proxies.items(): address = keys.get("address") proxy_type = keys.get("proxy_type") table.add_row(name, address, proxy_type) @@ -1910,8 +1920,7 @@ def is_valid_proxy_name_or_ss58(self, address: Optional[str]) -> Optional[str]: """ if address is None: return None - config_proxies = self.config.get("proxies", {}) - outer_proxy_from_config = config_proxies.get(address, {}) + outer_proxy_from_config = self.proxies.get(address, {}) proxy_from_config = outer_proxy_from_config.get("address") if proxy_from_config is not None: if not btwallet_is_valid_ss58_address(proxy_from_config): diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 0d17ccaf4..b349f76c2 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -116,9 +116,13 @@ class config: "HOTKEY": True, "COLDKEY": True, }, - "proxies": {}, } + class proxies: + base_path = "~/.bittensor" + path = "~/.bittensor/proxy-address-book.yml" + dictionary = {} + class subtensor: network = "finney" chain_endpoint = None From d194e4782af9211211153d7315ca4829f61a5ff2 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 14:12:14 +0200 Subject: [PATCH 025/179] Cleanup --- bittensor_cli/cli.py | 2 +- bittensor_cli/src/commands/proxy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7caba2038..98ec6b83d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1832,7 +1832,7 @@ def get_config(self): box=box.SIMPLE_HEAD, title=f"[{COLORS.G.HEADER}]BTCLI Config[/{COLORS.G.HEADER}]: {arg__(self.config_path)}", ) - + value: Optional[str] for key, value in self.config.items(): if key == "network": if value is None: diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index d71b01df0..04f53e426 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -163,7 +163,7 @@ async def create_proxy( if not prompt: console.print( f" You can add this to your config with {arg_start}" - f"btcli config add-proxy " # TODO change after changing to proxy app + f"btcli config add-proxy " f"--name --address {created_pure} --proxy-type {created_proxy_type}" f"{arg_end}" ) From 642f3d3d5e370d69943b1ad2b9f2580c741d5081 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 14:18:57 +0200 Subject: [PATCH 026/179] Proxy address book new command: remove --- bittensor_cli/cli.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 98ec6b83d..ccfdcbad1 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -923,6 +923,7 @@ def __init__(self): self.config_app.command("clear")(self.del_config) self.config_app.command("add-proxy")(self.config_add_proxy) self.config_app.command("proxies")(self.config_get_proxies) + self.config_app.command("remove-proxy")(self.config_remove_proxy) # self.config_app.command("metagraph", hidden=True)(self.metagraph_config) # wallet commands @@ -1880,11 +1881,35 @@ def config_add_proxy( ], ): """ - Adds a new proxy to the address book. + Adds a new pure proxy to the address book. """ self.proxies[name] = {"proxy_type": proxy_type.value, "address": address} - with open(self.config_path, "w") as f: - safe_dump(self.config, f) + with open(self.proxies_path, "w+") as f: + safe_dump(self.proxies, f) + self.config_get_proxies() + + def config_remove_proxy( + self, + name: Annotated[ + str, + typer.Option( + help="Name of the proxy to be removed", + prompt="Enter the name of the proxy to be removed", + ), + ], + ): + """ + Removes a pure proxy from the address book. + + Note: Does not remove the proxy on chain. Only removes it from the address book. + """ + if name in self.proxies: + del self.proxies[name] + console.print(f"Removed {name} from the address book.") + with open(self.proxies_path, "w+") as f: + safe_dump(self.proxies, f) + else: + err_console.print(f"Proxy {name} not found in address book.") self.config_get_proxies() def config_get_proxies(self): From 78457eb2b2bd16be48e13a021be329df681bb921 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 14:30:11 +0200 Subject: [PATCH 027/179] Help panels --- bittensor_cli/cli.py | 24 ++++++++++++------------ bittensor_cli/src/__init__.py | 3 +++ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ccfdcbad1..e599e9efb 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1146,18 +1146,18 @@ def __init__(self): )(self.view_dashboard) # proxy commands - self.proxy_app.command( - "create", # TODO add rich help panel - )(self.proxy_create) - self.proxy_app.command( - "add", # TODO add rich help panel - )(self.proxy_add) - self.proxy_app.command( - "remove", # TODO add rich help panel - )(self.proxy_remove) - self.proxy_app.command( - "kill", # TODO add rich help panel - )(self.proxy_kill) + self.proxy_app.command("create", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])( + self.proxy_create + ) + self.proxy_app.command("add", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])( + self.proxy_add + ) + self.proxy_app.command("remove", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])( + self.proxy_remove + ) + self.proxy_app.command("kill", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])( + self.proxy_kill + ) # Sub command aliases # Wallet diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index b349f76c2..eb0ccb742 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -743,6 +743,9 @@ class RootSudoOnly(Enum): "PARTICIPANT": "Crowdloan Participation", "INFO": "Crowdloan Information", }, + "PROXY": { + "MGMT": "Proxy Account Management", + }, } From 263470308e4a710c5b62398ed17f679285242fc3 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 14:43:13 +0200 Subject: [PATCH 028/179] Stake swap --- bittensor_cli/cli.py | 4 + bittensor_cli/src/commands/stake/move.py | 101 ++++++++++++----------- 2 files changed, 55 insertions(+), 50 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e599e9efb..728df7c04 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5208,6 +5208,7 @@ def stake_swap( "--all", help="Swap all available stake", ), + proxy: Optional[str] = Options.proxy, period: int = Options.period, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, @@ -5237,6 +5238,7 @@ def stake_swap( [green]$[/green] btcli stake swap --wallet-name default --wallet-hotkey default --origin-netuid 1 --dest-netuid 2 --amount 100 """ self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) console.print( "[dim]This command moves stake from one subnet to another subnet while keeping " "the same coldkey-hotkey pair.[/dim]" @@ -5272,6 +5274,7 @@ def stake_swap( f"amount: {amount}\n" f"swap_all: {swap_all}\n" f"era: {period}\n" + f"proxy: {proxy}\n" f"interactive_selection: {interactive_selection}\n" f"prompt: {prompt}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" @@ -5288,6 +5291,7 @@ def stake_swap( era=period, interactive_selection=interactive_selection, prompt=prompt, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 277b588ce..42cb4d4af 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -802,6 +802,7 @@ async def swap_stake( amount: float, swap_all: bool = False, era: int = 3, + proxy: Optional[str] = None, interactive_selection: bool = False, prompt: bool = True, wait_for_inclusion: bool = True, @@ -810,15 +811,18 @@ async def swap_stake( """Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership. Args: - wallet (Wallet): The wallet to swap stake from. - subtensor (SubtensorInterface): Subtensor interface instance. - origin_netuid (int): The netuid from which stake is removed. - destination_netuid (int): The netuid to which stake is added. - amount (float): The amount to swap. - interactive_selection (bool): If true, prompts for selection of origin and destination subnets. - prompt (bool): If true, prompts for confirmation before executing swap. - wait_for_inclusion (bool): If true, waits for the transaction to be included in a block. - wait_for_finalization (bool): If true, waits for the transaction to be finalized. + wallet: The wallet to swap stake from. + subtensor: Subtensor interface instance. + origin_netuid: The netuid from which stake is removed. + destination_netuid: The netuid to which stake is added. + amount: The amount to swap. + swap_all: Whether to swap all stakes. + era: The period (number of blocks) that the extrinsic is valid for + proxy: Optional proxy to use for this extrinsic submission + interactive_selection: If true, prompts for selection of origin and destination subnets. + prompt: If true, prompts for confirmation before executing swap. + wait_for_inclusion: If true, waits for the transaction to be included in a block. + wait_for_finalization: If true, waits for the transaction to be finalized. Returns: (success, extrinsic_identifier): @@ -892,7 +896,7 @@ async def swap_stake( destination_netuid=destination_netuid, amount=amount_to_swap.rao, ), - subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), ) # Display stake movement details @@ -924,48 +928,45 @@ async def swap_stake( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " f"to netuid [blue]{destination_netuid}[/blue]..." ): - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) - - response = await subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=wait_for_inclusion, + success_, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, wait_for_finalization=wait_for_finalization, + wait_for_inclusion=wait_for_inclusion, ) - ext_id = await response.get_extrinsic_identifier() - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, ext_id + if success_: + await print_extrinsic_id(response) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, await response.get_extrinsic_identifier() + else: + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=destination_netuid, + ), + ) - if not await response.is_success: - err_console.print( - f":cross_mark: [red]Failed[/red] with error: " - f"{format_error_message(await response.error_message)}" - ) - return False, "" - await print_extrinsic_id(response) - # Get and display new stake balances - new_stake, new_dest_stake = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - netuid=origin_netuid, - ), - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - netuid=destination_netuid, - ), - ) + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" + ) + return True, ext_id - console.print( - f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" - ) - console.print( - f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}" - ) - return True, ext_id + else: + err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") + return False, "" From 9800121a1a8fbe8622ca186701287579af445108 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 14:53:01 +0200 Subject: [PATCH 029/179] Child keys --- bittensor_cli/cli.py | 22 ++++++++++--- .../src/commands/stake/children_hotkeys.py | 33 +++++++++++-------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 728df7c04..06ec33755 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5382,6 +5382,7 @@ def stake_set_children( help="Enter the stake weight proportions for the child hotkeys (sum should be less than or equal to 1)", prompt=False, ), + proxy: Optional[str] = None, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, @@ -5401,6 +5402,7 @@ def stake_set_children( [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 --prop 0.3 --prop 0.7 """ self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) netuid = get_optional_netuid(netuid, all_netuids) children = list_prompt( @@ -5435,6 +5437,7 @@ def stake_set_children( "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" + f"proxy: {proxy}\n" f"children: {children}\n" f"proportions: {proportions}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" @@ -5451,6 +5454,7 @@ def stake_set_children( wait_for_inclusion=wait_for_inclusion, prompt=prompt, json_output=json_output, + proxy=proxy, ) ) @@ -5472,6 +5476,7 @@ def stake_revoke_children( "--allnetuids", help="When this flag is used it sets child hotkeys on all the subnets.", ), + proxy: Optional[str] = Options.proxy, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, @@ -5489,6 +5494,7 @@ def stake_revoke_children( [green]$[/green] btcli stake child revoke --hotkey --netuid 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5509,16 +5515,18 @@ def stake_revoke_children( "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" + f"proxy: {proxy}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" ) return self._run_command( children_hotkeys.revoke_children( - wallet, - self.initialize_chain(network), - netuid, - wait_for_inclusion, - wait_for_finalization, + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + proxy=proxy, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, prompt=prompt, json_output=json_output, ) @@ -5556,6 +5564,7 @@ def stake_childkey_take( "take value.", prompt=False, ), + proxy: Optional[str] = Options.proxy, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -5579,6 +5588,7 @@ def stake_childkey_take( [green]$[/green] btcli stake child take --child-hotkey-ss58 --take 0.12 --netuid 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5600,6 +5610,7 @@ def stake_childkey_take( f"network: {network}\n" f"netuid: {netuid}\n" f"take: {take}\n" + f"proxy: {proxy}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" ) @@ -5609,6 +5620,7 @@ def stake_childkey_take( subtensor=self.initialize_chain(network), netuid=netuid, take=take, + proxy=proxy, hotkey=child_hotkey_ss58, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index d50ecc65a..eda0b53b4 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -56,6 +56,7 @@ async def set_children_extrinsic( wallet: Wallet, hotkey: str, netuid: int, + proxy: Optional[str], children_with_proportions: list[tuple[float, str]], wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -122,7 +123,7 @@ async def set_children_extrinsic( }, ) success, error_message, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization, proxy=proxy ) if not wait_for_finalization and not wait_for_inclusion: @@ -151,6 +152,7 @@ async def set_childkey_take_extrinsic( hotkey: str, netuid: int, take: float, + proxy: Optional[str] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = True, @@ -163,6 +165,7 @@ async def set_childkey_take_extrinsic( :param: hotkey: Child hotkey. :param: take: Childkey Take value. :param: netuid: Unique identifier of for the subnet. + :param: proxy: Optional proxy to use to make this extrinsic submission. :param: wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. :param: wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning ` @@ -206,7 +209,7 @@ async def set_childkey_take_extrinsic( error_message, ext_receipt, ) = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization, proxy=proxy ) if not wait_for_finalization and not wait_for_inclusion: @@ -223,17 +226,9 @@ async def set_childkey_take_extrinsic( if wait_for_finalization: modifier = "finalized" console.print(":white_heavy_check_mark: [green]Finalized[/green]") - # bittensor.logging.success( - # prefix="Setting childkey take", - # suffix="Finalized: " + str(success), - # ) return True, f"Successfully {modifier} childkey take", ext_id else: console.print(f":cross_mark: [red]Failed[/red]: {error_message}") - # bittensor.logging.warning( - # prefix="Setting childkey take", - # suffix="Failed: " + str(error_message), - # ) return False, error_message, None except SubstrateRequestException as e: @@ -509,8 +504,10 @@ async def set_children( wait_for_finalization: bool = True, prompt: bool = True, json_output: bool = False, + proxy: Optional[str] = None, ): """Set children hotkeys.""" + # TODO holy shit I hate this. It needs to be rewritten. # Validate children SS58 addresses # TODO check to see if this should be allowed to be specified by user instead of pulling from wallet hotkey = get_hotkey_pub_ss58(wallet) @@ -536,6 +533,7 @@ async def set_children( wallet=wallet, netuid=netuid, hotkey=hotkey, + proxy=proxy, children_with_proportions=children_with_proportions, prompt=prompt, wait_for_inclusion=wait_for_inclusion, @@ -579,6 +577,7 @@ async def set_children( wallet=wallet, netuid=netuid_, hotkey=hotkey, + proxy=proxy, children_with_proportions=children_with_proportions, prompt=prompt, wait_for_inclusion=True, @@ -609,6 +608,7 @@ async def revoke_children( wallet: Wallet, subtensor: "SubtensorInterface", netuid: Optional[int] = None, + proxy: Optional[str] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, @@ -625,6 +625,7 @@ async def revoke_children( netuid=netuid, hotkey=get_hotkey_pub_ss58(wallet), children_with_proportions=[], + proxy=proxy, prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -665,6 +666,7 @@ async def revoke_children( netuid=netuid, # TODO should this be able to allow netuid = None ? hotkey=get_hotkey_pub_ss58(wallet), children_with_proportions=[], + proxy=proxy, prompt=prompt, wait_for_inclusion=True, wait_for_finalization=False, @@ -701,6 +703,7 @@ async def childkey_take( take: Optional[float], hotkey: Optional[str] = None, netuid: Optional[int] = None, + proxy: Optional[str] = None, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, @@ -761,28 +764,29 @@ async def set_chk_take_subnet( subnet: int, chk_take: float ) -> tuple[bool, Optional[str]]: """Set the childkey take for a single subnet""" - success, message, ext_id = await set_childkey_take_extrinsic( + success_, message, ext_id_ = await set_childkey_take_extrinsic( subtensor=subtensor, wallet=wallet, netuid=subnet, hotkey=get_hotkey_pub_ss58(wallet), take=chk_take, + proxy=proxy, prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) # Result - if success: + if success_: console.print(":white_heavy_check_mark: [green]Set childkey take.[/green]") console.print( f"The childkey take for {get_hotkey_pub_ss58(wallet)} is now set to {take * 100:.2f}%." ) - return True, ext_id + return True, ext_id_ else: console.print( f":cross_mark:[red] Unable to set childkey take.[/red] {message}" ) - return False, ext_id + return False, ext_id_ # Print childkey take for other user and return (dont offer to change take rate) wallet_hk = get_hotkey_pub_ss58(wallet) @@ -847,6 +851,7 @@ async def set_chk_take_subnet( netuid=netuid_, hotkey=wallet_hk, take=take, + proxy=proxy, prompt=prompt, wait_for_inclusion=True, wait_for_finalization=False, From cdae3da8bb20c9c3de853eac8292d697d66098c3 Mon Sep 17 00:00:00 2001 From: Dera Okeke Date: Fri, 7 Nov 2025 13:59:10 +0100 Subject: [PATCH 030/179] proxy help text --- bittensor_cli/cli.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 12ffc0fe7..deb14ca6a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8117,10 +8117,10 @@ def proxy_create( [bold]Common Examples:[/bold] 1. Create a pure proxy account - [green]$[/green] btcli proxy create --proxy-type any + [green]$[/green] btcli proxy create --proxy-type Any 2. Create a delayed pure proxy account - [green]$[/green] btcli proxy create --proxy-type any --delay 1000 + [green]$[/green] btcli proxy create --proxy-type Any --delay 1000 """ # TODO add debug logger @@ -8172,8 +8172,7 @@ def proxy_add( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """ - + """ Registers an existing account as a standard proxy for the delegator. Grants an existing account permission to execute transactions on your behalf with @@ -8302,12 +8301,15 @@ def proxy_kill( json_output: bool = Options.json_output, ): """ - Permanently removes a pure proxy account. - Once killed, the pure proxy account is cleared from chain storage and cannot be recovered. + Once killed, the pure proxy account is cleared from chain storage and cannot be recovered. + + [bold]⚠️ WARNING[/bold]: Killing a pure proxy permanently removes access to the account, and any funds remaining in it are lost. + + EXAMPLE - Requires the spawner account, proxy type, and creation block details.... TBC + [green]$[/green] btcli proxy kill --height 6345834 --index 3 --proxy-type Any --spawner 5x34SPAWN... --proxy 5CCProxy... """ # TODO add debug logger self.verbosity_handler(quiet, verbose, json_output) From b98f29da754ed4f8c8fb21bd54b7ecbf9c6b6779 Mon Sep 17 00:00:00 2001 From: Dera Okeke Date: Fri, 7 Nov 2025 14:05:13 +0100 Subject: [PATCH 031/179] updated proxy types --- bittensor_cli/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index deb14ca6a..47591f935 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8180,10 +8180,10 @@ def proxy_add( [bold]Common Examples:[/bold] 1. Create a standard proxy account - [green]$[/green] btcli proxy add --delegate 5GDeleg... --proxy-type small-transfer + [green]$[/green] btcli proxy add --delegate 5GDeleg... --proxy-type SmallTransfer 2. Create a delayed standard proxy account - [green]$[/green] btcli proxy add --delegate 5GDeleg... --proxy-type transfer --delay 500 + [green]$[/green] btcli proxy add --delegate 5GDeleg... --proxy-type Transfer --delay 500 """ # TODO add debug logger @@ -8244,7 +8244,7 @@ def proxy_remove( [bold]Common Examples:[/bold] 1. Revoke proxy permissions from a single proxy account - [green]$[/green] btcli proxy remove --delegate 5GDel... --proxy-type transfer + [green]$[/green] btcli proxy remove --delegate 5GDel... --proxy-type Transfer 2. Remove all proxies linked to an account [green]$[/green] btcli proxy remove --all From 7d7ba3bdf20f1988be706350ad42b20ce5684e0d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 15:28:17 +0200 Subject: [PATCH 032/179] Subnets and sudo --- bittensor_cli/cli.py | 87 ++++++++++++++----- .../src/commands/subnets/mechanisms.py | 6 ++ bittensor_cli/src/commands/subnets/subnets.py | 67 +++++++------- bittensor_cli/src/commands/sudo.py | 32 +++++-- 4 files changed, 131 insertions(+), 61 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 06ec33755..874790257 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5647,6 +5647,7 @@ def mechanism_count_set( "--mech-count", help="Number of mechanisms to set for the subnet.", ), + proxy: Optional[str] = Options.proxy, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -5668,7 +5669,7 @@ def mechanism_count_set( [green]$[/green] btcli subnet mech set --netuid 12 --count 2 --wallet.name my_wallet --wallet.hotkey admin """ - + proxy = self.is_valid_proxy_name_or_ss58(proxy) self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) @@ -5729,6 +5730,7 @@ def mechanism_count_set( f"network: {network}\n" f"netuid: {netuid}\n" f"mechanism_count: {mechanism_count}\n" + f"proxy: {proxy}\n" ) result, err_msg, ext_id = self._run_command( @@ -5737,6 +5739,7 @@ def mechanism_count_set( subtensor=subtensor, netuid=netuid, mechanism_count=mechanism_count, + proxy=proxy, previous_count=current_count or 0, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -5794,6 +5797,7 @@ def mechanism_emission_set( "--split", help="Comma-separated relative weights for each mechanism (normalised automatically).", ), + proxy: Optional[str] = Options.proxy, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -5816,7 +5820,7 @@ def mechanism_emission_set( 2. Apply a 70/30 distribution in one command: [green]$[/green] btcli subnet mech emissions-split --netuid 12 --split 70,30 --wallet.name my_wallet --wallet.hotkey admin """ - + proxy = self.is_valid_proxy_name_or_ss58(proxy) self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) wallet = self.wallet_ask( @@ -5831,6 +5835,7 @@ def mechanism_emission_set( subtensor=subtensor, wallet=wallet, netuid=netuid, + proxy=proxy, new_emission_split=split, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -5879,6 +5884,7 @@ def sudo_set( param_value: Optional[str] = typer.Option( "", "--value", help="Value to set the hyperparameter to." ), + proxy: Optional[str] = Options.proxy, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5894,6 +5900,7 @@ def sudo_set( [green]$[/green] btcli sudo set --netuid 1 --param tempo --value 400 """ self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if not param_name or not param_value: hyperparams = self._run_command( @@ -5901,6 +5908,7 @@ def sudo_set( exit_early=False, ) if not hyperparams: + # TODO this will cause a hanging connection, subtensor needs to be gracefully exited raise typer.Exit() if not param_name: @@ -5982,18 +5990,20 @@ def sudo_set( "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" + f"proxy: {proxy}\n" f"param_name: {param_name}\n" f"param_value: {param_value}" ) result, err_msg, ext_id = self._run_command( sudo.sudo_set_hyperparameter( - wallet, - self.initialize_chain(network), - netuid, - param_name, - param_value, - prompt, - json_output, + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + proxy=proxy, + param_name=param_name, + param_value=param_value, + prompt=prompt, + json_output=json_output, ) ) if json_output: @@ -6083,6 +6093,7 @@ def sudo_senate_vote( prompt="Enter the proposal hash", help="The hash of the proposal to vote on.", ), + proxy: Optional[str] = Options.proxy, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -6105,6 +6116,7 @@ def sudo_senate_vote( [green]$[/green] btcli sudo senate_vote --proposal """ # TODO discuss whether this should receive json_output. I don't think it should. + proxy = self.is_valid_proxy_name_or_ss58(proxy) self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) wallet = self.wallet_ask( wallet_name, @@ -6116,7 +6128,12 @@ def sudo_senate_vote( logger.debug(f"args:\nnetwork: {network}\nproposal: {proposal}\nvote: {vote}\n") return self._run_command( sudo.senate_vote( - wallet, self.initialize_chain(network), proposal, vote, prompt + wallet=wallet, + subtensor=self.initialize_chain(network), + proxy=proxy, + proposal_hash=proposal, + vote=vote, + prompt=prompt, ) ) @@ -6126,6 +6143,7 @@ def sudo_set_take( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, take: float = typer.Option(None, help="The new take value."), quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -6143,6 +6161,7 @@ def sudo_set_take( max_value = 0.18 min_value = 0.00 self.verbosity_handler(quiet, verbose, json_output, prompt=False) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, @@ -6153,6 +6172,7 @@ def sudo_set_take( ) self._run_command( + # TODO does this need to take the proxy account? sudo.display_current_take(self.initialize_chain(network), wallet), exit_early=False, ) @@ -6168,7 +6188,12 @@ def sudo_set_take( raise typer.Exit() logger.debug(f"args:\nnetwork: {network}\ntake: {take}") result, ext_id = self._run_command( - sudo.set_take(wallet, self.initialize_chain(network), take) + sudo.set_take( + wallet=wallet, + subtensor=self.initialize_chain(network), + take=take, + proxy=proxy, + ) ) if json_output: json_console.print( @@ -6220,6 +6245,7 @@ def sudo_trim( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, netuid: int = Options.netuid, + proxy: Optional[str] = Options.proxy, max_uids: int = typer.Option( None, "--max", @@ -6240,6 +6266,7 @@ def sudo_trim( [green]$[/green] btcli sudo trim --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey --max 64 """ self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, @@ -6255,6 +6282,7 @@ def sudo_trim( netuid=netuid, max_n=max_uids, period=period, + proxy=proxy, json_output=json_output, prompt=prompt, ) @@ -6516,6 +6544,7 @@ def subnets_create( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, + proxy: Optional[str] = Options.proxy, subnet_name: Optional[str] = typer.Option( None, "--subnet-name", help="Name of the subnet" ), @@ -6562,6 +6591,7 @@ def subnets_create( [green]$[/green] btcli subnets create --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6584,10 +6614,17 @@ def subnets_create( logo_url=logo_url, additional=additional_info, ) - logger.debug(f"args:\nnetwork: {network}\nidentity: {identity}\n") + logger.debug( + f"args:\nnetwork: {network}\nidentity: {identity}\nproxy: {proxy}\n" + ) self._run_command( subnets.create( - wallet, self.initialize_chain(network), identity, json_output, prompt + wallet=wallet, + subtensor=self.initialize_chain(network), + subnet_identity=identity, + proxy=proxy, + json_output=json_output, + prompt=prompt, ) ) @@ -6617,6 +6654,7 @@ def subnets_start( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, + proxy: Optional[str] = Options.proxy, netuid: int = Options.netuid, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -6632,6 +6670,7 @@ def subnets_start( [green]$[/green] btcli subnets start --netuid 1 --wallet-name alice """ self.verbosity_handler(quiet, verbose, json_output=False, prompt=prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if not wallet_name: wallet_name = Prompt.ask( "Enter the [blue]wallet name[/blue] [dim](which you used to create the subnet)[/dim]", @@ -6646,13 +6685,14 @@ def subnets_start( ], validate=WV.WALLET, ) - logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\n") + logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\nproxy: {proxy}\n") return self._run_command( subnets.start_subnet( - wallet, - self.initialize_chain(network), - netuid, - prompt, + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + proxy=proxy, + prompt=prompt, ) ) @@ -6685,6 +6725,7 @@ def subnets_set_identity( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, + proxy: Optional[str] = Options.proxy, subnet_name: Optional[str] = typer.Option( None, "--subnet-name", "--sn-name", help="Name of the subnet" ), @@ -6730,6 +6771,7 @@ def subnets_set_identity( [green]$[/green] btcli subnets set-identity --netuid 1 --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6763,11 +6805,16 @@ def subnets_set_identity( additional=additional_info, ) logger.debug( - f"args:\nnetwork: {network}\nnetuid: {netuid}\nidentity: {identity}" + f"args:\nnetwork: {network}\nnetuid: {netuid}\nidentity: {identity}\nproxy: {proxy}\n" ) success, ext_id = self._run_command( subnets.set_identity( - wallet, self.initialize_chain(network), netuid, identity, prompt + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + subnet_identity=identity, + prompt=prompt, + proxy=proxy, ) ) if json_output: diff --git a/bittensor_cli/src/commands/subnets/mechanisms.py b/bittensor_cli/src/commands/subnets/mechanisms.py index 2ad5d72db..dfa7c165b 100644 --- a/bittensor_cli/src/commands/subnets/mechanisms.py +++ b/bittensor_cli/src/commands/subnets/mechanisms.py @@ -182,6 +182,7 @@ async def set_emission_split( wallet: Wallet, netuid: int, new_emission_split: Optional[str], + proxy: Optional[str], wait_for_inclusion: bool, wait_for_finalization: bool, prompt: bool, @@ -356,6 +357,7 @@ async def set_emission_split( subtensor=subtensor, netuid=netuid, split=normalized_weights, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, json_output=json_output, @@ -406,6 +408,7 @@ async def set_mechanism_count( netuid: int, mechanism_count: int, previous_count: int, + proxy: Optional[str], wait_for_inclusion: bool, wait_for_finalization: bool, json_output: bool, @@ -436,6 +439,7 @@ async def set_mechanism_count( wallet=wallet, netuid=netuid, mech_count=mechanism_count, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -461,6 +465,7 @@ async def set_mechanism_emission( subtensor: "SubtensorInterface", netuid: int, split: list[int], + proxy: Optional[str], wait_for_inclusion: bool, wait_for_finalization: bool, json_output: bool, @@ -480,6 +485,7 @@ async def set_mechanism_emission( split=split, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) ext_id = await ext_receipt.get_extrinsic_identifier() if success else None diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index ed2f4350b..41911f54b 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -53,6 +53,7 @@ async def register_subnetwork_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, subnet_identity: dict, + proxy: Optional[str], wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, @@ -75,6 +76,7 @@ async def register_subnetwork_extrinsic( extrinsic_identifier: Optional extrinsic identifier, if the extrinsic was included. """ + # TODO why doesn't this have an era? async def _find_event_attributes_in_extrinsic_receipt( response_, event_name: str ) -> list: @@ -102,9 +104,9 @@ async def _find_event_attributes_in_extrinsic_receipt( sn_burn_cost = await burn_cost(subtensor) if sn_burn_cost > your_balance: err_console.print( - f"Your balance of: [{COLOR_PALETTE['POOLS']['TAO']}]{your_balance}[{COLOR_PALETTE['POOLS']['TAO']}]" + f"Your balance of: [{COLOR_PALETTE.POOLS.TAO}]{your_balance}[{COLOR_PALETTE.POOLS.TAO}]" f" is not enough to burn " - f"[{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}] " + f"[{COLOR_PALETTE.POOLS.TAO}]{sn_burn_cost}[{COLOR_PALETTE.POOLS.TAO}] " f"to register a subnet." ) return False, None, None @@ -174,24 +176,20 @@ async def _find_event_attributes_in_extrinsic_receipt( call_function=call_function, call_params=call_params, ) - extrinsic = await substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) - response = await substrate.submit_extrinsic( - extrinsic, + success, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: return True, None, None - if not await response.is_success: - err_console.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}" - ) - await asyncio.sleep(0.5) + if not success: + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") return False, None, None # Successful registration, final check for membership @@ -1620,6 +1618,7 @@ async def create( wallet: Wallet, subtensor: "SubtensorInterface", subnet_identity: dict, + proxy: Optional[str], json_output: bool, prompt: bool, ): @@ -1627,7 +1626,11 @@ async def create( # Call register command. success, netuid, ext_id = await register_subnetwork_extrinsic( - subtensor, wallet, subnet_identity, prompt=prompt + subtensor=subtensor, + wallet=wallet, + subnet_identity=subnet_identity, + prompt=prompt, + proxy=proxy, ) if json_output: # technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present @@ -1668,16 +1671,16 @@ async def create( ) await set_id( - wallet, - subtensor, - identity["name"], - identity["url"], - identity["image"], - identity["discord"], - identity["description"], - identity["additional"], - identity["github_repo"], - prompt, + wallet=wallet, + subtensor=subtensor, + name=identity["name"], + web_url=identity["url"], + image_url=identity["image"], + discord=identity["discord"], + description=identity["description"], + additional=identity["additional"], + github_repo=identity["github_repo"], + proxy=proxy, ) @@ -2362,6 +2365,7 @@ async def set_identity( netuid: int, subnet_identity: dict, prompt: bool = False, + proxy: Optional[str] = None, ) -> tuple[bool, Optional[str]]: """Set identity information for a subnet""" @@ -2422,7 +2426,7 @@ async def set_identity( spinner="earth", ): success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) if not success: @@ -2565,6 +2569,7 @@ async def start_subnet( wallet: "Wallet", subtensor: "SubtensorInterface", netuid: int, + proxy: Optional[str], prompt: bool = False, ) -> bool: """Start a subnet's emission schedule""" @@ -2578,6 +2583,7 @@ async def start_subnet( storage_function="SubnetOwner", params=[netuid], ) + # TODO should this check against proxy as well? if subnet_owner != wallet.coldkeypub.ss58_address: print_error(":cross_mark: This wallet doesn't own the specified subnet.") return False @@ -2599,26 +2605,21 @@ async def start_subnet( call_function="start_call", call_params={"netuid": netuid}, ) - - signed_ext = await subtensor.substrate.create_signed_extrinsic( + success, error_msg, response = await subtensor.sign_and_send_extrinsic( call=start_call, - keypair=wallet.coldkey, - ) - - response = await subtensor.substrate.submit_extrinsic( - extrinsic=signed_ext, + wallet=wallet, wait_for_inclusion=True, wait_for_finalization=True, + proxy=proxy, ) - if await response.is_success: + if success: await print_extrinsic_id(response) console.print( f":white_heavy_check_mark: [green]Successfully started subnet {netuid}'s emission schedule.[/green]" ) return True else: - error_msg = format_error_message(await response.error_message) if "FirstEmissionBlockNumberAlreadySet" in error_msg: console.print( f"[dark_sea_green3]Subnet {netuid} already has an emission schedule.[/dark_sea_green3]" diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 76cc0addd..f192bbd66 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -176,6 +176,7 @@ async def set_mechanism_count_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", netuid: int, + proxy: Optional[str], mech_count: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, @@ -204,6 +205,7 @@ async def set_mechanism_count_extrinsic( wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) if not success: @@ -216,6 +218,7 @@ async def set_mechanism_emission_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", netuid: int, + proxy: Optional[str], split: list[int], wait_for_inclusion: bool = True, wait_for_finalization: bool = True, @@ -242,6 +245,7 @@ async def set_mechanism_emission_extrinsic( wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) if not success: @@ -254,6 +258,7 @@ async def set_hyperparameter_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", netuid: int, + proxy: Optional[str], parameter: str, value: Optional[Union[str, float, list[float]]], wait_for_inclusion: bool = False, @@ -265,6 +270,7 @@ async def set_hyperparameter_extrinsic( :param subtensor: initialized SubtensorInterface object :param wallet: bittensor wallet object. :param netuid: Subnetwork `uid`. + :param proxy: Optional proxy to use for this extrinsic submission. :param parameter: Hyperparameter name. :param value: New hyperparameter value. :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns @@ -285,6 +291,7 @@ async def set_hyperparameter_extrinsic( storage_function="SubnetOwner", params=[netuid], ) + # TODO does this need to check proxy? if subnet_owner != wallet.coldkeypub.ss58_address: err_msg = ( ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" @@ -384,7 +391,7 @@ async def set_hyperparameter_extrinsic( spinner="earth", ): success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization, proxy=proxy ) if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") @@ -565,6 +572,7 @@ async def _is_senate_member(subtensor: "SubtensorInterface", hotkey_ss58: str) - async def vote_senate_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, + proxy: Optional[str], proposal_hash: str, proposal_idx: int, vote: bool, @@ -576,6 +584,7 @@ async def vote_senate_extrinsic( :param subtensor: The SubtensorInterface object to use for the query :param wallet: Bittensor wallet object, with coldkey and hotkey unlocked. + :param proxy: Optional proxy address to use for the extrinsic submission :param proposal_hash: The hash of the proposal for which voting data is requested. :param proposal_idx: The index of the proposal to vote. :param vote: Whether to vote aye or nay. @@ -606,11 +615,10 @@ async def vote_senate_extrinsic( }, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization, proxy=proxy ) if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - await asyncio.sleep(0.5) return False # Successful vote, final check for data else: @@ -638,6 +646,7 @@ async def set_take_extrinsic( wallet: Wallet, delegate_ss58: str, take: float = 0.0, + proxy: Optional[str] = None, ) -> tuple[bool, Optional[str]]: """ Set delegate hotkey take @@ -646,6 +655,7 @@ async def set_take_extrinsic( :param wallet: The wallet containing the hotkey to be nominated. :param delegate_ss58: Hotkey :param take: Delegate take on subnet ID + :param proxy: Optional proxy address to use for the extrinsic submission :return: `True` if the process is successful, `False` otherwise. @@ -682,7 +692,7 @@ async def set_take_extrinsic( }, ) success, err, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) else: @@ -702,7 +712,7 @@ async def set_take_extrinsic( }, ) success, err, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) if not success: @@ -724,6 +734,7 @@ async def sudo_set_hyperparameter( wallet: Wallet, subtensor: "SubtensorInterface", netuid: int, + proxy: Optional[str], param_name: str, param_value: Optional[str], prompt: bool, @@ -741,7 +752,7 @@ async def sudo_set_hyperparameter( if json_output: prompt = False success, err_msg, ext_id = await set_hyperparameter_extrinsic( - subtensor, wallet, netuid, param_name, value, prompt=prompt + subtensor, wallet, netuid, proxy, param_name, value, prompt=prompt ) if json_output: return success, err_msg, ext_id @@ -955,6 +966,7 @@ async def proposals( async def senate_vote( wallet: Wallet, subtensor: "SubtensorInterface", + proxy: Optional[str], proposal_hash: str, vote: bool, prompt: bool, @@ -991,6 +1003,7 @@ async def senate_vote( success = await vote_senate_extrinsic( subtensor=subtensor, wallet=wallet, + proxy=proxy, proposal_hash=proposal_hash, proposal_idx=vote_data.index, vote=vote, @@ -1015,7 +1028,7 @@ async def display_current_take(subtensor: "SubtensorInterface", wallet: Wallet) async def set_take( - wallet: Wallet, subtensor: "SubtensorInterface", take: float + wallet: Wallet, subtensor: "SubtensorInterface", take: float, proxy: Optional[str] ) -> tuple[bool, Optional[str]]: """Set delegate take.""" @@ -1042,6 +1055,7 @@ async def _do_set_take() -> tuple[bool, Optional[str]]: wallet=wallet, delegate_ss58=hotkey_ss58, take=take, + proxy=proxy, ) success, ext_id = result @@ -1069,6 +1083,7 @@ async def trim( wallet: Wallet, subtensor: "SubtensorInterface", netuid: int, + proxy: Optional[str], max_n: int, period: int, prompt: bool, @@ -1083,6 +1098,7 @@ async def trim( storage_function="SubnetOwner", params=[netuid], ) + # TODO should this check proxy also? if subnet_owner != wallet.coldkeypub.ss58_address: err_msg = "This wallet doesn't own the specified subnet." if json_output: @@ -1102,7 +1118,7 @@ async def trim( call_params={"netuid": netuid, "max_n": max_n}, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call=call, wallet=wallet, era={"period": period} + call=call, wallet=wallet, era={"period": period}, proxy=proxy ) if not success: if json_output: From 7829ad1eadc9df60d554d8df8fc1cb4d06774c85 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 16:45:39 +0200 Subject: [PATCH 033/179] Weights --- bittensor_cli/cli.py | 68 ++++++----- .../src/bittensor/subtensor_interface.py | 6 +- bittensor_cli/src/commands/subnets/subnets.py | 22 ++-- bittensor_cli/src/commands/weights.py | 108 ++++++++---------- 4 files changed, 106 insertions(+), 98 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 874790257..18fc5fd52 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6953,7 +6953,9 @@ def subnets_register( ask_for=[WO.NAME, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\nperiod: {period}\n") + logger.debug( + f"args:\nnetwork: {network}\nnetuid: {netuid}\nperiod: {period}\nproxy: {proxy}\n" + ) return self._run_command( subnets.register( wallet=wallet, @@ -7074,6 +7076,8 @@ def subnets_set_symbol( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, + proxy: Optional[str] = Options.proxy, + period: int = Options.period, json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -7095,6 +7099,7 @@ def subnets_set_symbol( [#AFEFFF]{success: [dark_orange]bool[/dark_orange], message: [dark_orange]str[/dark_orange]}[/#AFEFFF] """ self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if len(symbol) > 1: err_console.print("Your symbol must be a single character.") return False @@ -7105,12 +7110,21 @@ def subnets_set_symbol( ask_for=[WO.NAME, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug( + "args:\n" + f"network: {network}\n" + f"netuid: {netuid}\n" + f"proxy: {proxy}\n" + f"symbol: {symbol}\n" + ) return self._run_command( subnets.set_symbol( wallet=wallet, subtensor=self.initialize_chain(network), netuid=netuid, symbol=symbol, + proxy=proxy, + period=period, prompt=prompt, json_output=json_output, ) @@ -7123,6 +7137,7 @@ def weights_reveal( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: int = Options.netuid, + proxy: Optional[str] = Options.proxy, uids: str = typer.Option( None, "--uids", @@ -7151,11 +7166,7 @@ def weights_reveal( [green]$[/green] btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - # TODO think we need to ','.split uids and weights ? - uids = list_prompt(uids, int, "UIDs of interest for the specified netuid") - weights = list_prompt( - weights, float, "Corresponding weights for the specified UIDs" - ) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if uids: uids = parse_to_list( uids, @@ -7164,7 +7175,7 @@ def weights_reveal( ) else: uids = list_prompt( - uids, int, "Corresponding UIDs for the specified netuid (eg: 1,2,3)" + [], int, "Corresponding UIDs for the specified netuid (eg: 1,2,3)" ) if weights: @@ -7175,7 +7186,7 @@ def weights_reveal( ) else: weights = list_prompt( - weights, + [], float, "Corresponding weights for the specified UIDs (eg: 0.2,0.3,0.4)", ) @@ -7193,7 +7204,7 @@ def weights_reveal( "Salt must be a comma-separated list of ints, e.g., `--weights 123,163,194`.", ) else: - salt = list_prompt(salt, int, "Corresponding salt for the hash function") + salt = list_prompt([], int, "Corresponding salt for the hash function") wallet = self.wallet_ask( wallet_name, @@ -7204,13 +7215,14 @@ def weights_reveal( ) return self._run_command( weights_cmds.reveal_weights( - self.initialize_chain(network), - wallet, - netuid, - uids, - weights, - salt, - __version_as_int__, + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + proxy=proxy, + uids=uids, + weights=weights, + salt=salt, + version=__version_as_int__, prompt=prompt, json_output=json_output, ) @@ -7223,6 +7235,7 @@ def weights_commit( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: int = Options.netuid, + proxy: Optional[str] = Options.proxy, uids: str = typer.Option( None, "--uids", @@ -7255,7 +7268,7 @@ def weights_commit( permissions. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - + proxy = self.is_valid_proxy_name_or_ss58(proxy) if uids: uids = parse_to_list( uids, @@ -7264,7 +7277,7 @@ def weights_commit( ) else: uids = list_prompt( - uids, int, "UIDs of interest for the specified netuid (eg: 1,2,3)" + [], int, "UIDs of interest for the specified netuid (eg: 1,2,3)" ) if weights: @@ -7275,7 +7288,7 @@ def weights_commit( ) else: weights = list_prompt( - weights, + [], float, "Corresponding weights for the specified UIDs (eg: 0.2,0.3,0.4)", ) @@ -7292,7 +7305,7 @@ def weights_commit( "Salt must be a comma-separated list of ints, e.g., `--weights 123,163,194`.", ) else: - salt = list_prompt(salt, int, "Corresponding salt for the hash function") + salt = list_prompt([], int, "Corresponding salt for the hash function") wallet = self.wallet_ask( wallet_name, @@ -7303,13 +7316,14 @@ def weights_commit( ) return self._run_command( weights_cmds.commit_weights( - self.initialize_chain(network), - wallet, - netuid, - uids, - weights, - salt, - __version_as_int__, + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + uids=uids, + proxy=proxy, + weights=weights, + salt=salt, + version=__version_as_int__, json_output=json_output, prompt=prompt, ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 7e5ab92cd..e399220cc 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,7 +1,7 @@ import asyncio import os import time -from typing import Optional, Any, Union, TypedDict, Iterable +from typing import Optional, Any, Union, TypedDict, Iterable, Literal from async_substrate_interface import AsyncExtrinsicReceipt from async_substrate_interface.async_substrate import ( @@ -1152,6 +1152,7 @@ async def sign_and_send_extrinsic( era: Optional[dict[str, int]] = None, proxy: Optional[str] = None, nonce: Optional[str] = None, + sign_with: Literal["coldkey", "hotkey", "coldkeypub"] = "coldkey", ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1163,6 +1164,7 @@ async def sign_and_send_extrinsic( :param era: The length (in blocks) for which a transaction should be valid. :param proxy: The real account used to create the proxy. None if not using a proxy for this call. :param nonce: The nonce used to submit this extrinsic call. + :param sign_with: Determine which of the wallet's keypairs to use to sign the extrinsic call. :return: (success, error message, extrinsic receipt | None) """ @@ -1174,7 +1176,7 @@ async def sign_and_send_extrinsic( ) call_args: dict[str, Union[GenericCall, Keypair, dict[str, int], int]] = { "call": call, - "keypair": wallet.coldkey, + "keypair": getattr(wallet, sign_with), "nonce": nonce, } if era is not None: diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 41911f54b..ec462c114 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -2636,6 +2636,8 @@ async def set_symbol( subtensor: "SubtensorInterface", netuid: int, symbol: str, + proxy: Optional[str], + period: int, prompt: bool = False, json_output: bool = False, ) -> bool: @@ -2676,16 +2678,11 @@ async def set_symbol( call_params={"netuid": netuid, "symbol": symbol.encode("utf-8")}, ) - signed_ext = await subtensor.substrate.create_signed_extrinsic( - call=start_call, - keypair=wallet.coldkey, + success, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=start_call, wallet=wallet, proxy=proxy, era={"period": period} ) - response = await subtensor.substrate.submit_extrinsic( - extrinsic=signed_ext, - wait_for_inclusion=True, - ) - if await response.is_success: + if success: ext_id = await response.get_extrinsic_identifier() await print_extrinsic_id(response) message = f"Successfully updated SN{netuid}'s symbol to {symbol}." @@ -2701,11 +2698,14 @@ async def set_symbol( console.print(f":white_heavy_check_mark:[dark_sea_green3] {message}\n") return True else: - err = format_error_message(await response.error_message) if json_output: json_console.print_json( - data={"success": False, "message": err, "extrinsic_identifier": None} + data={ + "success": False, + "message": err_msg, + "extrinsic_identifier": None, + } ) else: - err_console.print(f":cross_mark: [red]Failed[/red]: {err}") + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") return False diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index b61bbd81f..99e110c7d 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -36,6 +36,7 @@ def __init__( subtensor: "SubtensorInterface", wallet: Wallet, netuid: int, + proxy: Optional[str], uids: NDArray, weights: NDArray, salt: list[int], @@ -47,6 +48,7 @@ def __init__( self.subtensor = subtensor self.wallet = wallet self.netuid = netuid + self.proxy = proxy self.uids = uids self.weights = weights self.salt = salt @@ -222,19 +224,12 @@ async def reveal(self, weight_uids, weight_vals) -> tuple[bool, str, Optional[st console.print( ":white_heavy_check_mark: [green]Weights hash revealed on chain[/green]" ) - # bittensor.logging.success(prefix="Weights hash revealed", suffix=str(msg)) - return ( True, "Successfully revealed previously commited weights hash.", ext_id, ) else: - # bittensor.logging.error( - # msg=msg, - # prefix=f"Failed to reveal previously commited weights hash for salt: {salt}", - # suffix="Failed: ", - # ) return False, "Failed to reveal weights.", None async def _set_weights_without_commit_reveal( @@ -254,29 +249,25 @@ async def _do_set_weights() -> tuple[bool, str, Optional[str]]: }, ) # Period dictates how long the extrinsic will stay as part of waiting pool - extrinsic = await self.subtensor.substrate.create_signed_extrinsic( + success, err_msg, response = await self.subtensor.sign_and_send_extrinsic( call=call, - keypair=self.wallet.hotkey, + sign_with="hotkey", + wallet=self.wallet, era={"period": 5}, + wait_for_finalization=True, + wait_for_inclusion=True, + proxy=self.proxy, ) - try: - response = await self.subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=self.wait_for_inclusion, - wait_for_finalization=self.wait_for_finalization, - ) - except SubstrateRequestException as e: - return False, format_error_message(e), None # We only wait here if we expect finalization. if not self.wait_for_finalization and not self.wait_for_inclusion: return True, "Not waiting for finalization or inclusion.", None - if await response.is_success: + if success: ext_id_ = await response.get_extrinsic_identifier() await print_extrinsic_id(response) return True, "Successfully set weights.", ext_id_ else: - return False, format_error_message(await response.error_message), None + return False, err_msg, None with console.status( f":satellite: Setting weights on [white]{self.subtensor.network}[/white] ..." @@ -311,43 +302,27 @@ async def reveal_weights_extrinsic( "version_key": self.version_key, }, ) - extrinsic = await self.subtensor.substrate.create_signed_extrinsic( + success, error_message, response = await self.subtensor.sign_and_send_extrinsic( call=call, - keypair=self.wallet.hotkey, + wallet=self.wallet, + sign_with="hotkey", + wait_for_inclusion=self.wait_for_inclusion, + wait_for_finalization=self.wait_for_finalization, + proxy=self.proxy, ) - try: - response = await self.subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=self.wait_for_inclusion, - wait_for_finalization=self.wait_for_finalization, - ) - except SubstrateRequestException as e: - return False, format_error_message(e), None if not self.wait_for_finalization and not self.wait_for_inclusion: - success, error_message, ext_id = True, "", None - - else: - if await response.is_success: - success, error_message, ext_id = ( - True, - "", - await response.get_extrinsic_identifier(), - ) - await print_extrinsic_id(response) - else: - success, error_message, ext_id = ( - False, - format_error_message(await response.error_message), - None, - ) + return True, "", None if success: - # bittensor.logging.info("Successfully revealed weights.") - return True, "Successfully revealed weights.", ext_id + await print_extrinsic_id(response) + return ( + True, + "Successfully revealed weights.", + await response.get_extrinsic_identifier(), + ) else: - # bittensor.logging.error(f"Failed to reveal weights: {error_message}") - return False, error_message, ext_id + return False, error_message, None async def do_commit_weights( self, commit_hash @@ -360,25 +335,24 @@ async def do_commit_weights( "commit_hash": commit_hash, }, ) - extrinsic = await self.subtensor.substrate.create_signed_extrinsic( + success, err_msg, response = await self.subtensor.sign_and_send_extrinsic( call=call, - keypair=self.wallet.hotkey, - ) - response = await self.subtensor.substrate.submit_extrinsic( - extrinsic, + wallet=self.wallet, + sign_with="hotkey", wait_for_inclusion=self.wait_for_inclusion, wait_for_finalization=self.wait_for_finalization, + proxy=self.proxy, ) if not self.wait_for_finalization and not self.wait_for_inclusion: return True, None, None - if await response.is_success: + if success: ext_id = await response.get_extrinsic_identifier() await print_extrinsic_id(response) return True, None, ext_id else: - return False, await response.error_message, None + return False, err_msg, None # commands @@ -388,6 +362,7 @@ async def reveal_weights( subtensor: "SubtensorInterface", wallet: Wallet, netuid: int, + proxy: Optional[str], uids: list[int], weights: list[float], salt: list[int], @@ -413,7 +388,15 @@ async def reveal_weights( ) # Call the reveal function in the module set_weights from extrinsics package extrinsic = SetWeightsExtrinsic( - subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + uids=uids_, + weights=weights_, + salt=list(salt_), + version_key=version, + prompt=prompt, + proxy=proxy, ) success, message, ext_id = await extrinsic.reveal(weight_uids, weight_vals) if json_output: @@ -434,6 +417,7 @@ async def commit_weights( wallet: Wallet, netuid: int, uids: list[int], + proxy: Optional[str], weights: list[float], salt: list[int], version: int, @@ -454,7 +438,15 @@ async def commit_weights( dtype=np.int64, ) extrinsic = SetWeightsExtrinsic( - subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + uids=uids_, + weights=weights_, + salt=list(salt_), + version_key=version, + prompt=prompt, + proxy=proxy, ) success, message, ext_id = await extrinsic.set_weights_extrinsic() if json_output: From 954bb9ab9842760279d399b4d7648039717a8ada Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 17:05:59 +0200 Subject: [PATCH 034/179] Stake --- bittensor_cli/cli.py | 251 +++++++++++----------- bittensor_cli/src/commands/stake/claim.py | 23 +- 2 files changed, 141 insertions(+), 133 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 18fc5fd52..8f161e6e1 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5302,6 +5302,130 @@ def stake_swap( ) return result + def stake_set_claim_type( + self, + claim_type: Annotated[ + Optional[claim_stake.ClaimType], + typer.Argument( + None, + help="Claim type: 'keep' or 'swap'. If omitted, user will be prompted.", + ), + ] = None, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + network: Optional[list[str]] = Options.network, + proxy: Optional[str] = Options.proxy, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Set the root claim type for your coldkey. + + Root claim types control how staking emissions are handled on the ROOT network (subnet 0): + + [bold]Claim Types:[/bold] + • [green]Swap[/green]: Future Root Alpha Emissions are swapped to TAO and added to root stake (default) + • [yellow]Keep[/yellow]: Future Root Alpha Emissions are kept as Alpha tokens + + USAGE: + + [green]$[/green] btcli stake claim + [green]$[/green] btcli stake claim keep + [green]$[/green] btcli stake claim swap + + With specific wallet: + + [green]$[/green] btcli stake claim swap --wallet-name my_wallet + """ + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + ) + return self._run_command( + claim_stake.set_claim_type( + wallet=wallet, + subtensor=self.initialize_chain(network), + claim_type=claim_type, + proxy=proxy, + prompt=prompt, + json_output=json_output, + ) + ) + + def stake_process_claim( + self, + netuids: Optional[str] = Options.netuids, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + network: Optional[list[str]] = Options.network, + proxy: Optional[str] = Options.proxy, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Manually claim accumulated root network emissions for your coldkey. + + [bold]Note:[/bold] The network will eventually process your pending emissions automatically. + However, you can choose to manually claim your emissions with a small extrinsic fee. + + A maximum of 5 netuids can be processed in one call. + + USAGE: + + [green]$[/green] btcli stake process-claim + + Claim from specific netuids: + + [green]$[/green] btcli stake process-claim --netuids 1,2,3 + + Claim with specific wallet: + + [green]$[/green] btcli stake process-claim --netuids 1,2 --wallet-name my_wallet + + """ + self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) + parsed_netuids = None + if netuids: + parsed_netuids = parse_to_list( + netuids, + int, + "Netuids must be a comma-separated list of ints, e.g., `--netuids 1,2,3`.", + ) + + if len(parsed_netuids) > 5: + print_error("Maximum 5 netuids allowed per claim") + return + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + ) + + return self._run_command( + claim_stake.process_pending_claims( + wallet=wallet, + subtensor=self.initialize_chain(network), + netuids=parsed_netuids, + proxy=proxy, + prompt=prompt, + json_output=json_output, + verbose=verbose, + ) + ) + def stake_get_children( self, wallet_name: Optional[str] = Options.wallet_name, @@ -7389,133 +7513,6 @@ def view_dashboard( ) ) - def stake_set_claim_type( - self, - claim_type: Optional[str] = typer.Argument( - None, - help="Claim type: 'keep' or 'swap'. If not provided, you'll be prompted to choose.", - ), - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - json_output: bool = Options.json_output, - ): - """ - Set the root claim type for your coldkey. - - Root claim types control how staking emissions are handled on the ROOT network (subnet 0): - - [bold]Claim Types:[/bold] - • [green]Swap[/green]: Future Root Alpha Emissions are swapped to TAO and added to root stake (default) - • [yellow]Keep[/yellow]: Future Root Alpha Emissions are kept as Alpha tokens - - USAGE: - - [green]$[/green] btcli stake claim - [green]$[/green] btcli stake claim keep - [green]$[/green] btcli stake claim swap - - With specific wallet: - - [green]$[/green] btcli stake claim swap --wallet-name my_wallet - """ - self.verbosity_handler(quiet, verbose, json_output, prompt) - - if claim_type is not None: - claim_type_normalized = claim_type.capitalize() - if claim_type_normalized not in ["Keep", "Swap"]: - err_console.print( - f":cross_mark: [red]Invalid claim type '{claim_type}'. Must be 'keep' or 'swap'.[/red]" - ) - raise typer.Exit() - else: - claim_type_normalized = None - - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME], - ) - return self._run_command( - claim_stake.set_claim_type( - wallet=wallet, - subtensor=self.initialize_chain(network), - claim_type=claim_type_normalized, - prompt=prompt, - json_output=json_output, - ) - ) - - def stake_process_claim( - self, - netuids: Optional[str] = Options.netuids, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - json_output: bool = Options.json_output, - ): - """ - Manually claim accumulated root network emissions for your coldkey. - - [bold]Note:[/bold] The network will eventually process your pending emissions automatically. - However, you can choose to manually claim your emissions with a small extrinsic fee. - - A maximum of 5 netuids can be processed in one call. - - USAGE: - - [green]$[/green] btcli stake process-claim - - Claim from specific netuids: - - [green]$[/green] btcli stake process-claim --netuids 1,2,3 - - Claim with specific wallet: - - [green]$[/green] btcli stake process-claim --netuids 1,2 --wallet-name my_wallet - - """ - self.verbosity_handler(quiet, verbose, json_output, prompt) - - parsed_netuids = None - if netuids: - parsed_netuids = parse_to_list( - netuids, - int, - "Netuids must be a comma-separated list of ints, e.g., `--netuids 1,2,3`.", - ) - - if len(parsed_netuids) > 5: - print_error("Maximum 5 netuids allowed per claim") - return - - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME], - ) - - return self._run_command( - claim_stake.process_pending_claims( - wallet=wallet, - subtensor=self.initialize_chain(network), - netuids=parsed_netuids, - prompt=prompt, - json_output=json_output, - verbose=verbose, - ) - ) - def liquidity_add( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 67147a82c..88096a5db 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -1,5 +1,6 @@ import asyncio import json +from enum import Enum from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet @@ -22,10 +23,16 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +class ClaimType(str, Enum): + keep = "Keep" + swap = "Swap" + + async def set_claim_type( wallet: Wallet, subtensor: "SubtensorInterface", - claim_type: Optional[str] = None, + claim_type: Optional[ClaimType], + proxy: Optional[str], prompt: bool = True, json_output: bool = False, ) -> tuple[bool, str, Optional[str]]: @@ -39,7 +46,8 @@ async def set_claim_type( Args: wallet: Bittensor wallet object subtensor: SubtensorInterface object - claim_type: Optional claim type ("Keep" or "Swap"). If None, user will be prompted. + claim_type: Claim type ("Keep" or "Swap"). If omitted, user will be prompted. + proxy: Optional proxy to use with this extrinsic submission. prompt: Whether to prompt for user confirmation json_output: Whether to output JSON @@ -80,7 +88,7 @@ async def set_claim_type( console.print(claim_table) new_type = ( - claim_type + claim_type.value if claim_type else Prompt.ask( "Select new root claim type", choices=["Swap", "Keep"], default=current_type @@ -160,7 +168,7 @@ async def set_claim_type( call_params={"new_root_claim_type": new_type}, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) if success: @@ -204,6 +212,7 @@ async def process_pending_claims( wallet: Wallet, subtensor: "SubtensorInterface", netuids: Optional[list[int]] = None, + proxy: Optional[str] = None, prompt: bool = True, json_output: bool = False, verbose: bool = False, @@ -305,7 +314,9 @@ async def process_pending_claims( call_function="claim_root", call_params={"subnets": selected_netuids}, ) - extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) console.print(f"\n[dim]Estimated extrinsic fee: {extrinsic_fee.tao:.9f} τ[/dim]") if prompt: @@ -346,7 +357,7 @@ async def process_pending_claims( spinner="earth", ): success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( - call, wallet + call, wallet, proxy=proxy ) if success: ext_id = await ext_receipt.get_extrinsic_identifier() From 5f09078e6e4e326c8fb3c2d016170b69e4a1fd39 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 7 Nov 2025 17:15:33 +0200 Subject: [PATCH 035/179] Fixed Annotated --- bittensor_cli/cli.py | 3 +-- bittensor_cli/src/commands/stake/claim.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8f161e6e1..88c303e10 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5307,8 +5307,7 @@ def stake_set_claim_type( claim_type: Annotated[ Optional[claim_stake.ClaimType], typer.Argument( - None, - help="Claim type: 'keep' or 'swap'. If omitted, user will be prompted.", + help="Claim type: 'Keep' or 'Swap'. If omitted, user will be prompted.", ), ] = None, wallet_name: Optional[str] = Options.wallet_name, diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 88096a5db..a765d6f02 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -23,9 +23,9 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -class ClaimType(str, Enum): - keep = "Keep" - swap = "Swap" +class ClaimType(Enum): + Keep = "Keep" + Swap = "Swap" async def set_claim_type( From 912949cb1726fe246426852e9722494dd1e6247d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 10 Nov 2025 20:26:17 +0200 Subject: [PATCH 036/179] Ruff --- bittensor_cli/cli.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d66715519..27043a284 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8382,13 +8382,13 @@ def proxy_create( """ Creates a new pure proxy account. The pure proxy account is a keyless account controlled by your wallet. - [bold]Note[/bold]: The proxy account has no private key and cannot sign transactions independently. + [bold]Note[/bold]: The proxy account has no private key and cannot sign transactions independently. All operations must be initiated and signed by the delegator. [bold]Common Examples:[/bold] 1. Create a pure proxy account - [green]$[/green] btcli proxy create --proxy-type Any + [green]$[/green] btcli proxy create --proxy-type Any 2. Create a delayed pure proxy account [green]$[/green] btcli proxy create --proxy-type Any --delay 1000 @@ -8455,11 +8455,11 @@ def proxy_add( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """ - Registers an existing account as a standard proxy for the delegator. + """ + Registers an existing account as a standard proxy for the delegator. - Grants an existing account permission to execute transactions on your behalf with - specified restrictions. + Grants an existing account permission to execute transactions on your behalf with + specified restrictions. [bold]Common Examples:[/bold] 1. Create a standard proxy account @@ -8536,7 +8536,7 @@ def proxy_remove( [bold]Common Examples:[/bold] - 1. Revoke proxy permissions from a single proxy account + 1. Revoke proxy permissions from a single proxy account [green]$[/green] btcli proxy remove --delegate 5GDel... --proxy-type Transfer 2. Remove all proxies linked to an account @@ -8613,9 +8613,9 @@ def proxy_kill( """ Permanently removes a pure proxy account. - Once killed, the pure proxy account is cleared from chain storage and cannot be recovered. + Once killed, the pure proxy account is cleared from chain storage and cannot be recovered. - [bold]⚠️ WARNING[/bold]: Killing a pure proxy permanently removes access to the account, and any funds remaining in it are lost. + [bold]⚠️ WARNING[/bold]: Killing a pure proxy permanently removes access to the account, and any funds remaining in it are lost. EXAMPLE From c066e03bf6c7268f3678653604574a6727267645 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 11 Nov 2025 15:19:05 +0200 Subject: [PATCH 037/179] Crowdloan --- bittensor_cli/cli.py | 48 ++++++++++++++----- .../src/commands/crowd/contribute.py | 16 ++++++- bittensor_cli/src/commands/crowd/create.py | 19 ++++++-- bittensor_cli/src/commands/crowd/dissolve.py | 5 ++ bittensor_cli/src/commands/crowd/refund.py | 5 ++ bittensor_cli/src/commands/crowd/update.py | 4 ++ .../src/commands/liquidity/liquidity.py | 37 ++++++++++---- 7 files changed, 107 insertions(+), 27 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c12f0d782..a390f6608 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6248,7 +6248,9 @@ def sudo_senate_vote( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - logger.debug(f"args:\nnetwork: {network}\nproposal: {proposal}\nvote: {vote}\n") + logger.debug( + f"args:\nnetwork: {network}\nproposal: {proposal}\nvote: {vote}\nproxy: {proxy}" + ) return self._run_command( sudo.senate_vote( wallet=wallet, @@ -6309,7 +6311,7 @@ def sudo_set_take( f"Take value must be between {min_value} and {max_value}. Provided value: {take}" ) raise typer.Exit() - logger.debug(f"args:\nnetwork: {network}\ntake: {take}") + logger.debug(f"args:\nnetwork: {network}\ntake: {take}\nproxy: {proxy}\n") result, ext_id = self._run_command( sudo.set_take( wallet=wallet, @@ -7519,6 +7521,7 @@ def liquidity_add( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, + proxy: Optional[str] = Options.proxy, liquidity_: Optional[float] = typer.Option( None, "--liquidity", @@ -7547,6 +7550,7 @@ def liquidity_add( ): """Add liquidity to the swap (as a combination of TAO + Alpha).""" self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if not netuid: netuid = Prompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -7591,6 +7595,7 @@ def liquidity_add( f"liquidity: {liquidity_}\n" f"price_low: {price_low}\n" f"price_high: {price_high}\n" + f"proxy: {proxy}\n" ) return self._run_command( liquidity.add_liquidity( @@ -7598,6 +7603,7 @@ def liquidity_add( wallet=wallet, hotkey_ss58=hotkey, netuid=netuid, + proxy=proxy, liquidity=liquidity_, price_low=price_low, price_high=price_high, @@ -7649,6 +7655,7 @@ def liquidity_remove( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, + proxy: Optional[str] = Options.proxy, position_id: Optional[int] = typer.Option( None, "--position-id", @@ -7669,7 +7676,7 @@ def liquidity_remove( """Remove liquidity from the swap (as a combination of TAO + Alpha).""" self.verbosity_handler(quiet, verbose, json_output, prompt) - + proxy = self.is_valid_proxy_name_or_ss58(proxy) if all_liquidity_ids and position_id: print_error("Cannot specify both --all and --position-id.") return @@ -7706,6 +7713,7 @@ def liquidity_remove( wallet=wallet, hotkey_ss58=hotkey, netuid=netuid, + proxy=proxy, position_id=position_id, prompt=prompt, all_liquidity_ids=all_liquidity_ids, @@ -7720,6 +7728,7 @@ def liquidity_modify( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, + proxy: Optional[str] = Options.proxy, position_id: Optional[int] = typer.Option( None, "--position-id", @@ -7739,6 +7748,7 @@ def liquidity_modify( ): """Modifies the liquidity position for the given subnet.""" self.verbosity_handler(quiet, verbose, json_output, prompt) + proxy = self.is_valid_proxy_name_or_ss58(proxy) if not netuid: netuid = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -7770,7 +7780,8 @@ def liquidity_modify( f"hotkey: {hotkey}\n" f"netuid: {netuid}\n" f"position_id: {position_id}\n" - f"liquidity_delta: {liquidity_delta}" + f"liquidity_delta: {liquidity_delta}\n" + f"proxy: {proxy}\n" ) return self._run_command( @@ -7779,6 +7790,7 @@ def liquidity_modify( wallet=wallet, hotkey_ss58=hotkey, netuid=netuid, + proxy=proxy, position_id=position_id, liquidity_delta=liquidity_delta, prompt=prompt, @@ -7880,6 +7892,7 @@ def crowd_create( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, deposit: Optional[float] = typer.Option( None, "--deposit", @@ -7956,7 +7969,7 @@ def crowd_create( [green]$[/green] btcli crowd create --subnet-lease --emissions-share 25 --lease-end-block 500000 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - + proxy = self.is_valid_proxy_name_or_ss58(proxy) wallet = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, @@ -7969,6 +7982,7 @@ def crowd_create( create_crowdloan.create_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, deposit_tao=deposit, min_contribution_tao=min_contribution, cap_tao=cap, @@ -8004,6 +8018,7 @@ def crowd_contribute( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8023,7 +8038,7 @@ def crowd_contribute( [green]$[/green] btcli crowd contribute --id 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - + proxy = self.is_valid_proxy_name_or_ss58(proxy) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8043,6 +8058,7 @@ def crowd_contribute( crowd_contribute.contribute_to_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, crowdloan_id=crowdloan_id, amount=amount, prompt=prompt, @@ -8065,6 +8081,7 @@ def crowd_withdraw( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8079,7 +8096,7 @@ def crowd_withdraw( Creators can only withdraw amounts above their initial deposit. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - + proxy = self.is_valid_proxy_name_or_ss58(proxy) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8099,6 +8116,7 @@ def crowd_withdraw( crowd_contribute.withdraw_from_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, crowdloan_id=crowdloan_id, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -8120,6 +8138,7 @@ def crowd_finalize( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8134,7 +8153,7 @@ def crowd_finalize( address (if specified) and execute any attached call (e.g., subnet creation). """ self.verbosity_handler(quiet, verbose, json_output, prompt) - + proxy = self.is_valid_proxy_name_or_ss58(proxy) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8155,6 +8174,7 @@ def crowd_finalize( subtensor=self.initialize_chain(network), wallet=wallet, crowdloan_id=crowdloan_id, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, @@ -8192,6 +8212,7 @@ def crowd_update( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8209,7 +8230,7 @@ def crowd_update( bounds, etc.). """ self.verbosity_handler(quiet, verbose, json_output, prompt) - + proxy = self.is_valid_proxy_name_or_ss58(proxy) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8234,6 +8255,7 @@ def crowd_update( crowd_update.update_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, crowdloan_id=crowdloan_id, min_contribution=min_contribution_balance, end=end, @@ -8254,6 +8276,7 @@ def crowd_refund( "--id", help="The ID of the crowdloan to refund", ), + proxy: Optional[str] = Options.proxy, network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, @@ -8274,7 +8297,7 @@ def crowd_refund( Contributors can call `btcli crowdloan withdraw` at will. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - + proxy = self.is_valid_proxy_name_or_ss58(proxy) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8294,6 +8317,7 @@ def crowd_refund( crowd_refund.refund_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, crowdloan_id=crowdloan_id, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, @@ -8315,6 +8339,7 @@ def crowd_dissolve( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + proxy: Optional[str] = Options.proxy, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8334,7 +8359,7 @@ def crowd_dissolve( you can run `btcli crowd refund` to refund the remaining contributors. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - + proxy = self.is_valid_proxy_name_or_ss58(proxy) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8354,6 +8379,7 @@ def crowd_dissolve( crowd_dissolve.dissolve_crowdloan( subtensor=self.initialize_chain(network), wallet=wallet, + proxy=proxy, crowdloan_id=crowdloan_id, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py index 480f6a7fa..13061f07c 100644 --- a/bittensor_cli/src/commands/crowd/contribute.py +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -53,6 +53,7 @@ def validate_for_contribution( async def contribute_to_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, amount: Optional[float], prompt: bool, @@ -65,11 +66,13 @@ async def contribute_to_crowdloan( Args: subtensor: SubtensorInterface object for chain interaction wallet: Wallet object containing coldkey for contribution + proxy: Optional proxy to use for this extrinsic crowdloan_id: ID of the crowdloan to contribute to amount: Amount to contribute in TAO (None to prompt) prompt: Whether to prompt for confirmation wait_for_inclusion: Wait for transaction inclusion wait_for_finalization: Wait for transaction finalization + json_output: Whether to output JSON output or human-readable text Returns: tuple[bool, str]: Success status and message @@ -159,7 +162,9 @@ async def contribute_to_crowdloan( "amount": contribution_amount.rao, }, ) - extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) updated_balance = user_balance - actual_contribution - extrinsic_fee table = Table( @@ -243,6 +248,7 @@ async def contribute_to_crowdloan( ) = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -345,6 +351,7 @@ async def contribute_to_crowdloan( async def withdraw_from_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, wait_for_inclusion: bool, wait_for_finalization: bool, @@ -360,10 +367,12 @@ async def withdraw_from_crowdloan( Args: subtensor: SubtensorInterface instance for blockchain interaction wallet: Wallet instance containing the user's keys + proxy: Optional proxy to use with this extrinsic submission. crowdloan_id: The ID of the crowdloan to withdraw from wait_for_inclusion: Whether to wait for transaction inclusion wait_for_finalization: Whether to wait for transaction finalization prompt: Whether to prompt for user confirmation + json_output: Whether to output the results as JSON or human-readable Returns: Tuple of (success, message) indicating the result @@ -429,7 +438,9 @@ async def withdraw_from_crowdloan( "crowdloan_id": crowdloan_id, }, ) - extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) await show_crowdloan_details( subtensor=subtensor, crowdloan_id=crowdloan_id, @@ -518,6 +529,7 @@ async def withdraw_from_crowdloan( ) = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 2c2625b2f..ee4e34dda 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -5,6 +5,7 @@ from bittensor_wallet import Wallet from rich.prompt import Confirm, IntPrompt, Prompt, FloatPrompt from rich.table import Table, Column, box +from scalecodec import GenericCall from bittensor_cli.src import COLORS from bittensor_cli.src.commands.crowd.view import show_crowdloan_details @@ -25,6 +26,7 @@ async def create_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], deposit_tao: Optional[int], min_contribution_tao: Optional[int], cap_tao: Optional[int], @@ -53,7 +55,7 @@ async def create_crowdloan( print_error(f"[red]{unlock_status.message}[/red]") return False, unlock_status.message - crowdloan_type = None + crowdloan_type: str if subnet_lease is not None: crowdloan_type = "subnet" if subnet_lease else "fundraising" elif prompt: @@ -125,7 +127,7 @@ async def create_crowdloan( else: print_error(f"[red]{error_msg}[/red]") return False, "Missing required options when prompts are disabled." - + duration = 0 deposit_value = deposit_tao while True: if deposit_value is None: @@ -210,8 +212,8 @@ async def create_crowdloan( break current_block = await subtensor.substrate.get_block_number(None) - call_to_attach = None - + call_to_attach: Optional[GenericCall] + lease_perpetual = None if crowdloan_type == "subnet": target_address = None @@ -352,6 +354,7 @@ async def create_crowdloan( success, error_message, extrinsic_receipt = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -432,6 +435,7 @@ async def create_crowdloan( async def finalize_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, wait_for_inclusion: bool, wait_for_finalization: bool, @@ -449,10 +453,12 @@ async def finalize_crowdloan( Args: subtensor: SubtensorInterface instance for blockchain interaction wallet: Wallet instance containing the user's keys + proxy: Optional proxy to use for this extrinsic submission crowdloan_id: The ID of the crowdloan to finalize wait_for_inclusion: Whether to wait for transaction inclusion wait_for_finalization: Whether to wait for transaction finalization prompt: Whether to prompt for user confirmation + json_output: Whether to output the crowdloan info as JSON or human-readable Returns: Tuple of (success, message) indicating the result @@ -512,7 +518,9 @@ async def finalize_crowdloan( "crowdloan_id": crowdloan_id, }, ) - extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) await show_crowdloan_details( subtensor=subtensor, @@ -601,6 +609,7 @@ async def finalize_crowdloan( wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + proxy=proxy, ) if not success: diff --git a/bittensor_cli/src/commands/crowd/dissolve.py b/bittensor_cli/src/commands/crowd/dissolve.py index b7513fb19..ce3f6c145 100644 --- a/bittensor_cli/src/commands/crowd/dissolve.py +++ b/bittensor_cli/src/commands/crowd/dissolve.py @@ -1,5 +1,6 @@ import asyncio import json +from typing import Optional from bittensor_wallet import Wallet from rich.prompt import Confirm @@ -21,6 +22,7 @@ async def dissolve_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -35,10 +37,12 @@ async def dissolve_crowdloan( Args: subtensor: SubtensorInterface object for chain interaction. wallet: Wallet object containing the creator's coldkey. + proxy: Optional proxy to use for this extrinsic submission crowdloan_id: ID of the crowdloan to dissolve. wait_for_inclusion: Wait for transaction inclusion. wait_for_finalization: Wait for transaction finalization. prompt: Whether to prompt for confirmation. + json_output: Whether to output the results as JSON or human-readable. Returns: tuple[bool, str]: Success status and message. @@ -172,6 +176,7 @@ async def dissolve_crowdloan( ) = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor_cli/src/commands/crowd/refund.py b/bittensor_cli/src/commands/crowd/refund.py index d08f91291..be264b3de 100644 --- a/bittensor_cli/src/commands/crowd/refund.py +++ b/bittensor_cli/src/commands/crowd/refund.py @@ -1,5 +1,6 @@ import asyncio import json +from typing import Optional from bittensor_wallet import Wallet from rich.prompt import Confirm @@ -22,6 +23,7 @@ async def refund_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -40,10 +42,12 @@ async def refund_crowdloan( Args: subtensor: SubtensorInterface object for chain interaction wallet: Wallet object containing coldkey (any wallet can call this) + proxy: Optional proxy to use for extrinsic submission crowdloan_id: ID of the crowdloan to refund wait_for_inclusion: Wait for transaction inclusion wait_for_finalization: Wait for transaction finalization prompt: Whether to prompt for confirmation + json_output: Whether to output as JSON or human-readable Returns: tuple[bool, str]: Success status and message @@ -183,6 +187,7 @@ async def refund_crowdloan( ) = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor_cli/src/commands/crowd/update.py b/bittensor_cli/src/commands/crowd/update.py index 2b2ee04f1..39ac69c64 100644 --- a/bittensor_cli/src/commands/crowd/update.py +++ b/bittensor_cli/src/commands/crowd/update.py @@ -24,6 +24,7 @@ async def update_crowdloan( subtensor: SubtensorInterface, wallet: Wallet, + proxy: Optional[str], crowdloan_id: int, min_contribution: Optional[Balance] = None, end: Optional[int] = None, @@ -38,6 +39,7 @@ async def update_crowdloan( Args: subtensor: SubtensorInterface object for chain interaction wallet: Wallet object containing coldkey (must be creator) + proxy: Optional proxy to use for this extrinsic submissions crowdloan_id: ID of the crowdloan to update min_contribution: New minimum contribution in TAO (None to prompt) end: New end block (None to prompt) @@ -45,6 +47,7 @@ async def update_crowdloan( wait_for_inclusion: Wait for transaction inclusion wait_for_finalization: Wait for transaction finalization prompt: Whether to prompt for values + json_output: Whether to output JSON or human-readable Returns: tuple[bool, str]: Success status and message @@ -368,6 +371,7 @@ async def update_crowdloan( ) = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index a262e8874..9059ae2e4 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -33,6 +33,7 @@ async def add_liquidity_extrinsic( wallet: "Wallet", hotkey_ss58: str, netuid: int, + proxy: Optional[str], liquidity: Balance, price_low: Balance, price_high: Balance, @@ -47,6 +48,7 @@ async def add_liquidity_extrinsic( 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. + proxy: Optional proxy to use for this extrinsic submission. 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. @@ -54,9 +56,10 @@ async def add_liquidity_extrinsic( 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. + tuple: + bool: True if successful, False otherwise. + str: success message if successful, error message otherwise. + AsyncExtrinsicReceipt: extrinsic receipt if successful, None otherwise. Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. @@ -82,6 +85,7 @@ async def add_liquidity_extrinsic( return await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -92,6 +96,7 @@ async def modify_liquidity_extrinsic( wallet: "Wallet", hotkey_ss58: str, netuid: int, + proxy: Optional[str], position_id: int, liquidity_delta: Balance, wait_for_inclusion: bool = True, @@ -104,15 +109,17 @@ async def modify_liquidity_extrinsic( 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. + proxy: Optional proxy to use for this extrinsic submission. 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. + tuple: + bool: True if successful, False otherwise. + str: success message if successful, error message otherwise. + AsyncExtrinsicReceipt: extrinsic receipt if successful, None otherwise. Note: Modifying is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. @@ -134,6 +141,7 @@ async def modify_liquidity_extrinsic( return await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -143,6 +151,7 @@ async def remove_liquidity_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", hotkey_ss58: str, + proxy: Optional[str], netuid: int, position_id: int, wait_for_inclusion: bool = True, @@ -154,15 +163,17 @@ async def remove_liquidity_extrinsic( 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. + proxy: Optional proxy to use for this extrinsic submission. 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. + tuple: + bool: True if successful, False otherwise. + str: success message if successful, error message otherwise. + AsyncExtrinsicReceipt: extrinsic receipt if successful, None otherwise. Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. @@ -183,6 +194,7 @@ async def remove_liquidity_extrinsic( return await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, + proxy=proxy, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -234,6 +246,7 @@ async def add_liquidity( wallet: "Wallet", hotkey_ss58: str, netuid: Optional[int], + proxy: Optional[str], liquidity: Balance, price_low: Balance, price_high: Balance, @@ -267,6 +280,7 @@ async def add_liquidity( wallet=wallet, hotkey_ss58=hotkey_ss58, netuid=netuid, + proxy=proxy, liquidity=liquidity, price_low=price_low, price_high=price_high, @@ -545,6 +559,7 @@ async def remove_liquidity( wallet: "Wallet", hotkey_ss58: str, netuid: int, + proxy: Optional[str], position_id: Optional[int] = None, prompt: Optional[bool] = None, all_liquidity_ids: Optional[bool] = None, @@ -579,12 +594,14 @@ async def remove_liquidity( if not Confirm.ask("Would you like to continue?"): return None + # TODO does this never break because of the nonce? results = await asyncio.gather( *[ remove_liquidity_extrinsic( subtensor=subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, + proxy=proxy, netuid=netuid, position_id=pos_id, ) @@ -615,6 +632,7 @@ async def modify_liquidity( wallet: "Wallet", hotkey_ss58: str, netuid: int, + proxy: Optional[str], position_id: int, liquidity_delta: Balance, prompt: Optional[bool] = None, @@ -646,6 +664,7 @@ async def modify_liquidity( wallet=wallet, hotkey_ss58=hotkey_ss58, netuid=netuid, + proxy=proxy, position_id=position_id, liquidity_delta=liquidity_delta, ) From 17e1342e5639ed912514dad4fcf794c6686fe352 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 11 Nov 2025 16:44:53 +0200 Subject: [PATCH 038/179] Get extrinsic fee --- bittensor_cli/src/commands/crowd/create.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index ee4e34dda..80495a362 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -291,7 +291,9 @@ async def create_crowdloan( }, ) - extrinsic_fee = await subtensor.get_extrinsic_fee(call, wallet.coldkeypub) + extrinsic_fee = await subtensor.get_extrinsic_fee( + call, wallet.coldkeypub, proxy=proxy + ) if prompt: duration_text = blocks_to_duration(duration) From 9f5b9673d7e5e55aa3060624f74c68ec6db20a0f Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 11 Nov 2025 18:20:37 +0200 Subject: [PATCH 039/179] Balance calculations. --- bittensor_cli/cli.py | 2 +- .../src/bittensor/extrinsics/transfer.py | 4 +- .../src/commands/crowd/contribute.py | 13 +- bittensor_cli/src/commands/crowd/create.py | 4 +- bittensor_cli/src/commands/proxy.py | 2 +- bittensor_cli/src/commands/stake/add.py | 19 +- bittensor_cli/src/commands/stake/remove.py | 190 +++++++++--------- bittensor_cli/src/commands/subnets/subnets.py | 24 ++- 8 files changed, 132 insertions(+), 126 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a390f6608..d074ab5b9 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1883,7 +1883,7 @@ def config_add_proxy( """ Adds a new pure proxy to the address book. """ - self.proxies[name] = {"proxy_type": proxy_type.value, "address": address} + self.proxies[name] = {"proxy_type": proxy_type, "address": address} with open(self.proxies_path, "w+") as f: safe_dump(self.proxies, f) self.config_get_proxies() diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 58eadb92a..d45933210 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -131,7 +131,7 @@ async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt] block_hash = await subtensor.substrate.get_chain_head() account_balance, existential_deposit = await asyncio.gather( subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash + proxy or wallet.coldkeypub.ss58_address, block_hash=block_hash ), subtensor.get_existential_deposit(block_hash=block_hash), ) @@ -200,7 +200,7 @@ async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt] if success: with console.status(":satellite: Checking Balance...", spinner="aesthetic"): new_balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address, reuse_block=False + proxy or wallet.coldkeypub.ss58_address, reuse_block=False ) console.print( f"Balance:\n" diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py index 13061f07c..4aef6489b 100644 --- a/bittensor_cli/src/commands/crowd/contribute.py +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -99,7 +99,7 @@ async def contribute_to_crowdloan( print_error(f"[red]{error_message}[/red]") return False, error_message - contributor_address = wallet.coldkeypub.ss58_address + contributor_address = proxy or wallet.coldkeypub.ss58_address current_contribution, user_balance, _ = await asyncio.gather( subtensor.get_crowdloan_contribution(crowdloan_id, contributor_address), subtensor.get_balance(contributor_address), @@ -399,11 +399,12 @@ async def withdraw_from_crowdloan( print_error(f"[red]{error_msg}[/red]") return False, "Cannot withdraw from finalized crowdloan." + contributor_address = proxy or wallet.coldkeypub.ss58_address user_contribution, user_balance = await asyncio.gather( subtensor.get_crowdloan_contribution( - crowdloan_id, wallet.coldkeypub.ss58_address + crowdloan_id, ), - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance(contributor_address), ) if user_contribution == Balance.from_tao(0): @@ -551,10 +552,8 @@ async def withdraw_from_crowdloan( return False, error_message or "Failed to withdraw from crowdloan." new_balance, updated_contribution, updated_crowdloan = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), - subtensor.get_crowdloan_contribution( - crowdloan_id, wallet.coldkeypub.ss58_address - ), + subtensor.get_balance(contributor_address), + subtensor.get_crowdloan_contribution(crowdloan_id, contributor_address), subtensor.get_single_crowdloan(crowdloan_id), ) diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index 80495a362..e43420113 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -268,7 +268,9 @@ async def create_crowdloan( call_to_attach = None - creator_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + creator_balance = await subtensor.get_balance( + proxy or wallet.coldkeypub.ss58_address + ) if deposit > creator_balance: print_error( f"[red]Insufficient balance to cover the deposit. " diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 04f53e426..6d8d99433 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -169,7 +169,7 @@ async def create_proxy( ) else: if Confirm.ask("Would you like to add this to your address book?"): - proxy_name = Prompt.ask("Name this proxy:") + proxy_name = Prompt.ask("Name this proxy") return True, proxy_name, created_pure, created_proxy_type if json_output: diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 1b5d93bdb..5d7811806 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -122,8 +122,8 @@ async def safe_stake_extrinsic( f":cross_mark: [red]Failed[/red] to stake {amount_} on Netuid {netuid_}" ) current_balance, next_nonce, call = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), - subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.get_balance(coldkey_ss58), + subtensor.substrate.get_account_next_index(coldkey_ss58), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake_limit", @@ -162,10 +162,10 @@ async def safe_stake_extrinsic( await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), + subtensor.get_balance(coldkey_ss58, block_hash), subtensor.get_stake( hotkey_ss58=hotkey_ss58_, - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, netuid=netuid_, block_hash=block_hash, ), @@ -205,10 +205,8 @@ async def stake_extrinsic( err_out = partial(print_error, status=status) block_hash = await subtensor.substrate.get_chain_head() current_balance, next_nonce, call = await asyncio.gather( - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash - ), - subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.get_balance(coldkey_ss58, block_hash=block_hash), + subtensor.substrate.get_account_next_index(coldkey_ss58), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="add_stake", @@ -272,6 +270,7 @@ async def stake_extrinsic( netuids = ( netuids if netuids is not None else await subtensor.get_all_subnet_netuids() ) + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address hotkeys_to_stake_to = _get_hotkeys_to_stake_to( wallet=wallet, @@ -285,10 +284,10 @@ async def stake_extrinsic( _all_subnets, _stake_info, current_wallet_balance = await asyncio.gather( subtensor.all_subnets(block_hash=chain_head), subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, block_hash=chain_head, ), - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=chain_head), + subtensor.get_balance(coldkey_ss58, block_hash=chain_head), ) all_subnets = {di.netuid: di for di in _all_subnets} diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index eb602bd1a..c0a97e84d 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -51,7 +51,7 @@ async def unstake( proxy: Optional[str], ): """Unstake from hotkey(s).""" - + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address with console.status( f"Retrieving subnet data & identities from {subtensor.network}...", spinner="earth", @@ -66,9 +66,7 @@ async def unstake( subtensor.all_subnets(block_hash=chain_head), subtensor.fetch_coldkey_hotkey_identities(block_hash=chain_head), subtensor.get_delegate_identities(block_hash=chain_head), - subtensor.get_stake_for_coldkey( - wallet.coldkeypub.ss58_address, block_hash=chain_head - ), + subtensor.get_stake_for_coldkey(coldkey_ss58, block_hash=chain_head), ) all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} @@ -300,7 +298,7 @@ async def unstake( table = _create_unstake_table( wallet_name=wallet.name, - wallet_coldkey_ss58=wallet.coldkeypub.ss58_address, + wallet_coldkey_ss58=coldkey_ss58, network=subtensor.network, total_received_amount=total_received_amount, safe_staking=safe_staking, @@ -329,6 +327,7 @@ async def unstake( "hotkey_ss58": op["hotkey_ss58"], "status": status, "era": era, + "proxy": proxy, } if safe_staking and op["netuid"] != 0: @@ -378,6 +377,7 @@ async def unstake_all( """Unstakes all stakes from all hotkeys in all subnets.""" include_hotkeys = include_hotkeys or [] exclude_hotkeys = exclude_hotkeys or [] + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address with console.status( f"Retrieving stake information & identities from {subtensor.network}...", spinner="earth", @@ -389,11 +389,11 @@ async def unstake_all( all_sn_dynamic_info_, current_wallet_balance, ) = await asyncio.gather( - subtensor.get_stake_for_coldkey(wallet.coldkeypub.ss58_address), + subtensor.get_stake_for_coldkey(coldkey_ss58), subtensor.fetch_coldkey_hotkey_identities(), subtensor.get_delegate_identities(), subtensor.all_subnets(), - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance(coldkey_ss58), ) if all_hotkeys: @@ -437,7 +437,7 @@ async def unstake_all( title=( f"\n[{COLOR_PALETTE.G.HEADER}]{table_title}[/{COLOR_PALETTE.G.HEADER}]\n" f"Wallet: [{COLOR_PALETTE.G.COLDKEY}]{wallet.name}[/{COLOR_PALETTE.G.COLDKEY}], " - f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n" + f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{coldkey_ss58}[/{COLOR_PALETTE.G.CK}]\n" f"Network: [{COLOR_PALETTE.G.HEADER}]{subtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n" ), show_footer=True, @@ -572,6 +572,7 @@ async def _unstake_extrinsic( hotkey_ss58: str, status=None, era: int = 3, + proxy: Optional[str] = None, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a standard unstake extrinsic. @@ -584,11 +585,14 @@ async def _unstake_extrinsic( subtensor: Subtensor interface status: Optional status for console updates era: blocks for which the transaction is valid + proxy: Optional proxy to use for this extrinsic submission + """ err_out = partial(print_error, status=status) failure_prelude = ( f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}" ) + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address if status: status.update( @@ -596,7 +600,7 @@ async def _unstake_extrinsic( ) current_balance, call = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance(coldkey_ss58), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake", @@ -607,28 +611,18 @@ async def _unstake_extrinsic( }, ), ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False - ) - if not await response.is_success: - err_out( - f"{failure_prelude} with error: " - f"{format_error_message(await response.error_message)}" - ) - return False, None - # Fetch latest balance and stake + success, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, wallet=wallet, era={"period": era}, proxy=proxy + ) + if success: await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), + subtensor.get_balance(coldkey_ss58, block_hash), subtensor.get_stake( hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, netuid=netuid, block_hash=block_hash, ), @@ -643,9 +637,11 @@ async def _unstake_extrinsic( f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_stake}" ) return True, response - - except Exception as e: - err_out(f"{failure_prelude} with error: {str(e)}") + else: + err_out( + f"{failure_prelude} with error: " + f"{format_error_message(await response.error_message)}" + ) return False, None @@ -659,6 +655,7 @@ async def _safe_unstake_extrinsic( allow_partial_stake: bool, status=None, era: int = 3, + proxy: Optional[str] = None, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a safe unstake extrinsic with price limit. @@ -671,11 +668,14 @@ async def _safe_unstake_extrinsic( subtensor: Subtensor interface allow_partial_stake: Whether to allow partial unstaking status: Optional status for console updates + proxy: Optional proxy to use for unstake extrinsic + """ err_out = partial(print_error, status=status) failure_prelude = ( f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}" ) + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address if status: status.update( @@ -685,11 +685,11 @@ async def _safe_unstake_extrinsic( block_hash = await subtensor.substrate.get_chain_head() current_balance, next_nonce, current_stake, call = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), - subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.get_balance(coldkey_ss58, block_hash), + subtensor.substrate.get_account_next_index(coldkey_ss58), subtensor.get_stake( hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, netuid=netuid, block_hash=block_hash, ), @@ -707,62 +707,57 @@ async def _safe_unstake_extrinsic( ), ) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + success, err_msg, response = await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + nonce=next_nonce, + era={"period": era}, + proxy=proxy, ) - - try: - response = await subtensor.substrate.submit_extrinsic( - extrinsic, wait_for_inclusion=True, wait_for_finalization=False + if success: + await print_extrinsic_id(response) + block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance(coldkey_ss58, block_hash), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=coldkey_ss58, + netuid=netuid, + block_hash=block_hash, + ), ) - except SubstrateRequestException as e: - if "Custom error: 8" in str(e): - print_error( - f"\n{failure_prelude}: Price exceeded tolerance limit. " - f"Transaction rejected because partial unstaking is disabled. " - f"Either increase price tolerance or enable partial unstaking.", - status=status, - ) - else: - err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") - return False, None - if not await response.is_success: - err_out( - f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}" ) - return False, None - await print_extrinsic_id(response) - block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), - subtensor.get_stake( - hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid, - block_hash=block_hash, - ), - ) - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}" - ) + amount_unstaked = current_stake - new_stake + if allow_partial_stake and (amount_unstaked != amount): + console.print( + "Partial unstake transaction. Unstaked:\n" + f" [{COLOR_PALETTE.S.AMOUNT}]{amount_unstaked.set_unit(netuid=netuid)}[/{COLOR_PALETTE.S.AMOUNT}] " + f"instead of " + f"[blue]{amount}[/blue]" + ) - amount_unstaked = current_stake - new_stake - if allow_partial_stake and (amount_unstaked != amount): console.print( - "Partial unstake transaction. Unstaked:\n" - f" [{COLOR_PALETTE.S.AMOUNT}]{amount_unstaked.set_unit(netuid=netuid)}[/{COLOR_PALETTE.S.AMOUNT}] " - f"instead of " - f"[blue]{amount}[/blue]" + f"Subnet: [{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] " + f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_stake}" ) - - console.print( - f"Subnet: [{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] " - f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_stake}" - ) - return True, response + return True, response + elif "Custom error: 8" in err_msg: + print_error( + f"\n{failure_prelude}: Price exceeded tolerance limit. " + f"Transaction rejected because partial unstaking is disabled. " + f"Either increase price tolerance or enable partial unstaking.", + status=status, + ) + else: + err_out( + f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" + ) + return False, None async def _unstake_all_extrinsic( @@ -789,6 +784,7 @@ async def _unstake_all_extrinsic( failure_prelude = ( f":cross_mark: [red]Failed[/red] to unstake all from {hotkey_name}" ) + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address if status: status.update( @@ -800,17 +796,15 @@ async def _unstake_all_extrinsic( previous_root_stake, current_balance = await asyncio.gather( subtensor.get_stake( hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, netuid=0, block_hash=block_hash, ), - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash - ), + subtensor.get_balance(coldkey_ss58, block_hash=block_hash), ) else: current_balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash + coldkey_ss58, block_hash=block_hash ) previous_root_stake = None @@ -840,22 +834,23 @@ async def _unstake_all_extrinsic( new_root_stake, new_balance = await asyncio.gather( subtensor.get_stake( hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, + coldkey_ss58=coldkey_ss58, netuid=0, block_hash=block_hash, ), - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash - ), + subtensor.get_balance(coldkey_ss58, block_hash=block_hash), ) else: new_balance = await subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=block_hash + coldkey_ss58, block_hash=block_hash ) new_root_stake = None msg_modifier = "Alpha " if unstake_all_alpha else "" - success_message = f":white_heavy_check_mark: [green]Included: Successfully unstaked all {msg_modifier}stakes[/green]" + success_message = ( + f":white_heavy_check_mark: [green]Included:" + f" Successfully unstaked all {msg_modifier}stakes[/green]" + ) console.print(f"{success_message} from {hotkey_name}") console.print( f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}" @@ -980,7 +975,7 @@ async def _unstake_selection( # Display existing hotkeys, id, and staked netuids. subnet_filter = f" for Subnet {netuid}" if netuid is not None else "" table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes{subnet_filter}\n", + title=f"\n[{COLOR_PALETTE.G.HEADER}]Hotkeys with Stakes{subnet_filter}\n", show_footer=True, show_edge=False, header_style="bold white", @@ -991,9 +986,9 @@ async def _unstake_selection( pad_edge=True, ) table.add_column("Index", justify="right") - table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) - table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"]) - table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) + table.add_column("Identity", style=COLOR_PALETTE.G.SUBHEAD) + table.add_column("Netuids", style=COLOR_PALETTE.G.NETUID) + table.add_column("Hotkey Address", style=COLOR_PALETTE.G.HK) for hotkey_info in hotkeys_info: index = str(hotkey_info["index"]) @@ -1339,10 +1334,11 @@ def _print_table_and_slippage( if max_float_slippage > 5: console.print( "\n" - f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_float_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]," + f"[{COLOR_PALETTE.S.SLIPPAGE_TEXT}]{'-' * console.width}\n" + f"[bold]WARNING:[/bold] The slippage on one of your operations is high: " + f"[{COLOR_PALETTE.S.SLIPPAGE_PERCENT}]{max_float_slippage} %[/{COLOR_PALETTE.S.SLIPPAGE_PERCENT}]," " this may result in a loss of funds.\n" - f"-------------------------------------------------------------------------------------------------------------------\n" + f"{'-' * console.width}\n" ) base_description = """ [bold white]Description[/bold white]: diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index ec462c114..5cec49992 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -3,6 +3,7 @@ import sqlite3 from typing import TYPE_CHECKING, Optional, cast +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet from rich.prompt import Confirm, Prompt from rich.console import Group @@ -78,7 +79,7 @@ async def register_subnetwork_extrinsic( # TODO why doesn't this have an era? async def _find_event_attributes_in_extrinsic_receipt( - response_, event_name: str + response_: AsyncExtrinsicReceipt, event_name: str ) -> list: """ Searches for the attributes of a specified event within an extrinsic receipt. @@ -95,10 +96,10 @@ async def _find_event_attributes_in_extrinsic_receipt( if event_details["event_id"] == event_name: # Once found, you can access the attributes of the event_name return event_details["attributes"] - return [-1] + return [] print_verbose("Fetching balance") - your_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + your_balance = await subtensor.get_balance(proxy or wallet.coldkeypub.ss58_address) print_verbose("Fetching burn_cost") sn_burn_cost = await burn_cost(subtensor) @@ -199,9 +200,16 @@ async def _find_event_attributes_in_extrinsic_receipt( ) await print_extrinsic_id(response) ext_id = await response.get_extrinsic_identifier() - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" - ) + if not attributes: + console.print( + ":exclamation: [yellow]A possible error has occurred[/yellow]. The extrinsic reports success, but " + "we are unable to locate the 'NetworkAdded' event inside the extrinsic's events." + "" + ) + else: + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + ) return True, int(attributes[0]), ext_id @@ -1746,7 +1754,9 @@ async def register( subtensor.get_hyperparameter( param_name="Burn", netuid=netuid, block_hash=block_hash ), - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), + subtensor.get_balance( + proxy or wallet.coldkeypub.ss58_address, block_hash=block_hash + ), ) current_recycle = ( Balance.from_rao(int(current_recycle_)) if current_recycle_ else Balance(0) From 4c7c2d936fe5ca0cbd6df744de5fb6c1f75dff44 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 11 Nov 2025 18:23:45 +0200 Subject: [PATCH 040/179] More balances --- bittensor_cli/src/commands/subnets/subnets.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 5cec49992..ba58dde9a 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1631,7 +1631,7 @@ async def create( prompt: bool, ): """Register a subnetwork""" - + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address # Call register command. success, netuid, ext_id = await register_subnetwork_extrinsic( subtensor=subtensor, @@ -1657,7 +1657,7 @@ async def create( if do_set_identity: current_identity = await get_id( - subtensor, wallet.coldkeypub.ss58_address, "Current on-chain identity" + subtensor, coldkey_ss58, "Current on-chain identity" ) if prompt: if not Confirm.ask( @@ -1732,7 +1732,7 @@ async def register( proxy: Optional[str] = None, ): """Register neuron by recycling some TAO.""" - + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address # Verify subnet exists print_verbose("Checking subnet status") block_hash = await subtensor.substrate.get_chain_head() @@ -1754,9 +1754,7 @@ async def register( subtensor.get_hyperparameter( param_name="Burn", netuid=netuid, block_hash=block_hash ), - subtensor.get_balance( - proxy or wallet.coldkeypub.ss58_address, block_hash=block_hash - ), + subtensor.get_balance(coldkey_ss58, block_hash=block_hash), ) current_recycle = ( Balance.from_rao(int(current_recycle_)) if current_recycle_ else Balance(0) @@ -1819,7 +1817,7 @@ async def register( f"{Balance.get_unit(netuid)}", f"τ {current_recycle.tao:.4f}", f"{get_hotkey_pub_ss58(wallet)}", - f"{wallet.coldkeypub.ss58_address}", + f"{coldkey_ss58}", ) console.print(table) if not ( @@ -2583,7 +2581,7 @@ async def start_subnet( prompt: bool = False, ) -> bool: """Start a subnet's emission schedule""" - + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address if not await subtensor.subnet_exists(netuid): print_error(f"Subnet {netuid} does not exist.") return False @@ -2594,7 +2592,7 @@ async def start_subnet( params=[netuid], ) # TODO should this check against proxy as well? - if subnet_owner != wallet.coldkeypub.ss58_address: + if subnet_owner != coldkey_ss58: print_error(":cross_mark: This wallet doesn't own the specified subnet.") return False From c853d89ef42c6290072679725f6835451fe6aee7 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 11 Nov 2025 18:29:16 +0200 Subject: [PATCH 041/179] fmt --- bittensor_cli/src/commands/subnets/subnets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index ba58dde9a..18f15bf95 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1774,8 +1774,11 @@ async def register( # TODO make this a reusable function, also used in subnets list # Show creation table. table = Table( - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Register to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]netuid: {netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", + title=( + f"\n[{COLOR_PALETTE.G.HEADER}]" + f"Register to [{COLOR_PALETTE.G.SUBHEAD}]netuid: {netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" + f"\nNetwork: [{COLOR_PALETTE.G.SUBHEAD}]{subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n" + ), show_footer=True, show_edge=False, header_style="bold white", From ff4f64a29188470ff28b308a3a4716ecddf6aa0e Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 11 Nov 2025 19:17:22 +0200 Subject: [PATCH 042/179] Handle `w transfer` warning for when the reported owner is the genesis address --- bittensor_cli/src/bittensor/extrinsics/transfer.py | 4 ++-- bittensor_cli/src/bittensor/subtensor_interface.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index d45933210..e0032ac2e 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -7,7 +7,7 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface, GENESIS_ADDRESS from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -163,7 +163,7 @@ async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt] # Ask before moving on. if prompt: hk_owner = await subtensor.get_hotkey_owner(destination, check_exists=False) - if hk_owner and hk_owner != destination: + if hk_owner and hk_owner != destination and hk_owner != GENESIS_ADDRESS: if not Confirm.ask( f"The destination appears to be a hotkey, owned by [bright_magenta]{hk_owner}[/bright_magenta]. " f"Only proceed if you are absolutely sure that [bright_magenta]{destination}[/bright_magenta] is the " diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e399220cc..cd8b5399b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -45,6 +45,8 @@ get_hotkey_pub_ss58, ) +GENESIS_ADDRESS = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + class ParamWithTypes(TypedDict): name: str # Name of the parameter. @@ -1116,7 +1118,7 @@ async def does_hotkey_exist( block_hash=block_hash, reuse_block_hash=reuse_block, ) - return_val = result != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" + return_val = result != GENESIS_ADDRESS return return_val async def get_hotkey_owner( From 9d20db92ad66326379a8ef819d10844021fa00e9 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 11 Nov 2025 20:13:23 +0200 Subject: [PATCH 043/179] Genesis --- bittensor_cli/src/commands/wallets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index ce40d15f1..a10e1f55a 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -30,7 +30,10 @@ ) from bittensor_cli.src.bittensor.extrinsics.transfer import transfer_extrinsic from bittensor_cli.src.bittensor.networking import int_to_ip -from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.subtensor_interface import ( + SubtensorInterface, + GENESIS_ADDRESS, +) from bittensor_cli.src.bittensor.utils import ( RAO_PER_TAO, console, @@ -2269,10 +2272,7 @@ async def check_swap_status( chain_reported_completion_block, destination_address = await subtensor.query( "SubtensorModule", "ColdkeySwapScheduled", [origin_ss58] ) - if ( - chain_reported_completion_block != 0 - and destination_address != "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" - ): + if chain_reported_completion_block != 0 and destination_address != GENESIS_ADDRESS: is_pending = True else: is_pending = False From bda7b3d62376fc58e64d8f17b4b12ff9329e87f3 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 11 Nov 2025 20:13:51 +0200 Subject: [PATCH 044/179] Better handle real account pays extrinsics fee for proxy transactions. --- bittensor_cli/cli.py | 6 ++--- .../src/bittensor/extrinsics/transfer.py | 24 +++++++------------ .../src/bittensor/subtensor_interface.py | 15 +++++++++++- .../src/commands/crowd/contribute.py | 10 ++++++-- bittensor_cli/src/commands/crowd/create.py | 10 ++++++-- bittensor_cli/src/commands/proxy.py | 6 ++--- bittensor_cli/src/commands/stake/add.py | 5 +++- bittensor_cli/src/commands/stake/claim.py | 5 +++- bittensor_cli/src/commands/stake/move.py | 10 +++++--- bittensor_cli/src/commands/stake/remove.py | 8 +++++-- bittensor_cli/src/commands/subnets/subnets.py | 4 ++-- 11 files changed, 68 insertions(+), 35 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d074ab5b9..8c0909e01 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1883,7 +1883,7 @@ def config_add_proxy( """ Adds a new pure proxy to the address book. """ - self.proxies[name] = {"proxy_type": proxy_type, "address": address} + self.proxies[name] = {"proxy_type": proxy_type.value, "address": address} with open(self.proxies_path, "w+") as f: safe_dump(self.proxies, f) self.config_get_proxies() @@ -6726,7 +6726,7 @@ def subnets_create( WO.HOTKEY, WO.PATH, ], - validate=WV.WALLET_AND_HOTKEY, + validate=WV.WALLET, ) identity = prompt_for_subnet_identity( current_identity={}, @@ -8515,7 +8515,7 @@ def proxy_add( validate=WV.WALLET, ) return self._run_command( - proxy_commands.remove_proxy( + proxy_commands.add_proxy( subtensor=self.initialize_chain(network), wallet=wallet, delegate=delegate, diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index e0032ac2e..e787bb012 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -7,7 +7,10 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface, GENESIS_ADDRESS +from bittensor_cli.src.bittensor.subtensor_interface import ( + SubtensorInterface, + GENESIS_ADDRESS, +) from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -63,20 +66,11 @@ async def get_transfer_fee() -> Balance: call_function=call_function, call_params=call_params, ) - - try: - payment_info = await subtensor.substrate.get_payment_info( - call=call, keypair=wallet.coldkeypub - ) - except SubstrateRequestException as e: - payment_info = {"partial_fee": int(2e7)} # assume 0.02 Tao - err_console.print( - f":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n" - f" {format_error_message(e)}[/bold white]\n" - f" Defaulting to default transfer fee: {payment_info['partialFee']}" - ) - - return Balance.from_rao(payment_info["partial_fee"]) + return await subtensor.get_extrinsic_fee( + call=call, + keypair=wallet.coldkeypub, + proxy=proxy + ) async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt]]: """ diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index cd8b5399b..711e6c4ec 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1200,7 +1200,20 @@ async def sign_and_send_extrinsic( else: return False, format_error_message(await response.error_message), None except SubstrateRequestException as e: - return False, format_error_message(e), None + err_msg = format_error_message(e) + if proxy and "Invalid Transaction" in err_msg: + extrinsic_fee, real_balance = await asyncio.gather( + self.get_extrinsic_fee( + call, keypair=wallet.coldkeypub, proxy=proxy + ), + self.get_balance(wallet.coldkeypub.ss58_address), + ) + if extrinsic_fee > real_balance: + err_msg += ( + "\nAs this is a proxy transaction, the real account needs to pay the extrinsic fee. However, " + f"the balance of the real account is {real_balance}, and the extrinsic fee is {extrinsic_fee}." + ) + return False, err_msg, None async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: """ diff --git a/bittensor_cli/src/commands/crowd/contribute.py b/bittensor_cli/src/commands/crowd/contribute.py index 4aef6489b..fef4d7f65 100644 --- a/bittensor_cli/src/commands/crowd/contribute.py +++ b/bittensor_cli/src/commands/crowd/contribute.py @@ -165,7 +165,10 @@ async def contribute_to_crowdloan( extrinsic_fee = await subtensor.get_extrinsic_fee( call, wallet.coldkeypub, proxy=proxy ) - updated_balance = user_balance - actual_contribution - extrinsic_fee + if proxy: + updated_balance = user_balance - actual_contribution + else: + updated_balance = user_balance - actual_contribution - extrinsic_fee table = Table( Column("[bold white]Field", style=COLORS.G.SUBHEAD), @@ -452,7 +455,10 @@ async def withdraw_from_crowdloan( ) if prompt: - new_balance = user_balance + withdrawable - extrinsic_fee + if proxy: + new_balance = user_balance + withdrawable + else: + new_balance = user_balance + withdrawable - extrinsic_fee new_raised = crowdloan.raised - withdrawable table = Table( Column("[bold white]Field", style=COLORS.G.SUBHEAD), diff --git a/bittensor_cli/src/commands/crowd/create.py b/bittensor_cli/src/commands/crowd/create.py index e43420113..cb1ab4558 100644 --- a/bittensor_cli/src/commands/crowd/create.py +++ b/bittensor_cli/src/commands/crowd/create.py @@ -340,7 +340,9 @@ async def create_crowdloan( table.add_row("Duration", f"[bold]{duration}[/bold] blocks (~{duration_text})") table.add_row("Ends at block", f"[bold]{end_block}[/bold]") table.add_row( - "Estimated fee", f"[{COLORS.P.TAO}]{extrinsic_fee}[/{COLORS.P.TAO}]" + "Estimated fee", + f"[{COLORS.P.TAO}]{extrinsic_fee}[/{COLORS.P.TAO}]" + + (" (paid by real account)" if proxy else ""), ) console.print(table) @@ -570,7 +572,11 @@ async def finalize_crowdloan( else: table.add_row("Call to Execute", "[dim]None[/dim]") - table.add_row("Transaction Fee", str(extrinsic_fee)) + table.add_row( + "Transaction Fee", + f"[{COLORS.S.TAO}]{extrinsic_fee.tao}[/{COLORS.S.TAO}]" + + (" (paid by real account)" if proxy else ""), + ) table.add_section() table.add_row( diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 6d8d99433..939cf66e8 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -155,16 +155,16 @@ async def create_proxy( attrs = event["attributes"] created_pure = attrs["pure"] created_spawner = attrs["who"] - created_proxy_type = attrs["proxy_type"] + created_proxy_type = getattr(ProxyType, attrs["proxy_type"]) arg_start = "`" if json_output else "[blue]" arg_end = "`" if json_output else "[/blue]" - msg = f"Created pure '{created_pure}' from spawner '{created_spawner}' with proxy type '{created_proxy_type}'." + msg = f"Created pure '{created_pure}' from spawner '{created_spawner}' with proxy type '{created_proxy_type.value}'." console.print(msg) if not prompt: console.print( f" You can add this to your config with {arg_start}" f"btcli config add-proxy " - f"--name --address {created_pure} --proxy-type {created_proxy_type}" + f"--name --address {created_pure} --proxy-type {created_proxy_type.value}" f"{arg_end}" ) else: diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 5d7811806..caaa78471 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -396,10 +396,13 @@ async def stake_extrinsic( ) row_extension = [] # TODO this should be asyncio gathered before the for loop + amount_minus_fee = ( + (amount_to_stake - extrinsic_fee) if not proxy else amount_to_stake + ) sim_swap = await subtensor.sim_swap( origin_netuid=0, destination_netuid=netuid, - amount=(amount_to_stake - extrinsic_fee).rao, + amount=amount_minus_fee.rao, ) received_amount = sim_swap.alpha_amount # Add rows for the table diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index a765d6f02..ac4cf94af 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -317,7 +317,10 @@ async def process_pending_claims( extrinsic_fee = await subtensor.get_extrinsic_fee( call, wallet.coldkeypub, proxy=proxy ) - console.print(f"\n[dim]Estimated extrinsic fee: {extrinsic_fee.tao:.9f} τ[/dim]") + console.print( + f"\n[dim]Estimated extrinsic fee: {extrinsic_fee.tao:.9f} τ" + + (" (paid by real account)" if proxy else "") + ) if prompt: if not Confirm.ask("Do you want to proceed?"): diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 42cb4d4af..2d6fdd0aa 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -36,14 +36,15 @@ async def display_stake_movement_cross_subnets( amount_to_move: Balance, stake_fee: Balance, extrinsic_fee: Balance, + proxy: Optional[str] = None, ) -> tuple[Balance, str]: """Calculate and display stake movement information""" if origin_netuid == destination_netuid: subnet = await subtensor.subnet(origin_netuid) - received_amount_tao = ( - subnet.alpha_to_tao(amount_to_move - stake_fee) - extrinsic_fee - ) + received_amount_tao = subnet.alpha_to_tao(amount_to_move - stake_fee) + if not proxy: + received_amount_tao -= extrinsic_fee received_amount = subnet.tao_to_alpha(received_amount_tao) if received_amount < Balance.from_tao(0).set_unit(destination_netuid): @@ -546,6 +547,7 @@ async def move_stake( if origin_netuid != 0 else sim_swap.tao_fee, extrinsic_fee=extrinsic_fee, + proxy=proxy, ) except ValueError: return False, "" @@ -739,6 +741,7 @@ async def transfer_stake( if origin_netuid != 0 else sim_swap.tao_fee, extrinsic_fee=extrinsic_fee, + proxy=proxy, ) except ValueError: return False, "" @@ -913,6 +916,7 @@ async def swap_stake( if origin_netuid != 0 else sim_swap.tao_fee, extrinsic_fee=extrinsic_fee, + proxy=proxy, ) except ValueError: return False, "" diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index c0a97e84d..c253d50e0 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -237,7 +237,9 @@ async def unstake( sim_swap = await subtensor.sim_swap( netuid, 0, amount_to_unstake_as_balance.rao ) - received_amount = sim_swap.tao_amount - extrinsic_fee + received_amount = sim_swap.tao_amount + if not proxy: + received_amount -= extrinsic_fee except ValueError: continue total_received_amount += received_amount @@ -507,7 +509,9 @@ async def unstake_all( proxy=proxy, ) sim_swap = await subtensor.sim_swap(stake.netuid, 0, stake_amount.rao) - received_amount = sim_swap.tao_amount - extrinsic_fee + received_amount = sim_swap.tao_amount + if not proxy: + received_amount -= extrinsic_fee if received_amount < Balance.from_tao(0): print_error("Not enough Alpha to pay the transaction fee.") diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 18f15bf95..ba00452fd 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -11,7 +11,7 @@ from rich.table import Column, Table from rich import box -from bittensor_cli.src import COLOR_PALETTE, Constants +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.registration import ( register_extrinsic, @@ -27,7 +27,6 @@ err_console, print_verbose, print_error, - format_error_message, get_metadata_table, millify_tao, render_table, @@ -99,6 +98,7 @@ async def _find_event_attributes_in_extrinsic_receipt( return [] print_verbose("Fetching balance") + your_balance = await subtensor.get_balance(proxy or wallet.coldkeypub.ss58_address) print_verbose("Fetching burn_cost") From c9409631288f7b221798faf56f1b45dead6abff9 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 11 Nov 2025 20:20:00 +0200 Subject: [PATCH 045/179] Ruff --- bittensor_cli/src/bittensor/extrinsics/transfer.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index e787bb012..ef61ca854 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -67,9 +67,7 @@ async def get_transfer_fee() -> Balance: call_params=call_params, ) return await subtensor.get_extrinsic_fee( - call=call, - keypair=wallet.coldkeypub, - proxy=proxy + call=call, keypair=wallet.coldkeypub, proxy=proxy ) async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt]]: From dd91d0e2912222326c96cc4d3b417a1d28d5c3ca Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 11 Nov 2025 20:52:26 +0200 Subject: [PATCH 046/179] Wording --- bittensor_cli/src/bittensor/subtensor_interface.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 711e6c4ec..7b4f5c7f8 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1202,16 +1202,17 @@ async def sign_and_send_extrinsic( except SubstrateRequestException as e: err_msg = format_error_message(e) if proxy and "Invalid Transaction" in err_msg: - extrinsic_fee, real_balance = await asyncio.gather( + extrinsic_fee, signer_balance = await asyncio.gather( self.get_extrinsic_fee( call, keypair=wallet.coldkeypub, proxy=proxy ), self.get_balance(wallet.coldkeypub.ss58_address), ) - if extrinsic_fee > real_balance: + if extrinsic_fee > signer_balance: err_msg += ( - "\nAs this is a proxy transaction, the real account needs to pay the extrinsic fee. However, " - f"the balance of the real account is {real_balance}, and the extrinsic fee is {extrinsic_fee}." + "\nAs this is a proxy transaction, the signing account needs to pay the extrinsic fee. " + f"However, the balance of the signing account is {signer_balance}, and the extrinsic fee is " + f"{extrinsic_fee}." ) return False, err_msg, None From e675ee22f2b96ca9877b4e7c8ff93c497c433c99 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 15:02:07 +0200 Subject: [PATCH 047/179] Handle pre-3.11 StrEnum --- bittensor_cli/src/commands/proxy.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 939cf66e8..4752a4e75 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -1,5 +1,5 @@ -from enum import Enum from typing import TYPE_CHECKING, Optional +import sys from rich.prompt import Confirm, Prompt from scalecodec import GenericCall @@ -17,7 +17,16 @@ from bittensor_wallet.bittensor_wallet import Wallet -class ProxyType(str, Enum): +# TODO when 3.10 support is dropped in Oct 2026, remove this +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from enum import Enum + class StrEnum(str, Enum): + pass + + +class ProxyType(StrEnum): Any = "Any" Owner = "Owner" NonCritical = "NonCritical" From 3dbdcaf617e0bd73ce237e1f9b1eebf1211541a1 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 20:30:46 +0200 Subject: [PATCH 048/179] Added TODOs --- bittensor_cli/cli.py | 2 ++ bittensor_cli/src/commands/proxy.py | 1 + 2 files changed, 3 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8c0909e01..4d446265f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1883,6 +1883,8 @@ def config_add_proxy( """ Adds a new pure proxy to the address book. """ + # TODO check if name already exists, Confirmation + # TODO add spawner self.proxies[name] = {"proxy_type": proxy_type.value, "address": address} with open(self.proxies_path, "w+") as f: safe_dump(self.proxies, f) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 4752a4e75..495db238d 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -266,6 +266,7 @@ async def add_proxy( period: int, json_output: bool, ): + # TODO add to address book if prompt: if not Confirm.ask( f"This will add a proxy of type {proxy_type.value} for delegate {delegate}." From 3cb100b908cd4a2d53c0587861a5d1cd120e658f Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 21:02:10 +0200 Subject: [PATCH 049/179] Add TODO --- bittensor_cli/cli.py | 21 +++++++++++++++++++++ bittensor_cli/src/commands/proxy.py | 1 + 2 files changed, 22 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4d446265f..5108763cf 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2225,6 +2225,8 @@ def wallet_ask( else: return wallet + # Wallet + def wallet_list( self, wallet_name: Optional[str] = Options.wallet_name, @@ -3921,6 +3923,8 @@ def wallet_swap_coldkey( ) ) + # Stake + def get_auto_stake( self, network: Optional[list[str]] = Options.network, @@ -5759,6 +5763,8 @@ def stake_childkey_take( json_console.print(json.dumps(output)) return results + # Mechanism + def mechanism_count_set( self, network: Optional[list[str]] = Options.network, @@ -5996,6 +6002,8 @@ def mechanism_emission_get( ) ) + # Sudo + def sudo_set( self, network: Optional[list[str]] = Options.network, @@ -6415,6 +6423,8 @@ def sudo_trim( ) ) + # Subnets + def subnets_list( self, network: Optional[list[str]] = Options.network, @@ -7257,6 +7267,8 @@ def subnets_set_symbol( ) ) + # Weights + def weights_reveal( self, network: Optional[list[str]] = Options.network, @@ -7456,6 +7468,8 @@ def weights_commit( ) ) + # View + def view_dashboard( self, network: Optional[list[str]] = Options.network, @@ -7516,6 +7530,8 @@ def view_dashboard( ) ) + # Liquidity + def liquidity_add( self, network: Optional[list[str]] = Options.network, @@ -7800,6 +7816,8 @@ def liquidity_modify( ) ) + # Crowd + def crowd_list( self, network: Optional[list[str]] = Options.network, @@ -8390,6 +8408,9 @@ def crowd_dissolve( ) ) + # Proxy + # TODO check announcements: how do they work? + def proxy_create( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 495db238d..78c57c172 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -22,6 +22,7 @@ from enum import StrEnum else: from enum import Enum + class StrEnum(str, Enum): pass From 54054a1a3075fd739137b5d8edc932c808cd0b94 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 12 Nov 2025 21:17:17 +0200 Subject: [PATCH 050/179] Add TODO --- bittensor_cli/src/commands/proxy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 78c57c172..d1467262c 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -48,6 +48,8 @@ class ProxyType(StrEnum): RootClaim = "RootClaim" +# TODO add announce with also --reject and --remove + async def submit_proxy( subtensor: "SubtensorInterface", wallet: "Wallet", From a12da83ab6f4e35d408c63fc018bfa67f5b0abcd Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 14 Nov 2025 14:12:10 +0200 Subject: [PATCH 051/179] Handle non-sn owner sudo set --- bittensor_cli/src/commands/subnets/subnets.py | 44 +++++++++---------- bittensor_cli/src/commands/sudo.py | 20 ++++++--- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index ba00452fd..cd31e5aca 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -185,32 +185,32 @@ async def _find_event_attributes_in_extrinsic_receipt( proxy=proxy, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None, None + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, None, None - if not success: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False, None, None + if not success: + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + return False, None, None - # Successful registration, final check for membership + # Successful registration, final check for membership + else: + attributes = await _find_event_attributes_in_extrinsic_receipt( + response, "NetworkAdded" + ) + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() + if not attributes: + console.print( + ":exclamation: [yellow]A possible error has occurred[/yellow]. The extrinsic reports success, but " + "we are unable to locate the 'NetworkAdded' event inside the extrinsic's events." + "" + ) else: - attributes = await _find_event_attributes_in_extrinsic_receipt( - response, "NetworkAdded" + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" ) - await print_extrinsic_id(response) - ext_id = await response.get_extrinsic_identifier() - if not attributes: - console.print( - ":exclamation: [yellow]A possible error has occurred[/yellow]. The extrinsic reports success, but " - "we are unable to locate the 'NetworkAdded' event inside the extrinsic's events." - "" - ) - else: - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" - ) - return True, int(attributes[0]), ext_id + return True, int(attributes[0]), ext_id # commands diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index f192bbd66..f0c0088e7 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -286,18 +286,12 @@ async def set_hyperparameter_extrinsic( extrinsic_identifier: optional extrinsic identifier if the extrinsic was included """ print_verbose("Confirming subnet owner") + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address subnet_owner = await subtensor.query( module="SubtensorModule", storage_function="SubnetOwner", params=[netuid], ) - # TODO does this need to check proxy? - if subnet_owner != wallet.coldkeypub.ss58_address: - err_msg = ( - ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" - ) - err_console.print(err_msg) - return False, err_msg, None if not (ulw := unlock_key(wallet)).success: return False, ulw.message, None @@ -381,8 +375,20 @@ async def set_hyperparameter_extrinsic( call_params={"call": call_}, ) else: + if subnet_owner != coldkey_ss58: + err_msg = ( + ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" + ) + err_console.print(err_msg) + return False, err_msg, None call = call_ else: + if subnet_owner != coldkey_ss58: + err_msg = ( + ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" + ) + err_console.print(err_msg) + return False, err_msg, None call = call_ with console.status( f":satellite: Setting hyperparameter [{COLOR_PALETTE.G.SUBHEAD}]{parameter}[/{COLOR_PALETTE.G.SUBHEAD}]" From 08165a37f6aa7e8aaea39140b861ded9fde43f14 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 14 Nov 2025 14:45:52 +0200 Subject: [PATCH 052/179] Added `--announce-only` param --- bittensor_cli/cli.py | 47 ++++++++++++++++++++++++++++- bittensor_cli/src/commands/proxy.py | 1 + bittensor_cli/src/commands/sudo.py | 4 +-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5108763cf..7b1fb03d7 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -394,6 +394,11 @@ def edit_help(cls, option_name: str, help_text: str): help="Optional proxy to use for the transaction: either the SS58 or the name of the proxy if you " f"have added it with {arg__('btcli config add-proxy')}.", ) + announce_only: bool = typer.Option( + False, + help=f"If set along with [{COLORS.G.ARG}]--proxy[/{COLORS.G.ARG}], will not actually make the extrinsic call, " + f"but rather just announce it to be made later.", + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -2401,6 +2406,7 @@ def wallet_transfer( ), period: int = Options.period, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -2489,6 +2495,7 @@ def wallet_swap_hotkey( prompt: bool = Options.prompt, json_output: bool = Options.json_output, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, ): """ Swap hotkeys of a given wallet on the blockchain. For a registered key pair, for example, a (coldkeyA, hotkeyA) pair, this command swaps the hotkeyA with a new, unregistered, hotkeyB to move the original registration to the (coldkeyA, hotkeyB) pair. @@ -3085,6 +3092,7 @@ def wallet_associate_hotkey( wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3580,6 +3588,7 @@ def wallet_set_id( help="The GitHub repository for the identity.", ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -3845,6 +3854,7 @@ def wallet_swap_coldkey( ), network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, quiet: bool = Options.quiet, verbose: bool = Options.verbose, force_swap: bool = typer.Option( @@ -3987,6 +3997,7 @@ def set_auto_stake( wallet_path: Optional[str] = Options.wallet_path, netuid: Optional[int] = Options.netuid_not_req, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -4180,6 +4191,7 @@ def stake_add( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, network: Optional[list[str]] = Options.network, rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, @@ -4481,6 +4493,7 @@ def stake_remove( "hotkeys in `--include-hotkeys`.", ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, @@ -4827,6 +4840,7 @@ def stake_move( False, "--stake-all", "--all", help="Stake all", prompt=False ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, period: int = Options.period, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -5025,6 +5039,7 @@ def stake_transfer( ), period: int = Options.period, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5215,6 +5230,7 @@ def stake_swap( help="Swap all available stake", ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, period: int = Options.period, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, @@ -5321,6 +5337,7 @@ def stake_set_claim_type( wallet_hotkey: Optional[str] = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5372,6 +5389,7 @@ def stake_process_claim( wallet_hotkey: Optional[str] = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5511,7 +5529,8 @@ def stake_set_children( help="Enter the stake weight proportions for the child hotkeys (sum should be less than or equal to 1)", prompt=False, ), - proxy: Optional[str] = None, + proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, @@ -5606,6 +5625,7 @@ def stake_revoke_children( help="When this flag is used it sets child hotkeys on all the subnets.", ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, @@ -5694,6 +5714,7 @@ def stake_childkey_take( prompt=False, ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -5779,6 +5800,7 @@ def mechanism_count_set( help="Number of mechanisms to set for the subnet.", ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -5929,6 +5951,7 @@ def mechanism_emission_set( help="Comma-separated relative weights for each mechanism (normalised automatically).", ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, prompt: bool = Options.prompt, @@ -6018,6 +6041,7 @@ def sudo_set( "", "--value", help="Value to set the hyperparameter to." ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -6227,6 +6251,7 @@ def sudo_senate_vote( help="The hash of the proposal to vote on.", ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -6279,6 +6304,7 @@ def sudo_set_take( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, take: float = typer.Option(None, help="The new take value."), quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -6381,6 +6407,7 @@ def sudo_trim( wallet_hotkey: Optional[str] = Options.wallet_hotkey, netuid: int = Options.netuid, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, max_uids: int = typer.Option( None, "--max", @@ -6682,6 +6709,7 @@ def subnets_create( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, subnet_name: Optional[str] = typer.Option( None, "--subnet-name", help="Name of the subnet" ), @@ -6792,6 +6820,7 @@ def subnets_start( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, netuid: int = Options.netuid, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -6863,6 +6892,7 @@ def subnets_set_identity( network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, subnet_name: Optional[str] = typer.Option( None, "--subnet-name", "--sn-name", help="Name of the subnet" ), @@ -7065,6 +7095,7 @@ def subnets_register( "use an era for this transaction that you may pay a different fee to register than the one stated.", ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -7214,6 +7245,7 @@ def subnets_set_symbol( network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, period: int = Options.period, json_output: bool = Options.json_output, prompt: bool = Options.prompt, @@ -7277,6 +7309,7 @@ def weights_reveal( wallet_hotkey: str = Options.wallet_hotkey, netuid: int = Options.netuid, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, uids: str = typer.Option( None, "--uids", @@ -7375,6 +7408,7 @@ def weights_commit( wallet_hotkey: str = Options.wallet_hotkey, netuid: int = Options.netuid, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, uids: str = typer.Option( None, "--uids", @@ -7540,6 +7574,7 @@ def liquidity_add( wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, liquidity_: Optional[float] = typer.Option( None, "--liquidity", @@ -7674,6 +7709,7 @@ def liquidity_remove( wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, position_id: Optional[int] = typer.Option( None, "--position-id", @@ -7747,6 +7783,7 @@ def liquidity_modify( wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, position_id: Optional[int] = typer.Option( None, "--position-id", @@ -7913,6 +7950,7 @@ def crowd_create( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, deposit: Optional[float] = typer.Option( None, "--deposit", @@ -8039,6 +8077,7 @@ def crowd_contribute( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8102,6 +8141,7 @@ def crowd_withdraw( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8159,6 +8199,7 @@ def crowd_finalize( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8233,6 +8274,7 @@ def crowd_update( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8297,6 +8339,7 @@ def crowd_refund( help="The ID of the crowdloan to refund", ), proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, @@ -8360,6 +8403,7 @@ def crowd_dissolve( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -8647,6 +8691,7 @@ def proxy_kill( network: Optional[list[str]] = Options.network, proxy_type: ProxyType = Options.proxy_type, proxy: Optional[str] = Options.proxy, + announce_only: bool = Options.announce_only, idx: int = typer.Option(0, "--index", help="TODO lol"), wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index d1467262c..a3bee8a52 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -50,6 +50,7 @@ class ProxyType(StrEnum): # TODO add announce with also --reject and --remove + async def submit_proxy( subtensor: "SubtensorInterface", wallet: "Wallet", diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index f0c0088e7..9992fe77e 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -376,9 +376,7 @@ async def set_hyperparameter_extrinsic( ) else: if subnet_owner != coldkey_ss58: - err_msg = ( - ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" - ) + err_msg = ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" err_console.print(err_msg) return False, err_msg, None call = call_ From 5c7c53509e3d4f807796d8189292d3691c5df05c Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 14 Nov 2025 14:51:56 +0200 Subject: [PATCH 053/179] Better formatting, param handling --- bittensor_cli/cli.py | 94 ++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7b1fb03d7..87d679bf5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1533,7 +1533,7 @@ def verbosity_handler( ) -> None: if json_output and prompt: raise typer.BadParameter( - f"Cannot specify both {arg__('json-output')} and {arg__('prompt')}" + f"Cannot specify both '--json-output' and '--prompt'" ) if quiet and verbose: err_console.print("Cannot specify both `--quiet` and `--verbose`") @@ -1936,21 +1936,29 @@ def config_get_proxies(self): table.add_row(name, address, proxy_type) console.print(table) - def is_valid_proxy_name_or_ss58(self, address: Optional[str]) -> Optional[str]: + def is_valid_proxy_name_or_ss58( + self, address: Optional[str], announce_only: bool + ) -> Optional[str]: """ Evaluates whether a non-None address is a valid SS58 address. Used as a callback for Annotated typer params. Args: address: an SS58 address, proxy name in config, or None + announce_only: whether the call should be made as just an announcement or the actual call made Returns: the SS58 address (if valid) or None (if None) Raises: - typer.BadParameter: if the address is not a valid SS58 address + typer.BadParameter: if the address is not a valid SS58 address, or if `--announce-only` is supplied but + without a proxy. """ if address is None: + if announce_only is True: + raise typer.BadParameter( + f"Cannot supply '--announce-only' without supplying '--proxy'" + ) return None outer_proxy_from_config = self.proxies.get(address, {}) proxy_from_config = outer_proxy_from_config.get("address") @@ -2439,7 +2447,7 @@ def wallet_transfer( raise typer.Exit() self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2522,7 +2530,7 @@ def wallet_swap_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) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) self.verbosity_handler(quiet, verbose, json_output, prompt) # Warning for netuid 0 - only swaps on root network, not a full swap @@ -3110,7 +3118,7 @@ def wallet_associate_hotkey( [green]$[/green] btcli wallet associate-hotkey --hotkey-ss58 5DkQ4... """ self.verbosity_handler(quiet, verbose, json_output=False, prompt=prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not wallet_name: wallet_name = Prompt.ask( "Enter the [blue]wallet name[/blue] [dim](which you want to associate with the hotkey)[/dim]", @@ -3612,7 +3620,7 @@ def wallet_set_id( [bold]Note[/bold]: This command should only be used if the user is willing to incur the a recycle fee associated with setting an identity on the blockchain. It is a high-level command that makes changes to the blockchain state and should not be used programmatically as part of other scripts or applications. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -3877,7 +3885,7 @@ def wallet_swap_coldkey( [green]$[/green] btcli wallet schedule-coldkey-swap --new-coldkey-ss58 5Dk...X3q """ self.verbosity_handler(quiet, verbose, prompt=False, json_output=False) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not wallet_name: wallet_name = Prompt.ask( @@ -4008,7 +4016,7 @@ def set_auto_stake( """Set the auto-stake destination hotkey for a coldkey.""" self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, @@ -4238,7 +4246,7 @@ def stake_add( """ netuids = netuids or [] self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: rate_tolerance = self.ask_rate_tolerance(rate_tolerance) @@ -4540,7 +4548,7 @@ def stake_remove( • [blue]--partial[/blue]: Complete partial unstake if rates exceed tolerance """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not unstake_all and not unstake_all_alpha: safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: @@ -4868,7 +4876,7 @@ def stake_move( [green]$[/green] btcli stake move """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if prompt: if not Confirm.ask( "This transaction will [bold]move stake[/bold] to another hotkey while keeping the same " @@ -5078,7 +5086,7 @@ def stake_transfer( [green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if prompt: if not Confirm.ask( "This transaction will [bold]transfer ownership[/bold] from one coldkey to another, in subnets " @@ -5260,7 +5268,7 @@ def stake_swap( [green]$[/green] btcli stake swap --wallet-name default --wallet-hotkey default --origin-netuid 1 --dest-netuid 2 --amount 100 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) console.print( "[dim]This command moves stake from one subnet to another subnet while keeping " "the same coldkey-hotkey pair.[/dim]" @@ -5363,7 +5371,7 @@ def stake_set_claim_type( [green]$[/green] btcli stake claim swap --wallet-name my_wallet """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5417,7 +5425,7 @@ def stake_process_claim( """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) parsed_netuids = None if netuids: parsed_netuids = parse_to_list( @@ -5550,7 +5558,7 @@ def stake_set_children( [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 --prop 0.3 --prop 0.7 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) netuid = get_optional_netuid(netuid, all_netuids) children = list_prompt( @@ -5643,7 +5651,7 @@ def stake_revoke_children( [green]$[/green] btcli stake child revoke --hotkey --netuid 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5738,7 +5746,7 @@ def stake_childkey_take( [green]$[/green] btcli stake child take --child-hotkey-ss58 --take 0.12 --netuid 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5822,7 +5830,7 @@ def mechanism_count_set( [green]$[/green] btcli subnet mech set --netuid 12 --count 2 --wallet.name my_wallet --wallet.hotkey admin """ - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) @@ -5974,7 +5982,7 @@ def mechanism_emission_set( 2. Apply a 70/30 distribution in one command: [green]$[/green] btcli subnet mech emissions-split --netuid 12 --split 70,30 --wallet.name my_wallet --wallet.hotkey admin """ - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) self.verbosity_handler(quiet, verbose, json_output, prompt) subtensor = self.initialize_chain(network) wallet = self.wallet_ask( @@ -6057,7 +6065,7 @@ def sudo_set( [green]$[/green] btcli sudo set --netuid 1 --param tempo --value 400 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not param_name or not param_value: hyperparams = self._run_command( @@ -6274,7 +6282,7 @@ def sudo_senate_vote( [green]$[/green] btcli sudo senate_vote --proposal """ # TODO discuss whether this should receive json_output. I don't think it should. - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) wallet = self.wallet_ask( wallet_name, @@ -6322,7 +6330,7 @@ def sudo_set_take( max_value = 0.18 min_value = 0.00 self.verbosity_handler(quiet, verbose, json_output, prompt=False) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, @@ -6428,7 +6436,7 @@ def sudo_trim( [green]$[/green] btcli sudo trim --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey --max 64 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, @@ -6756,7 +6764,7 @@ def subnets_create( [green]$[/green] btcli subnets create --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -6836,7 +6844,7 @@ def subnets_start( [green]$[/green] btcli subnets start --netuid 1 --wallet-name alice """ self.verbosity_handler(quiet, verbose, json_output=False, prompt=prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not wallet_name: wallet_name = Prompt.ask( "Enter the [blue]wallet name[/blue] [dim](which you used to create the subnet)[/dim]", @@ -6938,7 +6946,7 @@ def subnets_set_identity( [green]$[/green] btcli subnets set-identity --netuid 1 --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -7113,7 +7121,7 @@ def subnets_register( [green]$[/green] btcli subnets register --netuid 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -7268,7 +7276,7 @@ def subnets_set_symbol( [#AFEFFF]{success: [dark_orange]bool[/dark_orange], message: [dark_orange]str[/dark_orange]}[/#AFEFFF] """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if len(symbol) > 1: err_console.print("Your symbol must be a single character.") return False @@ -7338,7 +7346,7 @@ def weights_reveal( [green]$[/green] btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if uids: uids = parse_to_list( uids, @@ -7441,7 +7449,7 @@ def weights_commit( permissions. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if uids: uids = parse_to_list( uids, @@ -7603,7 +7611,7 @@ def liquidity_add( ): """Add liquidity to the swap (as a combination of TAO + Alpha).""" self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not netuid: netuid = Prompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -7730,7 +7738,7 @@ def liquidity_remove( """Remove liquidity from the swap (as a combination of TAO + Alpha).""" self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if all_liquidity_ids and position_id: print_error("Cannot specify both --all and --position-id.") return @@ -7803,7 +7811,7 @@ def liquidity_modify( ): """Modifies the liquidity position for the given subnet.""" self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if not netuid: netuid = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -8027,7 +8035,7 @@ def crowd_create( [green]$[/green] btcli crowd create --subnet-lease --emissions-share 25 --lease-end-block 500000 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, @@ -8097,7 +8105,7 @@ def crowd_contribute( [green]$[/green] btcli crowd contribute --id 1 """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8156,7 +8164,7 @@ def crowd_withdraw( Creators can only withdraw amounts above their initial deposit. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8214,7 +8222,7 @@ def crowd_finalize( address (if specified) and execute any attached call (e.g., subnet creation). """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8292,7 +8300,7 @@ def crowd_update( bounds, etc.). """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8360,7 +8368,7 @@ def crowd_refund( Contributors can call `btcli crowdloan withdraw` at will. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8423,7 +8431,7 @@ def crowd_dissolve( you can run `btcli crowd refund` to refund the remaining contributors. """ self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) if crowdloan_id is None: crowdloan_id = IntPrompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]crowdloan id[/{COLORS.G.SUBHEAD_MAIN}]", @@ -8729,7 +8737,7 @@ def proxy_kill( f"era: {period}\n" ) self.verbosity_handler(quiet, verbose, json_output, prompt) - proxy = self.is_valid_proxy_name_or_ss58(proxy) + proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) wallet = self.wallet_ask( wallet_name=wallet_name, wallet_path=wallet_path, From 162d3d440f58bde2c5c0c4742e3101dc29e04e64 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 14 Nov 2025 16:39:21 +0200 Subject: [PATCH 054/179] Address book in sqlite --- bittensor_cli/src/bittensor/utils.py | 245 +++++++++++++++++- bittensor_cli/src/commands/subnets/subnets.py | 4 +- 2 files changed, 243 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index f8e322f01..336c82db0 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -806,14 +806,243 @@ def normalize_hyperparameters( return normalized_values +class TableDefinition: + name: str + cols: tuple[tuple[str, str], ...] + + @classmethod + def create_if_not_exists(cls, conn: sqlite3.Connection, _: sqlite3.Cursor) -> None: + """ + Creates the table if it doesn't exist. + Args: + conn: sqlite3 connection + _: sqlite3 cursor + """ + columns_ = ", ".join([" ".join(x) for x in cls.cols]) + conn.execute(f"CREATE TABLE IF NOT EXISTS {cls.name} ({columns_})") + conn.commit() + + @classmethod + def read_rows( + cls, + _: sqlite3.Connection, + cursor: sqlite3.Cursor, + include_header: bool = True, + ) -> list[tuple[Union[str, int], ...]]: + """ + Reads rows from a table. + + Args: + _: sqlite3 connection + cursor: sqlite3 cursor + include_header: Whether to include the header row + + Returns: + rows of the table, with column names as the header row if `include_header` is set + + """ + header = tuple(x[0] for x in cls.cols) + cols = ", ".join(header) + cursor.execute(f"SELECT {cols} FROM {cls.name}") + rows = cursor.fetchall() + if not include_header: + return rows + else: + return [header] + rows + + @classmethod + def update_entry(cls, *args, **kwargs): + raise NotImplementedError() + + @classmethod + def add_entry(cls, *args, **kwargs): + raise NotImplementedError() + + @classmethod + def delete_entry(cls, *args, **kwargs): + raise NotImplementedError() + + +class AddressBook(TableDefinition): + name = "address_book" + cols = (("name", "TEXT"), ("ss58_address", "TEXT"), ("note", "TEXT")) + + @classmethod + def add_entry( + cls, + conn: sqlite3.Connection, + _: sqlite3.Cursor, + *, + name: str, + ss58_address: str, + note: str, + ) -> None: + conn.execute( + f"INSERT INTO {cls.name} (name, ss58_address, note) VALUES (?, ?, ?)", + (name, ss58_address, note), + ) + conn.commit() + + @classmethod + def update_entry( + cls, + conn: sqlite3.Connection, + cursor: sqlite3.Cursor, + *, + name: str, + ss58_address: Optional[str] = None, + note: Optional[str] = None, + ): + cursor.execute( + f"SELECT ss58_address, note FROM {cls.name} WHERE name = ?", + (name,), + ) + row = cursor.fetchone()[0] + ss58_address_ = ss58_address or row[0] + note_ = note or row[1] + conn.execute( + f"UPDATE {cls.name} SET ss58_address = ?, note = ? WHERE name = ?", + (ss58_address_, note_, name), + ) + conn.commit() + + @classmethod + def delete_entry( + cls, conn: sqlite3.Connection, cursor: sqlite3.Cursor, *, name: str + ): + conn.execute( + f"DELETE FROM {cls.name} WHERE name = ?", + (name,), + ) + conn.commit() + + +class ProxyAddressBook(TableDefinition): + name = "proxy_address_book" + cols = ( + ("name", "TEXT"), + ("ss58_address", "TEXT"), + ("spawner", "TEXT"), + ("proxy_type", "TEXT"), + ("note", "TEXT"), + ) + + @classmethod + def update_entry( + cls, + conn: sqlite3.Connection, + cursor: sqlite3.Cursor, + *, + name: str, + ss58_address: Optional[str] = None, + spawner: Optional[str] = None, + proxy_type: Optional[str] = None, + note: Optional[str] = None, + ) -> None: + cursor.execute( + f"SELECT ss58_address, spawner, proxy_type, note FROM {cls.name} WHERE name = ?", + (name,), + ) + row = cursor.fetchone()[0] + ss58_address_ = ss58_address or row[0] + spawner_ = spawner or row[1] + proxy_type_ = proxy_type or row[2] + note_ = note or row[3] + conn.execute( + f"UPDATE {cls.name} SET ss58_address = ?, spawner = ?, proxy_type = ?, note = ? WHERE name = ?", + (ss58_address_, spawner_, proxy_type_, note_, name), + ) + conn.commit() + + @classmethod + def add_entry( + cls, + conn: sqlite3.Connection, + _: sqlite3.Cursor, + *, + name: str, + ss58_address: str, + spawner: str, + proxy_type: str, + note: str, + ) -> None: + conn.execute( + f"INSERT INTO {cls.name} (name, ss58_address, spawner, proxy_type, note) VALUES (?, ?, ?, ?, ?)", + (name, ss58_address, spawner, proxy_type, note), + ) + conn.commit() + + @classmethod + def delete_entry( + cls, + conn: sqlite3.Connection, + _: sqlite3.Cursor, + *, + name: str, + ): + conn.execute( + f"DELETE FROM {cls.name} WHERE name = ?", + (name,), + ) + conn.commit() + + +class ProxyAnnouncements(TableDefinition): + name = "proxy_announcements" + cols = ( + ("address", "TEXT"), + ("epoch_time", "INTEGER"), + ("block", "INTEGER"), + ("call_hash", "TEXT"), + ) + + @classmethod + def add_entry( + cls, + conn: sqlite3.Connection, + _: sqlite3.Cursor, + *, + address: str, + epoch_time: int, + block: int, + call_hash: str, + ) -> None: + conn.execute( + f"INSERT INTO {cls.name} (address, epoch_time, block, call_hash) VALUES (?, ?, ?, ?)", + (address, epoch_time, block, call_hash), + ) + conn.commit() + + @classmethod + def delete_entry( + cls, + conn: sqlite3.Connection, + _: sqlite3.Cursor, + *, + address: str, + epoch_time: int, + block: int, + call_hash: str, + ): + conn.execute( + f"DELETE FROM {cls.name} WHERE call_hash = ?, address = ?, epoch_time = ?, block = ?", + (call_hash, address, epoch_time, block), + ) + conn.commit() + + class DB: """ For ease of interaction with the SQLite database used for --reuse-last and --html outputs of tables + + Also for address book """ def __init__( self, - db_path: str = os.path.expanduser("~/.bittensor/bittensor.db"), + db_path: str = os.path.join( + os.path.expanduser(defaults.config.base_path), "bittensor.db" + ), row_factory=None, ): self.db_path = db_path @@ -830,10 +1059,15 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.conn.close() -def create_table(title: str, columns: list[tuple[str, str]], rows: list[list]) -> None: +def create_and_populate_table( + title: str, columns: list[tuple[str, str]], rows: list[list] +) -> None: """ Creates and populates the rows of a table in the SQLite database. + Warning: + Will overwrite the existing table. + :param title: title of the table :param columns: [(column name, column type), ...] :param rows: [[element, element, ...], ...] @@ -853,8 +1087,7 @@ def create_table(title: str, columns: list[tuple[str, str]], rows: list[list]) - conn.commit() columns_ = ", ".join([" ".join(x) for x in columns]) creation_query = f"CREATE TABLE IF NOT EXISTS {title} ({columns_})" - conn.commit() - cursor.execute(creation_query) + conn.execute(creation_query) conn.commit() query = f"INSERT INTO {title} ({', '.join([x[0] for x in columns])}) VALUES ({', '.join(['?'] * len(columns))})" cursor.executemany(query, rows) @@ -1026,6 +1259,10 @@ def render_tree( webbrowser.open(f"file://{output_file}") +def ensure_address_book_tables_exist(): + tables = [("address_book", (""))] + + def group_subnets(registrations): if not registrations: return "" diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index cd31e5aca..1a9e894c6 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -23,7 +23,7 @@ from bittensor_cli.src.commands.wallets import set_id, get_id from bittensor_cli.src.bittensor.utils import ( console, - create_table, + create_and_populate_table, err_console, print_verbose, print_error, @@ -2000,7 +2000,7 @@ async def metagraph_cmd( } if not no_cache: update_metadata_table("metagraph", metadata_info) - create_table( + create_and_populate_table( "metagraph", columns=[ ("UID", "INTEGER"), From 05f348b2a4b7b80393961c7c3acbccd27b4ee4f6 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 14 Nov 2025 17:21:49 +0200 Subject: [PATCH 055/179] Config mgmt of new sqlite proxy address book --- bittensor_cli/cli.py | 125 +++++++++++++++++++++++---- bittensor_cli/src/bittensor/utils.py | 32 ++++++- 2 files changed, 137 insertions(+), 20 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 87d679bf5..2bd5bbdc4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -67,6 +67,8 @@ prompt_for_subnet_identity, validate_rate_tolerance, get_hotkey_pub_ss58, + ensure_address_book_tables_exist, + ProxyAddressBook, ) from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds @@ -1518,14 +1520,17 @@ def main_callback( asi_logger.addHandler(handler) logger.addHandler(handler) + ensure_address_book_tables_exist() # load proxies address book - if os.path.exists(self.proxies_path): - with open(self.proxies_path, "r") as f: - proxies = safe_load(f) or {} - else: - proxies = {} - with open(self.proxies_path, "w+") as f: - safe_dump(proxies, f) + with ProxyAddressBook.get_db() as (conn, cursor): + rows = ProxyAddressBook.read_rows(conn, cursor, include_header=False) + proxies = {} + for name, ss58_address, spawner, proxy_type, _ in rows: + proxies[name] = { + "address": ss58_address, + "spawner": spawner, + "proxy_type": proxy_type, + } self.proxies = proxies def verbosity_handler( @@ -1884,15 +1889,46 @@ def config_add_proxy( prompt="Enter the type of this pure proxy", ), ], + spawner: Annotated[ + str, + typer.Option( + callback=is_valid_ss58_address_param, + help="The SS58 address of the spawner", + prompt="Enter the SS58 address of the spawner", + ), + ], + note: str = typer.Option("", help="Any notes about this entry"), ): """ Adds a new pure proxy to the address book. """ - # TODO check if name already exists, Confirmation - # TODO add spawner - self.proxies[name] = {"proxy_type": proxy_type.value, "address": address} - with open(self.proxies_path, "w+") as f: - safe_dump(self.proxies, f) + if self.proxies.get(name) is not None: + err_console.print( + f"Proxy {name} already exists. Use `btcli config update-proxy` to update it." + ) + raise typer.Exit() + proxy_type_val: str + if isinstance(proxy_type, ProxyType): + proxy_type_val = proxy_type.value + else: + proxy_type_val = proxy_type + + self.proxies[name] = { + "proxy_type": proxy_type_val, + "address": address, + "spawner": spawner, + "note": note, + } + with ProxyAddressBook.get_db() as (conn, cursor): + ProxyAddressBook.add_entry( + conn, + cursor, + name=name, + ss58_address=address, + spawner=spawner, + proxy_type=proxy_type_val, + note=note, + ) self.config_get_proxies() def config_remove_proxy( @@ -1912,9 +1948,9 @@ def config_remove_proxy( """ if name in self.proxies: del self.proxies[name] + with ProxyAddressBook.get_db() as (conn, cursor): + ProxyAddressBook.delete_entry(conn, cursor, name=name) console.print(f"Removed {name} from the address book.") - with open(self.proxies_path, "w+") as f: - safe_dump(self.proxies, f) else: err_console.print(f"Proxy {name} not found in address book.") self.config_get_proxies() @@ -1926,16 +1962,69 @@ def config_get_proxies(self): table = Table( Column("[bold white]Name", style=f"{COLORS.G.ARG}"), Column("[bold white]Address", style="gold1"), + Column("Spawner", style="medium_purple"), Column("Proxy Type", style="medium_purple"), + Column("Proxy Address", style="dim"), box=box.SIMPLE_HEAD, title=f"[{COLORS.G.HEADER}]BTCLI Proxies Address Book[/{COLORS.G.HEADER}]: {arg__(self.proxies_path)}", ) - for name, keys in self.proxies.items(): - address = keys.get("address") - proxy_type = keys.get("proxy_type") - table.add_row(name, address, proxy_type) + with ProxyAddressBook.get_db() as (conn, cursor): + rows = ProxyAddressBook.read_rows(conn, cursor, include_header=False) + for name, ss58_address, spawner, proxy_type, note in rows: + table.add_row(name, ss58_address, spawner, proxy_type, note) console.print(table) + def config_update_proxy( + self, + name: Annotated[ + str, + typer.Option( + help="Name of the proxy", prompt="Enter a name for this proxy" + ), + ], + address: Annotated[ + Optional[str], + typer.Option( + callback=is_valid_ss58_address_param, + help="The SS58 address of the pure proxy", + ), + ] = None, + proxy_type: Annotated[ + Optional[ProxyType], + typer.Option( + help="The type of this pure proxy", + ), + ] = None, + spawner: Annotated[ + str, + typer.Option( + callback=is_valid_ss58_address_param, + help="The SS58 address of the spawner", + ), + ] = None, + note: Optional[str] = typer.Option(None, help="Any notes about this entry"), + ): + if name not in self.proxies: + err_console.print(f"Proxy {name} not found in address book.") + return + else: + if isinstance(proxy_type, ProxyType): + proxy_type_val = proxy_type.value + else: + proxy_type_val = proxy_type + with ProxyAddressBook.get_db() as (conn, cursor): + ProxyAddressBook.update_entry( + conn, + cursor, + name=name, + ss58_address=address, + proxy_type=proxy_type_val, + spawner=spawner, + note=note, + ) + console.print("Proxy updated") + self.config_get_proxies() + def is_valid_proxy_name_or_ss58( self, address: Optional[str], announce_only: bool ) -> Optional[str]: diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 336c82db0..e3bd9acaf 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -5,7 +5,7 @@ import sqlite3 import webbrowser from pathlib import Path -from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable +from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable, Generator from urllib.parse import urlparse from functools import partial import re @@ -807,9 +807,21 @@ def normalize_hyperparameters( class TableDefinition: + """ + Base class for address book table definitions/functions + """ + name: str cols: tuple[tuple[str, str], ...] + @staticmethod + def get_db() -> Generator[tuple[sqlite3.Connection, sqlite3.Cursor], None]: + """ + Helper function to get a DB connection + """ + with DB() as (conn, cursor): + yield conn, cursor + @classmethod def create_if_not_exists(cls, conn: sqlite3.Connection, _: sqlite3.Cursor) -> None: """ @@ -852,14 +864,23 @@ def read_rows( @classmethod def update_entry(cls, *args, **kwargs): + """ + Updates an existing entry in the table. + """ raise NotImplementedError() @classmethod def add_entry(cls, *args, **kwargs): + """ + Adds an entry to the table. + """ raise NotImplementedError() @classmethod def delete_entry(cls, *args, **kwargs): + """ + Deletes an entry from the table. + """ raise NotImplementedError() @@ -1260,7 +1281,14 @@ def render_tree( def ensure_address_book_tables_exist(): - tables = [("address_book", (""))] + """ + Creates address book tables if they don't exist. + + Should be run at startup to ensure that the address book tables exist. + """ + with DB() as (conn, cursor): + for table in (AddressBook, ProxyAddressBook, ProxyAnnouncements): + table.create_if_not_exists(conn, cursor) def group_subnets(registrations): From 05c953b7b1d7dbd2aae48833a53af0e3b9fa5d9d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 14 Nov 2025 17:22:31 +0200 Subject: [PATCH 056/179] Add config update-proxy cmd --- bittensor_cli/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2bd5bbdc4..0955cb178 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -931,6 +931,7 @@ def __init__(self): self.config_app.command("add-proxy")(self.config_add_proxy) self.config_app.command("proxies")(self.config_get_proxies) self.config_app.command("remove-proxy")(self.config_remove_proxy) + self.config_app.command("update-proxy")(self.config_update_proxy) # self.config_app.command("metagraph", hidden=True)(self.metagraph_config) # wallet commands From 9bb3ebda0ab691e6468b816f504f72412fc476b1 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 14 Nov 2025 17:25:25 +0200 Subject: [PATCH 057/179] Updated with context manager --- bittensor_cli/src/bittensor/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index e3bd9acaf..9593b7fe2 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -4,6 +4,7 @@ import os import sqlite3 import webbrowser +from contextlib import contextmanager from pathlib import Path from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable, Generator from urllib.parse import urlparse @@ -815,6 +816,7 @@ class TableDefinition: cols: tuple[tuple[str, str], ...] @staticmethod + @contextmanager def get_db() -> Generator[tuple[sqlite3.Connection, sqlite3.Cursor], None]: """ Helper function to get a DB connection From c540493cbbfb16df599373419f97c3d996416eb2 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 14 Nov 2025 17:26:43 +0200 Subject: [PATCH 058/179] Updated defaults --- bittensor_cli/src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 663f12126..9b4e1c3c3 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -116,7 +116,7 @@ class config: class proxies: base_path = "~/.bittensor" - path = "~/.bittensor/proxy-address-book.yml" + path = "~/.bittensor/bittensor.db" dictionary = {} class subtensor: From b5a1ababb706ff2c28af037fa23783bc8b3c02e7 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 18 Nov 2025 15:22:01 +0200 Subject: [PATCH 059/179] Display the total even with the `--verbose` flag --- bittensor_cli/src/commands/stake/list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index b2407bab7..2ced76d80 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -628,7 +628,7 @@ def format_cell( total_tao_value = ( f"τ {millify_tao(all_hks_tao_value.tao + balance.tao)}" if not verbose - else all_hks_tao_value + else all_hks_tao_value + balance ) total_swapped_tao_value = ( f"τ {millify_tao(all_hks_swapped_tao_value.tao)}" @@ -648,8 +648,8 @@ def format_cell( f"[{COLOR_PALETTE.G.BALANCE}]{total_tao_value}[/{COLOR_PALETTE.G.BALANCE}]\n" ) dict_output["free_balance"] = balance.tao - dict_output["total_tao_value"] = all_hks_tao_value.tao - # dict_output["total_swapped_tao_value"] = all_hks_swapped_tao_value.tao + dict_output["total_tao_value"] = all_hks_tao_value.tao + balance.tao + dict_output["total_swapped_tao_value"] = all_hks_swapped_tao_value.tao if json_output: json_console.print(json.dumps(dict_output)) if not sub_stakes: From 49a0fb8d5bf8dac0d77416eb2e0bf31c7d873324 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 13:30:44 +0200 Subject: [PATCH 060/179] Name shadowing --- bittensor_cli/src/commands/stake/list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index b2407bab7..4b9a69b4c 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -50,9 +50,9 @@ async def get_stake_data(block_hash_: str = None): subtensor.all_subnets(block_hash=block_hash_), ) - claimable_amounts = {} + claimable_amounts_ = {} if sub_stakes_: - claimable_amounts = await subtensor.get_claimable_stakes_for_coldkey( + claimable_amounts_ = await subtensor.get_claimable_stakes_for_coldkey( coldkey_ss58=coldkey_address, stakes_info=sub_stakes_, block_hash=block_hash_, @@ -63,7 +63,7 @@ async def get_stake_data(block_hash_: str = None): sub_stakes_, registered_delegate_info_, dynamic_info__, - claimable_amounts, + claimable_amounts_, ) def define_table( From 4b9699981d440c8d5e97940a47219ccade5cba54 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 14:06:49 +0200 Subject: [PATCH 061/179] Announcements beginning --- bittensor_cli/cli.py | 1 + .../src/bittensor/extrinsics/transfer.py | 3 ++ .../src/bittensor/subtensor_interface.py | 42 +++++++++++++++---- bittensor_cli/src/bittensor/utils.py | 8 +++- bittensor_cli/src/commands/wallets.py | 2 + 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0955cb178..932db3fc9 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2574,6 +2574,7 @@ def wallet_transfer( prompt=prompt, json_output=json_output, proxy=proxy, + announce_only=announce_only, ) ) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 2754fe0f5..6cf5580f4 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -34,6 +34,7 @@ async def transfer_extrinsic( wait_for_finalization: bool = False, prompt: bool = False, proxy: Optional[str] = None, + announce_only: bool = False, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Transfers funds from this wallet to the destination public key address. @@ -50,6 +51,7 @@ async def transfer_extrinsic( `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. :param prompt: If `True`, the call waits for confirmation from the user before proceeding. :param proxy: Optional proxy to use for this call. + :param announce_only: If set along with proxy, will make this call as an announcement, rather than making the call :return: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for finalization / inclusion, the response is `True`, regardless of its inclusion. @@ -87,6 +89,7 @@ async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt] wait_for_inclusion=wait_for_inclusion, proxy=proxy, era={"period": era}, + announce_only=announce_only, ) block_hash_ = receipt_.block_hash if receipt_ is not None else "" return success_, block_hash_, error_msg_, receipt_ diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 7d4987893..6ab77eb89 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -43,6 +43,7 @@ u16_normalized_float, U16_MAX, get_hotkey_pub_ss58, + ProxyAnnouncements, ) GENESIS_ADDRESS = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM" @@ -1156,6 +1157,7 @@ async def sign_and_send_extrinsic( proxy: Optional[str] = None, nonce: Optional[str] = None, sign_with: Literal["coldkey", "hotkey", "coldkeypub"] = "coldkey", + announce_only: bool = False, ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1168,25 +1170,30 @@ async def sign_and_send_extrinsic( :param proxy: The real account used to create the proxy. None if not using a proxy for this call. :param nonce: The nonce used to submit this extrinsic call. :param sign_with: Determine which of the wallet's keypairs to use to sign the extrinsic call. + :param announce_only: If set, makes the call as an announcement, rather than making the call. :return: (success, error message, extrinsic receipt | None) """ if proxy is not None: - call = await self.substrate.compose_call( - "Proxy", - "proxy", - {"real": proxy, "call": call, "force_proxy_type": None}, - ) + if announce_only: + call = await self.substrate.compose_call( + "Proxy", "announce", {"real": proxy, "call_hash": call.call_hash} + ) + else: + call = await self.substrate.compose_call( + "Proxy", + "proxy", + {"real": proxy, "call": call, "force_proxy_type": None}, + ) call_args: dict[str, Union[GenericCall, Keypair, dict[str, int], int]] = { "call": call, + # sign with specified key "keypair": getattr(wallet, sign_with), "nonce": nonce, } if era is not None: call_args["era"] = era - extrinsic = await self.substrate.create_signed_extrinsic( - **call_args - ) # sign with coldkey + extrinsic = await self.substrate.create_signed_extrinsic(**call_args) try: response = await self.substrate.submit_extrinsic( extrinsic, @@ -1197,6 +1204,25 @@ async def sign_and_send_extrinsic( if not wait_for_finalization and not wait_for_inclusion: return True, "", response if await response.is_success: + if announce_only: + block = await self.substrate.get_block_number(response.block_hash) + with ProxyAnnouncements.get_db() as (conn, cursor): + ProxyAnnouncements.add_entry( + conn, + cursor, + address=proxy, + epoch_time=int(time.time()), + block=block, + call_hash=call.call_hash, + call_args={ + "module": call.call_module, + "function": call.call_function, + "args": call.call_args, + }, + ) + console.print( + f"Added entry {call.call_hash} at block {block} to your ProxyAnnouncements address book." + ) return True, "", response else: return False, format_error_message(await response.error_message), None diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 9593b7fe2..fc1b80a03 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1,4 +1,5 @@ import ast +import json from collections import namedtuple import math import os @@ -1017,6 +1018,7 @@ class ProxyAnnouncements(TableDefinition): ("epoch_time", "INTEGER"), ("block", "INTEGER"), ("call_hash", "TEXT"), + ("call_args", "TEXT"), ) @classmethod @@ -1029,10 +1031,12 @@ def add_entry( epoch_time: int, block: int, call_hash: str, + call_args: dict, ) -> None: + call_args_ = json.dumps(call_args) conn.execute( - f"INSERT INTO {cls.name} (address, epoch_time, block, call_hash) VALUES (?, ?, ?, ?)", - (address, epoch_time, block, call_hash), + f"INSERT INTO {cls.name} (address, epoch_time, block, call_hash, call_args) VALUES (?, ?, ?, ?, ?)", + (address, epoch_time, block, call_hash, call_args_), ) conn.commit() diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 91598fbb5..8813e6839 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1536,6 +1536,7 @@ async def transfer( prompt: bool, json_output: bool, proxy: Optional[str] = None, + announce_only: bool = False, ): """Transfer token of amount to destination.""" result, ext_receipt = await transfer_extrinsic( @@ -1548,6 +1549,7 @@ async def transfer( era=era, prompt=prompt, proxy=proxy, + announce_only=announce_only, ) ext_id = (await ext_receipt.get_extrinsic_identifier()) if result else None if json_output: From 6f01824eca8918d7a6f05a136746957282255cf6 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 15:31:39 +0200 Subject: [PATCH 062/179] Improved proxy address book --- bittensor_cli/cli.py | 32 +++++---- bittensor_cli/src/bittensor/utils.py | 16 +++-- bittensor_cli/src/commands/proxy.py | 103 +++++++++++++++++++++++++-- 3 files changed, 124 insertions(+), 27 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 932db3fc9..992d8c960 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1526,7 +1526,7 @@ def main_callback( with ProxyAddressBook.get_db() as (conn, cursor): rows = ProxyAddressBook.read_rows(conn, cursor, include_header=False) proxies = {} - for name, ss58_address, spawner, proxy_type, _ in rows: + for name, ss58_address, _, spawner, proxy_type, _ in rows: proxies[name] = { "address": ss58_address, "spawner": spawner, @@ -1879,25 +1879,26 @@ def config_add_proxy( str, typer.Option( callback=is_valid_ss58_address_param, - help="The SS58 address of the pure proxy", - prompt="Enter the SS58 address of the pure proxy", + help="The SS58 address of the pure proxy/delegatee", + prompt="Enter the SS58 address of the pure proxy/delegatee", ), ], proxy_type: Annotated[ ProxyType, typer.Option( - help="The type of this pure proxy", - prompt="Enter the type of this pure proxy", + help="The type of this proxy", + prompt="Enter the type of this proxy", ), ], spawner: Annotated[ str, typer.Option( callback=is_valid_ss58_address_param, - help="The SS58 address of the spawner", - prompt="Enter the SS58 address of the spawner", + help="The SS58 address of the spawner (pure proxy)/delegator (regular proxy)", + prompt="Enter the SS58 address of the spawner (pure proxy)/delegator (regular proxy)", ), ], + delay: int = typer.Option(0, help="Delay, in blocks."), note: str = typer.Option("", help="Any notes about this entry"), ): """ @@ -1918,6 +1919,7 @@ def config_add_proxy( "proxy_type": proxy_type_val, "address": address, "spawner": spawner, + "delay": delay, "note": note, } with ProxyAddressBook.get_db() as (conn, cursor): @@ -1928,6 +1930,7 @@ def config_add_proxy( ss58_address=address, spawner=spawner, proxy_type=proxy_type_val, + delay=delay, note=note, ) self.config_get_proxies() @@ -1962,17 +1965,18 @@ def config_get_proxies(self): """ table = Table( Column("[bold white]Name", style=f"{COLORS.G.ARG}"), - Column("[bold white]Address", style="gold1"), - Column("Spawner", style="medium_purple"), + Column("Address", style="gold1"), + Column("Spawner/Delegator", style="medium_purple"), Column("Proxy Type", style="medium_purple"), - Column("Proxy Address", style="dim"), + Column("Delay", style="dim"), + Column("Note", style="dim"), box=box.SIMPLE_HEAD, title=f"[{COLORS.G.HEADER}]BTCLI Proxies Address Book[/{COLORS.G.HEADER}]: {arg__(self.proxies_path)}", ) with ProxyAddressBook.get_db() as (conn, cursor): rows = ProxyAddressBook.read_rows(conn, cursor, include_header=False) - for name, ss58_address, spawner, proxy_type, note in rows: - table.add_row(name, ss58_address, spawner, proxy_type, note) + for name, ss58_address, delay, spawner, proxy_type, note in rows: + table.add_row(name, ss58_address, spawner, proxy_type, str(delay), note) console.print(table) def config_update_proxy( @@ -8606,7 +8610,7 @@ def proxy_create( f"prompt: {prompt}\n" ) - should_update, proxy_name, created_pure, created_type = self._run_command( + self._run_command( proxy_commands.create_proxy( subtensor=self.initialize_chain(network), wallet=wallet, @@ -8620,8 +8624,6 @@ def proxy_create( period=period, ) ) - if should_update: - self.config_add_proxy(proxy_name, created_pure, created_type) def proxy_add( self, diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index fc1b80a03..42bf2f441 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -946,6 +946,7 @@ class ProxyAddressBook(TableDefinition): cols = ( ("name", "TEXT"), ("ss58_address", "TEXT"), + ("delay", "INTEGER"), ("spawner", "TEXT"), ("proxy_type", "TEXT"), ("note", "TEXT"), @@ -959,22 +960,24 @@ def update_entry( *, name: str, ss58_address: Optional[str] = None, + delay: Optional[int] = None, spawner: Optional[str] = None, proxy_type: Optional[str] = None, note: Optional[str] = None, ) -> None: cursor.execute( - f"SELECT ss58_address, spawner, proxy_type, note FROM {cls.name} WHERE name = ?", + f"SELECT ss58_address, spawner, proxy_type, delay, note FROM {cls.name} WHERE name = ?", (name,), ) row = cursor.fetchone()[0] ss58_address_ = ss58_address or row[0] spawner_ = spawner or row[1] proxy_type_ = proxy_type or row[2] - note_ = note or row[3] + delay = delay if delay is not None else row[3] + note_ = note or row[4] conn.execute( - f"UPDATE {cls.name} SET ss58_address = ?, spawner = ?, proxy_type = ?, note = ? WHERE name = ?", - (ss58_address_, spawner_, proxy_type_, note_, name), + f"UPDATE {cls.name} SET ss58_address = ?, spawner = ?, proxy_type = ?, delay = ?, note = ? WHERE name = ?", + (ss58_address_, spawner_, proxy_type_, note_, delay, name), ) conn.commit() @@ -986,13 +989,14 @@ def add_entry( *, name: str, ss58_address: str, + delay: int, spawner: str, proxy_type: str, note: str, ) -> None: conn.execute( - f"INSERT INTO {cls.name} (name, ss58_address, spawner, proxy_type, note) VALUES (?, ?, ?, ?, ?)", - (name, ss58_address, spawner, proxy_type, note), + f"INSERT INTO {cls.name} (name, ss58_address, delay, spawner, proxy_type, note) VALUES (?, ?, ?, ?, ?, ?)", + (name, ss58_address, delay, spawner, proxy_type, note), ) conn.commit() diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index a3bee8a52..ff99af1ad 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -4,12 +4,14 @@ from rich.prompt import Confirm, Prompt from scalecodec import GenericCall +from bittensor_cli.src import COLORS from bittensor_cli.src.bittensor.utils import ( print_extrinsic_id, json_console, console, err_console, unlock_key, + ProxyAddressBook, ) if TYPE_CHECKING: @@ -171,7 +173,12 @@ async def create_proxy( created_proxy_type = getattr(ProxyType, attrs["proxy_type"]) arg_start = "`" if json_output else "[blue]" arg_end = "`" if json_output else "[/blue]" - msg = f"Created pure '{created_pure}' from spawner '{created_spawner}' with proxy type '{created_proxy_type.value}'." + msg = ( + f"Created pure '{created_pure}' " + f"from spawner '{created_spawner}' " + f"with proxy type '{created_proxy_type.value}' " + f"with delay {delay}." + ) console.print(msg) if not prompt: console.print( @@ -183,6 +190,22 @@ async def create_proxy( else: if Confirm.ask("Would you like to add this to your address book?"): proxy_name = Prompt.ask("Name this proxy") + note = Prompt.ask("[Optional] Add a note for this proxy", default="") + with ProxyAddressBook.get_db() as (conn, cursor): + ProxyAddressBook.add_entry( + conn, + cursor, + name=proxy_name, + ss58_address=created_pure, + delay=delay, + proxy_type=created_proxy_type.value, + note=note, + spawner=created_spawner, + ) + console.print( + f"Added to Proxy Address Book.\n" + f"Show this information with [{COLORS.G.ARG}]btcli config proxies[/{COLORS.G.ARG}]" + ) return True, proxy_name, created_pure, created_proxy_type if json_output: @@ -298,15 +321,83 @@ async def add_proxy( "delegate": delegate, }, ) - return await submit_proxy( - subtensor=subtensor, - wallet=wallet, + success, msg, receipt = await subtensor.sign_and_send_extrinsic( call=call, + wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - period=period, - json_output=json_output, + era={"period": period}, ) + if success: + await print_extrinsic_id(receipt) + delegatee = None + delegator = None + created_proxy_type = None + for event in await receipt.triggered_events: + if event["event_id"] == "PureCreated": + attrs = event["attributes"] + delegatee = attrs["delegatee"] + delegator = attrs["delegator"] + created_proxy_type = getattr(ProxyType, attrs["proxy_type"]) + arg_start = "`" if json_output else "[blue]" + arg_end = "`" if json_output else "[/blue]" + msg = ( + f"Added proxy delegatee '{delegatee}' " + f"from delegator '{delegator}' " + f"with proxy type '{created_proxy_type.value}' " + f"with delay {delay}." + ) + console.print(msg) + if not prompt: + console.print( + f" You can add this to your config with {arg_start}" + f"btcli config add-proxy " + # TODO adjust for new address book style + f"--name --address {delegatee} --proxy-type {created_proxy_type.value}" + f"{arg_end}" + ) + else: + if Confirm.ask("Would you like to add this to your address book?"): + proxy_name = Prompt.ask("Name this proxy") + note = Prompt.ask("[Optional] Add a note for this proxy", default="") + with ProxyAddressBook.get_db() as (conn, cursor): + ProxyAddressBook.add_entry( + conn, + cursor, + name=proxy_name, + # TODO verify this is correct (it's opposite of create pure) + ss58_address=delegator, + delay=delay, + proxy_type=created_proxy_type.value, + note=note, + spawner=delegatee, + ) + console.print( + f"Added to Proxy Address Book.\n" + f"Show this information with [{COLORS.G.ARG}]btcli config proxies[/{COLORS.G.ARG}]" + ) + + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_id": await receipt.get_extrinsic_identifier(), + } + ) + + else: + if json_output: + json_console.print_json( + data={ + "success": success, + "message": msg, + "extrinsic_id": None, + } + ) + else: + err_console.print(f"Failure: {msg}") # TODO add more shit here + return None async def kill_proxy( From 658c6894c8bfae7df503355e5cc7b4a1481f7d8a Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 15:38:39 +0200 Subject: [PATCH 063/179] Improved proxy address book --- bittensor_cli/cli.py | 2 ++ bittensor_cli/src/commands/proxy.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 992d8c960..cff89dff5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1893,6 +1893,8 @@ def config_add_proxy( spawner: Annotated[ str, typer.Option( + "--spawner", + "--delegator", callback=is_valid_ss58_address_param, help="The SS58 address of the spawner (pure proxy)/delegator (regular proxy)", prompt="Enter the SS58 address of the spawner (pure proxy)/delegator (regular proxy)", diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index ff99af1ad..833df461f 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -352,8 +352,8 @@ async def add_proxy( console.print( f" You can add this to your config with {arg_start}" f"btcli config add-proxy " - # TODO adjust for new address book style - f"--name --address {delegatee} --proxy-type {created_proxy_type.value}" + f"--name --address {delegatee} --proxy-type {created_proxy_type.value} --delegator " + f"{delegator} --delay {delay}" f"{arg_end}" ) else: From 77a309e670fcf05cef20187e82529dbcef92cac6 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 15:39:17 +0200 Subject: [PATCH 064/179] Updated text --- bittensor_cli/src/commands/proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 833df461f..df2db6c11 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -184,7 +184,8 @@ async def create_proxy( console.print( f" You can add this to your config with {arg_start}" f"btcli config add-proxy " - f"--name --address {created_pure} --proxy-type {created_proxy_type.value}" + f"--name --address {created_pure} --proxy-type {created_proxy_type.value} " + f"--delay {delay} --spawner {created_spawner}" f"{arg_end}" ) else: From 87cb7117b7771f24fd0bb359036880a9effc3922 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 15:51:35 +0200 Subject: [PATCH 065/179] Updated text --- bittensor_cli/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index cff89dff5..5afea9530 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2009,6 +2009,7 @@ def config_update_proxy( help="The SS58 address of the spawner", ), ] = None, + delay: Optional[int] = typer.Option(None, help="Delay, in blocks."), note: Optional[str] = typer.Option(None, help="Any notes about this entry"), ): if name not in self.proxies: @@ -2028,6 +2029,7 @@ def config_update_proxy( proxy_type=proxy_type_val, spawner=spawner, note=note, + delay=delay, ) console.print("Proxy updated") self.config_get_proxies() From 744bdf648aea9ffdc754f3887618a969c80b8f56 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 15:54:39 +0200 Subject: [PATCH 066/179] Added delay confirmation to create/add --- bittensor_cli/src/commands/proxy.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index df2db6c11..d78acd46f 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -136,6 +136,12 @@ async def create_proxy( f"This will create a Pure Proxy of type {proxy_type.value}. Do you want to proceed?", ): return False, "", "", "" + if delay > 0: + if not Confirm.ask( + f"By adding a non-zero delay ({delay}), all proxy calls must be announced " + f"{delay} blocks before they will be able to be made. Continue?" + ): + return False, "", "", "" if not (ulw := unlock_key(wallet, print_out=not json_output)).success: if not json_output: err_console.print(ulw.message) @@ -301,6 +307,12 @@ async def add_proxy( f"Do you want to proceed?" ): return None + if delay > 0: + if not Confirm.ask( + f"By adding a non-zero delay ({delay}), all proxy calls must be announced " + f"{delay} blocks before they will be able to be made. Continue?" + ): + return False, "", "", "" if not (ulw := unlock_key(wallet, print_out=not json_output)).success: if not json_output: err_console.print(ulw.message) From 0e0a1edb7c8a2fec269216a06589242b5013919b Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 18:12:22 +0200 Subject: [PATCH 067/179] More announcement work --- .../src/bittensor/subtensor_interface.py | 8 +- bittensor_cli/src/bittensor/utils.py | 23 +++- bittensor_cli/src/commands/proxy.py | 120 +++++++++++++++++- 3 files changed, 138 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 6ab77eb89..d6d5aa702 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1213,12 +1213,8 @@ async def sign_and_send_extrinsic( address=proxy, epoch_time=int(time.time()), block=block, - call_hash=call.call_hash, - call_args={ - "module": call.call_module, - "function": call.call_function, - "args": call.call_args, - }, + call_hash=call.call_hash.hex(), + call=call, ) console.print( f"Added entry {call.call_hash} at block {block} to your ProxyAnnouncements address book." diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 42bf2f441..8158b2606 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -24,6 +24,7 @@ from numpy.typing import NDArray from rich.console import Console from rich.prompt import Prompt +from scalecodec import GenericCall from scalecodec.utils.ss58 import ss58_encode, ss58_decode import typer @@ -406,6 +407,15 @@ def is_valid_ss58_address(address: str) -> bool: return False +def is_valid_ss58_address_prompt(text: str) -> str: + valid = False + address = "" + while not valid: + address = Prompt.ask(text).strip() + valid = is_valid_ss58_address(address) + return address + + def is_valid_ed25519_pubkey(public_key: Union[str, bytes]) -> bool: """ Checks if the given public_key is a valid ed25519 key. @@ -1022,7 +1032,8 @@ class ProxyAnnouncements(TableDefinition): ("epoch_time", "INTEGER"), ("block", "INTEGER"), ("call_hash", "TEXT"), - ("call_args", "TEXT"), + ("call", "TEXT"), + ("call_serialized", "TEXT"), ) @classmethod @@ -1035,12 +1046,14 @@ def add_entry( epoch_time: int, block: int, call_hash: str, - call_args: dict, + call: GenericCall, ) -> None: - call_args_ = json.dumps(call_args) + call_hex = call.data.to_hex() + call_serialized = call.serialize() conn.execute( - f"INSERT INTO {cls.name} (address, epoch_time, block, call_hash, call_args) VALUES (?, ?, ?, ?, ?)", - (address, epoch_time, block, call_hash, call_args_), + f"INSERT INTO {cls.name} (address, epoch_time, block, call_hash, call, call_serialized)" + " VALUES (?, ?, ?, ?, ?, ?)", + (address, epoch_time, block, call_hash, call_hex, call_serialized), ) conn.commit() diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index d78acd46f..2dd0d2642 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -1,10 +1,11 @@ from typing import TYPE_CHECKING, Optional import sys -from rich.prompt import Confirm, Prompt -from scalecodec import GenericCall +from rich.prompt import Confirm, Prompt, FloatPrompt, IntPrompt +from scalecodec import GenericCall, ScaleBytes from bittensor_cli.src import COLORS +from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( print_extrinsic_id, json_console, @@ -12,6 +13,7 @@ err_console, unlock_key, ProxyAddressBook, + is_valid_ss58_address_prompt, ) if TYPE_CHECKING: @@ -472,3 +474,117 @@ async def kill_proxy( json_output=json_output, proxy=proxy, ) + + +async def execute_announced( + subtensor: "SubtensorInterface", + wallet: "Wallet", + delegate: str, + real: str, + period: int, + call_hex: Optional[str], + delay: int = 0, + created_block: Optional[int] = None, + prompt: bool = True, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = False, +): + if prompt and created_block is not None: + current_block = await subtensor.substrate.get_block_number() + if current_block - delay > created_block: + if not Confirm.ask( + f"The delay for this account is set to {delay} blocks, but the call was created" + f" at block {created_block}. It is currently only {current_block}. The call will likely fail." + f" Do you want to proceed?" + ): + return None + + if call_hex is None: + if not prompt: + err_console.print( + f":cross_mark:[red]You have not provided a call, and are using" + f" [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}], so we are unable to request" + f"the information to craft this call." + ) + return None + else: + call_args = {} + failure_ = f"Instead create the call using btcli commands with [{COLORS.G.ARG}]--announce-only[/{COLORS.G.ARG}]" + block_hash = await subtensor.substrate.get_chain_head() + fns = await subtensor.substrate.get_metadata_call_functions( + block_hash=block_hash + ) + module = Prompt.ask( + "Enter the module name for the call", + choices=list(fns.keys()), + show_choices=True, + ) + call_fn = Prompt.ask( + "Enter the call function for the call", + choices=list(fns[module].keys()), + show_choices=True, + ) + for arg in fns[module][call_fn].keys(): + type_name = fns[module][call_fn][arg]["typeName"] + if type_name == "AccountIdLookupOf": + value = is_valid_ss58_address_prompt( + f"Enter the SS58 Address for {arg}" + ) + elif type_name == "T::Balance": + value = FloatPrompt.ask(f"Enter the amount of Tao for {arg}") + value = Balance.from_tao(value) + elif "RuntimeCall" in type_name: + err_console.print( + f":cross_mark:[red]Unable to craft a Call Type for arg {arg}. {failure_}" + ) + return None + elif type_name == "NetUid": + value = IntPrompt.ask(f"Enter the netuid for {arg}") + elif type_name in ("u16", "u64"): + value = IntPrompt.ask(f"Enter the int value for {arg}") + elif type_name == "bool": + value = Prompt.ask( + f"Enter the bool value for {arg}", + choices=["True", "False"], + show_choices=True, + ) + if value == "True": + value = True + else: + value = False + else: + err_console.print( + f":cross_mark:[red]Unrecogized type name {type_name}. {failure_}" + ) + return None + call_args[arg] = value + inner_call = await subtensor.substrate.compose_call( + module, + call_fn, + call_params=call_args, + block_hash=block_hash, + ) + else: + runtime = await subtensor.substrate.init_runtime(block_id=created_block) + inner_call = GenericCall( + data=ScaleBytes(data=bytes.fromhex(call_hex)), metadata=runtime.metadata + ) + inner_call.process() + + announced_call = await subtensor.substrate.compose_call( + "Proxy", + "proxy_announced", + { + "delegate": delegate, + "real": real, + "call": inner_call, + "force_proxy_type": None, + }, + ) + return await subtensor.sign_and_send_extrinsic( + call=announced_call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + era={"period": period}, + ) From 70ec7ed7ebb41df522da44192c1fef091bbb8517 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 18:16:28 +0200 Subject: [PATCH 068/179] Typo --- bittensor_cli/src/commands/proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 2dd0d2642..1f3f9ce05 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -554,7 +554,7 @@ async def execute_announced( value = False else: err_console.print( - f":cross_mark:[red]Unrecogized type name {type_name}. {failure_}" + f":cross_mark:[red]Unrecognized type name {type_name}. {failure_}" ) return None call_args[arg] = value @@ -567,7 +567,7 @@ async def execute_announced( else: runtime = await subtensor.substrate.init_runtime(block_id=created_block) inner_call = GenericCall( - data=ScaleBytes(data=bytes.fromhex(call_hex)), metadata=runtime.metadata + data=ScaleBytes(data=bytearray.fromhex(call_hex)), metadata=runtime.metadata ) inner_call.process() From 6bd022cefd8941fb2a8ffc3097742c0e8e3bd1f6 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 19:55:37 +0200 Subject: [PATCH 069/179] Proxy announce executed --- bittensor_cli/cli.py | 153 +++++++++++++++++- .../src/bittensor/subtensor_interface.py | 4 +- bittensor_cli/src/bittensor/utils.py | 2 +- 3 files changed, 155 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5afea9530..16c4cce25 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2,6 +2,7 @@ import asyncio import copy import curses +import datetime import importlib import json import logging @@ -69,6 +70,7 @@ get_hotkey_pub_ss58, ensure_address_book_tables_exist, ProxyAddressBook, + ProxyAnnouncements, ) from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds @@ -1526,11 +1528,12 @@ def main_callback( with ProxyAddressBook.get_db() as (conn, cursor): rows = ProxyAddressBook.read_rows(conn, cursor, include_header=False) proxies = {} - for name, ss58_address, _, spawner, proxy_type, _ in rows: + for name, ss58_address, delay, spawner, proxy_type, _ in rows: proxies[name] = { "address": ss58_address, "spawner": spawner, "proxy_type": proxy_type, + "delay": delay, } self.proxies = proxies @@ -8796,7 +8799,6 @@ def proxy_kill( network: Optional[list[str]] = Options.network, proxy_type: ProxyType = Options.proxy_type, proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, idx: int = typer.Option(0, "--index", help="TODO lol"), wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, @@ -8861,6 +8863,153 @@ def proxy_kill( ) ) + def proxy_execute_announced( + self, + proxy: str = Options.proxy, + call_hash: Optional[str] = typer.Option( + None, + help="The hash proxy call to execute", + ), + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + period: int = Options.period, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + self.verbosity_handler(quiet, verbose, json_output, prompt) + outer_proxy_from_config = self.proxies.get(proxy, {}) + proxy_from_config = outer_proxy_from_config.get("address") + delay = 0 + got_delay_from_config = False + if proxy_from_config is not None: + proxy = proxy_from_config + delay = outer_proxy_from_config["delay"] + got_delay_from_config = True + else: + if not is_valid_ss58_address(proxy): + raise typer.BadParameter( + f"proxy {proxy} is not a valid SS58 address or proxy address book name." + ) + proxy = self.is_valid_proxy_name_or_ss58(proxy, False) + 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, + ) + with ProxyAnnouncements.get_db() as (conn, cursor): + announcements = ProxyAnnouncements.read_rows(conn, cursor) + if not got_delay_from_config: + proxies = ProxyAddressBook.read_rows(conn, cursor) + else: + proxies = [] + potential_matches = [] + for row in proxies: + p_name, ss58_address, delay_, spawner, proxy_type, note = row + if proxy == ss58_address: + potential_matches.append(row) + if len(potential_matches) == 1: + delay = potential_matches[0][2] + elif len(potential_matches) > 1: + if not prompt: + err_console.print( + f":cross_mark:[red]Error: The proxy ss58 you provided: {proxy} matched the address book" + f" ambiguously (more than one match). To use this (rather than the address book name), you will " + f"have to use without {arg__('--no-prompt')}" + ) + return + else: + console.print( + f"The proxy ss58 you provided matches the address book ambiguously. The results will be" + f"iterated, for you to select your intended proxy." + ) + for row in potential_matches: + p_name, ss58_address, delay_, spawner, proxy_type, note = row + console.print( + f"Name: {p_name}\n" + f"Delay: {delay_}\n" + f"Spawner/Delegator: {spawner}\n" + f"Proxy Type: {proxy_type}\n" + f"Note: {note}\n" + ) + if Confirm.ask("Is this the intended proxy?"): + delay = delay_ + got_delay_from_config = True + if not got_delay_from_config: + verbose_console.print( + f"Unable to retrieve proxy from address book: {proxy}" + ) + call_hex = None + block = None + potential_call_matches = [] + for row in announcements: + ( + address, + epoch_time, + block_, + call_hash_, + call_hex_, + call_serialized, + ) = row + if call_hash_ == call_hash and address == proxy: + potential_call_matches.append(row) + if len(potential_call_matches) == 1: + block = potential_call_matches[0][2] + call_hex = potential_call_matches[0][4] + elif len(potential_call_matches) > 1: + if not prompt: + err_console.print( + f":cross_mark:[red]Error: The call hash you have provided matches {len(potential_call_matches)}" + f" possible entries. In order to choose which one, you will need to run " + f"without {arg__('--no-prompt')}" + ) + return + else: + console.print( + f"The call hash you have provided matches {len(potential_call_matches)}" + f" possible entries. The results will be iterated for you to selected your intended" + f"call." + ) + for row in potential_call_matches: + ( + address, + epoch_time, + block_, + call_hash_, + call_hex_, + call_serialized, + ) = row + console.print( + f"Time: {datetime.datetime.fromtimestamp(epoch_time)}\nCall:\n" + ) + console.print_json(call_serialized) + if Confirm.ask("Is this the intended call?"): + call_hex = call_hex_ + block = block_ + + return self._run_command( + proxy_commands.execute_announced( + subtensor=self.initialize_chain(network), + wallet=wallet, + delegate=proxy, + real=wallet.coldkeypub.ss58_address, + period=period, + call_hex=call_hex, + delay=delay, + created_block=block, + prompt=prompt, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + ) + @staticmethod def convert( from_rao: Optional[str] = typer.Option( diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index d6d5aa702..86ab150b2 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1177,7 +1177,9 @@ async def sign_and_send_extrinsic( if proxy is not None: if announce_only: call = await self.substrate.compose_call( - "Proxy", "announce", {"real": proxy, "call_hash": call.call_hash} + "Proxy", + "announce", + {"real": proxy, "call_hash": f"0x{call.call_hash}"}, ) else: call = await self.substrate.compose_call( diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 8158b2606..4ae0a59bb 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1049,7 +1049,7 @@ def add_entry( call: GenericCall, ) -> None: call_hex = call.data.to_hex() - call_serialized = call.serialize() + call_serialized = json.dumps(call.serialize()) conn.execute( f"INSERT INTO {cls.name} (address, epoch_time, block, call_hash, call, call_serialized)" " VALUES (?, ?, ?, ?, ?, ?)", From 7eca41feea5b568573df46bfae7ebef8d73641c7 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 20:01:06 +0200 Subject: [PATCH 070/179] Proxy announce execute improved --- bittensor_cli/cli.py | 68 +++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 16c4cce25..9a837e7ee 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8911,43 +8911,48 @@ def proxy_execute_announced( else: proxies = [] potential_matches = [] - for row in proxies: - p_name, ss58_address, delay_, spawner, proxy_type, note = row - if proxy == ss58_address: - potential_matches.append(row) - if len(potential_matches) == 1: - delay = potential_matches[0][2] - elif len(potential_matches) > 1: - if not prompt: - err_console.print( - f":cross_mark:[red]Error: The proxy ss58 you provided: {proxy} matched the address book" - f" ambiguously (more than one match). To use this (rather than the address book name), you will " - f"have to use without {arg__('--no-prompt')}" - ) - return - else: - console.print( - f"The proxy ss58 you provided matches the address book ambiguously. The results will be" - f"iterated, for you to select your intended proxy." - ) - for row in potential_matches: - p_name, ss58_address, delay_, spawner, proxy_type, note = row + if not got_delay_from_config: + for row in proxies: + p_name, ss58_address, delay_, spawner, proxy_type, note = row + if proxy == ss58_address: + potential_matches.append(row) + if len(potential_matches) == 1: + delay = potential_matches[0][2] + got_delay_from_config = True + elif len(potential_matches) > 1: + if not prompt: + err_console.print( + f":cross_mark:[red]Error: The proxy ss58 you provided: {proxy} matched the address book" + f" ambiguously (more than one match). To use this (rather than the address book name), you will " + f"have to use without {arg__('--no-prompt')}" + ) + return + else: console.print( - f"Name: {p_name}\n" - f"Delay: {delay_}\n" - f"Spawner/Delegator: {spawner}\n" - f"Proxy Type: {proxy_type}\n" - f"Note: {note}\n" + f"The proxy ss58 you provided matches the address book ambiguously. The results will be" + f"iterated, for you to select your intended proxy." ) - if Confirm.ask("Is this the intended proxy?"): - delay = delay_ - got_delay_from_config = True + for row in potential_matches: + p_name, ss58_address, delay_, spawner, proxy_type, note = row + console.print( + f"Name: {p_name}\n" + f"Delay: {delay_}\n" + f"Spawner/Delegator: {spawner}\n" + f"Proxy Type: {proxy_type}\n" + f"Note: {note}\n" + ) + if Confirm.ask("Is this the intended proxy?"): + delay = delay_ + got_delay_from_config = True + break + if not got_delay_from_config: verbose_console.print( f"Unable to retrieve proxy from address book: {proxy}" ) call_hex = None block = None + got_call_from_db = False potential_call_matches = [] for row in announcements: ( @@ -8963,6 +8968,7 @@ def proxy_execute_announced( if len(potential_call_matches) == 1: block = potential_call_matches[0][2] call_hex = potential_call_matches[0][4] + got_call_from_db = True elif len(potential_call_matches) > 1: if not prompt: err_console.print( @@ -8993,6 +8999,10 @@ def proxy_execute_announced( if Confirm.ask("Is this the intended call?"): call_hex = call_hex_ block = block_ + got_call_from_db = True + break + if not got_call_from_db: + console.print("Unable to retrieve call from DB. Proceeding without.") return self._run_command( proxy_commands.execute_announced( From f12f591f8e458734e43f47a2f490f0d862a80f70 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 21:17:35 +0200 Subject: [PATCH 071/179] Add ability to specify call hex --- bittensor_cli/cli.py | 103 ++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9a837e7ee..abfb13bf4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8870,6 +8870,9 @@ def proxy_execute_announced( None, help="The hash proxy call to execute", ), + call_hex: Optional[str] = typer.Option( + None, help="The hex of the call to specify" + ), network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, @@ -8950,59 +8953,59 @@ def proxy_execute_announced( verbose_console.print( f"Unable to retrieve proxy from address book: {proxy}" ) - call_hex = None block = None - got_call_from_db = False - potential_call_matches = [] - for row in announcements: - ( - address, - epoch_time, - block_, - call_hash_, - call_hex_, - call_serialized, - ) = row - if call_hash_ == call_hash and address == proxy: - potential_call_matches.append(row) - if len(potential_call_matches) == 1: - block = potential_call_matches[0][2] - call_hex = potential_call_matches[0][4] - got_call_from_db = True - elif len(potential_call_matches) > 1: - if not prompt: - err_console.print( - f":cross_mark:[red]Error: The call hash you have provided matches {len(potential_call_matches)}" - f" possible entries. In order to choose which one, you will need to run " - f"without {arg__('--no-prompt')}" - ) - return - else: - console.print( - f"The call hash you have provided matches {len(potential_call_matches)}" - f" possible entries. The results will be iterated for you to selected your intended" - f"call." - ) - for row in potential_call_matches: - ( - address, - epoch_time, - block_, - call_hash_, - call_hex_, - call_serialized, - ) = row + if not call_hex: + got_call_from_db = False + potential_call_matches = [] + for row in announcements: + ( + address, + epoch_time, + block_, + call_hash_, + call_hex_, + call_serialized, + ) = row + if call_hash_ == call_hash and address == proxy: + potential_call_matches.append(row) + if len(potential_call_matches) == 1: + block = potential_call_matches[0][2] + call_hex = potential_call_matches[0][4] + got_call_from_db = True + elif len(potential_call_matches) > 1: + if not prompt: + err_console.print( + f":cross_mark:[red]Error: The call hash you have provided matches {len(potential_call_matches)}" + f" possible entries. In order to choose which one, you will need to run " + f"without {arg__('--no-prompt')}" + ) + return + else: console.print( - f"Time: {datetime.datetime.fromtimestamp(epoch_time)}\nCall:\n" + f"The call hash you have provided matches {len(potential_call_matches)}" + f" possible entries. The results will be iterated for you to selected your intended" + f"call." ) - console.print_json(call_serialized) - if Confirm.ask("Is this the intended call?"): - call_hex = call_hex_ - block = block_ - got_call_from_db = True - break - if not got_call_from_db: - console.print("Unable to retrieve call from DB. Proceeding without.") + for row in potential_call_matches: + ( + address, + epoch_time, + block_, + call_hash_, + call_hex_, + call_serialized, + ) = row + console.print( + f"Time: {datetime.datetime.fromtimestamp(epoch_time)}\nCall:\n" + ) + console.print_json(call_serialized) + if Confirm.ask("Is this the intended call?"): + call_hex = call_hex_ + block = block_ + got_call_from_db = True + break + if not got_call_from_db: + console.print("Unable to retrieve call from DB. Proceeding without.") return self._run_command( proxy_commands.execute_announced( From 8b5c45786e3675826eef0b48f87946fadf9e78b2 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 21:19:42 +0200 Subject: [PATCH 072/179] Allow for 0x --- bittensor_cli/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index abfb13bf4..d6aa7529a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -9006,6 +9006,9 @@ def proxy_execute_announced( break if not got_call_from_db: console.print("Unable to retrieve call from DB. Proceeding without.") + else: + if call_hex[0:2] == "0x": + call_hex = call_hex[2:] return self._run_command( proxy_commands.execute_announced( From 197730e40d0499ce9a6ebfd042163692e7dea84b Mon Sep 17 00:00:00 2001 From: LeO <162738908+OxLeOx@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:00:54 -0500 Subject: [PATCH 073/179] Make `btcli st wizard` (#720) --- bittensor_cli/cli.py | 127 ++++++++ bittensor_cli/src/commands/stake/wizard.py | 323 +++++++++++++++++++++ 2 files changed, 450 insertions(+) create mode 100644 bittensor_cli/src/commands/stake/wizard.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 73e77736d..9ad6c89df 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -88,6 +88,7 @@ add as add_stake, remove as remove_stake, claim as claim_stake, + wizard as stake_wizard, ) from bittensor_cli.src.commands.subnets import ( price, @@ -971,6 +972,9 @@ def __init__(self): self.stake_app.command( "swap", rich_help_panel=HELP_PANELS["STAKE"]["MOVEMENT"] )(self.stake_swap) + self.stake_app.command( + "wizard", rich_help_panel=HELP_PANELS["STAKE"]["MOVEMENT"] + )(self.stake_wizard) self.stake_app.command( "set-claim", rich_help_panel=HELP_PANELS["STAKE"]["CLAIM"] )(self.stake_set_claim_type) @@ -5074,6 +5078,129 @@ def stake_swap( ) return result + def stake_wizard( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + period: int = Options.period, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Interactive wizard that guides you through stake movement operations. + + This wizard helps you understand and choose the right stake movement command: + - [bold]Move[/bold]: Move stake between hotkeys (same coldkey) + - [bold]Transfer[/bold]: Transfer stake between coldkeys (same hotkey) + - [bold]Swap[/bold]: Swap stake between subnets (same coldkey-hotkey pair) + + The wizard will: + 1. Explain the differences between each operation + 2. Help you select the appropriate operation + 3. Guide you through the selection process + 4. Execute the operation with your choices + + EXAMPLE + + Start the wizard: + [green]$[/green] btcli stake wizard + """ + self.verbosity_handler(quiet, verbose) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + subtensor = self.initialize_chain(network) + + wizard_result = self._run_command( + stake_wizard.stake_movement_wizard( + subtensor=subtensor, + wallet=wallet, + ), + exit_early=False, + ) + + if not wizard_result or not isinstance(wizard_result, dict): + return False + + operation = wizard_result.get("operation") + if not operation: + return False + + if operation == "move": + # Execute move operation + result, ext_id = self._run_command( + move_stake.move_stake( + subtensor=subtensor, + wallet=wallet, + origin_netuid=wizard_result["origin_netuid"], + origin_hotkey=wizard_result["origin_hotkey"], + destination_netuid=wizard_result["destination_netuid"], + destination_hotkey=wizard_result["destination_hotkey"], + amount=wizard_result.get("amount"), + stake_all=wizard_result.get("stake_all", False), + era=period, + interactive_selection=False, + prompt=prompt, + ) + ) + elif operation == "transfer": + # Execute transfer operation + dest_coldkey = wizard_result.get("destination_coldkey") + if not is_valid_ss58_address(dest_coldkey): + # Assume it's a wallet name + dest_wallet = self.wallet_ask( + dest_coldkey, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + dest_coldkey = dest_wallet.coldkeypub.ss58_address + + result, ext_id = self._run_command( + move_stake.transfer_stake( + wallet=wallet, + subtensor=subtensor, + origin_hotkey=wizard_result["origin_hotkey"], + origin_netuid=wizard_result["origin_netuid"], + dest_netuid=wizard_result["destination_netuid"], + dest_coldkey_ss58=dest_coldkey, + amount=wizard_result.get("amount"), + stake_all=wizard_result.get("stake_all", False), + era=period, + interactive_selection=False, + prompt=prompt, + ) + ) + elif operation == "swap": + # Execute swap operation + result, ext_id = self._run_command( + move_stake.swap_stake( + wallet=wallet, + subtensor=subtensor, + origin_netuid=wizard_result["origin_netuid"], + destination_netuid=wizard_result["destination_netuid"], + amount=wizard_result.get("amount"), + swap_all=False, + era=period, + interactive_selection=False, + prompt=prompt, + ) + ) + else: + print_error(f"Unknown operation: {operation}") + return False + return result + def stake_get_children( self, wallet_name: Optional[str] = Options.wallet_name, diff --git a/bittensor_cli/src/commands/stake/wizard.py b/bittensor_cli/src/commands/stake/wizard.py new file mode 100644 index 000000000..f1886f65e --- /dev/null +++ b/bittensor_cli/src/commands/stake/wizard.py @@ -0,0 +1,323 @@ +""" +Wizard command for guiding users through stake movement operations. + +This module provides an interactive wizard that helps users understand and select +the appropriate stake movement command (move, transfer, or swap) based on their needs. +""" + +import asyncio +from typing import TYPE_CHECKING, Optional + +from bittensor_wallet import Wallet +from rich.prompt import Prompt +from rich.table import Table +from rich.panel import Panel + +from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.utils import ( + console, + print_error, + is_valid_ss58_address, + get_hotkey_pub_ss58, + group_subnets, + get_hotkey_wallets_for_wallet, +) +from bittensor_cli.src.commands.stake.move import ( + stake_move_transfer_selection, + stake_swap_selection, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def stake_movement_wizard( + subtensor: "SubtensorInterface", + wallet: Wallet, +) -> Optional[dict]: + """ + Interactive wizard that guides users through stake movement operations. + + This wizard helps users understand the differences between: + - move: Move stake between hotkeys (same coldkey) + - transfer: Transfer stake between coldkeys (same hotkey) + - swap: Swap stake between subnets (same coldkey-hotkey pair) + + Args: + subtensor: SubtensorInterface object + wallet: Wallet object + + Returns: + dict: Contains the operation type and parameters needed to execute the operation + """ + + # Display welcome message and explanation + console.print("\n") + console.print( + Panel( + "[bold cyan]Stake Movement Wizard[/bold cyan]\n\n" + "This wizard will help you choose the right stake movement operation.\n" + "There are three types of stake movements:\n\n" + "[bold]1. Move[/bold] - Move stake between [blue]hotkeys[/blue] while keeping the same [blue]coldkey[/blue]\n" + " Example: Moving stake from hotkey A to hotkey B (both owned by your coldkey)\n\n" + "[bold]2. Transfer[/bold] - Transfer stake between [blue]coldkeys[/blue] while keeping the same [blue]hotkey[/blue]\n" + " Example: Transferring stake ownership from your coldkey to another coldkey (same hotkey)\n\n" + "[bold]3. Swap[/bold] - Swap stake between [blue]subnets[/blue] while keeping the same [blue]coldkey-hotkey pair[/blue]\n" + " Example: Moving stake from subnet 1 to subnet 2 (same wallet and hotkey)\n", + title="Welcome", + border_style="cyan", + ) + ) + + # Ask user what they want to do + operation_choice = Prompt.ask( + "\n[bold]What would you like to do?[/bold]", + choices=["1", "2", "3", "move", "transfer", "swap", "q"], + default="q", + ) + + if operation_choice.lower() == "q": + console.print("[yellow]Wizard cancelled.[/yellow]") + return None + + # Normalize choice + if operation_choice in ["1", "move"]: + operation = "move" + operation_name = "Move" + description = "Move stake between hotkeys (same coldkey)" + elif operation_choice in ["2", "transfer"]: + operation = "transfer" + operation_name = "Transfer" + description = "Transfer stake between coldkeys (same hotkey)" + elif operation_choice in ["3", "swap"]: + operation = "swap" + operation_name = "Swap" + description = "Swap stake between subnets (same coldkey-hotkey pair)" + else: + print_error("Invalid choice") + return None + + console.print(f"\n[bold green]Selected: {operation_name}[/bold green]") + console.print(f"[dim]{description}[/dim]\n") + + # Get stakes for the wallet + with console.status("Retrieving stake information..."): + stakes, ck_hk_identities, old_identities = await asyncio.gather( + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address + ), + subtensor.fetch_coldkey_hotkey_identities(), + subtensor.get_delegate_identities(), + ) + + # Filter stakes with actual amounts + available_stakes = [s for s in stakes if s.stake.tao > 0] + + if not available_stakes: + print_error("You have no stakes available to move.") + return None + + # Display available stakes + _display_available_stakes(available_stakes, ck_hk_identities, old_identities) + + # Guide user through the specific operation + if operation == "move": + return await _guide_move_operation( + subtensor, wallet, available_stakes, ck_hk_identities, old_identities + ) + elif operation == "transfer": + return await _guide_transfer_operation( + subtensor, wallet, available_stakes, ck_hk_identities, old_identities + ) + elif operation == "swap": + return await _guide_swap_operation(subtensor, wallet, available_stakes) + else: + raise ValueError(f"Unknown operation: {operation}") + + +def _display_available_stakes( + stakes: list, + ck_hk_identities: dict, + old_identities: dict, +): + """Display a table of available stakes.""" + # Group stakes by hotkey + hotkey_stakes = {} + for stake in stakes: + hotkey = stake.hotkey_ss58 + if hotkey not in hotkey_stakes: + hotkey_stakes[hotkey] = {} + hotkey_stakes[hotkey][stake.netuid] = stake.stake + + # Get identities + def get_identity(hotkey_ss58_: str) -> str: + if hk_identity := ck_hk_identities["hotkeys"].get(hotkey_ss58_): + return hk_identity.get("identity", {}).get("name", "") or hk_identity.get( + "display", "~" + ) + elif old_identity := old_identities.get(hotkey_ss58_): + return old_identity.display + return "~" + + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Your Available Stakes[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", + show_edge=False, + header_style="bold white", + border_style="bright_black", + title_justify="center", + ) + + table.add_column("Hotkey Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) + table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) + table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"]) + table.add_column("Total Stake", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) + + for hotkey_ss58, netuid_stakes in hotkey_stakes.items(): + identity = get_identity(hotkey_ss58) + netuids = sorted(netuid_stakes.keys()) + total_stake = sum( + netuid_stakes.values(), start=stakes[0].stake.__class__.from_tao(0) + ) + + table.add_row( + identity, + f"{hotkey_ss58[:8]}...{hotkey_ss58[-8:]}", + group_subnets(netuids), + str(total_stake), + ) + + console.print(table) + + +async def _guide_move_operation( + subtensor: "SubtensorInterface", + wallet: Wallet, + available_stakes: list, + ck_hk_identities: dict, + old_identities: dict, +) -> dict: + """Guide user through move operation.""" + console.print( + "\n[bold cyan]Move Operation[/bold cyan]\n" + "You will move stake from one hotkey to another hotkey.\n" + "Both hotkeys must be owned by the same coldkey (your wallet).\n" + ) + + try: + selection = await stake_move_transfer_selection(subtensor, wallet) + + # Get available hotkeys for destination + all_hotkeys = get_hotkey_wallets_for_wallet(wallet=wallet) + available_hotkeys = [ + (hk.hotkey_str, get_hotkey_pub_ss58(hk)) for hk in all_hotkeys + ] + + # Ask for destination hotkey + console.print("\n[bold]Destination Hotkey[/bold]") + if available_hotkeys: + console.print("\nAvailable hotkeys in your wallet:") + for idx, (name, ss58) in enumerate(available_hotkeys): + console.print(f" {idx}: {name} ({ss58[:8]}...{ss58[-8:]})") + + dest_choice = Prompt.ask( + "\nEnter the [blue]index[/blue] of the destination hotkey, or [blue]SS58 address[/blue]", + ) + + try: + dest_idx = int(dest_choice) + if 0 <= dest_idx < len(available_hotkeys): + dest_hotkey = available_hotkeys[dest_idx][1] + else: + raise ValueError("Invalid index") + except ValueError: + # Assume it's an SS58 address + if is_valid_ss58_address(dest_choice): + dest_hotkey = dest_choice + else: + print_error( + "Invalid hotkey selection. Please provide a valid index or SS58 address." + ) + raise ValueError("Invalid destination hotkey") + else: + dest_hotkey = Prompt.ask( + "Enter the [blue]destination hotkey[/blue] SS58 address" + ) + if not is_valid_ss58_address(dest_hotkey): + print_error("Invalid SS58 address") + raise ValueError("Invalid destination hotkey") + + return { + "operation": "move", + "origin_hotkey": selection["origin_hotkey"], + "origin_netuid": selection["origin_netuid"], + "destination_netuid": selection["destination_netuid"], + "destination_hotkey": dest_hotkey, + "amount": selection["amount"], + "stake_all": selection["stake_all"], + } + except ValueError: + raise + + +async def _guide_transfer_operation( + subtensor: "SubtensorInterface", + wallet: Wallet, + available_stakes: list, + ck_hk_identities: dict, + old_identities: dict, +) -> dict: + """Guide user through transfer operation.""" + console.print( + "\n[bold cyan]Transfer Operation[/bold cyan]\n" + "You will transfer stake ownership from one coldkey to another coldkey.\n" + "The hotkey remains the same, but ownership changes.\n" + "[yellow]Warning:[/yellow] Make sure the destination coldkey is not a validator hotkey.\n" + ) + + try: + selection = await stake_move_transfer_selection(subtensor, wallet) + + # Ask for destination coldkey + console.print("\n[bold]Destination Coldkey[/bold]") + dest_coldkey = Prompt.ask( + "Enter the [blue]destination coldkey[/blue] SS58 address or wallet name" + ) + + # Note: The CLI will handle wallet name resolution if it's not an SS58 address + + return { + "operation": "transfer", + "origin_hotkey": selection["origin_hotkey"], + "origin_netuid": selection["origin_netuid"], + "destination_netuid": selection["destination_netuid"], + "destination_coldkey": dest_coldkey, + "amount": selection["amount"], + "stake_all": selection["stake_all"], + } + except ValueError: + raise + + +async def _guide_swap_operation( + subtensor: "SubtensorInterface", + wallet: Wallet, + available_stakes: list, +) -> dict: + """Guide user through swap operation.""" + console.print( + "\n[bold cyan]Swap Operation[/bold cyan]\n" + "You will swap stake between subnets.\n" + "The same coldkey-hotkey pair is used, but stake moves between subnets.\n" + ) + + try: + selection = await stake_swap_selection(subtensor, wallet) + + return { + "operation": "swap", + "origin_netuid": selection["origin_netuid"], + "destination_netuid": selection["destination_netuid"], + "amount": selection["amount"], + } + except ValueError: + raise From 4491fe08c98abaa66793425f448609f4f66222da Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 25 Nov 2025 13:40:34 +0200 Subject: [PATCH 074/179] Added TODO --- bittensor_cli/src/bittensor/subtensor_interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 86ab150b2..027af1908 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2258,6 +2258,7 @@ async def get_subnet_price( :return: The current Alpha price in TAO units for the specified subnet. """ + # TODO update this to use the runtime call SwapRuntimeAPI.current_alpha_price current_sqrt_price = await self.query( module="Swap", storage_function="AlphaSqrtPrice", From 273742a0f7b808feddf9243050e8adab4135850e Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 25 Nov 2025 13:56:03 +0200 Subject: [PATCH 075/179] Added optionally specified `real` param to proxy execute announced. --- bittensor_cli/cli.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 17fec93ed..24ca260e4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8993,6 +8993,10 @@ def proxy_kill( def proxy_execute_announced( self, proxy: str = Options.proxy, + real: Optional[str] = Options.edit_help( + "proxy", + "The real account making this call. If omitted, the wallet's coldkeypub ss58 is used." + ), call_hash: Optional[str] = typer.Option( None, help="The hash proxy call to execute", @@ -9034,6 +9038,7 @@ def proxy_execute_announced( ask_for=[WO.NAME, WO.PATH], validate=WV.WALLET, ) + real = self.is_valid_proxy_name_or_ss58(real, False) or wallet.coldkeypub.ss58_address with ProxyAnnouncements.get_db() as (conn, cursor): announcements = ProxyAnnouncements.read_rows(conn, cursor) if not got_delay_from_config: @@ -9142,7 +9147,7 @@ def proxy_execute_announced( subtensor=self.initialize_chain(network), wallet=wallet, delegate=proxy, - real=wallet.coldkeypub.ss58_address, + real=real, period=period, call_hex=call_hex, delay=delay, From 4b1d8a405b975b012021dc36bcc0e10086b08fad Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 25 Nov 2025 14:14:20 +0200 Subject: [PATCH 076/179] Updated balance check in w transfer to account for proxy. --- bittensor_cli/cli.py | 7 +- .../src/bittensor/extrinsics/transfer.py | 70 +++++++++++++------ 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 24ca260e4..503b6326e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8995,7 +8995,7 @@ def proxy_execute_announced( proxy: str = Options.proxy, real: Optional[str] = Options.edit_help( "proxy", - "The real account making this call. If omitted, the wallet's coldkeypub ss58 is used." + "The real account making this call. If omitted, the wallet's coldkeypub ss58 is used.", ), call_hash: Optional[str] = typer.Option( None, @@ -9038,7 +9038,10 @@ def proxy_execute_announced( ask_for=[WO.NAME, WO.PATH], validate=WV.WALLET, ) - real = self.is_valid_proxy_name_or_ss58(real, False) or wallet.coldkeypub.ss58_address + real = ( + self.is_valid_proxy_name_or_ss58(real, False) + or wallet.coldkeypub.ss58_address + ) with ProxyAnnouncements.get_db() as (conn, cursor): announcements = ProxyAnnouncements.read_rows(conn, cursor) if not got_delay_from_config: diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 6cf5580f4..4362f9524 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -124,36 +124,64 @@ async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt] # check existential deposit and fee print_verbose("Fetching existential and fee", status) block_hash = await subtensor.substrate.get_chain_head() - account_balance, existential_deposit = await asyncio.gather( + if proxy: + proxy_balance = await subtensor.get_balance(proxy, block_hash=block_hash) + account_balance, existential_deposit, fee = await asyncio.gather( subtensor.get_balance( - proxy or wallet.coldkeypub.ss58_address, block_hash=block_hash + wallet.coldkeypub.ss58_address, block_hash=block_hash ), subtensor.get_existential_deposit(block_hash=block_hash), + get_transfer_fee(), ) - fee = await get_transfer_fee() if allow_death: # Check if the transfer should keep alive the account existential_deposit = Balance(0) - if account_balance < (amount + fee + existential_deposit) and not allow_death: - err_console.print( - ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" - f" balance: [bright_cyan]{account_balance}[/bright_cyan]\n" - f" amount: [bright_cyan]{amount}[/bright_cyan]\n" - f" for fee: [bright_cyan]{fee}[/bright_cyan]\n" - f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n" - f"You can try again with `--allow-death`." - ) - return False, None - elif account_balance < (amount + fee) and allow_death: - print_error( - ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" - f" balance: [bright_red]{account_balance}[/bright_red]\n" - f" amount: [bright_red]{amount}[/bright_red]\n" - f" for fee: [bright_red]{fee}[/bright_red]" - ) - return False, None + if proxy: + if proxy_balance < (amount + existential_deposit) and not allow_death: + err_console.print( + ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" + f" balance: [bright_cyan]{proxy_balance}[/bright_cyan]\n" + f" amount: [bright_cyan]{amount}[/bright_cyan]\n" + f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n" + f"You can try again with `--allow-death`." + ) + return False, None + if account_balance < fee: + err_console.print( + ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" + f" balance: [bright_cyan]{account_balance}[/bright_cyan]\n" + f" fee: [bright_cyan]{fee}[/bright_cyan]\n" + f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n" + ) + return False, None + if account_balance < amount and allow_death: + print_error( + ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" + f" balance: [bright_red]{account_balance}[/bright_red]\n" + f" amount: [bright_red]{amount}[/bright_red]\n" + ) + return False, None + else: + if account_balance < (amount + fee + existential_deposit) and not allow_death: + err_console.print( + ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" + f" balance: [bright_cyan]{account_balance}[/bright_cyan]\n" + f" amount: [bright_cyan]{amount}[/bright_cyan]\n" + f" for fee: [bright_cyan]{fee}[/bright_cyan]\n" + f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n" + f"You can try again with `--allow-death`." + ) + return False, None + elif account_balance < (amount + fee) and allow_death: + print_error( + ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" + f" balance: [bright_red]{account_balance}[/bright_red]\n" + f" amount: [bright_red]{amount}[/bright_red]\n" + f" for fee: [bright_red]{fee}[/bright_red]" + ) + return False, None # Ask before moving on. if prompt: From e0251e28069189a86a4847ce7a543f7c44e80c86 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 25 Nov 2025 15:17:18 +0200 Subject: [PATCH 077/179] Added some unit tests --- .../src/bittensor/extrinsics/transfer.py | 2 + tests/unit_tests/test_cli.py | 429 ++++++++++++++++++ 2 files changed, 431 insertions(+) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 4362f9524..cbc53683a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -182,6 +182,8 @@ async def do_transfer() -> tuple[bool, str, str, Optional[AsyncExtrinsicReceipt] f" for fee: [bright_red]{fee}[/bright_red]" ) return False, None + if proxy: + account_balance = proxy_balance # Ask before moving on. if prompt: diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index a17ed8406..a4b2ed11d 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -228,3 +228,432 @@ def test_swap_hotkey_netuid_1_no_warning(mock_console): assert not any( "WARNING" in str(call) and "netuid 0" in str(call) for call in warning_calls ) + + +# ============================================================================ +# Tests for proxy parameter handling +# ============================================================================ + + +def test_is_valid_proxy_name_or_ss58_with_none_proxy(): + """Test that None proxy is accepted when announce_only is False""" + cli_manager = CLIManager() + result = cli_manager.is_valid_proxy_name_or_ss58(None, announce_only=False) + assert result is None + + +def test_is_valid_proxy_name_or_ss58_raises_with_announce_only_without_proxy(): + """Test that announce_only=True without proxy raises BadParameter""" + cli_manager = CLIManager() + with pytest.raises(typer.BadParameter) as exc_info: + cli_manager.is_valid_proxy_name_or_ss58(None, announce_only=True) + assert "Cannot supply '--announce-only' without supplying '--proxy'" in str( + exc_info.value + ) + + +def test_is_valid_proxy_name_or_ss58_with_valid_ss58(): + """Test that a valid SS58 address is accepted""" + cli_manager = CLIManager() + valid_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + result = cli_manager.is_valid_proxy_name_or_ss58(valid_ss58, announce_only=False) + assert result == valid_ss58 + + +def test_is_valid_proxy_name_or_ss58_with_invalid_ss58(): + """Test that an invalid SS58 address raises BadParameter""" + cli_manager = CLIManager() + invalid_ss58 = "invalid_address" + with pytest.raises(typer.BadParameter) as exc_info: + cli_manager.is_valid_proxy_name_or_ss58(invalid_ss58, announce_only=False) + assert "Invalid SS58 address" in str(exc_info.value) + + +def test_is_valid_proxy_name_or_ss58_with_proxy_from_config(): + """Test that a proxy name from config is resolved to SS58 address""" + cli_manager = CLIManager() + valid_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + cli_manager.proxies = {"my_proxy": {"address": valid_ss58}} + + result = cli_manager.is_valid_proxy_name_or_ss58("my_proxy", announce_only=False) + assert result == valid_ss58 + + +def test_is_valid_proxy_name_or_ss58_with_invalid_proxy_from_config(): + """Test that an invalid SS58 in config raises BadParameter""" + cli_manager = CLIManager() + cli_manager.proxies = {"my_proxy": {"address": "invalid_address"}} + + with pytest.raises(typer.BadParameter) as exc_info: + cli_manager.is_valid_proxy_name_or_ss58("my_proxy", announce_only=False) + assert "Invalid SS58 address" in str(exc_info.value) + assert "from config" in str(exc_info.value) + + +@patch("bittensor_cli.cli.is_valid_ss58_address") +def test_wallet_transfer_calls_proxy_validation(mock_is_valid_ss58): + """Test that wallet_transfer calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + mock_is_valid_ss58.return_value = True + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet_ask.return_value = Mock() + + cli_manager.wallet_transfer( + destination_ss58_address="5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + amount=10.0, + transfer_all=False, + allow_death=False, + period=100, + proxy=valid_proxy, + announce_only=False, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + network=None, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +@patch("bittensor_cli.cli.is_valid_ss58_address") +def test_wallet_transfer_with_announce_only_requires_proxy(mock_is_valid_ss58): + """Test that wallet_transfer with announce_only=True requires proxy""" + cli_manager = CLIManager() + mock_is_valid_ss58.return_value = True + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + ): + mock_wallet_ask.return_value = Mock() + + with pytest.raises(typer.BadParameter) as exc_info: + cli_manager.wallet_transfer( + destination_ss58_address="5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + amount=10.0, + transfer_all=False, + allow_death=False, + period=100, + proxy=None, + announce_only=True, # announce_only without proxy should fail + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + network=None, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + assert "Cannot supply '--announce-only' without supplying '--proxy'" in str( + exc_info.value + ) + + +def test_stake_add_calls_proxy_validation(): + """Test that stake_add calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch.object(cli_manager, "ask_safe_staking", return_value=False), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet_ask.return_value = Mock() + + cli_manager.stake_add( + stake_all=False, + amount=10.0, + include_hotkeys="", + exclude_hotkeys="", + all_hotkeys=False, + netuids="1", + all_netuids=False, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + proxy=valid_proxy, + announce_only=False, + network=None, + rate_tolerance=None, + safe_staking=False, + allow_partial_stake=None, + period=100, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_stake_remove_calls_proxy_validation(): + """Test that stake_remove calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch.object(cli_manager, "ask_safe_staking", return_value=False), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet_ask.return_value = Mock() + + cli_manager.stake_remove( + network=None, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + netuid=1, + all_netuids=False, + unstake_all=False, + unstake_all_alpha=False, + amount=10.0, + hotkey_ss58_address="", + include_hotkeys="", + exclude_hotkeys="", + all_hotkeys=False, + proxy=valid_proxy, + announce_only=False, + rate_tolerance=None, + safe_staking=False, + allow_partial_stake=None, + period=100, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_wallet_associate_hotkey_calls_proxy_validation(): + """Test that wallet_associate_hotkey calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + valid_hotkey = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch("bittensor_cli.cli.is_valid_ss58_address", return_value=True), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet = Mock() + mock_wallet.name = "test_wallet" + mock_wallet_ask.return_value = mock_wallet + + cli_manager.wallet_associate_hotkey( + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey=valid_hotkey, + network=None, + proxy=valid_proxy, + announce_only=False, + prompt=False, + quiet=True, + verbose=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_wallet_set_id_calls_proxy_validation(): + """Test that wallet_set_id calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet_ask.return_value = Mock() + + cli_manager.wallet_set_id( + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + network=None, + name="Test Name", + web_url="https://example.com", + image_url="https://example.com/image.png", + discord="testuser", + description="Test description", + additional="Additional info", + github_repo="test/repo", + proxy=valid_proxy, + announce_only=False, + quiet=True, + verbose=False, + prompt=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_wallet_swap_coldkey_calls_proxy_validation(): + """Test that wallet_swap_coldkey calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + new_coldkey = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + patch("bittensor_cli.cli.is_valid_ss58_address", return_value=True), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet = Mock() + mock_wallet.coldkeypub = Mock() + mock_wallet.coldkeypub.ss58_address = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + mock_wallet_ask.return_value = mock_wallet + + cli_manager.wallet_swap_coldkey( + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + new_wallet_or_ss58=new_coldkey, + network=None, + proxy=valid_proxy, + announce_only=False, + quiet=True, + verbose=False, + force_swap=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_stake_move_calls_proxy_validation(): + """Test that stake_move calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + dest_hotkey = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command", return_value=(None, None)), + patch("bittensor_cli.cli.is_valid_ss58_address", return_value=True), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet = Mock() + mock_wallet.hotkey_str = "test_hotkey" + mock_wallet_ask.return_value = mock_wallet + + cli_manager.stake_move( + network=None, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + origin_netuid=1, + destination_netuid=2, + destination_hotkey=dest_hotkey, + amount=10.0, + stake_all=False, + proxy=valid_proxy, + announce_only=False, + period=100, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) + + +def test_stake_transfer_calls_proxy_validation(): + """Test that stake_transfer calls is_valid_proxy_name_or_ss58""" + cli_manager = CLIManager() + valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + dest_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command", return_value=(None, None)), + patch("bittensor_cli.cli.is_valid_ss58_address", return_value=True), + patch.object( + cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy + ) as mock_proxy_validation, + ): + mock_wallet = Mock() + mock_wallet.hotkey_str = "test_hotkey" + mock_wallet_ask.return_value = mock_wallet + + cli_manager.stake_transfer( + network=None, + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="test_hotkey", + origin_netuid=1, + dest_netuid=2, + dest_ss58=dest_ss58, + amount=10.0, + stake_all=False, + period=100, + proxy=valid_proxy, + announce_only=False, + prompt=False, + quiet=True, + verbose=False, + json_output=False, + ) + + # Assert that proxy validation was called + mock_proxy_validation.assert_called_once_with(valid_proxy, False) From cda34059446cfc8c74e3f52946c2c901bab3a3e3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 11:56:51 -0800 Subject: [PATCH 078/179] update get_coldkey_claim_type --- .../src/bittensor/subtensor_interface.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 054d67f7a..d8e185dd1 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1832,13 +1832,14 @@ async def get_coldkey_claim_type( coldkey_ss58: str, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> str: + ) -> dict: """ Retrieves the root claim type for a specific coldkey. Root claim types control how staking emissions are handled on the ROOT network (subnet 0): - "Swap": Future Root Alpha Emissions are swapped to TAO at claim time and added to your root stake - "Keep": Future Root Alpha Emissions are kept as Alpha + - "KeepSubnets": Specific subnets kept as Alpha, rest swapped to TAO Args: coldkey_ss58: The SS58 address of the coldkey to query. @@ -1846,7 +1847,10 @@ async def get_coldkey_claim_type( reuse_block: Whether to reuse the last-used blockchain block hash. Returns: - str: The root claim type for the coldkey ("Swap" or "Keep"). + dict: Claim type information in one of these formats: + - {"type": "Swap"} + - {"type": "Keep"} + - {"type": "KeepSubnets", "subnets": [1, 5, 10, ...]} """ result = await self.query( module="SubtensorModule", @@ -1857,8 +1861,16 @@ async def get_coldkey_claim_type( ) if result is None: - return "Swap" - return next(iter(result.keys())) + return {"type": "Swap"} + + claim_type_key = next(iter(result.keys())) + + if claim_type_key == "KeepSubnets": + subnets_data = result["KeepSubnets"]["subnets"] + subnet_list = sorted([subnet for subnet in subnets_data[0]]) + return {"type": "KeepSubnets", "subnets": subnet_list} + else: + return {"type": claim_type_key} async def get_all_coldkeys_claim_type( self, From 12696d733d38a69f3c61c15804a1106ca67913a4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 11:57:09 -0800 Subject: [PATCH 079/179] update get_all_coldkeys_claim_type --- .../src/bittensor/subtensor_interface.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index d8e185dd1..1fbe5dccd 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1876,7 +1876,7 @@ async def get_all_coldkeys_claim_type( self, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, str]: + ) -> dict[str, dict]: """ Retrieves all root claim types for all coldkeys in the network. @@ -1885,7 +1885,7 @@ async def get_all_coldkeys_claim_type( reuse_block: Whether to reuse the last-used blockchain block hash. Returns: - dict[str, str]: A dictionary mapping coldkey SS58 addresses to their root claim type ("Keep" or "Swap"). + dict[str, dict]: Mapping of coldkey SS58 addresses to claim type dicts """ result = await self.substrate.query_map( module="SubtensorModule", @@ -1896,10 +1896,20 @@ async def get_all_coldkeys_claim_type( ) root_claim_types = {} - async for coldkey, claim_type in result: + async for coldkey, claim_type_data in result: coldkey_ss58 = decode_account_id(coldkey[0]) - claim_type = next(iter(claim_type.value.keys())) - root_claim_types[coldkey_ss58] = claim_type + + claim_type_key = next(iter(claim_type_data.value.keys())) + + if claim_type_key == "KeepSubnets": + subnets_data = claim_type_data.value["KeepSubnets"]["subnets"] + subnet_list = sorted([subnet for subnet in subnets_data[0]]) + root_claim_types[coldkey_ss58] = { + "type": "KeepSubnets", + "subnets": subnet_list, + } + else: + root_claim_types[coldkey_ss58] = {"type": claim_type_key} return root_claim_types From dcd8b0ab0aa2cc37faec32e4df9b7521a37a917f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 12:07:30 -0800 Subject: [PATCH 080/179] adds parse_subnet_range --- bittensor_cli/src/bittensor/utils.py | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index f8e322f01..00f432930 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1051,6 +1051,55 @@ def group_subnets(registrations): return ", ".join(ranges) +def parse_subnet_range(input_str: str) -> list[int]: + """ + Parse subnet range input like "1-24, 30-40, 5". + + Args: + input_str: Comma-separated list of subnets and ranges + Examples: "1-5", "1,2,3", "1-5, 10, 20-25" + + Returns: + Sorted list of unique subnet IDs + + Raises: + ValueError: If input format is invalid + + Examples: + >>> parse_subnet_range("1-5, 10") + [1, 2, 3, 4, 5, 10] + >>> parse_subnet_range("5, 3, 1") + [1, 3, 5] + """ + subnets = set() + parts = [p.strip() for p in input_str.split(",") if p.strip()] + for part in parts: + if "-" in part: + try: + start, end = part.split("-", 1) + start_num = int(start.strip()) + end_num = int(end.strip()) + + if start_num > end_num: + raise ValueError(f"Invalid range '{part}': start must be ≤ end") + + if end_num - start_num > 128: + raise ValueError(f"Range '{part}' is too large (max 128 subnets)") + + subnets.update(range(start_num, end_num + 1)) + except ValueError as e: + if "invalid literal" in str(e): + raise ValueError(f"Invalid range '{part}': must be 'start-end'") + raise + else: + try: + subnets.add(int(part)) + except ValueError: + raise ValueError(f"Invalid subnet ID '{part}': must be a number") + + return sorted(subnets) + + def validate_chain_endpoint(endpoint_url) -> tuple[bool, str]: parsed = urlparse(endpoint_url) if parsed.scheme not in ("ws", "wss"): From b953cf0f0360884140557d57c1bffdce9f9efb06 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 12:08:05 -0800 Subject: [PATCH 081/179] adds total_networks call --- .../src/bittensor/subtensor_interface.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 1fbe5dccd..190251979 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -652,6 +652,26 @@ async def subnet_exists( ) return result + async def total_networks( + self, block_hash: Optional[str] = None, reuse_block: bool = False + ) -> int: + """ + Returns the total number of subnets in the Bittensor network. + + :param block_hash: The hash of the blockchain block number at which to check the subnet existence. + :param reuse_block: Whether to reuse the last-used block hash. + + :return: The total number of subnets in the network. + """ + result = await self.query( + module="SubtensorModule", + storage_function="TotalNetworks", + params=[], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return result + async def get_subnet_state( self, netuid: int, block_hash: Optional[str] = None ) -> Optional["SubnetState"]: From ff93c67ef58955ac23b680b299ef9b96239dbe46 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 14:41:40 -0800 Subject: [PATCH 082/179] adds netuid args to cmd --- bittensor_cli/cli.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9ad6c89df..7919d9b45 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -64,6 +64,7 @@ prompt_for_subnet_identity, validate_rate_tolerance, get_hotkey_pub_ss58, + parse_subnet_range, ) from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds @@ -7216,6 +7217,12 @@ def stake_set_claim_type( None, help="Claim type: 'keep' or 'swap'. If not provided, you'll be prompted to choose.", ), + netuids: Optional[str] = typer.Option( + None, + "--netuids", + "-n", + help="Netuids to select. Supports ranges and comma-separated values, e.g., '1-5,10,20-30'.", + ), wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, @@ -7233,12 +7240,15 @@ def stake_set_claim_type( [bold]Claim Types:[/bold] • [green]Swap[/green]: Future Root Alpha Emissions are swapped to TAO and added to root stake (default) • [yellow]Keep[/yellow]: Future Root Alpha Emissions are kept as Alpha tokens + • [cyan]Keep Specific[/cyan]: Keep specific subnets as Alpha, swap others to TAO USAGE: [green]$[/green] btcli stake claim [green]$[/green] btcli stake claim keep [green]$[/green] btcli stake claim swap + [green]$[/green] btcli stake claim keep --netuids 1-5,10,20-30 + [green]$[/green] btcli stake claim swap --netuids 1-30 With specific wallet: @@ -7246,6 +7256,14 @@ def stake_set_claim_type( """ self.verbosity_handler(quiet, verbose, json_output) + parsed_netuids = None + if netuids is not None: + try: + parsed_netuids = parse_subnet_range(netuids) + except ValueError as e: + err_console.print(f":cross_mark: [red]Invalid netuid format: {e}[/red]") + raise typer.Exit() + if claim_type is not None: claim_type_normalized = claim_type.capitalize() if claim_type_normalized not in ["Keep", "Swap"]: @@ -7267,6 +7285,7 @@ def stake_set_claim_type( wallet=wallet, subtensor=self.initialize_chain(network), claim_type=claim_type_normalized, + netuids=parsed_netuids, prompt=prompt, json_output=json_output, ) From 75591bac8a0846ec18187e64f14f6dae7ae1ed74 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 14:46:15 -0800 Subject: [PATCH 083/179] update cmd set_claim_type --- bittensor_cli/src/commands/stake/claim.py | 167 +++++++++++----------- 1 file changed, 87 insertions(+), 80 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 67147a82c..70323b3e0 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -4,6 +4,7 @@ from bittensor_wallet import Wallet from rich.prompt import Confirm, Prompt +from rich.panel import Panel from rich.table import Table, Column from rich import box @@ -16,6 +17,8 @@ print_extrinsic_id, json_console, millify_tao, + group_subnets, + parse_subnet_range, ) if TYPE_CHECKING: @@ -26,6 +29,7 @@ async def set_claim_type( wallet: Wallet, subtensor: "SubtensorInterface", claim_type: Optional[str] = None, + netuids: Optional[list[int]] = None, prompt: bool = True, json_output: bool = False, ) -> tuple[bool, str, Optional[str]]: @@ -35,11 +39,13 @@ async def set_claim_type( Root claim types control how staking emissions are handled on the ROOT network (subnet 0): - "Swap": Future Root Alpha Emissions are swapped to TAO at claim time and added to root stake - "Keep": Future Root Alpha Emissions are kept as Alpha tokens + - "KeepSubnets": Specific subnets kept as Alpha, rest swapped to TAO Args: wallet: Bittensor wallet object subtensor: SubtensorInterface object claim_type: Optional claim type ("Keep" or "Swap"). If None, user will be prompted. + netuids: Optional list of subnet IDs to keep (only valid with "Keep" type) prompt: Whether to prompt for user confirmation json_output: Whether to output JSON @@ -50,44 +56,86 @@ async def set_claim_type( - Optional[str]: Extrinsic identifier if successful """ - current_type = await subtensor.get_coldkey_claim_type( - coldkey_ss58=wallet.coldkeypub.ss58_address + current_claim_info, all_netuids = await asyncio.gather( + subtensor.get_coldkey_claim_type(coldkey_ss58=wallet.coldkeypub.ss58_address), + subtensor.get_all_subnet_netuids(), ) + all_subnets = sorted([n for n in all_netuids if n != 0]) claim_table = Table( + Column("[bold white]Coldkey", style=COLORS.GENERAL.COLDKEY, justify="left"), Column( - "[bold white]Coldkey", - style=COLORS.GENERAL.COLDKEY, - justify="left", - ), - Column( - "[bold white]Root Claim Type", - style=COLORS.GENERAL.SUBHEADING, - justify="center", + "[bold white]Current Type", style=COLORS.GENERAL.SUBHEADING, justify="left" ), show_header=True, - show_footer=False, - show_edge=True, border_style="bright_black", box=box.SIMPLE, - pad_edge=False, - width=None, - title=f"\n[{COLORS.GENERAL.HEADER}]Current root claim type:[/{COLORS.GENERAL.HEADER}]", + title=f"\n[{COLORS.GENERAL.HEADER}]Current Root Claim Type[/{COLORS.GENERAL.HEADER}]", ) claim_table.add_row( - wallet.coldkeypub.ss58_address, f"[yellow]{current_type}[/yellow]" + wallet.coldkeypub.ss58_address, + _format_claim_type_display(current_claim_info, all_subnets), ) console.print(claim_table) - new_type = ( - claim_type - if claim_type - else Prompt.ask( - "Select new root claim type", choices=["Swap", "Keep"], default=current_type - ) - ) - if new_type == current_type: - msg = f"Root claim type is already set to '{current_type}'. No change needed." + # Full wizard + if claim_type is None and netuids is None: + new_claim_info = await _ask_for_claim_types(wallet, subtensor, all_subnets) + if new_claim_info is None: + msg = "Operation cancelled." + console.print(f"[yellow]{msg}[/yellow]") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "message": msg, + "extrinsic_identifier": None, + } + ) + ) + return False, msg, None + + # Keep netuids passed thru the cli and assume Keep type + elif claim_type is None and netuids is not None: + new_claim_info = {"type": "KeepSubnets", "subnets": netuids} + + else: + # Keep or Swap all subnets + claim_type_upper = claim_type.capitalize() + if claim_type_upper not in ["Swap", "Keep"]: + msg = f"Invalid claim type: {claim_type}. Use 'Swap' or 'Keep', or omit for interactive mode." + err_console.print(msg) + if json_output: + json_console.print(json.dumps({"success": False, "message": msg})) + return False, msg, None + + # Netuids passed with Keep type + if netuids is not None and claim_type_upper == "Keep": + new_claim_info = {"type": "KeepSubnets", "subnets": netuids} + + # Netuids passed with Swap type + elif netuids is not None and claim_type_upper == "Swap": + keep_subnets = [n for n in all_subnets if n not in netuids] + invalid = [n for n in netuids if n not in all_subnets] + if invalid: + msg = f"Invalid subnets (not available): {group_subnets(invalid)}" + err_console.print(msg) + if json_output: + json_console.print(json.dumps({"success": False, "message": msg})) + return False, msg, None + + if not keep_subnets: + new_claim_info = {"type": "Swap"} + elif set(keep_subnets) == set(all_subnets): + new_claim_info = {"type": "Keep"} + else: + new_claim_info = {"type": "KeepSubnets", "subnets": keep_subnets} + else: + new_claim_info = {"type": claim_type_upper} + + if _claim_types_equal(current_claim_info, new_claim_info): + msg = f"Claim type already set to {_format_claim_type_display(new_claim_info)}. No change needed." console.print(f"[yellow]{msg}[/yellow]") if json_output: json_console.print( @@ -96,8 +144,6 @@ async def set_claim_type( "success": True, "message": msg, "extrinsic_identifier": None, - "old_type": current_type, - "new_type": current_type, } ) ) @@ -105,59 +151,33 @@ async def set_claim_type( if prompt: console.print( - f"\n[bold]Changing root claim type from '{current_type}' -> '{new_type}'[/bold]\n" - ) - - if new_type == "Swap": - console.print( - "[yellow]Note:[/yellow] With 'Swap', future root alpha emissions will be swapped to TAO and added to root stake." - ) - else: - console.print( - "[yellow]Note:[/yellow] With 'Keep', future root alpha emissions will be kept as Alpha tokens." + Panel( + f"[{COLORS.GENERAL.HEADER}]Confirm Claim Type Change[/{COLORS.GENERAL.HEADER}]\n\n" + f"[yellow]FROM:[/yellow] {_format_claim_type_display(current_claim_info, all_subnets)}\n\n" + f"[yellow]TO:[/yellow] {_format_claim_type_display(new_claim_info, all_subnets)}" ) + ) - if not Confirm.ask("\nDo you want to proceed?"): + if not Confirm.ask("\nProceed with this change?"): msg = "Operation cancelled." console.print(f"[yellow]{msg}[/yellow]") if json_output: - json_console.print( - json.dumps( - { - "success": False, - "message": msg, - "extrinsic_identifier": None, - "old_type": current_type, - "new_type": new_type, - } - ) - ) + json_console.print(json.dumps({"success": False, "message": msg})) return False, msg, None if not (unlock := unlock_key(wallet)).success: msg = f"Failed to unlock wallet: {unlock.message}" err_console.print(f":cross_mark: [red]{msg}[/red]") if json_output: - json_console.print( - json.dumps( - { - "success": False, - "message": msg, - "extrinsic_identifier": None, - "old_type": current_type, - "new_type": new_type, - } - ) - ) + json_console.print(json.dumps({"success": False, "message": msg})) return False, msg, None - with console.status( - f":satellite: Setting root claim type to '{new_type}'...", spinner="earth" - ): + with console.status(":satellite: Setting root claim type...", spinner="earth"): + claim_type_param = _prepare_claim_type_args(new_claim_info) call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="set_root_claim_type", - call_params={"new_root_claim_type": new_type}, + call_params={"new_root_claim_type": claim_type_param}, ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet @@ -165,7 +185,7 @@ async def set_claim_type( if success: ext_id = await ext_receipt.get_extrinsic_identifier() - msg = f"Successfully set root claim type to '{new_type}'" + msg = f"Successfully set claim type to {_format_claim_type_display(new_claim_info)}" console.print(f":white_heavy_check_mark: [green]{msg}[/green]") await print_extrinsic_id(ext_receipt) if json_output: @@ -175,28 +195,15 @@ async def set_claim_type( "success": True, "message": msg, "extrinsic_identifier": ext_id, - "old_type": current_type, - "new_type": new_type, } ) ) return True, msg, ext_id - else: - msg = f"Failed to set root claim type: {err_msg}" + msg = f"Failed to set claim type: {err_msg}" err_console.print(f":cross_mark: [red]{msg}[/red]") if json_output: - json_console.print( - json.dumps( - { - "success": False, - "message": msg, - "extrinsic_identifier": None, - "old_type": current_type, - "new_type": new_type, - } - ) - ) + json_console.print(json.dumps({"success": False, "message": msg})) return False, msg, None From c8cb1a5b1d3b63bb0df6e4a7385551998c4b997e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 14:46:52 -0800 Subject: [PATCH 084/179] add _ask_for_claim_types --- bittensor_cli/src/commands/stake/claim.py | 57 +++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 70323b3e0..fe0fb0a69 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -486,3 +486,60 @@ def _print_claimable_table( first_row = False console.print(table) + + +async def _ask_for_claim_types( + wallet: Wallet, + subtensor: "SubtensorInterface", + all_subnets: list, +) -> Optional[dict]: + """ + Interactive prompts for claim type selection. + + Flow: + 1. Ask "Keep or Swap?" + 2. Ask "All subnets?" + - If yes → return simple type (Keep or Swap) + - If no → enter subnet selection + + Returns: + dict: Selected claim type, or None if cancelled + """ + + console.print("\n") + console.print( + Panel( + f"[{COLORS.GENERAL.HEADER}]Root Claim Type Selection[/{COLORS.GENERAL.HEADER}]\n\n" + "Configure how your root network emissions are claimed.\n\n" + "[yellow]Options:[/yellow]\n" + " • [green]Swap[/green] - Convert emissions to TAO\n" + " • [green]Keep[/green] - Keep emissions as Alpha\n" + " • [green]Keep Specific[/green] - Keep selected subnets, swap others\n", + ) + ) + + primary_choice = Prompt.ask( + "\nSelect new root claim type", + choices=["keep", "swap", "cancel"], + default="cancel", + ) + if primary_choice == "cancel": + return None + + apply_to_all = Confirm.ask("\nApply to ALL subnets?", default=True) + + if apply_to_all: + return {"type": primary_choice.capitalize()} + + if primary_choice == "keep": + console.print( + "\nYou can select which subnets to KEEP as Alpha (others will be swapped to TAO).\n" + ) + else: + console.print( + "\nYou can select which subnets to SWAP to TAO (others will be kept as Alpha).\n" + ) + + return await _prompt_claim_netuids( + wallet, subtensor, all_subnets, mode=primary_choice + ) From b36b7e45790fe379317bbbaa57815771023f8e20 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 14:47:13 -0800 Subject: [PATCH 085/179] add _prompt_claim_netuids --- bittensor_cli/src/commands/stake/claim.py | 83 +++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index fe0fb0a69..d9e16e376 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -543,3 +543,86 @@ async def _ask_for_claim_types( return await _prompt_claim_netuids( wallet, subtensor, all_subnets, mode=primary_choice ) + + +async def _prompt_claim_netuids( + wallet: Wallet, + subtensor: "SubtensorInterface", + all_subnets: list, + mode: str = "keep", +) -> Optional[dict]: + """ + Interactive subnet selection. + + Args: + mode: "keep" to select subnets to keep as Alpha, "swap" to select subnets to swap to TAO + + Returns: + dict: KeepSubnets claim type or None if cancelled + """ + + if not all_subnets: + console.print("[yellow]No subnets available.[/yellow]") + return {"type": "Swap"} + + if mode == "keep": + action = "KEEP as Alpha" + else: + action = "SWAP to TAO" + + console.print( + Panel( + f"[{COLORS.GENERAL.HEADER}]Subnet Selection[/{COLORS.GENERAL.HEADER}]\n\n" + f"[bold]Available subnets:[/bold] {group_subnets(sorted(all_subnets))}\n" + f"[dim]Total: {len(all_subnets)} subnets[/dim]\n\n" + "[yellow]Input examples:[/yellow]\n" + " • [cyan]1-10[/cyan] - Range from 1 to 10\n" + " • [cyan]1, 5, 10[/cyan] - Specific subnets\n" + " • [cyan]1-10, 20-30, 50[/cyan] - Mixed" + ) + ) + + while True: + subnet_input = Prompt.ask( + f"\nEnter subnets to {action} [dim]{group_subnets(sorted(all_subnets))}", + default="", + ) + + if not subnet_input.strip(): + err_console.print("[red]No subnets entered. Please try again.[/red]") + continue + + try: + selected = parse_subnet_range(subnet_input) + invalid = [s for s in selected if s not in all_subnets] + if invalid: + err_console.print( + f"[red]Invalid subnets (not available): {group_subnets(invalid)}[/red]" + ) + err_console.print("[yellow]Please try again.[/yellow]") + continue + + if mode == "keep": + keep_subnets = selected + else: + keep_subnets = [n for n in all_subnets if n not in selected] + + if _preview_subnet_selection(keep_subnets, all_subnets): + if not keep_subnets: + return {"type": "Swap"} + elif set(keep_subnets) == set(all_subnets): + return {"type": "Keep"} + else: + return {"type": "KeepSubnets", "subnets": keep_subnets} + else: + console.print( + "[yellow]Selection cancelled. Starting over...[/yellow]\n" + ) + return await _prompt_claim_netuids( + wallet, subtensor, all_subnets, mode=mode + ) + + except ValueError as e: + err_console.print( + f"Invalid subnet selection: {e}\n[yellow]Please try again." + ) From 356d74121348ccbf42a069a802f247068b8614a5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 14:47:29 -0800 Subject: [PATCH 086/179] add _preview_subnet_selection --- bittensor_cli/src/commands/stake/claim.py | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index d9e16e376..9094bb90e 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -626,3 +626,32 @@ async def _prompt_claim_netuids( err_console.print( f"Invalid subnet selection: {e}\n[yellow]Please try again." ) + + +def _preview_subnet_selection(keep_subnets: list[int], all_subnets: list[int]) -> bool: + """Show preview and ask for confirmation.""" + + swap_subnets = [n for n in all_subnets if n not in keep_subnets] + preview_content = ( + f"[{COLORS.GENERAL.HEADER}]Preview Your Selection[/{COLORS.GENERAL.HEADER}]\n\n" + ) + + if keep_subnets: + preview_content += ( + f"[green]✓ Keep as Alpha:[/green] {group_subnets(keep_subnets)}\n" + f"[dim] ({len(keep_subnets)} subnet{'s' if len(keep_subnets) != 1 else ''})[/dim]" + ) + else: + preview_content += "[dim]No subnets kept as Alpha[/dim]" + + if swap_subnets: + preview_content += ( + f"\n\n[yellow]⟳ Swap to TAO:[/yellow] {group_subnets(swap_subnets)}\n" + f"[dim] ({len(swap_subnets)} subnet{'s' if len(swap_subnets) != 1 else ''})[/dim]" + ) + else: + preview_content += "\n\n[dim]No subnets swapped to TAO[/dim]" + + console.print(Panel(preview_content)) + + return Confirm.ask("\nIs this correct?", default=True) From e887feb040e75925972e9e9268c6cec35b202421 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 14:47:45 -0800 Subject: [PATCH 087/179] _format_claim_type_display --- bittensor_cli/src/commands/stake/claim.py | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 9094bb90e..1b636189b 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -655,3 +655,39 @@ def _preview_subnet_selection(keep_subnets: list[int], all_subnets: list[int]) - console.print(Panel(preview_content)) return Confirm.ask("\nIs this correct?", default=True) + + +def _format_claim_type_display( + claim_info: dict, all_subnets: Optional[list[int]] = None +) -> str: + """ + Format claim type for human-readable display. + + Args: + claim_info: Claim type information dict + all_subnets: Optional list of all available subnets (for showing swap info) + """ + + claim_type = claim_info["type"] + if claim_type == "Swap": + return "[yellow]Swap All[/yellow]" + + elif claim_type == "Keep": + return "[dark_sea_green3]Keep All[/dark_sea_green3]" + + elif claim_type == "KeepSubnets": + subnets = claim_info["subnets"] + subnet_display = group_subnets(subnets) + + result = ( + f"[cyan]Keep Specific[/cyan]\n[green] ✓ Keep:[/green] {subnet_display}" + ) + if all_subnets: + swap_subnets = [n for n in all_subnets if n not in subnets] + if swap_subnets: + swap_display = group_subnets(swap_subnets) + result += f"\n[yellow] ⟳ Swap:[/yellow] {swap_display}" + + return result + else: + return "[red]Unknown[/red]" From 9929a9ed92e47bc499fa9784b932daa1521187a4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 14:47:57 -0800 Subject: [PATCH 088/179] _claim_types_equal --- bittensor_cli/src/commands/stake/claim.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 1b636189b..8215db575 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -691,3 +691,17 @@ def _format_claim_type_display( return result else: return "[red]Unknown[/red]" + + +def _claim_types_equal(claim1: dict, claim2: dict) -> bool: + """Check if two claim type configs are equivalent.""" + + if claim1["type"] != claim2["type"]: + return False + + if claim1["type"] == "KeepSubnets": + subnets1 = sorted(claim1.get("subnets", [])) + subnets2 = sorted(claim2.get("subnets", [])) + return subnets1 == subnets2 + + return True From ebf7cc7ffb9e3bbb2e6d5eef5bea7732edac795b Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 14:48:03 -0800 Subject: [PATCH 089/179] _prepare_claim_type_args --- bittensor_cli/src/commands/stake/claim.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 8215db575..e39248c62 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -705,3 +705,18 @@ def _claim_types_equal(claim1: dict, claim2: dict) -> bool: return subnets1 == subnets2 return True + + +def _prepare_claim_type_args(claim_info: dict) -> dict: + """Convert claim type arguments for chain call""" + + claim_type = claim_info["type"] + if claim_type == "Swap": + return {"Swap": None} + elif claim_type == "Keep": + return {"Keep": None} + elif claim_type == "KeepSubnets": + subnets = claim_info["subnets"] + return {"KeepSubnets": {"subnets": subnets}} + else: + raise ValueError(f"Unknown claim type: {claim_type}") From 202aaf41f432f18863e3bda293a2dfd09a5f73b4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 16:10:19 -0800 Subject: [PATCH 090/179] format_claim_type_for_root --- bittensor_cli/src/commands/subnets/subnets.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index c2346880b..e8499bdae 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -49,6 +49,35 @@ # helpers and extrinsics +def format_claim_type_for_root(claim_info: dict, total_subnets: int) -> str: + """ + Format claim type for root network metagraph. + + Args: + claim_info: Claim type dict {"type": "...", "subnets": [...]} + total_subnets: Total number of subnets in network (excluding netuid 0) + + Returns: + Formatted string showing keep/swap counts + + Examples: + {"type": "Keep"} → "Keep all" + {"type": "Swap"} → "Swap all" + {"type": "KeepSubnets", "subnets": [1,2,3]} → "Keep (3), Swap (54)" + """ + claim_type = claim_info.get("type", "Swap") + + if claim_type == "Keep": + return "Keep all" + elif claim_type == "Swap": + return "Swap all" + else: + keep_subnets = claim_info.get("subnets", []) + keep_count = len(keep_subnets) + swap_count = total_subnets - keep_count + return f"Keep ({keep_count}), Swap ({swap_count})" + + async def register_subnetwork_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, From e410fe1726fd4af69d054c5e378f66e6cbf293f6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 16:10:29 -0800 Subject: [PATCH 091/179] format_claim_type_for_subnet --- bittensor_cli/src/commands/subnets/subnets.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index e8499bdae..76467a299 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -78,6 +78,35 @@ def format_claim_type_for_root(claim_info: dict, total_subnets: int) -> str: return f"Keep ({keep_count}), Swap ({swap_count})" +def format_claim_type_for_subnet(claim_info: dict, current_netuid: int) -> str: + """ + Format claim type for specific subnet metagraph. + Shows whether THIS subnet's emissions are kept or swapped. + + Args: + claim_info: Claim type dict {"type": "...", "subnets": [...]} + current_netuid: The netuid being viewed + + Returns: + "Keep" if this subnet is kept, "Swap" if swapped + + Examples: + {"type": "Keep"}, netuid=5 → "Keep" + {"type": "Swap"}, netuid=5 → "Swap" + {"type": "KeepSubnets", "subnets": [1,5,10]}, netuid=5 → "Keep" + {"type": "KeepSubnets", "subnets": [1,5,10]}, netuid=3 → "Swap" + """ + claim_type = claim_info.get("type", "Swap") + + if claim_type == "Keep": + return "Keep" + elif claim_type == "Swap": + return "Swap" + else: + keep_subnets = claim_info.get("subnets", []) + return "Keep" if current_netuid in keep_subnets else "Swap" + + async def register_subnetwork_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, From 0b7d315690b93b6746117f72b57bb0a251b3697f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 16:11:12 -0800 Subject: [PATCH 092/179] update root metagraph --- bittensor_cli/src/commands/subnets/subnets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 76467a299..e3c0210a6 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1135,7 +1135,9 @@ async def show_root(): ) coldkey_ss58 = root_state.coldkeys[idx] - claim_type = root_claim_types.get(coldkey_ss58, "Swap") + claim_type_info = root_claim_types.get(coldkey_ss58, {"type": "Swap"}) + total_subnets = len([n for n in all_subnets if n != 0]) + claim_type = format_claim_type_for_root(claim_type_info, total_subnets) sorted_rows.append( ( From bfbc38af0aeadc8b8b53b97ad710e1ccfe319663 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 16:11:31 -0800 Subject: [PATCH 093/179] update full metagraph --- bittensor_cli/src/commands/subnets/subnets.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index e3c0210a6..68d835111 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1386,10 +1386,12 @@ async def show_subnet( # Get claim type for this coldkey if applicable TAO stake coldkey_ss58 = metagraph_info.coldkeys[idx] + claim_type_info = {"type": "Swap"} # Default + claim_type = "-" + if tao_stake.tao > 0: - claim_type = root_claim_types.get(coldkey_ss58, "Swap") - else: - claim_type = "-" + claim_type_info = root_claim_types.get(coldkey_ss58, {"type": "Swap"}) + claim_type = format_claim_type_for_subnet(claim_type_info, netuid_) rows.append( ( @@ -1430,7 +1432,12 @@ async def show_subnet( "hotkey": metagraph_info.hotkeys[idx], "coldkey": metagraph_info.coldkeys[idx], "identity": uid_identity, - "claim_type": claim_type, + "claim_type": claim_type_info.get("type") + if tao_stake.tao > 0 + else None, + "claim_type_subnets": claim_type_info.get("subnets") + if claim_type_info.get("type") == "KeepSubnets" + else None, } ) From 5451ab92e271789e3920c831000fcb3039ecd118 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 16:11:46 -0800 Subject: [PATCH 094/179] cleanup --- bittensor_cli/src/commands/stake/claim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index e39248c62..fbce1bc10 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -185,7 +185,7 @@ async def set_claim_type( if success: ext_id = await ext_receipt.get_extrinsic_identifier() - msg = f"Successfully set claim type to {_format_claim_type_display(new_claim_info)}" + msg = "Successfully changed claim type" console.print(f":white_heavy_check_mark: [green]{msg}[/green]") await print_extrinsic_id(ext_receipt) if json_output: From c9291b9f40eafbb79c5de512e0384783536648af Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 16:24:49 -0800 Subject: [PATCH 095/179] remove parsing in cli --- bittensor_cli/cli.py | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7919d9b45..53050333c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -64,7 +64,6 @@ prompt_for_subnet_identity, validate_rate_tolerance, get_hotkey_pub_ss58, - parse_subnet_range, ) from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds @@ -7256,24 +7255,6 @@ def stake_set_claim_type( """ self.verbosity_handler(quiet, verbose, json_output) - parsed_netuids = None - if netuids is not None: - try: - parsed_netuids = parse_subnet_range(netuids) - except ValueError as e: - err_console.print(f":cross_mark: [red]Invalid netuid format: {e}[/red]") - raise typer.Exit() - - if claim_type is not None: - claim_type_normalized = claim_type.capitalize() - if claim_type_normalized not in ["Keep", "Swap"]: - err_console.print( - f":cross_mark: [red]Invalid claim type '{claim_type}'. Must be 'keep' or 'swap'.[/red]" - ) - raise typer.Exit() - else: - claim_type_normalized = None - wallet = self.wallet_ask( wallet_name, wallet_path, @@ -7284,8 +7265,8 @@ def stake_set_claim_type( claim_stake.set_claim_type( wallet=wallet, subtensor=self.initialize_chain(network), - claim_type=claim_type_normalized, - netuids=parsed_netuids, + claim_type=claim_type, + netuids=netuids, prompt=prompt, json_output=json_output, ) From d85a1b590898c14f905f2922a147a42e204c5b48 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 16:24:59 -0800 Subject: [PATCH 096/179] parse_subnet_range update --- bittensor_cli/src/bittensor/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 00f432930..94ae015ba 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1051,13 +1051,14 @@ def group_subnets(registrations): return ", ".join(ranges) -def parse_subnet_range(input_str: str) -> list[int]: +def parse_subnet_range(input_str: str, total_subnets: int) -> list[int]: """ Parse subnet range input like "1-24, 30-40, 5". Args: input_str: Comma-separated list of subnets and ranges Examples: "1-5", "1,2,3", "1-5, 10, 20-25" + total_subnets: Total number of subnets available Returns: Sorted list of unique subnet IDs @@ -1083,8 +1084,10 @@ def parse_subnet_range(input_str: str) -> list[int]: if start_num > end_num: raise ValueError(f"Invalid range '{part}': start must be ≤ end") - if end_num - start_num > 128: - raise ValueError(f"Range '{part}' is too large (max 128 subnets)") + if end_num - start_num > total_subnets: + raise ValueError( + f"Range '{part}' is not valid (total of {total_subnets} subnets)" + ) subnets.update(range(start_num, end_num + 1)) except ValueError as e: From a0a1e6e99e900e453ec184f7e148b1e4f92b95e3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 16:25:24 -0800 Subject: [PATCH 097/179] validity checks --- bittensor_cli/src/commands/stake/claim.py | 58 ++++++++++++++--------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index fbce1bc10..633658255 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -29,7 +29,7 @@ async def set_claim_type( wallet: Wallet, subtensor: "SubtensorInterface", claim_type: Optional[str] = None, - netuids: Optional[list[int]] = None, + netuids: Optional[str] = None, prompt: bool = True, json_output: bool = False, ) -> tuple[bool, str, Optional[str]]: @@ -45,7 +45,7 @@ async def set_claim_type( wallet: Bittensor wallet object subtensor: SubtensorInterface object claim_type: Optional claim type ("Keep" or "Swap"). If None, user will be prompted. - netuids: Optional list of subnet IDs to keep (only valid with "Keep" type) + netuids: Optional string of subnet IDs (e.g., "1-5,10,20-30"). Will be parsed internally. prompt: Whether to prompt for user confirmation json_output: Whether to output JSON @@ -56,12 +56,34 @@ async def set_claim_type( - Optional[str]: Extrinsic identifier if successful """ + if claim_type is not None: + claim_type = claim_type.capitalize() + if claim_type not in ["Keep", "Swap"]: + msg = f"Invalid claim type: {claim_type}. Use 'Keep' or 'Swap', or omit for interactive mode." + err_console.print(f"[red]{msg}[/red]") + if json_output: + json_console.print(json.dumps({"success": False, "message": msg})) + return False, msg, None + current_claim_info, all_netuids = await asyncio.gather( subtensor.get_coldkey_claim_type(coldkey_ss58=wallet.coldkeypub.ss58_address), subtensor.get_all_subnet_netuids(), ) all_subnets = sorted([n for n in all_netuids if n != 0]) + selected_netuids = None + if netuids is not None: + try: + selected_netuids = parse_subnet_range( + netuids, total_subnets=len(all_subnets) + ) + except ValueError as e: + msg = f"Invalid netuid format: {e}" + err_console.print(f"[red]{msg}[/red]") + if json_output: + json_console.print(json.dumps({"success": False, "message": msg})) + return False, msg, None + claim_table = Table( Column("[bold white]Coldkey", style=COLORS.GENERAL.COLDKEY, justify="left"), Column( @@ -79,7 +101,7 @@ async def set_claim_type( console.print(claim_table) # Full wizard - if claim_type is None and netuids is None: + if claim_type is None and selected_netuids is None: new_claim_info = await _ask_for_claim_types(wallet, subtensor, all_subnets) if new_claim_info is None: msg = "Operation cancelled." @@ -97,27 +119,18 @@ async def set_claim_type( return False, msg, None # Keep netuids passed thru the cli and assume Keep type - elif claim_type is None and netuids is not None: - new_claim_info = {"type": "KeepSubnets", "subnets": netuids} + elif claim_type is None and selected_netuids is not None: + new_claim_info = {"type": "KeepSubnets", "subnets": selected_netuids} else: - # Keep or Swap all subnets - claim_type_upper = claim_type.capitalize() - if claim_type_upper not in ["Swap", "Keep"]: - msg = f"Invalid claim type: {claim_type}. Use 'Swap' or 'Keep', or omit for interactive mode." - err_console.print(msg) - if json_output: - json_console.print(json.dumps({"success": False, "message": msg})) - return False, msg, None - # Netuids passed with Keep type - if netuids is not None and claim_type_upper == "Keep": - new_claim_info = {"type": "KeepSubnets", "subnets": netuids} + if selected_netuids is not None and claim_type == "Keep": + new_claim_info = {"type": "KeepSubnets", "subnets": selected_netuids} # Netuids passed with Swap type - elif netuids is not None and claim_type_upper == "Swap": - keep_subnets = [n for n in all_subnets if n not in netuids] - invalid = [n for n in netuids if n not in all_subnets] + elif selected_netuids is not None and claim_type == "Swap": + keep_subnets = [n for n in all_subnets if n not in selected_netuids] + invalid = [n for n in selected_netuids if n not in all_subnets] if invalid: msg = f"Invalid subnets (not available): {group_subnets(invalid)}" err_console.print(msg) @@ -132,11 +145,12 @@ async def set_claim_type( else: new_claim_info = {"type": "KeepSubnets", "subnets": keep_subnets} else: - new_claim_info = {"type": claim_type_upper} + new_claim_info = {"type": claim_type} if _claim_types_equal(current_claim_info, new_claim_info): - msg = f"Claim type already set to {_format_claim_type_display(new_claim_info)}. No change needed." - console.print(f"[yellow]{msg}[/yellow]") + console.print( + f"Claim type already set to {_format_claim_type_display(new_claim_info)}. No change needed." + ) if json_output: json_console.print( json.dumps( From 5ca255423f49b561aaccafbbafb32fdcc0395e12 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 25 Nov 2025 16:42:55 -0800 Subject: [PATCH 098/179] nits --- bittensor_cli/src/commands/stake/claim.py | 6 +++--- bittensor_cli/src/commands/subnets/subnets.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 633658255..bae5d162d 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -167,8 +167,8 @@ async def set_claim_type( console.print( Panel( f"[{COLORS.GENERAL.HEADER}]Confirm Claim Type Change[/{COLORS.GENERAL.HEADER}]\n\n" - f"[yellow]FROM:[/yellow] {_format_claim_type_display(current_claim_info, all_subnets)}\n\n" - f"[yellow]TO:[/yellow] {_format_claim_type_display(new_claim_info, all_subnets)}" + f"FROM: {_format_claim_type_display(current_claim_info, all_subnets)}\n\n" + f"TO: {_format_claim_type_display(new_claim_info, all_subnets)}" ) ) @@ -607,7 +607,7 @@ async def _prompt_claim_netuids( continue try: - selected = parse_subnet_range(subnet_input) + selected = parse_subnet_range(subnet_input, total_subnets=len(all_subnets)) invalid = [s for s in selected if s not in all_subnets] if invalid: err_console.print( diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 68d835111..4db9b3dd3 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1211,7 +1211,8 @@ async def show_root(): - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. - Hotkey: The hotkey ss58 address. - Coldkey: The coldkey ss58 address. - - Root Claim: The root claim type for this coldkey. 'Swap' converts Alpha to TAO every epoch. 'Keep' keeps Alpha emissions. + - Root Claim: The root claim type for this coldkey. 'Swap' converts Alpha to TAO every epoch. 'Keep' keeps Alpha emissions. + 'Keep (count)' indicates how many subnets this coldkey is keeping Alpha emissions for. """ ) if delegate_selection: From f92750e80fab8e6129848e57b686b2195f8e7610 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 26 Nov 2025 13:08:32 +0200 Subject: [PATCH 099/179] WIP adding tests --- bittensor_cli/cli.py | 22 +- .../src/bittensor/subtensor_interface.py | 13 +- bittensor_cli/src/commands/proxy.py | 37 ++- tests/e2e_tests/conftest.py | 2 +- tests/e2e_tests/test_proxy.py | 223 ++++++++++++++++++ 5 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 tests/e2e_tests/test_proxy.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 503b6326e..600269f85 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -399,6 +399,11 @@ def edit_help(cls, option_name: str, help_text: str): help="Optional proxy to use for the transaction: either the SS58 or the name of the proxy if you " f"have added it with {arg__('btcli config add-proxy')}.", ) + real_proxy: Optional[str] = typer.Option( + None, + "--real", + help="The real account making this call. If omitted, the wallet's coldkeypub ss58 is used.", + ) announce_only: bool = typer.Option( False, help=f"If set along with [{COLORS.G.ARG}]--proxy[/{COLORS.G.ARG}], will not actually make the extrinsic call, " @@ -1172,6 +1177,10 @@ def __init__(self): self.proxy_app.command("kill", rich_help_panel=HELP_PANELS["PROXY"]["MGMT"])( self.proxy_kill ) + self.proxy_app.command( + "execute", + rich_help_panel=HELP_PANELS["PROXY"]["MGMT"], + )(self.proxy_execute_announced) # Sub command aliases # Wallet @@ -8697,7 +8706,12 @@ def proxy_create( network: Optional[list[str]] = Options.network, proxy_type: ProxyType = Options.proxy_type, delay: int = typer.Option(0, help="Delay, in number of blocks"), - idx: int = typer.Option(0, "--index", help="TODO lol"), + idx: int = typer.Option( + 0, + "--index", + help="A disambiguation index, in case this is called multiple times in the same transaction" + " (e.g. with utility::batch). Unless you're using batch you probably just want to use 0." + ), wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -8993,10 +9007,7 @@ def proxy_kill( def proxy_execute_announced( self, proxy: str = Options.proxy, - real: Optional[str] = Options.edit_help( - "proxy", - "The real account making this call. If omitted, the wallet's coldkeypub ss58 is used.", - ), + real: Optional[str] = Options.real_proxy, call_hash: Optional[str] = typer.Option( None, help="The hash proxy call to execute", @@ -9158,6 +9169,7 @@ def proxy_execute_announced( prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + json_output=json_output ) ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 027af1908..290746d4e 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1155,7 +1155,7 @@ async def sign_and_send_extrinsic( wait_for_finalization: bool = False, era: Optional[dict[str, int]] = None, proxy: Optional[str] = None, - nonce: Optional[str] = None, + nonce: Optional[int] = None, sign_with: Literal["coldkey", "hotkey", "coldkeypub"] = "coldkey", announce_only: bool = False, ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: @@ -1176,10 +1176,11 @@ async def sign_and_send_extrinsic( """ if proxy is not None: if announce_only: + call = await self.substrate.compose_call( "Proxy", "announce", - {"real": proxy, "call_hash": f"0x{call.call_hash}"}, + {"real": proxy, "call_hash": f"0x{call.call_hash.hex()}"}, ) else: call = await self.substrate.compose_call( @@ -1187,14 +1188,18 @@ async def sign_and_send_extrinsic( "proxy", {"real": proxy, "call": call, "force_proxy_type": None}, ) + keypair = getattr(wallet, sign_with) call_args: dict[str, Union[GenericCall, Keypair, dict[str, int], int]] = { "call": call, # sign with specified key - "keypair": getattr(wallet, sign_with), - "nonce": nonce, + "keypair": keypair, } if era is not None: call_args["era"] = era + if nonce is not None: + call_args["nonce"] = nonce + else: + call_args["nonce"] = await self.substrate.get_account_next_index(keypair.ss58_address) extrinsic = await self.substrate.create_signed_extrinsic(**call_args) try: response = await self.substrate.submit_extrinsic( diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 1f3f9ce05..07c47c813 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -167,6 +167,7 @@ async def create_proxy( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, era={"period": period}, + nonce=await subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address) ) if success: await print_extrinsic_id(receipt) @@ -222,6 +223,12 @@ async def create_proxy( data={ "success": success, "message": msg, + "data": { + "pure": created_pure, + "spawner": created_spawner, + "proxy_type": created_proxy_type.value, + "delay": delay, + }, "extrinsic_id": await receipt.get_extrinsic_identifier(), } ) @@ -232,6 +239,7 @@ async def create_proxy( data={ "success": success, "message": msg, + "data": None, "extrinsic_id": None, } ) @@ -302,7 +310,6 @@ async def add_proxy( period: int, json_output: bool, ): - # TODO add to address book if prompt: if not Confirm.ask( f"This will add a proxy of type {proxy_type.value} for delegate {delegate}." @@ -488,7 +495,10 @@ async def execute_announced( prompt: bool = True, wait_for_inclusion: bool = False, wait_for_finalization: bool = False, -): + json_output: bool = False, +) -> None: + # TODO should this remove from the ProxyAnnouncements after successful completion, or should it mark it as completed + # in the DB? if prompt and created_block is not None: current_block = await subtensor.substrate.get_block_number() if current_block - delay > created_block: @@ -581,10 +591,31 @@ async def execute_announced( "force_proxy_type": None, }, ) - return await subtensor.sign_and_send_extrinsic( + success, msg, receipt = await subtensor.sign_and_send_extrinsic( call=announced_call, wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, era={"period": period}, ) + if success is True: + if json_output: + json_console.print_json(data={ + "success": True, + "message": msg, + "extrinsic_identifier": await receipt.get_extrinsic_identifier() + }) + else: + console.print(":white_check_mark:[green]Success![/green]") + await print_extrinsic_id(receipt) + else: + if json_output: + json_console.print_json(data={ + "success": False, + "message": msg, + "extrinsic_identifier": None + }) + else: + err_console.print( + f":cross_mark:[red]Failed[/red]. {msg} " + ) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 0e1b13cc6..8f6db6583 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -17,7 +17,7 @@ from .utils import setup_wallet, ExecCommand -LOCALNET_IMAGE_NAME = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" +LOCALNET_IMAGE_NAME = "ghcr.io/opentensor/subtensor-localnet:main" def wait_for_node_start(process, pattern, timestamp: int = None): diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py new file mode 100644 index 000000000..dea846848 --- /dev/null +++ b/tests/e2e_tests/test_proxy.py @@ -0,0 +1,223 @@ +import asyncio +import json +from time import sleep + +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.utils import ProxyAnnouncements +from .utils import ( + extract_coldkey_balance, + validate_wallet_inspect, + validate_wallet_overview, + verify_subnet_entry, +) + +""" +Verify commands: + +* btcli proxy create +* btcli proxy add +* btcli proxy remove +* btcli proxy kill +* btcli proxy execute +""" + + +def test_proxy_create(local_chain, wallet_setup): + """ + Tests the pure proxy logic (create/kill) + """ + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + + # Create wallets for Alice and Bob + 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 + ) + proxy_type = "Any" + delay = 12 + + # create a pure proxy + create_result = exec_command_alice( + command="proxy", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy-type", + proxy_type, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output" + ] + ) + create_result_output = json.loads(create_result.stdout) + assert create_result_output["success"] is True + assert create_result_output["message"] is not None + assert create_result_output["extrinsic_id"] is not None + created_pure = create_result_output["data"]["pure"] + spawner = create_result_output["data"]["spawner"] + created_proxy_type = create_result_output["data"]["proxy_type"] + created_delay = create_result_output["data"]["delay"] + assert isinstance(created_pure, str) + assert isinstance(spawner, str) + assert spawner == wallet_alice.coldkeypub.ss58_address + assert created_proxy_type == proxy_type + assert created_delay == delay + + # transfer some funds from alice to the pure proxy + amount_to_transfer = 1_000 + transfer_result = exec_command_alice( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--dest", + created_pure, + "--amount", + str(amount_to_transfer), + "--no-prompt", + "--json-output" + ] + ) + transfer_result_output = json.loads(transfer_result.stdout) + assert transfer_result_output["success"] is True + + # ensure the proxy has the transferred funds + 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", + "--ss58", + created_pure, + "--json-output" + ] + ) + balance_result_output = json.loads(balance_result.stdout) + assert balance_result_output["balances"]["Provided Address 1"]["coldkey"] == created_pure + assert balance_result_output["balances"]["Provided Address 1"]["free"] == float(amount_to_transfer) + + # transfer some of the pure proxy's funds to bob, but don't announce it + amount_to_transfer_proxy = 100 + transfer_result_proxy = exec_command_alice( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--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 it wasn't announced + assert transfer_result_proxy_output["success"] is False + + # announce the same extrinsic + transfer_result_proxy = exec_command_alice( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--dest", + keypair_bob.ss58_address, + "--amount", + str(amount_to_transfer_proxy), + "--no-prompt", + "--json-output", + "--announce-only", + ] + ) + transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) + assert transfer_result_proxy_output["success"] is True + with ProxyAnnouncements.get_db() as (conn, cursor): + rows = ProxyAnnouncements.read_rows(conn, cursor, include_header=False) + latest_announcement = next(iter(sorted(rows, key=lambda row: row[1], reverse=True))) # sort by epoch time + address, epoch_time, block, call_hash, call, call_serialized = latest_announcement + assert address == created_pure + async def _handler(_): + return True + + # wait for delay (probably already happened if fastblocks is on) + asyncio.run(local_chain.wait_for_block(block+delay, _handler, False)) + + announce_execution_result = exec_command_alice( + command="proxy", + sub_command="execute", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--call-hash", + call_hash, + "--no-prompt", + "--verbose" + # "--json-output" + ] + ) + print(announce_execution_result.stdout, announce_execution_result.stderr) + announce_execution_result_output = json.loads(announce_execution_result.stdout) + assert announce_execution_result_output["success"] is True + assert announce_execution_result_output["message"] == "" + + + # ensure bob has the transferred 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", + "--json-output" + ] + ) + balance_result_output = json.loads(balance_result.stdout) + print(balance_result_output) + # assert balance_result_output["balances"]["Provided Address 1"]["coldkey"] == created_pure + # assert balance_result_output["balances"]["Provided Address 1"]["free"] == float(amount_to_transfer) + From c9f57d7780fac251000f48f75bff3364ec85cdd6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 26 Nov 2025 12:51:10 -0800 Subject: [PATCH 100/179] improve help commands --- bittensor_cli/cli.py | 12 ++++++------ bittensor_cli/src/commands/stake/claim.py | 9 +++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 53050333c..570c14893 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7239,15 +7239,15 @@ def stake_set_claim_type( [bold]Claim Types:[/bold] • [green]Swap[/green]: Future Root Alpha Emissions are swapped to TAO and added to root stake (default) • [yellow]Keep[/yellow]: Future Root Alpha Emissions are kept as Alpha tokens - • [cyan]Keep Specific[/cyan]: Keep specific subnets as Alpha, swap others to TAO + • [cyan]Keep Specific[/cyan]: Keep specific subnets as Alpha, swap others to TAO. You can use this type by selecting the netuids. USAGE: - [green]$[/green] btcli stake claim - [green]$[/green] btcli stake claim keep - [green]$[/green] btcli stake claim swap - [green]$[/green] btcli stake claim keep --netuids 1-5,10,20-30 - [green]$[/green] btcli stake claim swap --netuids 1-30 + [green]$[/green] btcli stake claim [cyan](Full wizard)[/cyan] + [green]$[/green] btcli stake claim keep [cyan](Keep all subnets)[/cyan] + [green]$[/green] btcli stake claim swap [cyan](Swap all subnets)[/cyan] + [green]$[/green] btcli stake claim keep --netuids 1-5,10,20-30 [cyan](Keep specific subnets)[/cyan] + [green]$[/green] btcli stake claim swap --netuids 1-30 [cyan](Swap specific subnets)[/cyan] With specific wallet: diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index bae5d162d..daf2aae62 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -148,9 +148,8 @@ async def set_claim_type( new_claim_info = {"type": claim_type} if _claim_types_equal(current_claim_info, new_claim_info): - console.print( - f"Claim type already set to {_format_claim_type_display(new_claim_info)}. No change needed." - ) + msg = f"Claim type already set to {_format_claim_type_display(new_claim_info)}. \nNo change needed." + console.print(msg) if json_output: json_console.print( json.dumps( @@ -540,7 +539,9 @@ async def _ask_for_claim_types( if primary_choice == "cancel": return None - apply_to_all = Confirm.ask("\nApply to ALL subnets?", default=True) + apply_to_all = Confirm.ask( + f"\nSet {primary_choice.capitalize()} to ALL subnets?", default=True + ) if apply_to_all: return {"type": primary_choice.capitalize()} From 236bd3661f47c866edd9f55c3babd62578fa9486 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 27 Nov 2025 09:26:01 +0200 Subject: [PATCH 101/179] WIP --- bittensor_cli/cli.py | 34 ++++++++----- .../src/bittensor/subtensor_interface.py | 5 +- bittensor_cli/src/bittensor/utils.py | 25 ++++++++-- bittensor_cli/src/commands/proxy.py | 39 ++++++++------- tests/e2e_tests/test_proxy.py | 50 +++++++++++-------- tests/unit_tests/test_cli.py | 4 +- 6 files changed, 99 insertions(+), 58 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 600269f85..b15d11234 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8710,7 +8710,7 @@ def proxy_create( 0, "--index", help="A disambiguation index, in case this is called multiple times in the same transaction" - " (e.g. with utility::batch). Unless you're using batch you probably just want to use 0." + " (e.g. with utility::batch). Unless you're using batch you probably just want to use 0.", ), wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, @@ -9100,24 +9100,28 @@ def proxy_execute_announced( f"Unable to retrieve proxy from address book: {proxy}" ) block = None + # index of the call if retrieved from DB + got_call_from_db: Optional[int] = None if not call_hex: - got_call_from_db = False potential_call_matches = [] for row in announcements: ( + id_, address, epoch_time, block_, call_hash_, call_hex_, call_serialized, + executed_int, ) = row - if call_hash_ == call_hash and address == proxy: + executed = bool(executed_int) + if call_hash_ == call_hash and address == proxy and executed is False: potential_call_matches.append(row) if len(potential_call_matches) == 1: - block = potential_call_matches[0][2] - call_hex = potential_call_matches[0][4] - got_call_from_db = True + block = potential_call_matches[0][3] + call_hex = potential_call_matches[0][5] + got_call_from_db = potential_call_matches[0][0] elif len(potential_call_matches) > 1: if not prompt: err_console.print( @@ -9134,12 +9138,14 @@ def proxy_execute_announced( ) for row in potential_call_matches: ( + id_, address, epoch_time, block_, call_hash_, call_hex_, call_serialized, + executed_int, ) = row console.print( f"Time: {datetime.datetime.fromtimestamp(epoch_time)}\nCall:\n" @@ -9148,15 +9154,14 @@ def proxy_execute_announced( if Confirm.ask("Is this the intended call?"): call_hex = call_hex_ block = block_ - got_call_from_db = True + got_call_from_db = row break - if not got_call_from_db: + if got_call_from_db is None: console.print("Unable to retrieve call from DB. Proceeding without.") - else: - if call_hex[0:2] == "0x": - call_hex = call_hex[2:] + if isinstance(call_hex, str) and call_hex[0:2] == "0x": + call_hex = call_hex[2:] - return self._run_command( + success = self._run_command( proxy_commands.execute_announced( subtensor=self.initialize_chain(network), wallet=wallet, @@ -9169,9 +9174,12 @@ def proxy_execute_announced( prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, - json_output=json_output + json_output=json_output, ) ) + if success and got_call_from_db is not None: + with ProxyAnnouncements.get_db() as (conn, cursor): + ProxyAnnouncements.mark_as_executed(conn, cursor, got_call_from_db) @staticmethod def convert( diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 290746d4e..e3752d458 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1176,7 +1176,6 @@ async def sign_and_send_extrinsic( """ if proxy is not None: if announce_only: - call = await self.substrate.compose_call( "Proxy", "announce", @@ -1199,7 +1198,9 @@ async def sign_and_send_extrinsic( if nonce is not None: call_args["nonce"] = nonce else: - call_args["nonce"] = await self.substrate.get_account_next_index(keypair.ss58_address) + call_args["nonce"] = await self.substrate.get_account_next_index( + keypair.ss58_address + ) extrinsic = await self.substrate.create_signed_extrinsic(**call_args) try: response = await self.substrate.submit_extrinsic( diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 4ae0a59bb..fed6fb465 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1028,12 +1028,14 @@ def delete_entry( class ProxyAnnouncements(TableDefinition): name = "proxy_announcements" cols = ( + ("id", "INTEGER PRIMARY KEY"), ("address", "TEXT"), ("epoch_time", "INTEGER"), ("block", "INTEGER"), ("call_hash", "TEXT"), ("call", "TEXT"), ("call_serialized", "TEXT"), + ("executed", "INTEGER"), ) @classmethod @@ -1047,13 +1049,23 @@ def add_entry( block: int, call_hash: str, call: GenericCall, + executed: bool = False, ) -> None: call_hex = call.data.to_hex() call_serialized = json.dumps(call.serialize()) + executed_int = int(executed) conn.execute( - f"INSERT INTO {cls.name} (address, epoch_time, block, call_hash, call, call_serialized)" - " VALUES (?, ?, ?, ?, ?, ?)", - (address, epoch_time, block, call_hash, call_hex, call_serialized), + f"INSERT INTO {cls.name} (address, epoch_time, block, call_hash, call, call_serialized, executed)" + " VALUES (?, ?, ?, ?, ?, ?, ?)", + ( + address, + epoch_time, + block, + call_hash, + call_hex, + call_serialized, + executed_int, + ), ) conn.commit() @@ -1074,6 +1086,13 @@ def delete_entry( ) conn.commit() + @classmethod + def mark_as_executed(cls, conn: sqlite3.Connection, _: sqlite3.Cursor, idx: int): + conn.execute( + f"UPDATE {cls.name} SET executed = ? WHERE id = ?", + (True, idx), + ) + class DB: """ diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 07c47c813..7834622de 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -167,7 +167,9 @@ async def create_proxy( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, era={"period": period}, - nonce=await subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address) + nonce=await subtensor.substrate.get_account_next_index( + wallet.coldkeypub.ss58_address + ), ) if success: await print_extrinsic_id(receipt) @@ -496,7 +498,7 @@ async def execute_announced( wait_for_inclusion: bool = False, wait_for_finalization: bool = False, json_output: bool = False, -) -> None: +) -> bool: # TODO should this remove from the ProxyAnnouncements after successful completion, or should it mark it as completed # in the DB? if prompt and created_block is not None: @@ -507,7 +509,7 @@ async def execute_announced( f" at block {created_block}. It is currently only {current_block}. The call will likely fail." f" Do you want to proceed?" ): - return None + return False if call_hex is None: if not prompt: @@ -516,7 +518,7 @@ async def execute_announced( f" [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}], so we are unable to request" f"the information to craft this call." ) - return None + return False else: call_args = {} failure_ = f"Instead create the call using btcli commands with [{COLORS.G.ARG}]--announce-only[/{COLORS.G.ARG}]" @@ -547,7 +549,7 @@ async def execute_announced( err_console.print( f":cross_mark:[red]Unable to craft a Call Type for arg {arg}. {failure_}" ) - return None + return False elif type_name == "NetUid": value = IntPrompt.ask(f"Enter the netuid for {arg}") elif type_name in ("u16", "u64"): @@ -566,7 +568,7 @@ async def execute_announced( err_console.print( f":cross_mark:[red]Unrecognized type name {type_name}. {failure_}" ) - return None + return False call_args[arg] = value inner_call = await subtensor.substrate.compose_call( module, @@ -600,22 +602,21 @@ async def execute_announced( ) if success is True: if json_output: - json_console.print_json(data={ - "success": True, - "message": msg, - "extrinsic_identifier": await receipt.get_extrinsic_identifier() - }) + json_console.print_json( + data={ + "success": True, + "message": msg, + "extrinsic_identifier": await receipt.get_extrinsic_identifier(), + } + ) else: console.print(":white_check_mark:[green]Success![/green]") await print_extrinsic_id(receipt) else: if json_output: - json_console.print_json(data={ - "success": False, - "message": msg, - "extrinsic_identifier": None - }) - else: - err_console.print( - f":cross_mark:[red]Failed[/red]. {msg} " + json_console.print_json( + data={"success": False, "message": msg, "extrinsic_identifier": None} ) + else: + err_console.print(f":cross_mark:[red]Failed[/red]. {msg} ") + return success diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index dea846848..c6ad68c21 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -57,8 +57,8 @@ def test_proxy_create(local_chain, wallet_setup): "--period", "128", "--no-prompt", - "--json-output" - ] + "--json-output", + ], ) create_result_output = json.loads(create_result.stdout) assert create_result_output["success"] is True @@ -91,8 +91,8 @@ def test_proxy_create(local_chain, wallet_setup): "--amount", str(amount_to_transfer), "--no-prompt", - "--json-output" - ] + "--json-output", + ], ) transfer_result_output = json.loads(transfer_result.stdout) assert transfer_result_output["success"] is True @@ -110,12 +110,17 @@ def test_proxy_create(local_chain, wallet_setup): "default", "--ss58", created_pure, - "--json-output" - ] + "--json-output", + ], ) balance_result_output = json.loads(balance_result.stdout) - assert balance_result_output["balances"]["Provided Address 1"]["coldkey"] == created_pure - assert balance_result_output["balances"]["Provided Address 1"]["free"] == float(amount_to_transfer) + assert ( + balance_result_output["balances"]["Provided Address 1"]["coldkey"] + == created_pure + ) + assert balance_result_output["balances"]["Provided Address 1"]["free"] == float( + amount_to_transfer + ) # transfer some of the pure proxy's funds to bob, but don't announce it amount_to_transfer_proxy = 100 @@ -136,8 +141,8 @@ def test_proxy_create(local_chain, wallet_setup): "--amount", str(amount_to_transfer_proxy), "--no-prompt", - "--json-output" - ] + "--json-output", + ], ) transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) # should fail, because it wasn't announced @@ -163,20 +168,27 @@ def test_proxy_create(local_chain, wallet_setup): "--no-prompt", "--json-output", "--announce-only", - ] + ], ) + print(transfer_result_proxy.stdout, transfer_result_proxy.stderr) transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) assert transfer_result_proxy_output["success"] is True with ProxyAnnouncements.get_db() as (conn, cursor): rows = ProxyAnnouncements.read_rows(conn, cursor, include_header=False) - latest_announcement = next(iter(sorted(rows, key=lambda row: row[1], reverse=True))) # sort by epoch time - address, epoch_time, block, call_hash, call, call_serialized = latest_announcement + latest_announcement = next( + iter(sorted(rows, key=lambda row: row[2], reverse=True)) + ) # sort by epoch time + idx, address, epoch_time, block, call_hash, call, call_serialized, executed_int = ( + latest_announcement + ) assert address == created_pure + assert executed_int == 0 + async def _handler(_): return True # wait for delay (probably already happened if fastblocks is on) - asyncio.run(local_chain.wait_for_block(block+delay, _handler, False)) + asyncio.run(local_chain.wait_for_block(block + delay, _handler, False)) announce_execution_result = exec_command_alice( command="proxy", @@ -193,16 +205,15 @@ async def _handler(_): "--call-hash", call_hash, "--no-prompt", - "--verbose" + "--verbose", # "--json-output" - ] + ], ) print(announce_execution_result.stdout, announce_execution_result.stderr) announce_execution_result_output = json.loads(announce_execution_result.stdout) assert announce_execution_result_output["success"] is True assert announce_execution_result_output["message"] == "" - # ensure bob has the transferred funds balance_result = exec_command_bob( command="wallet", @@ -213,11 +224,10 @@ async def _handler(_): "--chain", "ws://127.0.0.1:9945", "--wallet-name", - "--json-output" - ] + "--json-output", + ], ) balance_result_output = json.loads(balance_result.stdout) print(balance_result_output) # assert balance_result_output["balances"]["Provided Address 1"]["coldkey"] == created_pure # assert balance_result_output["balances"]["Provided Address 1"]["free"] == float(amount_to_transfer) - diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index a4b2ed11d..a061910e5 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -553,7 +553,9 @@ def test_wallet_swap_coldkey_calls_proxy_validation(): ): mock_wallet = Mock() mock_wallet.coldkeypub = Mock() - mock_wallet.coldkeypub.ss58_address = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + mock_wallet.coldkeypub.ss58_address = ( + "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + ) mock_wallet_ask.return_value = mock_wallet cli_manager.wallet_swap_coldkey( From e3b679158583b70163b179d22fc5c2d0c5af0283 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 27 Nov 2025 11:07:42 +0200 Subject: [PATCH 102/179] Passing --- bittensor_cli/cli.py | 16 ++++--- .../src/bittensor/subtensor_interface.py | 12 ++++-- bittensor_cli/src/bittensor/utils.py | 3 +- tests/e2e_tests/test_proxy.py | 43 ++++++++++++++++--- 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b15d11234..60b7c2b9f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -402,7 +402,7 @@ def edit_help(cls, option_name: str, help_text: str): real_proxy: Optional[str] = typer.Option( None, "--real", - help="The real account making this call. If omitted, the wallet's coldkeypub ss58 is used.", + help="The real account making this call. If omitted, the proxy's ss58 is used.", ) announce_only: bool = typer.Option( False, @@ -9008,6 +9008,11 @@ def proxy_execute_announced( self, proxy: str = Options.proxy, real: Optional[str] = Options.real_proxy, + delegate: Optional[str] = typer.Option( + None, + "--delegate", + help="The delegate of the call. If omitted, the wallet's coldkey ss58 is used.", + ), call_hash: Optional[str] = typer.Option( None, help="The hash proxy call to execute", @@ -9049,10 +9054,8 @@ def proxy_execute_announced( ask_for=[WO.NAME, WO.PATH], validate=WV.WALLET, ) - real = ( - self.is_valid_proxy_name_or_ss58(real, False) - or wallet.coldkeypub.ss58_address - ) + real = self.is_valid_proxy_name_or_ss58(real, False) or proxy + delegate = delegate or wallet.coldkeypub.ss58_address with ProxyAnnouncements.get_db() as (conn, cursor): announcements = ProxyAnnouncements.read_rows(conn, cursor) if not got_delay_from_config: @@ -9165,7 +9168,8 @@ def proxy_execute_announced( proxy_commands.execute_announced( subtensor=self.initialize_chain(network), wallet=wallet, - delegate=proxy, + # TODO this might be backwards with pure vs non-pure proxies + delegate=delegate, real=real, period=period, call_hex=call_hex, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e3752d458..263cc0c6a 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1176,10 +1176,14 @@ async def sign_and_send_extrinsic( """ if proxy is not None: if announce_only: + call_to_announce = call call = await self.substrate.compose_call( "Proxy", "announce", - {"real": proxy, "call_hash": f"0x{call.call_hash.hex()}"}, + { + "real": proxy, + "call_hash": f"0x{call_to_announce.call_hash.hex()}", + }, ) else: call = await self.substrate.compose_call( @@ -1221,11 +1225,11 @@ async def sign_and_send_extrinsic( address=proxy, epoch_time=int(time.time()), block=block, - call_hash=call.call_hash.hex(), - call=call, + call_hash=call_to_announce.call_hash.hex(), + call=call_to_announce, ) console.print( - f"Added entry {call.call_hash} at block {block} to your ProxyAnnouncements address book." + f"Added entry {call_to_announce.call_hash} at block {block} to your ProxyAnnouncements address book." ) return True, "", response else: diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index fed6fb465..428f1e672 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1090,8 +1090,9 @@ def delete_entry( def mark_as_executed(cls, conn: sqlite3.Connection, _: sqlite3.Cursor, idx: int): conn.execute( f"UPDATE {cls.name} SET executed = ? WHERE id = ?", - (True, idx), + (1, idx), ) + conn.commit() class DB: diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index c6ad68c21..85b9d0448 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -3,7 +3,7 @@ from time import sleep from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.utils import ProxyAnnouncements +from bittensor_cli.src.bittensor.utils import ProxyAnnouncements, decode_account_id from .utils import ( extract_coldkey_balance, validate_wallet_inspect, @@ -11,6 +11,9 @@ verify_subnet_entry, ) +# temporary +from async_substrate_interface.sync_substrate import SubstrateInterface + """ Verify commands: @@ -37,7 +40,7 @@ def test_proxy_create(local_chain, wallet_setup): wallet_path_bob ) proxy_type = "Any" - delay = 12 + delay = 1 # create a pure proxy create_result = exec_command_alice( @@ -190,6 +193,27 @@ async def _handler(_): # wait for delay (probably already happened if fastblocks is on) asyncio.run(local_chain.wait_for_block(block + delay, _handler, False)) + # get 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"] + announce_execution_result = exec_command_alice( command="proxy", sub_command="execute", @@ -205,8 +229,7 @@ async def _handler(_): "--call-hash", call_hash, "--no-prompt", - "--verbose", - # "--json-output" + "--json-output", ], ) print(announce_execution_result.stdout, announce_execution_result.stderr) @@ -224,10 +247,16 @@ async def _handler(_): "--chain", "ws://127.0.0.1:9945", "--wallet-name", + "default", "--json-output", ], ) balance_result_output = json.loads(balance_result.stdout) - print(balance_result_output) - # assert balance_result_output["balances"]["Provided Address 1"]["coldkey"] == created_pure - # assert balance_result_output["balances"]["Provided Address 1"]["free"] == float(amount_to_transfer) + assert ( + balance_result_output["balances"]["default"]["coldkey"] + == wallet_bob.coldkeypub.ss58_address + ) + assert ( + balance_result_output["balances"]["default"]["free"] + == float(amount_to_transfer_proxy) + bob_init_balance + ) From 9a26fd022f0feb7a1278f700ce5f98bd07b67ece Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 11:23:08 -0800 Subject: [PATCH 103/179] add get_mev_shield_next_key --- .../src/bittensor/subtensor_interface.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 054d67f7a..2af67f2ca 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -41,7 +41,7 @@ decode_hex_identity_dict, validate_chain_endpoint, u16_normalized_float, - U16_MAX, + MEV_SHIELD_PUBLIC_KEY_SIZE, get_hotkey_pub_ss58, ) @@ -2299,6 +2299,34 @@ async def get_subnet_ema_tao_inflow( ema_value = fixed_to_float(raw_ema_value) return Balance.from_rao(ema_value) + async def get_mev_shield_next_key( + self, + block_hash: Optional[str] = None, + ) -> Optional[tuple[bytes, int]]: + """ + Get the next MEV Shield public key and epoch from chain storage. + + Args: + block_hash: Optional block hash to query at. + + Returns: + Tuple of (public_key_bytes, epoch) or None if not available. + """ + result = await self.query( + module="MevShield", + storage_function="NextKey", + block_hash=block_hash, + ) + public_key_bytes = bytes(next(iter(result))) + + if len(public_key_bytes) != MEV_SHIELD_PUBLIC_KEY_SIZE: + raise ValueError( + f"Invalid ML-KEM-768 public key size: {len(public_key_bytes)} bytes. " + f"Expected exactly {MEV_SHIELD_PUBLIC_KEY_SIZE} bytes." + ) + + return public_key_bytes + async def best_connection(networks: list[str]): """ From 33eb90abb9aea827ab3ceaeba16c1a48465eb490 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 11:40:09 -0800 Subject: [PATCH 104/179] adds encrypt_call --- .../src/bittensor/extrinsics/mev_shield.py | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 bittensor_cli/src/bittensor/extrinsics/mev_shield.py diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py new file mode 100644 index 000000000..7c48b01a6 --- /dev/null +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -0,0 +1,88 @@ +import asyncio +import hashlib +from typing import TYPE_CHECKING, Optional + +from bittensor_drand import encrypt_mlkem768, mlkem_kdf_id +from bittensor_cli.src.bittensor.utils import encode_account_id + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from scalecodec import GenericCall + from async_substrate_interface import AsyncExtrinsicReceipt + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def encrypt_call( + subtensor: "SubtensorInterface", + wallet: "Wallet", + call: "GenericCall", +) -> "GenericCall": + """ + Encrypt a call using MEV Shield. + + Takes any call and returns a MevShield.submit_encrypted call + that can be submitted like any regular extrinsic. + + Args: + subtensor: The SubtensorInterface instance for chain queries. + wallet: The wallet whose coldkey will sign the inner payload. + call: The call to encrypt. + + Returns: + A MevShield.submit_encrypted call. + + Raises: + ValueError: If MEV Shield NextKey is not available on chain. + """ + + next_key_result, genesis_hash, nonce = await asyncio.gather( + subtensor.get_mev_shield_next_key(), + subtensor.substrate.get_block_hash(0), + subtensor.substrate.get_account_nonce(wallet.coldkey.ss58_address), + ) + if next_key_result is None: + raise ValueError("MEV Shield NextKey not available on chain") + + nonce = nonce + 1 # TODO: Update once chain is updated + ml_kem_768_public_key = next_key_result + + # Create payload_core: signer (32B) + nonce (u32 LE) + SCALE(call) + signer_bytes = encode_account_id(wallet.coldkey.ss58_address) + nonce_bytes = (nonce & 0xFFFFFFFF).to_bytes(4, byteorder="little") + scale_call_bytes = bytes(call.data.data) + + payload_core = signer_bytes + nonce_bytes + scale_call_bytes + + mev_shield_version = mlkem_kdf_id() + genesis_hash_clean = ( + genesis_hash[2:] if genesis_hash.startswith("0x") else genesis_hash + ) + genesis_hash_bytes = bytes.fromhex(genesis_hash_clean) + + # Sign: coldkey.sign(b"mev-shield:v1" + genesis_hash + payload_core) + message_to_sign = ( + b"mev-shield:" + mev_shield_version + genesis_hash_bytes + payload_core + ) + signature = wallet.coldkey.sign(message_to_sign) + + # Plaintext: payload_core + b"\x01" + signature + plaintext = payload_core + b"\x01" + signature + + # Encrypt using ML-KEM-768 + ciphertext = encrypt_mlkem768(ml_kem_768_public_key, plaintext) + + # Commitment: blake2_256(payload_core) + commitment_hash = hashlib.blake2b(payload_core, digest_size=32).digest() + commitment_hex = "0x" + commitment_hash.hex() + + # Create the MevShield.submit_encrypted call + encrypted_call = await subtensor.substrate.compose_call( + call_module="MevShield", + call_function="submit_encrypted", + call_params={ + "commitment": commitment_hex, + "ciphertext": ciphertext, + }, + ) + + return encrypted_call From a58c38deffda411a31364508ffa87a092e244d36 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 11:40:26 -0800 Subject: [PATCH 105/179] extract_mev_shield_id --- .../src/bittensor/extrinsics/mev_shield.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 7c48b01a6..7f91d7b9b 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -86,3 +86,22 @@ async def encrypt_call( ) return encrypted_call + + +async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[str]: + """ + Extract the MEV Shield wrapper ID from an extrinsic response. + + After submitting a MEV Shield encrypted call, the EncryptedSubmitted event + contains the wrapper ID needed to track execution. + + Args: + response: The extrinsic receipt from submit_extrinsic. + + Returns: + The wrapper ID (hex string) or None if not found. + """ + for event in await response.triggered_events: + if event["event_id"] == "EncryptedSubmitted": + return event["attributes"]["id"] + return None From fb6dcb645b00960a2fa6d1c76560013956428521 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 11:40:36 -0800 Subject: [PATCH 106/179] wait_for_mev_execution --- .../src/bittensor/extrinsics/mev_shield.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 7f91d7b9b..bd67c089f 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -105,3 +105,65 @@ async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[s if event["event_id"] == "EncryptedSubmitted": return event["attributes"]["id"] return None + + +async def wait_for_mev_execution( + subtensor: "SubtensorInterface", + wrapper_id: str, + timeout_blocks: int = 4, + status=None, +) -> tuple[bool, Optional[str]]: + """ + Wait for MEV Shield inner call execution. + + After submit_encrypted succeeds, the block author will decrypt and execute + the inner call via execute_revealed. This function polls for the + DecryptedExecuted or DecryptedRejected event. + + Args: + subtensor: SubtensorInterface instance. + wrapper_id: The ID from EncryptedSubmitted event. + timeout_blocks: Max blocks to wait (default 4). + status: Optional rich.Status object for progress updates. + + Returns: + Tuple of (success: bool, error: Optional[str]). + - (True, None) if DecryptedExecuted was found. + - (False, error_message) if DecryptedRejected or timeout. + """ + + start_block = await subtensor.substrate.get_block_number() + current_block = start_block + + while current_block - start_block <= timeout_blocks: + if status: + status.update( + f":hourglass: Waiting for MEV Shield execution " + f"(block {current_block - start_block + 1}/{timeout_blocks})..." + ) + + block_hash = await subtensor.substrate.get_block_hash(current_block) + events = await subtensor.substrate.get_events(block_hash) + + for event in events: + event_id = event.get("event_id", "") + if event_id == "DecryptedExecuted": + if event.get("attributes", {}).get("id") == wrapper_id: + return True, None + elif event_id == "DecryptedRejected": + if event.get("attributes", {}).get("id") == wrapper_id: + error = event.get("attributes", {}).get("error", "Unknown error") + return False, f"MEV Shield execution failed: {error}" + + current_block += 1 + + async def _noop(_): + return True + + await subtensor.substrate.wait_for_block( + current_block, + result_handler=_noop, + task_return=False, + ) + + return False, "Timeout waiting for MEV Shield execution" From 121ac08fa1c91d07f1f4105537e40cd92d9f4976 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 11:42:02 -0800 Subject: [PATCH 107/179] update deps --- bittensor_cli/src/bittensor/utils.py | 1 + pyproject.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index f8e322f01..1ca8325ba 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -37,6 +37,7 @@ BT_DOCS_LINK = "https://docs.learnbittensor.org" GLOBAL_MAX_SUBNET_COUNT = 4096 +MEV_SHIELD_PUBLIC_KEY_SIZE = 1184 console = Console() json_console = Console() diff --git a/pyproject.toml b/pyproject.toml index 9eefb8d4d..549a8f2bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "async-substrate-interface>=1.5.2", "aiohttp~=3.13", "backoff~=2.2.1", + "bittensor-drand>=1.2.0", "GitPython>=3.0.0", "netaddr~=1.3.0", "numpy>=2.0.1,<3.0.0", From 5c5a7fcc4b0c1d36fd0eaa0f69cbcce4b695925d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 12:50:49 -0800 Subject: [PATCH 108/179] handle inner call failure --- .../src/bittensor/extrinsics/mev_shield.py | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index bd67c089f..5c437039b 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -43,7 +43,7 @@ async def encrypt_call( if next_key_result is None: raise ValueError("MEV Shield NextKey not available on chain") - nonce = nonce + 1 # TODO: Update once chain is updated + nonce = nonce + 1 # TODO: Update once chain is updated ml_kem_768_public_key = next_key_result # Create payload_core: signer (32B) + nonce (u32 LE) + SCALE(call) @@ -129,13 +129,13 @@ async def wait_for_mev_execution( Returns: Tuple of (success: bool, error: Optional[str]). - (True, None) if DecryptedExecuted was found. - - (False, error_message) if DecryptedRejected or timeout. + - (False, error_message) if the call failed or timeout. """ start_block = await subtensor.substrate.get_block_number() current_block = start_block - while current_block - start_block <= timeout_blocks: + while current_block - start_block < timeout_blocks: if status: status.update( f":hourglass: Waiting for MEV Shield execution " @@ -143,17 +143,45 @@ async def wait_for_mev_execution( ) block_hash = await subtensor.substrate.get_block_hash(current_block) - events = await subtensor.substrate.get_events(block_hash) - - for event in events: - event_id = event.get("event_id", "") - if event_id == "DecryptedExecuted": - if event.get("attributes", {}).get("id") == wrapper_id: - return True, None - elif event_id == "DecryptedRejected": - if event.get("attributes", {}).get("id") == wrapper_id: - error = event.get("attributes", {}).get("error", "Unknown error") - return False, f"MEV Shield execution failed: {error}" + events, extrinsics = await asyncio.gather( + subtensor.substrate.get_events(block_hash), + subtensor.substrate.get_extrinsics(block_hash), + ) + + # Look for execute_revealed extrinsic + execute_revealed_index = None + for idx, extrinsic in enumerate(extrinsics): + call = extrinsic.get("call", {}) + call_module = call.get("call_module") + call_function = call.get("call_function") + + if call_module == "MevShield" and call_function == "execute_revealed": + call_args = call.get("call_args", []) + for arg in call_args: + if arg.get("name") == "id": + extrinsic_wrapper_id = arg.get("value") + if extrinsic_wrapper_id == wrapper_id: + execute_revealed_index = idx + break + + if execute_revealed_index is not None: + break + + # Check for success or failure events in the extrinsic + if execute_revealed_index is not None: + for event in events: + event_id = event.get("event_id", "") + event_extrinsic_idx = event.get("extrinsic_idx") + + if event_extrinsic_idx == execute_revealed_index: + if event_id == "ExtrinsicSuccess": + return True, None + elif event_id == "ExtrinsicFailed": + dispatch_error = event.get("attributes", {}).get( + "dispatch_error", {} + ) + error_msg = f"{dispatch_error}" + return False, error_msg current_block += 1 From f368f42fc8e81dc4341c76f6559316d055760c38 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 12:53:45 -0800 Subject: [PATCH 109/179] mev shield in stake add ops --- bittensor_cli/src/commands/stake/add.py | 258 +++++++++++++----------- 1 file changed, 145 insertions(+), 113 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 5046981da..0e927c5f3 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -11,6 +11,11 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + encrypt_call, + extract_mev_shield_id, + wait_for_mev_execution, +) from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -134,6 +139,7 @@ async def safe_stake_extrinsic( }, ), ) + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, @@ -160,49 +166,59 @@ async def safe_stake_extrinsic( err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" err_out("\n" + err_msg) return False, err_msg, None - else: - if json_output: - # the rest of this checking is not necessary if using json_output - return True, "", response - await print_extrinsic_id(response) - block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), - subtensor.get_stake( - hotkey_ss58=hotkey_ss58_, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid_, - block_hash=block_hash, - ), - ) - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Finalized. " - f"Stake added to netuid: {netuid_}[/dark_sea_green3]" - ) - console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status ) + if not mev_success: + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None - amount_staked = current_balance - new_balance - if allow_partial_stake and (amount_staked != amount_): - console.print( - "Partial stake transaction. Staked:\n" - f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}" - f"[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " - f"instead of " - f"[blue]{amount_}[/blue]" - ) + if json_output: + # the rest of this checking is not necessary if using json_output + return True, "", response + await print_extrinsic_id(response) + block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58_, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid_, + block_hash=block_hash, + ), + ) + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Finalized. " + f"Stake added to netuid: {netuid_}[/dark_sea_green3]" + ) + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + amount_staked = current_balance - new_balance + if allow_partial_stake and (amount_staked != amount_): console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"{netuid_}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " - f"Stake:\n" - f" [blue]{current_stake}[/blue] " - f":arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + "Partial stake transaction. Staked:\n" + f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}" + f"[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"instead of " + f"[blue]{amount_}[/blue]" ) - return True, "", response + + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid_}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Stake:\n" + f" [blue]{current_stake}[/blue] " + f":arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + ) + return True, "", response async def stake_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None @@ -224,6 +240,7 @@ async def stake_extrinsic( failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} ) @@ -235,45 +252,54 @@ async def stake_extrinsic( err_msg = f"{failure_prelude} with error: {format_error_message(e)}" err_out("\n" + err_msg) return False, err_msg, None - else: - if not await response.is_success: - err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" + if not await response.is_success: + err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" + err_out("\n" + err_msg) + return False, err_msg, None + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, err_msg, None - else: - if json_output: - # the rest of this is not necessary if using json_output - return True, "", response - await print_extrinsic_id(response) - new_block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - subtensor.get_balance( - wallet.coldkeypub.ss58_address, block_hash=new_block_hash - ), - subtensor.get_stake( - hotkey_ss58=staking_address_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid_i, - block_hash=new_block_hash, - ), - ) - console.print( - f":white_heavy_check_mark: " - f"[dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" - ) - console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f"{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " - f"Stake:\n" - f" [blue]{current}[/blue] " - f":arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" - ) - return True, "", response + + if json_output: + # the rest of this is not necessary if using json_output + return True, "", response + await print_extrinsic_id(response) + new_block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=new_block_hash + ), + subtensor.get_stake( + hotkey_ss58=staking_address_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid_i, + block_hash=new_block_hash, + ), + ) + console.print( + f":white_heavy_check_mark: " + f"[dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" + ) + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Stake:\n" + f" [blue]{current}[/blue] " + f":arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + ) + return True, "", response netuids = ( netuids if netuids is not None else await subtensor.get_all_subnet_netuids() @@ -435,47 +461,53 @@ async def stake_extrinsic( if not unlock_key(wallet).success: return - if safe_staking: - stake_coroutines = {} - for i, (ni, am, curr, price_with_tolerance) in enumerate( - zip( - netuids, amounts_to_stake, current_stake_balances, prices_with_tolerance - ) - ): - for _, staking_address in hotkeys_to_stake_to: - # Regular extrinsic for root subnet - if ni == 0: - stake_coroutines[(ni, staking_address)] = stake_extrinsic( - netuid_i=ni, - amount_=am, - current=curr, - staking_address_ss58=staking_address, - ) - else: - stake_coroutines[(ni, staking_address)] = safe_stake_extrinsic( - netuid_=ni, - amount_=am, - current_stake=curr, - hotkey_ss58_=staking_address, - price_limit=price_with_tolerance, - ) - else: - stake_coroutines = { - (ni, staking_address): stake_extrinsic( - netuid_i=ni, - amount_=am, - current=curr, - staking_address_ss58=staking_address, - ) - for i, (ni, am, curr) in enumerate( - zip(netuids, amounts_to_stake, current_stake_balances) - ) - for _, staking_address in hotkeys_to_stake_to - } successes = defaultdict(dict) error_messages = defaultdict(dict) extrinsic_ids = defaultdict(dict) - with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): + with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ...") as status: + if safe_staking: + stake_coroutines = {} + for i, (ni, am, curr, price_with_tolerance) in enumerate( + zip( + netuids, + amounts_to_stake, + current_stake_balances, + prices_with_tolerance, + ) + ): + for _, staking_address in hotkeys_to_stake_to: + # Regular extrinsic for root subnet + if ni == 0: + stake_coroutines[(ni, staking_address)] = stake_extrinsic( + netuid_i=ni, + amount_=am, + current=curr, + staking_address_ss58=staking_address, + status=status, + ) + else: + stake_coroutines[(ni, staking_address)] = safe_stake_extrinsic( + netuid_=ni, + amount_=am, + current_stake=curr, + hotkey_ss58_=staking_address, + price_limit=price_with_tolerance, + status=status, + ) + else: + stake_coroutines = { + (ni, staking_address): stake_extrinsic( + netuid_i=ni, + amount_=am, + current=curr, + staking_address_ss58=staking_address, + status=status, + ) + for i, (ni, am, curr) in enumerate( + zip(netuids, amounts_to_stake, current_stake_balances) + ) + for _, staking_address in hotkeys_to_stake_to + } # We can gather them all at once but balance reporting will be in race-condition. for (ni, staking_address), coroutine in stake_coroutines.items(): success, er_msg, ext_receipt = await coroutine From 6558a2d2c56ee645001878ca1df18eb476dcc260 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 13:13:21 -0800 Subject: [PATCH 110/179] Add shield in remove ops --- bittensor_cli/src/commands/stake/remove.py | 33 ++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 5d125cc16..b8f37905f 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -11,6 +11,11 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + encrypt_call, + extract_mev_shield_id, + wait_for_mev_execution, +) from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( console, @@ -601,6 +606,8 @@ async def _unstake_extrinsic( }, ), ) + + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -615,6 +622,17 @@ async def _unstake_extrinsic( f"{format_error_message(await response.error_message)}" ) return False, None + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None + # Fetch latest balance and stake await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() @@ -680,7 +698,6 @@ async def _safe_unstake_extrinsic( current_balance, next_nonce, current_stake, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), - subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), subtensor.get_stake( hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address, @@ -701,8 +718,9 @@ async def _safe_unstake_extrinsic( ), ) + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + call=call, keypair=wallet.coldkey, era={"period": era} ) try: @@ -726,6 +744,17 @@ async def _safe_unstake_extrinsic( f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) return False, None + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None + await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( From b1f3770acb97064a7d164259b3aedcc98a4fdd1d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 21:56:08 -0800 Subject: [PATCH 111/179] add error handling --- .../src/bittensor/extrinsics/mev_shield.py | 141 +++++++++++++----- 1 file changed, 103 insertions(+), 38 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 5c437039b..395fbf636 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -2,13 +2,13 @@ import hashlib from typing import TYPE_CHECKING, Optional +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_drand import encrypt_mlkem768, mlkem_kdf_id -from bittensor_cli.src.bittensor.utils import encode_account_id +from bittensor_cli.src.bittensor.utils import encode_account_id, format_error_message if TYPE_CHECKING: from bittensor_wallet import Wallet from scalecodec import GenericCall - from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -112,7 +112,7 @@ async def wait_for_mev_execution( wrapper_id: str, timeout_blocks: int = 4, status=None, -) -> tuple[bool, Optional[str]]: +) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]: """ Wait for MEV Shield inner call execution. @@ -127,11 +127,14 @@ async def wait_for_mev_execution( status: Optional rich.Status object for progress updates. Returns: - Tuple of (success: bool, error: Optional[str]). - - (True, None) if DecryptedExecuted was found. - - (False, error_message) if the call failed or timeout. + Tuple of (success: bool, error: Optional[str], receipt: Optional[AsyncExtrinsicReceipt]). + - (True, None, receipt) if DecryptedExecuted was found. + - (False, error_message, None) if the call failed or timeout. """ + async def _noop(_): + return True + start_block = await subtensor.substrate.get_block_number() current_block = start_block @@ -143,15 +146,12 @@ async def wait_for_mev_execution( ) block_hash = await subtensor.substrate.get_block_hash(current_block) - events, extrinsics = await asyncio.gather( - subtensor.substrate.get_events(block_hash), - subtensor.substrate.get_extrinsics(block_hash), - ) + extrinsics = await subtensor.substrate.get_extrinsics(block_hash) - # Look for execute_revealed extrinsic + # Find executeRevealed extrinsic & match ids execute_revealed_index = None for idx, extrinsic in enumerate(extrinsics): - call = extrinsic.get("call", {}) + call = extrinsic.value.get("call", {}) call_module = call.get("call_module") call_function = call.get("call_function") @@ -167,31 +167,96 @@ async def wait_for_mev_execution( if execute_revealed_index is not None: break - # Check for success or failure events in the extrinsic - if execute_revealed_index is not None: - for event in events: - event_id = event.get("event_id", "") - event_extrinsic_idx = event.get("extrinsic_idx") - - if event_extrinsic_idx == execute_revealed_index: - if event_id == "ExtrinsicSuccess": - return True, None - elif event_id == "ExtrinsicFailed": - dispatch_error = event.get("attributes", {}).get( - "dispatch_error", {} - ) - error_msg = f"{dispatch_error}" - return False, error_msg - - current_block += 1 - - async def _noop(_): - return True - - await subtensor.substrate.wait_for_block( - current_block, - result_handler=_noop, - task_return=False, + if execute_revealed_index is None: + current_block += 1 + await subtensor.substrate.wait_for_block( + current_block, + result_handler=_noop, + task_return=False, + ) + continue + + receipt = AsyncExtrinsicReceipt( + substrate=subtensor.substrate, + block_hash=block_hash, + extrinsic_idx=execute_revealed_index, ) - return False, "Timeout waiting for MEV Shield execution" + # TODO: Activate this when we update up-stream + # if not await receipt.is_success: + # error_msg = format_error_message(await receipt.error_message) + # return False, error_msg, None + + error = await check_mev_shield_error(receipt, subtensor, wrapper_id) + if error: + error_msg = format_error_message(error) + return False, error_msg, None + + return True, None, receipt + + return False, "Timeout waiting for MEV Shield execution", None + + +async def check_mev_shield_error( + receipt: AsyncExtrinsicReceipt, + subtensor: "SubtensorInterface", + wrapper_id: str, +) -> Optional[dict]: + """ + Handles & extracts error messages in the MEV Shield extrinsics. + This is a temporary implementation until we update up-stream code. + + Args: + receipt: AsyncExtrinsicReceipt for the execute_revealed extrinsic. + subtensor: SubtensorInterface instance. + wrapper_id: The wrapper ID to verify we're checking the correct event. + + Returns: + Error dict to be used with format_error_message(), or None if no error. + """ + if not await receipt.is_success: + return await receipt.error_message + + for event in await receipt.triggered_events: + event_details = event.get("event", {}) + + if ( + event_details.get("module_id") == "MevShield" + and event_details.get("event_id") == "DecryptedRejected" + ): + attributes = event_details.get("attributes", {}) + event_wrapper_id = attributes.get("id") + + if event_wrapper_id != wrapper_id: + continue + + reason = attributes.get("reason", {}) + dispatch_error = reason.get("error", {}) + + try: + if "Module" in dispatch_error: + module_index = dispatch_error["Module"]["index"] + error_index = dispatch_error["Module"]["error"] + + if isinstance(error_index, str) and error_index.startswith("0x"): + error_index = int(error_index[2:4], 16) + + runtime = await subtensor.substrate.init_runtime( + block_hash=receipt.block_hash + ) + module_error = runtime.metadata.get_module_error( + module_index=module_index, + error_index=error_index, + ) + + return { + "type": "Module", + "name": module_error.name, + "docs": module_error.docs, + } + except Exception: + return dispatch_error + + return dispatch_error + + return None From 4b324674086885b7efa970df329979a35a4d2113 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 21:56:21 -0800 Subject: [PATCH 112/179] improve waiting --- .../src/bittensor/extrinsics/mev_shield.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 395fbf636..ea9da9ad9 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -136,15 +136,21 @@ async def _noop(_): return True start_block = await subtensor.substrate.get_block_number() - current_block = start_block + current_block = start_block + 1 - while current_block - start_block < timeout_blocks: + while current_block - start_block <= timeout_blocks: if status: status.update( - f":hourglass: Waiting for MEV Shield execution " - f"(block {current_block - start_block + 1}/{timeout_blocks})..." + f"Waiting for :shield: MEV Protection " + f"(checking block {current_block - start_block} of {timeout_blocks})..." ) + await subtensor.substrate.wait_for_block( + current_block, + result_handler=_noop, + task_return=False, + ) + block_hash = await subtensor.substrate.get_block_hash(current_block) extrinsics = await subtensor.substrate.get_extrinsics(block_hash) @@ -169,11 +175,6 @@ async def _noop(_): if execute_revealed_index is None: current_block += 1 - await subtensor.substrate.wait_for_block( - current_block, - result_handler=_noop, - task_return=False, - ) continue receipt = AsyncExtrinsicReceipt( From 2ca2ddff385a66663bed617ba405be7bc2fd8c3a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 21:57:23 -0800 Subject: [PATCH 113/179] update add/remove --- bittensor_cli/src/commands/stake/add.py | 4 ++-- bittensor_cli/src/commands/stake/remove.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 0e927c5f3..9bcf2c858 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -169,7 +169,7 @@ async def safe_stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: - mev_success, mev_error = await wait_for_mev_execution( + mev_success, mev_error, response = await wait_for_mev_execution( subtensor, mev_shield_id, status=status ) if not mev_success: @@ -259,7 +259,7 @@ async def stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: - mev_success, mev_error = await wait_for_mev_execution( + mev_success, mev_error, response = await wait_for_mev_execution( subtensor, mev_shield_id, status=status ) if not mev_success: diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index b8f37905f..e6dc4b38b 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -625,7 +625,7 @@ async def _unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: - mev_success, mev_error = await wait_for_mev_execution( + mev_success, mev_error, response = await wait_for_mev_execution( subtensor, mev_shield_id, status=status ) if not mev_success: @@ -747,7 +747,7 @@ async def _safe_unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: - mev_success, mev_error = await wait_for_mev_execution( + mev_success, mev_error, response = await wait_for_mev_execution( subtensor, mev_shield_id, status=status ) if not mev_success: From 142d93a635d891cbb30046f9f08c49e3036064e6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 21:57:56 -0800 Subject: [PATCH 114/179] add protection to move stakes --- bittensor_cli/src/commands/stake/move.py | 44 ++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 99a0b79ac..e28ecf3d0 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -8,6 +8,11 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + encrypt_call, + extract_mev_shield_id, + wait_for_mev_execution, +) from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -578,13 +583,24 @@ async def move_stake( f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: " f"[blue]{origin_netuid}[/blue] \nto " f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." - ): + ) as status: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" + ext_id = await response.get_extrinsic_identifier() if not prompt: @@ -765,7 +781,8 @@ async def transfer_stake( if not unlock_key(wallet).success: return False, "" - with console.status("\n:satellite: Transferring stake ..."): + with console.status("\n:satellite: Transferring stake ...") as status: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -773,6 +790,16 @@ async def transfer_stake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" + ext_id = await response.get_extrinsic_identifier() if not prompt: @@ -940,7 +967,8 @@ async def swap_stake( with console.status( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " f"to netuid [blue]{destination_netuid}[/blue]..." - ): + ) as status: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -950,6 +978,16 @@ async def swap_stake( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, status=status + ) + if not mev_success: + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" + ext_id = await response.get_extrinsic_identifier() if not prompt: From 00d7b636aceeb8b1151d12148cb09c11dbbe9c25 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 27 Nov 2025 22:04:27 -0800 Subject: [PATCH 115/179] remove statuses when in error state --- bittensor_cli/src/commands/stake/add.py | 2 ++ bittensor_cli/src/commands/stake/move.py | 3 +++ bittensor_cli/src/commands/stake/remove.py | 2 ++ 3 files changed, 7 insertions(+) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 9bcf2c858..e6d9490d5 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -173,6 +173,7 @@ async def safe_stake_extrinsic( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, err_msg, None @@ -263,6 +264,7 @@ async def stake_extrinsic( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, err_msg, None diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index e28ecf3d0..294e6a159 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -598,6 +598,7 @@ async def move_stake( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" @@ -797,6 +798,7 @@ async def transfer_stake( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" @@ -985,6 +987,7 @@ async def swap_stake( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index e6dc4b38b..91fdc69f8 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -629,6 +629,7 @@ async def _unstake_extrinsic( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, None @@ -751,6 +752,7 @@ async def _safe_unstake_extrinsic( subtensor, mev_shield_id, status=status ) if not mev_success: + status.stop() err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, None From ef1225761417b2ac5a2c73cc21de51879252edeb Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 28 Nov 2025 10:44:45 +0200 Subject: [PATCH 116/179] Nicely working test for pure proxies. --- bittensor_cli/cli.py | 11 +- bittensor_cli/src/bittensor/utils.py | 11 +- bittensor_cli/src/commands/proxy.py | 25 +- tests/e2e_tests/test_proxy.py | 511 ++++++++++++++++----------- 4 files changed, 334 insertions(+), 224 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 60b7c2b9f..07bfa5c17 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8939,8 +8939,14 @@ def proxy_kill( ] = None, network: Optional[list[str]] = Options.network, proxy_type: ProxyType = Options.proxy_type, - proxy: Optional[str] = Options.proxy, - idx: int = typer.Option(0, "--index", help="TODO lol"), + proxy: str = Options.proxy, + announce_only: bool = Options.announce_only, + idx: int = typer.Option( + 0, + "--index", + help="A disambiguation index, in case this is called multiple times in the same transaction" + " (e.g. with utility::batch). Unless you're using batch you probably just want to use 0.", + ), wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -8993,6 +8999,7 @@ def proxy_kill( proxy_type=proxy_type, height=height, proxy=proxy, + announce_only=announce_only, ext_index=ext_index, idx=idx, spawner=spawner, diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 428f1e672..d65974c11 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1104,11 +1104,16 @@ class DB: def __init__( self, - db_path: str = os.path.join( - os.path.expanduser(defaults.config.base_path), "bittensor.db" - ), + db_path: Optional[str] = None, row_factory=None, ): + if db_path is None: + if path_from_env := os.getenv("BTCLI_PROXIES_PATH"): + db_path = path_from_env + else: + db_path = os.path.join( + os.path.expanduser(defaults.config.base_path), "bittensor.db" + ) self.db_path = db_path self.conn: Optional[sqlite3.Connection] = None self.row_factory = row_factory diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 7834622de..89e118016 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -64,6 +64,7 @@ async def submit_proxy( period: int, json_output: bool, proxy: Optional[str] = None, + announce_only: bool = False, ) -> None: success, msg, receipt = await subtensor.sign_and_send_extrinsic( call=call, @@ -72,6 +73,7 @@ async def submit_proxy( wait_for_finalization=wait_for_finalization, era={"period": period}, proxy=proxy, + announce_only=announce_only, ) if success: await print_extrinsic_id(receipt) @@ -80,7 +82,7 @@ async def submit_proxy( data={ "success": success, "message": msg, - "extrinsic_id": await receipt.get_extrinsic_identifier(), + "extrinsic_identifier": await receipt.get_extrinsic_identifier(), } ) else: @@ -92,7 +94,7 @@ async def submit_proxy( data={ "success": success, "message": msg, - "extrinsic_id": None, + "extrinsic_identifier": None, } ) else: @@ -152,7 +154,7 @@ async def create_proxy( data={ "success": ulw.success, "message": ulw.message, - "extrinsic_id": None, + "extrinsic_identifier": None, } ) return False, "", "", "" @@ -231,7 +233,7 @@ async def create_proxy( "proxy_type": created_proxy_type.value, "delay": delay, }, - "extrinsic_id": await receipt.get_extrinsic_identifier(), + "extrinsic_identifier": await receipt.get_extrinsic_identifier(), } ) @@ -242,7 +244,7 @@ async def create_proxy( "success": success, "message": msg, "data": None, - "extrinsic_id": None, + "extrinsic_identifier": None, } ) else: @@ -276,7 +278,7 @@ async def remove_proxy( data={ "success": ulw.success, "message": ulw.message, - "extrinsic_id": None, + "extrinsic_identifier": None, } ) return None @@ -332,7 +334,7 @@ async def add_proxy( data={ "success": ulw.success, "message": ulw.message, - "extrinsic_id": None, + "extrinsic_identifier": None, } ) return None @@ -406,7 +408,7 @@ async def add_proxy( data={ "success": success, "message": msg, - "extrinsic_id": await receipt.get_extrinsic_identifier(), + "extrinsic_identifier": await receipt.get_extrinsic_identifier(), } ) @@ -416,7 +418,7 @@ async def add_proxy( data={ "success": success, "message": msg, - "extrinsic_id": None, + "extrinsic_identifier": None, } ) else: @@ -433,6 +435,7 @@ async def kill_proxy( spawner: Optional[str], idx: int, proxy: Optional[str], + announce_only: bool, prompt: bool, wait_for_inclusion: bool, wait_for_finalization: bool, @@ -456,7 +459,7 @@ async def kill_proxy( data={ "success": ulw.success, "message": ulw.message, - "extrinsic_id": None, + "extrinsic_identifier": None, } ) return None @@ -472,7 +475,6 @@ async def kill_proxy( "spawner": spawner, }, ) - return await submit_proxy( subtensor=subtensor, wallet=wallet, @@ -482,6 +484,7 @@ async def kill_proxy( period=period, json_output=json_output, proxy=proxy, + announce_only=announce_only, ) diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index 85b9d0448..c309bf0b9 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -1,5 +1,7 @@ import asyncio import json +import os +import time from time import sleep from bittensor_cli.src.bittensor.balances import Balance @@ -29,6 +31,8 @@ def test_proxy_create(local_chain, wallet_setup): """ Tests the pure proxy logic (create/kill) """ + testing_db_loc = "/tmp/btcli-test.db" + os.environ["BTCLI_PROXIES_PATH"] = testing_db_loc wallet_path_alice = "//Alice" wallet_path_bob = "//Bob" @@ -42,221 +46,312 @@ def test_proxy_create(local_chain, wallet_setup): proxy_type = "Any" delay = 1 - # create a pure proxy - create_result = exec_command_alice( - command="proxy", - sub_command="create", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--proxy-type", - proxy_type, - "--delay", - str(delay), - "--period", - "128", - "--no-prompt", - "--json-output", - ], - ) - create_result_output = json.loads(create_result.stdout) - assert create_result_output["success"] is True - assert create_result_output["message"] is not None - assert create_result_output["extrinsic_id"] is not None - created_pure = create_result_output["data"]["pure"] - spawner = create_result_output["data"]["spawner"] - created_proxy_type = create_result_output["data"]["proxy_type"] - created_delay = create_result_output["data"]["delay"] - assert isinstance(created_pure, str) - assert isinstance(spawner, str) - assert spawner == wallet_alice.coldkeypub.ss58_address - assert created_proxy_type == proxy_type - assert created_delay == delay + try: + # create a pure proxy + create_result = exec_command_alice( + command="proxy", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy-type", + proxy_type, + "--delay", + str(delay), + "--period", + "128", + "--no-prompt", + "--json-output", + ], + ) + create_result_output = json.loads(create_result.stdout) + assert create_result_output["success"] is True + assert create_result_output["message"] is not None + assert create_result_output["extrinsic_identifier"] is not None + created_extrinsic_id = create_result_output["extrinsic_identifier"].split("-") + created_block = int(created_extrinsic_id[0]) + created_extrinsic_idx = int(created_extrinsic_id[1]) + created_pure = create_result_output["data"]["pure"] + spawner = create_result_output["data"]["spawner"] + created_proxy_type = create_result_output["data"]["proxy_type"] + created_delay = create_result_output["data"]["delay"] + assert isinstance(created_pure, str) + assert isinstance(spawner, str) + assert spawner == wallet_alice.coldkeypub.ss58_address + assert created_proxy_type == proxy_type + assert created_delay == delay + print("Passed pure creation.") - # transfer some funds from alice to the pure proxy - amount_to_transfer = 1_000 - transfer_result = exec_command_alice( - command="wallet", - sub_command="transfer", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--dest", - created_pure, - "--amount", - str(amount_to_transfer), - "--no-prompt", - "--json-output", - ], - ) - transfer_result_output = json.loads(transfer_result.stdout) - assert transfer_result_output["success"] is True + # transfer some funds from alice to the pure proxy + amount_to_transfer = 1_000 + transfer_result = exec_command_alice( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--dest", + created_pure, + "--amount", + str(amount_to_transfer), + "--no-prompt", + "--json-output", + ], + ) + transfer_result_output = json.loads(transfer_result.stdout) + assert transfer_result_output["success"] is True - # ensure the proxy has the transferred funds - 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", - "--ss58", - created_pure, - "--json-output", - ], - ) - balance_result_output = json.loads(balance_result.stdout) - assert ( - balance_result_output["balances"]["Provided Address 1"]["coldkey"] - == created_pure - ) - assert balance_result_output["balances"]["Provided Address 1"]["free"] == float( - amount_to_transfer - ) + # ensure the proxy has the transferred funds + 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", + "--ss58", + created_pure, + "--json-output", + ], + ) + balance_result_output = json.loads(balance_result.stdout) + assert ( + balance_result_output["balances"]["Provided Address 1"]["coldkey"] + == created_pure + ) + assert balance_result_output["balances"]["Provided Address 1"]["free"] == float( + amount_to_transfer + ) - # transfer some of the pure proxy's funds to bob, but don't announce it - amount_to_transfer_proxy = 100 - transfer_result_proxy = exec_command_alice( - command="wallet", - sub_command="transfer", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--proxy", - created_pure, - "--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 it wasn't announced - assert transfer_result_proxy_output["success"] is False + # transfer some of the pure proxy's funds to bob, but don't announce it + amount_to_transfer_proxy = 100 + transfer_result_proxy = exec_command_alice( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--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 it wasn't announced + assert transfer_result_proxy_output["success"] is False - # announce the same extrinsic - transfer_result_proxy = exec_command_alice( - command="wallet", - sub_command="transfer", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--proxy", - created_pure, - "--dest", - keypair_bob.ss58_address, - "--amount", - str(amount_to_transfer_proxy), - "--no-prompt", - "--json-output", - "--announce-only", - ], - ) - print(transfer_result_proxy.stdout, transfer_result_proxy.stderr) - transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) - assert transfer_result_proxy_output["success"] is True - with ProxyAnnouncements.get_db() as (conn, cursor): - rows = ProxyAnnouncements.read_rows(conn, cursor, include_header=False) - latest_announcement = next( - iter(sorted(rows, key=lambda row: row[2], reverse=True)) - ) # sort by epoch time - idx, address, epoch_time, block, call_hash, call, call_serialized, executed_int = ( - latest_announcement - ) - assert address == created_pure - assert executed_int == 0 + # announce the same extrinsic + transfer_result_proxy = exec_command_alice( + command="wallet", + sub_command="transfer", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--dest", + keypair_bob.ss58_address, + "--amount", + str(amount_to_transfer_proxy), + "--no-prompt", + "--json-output", + "--announce-only", + ], + ) + print(transfer_result_proxy.stdout, transfer_result_proxy.stderr) + transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) + assert transfer_result_proxy_output["success"] is True + with ProxyAnnouncements.get_db() as (conn, cursor): + rows = ProxyAnnouncements.read_rows(conn, cursor, include_header=False) + latest_announcement = next( + iter(sorted(rows, key=lambda row: row[2], reverse=True)) + ) # sort by epoch time + ( + idx, + address, + epoch_time, + block, + call_hash, + call, + call_serialized, + executed_int, + ) = latest_announcement + assert address == created_pure + assert executed_int == 0 - async def _handler(_): - return True + # wait for delay (probably already happened if fastblocks is on) + time.sleep(3) - # wait for delay (probably already happened if fastblocks is on) - asyncio.run(local_chain.wait_for_block(block + delay, _handler, False)) + # get 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"] - # get 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"] + announce_execution_result = exec_command_alice( + command="proxy", + sub_command="execute", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--call-hash", + call_hash, + "--no-prompt", + "--json-output", + ], + ) + print(announce_execution_result.stdout, announce_execution_result.stderr) + announce_execution_result_output = json.loads(announce_execution_result.stdout) + assert announce_execution_result_output["success"] is True + assert announce_execution_result_output["message"] == "" + + # ensure bob has the transferred 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"]["coldkey"] + == wallet_bob.coldkeypub.ss58_address + ) + assert ( + balance_result_output["balances"]["default"]["free"] + == float(amount_to_transfer_proxy) + bob_init_balance + ) + print("Passed transfer with announcement") + + # announce kill of the created pure proxy + announce_kill_result = exec_command_alice( + command="proxy", + sub_command="kill", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--height", + str(created_block), + "--ext-index", + str(created_extrinsic_idx), + "--spawner", + spawner, + "--proxy-type", + created_proxy_type, + "--proxy", + created_pure, + "--json-output", + "--no-prompt", + "--announce-only", + ], + ) + print(announce_kill_result.stdout, announce_kill_result.stderr) + kill_result_output = json.loads(announce_kill_result.stdout) + assert kill_result_output["success"] is True + assert kill_result_output["message"] == "" + assert isinstance(kill_result_output["extrinsic_identifier"], str) + print("Passed kill announcement") - announce_execution_result = exec_command_alice( - command="proxy", - sub_command="execute", - extra_args=[ - "--wallet-path", - wallet_path_alice, - "--chain", - "ws://127.0.0.1:9945", - "--wallet-name", - "default", - "--proxy", - created_pure, - "--call-hash", + with ProxyAnnouncements.get_db() as (conn, cursor): + rows = ProxyAnnouncements.read_rows(conn, cursor, include_header=False) + latest_announcement = next( + iter(sorted(rows, key=lambda row: row[2], reverse=True)) + ) # sort by epoch time + ( + idx, + address, + epoch_time, + block, call_hash, - "--no-prompt", - "--json-output", - ], - ) - print(announce_execution_result.stdout, announce_execution_result.stderr) - announce_execution_result_output = json.loads(announce_execution_result.stdout) - assert announce_execution_result_output["success"] is True - assert announce_execution_result_output["message"] == "" + call, + call_serialized, + executed_int, + ) = latest_announcement + assert address == created_pure + assert executed_int == 0 + # wait for delay (probably already happened if fastblocks is on) + time.sleep(3) - # ensure bob has the transferred 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"]["coldkey"] - == wallet_bob.coldkeypub.ss58_address - ) - assert ( - balance_result_output["balances"]["default"]["free"] - == float(amount_to_transfer_proxy) + bob_init_balance - ) + kill_announce_execution_result = exec_command_alice( + command="proxy", + sub_command="execute", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + created_pure, + "--call-hash", + call_hash, + "--no-prompt", + "--json-output", + ], + ) + kill_announce_execution_result_output = json.loads( + kill_announce_execution_result.stdout + ) + assert kill_announce_execution_result_output["success"] is True + assert kill_announce_execution_result_output["message"] == "" + finally: + os.environ["BTCLI_PROXIES_PATH"] = "" + if os.path.exists(testing_db_loc): + os.remove(testing_db_loc) From 7aceee6588960aea15a51574936c26a06cdec089 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 28 Nov 2025 10:45:32 +0200 Subject: [PATCH 117/179] Removed unused imports --- tests/e2e_tests/test_proxy.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index c309bf0b9..dd2473dd1 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -1,20 +1,8 @@ -import asyncio import json import os import time -from time import sleep -from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.utils import ProxyAnnouncements, decode_account_id -from .utils import ( - extract_coldkey_balance, - validate_wallet_inspect, - validate_wallet_overview, - verify_subnet_entry, -) - -# temporary -from async_substrate_interface.sync_substrate import SubstrateInterface +from bittensor_cli.src.bittensor.utils import ProxyAnnouncements """ Verify commands: From f9c5b7759115a399d3c975f27127cd33a56d7d45 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 28 Nov 2025 12:03:34 +0200 Subject: [PATCH 118/179] Proxy add/remove test --- bittensor_cli/cli.py | 4 +- bittensor_cli/src/commands/proxy.py | 10 +- tests/e2e_tests/test_proxy.py | 343 +++++++++++++++++++++++++++- 3 files changed, 353 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 07bfa5c17..1b6472c82 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -8782,7 +8782,7 @@ def proxy_add( prompt="Enter the SS58 address of the delegate to add, e.g. 5dxds...", help="The SS58 address of the delegate to add", ), - ], + ] = "", network: Optional[list[str]] = Options.network, proxy_type: ProxyType = Options.proxy_type, delay: int = typer.Option(0, help="Delay, in number of blocks"), @@ -8854,7 +8854,7 @@ def proxy_remove( prompt="Enter the SS58 address of the delegate to remove, e.g. 5dxds...", help="The SS58 address of the delegate to remove", ), - ], + ] = "", network: Optional[list[str]] = Options.network, proxy_type: ProxyType = Options.proxy_type, delay: int = typer.Option(0, help="Delay, in number of blocks"), diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 89e118016..9621b5d0e 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -360,11 +360,12 @@ async def add_proxy( delegator = None created_proxy_type = None for event in await receipt.triggered_events: - if event["event_id"] == "PureCreated": + if event["event_id"] == "ProxyAdded": attrs = event["attributes"] delegatee = attrs["delegatee"] delegator = attrs["delegator"] created_proxy_type = getattr(ProxyType, attrs["proxy_type"]) + break arg_start = "`" if json_output else "[blue]" arg_end = "`" if json_output else "[/blue]" msg = ( @@ -408,6 +409,12 @@ async def add_proxy( data={ "success": success, "message": msg, + "data": { + "delegatee": delegatee, + "delegator": delegator, + "proxy_type": created_proxy_type.value, + "delay": delay, + }, "extrinsic_identifier": await receipt.get_extrinsic_identifier(), } ) @@ -418,6 +425,7 @@ async def add_proxy( data={ "success": success, "message": msg, + "data": None, "extrinsic_identifier": None, } ) diff --git a/tests/e2e_tests/test_proxy.py b/tests/e2e_tests/test_proxy.py index dd2473dd1..e5e76724e 100644 --- a/tests/e2e_tests/test_proxy.py +++ b/tests/e2e_tests/test_proxy.py @@ -18,6 +18,18 @@ def test_proxy_create(local_chain, wallet_setup): """ Tests the pure proxy logic (create/kill) + + Steps: + 1. Creates pure proxy (with delay) + 2. Fund pure proxy + 3. Verifies pure proxy balance + 4. Ensures unannounced call fails (bc of delay at creation) + 4. Makes announcement of pure proxy's intent to transfer to Bob + 5. Executes previous announcement of transfer to Bob + 6. Ensures Bob has received the funds + 7. Makes announcement of pure proxy's intent to kill + 8. Kills pure proxy + """ testing_db_loc = "/tmp/btcli-test.db" os.environ["BTCLI_PROXIES_PATH"] = testing_db_loc @@ -233,7 +245,6 @@ def test_proxy_create(local_chain, wallet_setup): "--json-output", ], ) - print(announce_execution_result.stdout, announce_execution_result.stderr) announce_execution_result_output = json.loads(announce_execution_result.stdout) assert announce_execution_result_output["success"] is True assert announce_execution_result_output["message"] == "" @@ -343,3 +354,333 @@ def test_proxy_create(local_chain, wallet_setup): os.environ["BTCLI_PROXIES_PATH"] = "" if os.path.exists(testing_db_loc): os.remove(testing_db_loc) + + +def test_add_proxy(local_chain, wallet_setup): + """ + Tests the non-pure (delegated) proxy logic (add/remove) + + Steps: + 1. Add Dave as a proxy of Alice (with delay) + 2. Attempt proxy transfer without announcement (it should fail) + 3. Make proxy transfer to Bob + 4. Ensure Bob got the funds, the funds were deducted from Alice, and that Dave paid the ext fee + 5. Remove Dave as a proxy of Alice + """ + 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 and Bob + 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 = "Any" + delay = 1 + + 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 "Added proxy delegatee" in add_result_output["message"] + 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 Add successful") + + # Check dave's init balance + dave_balance_result = exec_command_dave( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--wallet-name", + "default", + "--json-output", + "--chain", + "ws://127.0.0.1:9945", + ], + ) + dave_balance_output = json.loads(dave_balance_result.stdout) + assert ( + dave_balance_output["balances"]["default"]["coldkey"] + == wallet_dave.coldkeypub.ss58_address + ) + dave_init_balance = dave_balance_output["balances"]["default"]["free"] + + # Check Bob's init 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 init 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"] + + # transfer some of alice's funds to bob through the proxy, but don't announce it + 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 it wasn't announced + assert transfer_result_proxy_output["success"] is False + + # announce the same extrinsic + 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", + "--announce-only", + ], + ) + print(transfer_result_proxy.stdout, transfer_result_proxy.stderr) + transfer_result_proxy_output = json.loads(transfer_result_proxy.stdout) + assert transfer_result_proxy_output["success"] is True + with ProxyAnnouncements.get_db() as (conn, cursor): + rows = ProxyAnnouncements.read_rows(conn, cursor, include_header=False) + latest_announcement = next( + iter(sorted(rows, key=lambda row: row[2], reverse=True)) + ) # sort by epoch time + ( + idx, + address, + epoch_time, + block, + call_hash, + call, + call_serialized, + executed_int, + ) = latest_announcement + assert address == wallet_alice.coldkeypub.ss58_address + assert executed_int == 0 + + # wait for delay (probably already happened if fastblocks is on) + time.sleep(3) + + announce_execution_result = exec_command_dave( + command="proxy", + sub_command="execute", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + "default", + "--proxy", + wallet_alice.coldkeypub.ss58_address, + "--call-hash", + call_hash, + "--no-prompt", + "--json-output", + ], + ) + announce_execution_result_output = json.loads(announce_execution_result.stdout) + assert announce_execution_result_output["success"] is True + assert announce_execution_result_output["message"] == "" + + # ensure bob has the transferred 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"]["coldkey"] + == wallet_bob.coldkeypub.ss58_address + ) + assert ( + balance_result_output["balances"]["default"]["free"] + == float(amount_to_transfer_proxy) + bob_init_balance + ) + + # ensure the amount was subtracted from alice's balance, not dave's + 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 + ) + assert balance_result_output["balances"]["default"][ + "free" + ] == alice_init_balance - float(amount_to_transfer_proxy) + + # ensure dave paid the extrinsic fee + balance_result = exec_command_dave( + command="wallet", + sub_command="balance", + extra_args=[ + "--wallet-path", + wallet_path_dave, + "--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_dave.coldkeypub.ss58_address + ) + assert balance_result_output["balances"]["default"]["free"] < dave_init_balance + + print("Passed transfer with announcement") + + # remove the proxy + 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("Passed proxy removal") + finally: + os.environ["BTCLI_PROXIES_PATH"] = "" + if os.path.exists(testing_db_loc): + os.remove(testing_db_loc) From 9881a35a80192e8791d3753329bf7cfdb74cc260 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 1 Dec 2025 17:27:47 +0200 Subject: [PATCH 119/179] Fixed tests --- tests/e2e_tests/test_unstaking.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index f3173b5a7..cc29b614e 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -387,9 +387,7 @@ def test_unstaking(local_chain, wallet_setup): ], ) - assert ( - "✅ Finalized: Successfully unstaked all Alpha stakes" in unstake_alpha.stdout - ) + assert "✅ Included: Successfully unstaked all Alpha stakes" in unstake_alpha.stdout assert "Your extrinsic has been included" in unstake_alpha.stdout, ( unstake_alpha.stdout ) @@ -442,6 +440,6 @@ def test_unstaking(local_chain, wallet_setup): "144", ], ) - assert "✅ Finalized: Successfully unstaked all stakes from" in unstake_all.stdout + assert "✅ Included: Successfully unstaked all stakes from" in unstake_all.stdout assert "Your extrinsic has been included" in unstake_all.stdout, unstake_all.stdout print("Passed unstaking tests 🎉") From 227a5aa57ebeb57b2ceb5427000400808ee18a6e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 10:57:03 -0800 Subject: [PATCH 120/179] use submission block directly --- bittensor_cli/src/bittensor/extrinsics/mev_shield.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index ea9da9ad9..3ba60446c 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -110,6 +110,7 @@ async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[s async def wait_for_mev_execution( subtensor: "SubtensorInterface", wrapper_id: str, + submit_block_hash: str, timeout_blocks: int = 4, status=None, ) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]: @@ -123,6 +124,7 @@ async def wait_for_mev_execution( Args: subtensor: SubtensorInterface instance. wrapper_id: The ID from EncryptedSubmitted event. + submit_block_number: Block number where submit_encrypted was included. timeout_blocks: Max blocks to wait (default 4). status: Optional rich.Status object for progress updates. @@ -135,14 +137,14 @@ async def wait_for_mev_execution( async def _noop(_): return True - start_block = await subtensor.substrate.get_block_number() - current_block = start_block + 1 + starting_block = await subtensor.substrate.get_block_number(submit_block_hash) + current_block = starting_block + 1 - while current_block - start_block <= timeout_blocks: + while current_block - starting_block <= timeout_blocks: if status: status.update( f"Waiting for :shield: MEV Protection " - f"(checking block {current_block - start_block} of {timeout_blocks})..." + f"(checking block {current_block - starting_block} of {timeout_blocks})..." ) await subtensor.substrate.wait_for_block( From 0378be3786c5852b4ca63f607c5ae9c2ef01c43d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 10:57:38 -0800 Subject: [PATCH 121/179] handle edge case --- bittensor_cli/src/bittensor/subtensor_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 2af67f2ca..a733ed63e 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2175,8 +2175,8 @@ async def get_claimable_stakes_for_coldkey( root_stake: Balance claimable_stake: Balance for hotkey, netuid in target_pairs: - root_stake = root_stakes[hotkey] - rate = claimable_rates[hotkey].get(netuid, 0.0) + root_stake = root_stakes.get(hotkey, Balance(0)) + rate = claimable_rates.get(hotkey, {}).get(netuid, 0.0) claimable_stake = rate * root_stake already_claimed = claimed_amounts.get((hotkey, netuid), Balance(0)) net_claimable = max(claimable_stake - already_claimed, Balance(0)) From 11d8228f9003f2b75073304a510b52032555561f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 10:58:25 -0800 Subject: [PATCH 122/179] update mev shield calls --- bittensor_cli/src/commands/stake/add.py | 4 ++-- bittensor_cli/src/commands/stake/move.py | 6 +++--- bittensor_cli/src/commands/stake/remove.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index e6d9490d5..c97743d9b 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -170,7 +170,7 @@ async def safe_stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() @@ -261,7 +261,7 @@ async def stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 294e6a159..95303873e 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -595,7 +595,7 @@ async def move_stake( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() @@ -795,7 +795,7 @@ async def transfer_stake( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() @@ -984,7 +984,7 @@ async def swap_stake( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 91fdc69f8..0531c97ee 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -626,7 +626,7 @@ async def _unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() @@ -697,7 +697,7 @@ async def _safe_unstake_extrinsic( block_hash = await subtensor.substrate.get_chain_head() - current_balance, next_nonce, current_stake, call = await asyncio.gather( + current_balance, current_stake, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), subtensor.get_stake( hotkey_ss58=hotkey_ss58, @@ -749,7 +749,7 @@ async def _safe_unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, status=status + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: status.stop() From 55ea2d39036cab784adc8f04ba5ed0030055495e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 10:59:01 -0800 Subject: [PATCH 123/179] update e2e tests --- tests/e2e_tests/test_staking_sudo.py | 80 ++++++++++++++-------------- tests/e2e_tests/test_unstaking.py | 3 +- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 6bcaa60cc..719063d3e 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -1,6 +1,7 @@ import asyncio import json import re +import pytest from typing import Union from bittensor_cli.src.bittensor.balances import Balance @@ -23,7 +24,7 @@ * btcli sudo get """ - +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_staking(local_chain, wallet_setup): """ Test staking & sudo commands and inspect their output @@ -402,7 +403,7 @@ def test_staking(local_chain, wallet_setup): for line in show_stake_adding_single.stdout.splitlines() ] stake_added = cleaned_stake[8].split("│")[3].strip().split()[0] - assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(90) + assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(87) show_stake_json = exec_command_alice( command="stake", @@ -419,7 +420,7 @@ def test_staking(local_chain, wallet_setup): ) show_stake_json_output = json.loads(show_stake_json.stdout) alice_stake = show_stake_json_output["stake_info"][keypair_alice.ss58_address][0] - assert Balance.from_tao(alice_stake["stake_value"]) > Balance.from_tao(90.0) + assert Balance.from_tao(alice_stake["stake_value"]) >= Balance.from_tao(87.0) # Execute remove_stake command and remove all alpha stakes from Alice remove_stake = exec_command_alice( @@ -451,42 +452,43 @@ def test_staking(local_chain, wallet_setup): remove_stake.stdout ) - add_stake_multiple = exec_command_alice( - command="stake", - sub_command="add", - extra_args=[ - "--netuids", - ",".join(str(x) for x in multiple_netuids), - "--wallet-path", - wallet_path_alice, - "--wallet-name", - wallet_alice.name, - "--hotkey", - wallet_alice.hotkey_str, - "--chain", - "ws://127.0.0.1:9945", - "--amount", - "100", - "--tolerance", - "0.1", - "--partial", - "--no-prompt", - "--era", - "144", - "--json-output", - ], - ) - add_stake_multiple_output = json.loads(add_stake_multiple.stdout) - for netuid_ in multiple_netuids: - - def line(key: str) -> Union[str, bool]: - return add_stake_multiple_output[key][str(netuid_)][ - wallet_alice.hotkey.ss58_address - ] - - assert line("staking_success") is True - assert line("error_messages") == "" - assert isinstance(line("extrinsic_ids"), str) + # TODO: Add back when nonce stuff is updated in mev shield + # add_stake_multiple = exec_command_alice( + # command="stake", + # sub_command="add", + # extra_args=[ + # "--netuids", + # ",".join(str(x) for x in multiple_netuids), + # "--wallet-path", + # wallet_path_alice, + # "--wallet-name", + # wallet_alice.name, + # "--hotkey", + # wallet_alice.hotkey_str, + # "--chain", + # "ws://127.0.0.1:9945", + # "--amount", + # "100", + # "--tolerance", + # "0.1", + # "--partial", + # "--no-prompt", + # "--era", + # "32", + # "--json-output", + # ], + # ) + # add_stake_multiple_output = json.loads(add_stake_multiple.stdout) + # for netuid_ in multiple_netuids: + + # def line(key: str) -> Union[str, bool]: + # return add_stake_multiple_output[key][str(netuid_)][ + # wallet_alice.hotkey.ss58_address + # ] + + # assert line("staking_success") is True + # assert line("error_messages") == "" + # assert isinstance(line("extrinsic_ids"), str) # Fetch the hyperparameters of the subnet hyperparams = exec_command_alice( diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index f3173b5a7..072df278c 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -1,12 +1,13 @@ import asyncio import json import re - +import pytest from bittensor_cli.src.bittensor.balances import Balance from .utils import set_storage_extrinsic +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_unstaking(local_chain, wallet_setup): """ Test various unstaking scenarios including partial unstake, unstake all alpha, and unstake all. From 5c03f28930dcc5fe21bc11f0006ca867cb61b242 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 13:53:31 -0800 Subject: [PATCH 124/179] update mev shield working --- bittensor_cli/src/bittensor/extrinsics/mev_shield.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 3ba60446c..54ab666c4 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -35,23 +35,21 @@ async def encrypt_call( ValueError: If MEV Shield NextKey is not available on chain. """ - next_key_result, genesis_hash, nonce = await asyncio.gather( + next_key_result, genesis_hash = await asyncio.gather( subtensor.get_mev_shield_next_key(), subtensor.substrate.get_block_hash(0), - subtensor.substrate.get_account_nonce(wallet.coldkey.ss58_address), ) if next_key_result is None: raise ValueError("MEV Shield NextKey not available on chain") - nonce = nonce + 1 # TODO: Update once chain is updated ml_kem_768_public_key = next_key_result - # Create payload_core: signer (32B) + nonce (u32 LE) + SCALE(call) + # Create payload_core: signer (32B) + next_key (32B) + SCALE(call) signer_bytes = encode_account_id(wallet.coldkey.ss58_address) - nonce_bytes = (nonce & 0xFFFFFFFF).to_bytes(4, byteorder="little") scale_call_bytes = bytes(call.data.data) + next_key = hashlib.blake2b(next_key_result, digest_size=32).digest() - payload_core = signer_bytes + nonce_bytes + scale_call_bytes + payload_core = signer_bytes + next_key + scale_call_bytes mev_shield_version = mlkem_kdf_id() genesis_hash_clean = ( From c669b1ebf7798ca091d9458d911e888b5a27c637 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 13:53:44 -0800 Subject: [PATCH 125/179] uncomment bulk staking test --- tests/e2e_tests/test_staking_sudo.py | 74 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 719063d3e..9ebe03688 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -24,6 +24,7 @@ * btcli sudo get """ + @pytest.mark.parametrize("local_chain", [False], indirect=True) def test_staking(local_chain, wallet_setup): """ @@ -452,43 +453,42 @@ def test_staking(local_chain, wallet_setup): remove_stake.stdout ) - # TODO: Add back when nonce stuff is updated in mev shield - # add_stake_multiple = exec_command_alice( - # command="stake", - # sub_command="add", - # extra_args=[ - # "--netuids", - # ",".join(str(x) for x in multiple_netuids), - # "--wallet-path", - # wallet_path_alice, - # "--wallet-name", - # wallet_alice.name, - # "--hotkey", - # wallet_alice.hotkey_str, - # "--chain", - # "ws://127.0.0.1:9945", - # "--amount", - # "100", - # "--tolerance", - # "0.1", - # "--partial", - # "--no-prompt", - # "--era", - # "32", - # "--json-output", - # ], - # ) - # add_stake_multiple_output = json.loads(add_stake_multiple.stdout) - # for netuid_ in multiple_netuids: - - # def line(key: str) -> Union[str, bool]: - # return add_stake_multiple_output[key][str(netuid_)][ - # wallet_alice.hotkey.ss58_address - # ] - - # assert line("staking_success") is True - # assert line("error_messages") == "" - # assert isinstance(line("extrinsic_ids"), str) + add_stake_multiple = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuids", + ",".join(str(x) for x in multiple_netuids), + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "100", + "--tolerance", + "0.1", + "--partial", + "--no-prompt", + "--era", + "32", + "--json-output", + ], + ) + add_stake_multiple_output = json.loads(add_stake_multiple.stdout) + for netuid_ in multiple_netuids: + + def line(key: str) -> Union[str, bool]: + return add_stake_multiple_output[key][str(netuid_)][ + wallet_alice.hotkey.ss58_address + ] + + assert line("staking_success") is True + assert line("error_messages") == "" + assert isinstance(line("extrinsic_ids"), str) # Fetch the hyperparameters of the subnet hyperparams = exec_command_alice( From 0ef2e767c839e0e5e193ddf01de30248fc9aac8f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 14:32:23 -0800 Subject: [PATCH 126/179] dummy commit --- 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 9ebe03688..4f5346207 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -423,7 +423,7 @@ def test_staking(local_chain, wallet_setup): alice_stake = show_stake_json_output["stake_info"][keypair_alice.ss58_address][0] assert Balance.from_tao(alice_stake["stake_value"]) >= Balance.from_tao(87.0) - # Execute remove_stake command and remove all alpha stakes from Alice + # Execute remove_stake command and remove all alpha stakes from Alice's wallet remove_stake = exec_command_alice( command="stake", sub_command="remove", From c84afe94f17ad0d33bcb044f09fc7da66f32d1f9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 15:24:49 -0800 Subject: [PATCH 127/179] bump asi dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 549a8f2bc..5fe945fa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ ] dependencies = [ "wheel", - "async-substrate-interface>=1.5.2", + "async-substrate-interface>=1.5.13", "aiohttp~=3.13", "backoff~=2.2.1", "bittensor-drand>=1.2.0", From 752051dbae38c7fbd25b39db7e129a76c88bc7b2 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 15:44:38 -0800 Subject: [PATCH 128/179] update --- bittensor_cli/src/bittensor/extrinsics/mev_shield.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 54ab666c4..ff99466af 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -183,14 +183,8 @@ async def _noop(_): extrinsic_idx=execute_revealed_index, ) - # TODO: Activate this when we update up-stream - # if not await receipt.is_success: - # error_msg = format_error_message(await receipt.error_message) - # return False, error_msg, None - - error = await check_mev_shield_error(receipt, subtensor, wrapper_id) - if error: - error_msg = format_error_message(error) + if not await receipt.is_success: + error_msg = format_error_message(await receipt.error_message) return False, error_msg, None return True, None, receipt From 47dcdaf28c9db10f48229ac97c2d2ceaaaa73018 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 16:27:18 -0800 Subject: [PATCH 129/179] update liquidity test --- tests/e2e_tests/test_liquidity.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 7a210f0a1..4e1232248 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -1,9 +1,8 @@ +import pytest import asyncio import json -import re import time -from bittensor_cli.src.bittensor.balances import Balance from .utils import turn_off_hyperparam_freeze_window """ @@ -15,7 +14,7 @@ * btcli liquidity remove """ - +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_liquidity(local_chain, wallet_setup): wallet_path_alice = "//Alice" netuid = 2 From 14504f76a99a6de684b84b51d439937d73b144c3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 16:29:10 -0800 Subject: [PATCH 130/179] ruff --- tests/e2e_tests/test_liquidity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 4e1232248..eb10a7138 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -14,6 +14,7 @@ * btcli liquidity remove """ + @pytest.mark.parametrize("local_chain", [False], indirect=True) def test_liquidity(local_chain, wallet_setup): wallet_path_alice = "//Alice" From aa2f2e4f3bdc4d81823dee546c890cf72d173086 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 16:40:43 -0800 Subject: [PATCH 131/179] wip tests --- tests/e2e_tests/test_liquidity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index eb10a7138..55e4f9531 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -92,6 +92,7 @@ def test_liquidity(local_chain, wallet_setup): assert result_output["success"] is False assert f"Subnet with netuid: {netuid} is not active" in result_output["err_msg"] assert result_output["positions"] == [] + time.sleep(20) # start emissions schedule start_subnet_emissions = exec_command_alice( From 34e21219cd9795dd4067e2e732387c417e27c7a0 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 18:08:09 -0800 Subject: [PATCH 132/179] add shield to subnet creation --- bittensor_cli/src/commands/subnets/subnets.py | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index c2346880b..2ffdfd810 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -10,13 +10,18 @@ from rich.table import Column, Table from rich import box -from bittensor_cli.src import COLOR_PALETTE, Constants +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.registration import ( register_extrinsic, burned_register_extrinsic, ) from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + encrypt_call, + extract_mev_shield_id, + wait_for_mev_execution, +) from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph from bittensor_cli.src.commands.wallets import set_id, get_id @@ -174,6 +179,7 @@ async def _find_event_attributes_in_extrinsic_receipt( call_function=call_function, call_params=call_params, ) + call = await encrypt_call(subtensor, wallet, call) extrinsic = await substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey ) @@ -194,17 +200,28 @@ async def _find_event_attributes_in_extrinsic_receipt( await asyncio.sleep(0.5) return False, None, None - # Successful registration, final check for membership - else: - attributes = await _find_event_attributes_in_extrinsic_receipt( - response, "NetworkAdded" - ) - await print_extrinsic_id(response) - ext_id = await response.get_extrinsic_identifier() - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + # Check for MEV shield execution + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash ) - return True, int(attributes[0]), ext_id + if not mev_success: + err_console.print( + f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" + ) + return False, None, None + + # Successful registration, final check for membership + attributes = await _find_event_attributes_in_extrinsic_receipt( + response, "NetworkAdded" + ) + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + ) + return True, int(attributes[0]), ext_id # commands From 9ad5f2eb68543ebc0a5356dba29e2dfd09a3c82d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 18:37:47 -0800 Subject: [PATCH 133/179] wip --- tests/e2e_tests/test_liquidity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 55e4f9531..84829d5a2 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -92,7 +92,7 @@ def test_liquidity(local_chain, wallet_setup): assert result_output["success"] is False assert f"Subnet with netuid: {netuid} is not active" in result_output["err_msg"] assert result_output["positions"] == [] - time.sleep(20) + time.sleep(40) # start emissions schedule start_subnet_emissions = exec_command_alice( From 4ed0bfe2933a1dbbb50afed88441066858f44ebc Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 18:40:00 -0800 Subject: [PATCH 134/179] update both tests --- tests/e2e_tests/test_hyperparams_setting.py | 3 ++- tests/e2e_tests/test_set_identity.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index c336f6615..c21008bb5 100644 --- a/tests/e2e_tests/test_hyperparams_setting.py +++ b/tests/e2e_tests/test_hyperparams_setting.py @@ -1,6 +1,6 @@ import asyncio import json - +import pytest from bittensor_cli.src import HYPERPARAMS, RootSudoOnly from .utils import turn_off_hyperparam_freeze_window @@ -13,6 +13,7 @@ """ +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_hyperparams_setting(local_chain, wallet_setup): netuid = 2 wallet_path_alice = "//Alice" diff --git a/tests/e2e_tests/test_set_identity.py b/tests/e2e_tests/test_set_identity.py index 9c008cdcd..4e5311af5 100644 --- a/tests/e2e_tests/test_set_identity.py +++ b/tests/e2e_tests/test_set_identity.py @@ -1,4 +1,5 @@ import json +import pytest from unittest.mock import MagicMock, AsyncMock, patch @@ -10,6 +11,7 @@ """ +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_set_id(local_chain, wallet_setup): """ Tests that the user is prompted to confirm that the incorrect text/html URL is From 7a96cd5a4fa1bf8bf8402c7a4248cbdf1158ec54 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 19:26:19 -0800 Subject: [PATCH 135/179] update tests + statuses --- bittensor_cli/src/commands/subnets/subnets.py | 5 +++-- tests/e2e_tests/test_liquidity.py | 2 +- tests/e2e_tests/test_set_identity.py | 2 ++ tests/e2e_tests/test_wallet_interactions.py | 3 +++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 2ffdfd810..398d9fdcf 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -171,7 +171,7 @@ async def _find_event_attributes_in_extrinsic_receipt( if not unlock_key(wallet).success: return False, None, None - with console.status(":satellite: Registering subnet...", spinner="earth"): + with console.status(":satellite: Registering subnet...", spinner="earth") as status: substrate = subtensor.substrate # create extrinsic call call = await substrate.compose_call( @@ -204,9 +204,10 @@ async def _find_event_attributes_in_extrinsic_receipt( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash + subtensor, mev_shield_id, response.block_hash, status=status ) if not mev_success: + status.stop() err_console.print( f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" ) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 84829d5a2..0f674ee01 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -30,7 +30,7 @@ def test_liquidity(local_chain, wallet_setup): print( "Skipping turning off hyperparams freeze window. This indicates the call does not exist on the chain you are testing." ) - time.sleep(10) + time.sleep(50) # Register a subnet with sudo as Alice result = exec_command_alice( diff --git a/tests/e2e_tests/test_set_identity.py b/tests/e2e_tests/test_set_identity.py index 4e5311af5..89d6b8531 100644 --- a/tests/e2e_tests/test_set_identity.py +++ b/tests/e2e_tests/test_set_identity.py @@ -1,3 +1,4 @@ +import time import json import pytest from unittest.mock import MagicMock, AsyncMock, patch @@ -25,6 +26,7 @@ def test_set_id(local_chain, wallet_setup): keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( wallet_path_alice ) + time.sleep(50) # Register a subnet with sudo as Alice result = exec_command_alice( command="subnets", diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 3b92c4965..e80a00e41 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -1,3 +1,4 @@ +import pytest from time import sleep from bittensor_cli.src.bittensor.balances import Balance @@ -24,6 +25,7 @@ """ +@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_wallet_overview_inspect(local_chain, wallet_setup): """ Test the overview and inspect commands of the wallet by interaction with subnets @@ -43,6 +45,7 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): # Create wallet for Alice keypair, wallet, wallet_path, exec_command = wallet_setup(wallet_path_name) + sleep(50) # Register a subnet with sudo as Alice result = exec_command( From 244da8f29cec386b553a01307ffc4d848130b19c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 1 Dec 2025 19:40:05 -0800 Subject: [PATCH 136/179] testing --- tests/e2e_tests/test_liquidity.py | 2 +- tests/e2e_tests/test_wallet_interactions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 0f674ee01..1a93e45ae 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -92,7 +92,7 @@ def test_liquidity(local_chain, wallet_setup): assert result_output["success"] is False assert f"Subnet with netuid: {netuid} is not active" in result_output["err_msg"] assert result_output["positions"] == [] - time.sleep(40) + time.sleep(60) # start emissions schedule start_subnet_emissions = exec_command_alice( diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index e80a00e41..eb9038218 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -45,7 +45,7 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): # Create wallet for Alice keypair, wallet, wallet_path, exec_command = wallet_setup(wallet_path_name) - sleep(50) + sleep(70) # Register a subnet with sudo as Alice result = exec_command( From 26473d23bbe5e6aa387fabf28f930a5f94ce7e32 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 2 Dec 2025 15:07:10 +0200 Subject: [PATCH 137/179] Update type annotation --- bittensor_cli/src/bittensor/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index d65974c11..1b2224700 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -828,7 +828,7 @@ class TableDefinition: @staticmethod @contextmanager - def get_db() -> Generator[tuple[sqlite3.Connection, sqlite3.Cursor], None]: + def get_db() -> Generator[tuple[sqlite3.Connection, sqlite3.Cursor], None, None]: """ Helper function to get a DB connection """ From 230516aceaf12c23ec3106626ec0595dd3c5baa1 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 2 Dec 2025 18:38:16 +0200 Subject: [PATCH 138/179] Add docstrings, update return type. --- bittensor_cli/src/commands/proxy.py | 69 +++++++++++++++-------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/bittensor_cli/src/commands/proxy.py b/bittensor_cli/src/commands/proxy.py index 9621b5d0e..580636881 100644 --- a/bittensor_cli/src/commands/proxy.py +++ b/bittensor_cli/src/commands/proxy.py @@ -66,6 +66,13 @@ async def submit_proxy( proxy: Optional[str] = None, announce_only: bool = False, ) -> None: + """ + Submits the prepared call to the chain + + Returns: + None, prints out the result according to `json_output` flag. + + """ success, msg, receipt = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -86,7 +93,7 @@ async def submit_proxy( } ) else: - console.print("Success!") # TODO add more shit here + console.print(":white_check_mark:[green]Success![/green]") else: if json_output: @@ -98,7 +105,7 @@ async def submit_proxy( } ) else: - err_console.print(f"Failure: {msg}") # TODO add more shit here + console.print(":white_check_mark:[green]Success![/green]") async def create_proxy( @@ -112,40 +119,21 @@ async def create_proxy( wait_for_finalization: bool, period: int, json_output: bool, -) -> tuple[bool, str, str, str]: +) -> None: """ - - Args: - subtensor: - wallet: - proxy_type: - delay: - idx: - prompt: - wait_for_inclusion: - wait_for_finalization: - period: - json_output: - - Returns: - tuple containing the following: - should_update: True if the address book should be updated, False otherwise - name: name of the new pure proxy for the address book - address: SS58 address of the new pure proxy - proxy_type: proxy type of the new pure proxy - + Executes the create pure proxy call on the chain """ if prompt: if not Confirm.ask( f"This will create a Pure Proxy of type {proxy_type.value}. Do you want to proceed?", ): - return False, "", "", "" + return None if delay > 0: if not Confirm.ask( f"By adding a non-zero delay ({delay}), all proxy calls must be announced " f"{delay} blocks before they will be able to be made. Continue?" ): - return False, "", "", "" + return None if not (ulw := unlock_key(wallet, print_out=not json_output)).success: if not json_output: err_console.print(ulw.message) @@ -157,7 +145,7 @@ async def create_proxy( "extrinsic_identifier": None, } ) - return False, "", "", "" + return None call = await subtensor.substrate.compose_call( call_module="Proxy", call_function="create_pure", @@ -220,7 +208,7 @@ async def create_proxy( f"Added to Proxy Address Book.\n" f"Show this information with [{COLORS.G.ARG}]btcli config proxies[/{COLORS.G.ARG}]" ) - return True, proxy_name, created_pure, created_proxy_type + return None if json_output: json_console.print_json( @@ -248,8 +236,8 @@ async def create_proxy( } ) else: - err_console.print(f"Failure: {msg}") # TODO add more shit here - return False, "", "", "" + err_console.print(f":cross_mark:[red]Failed to create pure proxy: {msg}") + return None async def remove_proxy( @@ -264,6 +252,9 @@ async def remove_proxy( period: int, json_output: bool, ) -> None: + """ + Executes the remove proxy call on the chain + """ if prompt: if not Confirm.ask( f"This will remove a proxy of type {proxy_type.value} for delegate {delegate}." @@ -314,6 +305,9 @@ async def add_proxy( period: int, json_output: bool, ): + """ + Executes the add proxy call on the chain + """ if prompt: if not Confirm.ask( f"This will add a proxy of type {proxy_type.value} for delegate {delegate}." @@ -325,7 +319,7 @@ async def add_proxy( f"By adding a non-zero delay ({delay}), all proxy calls must be announced " f"{delay} blocks before they will be able to be made. Continue?" ): - return False, "", "", "" + return None if not (ulw := unlock_key(wallet, print_out=not json_output)).success: if not json_output: err_console.print(ulw.message) @@ -392,7 +386,6 @@ async def add_proxy( conn, cursor, name=proxy_name, - # TODO verify this is correct (it's opposite of create pure) ss58_address=delegator, delay=delay, proxy_type=created_proxy_type.value, @@ -430,7 +423,7 @@ async def add_proxy( } ) else: - err_console.print(f"Failure: {msg}") # TODO add more shit here + err_console.print(f":cross_mark:[red]Failed to add proxy: {msg}") return None @@ -450,6 +443,9 @@ async def kill_proxy( period: int, json_output: bool, ) -> None: + """ + Executes the pure proxy kill call on the chain + """ if prompt: confirmation = Prompt.ask( f"This will kill a Pure Proxy account of type {proxy_type.value}.\n" @@ -510,8 +506,13 @@ async def execute_announced( wait_for_finalization: bool = False, json_output: bool = False, ) -> bool: - # TODO should this remove from the ProxyAnnouncements after successful completion, or should it mark it as completed - # in the DB? + """ + Executes the previously-announced call on the chain. + + Returns: + True if the submission was successful, False otherwise. + + """ if prompt and created_block is not None: current_block = await subtensor.substrate.get_block_number() if current_block - delay > created_block: From 121a48d354a27dcd5ea0c9b6e59402d27da3433e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 14:03:29 -0800 Subject: [PATCH 139/179] bump version to 9.16.0rc1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5fe945fa8..4cac3ea69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "bittensor-cli" -version = "9.15.3" +version = "9.16.0rc1" description = "Bittensor CLI" readme = "README.md" authors = [ From 82915d89560c0f2b432889717ef41d32628f8a33 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 15:44:50 -0800 Subject: [PATCH 140/179] add mev protection arg --- bittensor_cli/cli.py | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9ad6c89df..7206b612f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -346,6 +346,12 @@ def edit_help(cls, option_name: str, help_text: str): "--dashboard.path", help="Path to save the dashboard HTML file. For example: `~/.bittensor/dashboard`.", ) + mev_protection = typer.Option( + True, + "--mev-protection/--no-mev-protection", + show_default=False, + help="Enable or disable MEV protection [dim](default: enabled)[/dim].", + ) json_output = typer.Option( False, "--json-output", @@ -3972,6 +3978,7 @@ def stake_add( rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, + mev_protection: bool = Options.mev_protection, period: int = Options.period, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -4201,6 +4208,7 @@ def stake_add( f"rate_tolerance: {rate_tolerance}\n" f"allow_partial_stake: {allow_partial_stake}\n" f"period: {period}\n" + f"mev_protection: {mev_protection}\n" ) return self._run_command( add_stake.stake_add( @@ -4218,6 +4226,7 @@ def stake_add( allow_partial_stake, json_output, period, + mev_protection, ) ) @@ -4270,6 +4279,7 @@ def stake_remove( safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, period: int = Options.period, + mev_protection: bool = Options.mev_protection, prompt: bool = Options.prompt, interactive: bool = typer.Option( False, @@ -4478,7 +4488,8 @@ def stake_remove( f"all_hotkeys: {all_hotkeys}\n" f"include_hotkeys: {include_hotkeys}\n" f"exclude_hotkeys: {exclude_hotkeys}\n" - f"era: {period}" + f"era: {period}\n" + f"mev_protection: {mev_protection}" ) return self._run_command( remove_stake.unstake_all( @@ -4492,6 +4503,7 @@ def stake_remove( prompt=prompt, json_output=json_output, era=period, + mev_protection=mev_protection, ) ) elif ( @@ -4544,7 +4556,8 @@ def stake_remove( f"safe_staking: {safe_staking}\n" f"rate_tolerance: {rate_tolerance}\n" f"allow_partial_stake: {allow_partial_stake}\n" - f"era: {period}" + f"era: {period}\n" + f"mev_protection: {mev_protection}\n" ) return self._run_command( @@ -4564,6 +4577,7 @@ def stake_remove( allow_partial_stake=allow_partial_stake, json_output=json_output, era=period, + mev_protection=mev_protection, ) ) @@ -4609,6 +4623,7 @@ def stake_move( False, "--stake-all", "--all", help="Stake all", prompt=False ), period: int = Options.period, + mev_protection: bool = Options.mev_protection, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4747,6 +4762,7 @@ def stake_move( f"era: {period}\n" f"interactive_selection: {interactive_selection}\n" f"prompt: {prompt}\n" + f"mev_protection: {mev_protection}\n" ) result, ext_id = self._run_command( move_stake.move_stake( @@ -4761,6 +4777,7 @@ def stake_move( era=period, interactive_selection=interactive_selection, prompt=prompt, + mev_protection=mev_protection, ) ) if json_output: @@ -4801,6 +4818,7 @@ def stake_transfer( stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), + mev_protection: bool = Options.mev_protection, period: int = Options.period, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -4933,7 +4951,8 @@ def stake_transfer( f"dest_coldkey_ss58: {dest_ss58}\n" f"amount: {amount}\n" f"era: {period}\n" - f"stake_all: {stake_all}" + f"stake_all: {stake_all}\n" + f"mev_protection: {mev_protection}" ) result, ext_id = self._run_command( move_stake.transfer_stake( @@ -4948,6 +4967,7 @@ def stake_transfer( interactive_selection=interactive_selection, stake_all=stake_all, prompt=prompt, + mev_protection=mev_protection, ) ) if json_output: @@ -4992,6 +5012,7 @@ def stake_swap( prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, + mev_protection: bool = Options.mev_protection, quiet: bool = Options.quiet, verbose: bool = Options.verbose, json_output: bool = Options.json_output, @@ -5056,6 +5077,7 @@ def stake_swap( f"prompt: {prompt}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" + f"mev_protection: {mev_protection}\n" ) result, ext_id = self._run_command( move_stake.swap_stake( @@ -5070,6 +5092,7 @@ def stake_swap( prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, + mev_protection=mev_protection, ) ) if json_output: @@ -5085,6 +5108,7 @@ def stake_wizard( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, period: int = Options.period, + mev_protection: bool = Options.mev_protection, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5150,6 +5174,7 @@ def stake_wizard( era=period, interactive_selection=False, prompt=prompt, + mev_protection=mev_protection, ) ) elif operation == "transfer": @@ -5179,6 +5204,7 @@ def stake_wizard( era=period, interactive_selection=False, prompt=prompt, + mev_protection=mev_protection, ) ) elif operation == "swap": @@ -5194,6 +5220,7 @@ def stake_wizard( era=period, interactive_selection=False, prompt=prompt, + mev_protection=mev_protection, ) ) else: @@ -6427,6 +6454,7 @@ def subnets_create( additional_info: Optional[str] = typer.Option( None, "--additional-info", help="Additional information" ), + mev_protection: bool = Options.mev_protection, json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -6472,7 +6500,12 @@ def subnets_create( logger.debug(f"args:\nnetwork: {network}\nidentity: {identity}\n") self._run_command( subnets.create( - wallet, self.initialize_chain(network), identity, json_output, prompt + wallet, + self.initialize_chain(network), + identity, + json_output, + prompt, + mev_protection, ) ) From 79a7682bdf686853ebebf9eb53c6cbb19ff56d37 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 15:51:17 -0800 Subject: [PATCH 141/179] add optional arg to add & rem stake --- bittensor_cli/src/commands/stake/add.py | 49 +++++++------- bittensor_cli/src/commands/stake/remove.py | 74 +++++++++++++++------- 2 files changed, 77 insertions(+), 46 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index c97743d9b..d88692261 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -51,6 +51,7 @@ async def stake_add( allow_partial_stake: bool, json_output: bool, era: int, + mev_protection: bool, ): """ Args: @@ -139,7 +140,8 @@ async def safe_stake_extrinsic( }, ), ) - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, @@ -167,16 +169,17 @@ async def safe_stake_extrinsic( err_out("\n" + err_msg) return False, err_msg, None - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None if json_output: # the rest of this checking is not necessary if using json_output @@ -241,7 +244,8 @@ async def stake_extrinsic( failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} ) @@ -258,16 +262,17 @@ async def stake_extrinsic( err_out("\n" + err_msg) return False, err_msg, None - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None if json_output: # the rest of this is not necessary if using json_output diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 0531c97ee..afd9310da 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -53,6 +53,7 @@ async def unstake( allow_partial_stake: bool, json_output: bool, era: int, + mev_protection: bool, ): """Unstake from hotkey(s).""" @@ -331,6 +332,7 @@ async def unstake( "hotkey_ss58": op["hotkey_ss58"], "status": status, "era": era, + "mev_protection": mev_protection, } if safe_staking and op["netuid"] != 0: @@ -375,6 +377,7 @@ async def unstake_all( era: int = 3, prompt: bool = True, json_output: bool = False, + mev_protection: bool = True, ) -> None: """Unstakes all stakes from all hotkeys in all subnets.""" include_hotkeys = include_hotkeys or [] @@ -551,6 +554,7 @@ async def unstake_all( unstake_all_alpha=unstake_all_alpha, status=status, era=era, + mev_protection=mev_protection, ) ext_id = await ext_receipt.get_extrinsic_identifier() if success else None successes[hotkey_ss58] = { @@ -571,6 +575,7 @@ async def _unstake_extrinsic( hotkey_ss58: str, status=None, era: int = 3, + mev_protection: bool = True, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a standard unstake extrinsic. @@ -607,7 +612,8 @@ async def _unstake_extrinsic( ), ) - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -623,16 +629,17 @@ async def _unstake_extrinsic( ) return False, None - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None # Fetch latest balance and stake await print_extrinsic_id(response) @@ -672,6 +679,7 @@ async def _safe_unstake_extrinsic( allow_partial_stake: bool, status=None, era: int = 3, + mev_protection: bool = True, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a safe unstake extrinsic with price limit. @@ -719,7 +727,8 @@ async def _safe_unstake_extrinsic( ), ) - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -746,16 +755,17 @@ async def _safe_unstake_extrinsic( ) return False, None - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() @@ -798,6 +808,7 @@ async def _unstake_all_extrinsic( unstake_all_alpha: bool, status=None, era: int = 3, + mev_protection: bool = True, ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute an unstake all extrinsic. @@ -845,6 +856,9 @@ async def _unstake_all_extrinsic( call_params={"hotkey": hotkey_ss58}, ) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) + try: response = await subtensor.substrate.submit_extrinsic( extrinsic=await subtensor.substrate.create_signed_extrinsic( @@ -860,8 +874,20 @@ async def _unstake_all_extrinsic( f"{format_error_message(await response.error_message)}" ) return False, None - else: - await print_extrinsic_id(response) + + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None + + await print_extrinsic_id(response) # Fetch latest balance and stake block_hash = await subtensor.substrate.get_chain_head() From 341140a9488e2dc3547afc9d7e3fd9666efb359f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 15:54:08 -0800 Subject: [PATCH 142/179] update movement cmds --- bittensor_cli/src/commands/stake/move.py | 69 +++++++++++++----------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 95303873e..a7f3f0782 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -463,6 +463,7 @@ async def move_stake( era: int, interactive_selection: bool = False, prompt: bool = True, + mev_protection: bool = True, ) -> tuple[bool, str]: if interactive_selection: try: @@ -584,7 +585,8 @@ async def move_stake( f"[blue]{origin_netuid}[/blue] \nto " f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." ) as status: - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -592,15 +594,16 @@ async def move_stake( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" ext_id = await response.get_extrinsic_identifier() @@ -661,6 +664,7 @@ async def transfer_stake( interactive_selection: bool = False, stake_all: bool = False, prompt: bool = True, + mev_protection: bool = True, ) -> tuple[bool, str]: """Transfers stake from one network to another. @@ -783,7 +787,8 @@ async def transfer_stake( return False, "" with console.status("\n:satellite: Transferring stake ...") as status: - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -792,15 +797,16 @@ async def transfer_stake( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" ext_id = await response.get_extrinsic_identifier() @@ -852,6 +858,7 @@ async def swap_stake( prompt: bool = True, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + mev_protection: bool = True, ) -> tuple[bool, str]: """Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership. @@ -970,7 +977,8 @@ async def swap_stake( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " f"to netuid [blue]{destination_netuid}[/blue]..." ) as status: - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} ) @@ -981,15 +989,16 @@ async def swap_stake( wait_for_finalization=wait_for_finalization, ) - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" ext_id = await response.get_extrinsic_identifier() From 57035e90a9ef3165e495f2e8f6ced72f015d71d7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 15:54:55 -0800 Subject: [PATCH 143/179] update sn creation --- bittensor_cli/src/commands/subnets/subnets.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 398d9fdcf..e2cc7bbd7 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -61,6 +61,7 @@ async def register_subnetwork_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, + mev_protection: bool = True, ) -> tuple[bool, Optional[int], Optional[str]]: """Registers a new subnetwork. @@ -179,7 +180,8 @@ async def _find_event_attributes_in_extrinsic_receipt( call_function=call_function, call_params=call_params, ) - call = await encrypt_call(subtensor, wallet, call) + if mev_protection: + call = await encrypt_call(subtensor, wallet, call) extrinsic = await substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey ) @@ -201,17 +203,18 @@ async def _find_event_attributes_in_extrinsic_receipt( return False, None, None # Check for MEV shield execution - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print( - f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" + if mev_protection: + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status ) - return False, None, None + if not mev_success: + status.stop() + err_console.print( + f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" + ) + return False, None, None # Successful registration, final check for membership attributes = await _find_event_attributes_in_extrinsic_receipt( @@ -1640,12 +1643,13 @@ async def create( subnet_identity: dict, json_output: bool, prompt: bool, + mev_protection: bool = True, ): """Register a subnetwork""" # Call register command. success, netuid, ext_id = await register_subnetwork_extrinsic( - subtensor, wallet, subnet_identity, prompt=prompt + subtensor, wallet, subnet_identity, prompt=prompt, mev_protection=mev_protection ) if json_output: # technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present From 7f2bcae017f9f8692a17480d3d64a5f7e481dcac Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 2 Dec 2025 18:59:22 -0800 Subject: [PATCH 144/179] update tests to use fastblocks --- tests/e2e_tests/test_liquidity.py | 7 +++---- tests/e2e_tests/test_set_identity.py | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 1a93e45ae..e97e1b6b4 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -1,4 +1,3 @@ -import pytest import asyncio import json import time @@ -15,7 +14,6 @@ """ -@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_liquidity(local_chain, wallet_setup): wallet_path_alice = "//Alice" netuid = 2 @@ -30,7 +28,6 @@ def test_liquidity(local_chain, wallet_setup): print( "Skipping turning off hyperparams freeze window. This indicates the call does not exist on the chain you are testing." ) - time.sleep(50) # Register a subnet with sudo as Alice result = exec_command_alice( @@ -63,6 +60,7 @@ def test_liquidity(local_chain, wallet_setup): "https://testsubnet.com/logo.png", "--no-prompt", "--json-output", + "--no-mev-protection", ], ) result_output = json.loads(result.stdout) @@ -92,7 +90,7 @@ def test_liquidity(local_chain, wallet_setup): assert result_output["success"] is False assert f"Subnet with netuid: {netuid} is not active" in result_output["err_msg"] assert result_output["positions"] == [] - time.sleep(60) + time.sleep(40) # start emissions schedule start_subnet_emissions = exec_command_alice( @@ -138,6 +136,7 @@ def test_liquidity(local_chain, wallet_setup): "--no-prompt", "--era", "144", + "--no-mev-protection", ], ) assert "✅ Finalized" in stake_to_enable_v3.stdout, stake_to_enable_v3.stderr diff --git a/tests/e2e_tests/test_set_identity.py b/tests/e2e_tests/test_set_identity.py index 89d6b8531..345927785 100644 --- a/tests/e2e_tests/test_set_identity.py +++ b/tests/e2e_tests/test_set_identity.py @@ -1,6 +1,4 @@ -import time import json -import pytest from unittest.mock import MagicMock, AsyncMock, patch @@ -12,7 +10,6 @@ """ -@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_set_id(local_chain, wallet_setup): """ Tests that the user is prompted to confirm that the incorrect text/html URL is @@ -26,7 +23,7 @@ def test_set_id(local_chain, wallet_setup): keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( wallet_path_alice ) - time.sleep(50) + # Register a subnet with sudo as Alice result = exec_command_alice( command="subnets", @@ -58,6 +55,7 @@ def test_set_id(local_chain, wallet_setup): "https://testsubnet.com/logo.png", "--no-prompt", "--json-output", + "--no-mev-protection", ], ) result_output = json.loads(result.stdout) From d4196beb3d4a33b823e433257f3cb6a3cd8da586 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 3 Dec 2025 07:51:10 +0200 Subject: [PATCH 145/179] Bumped asi version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9eefb8d4d..b165f18c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "bittensor-cli" -version = "9.15.3" +version = "9.15.13" description = "Bittensor CLI" readme = "README.md" authors = [ From 6ac50bb97bb175a39f31a12ca8e69490353f51ac Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 3 Dec 2025 07:51:42 +0200 Subject: [PATCH 146/179] Bumped asi version --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b165f18c0..c1a55f34b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "bittensor-cli" -version = "9.15.13" +version = "9.15.3" description = "Bittensor CLI" readme = "README.md" authors = [ @@ -30,7 +30,7 @@ classifiers = [ ] dependencies = [ "wheel", - "async-substrate-interface>=1.5.2", + "async-substrate-interface>=1.5.13", "aiohttp~=3.13", "backoff~=2.2.1", "GitPython>=3.0.0", From 6076bdf7f8191574549fe5ba1dea887d46ceb18e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 15:05:01 -0800 Subject: [PATCH 147/179] init setup --- tests/e2e_tests/test_stake_movement.py | 168 +++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 tests/e2e_tests/test_stake_movement.py diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py new file mode 100644 index 000000000..e35a524f0 --- /dev/null +++ b/tests/e2e_tests/test_stake_movement.py @@ -0,0 +1,168 @@ +import asyncio +import json +import pytest + +from .utils import set_storage_extrinsic + + +@pytest.mark.parametrize("local_chain", [False], indirect=True) +def test_stake_movement(local_chain, wallet_setup): + """ + Exercise stake move, transfer, and swap flows across subnets using Alice and Bob. + + Steps: + 0. Initial setup: Make alice own SN 0, create SN2, SN3, SN4, start emissions on all subnets. + 1. Activation: Register Bob on subnets 2 and 3; add initial stake for V3 activation. + 2. Move: Move stake from Alice's hotkey on netuid 2 to Bob's hotkey on netuid 3. + 3. Transfer: Transfer all root (netuid 0) stake from Alice's coldkey to Bob's coldkey. + 4. Swap: Swap Alice's stake from netuid 4 to the root netuid. + + Note: + - All movement commands executed with mev shield + - Stake commands executed without shield to speed up tests + - Shield for stake commands is already covered in its own test + """ + print("Testing stake movement commands 🧪") + + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + + 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 + ) + + # Force Alice to own SN0 by setting storage + sn0_owner_storage_items = [ + ( + bytes.fromhex( + "658faa385070e074c85bf6b568cf055536e3e82152c8758267395fe524fbbd160000" + ), + bytes.fromhex( + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ), + ) + ] + asyncio.run( + set_storage_extrinsic( + local_chain, + wallet=wallet_alice, + items=sn0_owner_storage_items, + ) + ) + + # Create SN2, SN3, SN4 for move/transfer/swap checks + subnets_to_create = [2, 3, 4] + for netuid in subnets_to_create: + create_subnet_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", + ], + ) + create_subnet_payload = json.loads(create_subnet_result.stdout) + assert create_subnet_payload["success"] is True + assert create_subnet_payload["netuid"] == netuid + + # Start emission schedule for subnets (including root netuid 0) + for netuid in [0] + subnets_to_create: + start_emission_result = exec_command_alice( + command="subnets", + sub_command="start", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-name", + wallet_alice.name, + "--no-prompt", + "--chain", + "ws://127.0.0.1:9945", + "--wallet-path", + wallet_path_alice, + ], + ) + assert ( + f"Successfully started subnet {netuid}'s emission schedule." + in start_emission_result.stdout + ) + + # Alice is already registered - register Bob on the two non-root subnets + for netuid in [2, 3]: + register_bob_result = exec_command_bob( + command="subnets", + sub_command="register", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "✅ Registered" in register_bob_result.stdout, register_bob_result.stderr + assert "Your extrinsic has been included" in register_bob_result.stdout, ( + register_bob_result.stdout + ) + + # Add initial stake to enable V3 (1 TAO) on all created subnets + for netuid in [2, 3, 4]: + add_initial_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + str(netuid), + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "1", + "--unsafe", + "--no-prompt", + "--era", + "144", + "--no-mev-protection", + ], + ) + assert "✅ Finalized" in add_initial_stake_result.stdout, ( + add_initial_stake_result.stderr + ) From d642f414944c5d4f91b2ebe0f2c678ec42fc87a7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 15:11:46 -0800 Subject: [PATCH 148/179] move stake test --- tests/e2e_tests/test_stake_movement.py | 109 +++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py index e35a524f0..b27be4ee4 100644 --- a/tests/e2e_tests/test_stake_movement.py +++ b/tests/e2e_tests/test_stake_movement.py @@ -166,3 +166,112 @@ def test_stake_movement(local_chain, wallet_setup): assert "✅ Finalized" in add_initial_stake_result.stdout, ( add_initial_stake_result.stderr ) + + ############################ + # TEST 1: Move stake command + # Move stake between hotkeys while keeping the same coldkey + ############################ + + # Add 25 TAO stake for move test for Alice + add_move_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "2", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "25", + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "✅ Finalized" in add_move_stake_result.stdout, add_move_stake_result.stderr + + # List Alice's stakes prior to the move + alice_stake_before_move = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + + # Check Alice's stakes before move to ensure sufficient stake on netuid 2 + alice_stake_list_before_move = json.loads(alice_stake_before_move.stdout) + alice_stakes_before_move = alice_stake_list_before_move.get("stake_info", {}) + for hotkey_ss58, stakes in alice_stakes_before_move.items(): + if hotkey_ss58 == wallet_alice.hotkey.ss58_address: + for stake in stakes: + if stake["netuid"] == 2: + assert stake["stake_value"] >= int(20) + + # Move stake from Alice's hotkey on netuid 2 -> Bob's hotkey on netuid 3 + move_amount = 20 + move_result = exec_command_alice( + command="stake", + sub_command="move", + extra_args=[ + "--origin-netuid", + "2", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "3", + "--dest", + wallet_bob.hotkey.ss58_address, + "--amount", + move_amount, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "✅ Sent" in move_result.stdout + + # Check Alice's stakes after move + alice_stake_after_move = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + # Assert stake was moved from Alice's hotkey on netuid 2 -> Bob's hotkey on netuid 3 + alice_stake_list_after_move = json.loads(alice_stake_after_move.stdout) + alice_stakes_after_move = alice_stake_list_after_move.get("stake_info", {}) + for hotkey_ss58, stakes in alice_stakes_after_move.items(): + if hotkey_ss58 == wallet_bob.hotkey.ss58_address: + for stake in stakes: + if stake["netuid"] == 3: + assert stake["stake_value"] >= int(move_amount) From 8ff6266c6a3b081eeb372eea8787f55b5443b3d7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 15:12:06 -0800 Subject: [PATCH 149/179] test transfer stake --- tests/e2e_tests/test_stake_movement.py | 123 +++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py index b27be4ee4..684e664e2 100644 --- a/tests/e2e_tests/test_stake_movement.py +++ b/tests/e2e_tests/test_stake_movement.py @@ -275,3 +275,126 @@ def test_stake_movement(local_chain, wallet_setup): for stake in stakes: if stake["netuid"] == 3: assert stake["stake_value"] >= int(move_amount) + + ################################ + # TEST 2: Transfer stake command + # Transfer stake between coldkeys while keeping the same hotkey + ################################ + + transfer_amount = 20 + transfer_fund_root_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "0", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + transfer_amount, + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "✅ Finalized" in transfer_fund_root_result.stdout, ( + transfer_fund_root_result.stderr + ) + + # Ensure Bob doesn't have any stake in root netuid before transfer + bob_stake_list_before_transfer = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + assert bob_stake_list_before_transfer.stdout == "" + + # Transfer stake from Alice's coldkey on netuid 0 -> Bob's coldkey on netuid 0 + transfer_result = exec_command_alice( + command="stake", + sub_command="transfer", + extra_args=[ + "--origin-netuid", + "0", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "0", + "--dest", + wallet_bob.coldkeypub.ss58_address, + "--all", + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "✅ Sent" in transfer_result.stdout + + # Check Bob's stakes after transfer + bob_stake_list_after_transfer = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + bob_stake_list_after_transfer = json.loads(bob_stake_list_after_transfer.stdout) + bob_stakes_after_transfer = bob_stake_list_after_transfer.get("stake_info", {}) + for hotkey_ss58, stakes in bob_stakes_after_transfer.items(): + for stake in stakes: + if stake["netuid"] == 0: + assert stake["stake_value"] >= int(transfer_amount) + + # Check Alice's stakes after transfer + alice_stake_list_after_transfer = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + + alice_stake_list_after_transfer = json.loads(alice_stake_list_after_transfer.stdout) + alice_stakes_after_transfer = alice_stake_list_after_transfer.get("stake_info", {}) + for hotkey_ss58, stakes in alice_stakes_after_transfer.items(): + for stake in stakes: + if stake["netuid"] == 0: + pytest.fail("Stake found in root netuid after transfer") From 34c384f4704f465bf26bdd89d3c79f41ac3b89b5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 15:12:15 -0800 Subject: [PATCH 150/179] test swap stake --- tests/e2e_tests/test_stake_movement.py | 109 +++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py index 684e664e2..b2660fadb 100644 --- a/tests/e2e_tests/test_stake_movement.py +++ b/tests/e2e_tests/test_stake_movement.py @@ -398,3 +398,112 @@ def test_stake_movement(local_chain, wallet_setup): for stake in stakes: if stake["netuid"] == 0: pytest.fail("Stake found in root netuid after transfer") + + ################################ + # TEST 3: Swap stake command + # Swap stake between subnets while keeping the same coldkey-hotkey pair + ################################ + + swap_seed_stake_result = exec_command_alice( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + "4", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--amount", + "25", + "--no-prompt", + "--era", + "144", + "--unsafe", + "--no-mev-protection", + ], + ) + assert "✅ Finalized" in swap_seed_stake_result.stdout, ( + swap_seed_stake_result.stderr + ) + + # Ensure stake was added to Alice's hotkey on netuid 4 + alice_stake_list_before_swap_cmd = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + + alice_stake_list_before_swap = json.loads(alice_stake_list_before_swap_cmd.stdout) + alice_stakes_before_swap = alice_stake_list_before_swap.get("stake_info", {}) + found_stake_in_netuid_4 = False + for hotkey_ss58, stakes in alice_stakes_before_swap.items(): + for stake in stakes: + if stake["netuid"] == 4: + found_stake_in_netuid_4 = True + break + if not found_stake_in_netuid_4: + pytest.fail("Stake not found in netuid 4 before swap") + + # Swap stake from Alice's hotkey on netuid 4 -> Bob's hotkey on netuid 0 + swap_result = exec_command_alice( + command="stake", + sub_command="swap", + extra_args=[ + "--origin-netuid", + "4", + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--dest-netuid", + "0", + "--all", + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "✅ Sent" in swap_result.stdout, swap_result.stderr + + # Check Alice's stakes after swap + alice_stake_list_after_swap_cmd = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + "--json-output", + ], + ) + + alice_stake_list_after_swap = json.loads(alice_stake_list_after_swap_cmd.stdout) + alice_stakes_after_swap = alice_stake_list_after_swap.get("stake_info", {}) + for hotkey_ss58, stakes in alice_stakes_after_swap.items(): + for stake in stakes: + if stake["netuid"] == 4: + pytest.fail("Stake found in netuid 4 after swap") + + print("Passed stake movement commands") From 31ffd5eb8e8f3b9b82058b7b7d225a82237ea6a6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 16:19:22 -0800 Subject: [PATCH 151/179] improvement --- tests/e2e_tests/test_stake_movement.py | 74 +++++++++++++------------- tests/e2e_tests/utils.py | 28 ++++++++++ 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/tests/e2e_tests/test_stake_movement.py b/tests/e2e_tests/test_stake_movement.py index b2660fadb..eb9a22b0e 100644 --- a/tests/e2e_tests/test_stake_movement.py +++ b/tests/e2e_tests/test_stake_movement.py @@ -2,7 +2,7 @@ import json import pytest -from .utils import set_storage_extrinsic +from .utils import find_stake_entries, set_storage_extrinsic @pytest.mark.parametrize("local_chain", [False], indirect=True) @@ -217,12 +217,13 @@ def test_stake_movement(local_chain, wallet_setup): # Check Alice's stakes before move to ensure sufficient stake on netuid 2 alice_stake_list_before_move = json.loads(alice_stake_before_move.stdout) - alice_stakes_before_move = alice_stake_list_before_move.get("stake_info", {}) - for hotkey_ss58, stakes in alice_stakes_before_move.items(): - if hotkey_ss58 == wallet_alice.hotkey.ss58_address: - for stake in stakes: - if stake["netuid"] == 2: - assert stake["stake_value"] >= int(20) + alice_stakes_before_move = find_stake_entries( + alice_stake_list_before_move, + netuid=2, + hotkey_ss58=wallet_alice.hotkey.ss58_address, + ) + for stake in alice_stakes_before_move: + assert stake["stake_value"] >= int(20) # Move stake from Alice's hotkey on netuid 2 -> Bob's hotkey on netuid 3 move_amount = 20 @@ -269,12 +270,13 @@ def test_stake_movement(local_chain, wallet_setup): ) # Assert stake was moved from Alice's hotkey on netuid 2 -> Bob's hotkey on netuid 3 alice_stake_list_after_move = json.loads(alice_stake_after_move.stdout) - alice_stakes_after_move = alice_stake_list_after_move.get("stake_info", {}) - for hotkey_ss58, stakes in alice_stakes_after_move.items(): - if hotkey_ss58 == wallet_bob.hotkey.ss58_address: - for stake in stakes: - if stake["netuid"] == 3: - assert stake["stake_value"] >= int(move_amount) + bob_stakes_after_move = find_stake_entries( + alice_stake_list_after_move, + netuid=3, + hotkey_ss58=wallet_bob.hotkey.ss58_address, + ) + for stake in bob_stakes_after_move: + assert stake["stake_value"] >= move_amount ################################ # TEST 2: Transfer stake command @@ -369,11 +371,12 @@ def test_stake_movement(local_chain, wallet_setup): ], ) bob_stake_list_after_transfer = json.loads(bob_stake_list_after_transfer.stdout) - bob_stakes_after_transfer = bob_stake_list_after_transfer.get("stake_info", {}) - for hotkey_ss58, stakes in bob_stakes_after_transfer.items(): - for stake in stakes: - if stake["netuid"] == 0: - assert stake["stake_value"] >= int(transfer_amount) + bob_stakes_after_transfer = find_stake_entries( + bob_stake_list_after_transfer, + netuid=0, + ) + for stake in bob_stakes_after_transfer: + assert stake["stake_value"] >= transfer_amount # Check Alice's stakes after transfer alice_stake_list_after_transfer = exec_command_alice( @@ -393,11 +396,12 @@ def test_stake_movement(local_chain, wallet_setup): ) alice_stake_list_after_transfer = json.loads(alice_stake_list_after_transfer.stdout) - alice_stakes_after_transfer = alice_stake_list_after_transfer.get("stake_info", {}) - for hotkey_ss58, stakes in alice_stakes_after_transfer.items(): - for stake in stakes: - if stake["netuid"] == 0: - pytest.fail("Stake found in root netuid after transfer") + alice_stakes_after_transfer = find_stake_entries( + alice_stake_list_after_transfer, + netuid=0, + ) + if alice_stakes_after_transfer: + pytest.fail("Stake found in root netuid after transfer") ################################ # TEST 3: Swap stake command @@ -449,14 +453,11 @@ def test_stake_movement(local_chain, wallet_setup): ) alice_stake_list_before_swap = json.loads(alice_stake_list_before_swap_cmd.stdout) - alice_stakes_before_swap = alice_stake_list_before_swap.get("stake_info", {}) - found_stake_in_netuid_4 = False - for hotkey_ss58, stakes in alice_stakes_before_swap.items(): - for stake in stakes: - if stake["netuid"] == 4: - found_stake_in_netuid_4 = True - break - if not found_stake_in_netuid_4: + alice_stakes_before_swap = find_stake_entries( + alice_stake_list_before_swap, + netuid=4, + ) + if not alice_stakes_before_swap: pytest.fail("Stake not found in netuid 4 before swap") # Swap stake from Alice's hotkey on netuid 4 -> Bob's hotkey on netuid 0 @@ -500,10 +501,11 @@ def test_stake_movement(local_chain, wallet_setup): ) alice_stake_list_after_swap = json.loads(alice_stake_list_after_swap_cmd.stdout) - alice_stakes_after_swap = alice_stake_list_after_swap.get("stake_info", {}) - for hotkey_ss58, stakes in alice_stakes_after_swap.items(): - for stake in stakes: - if stake["netuid"] == 4: - pytest.fail("Stake found in netuid 4 after swap") + alice_stakes_after_swap = find_stake_entries( + alice_stake_list_after_swap, + netuid=4, + ) + if alice_stakes_after_swap: + pytest.fail("Stake found in netuid 4 after swap") print("Passed stake movement commands") diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 323797356..effd4cef9 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -132,6 +132,34 @@ def extract_coldkey_balance( } +def find_stake_entries( + stake_payload: dict, netuid: int, hotkey_ss58: str | None = None +) -> list[dict]: + """ + Return stake entries matching a given netuid, optionally scoped to a specific hotkey. + Requires json payload using `--json-output` flag. + + Args: + stake_payload: Parsed JSON payload containing `stake_info`. + netuid: The subnet identifier to filter on. + hotkey_ss58: Optional hotkey address to further narrow results. + + Returns: + A list of stake dicts matching the criteria (may be empty). + """ + stake_info = stake_payload.get("stake_info", {}) or {} + matching_stakes: list[dict] = [] + + for stake_hotkey, stakes in stake_info.items(): + if hotkey_ss58 and stake_hotkey != hotkey_ss58: + continue + for stake in stakes or []: + if stake.get("netuid") == netuid: + matching_stakes.append(stake) + + return matching_stakes + + def verify_subnet_entry(output_text: str, netuid: str, ss58_address: str) -> bool: """ Verifies the presence of a specific subnet entry subnets list output. From 6799a229e996323b99753ef3ee44bd9ceedab07c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 3 Dec 2025 16:19:40 -0800 Subject: [PATCH 152/179] test_wallet_overview_inspect non fast-blocks --- tests/e2e_tests/test_wallet_interactions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index eb9038218..ae23c5b0e 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -1,4 +1,3 @@ -import pytest from time import sleep from bittensor_cli.src.bittensor.balances import Balance @@ -25,7 +24,6 @@ """ -@pytest.mark.parametrize("local_chain", [False], indirect=True) def test_wallet_overview_inspect(local_chain, wallet_setup): """ Test the overview and inspect commands of the wallet by interaction with subnets @@ -45,7 +43,6 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): # Create wallet for Alice keypair, wallet, wallet_path, exec_command = wallet_setup(wallet_path_name) - sleep(70) # Register a subnet with sudo as Alice result = exec_command( @@ -77,6 +74,7 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): "--additional-info", "Test subnet", "--no-prompt", + "--no-mev-protection", ], ) assert f"✅ Registered subnetwork with netuid: {netuid}" in result.stdout From 6a40b71df36b20a20c7bdf0706958907f966c2af Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 4 Dec 2025 16:06:39 -0800 Subject: [PATCH 153/179] add get_mev_shield_current_key --- .../src/bittensor/subtensor_interface.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a733ed63e..0566f14e8 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2327,6 +2327,34 @@ async def get_mev_shield_next_key( return public_key_bytes + async def get_mev_shield_current_key( + self, + block_hash: Optional[str] = None, + ) -> Optional[tuple[bytes, int]]: + """ + Get the current MEV Shield public key and epoch from chain storage. + + Args: + block_hash: Optional block hash to query at. + + Returns: + Tuple of (public_key_bytes, epoch) or None if not available. + """ + result = await self.query( + module="MevShield", + storage_function="CurrentKey", + block_hash=block_hash, + ) + public_key_bytes = bytes(next(iter(result))) + + if len(public_key_bytes) != MEV_SHIELD_PUBLIC_KEY_SIZE: + raise ValueError( + f"Invalid ML-KEM-768 public key size: {len(public_key_bytes)} bytes. " + f"Expected exactly {MEV_SHIELD_PUBLIC_KEY_SIZE} bytes." + ) + + return public_key_bytes + async def best_connection(networks: list[str]): """ From e7b181234b19dd0613ffb267da4e8b3a13249c18 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 4 Dec 2025 16:10:14 -0800 Subject: [PATCH 154/179] add supp for mark_decryption_failed --- .../src/bittensor/extrinsics/mev_shield.py | 70 ++----------------- 1 file changed, 4 insertions(+), 66 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index ff99466af..eadce8863 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -161,7 +161,10 @@ async def _noop(_): call_module = call.get("call_module") call_function = call.get("call_function") - if call_module == "MevShield" and call_function == "execute_revealed": + if call_module == "MevShield" and call_function in ( + "execute_revealed", + "mark_decryption_failed", + ): call_args = call.get("call_args", []) for arg in call_args: if arg.get("name") == "id": @@ -190,68 +193,3 @@ async def _noop(_): return True, None, receipt return False, "Timeout waiting for MEV Shield execution", None - - -async def check_mev_shield_error( - receipt: AsyncExtrinsicReceipt, - subtensor: "SubtensorInterface", - wrapper_id: str, -) -> Optional[dict]: - """ - Handles & extracts error messages in the MEV Shield extrinsics. - This is a temporary implementation until we update up-stream code. - - Args: - receipt: AsyncExtrinsicReceipt for the execute_revealed extrinsic. - subtensor: SubtensorInterface instance. - wrapper_id: The wrapper ID to verify we're checking the correct event. - - Returns: - Error dict to be used with format_error_message(), or None if no error. - """ - if not await receipt.is_success: - return await receipt.error_message - - for event in await receipt.triggered_events: - event_details = event.get("event", {}) - - if ( - event_details.get("module_id") == "MevShield" - and event_details.get("event_id") == "DecryptedRejected" - ): - attributes = event_details.get("attributes", {}) - event_wrapper_id = attributes.get("id") - - if event_wrapper_id != wrapper_id: - continue - - reason = attributes.get("reason", {}) - dispatch_error = reason.get("error", {}) - - try: - if "Module" in dispatch_error: - module_index = dispatch_error["Module"]["index"] - error_index = dispatch_error["Module"]["error"] - - if isinstance(error_index, str) and error_index.startswith("0x"): - error_index = int(error_index[2:4], 16) - - runtime = await subtensor.substrate.init_runtime( - block_hash=receipt.block_hash - ) - module_error = runtime.metadata.get_module_error( - module_index=module_index, - error_index=error_index, - ) - - return { - "type": "Module", - "name": module_error.name, - "docs": module_error.docs, - } - except Exception: - return dispatch_error - - return dispatch_error - - return None From 3d40ede4f7d9954415ed537b75e3d98bc087caab Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 4 Dec 2025 16:49:39 -0800 Subject: [PATCH 155/179] no mev prot --- tests/e2e_tests/test_wallet_interactions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index ae23c5b0e..7ed705b65 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -394,6 +394,7 @@ def test_wallet_identities(local_chain, wallet_setup): "--logo-url", "https://testsubnet.com/logo.png", "--no-prompt", + "--no-mev-protection", ], ) assert f"✅ Registered subnetwork with netuid: {netuid}" in result.stdout From bd621e9e7f075027fe8cfa4a72f996c687a162d7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 4 Dec 2025 18:05:31 -0800 Subject: [PATCH 156/179] bump ASI --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4cac3ea69..fbe506980 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ ] dependencies = [ "wheel", - "async-substrate-interface>=1.5.13", + "async-substrate-interface>=1.5.14", "aiohttp~=3.13", "backoff~=2.2.1", "bittensor-drand>=1.2.0", From 42055a33d936b3751998c956a4382f288fca4164 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 4 Dec 2025 18:12:52 -0800 Subject: [PATCH 157/179] improve example cmds --- bittensor_cli/cli.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7206b612f..8d911ffa6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4013,6 +4013,9 @@ def stake_add( 7. Stake the same amount to multiple subnets: [green]$[/green] btcli stake add --amount 100 --netuids 4,5,6 + 8. Stake without MEV protection: + [green]$[/green] btcli stake add --amount 100 --netuid 1 --no-mev-protection + [bold]Safe Staking Parameters:[/bold] • [blue]--safe[/blue]: Enables rate tolerance checks • [blue]--tolerance[/blue]: Maximum % rate change allowed (0.05 = 5%) @@ -4316,6 +4319,9 @@ def stake_remove( 6. Unstake all Alpha from a hotkey and stake to Root: [green]$[/green] btcli stake remove --all-alpha + 7. Unstake without MEV protection: + [green]$[/green] btcli stake remove --amount 100 --netuid 1 --no-mev-protection + [bold]Safe Staking Parameters:[/bold] • [blue]--safe[/blue]: Enables rate tolerance checks during unstaking • [blue]--tolerance[/blue]: Max allowed rate change (0.05 = 5%) @@ -4645,9 +4651,13 @@ def stake_move( If no arguments are provided, an interactive selection menu will be shown. - EXAMPLE + EXAMPLES + + 1. Interactive move (guided prompts): + [green]$[/green] btcli stake move - [green]$[/green] btcli stake move + 2. Move stake without MEV protection: + [green]$[/green] btcli stake move --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output) if prompt: @@ -4856,6 +4866,9 @@ def stake_transfer( Transfer all available stake from origin hotkey: [green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2 + + Transfer stake without MEV protection: + [green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --amount 100 --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output) if prompt: @@ -5032,10 +5045,13 @@ def stake_swap( If no arguments are provided, an interactive selection menu will be shown. - EXAMPLE + EXAMPLES - Swap 100 TAO from subnet 1 to subnet 2: - [green]$[/green] btcli stake swap --wallet-name default --wallet-hotkey default --origin-netuid 1 --dest-netuid 2 --amount 100 + 1. Swap 100 TAO from subnet 1 to subnet 2: + [green]$[/green] btcli stake swap --wallet-name default --wallet-hotkey default --origin-netuid 1 --dest-netuid 2 --amount 100 + + 2. Swap stake without MEV protection: + [green]$[/green] btcli stake swap --origin-netuid 1 --dest-netuid 2 --amount 100 --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output) console.print( @@ -6473,6 +6489,9 @@ def subnets_create( 2. Create with GitHub repo and contact email: [green]$[/green] btcli subnets create --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net + + 3. Create subnet without MEV protection: + [green]$[/green] btcli subnets create --no-mev-protection """ self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( From 3e8c1eb6de174acf3abc8d099d925a4e1706a372 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 10:21:57 +0200 Subject: [PATCH 158/179] Merge --- bittensor_cli/cli.py | 2 +- bittensor_cli/src/commands/stake/add.py | 27 +++++++++---------- bittensor_cli/src/commands/stake/claim.py | 8 +----- bittensor_cli/src/commands/stake/move.py | 21 ++++++++------- bittensor_cli/src/commands/stake/remove.py | 1 - bittensor_cli/src/commands/subnets/subnets.py | 4 +-- 6 files changed, 28 insertions(+), 35 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a58e41d63..f2ffcfbb2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7093,7 +7093,7 @@ def subnets_create( proxy=proxy, json_output=json_output, prompt=prompt, - mev_protection=mev_protection + mev_protection=mev_protection, ) ) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index ab7229708..36e7ad7f3 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -8,7 +8,6 @@ from rich.table import Table from rich.prompt import Confirm, Prompt -from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( @@ -19,7 +18,6 @@ from bittensor_cli.src.bittensor.utils import ( console, err_console, - format_error_message, get_hotkey_wallets_for_wallet, is_valid_ss58_address, print_error, @@ -71,6 +69,7 @@ async def stake_add( json_output: whether to output stake info in JSON format era: Blocks for which the transaction should be valid. proxy: Optional proxy to use for staking. + mev_protection: If true, will encrypt the extrinsic behind the mev protection shield. Returns: bool: True if stake operation is successful, False otherwise @@ -121,9 +120,9 @@ async def safe_stake_extrinsic( current_stake: Balance, hotkey_ss58_: str, price_limit: Balance, - status=None, + status_=None, ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - err_out = partial(print_error, status=status) + err_out = partial(print_error, status=status_) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount_} on Netuid {netuid_}" ) @@ -158,7 +157,7 @@ async def safe_stake_extrinsic( f"Transaction rejected because partial staking is disabled. " f"Either increase price tolerance or enable partial staking." ) - print_error("\n" + err_msg, status=status) + print_error("\n" + err_msg, status=status_) else: err_msg = f"{failure_prelude} with error: {err_msg}" err_out("\n" + err_msg) @@ -168,10 +167,10 @@ async def safe_stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status + subtensor, mev_shield_id, response.block_hash, status=status_ ) if not mev_success: - status.stop() + status_.stop() err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, err_msg, None @@ -219,9 +218,9 @@ async def safe_stake_extrinsic( return True, "", response async def stake_extrinsic( - netuid_i, amount_, current, staking_address_ss58, status=None + netuid_i, amount_, current, staking_address_ss58, status_=None ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - err_out = partial(print_error, status=status) + err_out = partial(print_error, status=status_) block_hash = await subtensor.substrate.get_chain_head() current_balance, next_nonce, call = await asyncio.gather( subtensor.get_balance(coldkey_ss58, block_hash=block_hash), @@ -258,10 +257,10 @@ async def stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) if mev_shield_id: mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status + subtensor, mev_shield_id, response.block_hash, status=status_ ) if not mev_success: - status.stop() + status_.stop() err_msg = f"{failure_prelude}: {mev_error}" err_out("\n" + err_msg) return False, err_msg, None @@ -485,7 +484,7 @@ async def stake_extrinsic( amount_=am, current=curr, staking_address_ss58=staking_address, - status=status, + status_=status, ) else: stake_coroutines[(ni, staking_address)] = safe_stake_extrinsic( @@ -494,7 +493,7 @@ async def stake_extrinsic( current_stake=curr, hotkey_ss58_=staking_address, price_limit=price_with_tolerance, - status=status, + status_=status, ) else: stake_coroutines = { @@ -503,7 +502,7 @@ async def stake_extrinsic( amount_=am, current=curr, staking_address_ss58=staking_address, - status=status, + status_=status, ) for i, (ni, am, curr) in enumerate( zip(netuids, amounts_to_stake, current_stake_balances) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index c5f9c9b7f..2648ad926 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -65,13 +65,7 @@ async def set_claim_type( """ if claim_type is not None: - claim_type = claim_type.capitalize() - if claim_type not in ["Keep", "Swap"]: - msg = f"Invalid claim type: {claim_type}. Use 'Keep' or 'Swap', or omit for interactive mode." - err_console.print(f"[red]{msg}[/red]") - if json_output: - json_console.print(json.dumps({"success": False, "message": msg})) - return False, msg, None + claim_type = claim_type.value current_claim_info, all_netuids = await asyncio.gather( subtensor.get_coldkey_claim_type(coldkey_ss58=wallet.coldkeypub.ss58_address), diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 92443003c..c38fc0893 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -17,7 +17,6 @@ console, err_console, print_error, - format_error_message, group_subnets, get_subnet_name, unlock_key, @@ -680,6 +679,7 @@ async def transfer_stake( era: number of blocks for which the extrinsic should be valid stake_all: If true, transfer all stakes. proxy: Optional proxy to use for this extrinsic + mev_protection: If true, will encrypt the extrinsic behind the mev protection shield. Returns: tuple: @@ -804,15 +804,15 @@ async def transfer_stake( if success_: if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" ext_id = await response.get_extrinsic_identifier() + mev_shield_id = await extract_mev_shield_id(response) + if mev_shield_id: + mev_success, mev_error, response = await wait_for_mev_execution( + subtensor, mev_shield_id, response.block_hash, status=status + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" await print_extrinsic_id(response) ext_id = await response.get_extrinsic_identifier() if not prompt: @@ -878,6 +878,7 @@ async def swap_stake( prompt: If true, prompts for confirmation before executing swap. wait_for_inclusion: If true, waits for the transaction to be included in a block. wait_for_finalization: If true, waits for the transaction to be finalized. + mev_protection: If true, will encrypt the extrinsic behind the mev protection shield. Returns: (success, extrinsic_identifier): diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 85efedd37..15cf134a3 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -9,7 +9,6 @@ from rich.prompt import Confirm, Prompt from rich.table import Table -from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( encrypt_call, diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 365fa96c9..9ed457c3d 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -273,7 +273,7 @@ async def _find_event_attributes_in_extrinsic_receipt( ) return False, None, None - # Successful registration, final check for membership + # Successful registration, final check for membership attributes = await _find_event_attributes_in_extrinsic_receipt( response, "NetworkAdded" @@ -1730,7 +1730,7 @@ async def create( subnet_identity=subnet_identity, prompt=prompt, proxy=proxy, - mev_protection=mev_protection + mev_protection=mev_protection, ) if json_output: # technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present From 0a6968d948effeda2256dd9d2964bb415a32152a Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 11:55:29 +0200 Subject: [PATCH 159/179] Check for success before getting extrinsic identifier --- .../src/commands/liquidity/liquidity.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index 9059ae2e4..48383e605 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -285,13 +285,18 @@ async def add_liquidity( price_low=price_low, price_high=price_high, ) - await print_extrinsic_id(ext_receipt) - ext_id = await ext_receipt.get_extrinsic_identifier() + if success: + await print_extrinsic_id(ext_receipt) + ext_id = await ext_receipt.get_extrinsic_identifier() + else: + ext_id = None if json_output: - json_console.print( - json.dumps( - {"success": success, "message": message, "extrinsic_identifier": ext_id} - ) + json_console.print_json( + data={ + "success": success, + "message": message, + "extrinsic_identifier": ext_id, + } ) else: if success: From 4b3246513e2e70c0af67143e5eb7c56f3ef7a0f9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 01:58:44 -0800 Subject: [PATCH 160/179] update encrypt_extrinsic --- .../src/bittensor/extrinsics/mev_shield.py | 166 ++---------------- 1 file changed, 15 insertions(+), 151 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index eadce8863..5e0a81f99 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -1,76 +1,48 @@ -import asyncio import hashlib from typing import TYPE_CHECKING, Optional from async_substrate_interface import AsyncExtrinsicReceipt -from bittensor_drand import encrypt_mlkem768, mlkem_kdf_id -from bittensor_cli.src.bittensor.utils import encode_account_id, format_error_message +from bittensor_drand import encrypt_mlkem768 +from bittensor_cli.src.bittensor.utils import format_error_message if TYPE_CHECKING: - from bittensor_wallet import Wallet - from scalecodec import GenericCall + from scalecodec import GenericCall, GenericExtrinsic from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -async def encrypt_call( +async def encrypt_extrinsic( subtensor: "SubtensorInterface", - wallet: "Wallet", - call: "GenericCall", + signed_extrinsic: "GenericExtrinsic", ) -> "GenericCall": """ - Encrypt a call using MEV Shield. + Encrypt a signed extrinsic using MEV Shield. - Takes any call and returns a MevShield.submit_encrypted call - that can be submitted like any regular extrinsic. + Takes a pre-signed extrinsic and returns a MevShield.submit_encrypted call. + The signed extrinsic should be created with nonce = current_nonce + 1, + as it will execute after the shield wrapper extrinsic. Args: subtensor: The SubtensorInterface instance for chain queries. - wallet: The wallet whose coldkey will sign the inner payload. - call: The call to encrypt. + signed_extrinsic: The signed extrinsic to encrypt. Returns: - A MevShield.submit_encrypted call. + A MevShield.submit_encrypted call to be signed with the current nonce. Raises: ValueError: If MEV Shield NextKey is not available on chain. """ - next_key_result, genesis_hash = await asyncio.gather( - subtensor.get_mev_shield_next_key(), - subtensor.substrate.get_block_hash(0), - ) - if next_key_result is None: + ml_kem_768_public_key = await subtensor.get_mev_shield_next_key() + if ml_kem_768_public_key is None: raise ValueError("MEV Shield NextKey not available on chain") - ml_kem_768_public_key = next_key_result - - # Create payload_core: signer (32B) + next_key (32B) + SCALE(call) - signer_bytes = encode_account_id(wallet.coldkey.ss58_address) - scale_call_bytes = bytes(call.data.data) - next_key = hashlib.blake2b(next_key_result, digest_size=32).digest() - - payload_core = signer_bytes + next_key + scale_call_bytes - - mev_shield_version = mlkem_kdf_id() - genesis_hash_clean = ( - genesis_hash[2:] if genesis_hash.startswith("0x") else genesis_hash - ) - genesis_hash_bytes = bytes.fromhex(genesis_hash_clean) - - # Sign: coldkey.sign(b"mev-shield:v1" + genesis_hash + payload_core) - message_to_sign = ( - b"mev-shield:" + mev_shield_version + genesis_hash_bytes + payload_core - ) - signature = wallet.coldkey.sign(message_to_sign) - - # Plaintext: payload_core + b"\x01" + signature - plaintext = payload_core + b"\x01" + signature + plaintext = bytes(signed_extrinsic.data.data) # Encrypt using ML-KEM-768 ciphertext = encrypt_mlkem768(ml_kem_768_public_key, plaintext) # Commitment: blake2_256(payload_core) - commitment_hash = hashlib.blake2b(payload_core, digest_size=32).digest() + commitment_hash = hashlib.blake2b(plaintext, digest_size=32).digest() commitment_hex = "0x" + commitment_hash.hex() # Create the MevShield.submit_encrypted call @@ -85,111 +57,3 @@ async def encrypt_call( return encrypted_call - -async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[str]: - """ - Extract the MEV Shield wrapper ID from an extrinsic response. - - After submitting a MEV Shield encrypted call, the EncryptedSubmitted event - contains the wrapper ID needed to track execution. - - Args: - response: The extrinsic receipt from submit_extrinsic. - - Returns: - The wrapper ID (hex string) or None if not found. - """ - for event in await response.triggered_events: - if event["event_id"] == "EncryptedSubmitted": - return event["attributes"]["id"] - return None - - -async def wait_for_mev_execution( - subtensor: "SubtensorInterface", - wrapper_id: str, - submit_block_hash: str, - timeout_blocks: int = 4, - status=None, -) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]: - """ - Wait for MEV Shield inner call execution. - - After submit_encrypted succeeds, the block author will decrypt and execute - the inner call via execute_revealed. This function polls for the - DecryptedExecuted or DecryptedRejected event. - - Args: - subtensor: SubtensorInterface instance. - wrapper_id: The ID from EncryptedSubmitted event. - submit_block_number: Block number where submit_encrypted was included. - timeout_blocks: Max blocks to wait (default 4). - status: Optional rich.Status object for progress updates. - - Returns: - Tuple of (success: bool, error: Optional[str], receipt: Optional[AsyncExtrinsicReceipt]). - - (True, None, receipt) if DecryptedExecuted was found. - - (False, error_message, None) if the call failed or timeout. - """ - - async def _noop(_): - return True - - starting_block = await subtensor.substrate.get_block_number(submit_block_hash) - current_block = starting_block + 1 - - while current_block - starting_block <= timeout_blocks: - if status: - status.update( - f"Waiting for :shield: MEV Protection " - f"(checking block {current_block - starting_block} of {timeout_blocks})..." - ) - - await subtensor.substrate.wait_for_block( - current_block, - result_handler=_noop, - task_return=False, - ) - - block_hash = await subtensor.substrate.get_block_hash(current_block) - extrinsics = await subtensor.substrate.get_extrinsics(block_hash) - - # Find executeRevealed extrinsic & match ids - execute_revealed_index = None - for idx, extrinsic in enumerate(extrinsics): - call = extrinsic.value.get("call", {}) - call_module = call.get("call_module") - call_function = call.get("call_function") - - if call_module == "MevShield" and call_function in ( - "execute_revealed", - "mark_decryption_failed", - ): - call_args = call.get("call_args", []) - for arg in call_args: - if arg.get("name") == "id": - extrinsic_wrapper_id = arg.get("value") - if extrinsic_wrapper_id == wrapper_id: - execute_revealed_index = idx - break - - if execute_revealed_index is not None: - break - - if execute_revealed_index is None: - current_block += 1 - continue - - receipt = AsyncExtrinsicReceipt( - substrate=subtensor.substrate, - block_hash=block_hash, - extrinsic_idx=execute_revealed_index, - ) - - if not await receipt.is_success: - error_msg = format_error_message(await receipt.error_message) - return False, error_msg, None - - return True, None, receipt - - return False, "Timeout waiting for MEV Shield execution", None From 11e15b68a01995a6dd40c2c139c7a60598dc69ed Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:30:38 -0800 Subject: [PATCH 161/179] add create_mev_protected_extrinsic --- .../src/bittensor/extrinsics/mev_shield.py | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 5e0a81f99..582dc1651 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -6,6 +6,7 @@ from bittensor_cli.src.bittensor.utils import format_error_message if TYPE_CHECKING: + from bittensor_wallet import Keypair from scalecodec import GenericCall, GenericExtrinsic from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -15,21 +16,18 @@ async def encrypt_extrinsic( signed_extrinsic: "GenericExtrinsic", ) -> "GenericCall": """ - Encrypt a signed extrinsic using MEV Shield. + Encrypts a signed extrinsic using MEV Shield. - Takes a pre-signed extrinsic and returns a MevShield.submit_encrypted call. - The signed extrinsic should be created with nonce = current_nonce + 1, - as it will execute after the shield wrapper extrinsic. + Takes a pre-signed extrinsic and returns a `MevShield.submit_encrypted` call. + The inner extrinsic must be signed with `nonce = current_nonce + 1` because it + executes after the wrapper. - Args: - subtensor: The SubtensorInterface instance for chain queries. - signed_extrinsic: The signed extrinsic to encrypt. + :param subtensor: SubtensorInterface instance for chain queries. + :param signed_extrinsic: The pre-signed extrinsic to encrypt. - Returns: - A MevShield.submit_encrypted call to be signed with the current nonce. + :return: `MevShield.submit_encrypted` call to sign with the current nonce. - Raises: - ValueError: If MEV Shield NextKey is not available on chain. + :raises ValueError: If a MEV Shield `NextKey` is not available on chain. """ ml_kem_768_public_key = await subtensor.get_mev_shield_next_key() @@ -57,3 +55,49 @@ async def encrypt_extrinsic( return encrypted_call + +async def create_mev_protected_extrinsic( + subtensor: "SubtensorInterface", + keypair: "Keypair", + call: "GenericCall", + nonce: int, + era: Optional[int] = None, +) -> tuple["GenericExtrinsic", str]: + """ + Creates a MEV-protected extrinsic. + + Handles MEV Shield wrapping by signing the inner call with the future nonce, + encrypting it, and then signing the wrapper with the current nonce. + + :param subtensor: SubtensorInterface instance. + :param keypair: Keypair to sign both inner and wrapper extrinsics. + :param call: Call to protect (for example, `add_stake`). + :param nonce: Current account nonce; the wrapper uses this, the inner uses `nonce + 1`. + :param era: Optional era period for the extrinsic. + + :return: Tuple of `(signed_shield_extrinsic, inner_extrinsic_hash)`, where + `inner_extrinsic_hash` is used to track the actual extrinsic execution. + """ + + next_nonce = await subtensor.substrate.get_account_next_index(keypair.ss58_address) + + async def create_signed(call_to_sign, n): + kwargs = { + "call": call_to_sign, + "keypair": keypair, + "nonce": n, + } + if era is not None: + kwargs["era"] = {"period": era} + return await subtensor.substrate.create_signed_extrinsic(**kwargs) + + # Actual call: Sign with future nonce (current_nonce + 1) + inner_extrinsic = await create_signed(call, next_nonce) + inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}" + + # MeV Shield wrapper: Sign with current nonce + shield_call = await encrypt_extrinsic(subtensor, inner_extrinsic) + shield_extrinsic = await create_signed(shield_call, nonce) + + return shield_extrinsic, inner_hash + From 0ba42b628a44027c8f47154ce42b74634cbdd834 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:31:02 -0800 Subject: [PATCH 162/179] wait_for_extrinsic_by_hash --- .../src/bittensor/extrinsics/mev_shield.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 582dc1651..893510d7e 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -101,3 +101,116 @@ async def create_signed(call_to_sign, n): return shield_extrinsic, inner_hash + +async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[str]: + """ + Extracts the MEV Shield wrapper ID from an extrinsic response. + + After submitting a MEV Shield encrypted call, the `EncryptedSubmitted` event + contains the wrapper ID needed to track execution. + + :param response: Extrinsic receipt from `submit_extrinsic`. + + :return: Wrapper ID (hex string) or `None` if not found. + """ + for event in await response.triggered_events: + if event["event_id"] == "EncryptedSubmitted": + return event["attributes"]["id"] + return None + + +async def wait_for_extrinsic_by_hash( + subtensor: "SubtensorInterface", + extrinsic_hash: str, + shield_id: str, + submit_block_hash: str, + timeout_blocks: int = 2, + status=None, +) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]: + """ + Waits for the result of a MEV Shield encrypted extrinsic. + + After `submit_encrypted` succeeds, the block author decrypts and submits the + inner extrinsic directly. This polls subsequent blocks for either the inner + extrinsic hash (success) or a `mark_decryption_failed` extrinsic for the + matching shield ID (failure). + + :param subtensor: SubtensorInterface instance. + :param extrinsic_hash: Hash of the inner extrinsic to find. + :param shield_id: Wrapper ID from the `EncryptedSubmitted` event (used to detect decryption failures). + :param submit_block_hash: Block hash where `submit_encrypted` was included. + :param timeout_blocks: Maximum blocks to wait before timing out (default 2). + :param status: Optional `rich.Status` object for progress updates. + + :return: Tuple `(success, error_message, receipt)` where: + - `success` is True if the extrinsic was found and succeeded. + - `error_message` contains the formatted failure reason, if any. + - `receipt` is the AsyncExtrinsicReceipt when available. + """ + + async def _noop(_): + return True + + starting_block = await subtensor.substrate.get_block_number(submit_block_hash) + current_block = starting_block + 1 + + while current_block - starting_block <= timeout_blocks: + if status: + status.update( + f"Waiting for :shield: MEV Protection " + f"(checking block {current_block - starting_block} of {timeout_blocks})..." + ) + + await subtensor.substrate.wait_for_block( + current_block, + result_handler=_noop, + task_return=False, + ) + + block_hash = await subtensor.substrate.get_block_hash(current_block) + extrinsics = await subtensor.substrate.get_extrinsics(block_hash) + + result_idx = None + for idx, extrinsic in enumerate(extrinsics): + # Success: Inner extrinsic executed + if ( + extrinsic.extrinsic_hash + and f"0x{extrinsic.extrinsic_hash.hex()}" == extrinsic_hash + ): + result_idx = idx + break + + # Failure: Decryption failed + call = extrinsic.value.get("call", {}) + if ( + call.get("call_module") == "MevShield" + and call.get("call_function") == "mark_decryption_failed" + ): + call_args = call.get("call_args", []) + for arg in call_args: + if arg.get("name") == "id" and arg.get("value") == shield_id: + result_idx = idx + break + if result_idx is not None: + break + + if result_idx is not None: + receipt = AsyncExtrinsicReceipt( + substrate=subtensor.substrate, + block_hash=block_hash, + extrinsic_idx=result_idx, + ) + + if not await receipt.is_success: + error_msg = format_error_message(await receipt.error_message) + return False, error_msg, receipt + + return True, None, receipt + + current_block += 1 + + return ( + False, + "Failed to find outcome of the shield extrinsic (The protected extrinsic wasn't decrypted)", + None, + ) From e318a976d23c089081b7eafd952e7a224cc3633f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:32:34 -0800 Subject: [PATCH 163/179] update add stake --- bittensor_cli/src/commands/stake/add.py | 84 ++++++++++++++++--------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index d88692261..48ebf9f75 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -12,9 +12,9 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - encrypt_call, + create_mev_protected_extrinsic, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from bittensor_cli.src.bittensor.utils import ( console, @@ -141,13 +141,21 @@ async def safe_stake_extrinsic( ), ) if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.coldkey, - nonce=next_nonce, - era={"period": era}, - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.coldkey, + nonce=next_nonce, + era={"period": era}, + ) try: response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False @@ -171,15 +179,18 @@ async def safe_stake_extrinsic( if mev_protection: mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None if json_output: # the rest of this checking is not necessary if using json_output @@ -245,10 +256,18 @@ async def stake_extrinsic( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) try: response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False @@ -264,15 +283,18 @@ async def stake_extrinsic( if mev_protection: mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None if json_output: # the rest of this is not necessary if using json_output From bf8b5685ccb9b609b95099ff5ff32c989ba6c05c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:34:05 -0800 Subject: [PATCH 164/179] update move cmds --- bittensor_cli/src/commands/stake/move.py | 118 +++++++++++++++-------- 1 file changed, 77 insertions(+), 41 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index a7f3f0782..b93130cf1 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -9,9 +9,9 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - encrypt_call, + create_mev_protected_extrinsic, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from bittensor_cli.src.bittensor.utils import ( console, @@ -548,13 +548,14 @@ async def move_stake( "alpha_amount": amount_to_move_as_balance.rao, }, ) - sim_swap, extrinsic_fee = await asyncio.gather( + sim_swap, extrinsic_fee, next_nonce = await asyncio.gather( subtensor.sim_swap( origin_netuid=origin_netuid, destination_netuid=destination_netuid, amount=amount_to_move_as_balance.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) # Display stake movement details @@ -586,24 +587,35 @@ async def move_stake( f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." ) as status: if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) if mev_protection: mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" ext_id = await response.get_extrinsic_identifier() @@ -752,13 +764,14 @@ async def transfer_stake( "alpha_amount": amount_to_transfer.rao, }, ) - sim_swap, extrinsic_fee = await asyncio.gather( + sim_swap, extrinsic_fee, next_nonce = await asyncio.gather( subtensor.sim_swap( origin_netuid=origin_netuid, destination_netuid=dest_netuid, amount=amount_to_transfer.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) # Display stake movement details @@ -788,10 +801,18 @@ async def transfer_stake( with console.status("\n:satellite: Transferring stake ...") as status: if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False @@ -799,14 +820,17 @@ async def transfer_stake( if mev_protection: mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" ext_id = await response.get_extrinsic_identifier() @@ -939,13 +963,14 @@ async def swap_stake( "alpha_amount": amount_to_swap.rao, }, ) - sim_swap, extrinsic_fee = await asyncio.gather( + sim_swap, extrinsic_fee, next_nonce = await asyncio.gather( subtensor.sim_swap( origin_netuid=origin_netuid, destination_netuid=destination_netuid, amount=amount_to_swap.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) # Display stake movement details @@ -978,10 +1003,18 @@ async def swap_stake( f"to netuid [blue]{destination_netuid}[/blue]..." ) as status: if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) response = await subtensor.substrate.submit_extrinsic( extrinsic, @@ -991,14 +1024,17 @@ async def swap_stake( if mev_protection: mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" ext_id = await response.get_extrinsic_identifier() From b812f2afcf943f376e6b5946e3247e183fd7deaf Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:34:35 -0800 Subject: [PATCH 165/179] update remove cmds --- bittensor_cli/src/commands/stake/remove.py | 133 +++++++++++++-------- 1 file changed, 86 insertions(+), 47 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index afd9310da..99796ec5e 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -12,9 +12,9 @@ from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - encrypt_call, + create_mev_protected_extrinsic, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( @@ -599,8 +599,9 @@ async def _unstake_extrinsic( f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..." ) - current_balance, call = await asyncio.gather( + current_balance, next_nonce, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake", @@ -613,10 +614,18 @@ async def _unstake_extrinsic( ) if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) try: response = await subtensor.substrate.submit_extrinsic( @@ -631,15 +640,18 @@ async def _unstake_extrinsic( if mev_protection: mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None # Fetch latest balance and stake await print_extrinsic_id(response) @@ -705,7 +717,7 @@ async def _safe_unstake_extrinsic( block_hash = await subtensor.substrate.get_chain_head() - current_balance, current_stake, call = await asyncio.gather( + current_balance, current_stake, next_nonce, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), subtensor.get_stake( hotkey_ss58=hotkey_ss58, @@ -713,6 +725,7 @@ async def _safe_unstake_extrinsic( netuid=netuid, block_hash=block_hash, ), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake_limit", @@ -728,10 +741,18 @@ async def _safe_unstake_extrinsic( ) if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) try: response = await subtensor.substrate.submit_extrinsic( @@ -757,15 +778,18 @@ async def _safe_unstake_extrinsic( if mev_protection: mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() @@ -850,20 +874,32 @@ async def _unstake_all_extrinsic( previous_root_stake = None call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all" - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function=call_function, - call_params={"hotkey": hotkey_ss58}, + call, next_nonce = await asyncio.gather( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params={"hotkey": hotkey_ss58}, + ), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) if mev_protection: - call = await encrypt_call(subtensor, wallet, call) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + keypair=wallet.coldkey, + call=call, + next_nonce=next_nonce, + era=era, + ) + else: + inner_hash = None + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + ) try: response = await subtensor.substrate.submit_extrinsic( - extrinsic=await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, era={"period": era} - ), + extrinsic, wait_for_inclusion=True, wait_for_finalization=False, ) @@ -877,15 +913,18 @@ async def _unstake_all_extrinsic( if mev_protection: mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None await print_extrinsic_id(response) From 70abc2163f7841004d599bb0ee74eb66a1befb72 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:35:37 -0800 Subject: [PATCH 166/179] update sn creation --- bittensor_cli/src/commands/subnets/subnets.py | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 14c026b01..d35a2701a 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -18,9 +18,9 @@ ) from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - encrypt_call, + create_mev_protected_extrinsic, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph @@ -232,17 +232,27 @@ async def _find_event_attributes_in_extrinsic_receipt( with console.status(":satellite: Registering subnet...", spinner="earth") as status: substrate = subtensor.substrate - # create extrinsic call - call = await substrate.compose_call( - call_module="SubtensorModule", - call_function=call_function, - call_params=call_params, + call, next_nonce = await asyncio.gather( + substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ), + substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - extrinsic = await substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey - ) + extrinsic, inner_hash = await create_mev_protected_extrinsic( + subtensor=subtensor, + wallet=wallet, + call=call, + next_nonce=next_nonce, + era=None, + ) + else: + inner_hash = None + extrinsic = await substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey, nonce=next_nonce + ) response = await substrate.submit_extrinsic( extrinsic, wait_for_inclusion=wait_for_inclusion, @@ -263,16 +273,19 @@ async def _find_event_attributes_in_extrinsic_receipt( # Check for MEV shield execution if mev_protection: mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + inner_hash=inner_hash, + mev_shield_id=mev_shield_id, + block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_console.print( + f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" ) - if not mev_success: - status.stop() - err_console.print( - f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" - ) - return False, None, None + return False, None, None # Successful registration, final check for membership attributes = await _find_event_attributes_in_extrinsic_receipt( From 6d5c050a027364a88a6a2ca0a49c2664c7de74a9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 02:43:51 -0800 Subject: [PATCH 167/179] wip --- .../src/bittensor/extrinsics/mev_shield.py | 101 ++++++++++-------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 893510d7e..2732b8a3f 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -16,18 +16,19 @@ async def encrypt_extrinsic( signed_extrinsic: "GenericExtrinsic", ) -> "GenericCall": """ - Encrypts a signed extrinsic using MEV Shield. + Encrypt a signed extrinsic using MEV Shield. - Takes a pre-signed extrinsic and returns a `MevShield.submit_encrypted` call. - The inner extrinsic must be signed with `nonce = current_nonce + 1` because it - executes after the wrapper. + Takes a pre-signed extrinsic and returns a MevShield.submit_encrypted call. - :param subtensor: SubtensorInterface instance for chain queries. - :param signed_extrinsic: The pre-signed extrinsic to encrypt. + Args: + subtensor: The SubtensorInterface instance for chain queries. + signed_extrinsic: The signed extrinsic to encrypt. - :return: `MevShield.submit_encrypted` call to sign with the current nonce. + Returns: + A MevShield.submit_encrypted call to be signed with the current nonce. - :raises ValueError: If a MEV Shield `NextKey` is not available on chain. + Raises: + ValueError: If MEV Shield NextKey is not available on chain. """ ml_kem_768_public_key = await subtensor.get_mev_shield_next_key() @@ -64,23 +65,25 @@ async def create_mev_protected_extrinsic( era: Optional[int] = None, ) -> tuple["GenericExtrinsic", str]: """ - Creates a MEV-protected extrinsic. - - Handles MEV Shield wrapping by signing the inner call with the future nonce, - encrypting it, and then signing the wrapper with the current nonce. - - :param subtensor: SubtensorInterface instance. - :param keypair: Keypair to sign both inner and wrapper extrinsics. - :param call: Call to protect (for example, `add_stake`). - :param nonce: Current account nonce; the wrapper uses this, the inner uses `nonce + 1`. - :param era: Optional era period for the extrinsic. - - :return: Tuple of `(signed_shield_extrinsic, inner_extrinsic_hash)`, where - `inner_extrinsic_hash` is used to track the actual extrinsic execution. + Create a MEV-protected extrinsic. + + This function handles MEV Shield wrapping: + 1. Fetches a future nonce (current_nonce + 1) & signs the inner call with it + 2. Encrypts it into a wrapper call + 3. Signs the wrapper with the current nonce + + Args: + subtensor: The SubtensorInterface instance. + keypair: Keypair for signing. + call: The call to protect (e.g., add_stake). + nonce: The current account nonce (wrapper will use this, inner uses nonce+1). + era: The era period for the extrinsic. + + Returns: + Tuple of (signed_shield_extrinsic, inner_extrinsic_hash). + The inner_extrinsic_hash is used for tracking actual extrinsic. """ - next_nonce = await subtensor.substrate.get_account_next_index(keypair.ss58_address) - async def create_signed(call_to_sign, n): kwargs = { "call": call_to_sign, @@ -91,6 +94,8 @@ async def create_signed(call_to_sign, n): kwargs["era"] = {"period": era} return await subtensor.substrate.create_signed_extrinsic(**kwargs) + next_nonce = await subtensor.substrate.get_account_next_index(keypair.ss58_address) + # Actual call: Sign with future nonce (current_nonce + 1) inner_extrinsic = await create_signed(call, next_nonce) inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}" @@ -104,14 +109,16 @@ async def create_signed(call_to_sign, n): async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[str]: """ - Extracts the MEV Shield wrapper ID from an extrinsic response. + Extract the MEV Shield wrapper ID from an extrinsic response. - After submitting a MEV Shield encrypted call, the `EncryptedSubmitted` event + After submitting a MEV Shield encrypted call, the EncryptedSubmitted event contains the wrapper ID needed to track execution. - :param response: Extrinsic receipt from `submit_extrinsic`. + Args: + response: The extrinsic receipt from submit_extrinsic. - :return: Wrapper ID (hex string) or `None` if not found. + Returns: + The wrapper ID (hex string) or None if not found. """ for event in await response.triggered_events: if event["event_id"] == "EncryptedSubmitted": @@ -128,24 +135,28 @@ async def wait_for_extrinsic_by_hash( status=None, ) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]: """ - Waits for the result of a MEV Shield encrypted extrinsic. - - After `submit_encrypted` succeeds, the block author decrypts and submits the - inner extrinsic directly. This polls subsequent blocks for either the inner - extrinsic hash (success) or a `mark_decryption_failed` extrinsic for the - matching shield ID (failure). - - :param subtensor: SubtensorInterface instance. - :param extrinsic_hash: Hash of the inner extrinsic to find. - :param shield_id: Wrapper ID from the `EncryptedSubmitted` event (used to detect decryption failures). - :param submit_block_hash: Block hash where `submit_encrypted` was included. - :param timeout_blocks: Maximum blocks to wait before timing out (default 2). - :param status: Optional `rich.Status` object for progress updates. - - :return: Tuple `(success, error_message, receipt)` where: - - `success` is True if the extrinsic was found and succeeded. - - `error_message` contains the formatted failure reason, if any. - - `receipt` is the AsyncExtrinsicReceipt when available. + Wait for the result of a MeV Shield encrypted extrinsic. + + After submit_encrypted succeeds, the block author will decrypt and submit + the inner extrinsic directly. This function polls subsequent blocks looking + for either: + - an extrinsic matching the provided hash (success) + OR + - a markDecryptionFailed extrinsic with matching shield ID (failure) + + Args: + subtensor: SubtensorInterface instance. + extrinsic_hash: The hash of the inner extrinsic to find. + shield_id: The wrapper ID from EncryptedSubmitted event (for detecting decryption failures). + submit_block_hash: Block hash where submit_encrypted was included. + timeout_blocks: Max blocks to wait (default 2). + status: Optional rich.Status object for progress updates. + + Returns: + Tuple of (success: bool, error: Optional[str], receipt: Optional[AsyncExtrinsicReceipt]). + - (True, None, receipt) if extrinsic was found and succeeded. + - (False, error_message, receipt) if extrinsic was found but failed. + - (False, "Timeout...", None) if not found within timeout. """ async def _noop(_): From fb91c5ab5d7c9160044be2c1e3e2c770e9a22322 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 03:11:45 -0800 Subject: [PATCH 168/179] updates --- .../src/bittensor/extrinsics/mev_shield.py | 5 +--- bittensor_cli/src/commands/stake/add.py | 16 ++++++------- bittensor_cli/src/commands/stake/move.py | 24 +++++++++---------- bittensor_cli/src/commands/stake/remove.py | 24 +++++++++---------- bittensor_cli/src/commands/subnets/subnets.py | 10 ++++---- 5 files changed, 38 insertions(+), 41 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 2732b8a3f..382d8029a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -184,10 +184,7 @@ async def _noop(_): result_idx = None for idx, extrinsic in enumerate(extrinsics): # Success: Inner extrinsic executed - if ( - extrinsic.extrinsic_hash - and f"0x{extrinsic.extrinsic_hash.hex()}" == extrinsic_hash - ): + if f"0x{extrinsic.extrinsic_hash.hex()}" == extrinsic_hash: result_idx = idx break diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 48ebf9f75..c86738a04 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -145,7 +145,7 @@ async def safe_stake_extrinsic( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -181,9 +181,9 @@ async def safe_stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: @@ -260,7 +260,7 @@ async def stake_extrinsic( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -285,9 +285,9 @@ async def stake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index b93130cf1..dc23fd17a 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -591,7 +591,7 @@ async def move_stake( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -607,9 +607,9 @@ async def move_stake( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: @@ -805,7 +805,7 @@ async def transfer_stake( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -822,9 +822,9 @@ async def transfer_stake( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: @@ -1007,7 +1007,7 @@ async def swap_stake( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -1026,9 +1026,9 @@ async def swap_stake( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 99796ec5e..bf669765f 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -618,7 +618,7 @@ async def _unstake_extrinsic( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -642,9 +642,9 @@ async def _unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: @@ -745,7 +745,7 @@ async def _safe_unstake_extrinsic( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -780,9 +780,9 @@ async def _safe_unstake_extrinsic( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: @@ -888,7 +888,7 @@ async def _unstake_all_extrinsic( subtensor=subtensor, keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=era, ) else: @@ -915,9 +915,9 @@ async def _unstake_all_extrinsic( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d35a2701a..a7167b07e 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -243,9 +243,9 @@ async def _find_event_attributes_in_extrinsic_receipt( if mev_protection: extrinsic, inner_hash = await create_mev_protected_extrinsic( subtensor=subtensor, - wallet=wallet, + keypair=wallet.coldkey, call=call, - next_nonce=next_nonce, + nonce=next_nonce, era=None, ) else: @@ -275,9 +275,9 @@ async def _find_event_attributes_in_extrinsic_receipt( mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, - inner_hash=inner_hash, - mev_shield_id=mev_shield_id, - block_hash=response.block_hash, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, status=status, ) if not mev_success: From 3c55d9cb0c9babebbdbd1227e8e3bef4f8ae40e1 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 16:38:29 +0200 Subject: [PATCH 169/179] Added block_number to AsyncExtrinsicReceipt from mev --- bittensor_cli/src/bittensor/extrinsics/mev_shield.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index 382d8029a..cfa2650df 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -206,6 +206,7 @@ async def _noop(_): receipt = AsyncExtrinsicReceipt( substrate=subtensor.substrate, block_hash=block_hash, + block_number=current_block, extrinsic_idx=result_idx, ) From 0780305f8a306f014ed1d0da2583b6ea18d8d158 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 17:44:30 +0200 Subject: [PATCH 170/179] Include mev protection logic in sign and send extrinsic method --- .../src/bittensor/subtensor_interface.py | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 70af13cdc..e8c69d51b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1178,6 +1178,7 @@ async def sign_and_send_extrinsic( nonce: Optional[int] = None, sign_with: Literal["coldkey", "hotkey", "coldkeypub"] = "coldkey", announce_only: bool = False, + mev_protection: bool = False, ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1192,8 +1193,24 @@ async def sign_and_send_extrinsic( :param sign_with: Determine which of the wallet's keypairs to use to sign the extrinsic call. :param announce_only: If set, makes the call as an announcement, rather than making the call. - :return: (success, error message, extrinsic receipt | None) + :return: (success, error message or inner extrinsic hash (if using mev_protection), extrinsic receipt | None) """ + + async def create_signed(call_to_sign, n): + kwargs = { + "call": call_to_sign, + "keypair": keypair, + "nonce": n, + } + if era is not None: + kwargs["era"] = {"period": era} + return await self.substrate.create_signed_extrinsic(**kwargs) + + if announce_only and mev_protection: + raise ValueError( + "Cannot use announce-only and mev-protection. Calls should be announced without mev protection," + "and executed with them." + ) if proxy is not None: if announce_only: call_to_announce = call @@ -1225,7 +1242,17 @@ async def sign_and_send_extrinsic( call_args["nonce"] = await self.substrate.get_account_next_index( keypair.ss58_address ) - extrinsic = await self.substrate.create_signed_extrinsic(**call_args) + inner_hash = "" + if mev_protection: + next_nonce = await self.substrate.get_account_next_index( + keypair.ss58_address + ) + inner_extrinsic = await create_signed(call, next_nonce) + inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}" + shield_call = await encrypt_extrinsic(self, inner_extrinsic) + extrinsic = await create_signed(shield_call, nonce) + else: + extrinsic = await self.substrate.create_signed_extrinsic(**call_args) try: response = await self.substrate.submit_extrinsic( extrinsic, @@ -1234,7 +1261,7 @@ async def sign_and_send_extrinsic( ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "", response + return True, inner_hash, response if await response.is_success: if announce_only: block = await self.substrate.get_block_number(response.block_hash) @@ -1251,7 +1278,7 @@ async def sign_and_send_extrinsic( console.print( f"Added entry {call_to_announce.call_hash} at block {block} to your ProxyAnnouncements address book." ) - return True, "", response + return True, inner_hash, response else: return False, format_error_message(await response.error_message), None except SubstrateRequestException as e: From b1b87483ef2916250b01326a2e7081978f7cf08d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 18:09:33 +0200 Subject: [PATCH 171/179] Makes things work with the proxy logic --- .../src/bittensor/extrinsics/mev_shield.py | 50 ----- .../src/bittensor/subtensor_interface.py | 7 +- bittensor_cli/src/commands/stake/add.py | 51 ++--- bittensor_cli/src/commands/stake/move.py | 192 +++++++++--------- bittensor_cli/src/commands/stake/remove.py | 63 +++--- bittensor_cli/src/commands/subnets/subnets.py | 7 +- 6 files changed, 169 insertions(+), 201 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index cfa2650df..4a95dbb73 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -57,56 +57,6 @@ async def encrypt_extrinsic( return encrypted_call -async def create_mev_protected_extrinsic( - subtensor: "SubtensorInterface", - keypair: "Keypair", - call: "GenericCall", - nonce: int, - era: Optional[int] = None, -) -> tuple["GenericExtrinsic", str]: - """ - Create a MEV-protected extrinsic. - - This function handles MEV Shield wrapping: - 1. Fetches a future nonce (current_nonce + 1) & signs the inner call with it - 2. Encrypts it into a wrapper call - 3. Signs the wrapper with the current nonce - - Args: - subtensor: The SubtensorInterface instance. - keypair: Keypair for signing. - call: The call to protect (e.g., add_stake). - nonce: The current account nonce (wrapper will use this, inner uses nonce+1). - era: The era period for the extrinsic. - - Returns: - Tuple of (signed_shield_extrinsic, inner_extrinsic_hash). - The inner_extrinsic_hash is used for tracking actual extrinsic. - """ - - async def create_signed(call_to_sign, n): - kwargs = { - "call": call_to_sign, - "keypair": keypair, - "nonce": n, - } - if era is not None: - kwargs["era"] = {"period": era} - return await subtensor.substrate.create_signed_extrinsic(**kwargs) - - next_nonce = await subtensor.substrate.get_account_next_index(keypair.ss58_address) - - # Actual call: Sign with future nonce (current_nonce + 1) - inner_extrinsic = await create_signed(call, next_nonce) - inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}" - - # MeV Shield wrapper: Sign with current nonce - shield_call = await encrypt_extrinsic(subtensor, inner_extrinsic) - shield_extrinsic = await create_signed(shield_call, nonce) - - return shield_extrinsic, inner_hash - - async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[str]: """ Extract the MEV Shield wrapper ID from an extrinsic response. diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e8c69d51b..e30772294 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -34,6 +34,7 @@ from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float from bittensor_cli.src import Constants, defaults, TYPE_REGISTRY +from bittensor_cli.src.bittensor.extrinsics.mev_shield import encrypt_extrinsic from bittensor_cli.src.bittensor.utils import ( format_error_message, console, @@ -2406,6 +2407,7 @@ async def get_all_subnet_ema_tao_inflow( else: _, raw_ema_value = value ema_value = fixed_to_float(raw_ema_value) + # TODO @abe is this intentional: float passed as int for from_rao ema_map[netuid] = Balance.from_rao(ema_value) return ema_map @@ -2437,12 +2439,13 @@ async def get_subnet_ema_tao_inflow( return Balance.from_rao(0) _, raw_ema_value = value ema_value = fixed_to_float(raw_ema_value) + # TODO @abe this is a float, but we're passing it as an int for from_rao, is this intentional? return Balance.from_rao(ema_value) async def get_mev_shield_next_key( self, block_hash: Optional[str] = None, - ) -> Optional[tuple[bytes, int]]: + ) -> bytes: """ Get the next MEV Shield public key and epoch from chain storage. @@ -2470,7 +2473,7 @@ async def get_mev_shield_next_key( async def get_mev_shield_current_key( self, block_hash: Optional[str] = None, - ) -> Optional[tuple[bytes, int]]: + ) -> bytes: """ Get the current MEV Shield public key and epoch from chain storage. diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 7146ade41..126854aff 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -11,7 +11,6 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - create_mev_protected_extrinsic, extract_mev_shield_id, wait_for_extrinsic_by_hash, ) @@ -141,14 +140,13 @@ async def safe_stake_extrinsic( }, ), ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, nonce=next_nonce, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if not success_: if "Custom error: 8" in err_msg: @@ -164,16 +162,20 @@ async def safe_stake_extrinsic( return False, err_msg, None else: if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status_ - ) - if not mev_success: - status_.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status_, + ) + if not mev_success: + status_.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None if json_output: # the rest of this checking is not necessary if using json_output return True, "", response @@ -239,14 +241,13 @@ async def stake_extrinsic( failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, nonce=next_nonce, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if not success_: err_msg = f"{failure_prelude} with error: {err_msg}" @@ -254,16 +255,20 @@ async def stake_extrinsic( return False, err_msg, None else: if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status_ - ) - if not mev_success: - status_.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status_, + ) + if not mev_success: + status_.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None if json_output: # the rest of this is not necessary if using json_output return True, "", response diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 97ec7baa6..86a5f9087 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -9,7 +9,6 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - create_mev_protected_extrinsic, extract_mev_shield_id, wait_for_extrinsic_by_hash, ) @@ -588,28 +587,30 @@ async def move_stake( f"[blue]{origin_netuid}[/blue] \nto " f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." ) as status: - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) - if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" - ext_id = await response.get_extrinsic_identifier() if response else "" if success_: + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" await print_extrinsic_id(response) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") @@ -797,59 +798,62 @@ async def transfer_stake( return False, "" with console.status("\n:satellite: Transferring stake ...") as status: - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) - if success_: - if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status + if success_: + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, ) if not mev_success: status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" - await print_extrinsic_id(response) - ext_id = await response.get_extrinsic_identifier() - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, ext_id - else: - # Get and display new stake balances - new_stake, new_dest_stake = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=origin_hotkey, - netuid=origin_netuid, - ), - subtensor.get_stake( - coldkey_ss58=dest_coldkey_ss58, - hotkey_ss58=origin_hotkey, - netuid=dest_netuid, - ), - ) + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, ext_id + else: + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=dest_coldkey_ss58, + hotkey_ss58=origin_hotkey, + netuid=dest_netuid, + ), + ) - console.print( - f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" - ) - console.print( - f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" - ) - return True, ext_id + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" + ) + return True, ext_id - else: - err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") - return False, "" + else: + err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") + return False, "" async def swap_stake( @@ -991,8 +995,6 @@ async def swap_stake( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " f"to netuid [blue]{destination_netuid}[/blue]..." ) as status: - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -1000,51 +1002,55 @@ async def swap_stake( proxy=proxy, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + mev_protection=mev_protection, ) - if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status + ext_id = await response.get_extrinsic_identifier() + + if success_: + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, ) if not mev_success: status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" + await print_extrinsic_id(response) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, await response.get_extrinsic_identifier() + else: + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=destination_netuid, + ), + ) - ext_id = await response.get_extrinsic_identifier() + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" + ) + return True, ext_id - if success_: - await print_extrinsic_id(response) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, await response.get_extrinsic_identifier() else: - # Get and display new stake balances - new_stake, new_dest_stake = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - netuid=origin_netuid, - ), - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - netuid=destination_netuid, - ), - ) - - console.print( - f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" - ) - console.print( - f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" - ) - return True, ext_id - - else: - err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") - return False, "" + err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") + return False, "" diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 2105d94fd..098acf0e6 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -11,7 +11,6 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - create_mev_protected_extrinsic, extract_mev_shield_id, wait_for_extrinsic_by_hash, ) @@ -626,24 +625,29 @@ async def _unstake_extrinsic( ), ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - success, err_msg, response = await subtensor.sign_and_send_extrinsic( - call=call, wallet=wallet, era={"period": era}, proxy=proxy + # TODO I think this should handle announce-only + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, + mev_protection=mev_protection, ) if success: if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, None await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -735,28 +739,29 @@ async def _safe_unstake_extrinsic( block_hash=block_hash, ), ) - - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, nonce=next_nonce, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if success: if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, None await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -860,16 +865,13 @@ async def _unstake_all_extrinsic( ), subtensor.substrate.get_account_next_index(coldkey_ss58), ) - - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - try: success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if not success_: @@ -877,6 +879,7 @@ async def _unstake_all_extrinsic( return False, None if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index b81a68344..9afed86f7 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -19,7 +19,6 @@ ) from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - create_mev_protected_extrinsic, extract_mev_shield_id, wait_for_extrinsic_by_hash, ) @@ -233,6 +232,8 @@ async def _find_event_attributes_in_extrinsic_receipt( if not unlock_key(wallet).success: return False, None, None + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address + with console.status(":satellite: Registering subnet...", spinner="earth") as status: substrate = subtensor.substrate call, next_nonce = await asyncio.gather( @@ -243,14 +244,13 @@ async def _find_event_attributes_in_extrinsic_receipt( ), substrate.get_account_next_index(coldkey_ss58), ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, proxy=proxy, + mev_protection=mev_protection, ) # We only wait here if we expect finalization. @@ -263,6 +263,7 @@ async def _find_event_attributes_in_extrinsic_receipt( else: # Check for MEV shield execution if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) mev_success, mev_error, response = await wait_for_extrinsic_by_hash( subtensor=subtensor, From 000c5d6635e86c0325b4d2d9472c89cbd0e3e3c5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 18:18:13 +0200 Subject: [PATCH 172/179] Resolved todos --- bittensor_cli/src/commands/stake/move.py | 6 ++---- tests/e2e_tests/test_unstaking.py | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 86a5f9087..a52b8c5a3 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -767,8 +767,7 @@ async def transfer_stake( amount=amount_to_transfer.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), - # TODO should this be proxy or signer? - subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index(proxy or wallet.coldkeypub.ss58_address), ) # Display stake movement details @@ -961,8 +960,7 @@ async def swap_stake( amount=amount_to_swap.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), - # TODO should this be proxy or signer? - subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index(proxy or wallet.coldkeypub.ss58_address), ) # Display stake movement details diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 8f17341a0..a13da8379 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -89,6 +89,7 @@ def test_unstaking(local_chain, wallet_setup): "--no-prompt", ], ) + print(result.stdout, result.stderr) assert "✅ Registered subnetwork with netuid: 2" in result.stdout assert "Your extrinsic has been included" in result.stdout, result.stdout From 116cec1b8a90b7aa09f522cd9dfadf8e033710bd Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 18:38:09 +0200 Subject: [PATCH 173/179] Small fixes --- bittensor_cli/src/bittensor/subtensor_interface.py | 7 +++++-- bittensor_cli/src/commands/stake/move.py | 8 ++++++-- bittensor_cli/src/commands/subnets/subnets.py | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e30772294..f48972a9a 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1192,7 +1192,10 @@ async def sign_and_send_extrinsic( :param proxy: The real account used to create the proxy. None if not using a proxy for this call. :param nonce: The nonce used to submit this extrinsic call. :param sign_with: Determine which of the wallet's keypairs to use to sign the extrinsic call. - :param announce_only: If set, makes the call as an announcement, rather than making the call. + :param announce_only: If set, makes the call as an announcement, rather than making the call. Cannot + be used with `mev_protection=True`. + :param mev_protection: If set, uses Mev Protection on the extrinsic, thus encrypting it. Cannot be + used with `announce_only=True`. :return: (success, error message or inner extrinsic hash (if using mev_protection), extrinsic receipt | None) """ @@ -1204,7 +1207,7 @@ async def create_signed(call_to_sign, n): "nonce": n, } if era is not None: - kwargs["era"] = {"period": era} + kwargs["era"] = era return await self.substrate.create_signed_extrinsic(**kwargs) if announce_only and mev_protection: diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index a52b8c5a3..d65f67e71 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -767,7 +767,9 @@ async def transfer_stake( amount=amount_to_transfer.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), - subtensor.substrate.get_account_next_index(proxy or wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index( + proxy or wallet.coldkeypub.ss58_address + ), ) # Display stake movement details @@ -960,7 +962,9 @@ async def swap_stake( amount=amount_to_swap.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), - subtensor.substrate.get_account_next_index(proxy or wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index( + proxy or wallet.coldkeypub.ss58_address + ), ) # Display stake movement details diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 9afed86f7..d0c90477e 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -250,6 +250,7 @@ async def _find_event_attributes_in_extrinsic_receipt( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, proxy=proxy, + nonce=next_nonce, mev_protection=mev_protection, ) From 9dc99645a585d902adece6598bee950c3f2ee8cf Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 18:58:08 +0200 Subject: [PATCH 174/179] Added missing nonces --- bittensor_cli/src/commands/stake/move.py | 3 +++ bittensor_cli/src/commands/stake/remove.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index d65f67e71..8f9255f70 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -593,6 +593,7 @@ async def move_stake( era={"period": era}, proxy=proxy, mev_protection=mev_protection, + nonce=next_nonce, ) ext_id = await response.get_extrinsic_identifier() if response else "" @@ -805,6 +806,7 @@ async def transfer_stake( era={"period": era}, proxy=proxy, mev_protection=mev_protection, + nonce=next_nonce, ) if success_: @@ -1005,6 +1007,7 @@ async def swap_stake( wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, mev_protection=mev_protection, + nonce=next_nonce, ) ext_id = await response.get_extrinsic_identifier() diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 098acf0e6..85ce18ff5 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -632,6 +632,7 @@ async def _unstake_extrinsic( era={"period": era}, proxy=proxy, mev_protection=mev_protection, + nonce=next_nonce, ) if success: if mev_protection: @@ -870,6 +871,7 @@ async def _unstake_all_extrinsic( call=call, wallet=wallet, era={"period": era}, + nonce=next_nonce, proxy=proxy, mev_protection=mev_protection, ) From fb9ed9b1eb61413072bfdbf25dbf7d560ac81f22 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 19:26:06 +0200 Subject: [PATCH 175/179] Ensure we remain inside the status --- bittensor_cli/src/commands/subnets/subnets.py | 78 +++++++++---------- tests/e2e_tests/test_unstaking.py | 1 - 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d0c90477e..509e36957 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -254,50 +254,50 @@ async def _find_event_attributes_in_extrinsic_receipt( mev_protection=mev_protection, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None, None + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, None, None - if not success: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False, None, None - else: - # Check for MEV shield execution - if mev_protection: - inner_hash = err_msg - mev_shield_id = await extract_mev_shield_id(response) - mev_success, mev_error, response = await wait_for_extrinsic_by_hash( - subtensor=subtensor, - extrinsic_hash=inner_hash, - shield_id=mev_shield_id, - submit_block_hash=response.block_hash, - status=status, - ) - if not mev_success: - status.stop() - err_console.print( - f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" + if not success: + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + return False, None, None + else: + # Check for MEV shield execution + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, ) - return False, None, None + if not mev_success: + status.stop() + err_console.print( + f":cross_mark: [red]Failed[/red]: MEV execution failed: {mev_error}" + ) + return False, None, None - # Successful registration, final check for membership + # Successful registration, final check for membership - attributes = await _find_event_attributes_in_extrinsic_receipt( - response, "NetworkAdded" - ) - await print_extrinsic_id(response) - ext_id = await response.get_extrinsic_identifier() - if not attributes: - console.print( - ":exclamation: [yellow]A possible error has occurred[/yellow]. The extrinsic reports success, but " - "we are unable to locate the 'NetworkAdded' event inside the extrinsic's events." - "" - ) - else: - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + attributes = await _find_event_attributes_in_extrinsic_receipt( + response, "NetworkAdded" ) - return True, int(attributes[0]), ext_id + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() + if not attributes: + console.print( + ":exclamation: [yellow]A possible error has occurred[/yellow]. The extrinsic reports success, but " + "we are unable to locate the 'NetworkAdded' event inside the extrinsic's events." + "" + ) + else: + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + ) + return True, int(attributes[0]), ext_id # commands diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index a13da8379..8f17341a0 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -89,7 +89,6 @@ def test_unstaking(local_chain, wallet_setup): "--no-prompt", ], ) - print(result.stdout, result.stderr) assert "✅ Registered subnetwork with netuid: 2" in result.stdout assert "Your extrinsic has been included" in result.stdout, result.stdout From 6b16a908e39cf507f3015e6b19c4e2a35f9f2b1a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 14:44:40 -0800 Subject: [PATCH 176/179] update ema calls --- bittensor_cli/src/bittensor/subtensor_interface.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index f48972a9a..10004797f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -2409,8 +2409,7 @@ async def get_all_subnet_ema_tao_inflow( ema_map[netuid] = Balance.from_rao(0) else: _, raw_ema_value = value - ema_value = fixed_to_float(raw_ema_value) - # TODO @abe is this intentional: float passed as int for from_rao + ema_value = int(fixed_to_float(raw_ema_value)) ema_map[netuid] = Balance.from_rao(ema_value) return ema_map @@ -2441,8 +2440,7 @@ async def get_subnet_ema_tao_inflow( if not value: return Balance.from_rao(0) _, raw_ema_value = value - ema_value = fixed_to_float(raw_ema_value) - # TODO @abe this is a float, but we're passing it as an int for from_rao, is this intentional? + ema_value = int(fixed_to_float(raw_ema_value)) return Balance.from_rao(ema_value) async def get_mev_shield_next_key( From 3887d5488daa58f44c384850c5ffa067466ab23a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Dec 2025 15:51:50 -0800 Subject: [PATCH 177/179] bumps version and changelog --- CHANGELOG.md | 18 ++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2447096f0..bb37bfc03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 9.16.0 /2025-12-09 + +## What's Changed +* proxy help text by @chideraao in https://github.com/opentensor/btcli/pull/707 +* In `btcli st list`, display the total even with the `--verbose` flag by @thewhaleking in https://github.com/opentensor/btcli/pull/717 +* Make `btcli st wizard` by @OxLeOx in https://github.com/opentensor/btcli/pull/720 +* Feat/Root claim update for netuids by @ibraheem-abe in https://github.com/opentensor/btcli/pull/722 +* Improve e2e tests workflow by @basfroman in https://github.com/opentensor/btcli/pull/728 +* Feat: MeV protection in staking operations by @ibraheem-abe in https://github.com/opentensor/btcli/pull/724 +* proxy app by @thewhaleking in https://github.com/opentensor/btcli/pull/706 +* Fixes: updated mev shield by @thewhaleking in https://github.com/opentensor/btcli/pull/731 + +## New Contributors +* @chideraao made their first contribution in https://github.com/opentensor/btcli/pull/707 +* @OxLeOx made their first contribution in https://github.com/opentensor/btcli/pull/720 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.15.3...v9.16.0 + ## 9.15.3 /2025-11-17 ## What's Changed diff --git a/pyproject.toml b/pyproject.toml index d802f19d8..e79d2cdd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "bittensor-cli" -version = "9.15.3" +version = "9.16.0" description = "Bittensor CLI" readme = "README.md" authors = [ From acfa70508c86d6e30e1abf4a2cc1130e257d2c27 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 9 Dec 2025 13:58:02 -0800 Subject: [PATCH 178/179] support case insensitive claim args --- bittensor_cli/cli.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f2ffcfbb2..3af83c735 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -149,6 +149,22 @@ def is_valid_ss58_address_param(address: Optional[str]) -> Optional[str]: return address +def validate_claim_type(value: Optional[str]) -> Optional[claim_stake.ClaimType]: + """ + Validates the claim type argument, allowing case-insensitive input. + """ + if value is None: + return None + + try: + for member in claim_stake.ClaimType: + if member.value.lower() == value.lower(): + return member + return claim_stake.ClaimType(value) + except ValueError: + raise typer.BadParameter(f"'{value}' is not one of 'Keep', 'Swap'.") + + class Options: """ Re-usable typer args @@ -5616,9 +5632,10 @@ def stake_wizard( def stake_set_claim_type( self, claim_type: Annotated[ - Optional[claim_stake.ClaimType], + Optional[str], typer.Argument( help="Claim type: 'Keep' or 'Swap'. If omitted, user will be prompted.", + callback=validate_claim_type, ), ] = None, netuids: Optional[str] = typer.Option( From 2c2965ecfd03f6e6d9d96884d8c56c58ba514552 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 9 Dec 2025 14:13:08 -0800 Subject: [PATCH 179/179] Update to allow executing change extrinsic --- bittensor_cli/src/commands/stake/claim.py | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/src/commands/stake/claim.py b/bittensor_cli/src/commands/stake/claim.py index 2648ad926..396255198 100644 --- a/bittensor_cli/src/commands/stake/claim.py +++ b/bittensor_cli/src/commands/stake/claim.py @@ -150,19 +150,20 @@ async def set_claim_type( new_claim_info = {"type": claim_type} if _claim_types_equal(current_claim_info, new_claim_info): - msg = f"Claim type already set to {_format_claim_type_display(new_claim_info)}. \nNo change needed." - console.print(msg) - if json_output: - json_console.print( - json.dumps( - { - "success": True, - "message": msg, - "extrinsic_identifier": None, - } + if new_claim_info["type"] == "KeepSubnets": + msg = f"Claim type already set to {_format_claim_type_display(new_claim_info)}. \nNo change needed." + console.print(msg) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": msg, + "extrinsic_identifier": None, + } + ) ) - ) - return True, msg, None + return True, msg, None if prompt: console.print(