From fb226f4c524b8eb8419333fc6bcea846271c74ce Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Jul 2025 20:54:57 +0200 Subject: [PATCH 1/2] Adds a new option to `btcli w transfer`: `--allow-death`, which allows death of the account (below the existential deposit) if specified. --- bittensor_cli/cli.py | 9 +++- .../src/bittensor/extrinsics/transfer.py | 52 +++++++++++++------ .../src/bittensor/subtensor_interface.py | 2 + bittensor_cli/src/commands/wallets.py | 2 + 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d751e06ad..3c92fc24f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1889,6 +1889,12 @@ def wallet_transfer( transfer_all: bool = typer.Option( False, "--all", prompt=False, help="Transfer all available balance." ), + allow_death: bool = typer.Option( + False, + "--allow-death", + prompt=False, + help="Transfer balance even if the resulting balance falls below the existential deposit.", + ), period: int = Options.period, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, @@ -1932,7 +1938,7 @@ def wallet_transfer( subtensor = self.initialize_chain(network) if transfer_all and amount: print_error("Cannot specify an amount and '--all' flag.") - raise typer.Exit() + return False elif transfer_all: amount = 0 elif not amount: @@ -1944,6 +1950,7 @@ def wallet_transfer( destination=destination_ss58_address, amount=amount, transfer_all=transfer_all, + allow_death=allow_death, era=period, prompt=prompt, json_output=json_output, diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index a720588bd..6507846e8 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -26,9 +26,9 @@ async def transfer_extrinsic( amount: Balance, era: int = 3, transfer_all: bool = False, + allow_death: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, - keep_alive: bool = True, prompt: bool = False, ) -> bool: """Transfers funds from this wallet to the destination public key address. @@ -39,11 +39,11 @@ async def transfer_extrinsic( :param amount: Amount to stake as Bittensor balance. :param era: Length (in blocks) for which the transaction should be valid. :param transfer_all: Whether to transfer all funds from this wallet to the destination address. + :param allow_death: Whether to allow for falling below the existential deposit when performing this transfer. :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 `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. - :param keep_alive: If set, keeps the account alive by keeping the balance above the existential deposit. :param prompt: If `True`, the call waits for confirmation from the user before proceeding. :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. @@ -57,8 +57,8 @@ async def get_transfer_fee() -> Balance: """ call = await subtensor.substrate.compose_call( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": destination, "value": amount.rao}, + call_function=call_function, + call_params=call_params, ) try: @@ -82,8 +82,8 @@ async def do_transfer() -> tuple[bool, str, str]: """ call = await subtensor.substrate.compose_call( call_module="Balances", - call_function="transfer_keep_alive", - call_params={"dest": destination, "value": amount.rao}, + call_function=call_function, + call_params=call_params, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, era={"period": era} @@ -102,6 +102,7 @@ async def do_transfer() -> tuple[bool, str, str]: block_hash_ = response.block_hash return True, block_hash_, "" else: + print(response) return False, "", format_error_message(await response.error_message) # Validate destination address. @@ -115,6 +116,20 @@ async def do_transfer() -> tuple[bool, str, str]: if not unlock_key(wallet).success: return False + call_params = {"dest": destination} + if transfer_all: + call_function = "transfer_all" + if allow_death: + call_params["keep_alive"] = False + else: + call_params["keep_alive"] = True + else: + call_params["value"] = amount.rao + if allow_death: + call_function = "transfer_allow_death" + else: + call_function = "transfer_keep_alive" + # Check balance. with console.status( f":satellite: Checking balance and fees on chain [white]{subtensor.network}[/white]", @@ -131,23 +146,26 @@ async def do_transfer() -> tuple[bool, str, str]: ) fee = await get_transfer_fee() - if not keep_alive: - # Check if the transfer should keep_alive the account + if allow_death: + # Check if the transfer should keep alive the account existential_deposit = Balance(0) - # Check if we have enough balance. - if transfer_all is True: - amount = account_balance - fee - existential_deposit - if amount < Balance(0): - print_error("Not enough balance to transfer") - return False - - if account_balance < (amount + fee + existential_deposit): + 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]" + 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 + 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 diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 4151910ec..2f117f147 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1457,6 +1457,8 @@ async def subnet( ), self.get_subnet_price(netuid=netuid, block_hash=block_hash), ) + if not result: + raise ValueError(f"Subnet {netuid} not found") subnet_ = DynamicInfo.from_any(result) subnet_.price = price return subnet_ diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 99842ef98..75a6f483e 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1408,6 +1408,7 @@ async def transfer( destination: str, amount: float, transfer_all: bool, + allow_death: bool, era: int, prompt: bool, json_output: bool, @@ -1419,6 +1420,7 @@ async def transfer( destination=destination, amount=Balance.from_tao(amount), transfer_all=transfer_all, + allow_death=allow_death, era=era, prompt=prompt, ) From be1898651e2cdf45924fe6ee0feaed775e63e0bb Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 24 Jul 2025 21:35:28 +0200 Subject: [PATCH 2/2] Removed debug --- bittensor_cli/src/bittensor/extrinsics/transfer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 6507846e8..5302a33d0 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -102,7 +102,6 @@ async def do_transfer() -> tuple[bool, str, str]: block_hash_ = response.block_hash return True, block_hash_, "" else: - print(response) return False, "", format_error_message(await response.error_message) # Validate destination address.