Skip to content

Commit 31b77f1

Browse files
authored
Merge pull request #566 from opentensor/release/9.9.0
Release/9.9.0
2 parents 0e7f549 + 5d9cb4e commit 31b77f1

File tree

9 files changed

+528
-152
lines changed

9 files changed

+528
-152
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 9.9.0 /2025-07-28
4+
* Feat/wallet verify by @ibraheem-abe in https://github.com/opentensor/btcli/pull/561
5+
* Improved speed of query_all_identities and fetch_coldkey_hotkey_identities by @thewhaleking in https://github.com/opentensor/btcli/pull/560
6+
* fix transfer all by @thewhaleking in https://github.com/opentensor/btcli/pull/562
7+
* Add extrinsic fees by @thewhaleking in https://github.com/opentensor/btcli/pull/564
8+
9+
**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.8.7...v9.9.0
10+
311
## 9.8.7 /2025-07-23
412
* Fix for handling tuples for `additional` by @thewhaleking in https://github.com/opentensor/btcli/pull/557
513

bittensor_cli/cli.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,9 @@ def __init__(self):
817817
self.wallet_app.command(
818818
"sign", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"]
819819
)(self.wallet_sign)
820+
self.wallet_app.command(
821+
"verify", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"]
822+
)(self.wallet_verify)
820823

821824
# stake commands
822825
self.stake_app.command(
@@ -1889,6 +1892,12 @@ def wallet_transfer(
18891892
transfer_all: bool = typer.Option(
18901893
False, "--all", prompt=False, help="Transfer all available balance."
18911894
),
1895+
allow_death: bool = typer.Option(
1896+
False,
1897+
"--allow-death",
1898+
prompt=False,
1899+
help="Transfer balance even if the resulting balance falls below the existential deposit.",
1900+
),
18921901
period: int = Options.period,
18931902
wallet_name: str = Options.wallet_name,
18941903
wallet_path: str = Options.wallet_path,
@@ -1932,7 +1941,7 @@ def wallet_transfer(
19321941
subtensor = self.initialize_chain(network)
19331942
if transfer_all and amount:
19341943
print_error("Cannot specify an amount and '--all' flag.")
1935-
raise typer.Exit()
1944+
return False
19361945
elif transfer_all:
19371946
amount = 0
19381947
elif not amount:
@@ -1944,6 +1953,7 @@ def wallet_transfer(
19441953
destination=destination_ss58_address,
19451954
amount=amount,
19461955
transfer_all=transfer_all,
1956+
allow_death=allow_death,
19471957
era=period,
19481958
prompt=prompt,
19491959
json_output=json_output,
@@ -3091,6 +3101,59 @@ def wallet_sign(
30913101

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

3104+
def wallet_verify(
3105+
self,
3106+
message: Optional[str] = typer.Option(
3107+
None, "--message", "-m", help="The message that was signed"
3108+
),
3109+
signature: Optional[str] = typer.Option(
3110+
None, "--signature", "-s", help="The signature to verify (hex format)"
3111+
),
3112+
public_key_or_ss58: Optional[str] = typer.Option(
3113+
None,
3114+
"--address",
3115+
"-a",
3116+
"--public-key",
3117+
"-p",
3118+
help="SS58 address or public key (hex) of the signer",
3119+
),
3120+
quiet: bool = Options.quiet,
3121+
verbose: bool = Options.verbose,
3122+
json_output: bool = Options.json_output,
3123+
):
3124+
"""
3125+
Verify a message signature using the signer's public key or SS58 address.
3126+
3127+
This command allows you to verify that a message was signed by the owner of a specific address.
3128+
3129+
USAGE
3130+
3131+
Provide the original message, the signature (in hex format), and either the SS58 address
3132+
or public key of the signer to verify the signature.
3133+
3134+
EXAMPLES
3135+
3136+
[green]$[/green] btcli wallet verify --message "Hello world" --signature "0xabc123..." --address "5GrwvaEF..."
3137+
3138+
[green]$[/green] btcli wallet verify -m "Test message" -s "0xdef456..." -p "0x1234abcd..."
3139+
"""
3140+
self.verbosity_handler(quiet, verbose, json_output)
3141+
3142+
if not public_key_or_ss58:
3143+
public_key_or_ss58 = Prompt.ask(
3144+
"Enter the [blue]address[/blue] (SS58 or hex format)"
3145+
)
3146+
3147+
if not message:
3148+
message = Prompt.ask("Enter the [blue]message[/blue]")
3149+
3150+
if not signature:
3151+
signature = Prompt.ask("Enter the [blue]signature[/blue]")
3152+
3153+
return self._run_command(
3154+
wallets.verify(message, signature, public_key_or_ss58, json_output)
3155+
)
3156+
30943157
def wallet_swap_coldkey(
30953158
self,
30963159
wallet_name: Optional[str] = Options.wallet_name,

bittensor_cli/src/bittensor/extrinsics/transfer.py

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ async def transfer_extrinsic(
2626
amount: Balance,
2727
era: int = 3,
2828
transfer_all: bool = False,
29+
allow_death: bool = False,
2930
wait_for_inclusion: bool = True,
3031
wait_for_finalization: bool = False,
31-
keep_alive: bool = True,
3232
prompt: bool = False,
3333
) -> bool:
3434
"""Transfers funds from this wallet to the destination public key address.
@@ -39,11 +39,11 @@ async def transfer_extrinsic(
3939
:param amount: Amount to stake as Bittensor balance.
4040
:param era: Length (in blocks) for which the transaction should be valid.
4141
:param transfer_all: Whether to transfer all funds from this wallet to the destination address.
42+
:param allow_death: Whether to allow for falling below the existential deposit when performing this transfer.
4243
:param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`,
4344
or returns `False` if the extrinsic fails to enter the block within the timeout.
4445
:param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning
4546
`True`, or returns `False` if the extrinsic fails to be finalized within the timeout.
46-
:param keep_alive: If set, keeps the account alive by keeping the balance above the existential deposit.
4747
:param prompt: If `True`, the call waits for confirmation from the user before proceeding.
4848
:return: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for
4949
finalization / inclusion, the response is `True`, regardless of its inclusion.
@@ -57,8 +57,8 @@ async def get_transfer_fee() -> Balance:
5757
"""
5858
call = await subtensor.substrate.compose_call(
5959
call_module="Balances",
60-
call_function="transfer_keep_alive",
61-
call_params={"dest": destination, "value": amount.rao},
60+
call_function=call_function,
61+
call_params=call_params,
6262
)
6363

6464
try:
@@ -82,8 +82,8 @@ async def do_transfer() -> tuple[bool, str, str]:
8282
"""
8383
call = await subtensor.substrate.compose_call(
8484
call_module="Balances",
85-
call_function="transfer_keep_alive",
86-
call_params={"dest": destination, "value": amount.rao},
85+
call_function=call_function,
86+
call_params=call_params,
8787
)
8888
extrinsic = await subtensor.substrate.create_signed_extrinsic(
8989
call=call, keypair=wallet.coldkey, era={"period": era}
@@ -115,6 +115,20 @@ async def do_transfer() -> tuple[bool, str, str]:
115115
if not unlock_key(wallet).success:
116116
return False
117117

118+
call_params = {"dest": destination}
119+
if transfer_all:
120+
call_function = "transfer_all"
121+
if allow_death:
122+
call_params["keep_alive"] = False
123+
else:
124+
call_params["keep_alive"] = True
125+
else:
126+
call_params["value"] = amount.rao
127+
if allow_death:
128+
call_function = "transfer_allow_death"
129+
else:
130+
call_function = "transfer_keep_alive"
131+
118132
# Check balance.
119133
with console.status(
120134
f":satellite: Checking balance and fees on chain [white]{subtensor.network}[/white]",
@@ -131,23 +145,26 @@ async def do_transfer() -> tuple[bool, str, str]:
131145
)
132146
fee = await get_transfer_fee()
133147

134-
if not keep_alive:
135-
# Check if the transfer should keep_alive the account
148+
if allow_death:
149+
# Check if the transfer should keep alive the account
136150
existential_deposit = Balance(0)
137151

138-
# Check if we have enough balance.
139-
if transfer_all is True:
140-
amount = account_balance - fee - existential_deposit
141-
if amount < Balance(0):
142-
print_error("Not enough balance to transfer")
143-
return False
144-
145-
if account_balance < (amount + fee + existential_deposit):
152+
if account_balance < (amount + fee + existential_deposit) and not allow_death:
146153
err_console.print(
147154
":cross_mark: [bold red]Not enough balance[/bold red]:\n\n"
148155
f" balance: [bright_cyan]{account_balance}[/bright_cyan]\n"
149156
f" amount: [bright_cyan]{amount}[/bright_cyan]\n"
150-
f" for fee: [bright_cyan]{fee}[/bright_cyan]"
157+
f" for fee: [bright_cyan]{fee}[/bright_cyan]\n"
158+
f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n"
159+
f"You can try again with `--allow-death`."
160+
)
161+
return False
162+
elif account_balance < (amount + fee) and allow_death:
163+
print_error(
164+
":cross_mark: [bold red]Not enough balance[/bold red]:\n\n"
165+
f" balance: [bright_red]{account_balance}[/bright_red]\n"
166+
f" amount: [bright_red]{amount}[/bright_red]\n"
167+
f" for fee: [bright_red]{fee}[/bright_red]"
151168
)
152169
return False
153170

bittensor_cli/src/bittensor/subtensor_interface.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from typing import Optional, Any, Union, TypedDict, Iterable
44

55
import aiohttp
6+
from async_substrate_interface.utils.storage import StorageKey
67
from bittensor_wallet import Wallet
8+
from bittensor_wallet.bittensor_wallet import Keypair
79
from bittensor_wallet.utils import SS58_FORMAT
810
from scalecodec import GenericCall
911
from async_substrate_interface.errors import SubstrateRequestException
@@ -881,9 +883,10 @@ async def query_all_identities(
881883
storage_function="IdentitiesV2",
882884
block_hash=block_hash,
883885
reuse_block_hash=reuse_block,
886+
fully_exhaust=True,
884887
)
885888
all_identities = {}
886-
async for ss58_address, identity in identities:
889+
for ss58_address, identity in identities.records:
887890
all_identities[decode_account_id(ss58_address[0])] = decode_hex_identity(
888891
identity.value
889892
)
@@ -939,22 +942,22 @@ async def fetch_coldkey_hotkey_identities(
939942
:param reuse_block: Whether to reuse the last-used blockchain block hash.
940943
:return: Dict with 'coldkeys' and 'hotkeys' as keys.
941944
"""
942-
943-
coldkey_identities = await self.query_all_identities()
945+
if block_hash is None:
946+
block_hash = await self.substrate.get_chain_head()
947+
coldkey_identities = await self.query_all_identities(block_hash=block_hash)
944948
identities = {"coldkeys": {}, "hotkeys": {}}
945-
if not coldkey_identities:
946-
return identities
947-
query = await self.substrate.query_multiple( # TODO probably more efficient to do this with query_multi
948-
params=list(coldkey_identities.keys()),
949-
module="SubtensorModule",
950-
storage_function="OwnedHotkeys",
951-
block_hash=block_hash,
952-
reuse_block_hash=reuse_block,
953-
)
949+
sks = [
950+
await self.substrate.create_storage_key(
951+
"SubtensorModule", "OwnedHotkeys", [ck], block_hash=block_hash
952+
)
953+
for ck in coldkey_identities.keys()
954+
]
955+
query = await self.substrate.query_multi(sks, block_hash=block_hash)
954956

955-
for coldkey_ss58, hotkeys in query.items():
957+
storage_key: StorageKey
958+
for storage_key, hotkeys in query:
959+
coldkey_ss58 = storage_key.params[0]
956960
coldkey_identity = coldkey_identities.get(coldkey_ss58)
957-
hotkeys = [decode_account_id(hotkey[0]) for hotkey in hotkeys or []]
958961

959962
identities["coldkeys"][coldkey_ss58] = {
960963
"identity": coldkey_identity,
@@ -1455,6 +1458,8 @@ async def subnet(
14551458
),
14561459
self.get_subnet_price(netuid=netuid, block_hash=block_hash),
14571460
)
1461+
if not result:
1462+
raise ValueError(f"Subnet {netuid} not found")
14581463
subnet_ = DynamicInfo.from_any(result)
14591464
subnet_.price = price
14601465
return subnet_
@@ -1484,6 +1489,19 @@ async def get_owned_hotkeys(
14841489

14851490
return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []]
14861491

1492+
async def get_extrinsic_fee(self, call: GenericCall, keypair: Keypair) -> Balance:
1493+
"""
1494+
Determines the fee for the extrinsic call.
1495+
Args:
1496+
call: Created extrinsic call
1497+
keypair: The keypair that would sign the extrinsic (usually you would just want to use the *pub for this)
1498+
1499+
Returns:
1500+
Balance object representing the fee for this extrinsic.
1501+
"""
1502+
fee_dict = await self.substrate.get_payment_info(call, keypair)
1503+
return Balance.from_rao(fee_dict["partial_fee"])
1504+
14871505
async def get_stake_fee(
14881506
self,
14891507
origin_hotkey_ss58: Optional[str],

0 commit comments

Comments
 (0)