From 069883e1a2f26a488949865d9a078cff4f54c187 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 6 Nov 2025 17:05:56 +0200 Subject: [PATCH 01/88] 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 02/88] 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 03/88] 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 04/88] 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 05/88] 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 06/88] 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 07/88] 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 08/88] 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 09/88] 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 10/88] 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 11/88] 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 12/88] 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 13/88] 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 14/88] 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 15/88] 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 16/88] 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 17/88] 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 18/88] 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 19/88] 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 20/88] 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 21/88] 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 22/88] 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 23/88] 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 24/88] 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 25/88] 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 26/88] 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 27/88] 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 28/88] 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 29/88] 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 30/88] 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 31/88] 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 32/88] 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 33/88] 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 34/88] 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 35/88] 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 36/88] 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 37/88] 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 38/88] 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 39/88] 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 40/88] 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 41/88] 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 42/88] 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 43/88] 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 44/88] 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 45/88] 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 46/88] 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 47/88] 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 48/88] 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 49/88] 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 50/88] 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 51/88] 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 52/88] 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 53/88] 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 54/88] 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 55/88] 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 56/88] 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 57/88] 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 58/88] 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 49a0fb8d5bf8dac0d77416eb2e0bf31c7d873324 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 19 Nov 2025 13:30:44 +0200 Subject: [PATCH 59/88] 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 60/88] 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 61/88] 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 62/88] 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 63/88] 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 64/88] 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 65/88] 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 66/88] 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 67/88] 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 68/88] 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 69/88] 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 70/88] 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 71/88] 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 4491fe08c98abaa66793425f448609f4f66222da Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 25 Nov 2025 13:40:34 +0200 Subject: [PATCH 72/88] 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 73/88] 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 74/88] 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 75/88] 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 f92750e80fab8e6129848e57b686b2195f8e7610 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 26 Nov 2025 13:08:32 +0200 Subject: [PATCH 76/88] 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 236bd3661f47c866edd9f55c3babd62578fa9486 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 27 Nov 2025 09:26:01 +0200 Subject: [PATCH 77/88] 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 78/88] 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 ef1225761417b2ac5a2c73cc21de51879252edeb Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 28 Nov 2025 10:44:45 +0200 Subject: [PATCH 79/88] 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 80/88] 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 81/88] 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 82/88] 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 26473d23bbe5e6aa387fabf28f930a5f94ce7e32 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 2 Dec 2025 15:07:10 +0200 Subject: [PATCH 83/88] 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 84/88] 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 d4196beb3d4a33b823e433257f3cb6a3cd8da586 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 3 Dec 2025 07:51:10 +0200 Subject: [PATCH 85/88] 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 86/88] 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 3e8c1eb6de174acf3abc8d099d925a4e1706a372 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 5 Dec 2025 10:21:57 +0200 Subject: [PATCH 87/88] 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 88/88] 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: