Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 9.9.0 /2025-07-28
* Feat/wallet verify by @ibraheem-abe in https://github.com/opentensor/btcli/pull/561
* Improved speed of query_all_identities and fetch_coldkey_hotkey_identities by @thewhaleking in https://github.com/opentensor/btcli/pull/560
* fix transfer all by @thewhaleking in https://github.com/opentensor/btcli/pull/562
* Add extrinsic fees by @thewhaleking in https://github.com/opentensor/btcli/pull/564

**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.8.7...v9.9.0

## 9.8.7 /2025-07-23
* Fix for handling tuples for `additional` by @thewhaleking in https://github.com/opentensor/btcli/pull/557

Expand Down
65 changes: 64 additions & 1 deletion bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,9 @@ def __init__(self):
self.wallet_app.command(
"sign", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"]
)(self.wallet_sign)
self.wallet_app.command(
"verify", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"]
)(self.wallet_verify)

# stake commands
self.stake_app.command(
Expand Down Expand Up @@ -1889,6 +1892,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,
Expand Down Expand Up @@ -1932,7 +1941,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:
Expand All @@ -1944,6 +1953,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,
Expand Down Expand Up @@ -3091,6 +3101,59 @@ def wallet_sign(

return self._run_command(wallets.sign(wallet, message, use_hotkey, json_output))

def wallet_verify(
self,
message: Optional[str] = typer.Option(
None, "--message", "-m", help="The message that was signed"
),
signature: Optional[str] = typer.Option(
None, "--signature", "-s", help="The signature to verify (hex format)"
),
public_key_or_ss58: Optional[str] = typer.Option(
None,
"--address",
"-a",
"--public-key",
"-p",
help="SS58 address or public key (hex) of the signer",
),
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
):
"""
Verify a message signature using the signer's public key or SS58 address.

This command allows you to verify that a message was signed by the owner of a specific address.

USAGE

Provide the original message, the signature (in hex format), and either the SS58 address
or public key of the signer to verify the signature.

EXAMPLES

[green]$[/green] btcli wallet verify --message "Hello world" --signature "0xabc123..." --address "5GrwvaEF..."

[green]$[/green] btcli wallet verify -m "Test message" -s "0xdef456..." -p "0x1234abcd..."
"""
self.verbosity_handler(quiet, verbose, json_output)

if not public_key_or_ss58:
public_key_or_ss58 = Prompt.ask(
"Enter the [blue]address[/blue] (SS58 or hex format)"
)

if not message:
message = Prompt.ask("Enter the [blue]message[/blue]")

if not signature:
signature = Prompt.ask("Enter the [blue]signature[/blue]")

return self._run_command(
wallets.verify(message, signature, public_key_or_ss58, json_output)
)

def wallet_swap_coldkey(
self,
wallet_name: Optional[str] = Options.wallet_name,
Expand Down
51 changes: 34 additions & 17 deletions bittensor_cli/src/bittensor/extrinsics/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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}
Expand Down Expand Up @@ -115,6 +115,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]",
Expand All @@ -131,23 +145,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

Expand Down
46 changes: 32 additions & 14 deletions bittensor_cli/src/bittensor/subtensor_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from typing import Optional, Any, Union, TypedDict, Iterable

import aiohttp
from async_substrate_interface.utils.storage import StorageKey
from bittensor_wallet import Wallet
from bittensor_wallet.bittensor_wallet import Keypair
from bittensor_wallet.utils import SS58_FORMAT
from scalecodec import GenericCall
from async_substrate_interface.errors import SubstrateRequestException
Expand Down Expand Up @@ -881,9 +883,10 @@ async def query_all_identities(
storage_function="IdentitiesV2",
block_hash=block_hash,
reuse_block_hash=reuse_block,
fully_exhaust=True,
)
all_identities = {}
async for ss58_address, identity in identities:
for ss58_address, identity in identities.records:
all_identities[decode_account_id(ss58_address[0])] = decode_hex_identity(
identity.value
)
Expand Down Expand Up @@ -939,22 +942,22 @@ async def fetch_coldkey_hotkey_identities(
:param reuse_block: Whether to reuse the last-used blockchain block hash.
:return: Dict with 'coldkeys' and 'hotkeys' as keys.
"""

coldkey_identities = await self.query_all_identities()
if block_hash is None:
block_hash = await self.substrate.get_chain_head()
coldkey_identities = await self.query_all_identities(block_hash=block_hash)
identities = {"coldkeys": {}, "hotkeys": {}}
if not coldkey_identities:
return identities
query = await self.substrate.query_multiple( # TODO probably more efficient to do this with query_multi
params=list(coldkey_identities.keys()),
module="SubtensorModule",
storage_function="OwnedHotkeys",
block_hash=block_hash,
reuse_block_hash=reuse_block,
)
sks = [
await self.substrate.create_storage_key(
"SubtensorModule", "OwnedHotkeys", [ck], block_hash=block_hash
)
for ck in coldkey_identities.keys()
]
query = await self.substrate.query_multi(sks, block_hash=block_hash)

for coldkey_ss58, hotkeys in query.items():
storage_key: StorageKey
for storage_key, hotkeys in query:
coldkey_ss58 = storage_key.params[0]
coldkey_identity = coldkey_identities.get(coldkey_ss58)
hotkeys = [decode_account_id(hotkey[0]) for hotkey in hotkeys or []]

identities["coldkeys"][coldkey_ss58] = {
"identity": coldkey_identity,
Expand Down Expand Up @@ -1455,6 +1458,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_
Expand Down Expand Up @@ -1484,6 +1489,19 @@ 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:
"""
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)

Returns:
Balance object representing the fee for this extrinsic.
"""
fee_dict = await self.substrate.get_payment_info(call, keypair)
return Balance.from_rao(fee_dict["partial_fee"])

async def get_stake_fee(
self,
origin_hotkey_ss58: Optional[str],
Expand Down
Loading
Loading