Skip to content

Commit e193146

Browse files
authored
Merge pull request #678 from opentensor/release/9.14.1
Release/9.14.1
2 parents 7610e34 + a02599e commit e193146

File tree

11 files changed

+341
-42
lines changed

11 files changed

+341
-42
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
# Changelog
2+
## 9.14.1 /2025-10-23
3+
* Updates kappa to root sudo only in-line with devnet-ready by @thewhaleking in https://github.com/opentensor/btcli/pull/668
4+
* Adds additional warnings for move vs transfer by @thewhaleking in https://github.com/opentensor/btcli/pull/672
5+
* Childkey take was incorrectly labeled. by @thewhaleking in https://github.com/opentensor/btcli/pull/669
6+
* Updates the help text of crownloan refund by @thewhaleking in https://github.com/opentensor/btcli/pull/674
7+
* Add a warn flag when --netuid 0 is used for btcli hotkey swap by @nstankov-stkd in https://github.com/opentensor/btcli/pull/666
8+
* Add warning and confirmation for `wallet swap_hotkey --netuid 0` to prevent accidental misuse. Using `--netuid 0` only swaps the hotkey on the root network (netuid 0) and does NOT move child hotkey delegation mappings. This is not a full swap across all subnets. Updated documentation and added comprehensive unit tests to clarify proper usage.
9+
* Edge case bug fixes by @thewhaleking in https://github.com/opentensor/btcli/pull/675
10+
* Adds wallet balance sorting by @thewhaleking in https://github.com/opentensor/btcli/pull/676
11+
12+
## New Contributors
13+
* @nstankov-stkd made their first contribution in https://github.com/opentensor/btcli/pull/666
14+
15+
**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.14.0...v9.14.1
16+
217
## 9.14.0 /2025-10-20
318
* Skips senate tests by @thewhaleking in https://github.com/opentensor/btcli/pull/658
419
* Feat/crowdloans by @ibraheem-abe in https://github.com/opentensor/btcli/pull/657

bittensor_cli/cli.py

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
Constants,
4040
COLORS,
4141
HYPERPARAMS,
42+
WalletOptions,
4243
)
4344
from bittensor_cli.src.bittensor import utils
4445
from bittensor_cli.src.bittensor.balances import Balance
@@ -92,6 +93,7 @@
9293
subnets,
9394
mechanisms as subnet_mechanisms,
9495
)
96+
from bittensor_cli.src.commands.wallets import SortByBalance
9597
from bittensor_cli.version import __version__, __version_as_int__
9698

9799
try:
@@ -1302,6 +1304,7 @@ def _run_command(self, cmd: Coroutine, exit_early: bool = True):
13021304

13031305
async def _run():
13041306
initiated = False
1307+
exception_occurred = False
13051308
try:
13061309
if self.subtensor:
13071310
await self.subtensor.substrate.initialize()
@@ -1311,6 +1314,7 @@ async def _run():
13111314
except (ConnectionRefusedError, ssl.SSLError, InvalidHandshake):
13121315
err_console.print(f"Unable to connect to the chain: {self.subtensor}")
13131316
verbose_console.print(traceback.format_exc())
1317+
exception_occurred = True
13141318
except (
13151319
ConnectionClosed,
13161320
SubstrateRequestException,
@@ -1322,22 +1326,25 @@ async def _run():
13221326
elif isinstance(e, RuntimeError):
13231327
pass # Temporarily to handle loop bound issues
13241328
verbose_console.print(traceback.format_exc())
1329+
exception_occurred = True
13251330
except Exception as e:
13261331
err_console.print(f"An unknown error has occurred: {e}")
13271332
verbose_console.print(traceback.format_exc())
1333+
exception_occurred = True
13281334
finally:
13291335
if initiated is False:
13301336
asyncio.create_task(cmd).cancel()
13311337
if (
13321338
exit_early is True
13331339
): # temporarily to handle multiple run commands in one session
1334-
try:
1335-
if self.subtensor:
1340+
if self.subtensor:
1341+
try:
13361342
await self.subtensor.substrate.close()
1343+
except Exception as e: # ensures we always exit cleanly
1344+
if not isinstance(e, (typer.Exit, RuntimeError)):
1345+
err_console.print(f"An unknown error has occurred: {e}")
1346+
if exception_occurred:
13371347
raise typer.Exit()
1338-
except Exception as e: # ensures we always exit cleanly
1339-
if not isinstance(e, (typer.Exit, RuntimeError)):
1340-
err_console.print(f"An unknown error has occurred: {e}")
13411348

13421349
return self.event_loop.run_until_complete(_run())
13431350

@@ -1910,7 +1917,7 @@ def wallet_ask(
19101917
wallet_name: Optional[str],
19111918
wallet_path: Optional[str],
19121919
wallet_hotkey: Optional[str],
1913-
ask_for: Optional[list[Literal[WO.NAME, WO.PATH, WO.HOTKEY]]] = None,
1920+
ask_for: Optional[list[WalletOptions]] = None,
19141921
validate: WV = WV.WALLET,
19151922
return_wallet_and_hotkey: bool = False,
19161923
) -> Union[Wallet, tuple[Wallet, str]]:
@@ -2286,15 +2293,44 @@ def wallet_swap_hotkey(
22862293
22872294
- Make sure that your original key pair (coldkeyA, hotkeyA) is already registered.
22882295
- Make sure that you use a newly created hotkeyB in this command. A hotkeyB that is already registered cannot be used in this command.
2289-
- You can specify the netuid for which you want to swap the hotkey for. If it is not defined, the swap will be initiated for all subnets.
2296+
- If NO netuid is specified, the swap will be initiated for ALL subnets (recommended for most users).
2297+
- If a SPECIFIC netuid is specified (e.g., --netuid 1), the swap will only affect that particular subnet.
2298+
- WARNING: Using --netuid 0 will ONLY swap on the root network (netuid 0), NOT a full swap across all subnets. Use without --netuid for full swap.
22902299
- Finally, note that this command requires a fee of 1 TAO for recycling and this fee is taken from your wallet (coldkeyA).
22912300
22922301
EXAMPLE
22932302
2303+
Full swap across all subnets (recommended):
2304+
[green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey
2305+
2306+
Swap for a specific subnet only:
22942307
[green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey --netuid 1
22952308
"""
22962309
netuid = get_optional_netuid(netuid, all_netuids)
22972310
self.verbosity_handler(quiet, verbose, json_output)
2311+
2312+
# Warning for netuid 0 - only swaps on root network, not a full swap
2313+
if netuid == 0 and prompt:
2314+
console.print(
2315+
"\n[bold yellow]⚠️ WARNING: Using --netuid 0 for swap_hotkey[/bold yellow]\n"
2316+
)
2317+
console.print(
2318+
"[yellow]Specifying --netuid 0 will ONLY swap the hotkey on the root network (netuid 0).[/yellow]\n"
2319+
)
2320+
console.print(
2321+
"[yellow]It will NOT move child hotkey delegation mappings on root.[/yellow]\n"
2322+
)
2323+
console.print(
2324+
f"[bold green]btcli wallet swap_hotkey {destination_hotkey_name or '<destination_hotkey>'} "
2325+
f"--wallet-name {wallet_name or '<wallet_name>'} "
2326+
f"--wallet-hotkey {wallet_hotkey or '<original_hotkey>'}[/bold green]\n"
2327+
)
2328+
2329+
if not Confirm.ask(
2330+
"Are you SURE you want to proceed with --netuid 0 (only root network swap)?",
2331+
default=False,
2332+
):
2333+
return
22982334
original_wallet = self.wallet_ask(
22992335
wallet_name,
23002336
wallet_path,
@@ -3132,6 +3168,11 @@ def wallet_balance(
31323168
"-a",
31333169
help="Whether to display the balances for all the wallets.",
31343170
),
3171+
sort_by: Optional[wallets.SortByBalance] = typer.Option(
3172+
None,
3173+
"--sort",
3174+
help="When using `--all`, sorts the wallets by a given column",
3175+
),
31353176
network: Optional[list[str]] = Options.network,
31363177
quiet: bool = Options.quiet,
31373178
verbose: bool = Options.verbose,
@@ -3231,7 +3272,7 @@ def wallet_balance(
32313272
subtensor = self.initialize_chain(network)
32323273
return self._run_command(
32333274
wallets.wallet_balance(
3234-
wallet, subtensor, all_balances, ss58_addresses, json_output
3275+
wallet, subtensor, all_balances, ss58_addresses, sort_by, json_output
32353276
)
32363277
)
32373278

@@ -4573,9 +4614,17 @@ def stake_move(
45734614
[green]$[/green] btcli stake move
45744615
"""
45754616
self.verbosity_handler(quiet, verbose, json_output)
4576-
console.print(
4577-
"[dim]This command moves stake from one hotkey to another hotkey while keeping the same coldkey.[/dim]"
4578-
)
4617+
if prompt:
4618+
if not Confirm.ask(
4619+
"This transaction will [bold]move stake[/bold] to another hotkey while keeping the same "
4620+
"coldkey ownership. Do you wish to continue? ",
4621+
default=False,
4622+
):
4623+
raise typer.Exit()
4624+
else:
4625+
console.print(
4626+
"[dim]This command moves stake from one hotkey to another hotkey while keeping the same coldkey.[/dim]"
4627+
)
45794628
if not destination_hotkey:
45804629
dest_wallet_or_ss58 = Prompt.ask(
45814630
"Enter the [blue]destination wallet[/blue] where destination hotkey is located or "
@@ -4770,9 +4819,18 @@ def stake_transfer(
47704819
[green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2
47714820
"""
47724821
self.verbosity_handler(quiet, verbose, json_output)
4773-
console.print(
4774-
"[dim]This command transfers stake from one coldkey to another while keeping the same hotkey.[/dim]"
4775-
)
4822+
if prompt:
4823+
if not Confirm.ask(
4824+
"This transaction will [bold]transfer ownership[/bold] from one coldkey to another, in subnets "
4825+
"which have enabled it. You should ensure that the destination coldkey is "
4826+
"[bold]not a validator hotkey[/bold] before continuing. Do you wish to continue?",
4827+
default=False,
4828+
):
4829+
raise typer.Exit()
4830+
else:
4831+
console.print(
4832+
"[dim]This command transfers stake from one coldkey to another while keeping the same hotkey.[/dim]"
4833+
)
47764834

47774835
if not dest_ss58:
47784836
dest_ss58 = Prompt.ask(
@@ -5229,7 +5287,7 @@ def stake_childkey_take(
52295287
network: Optional[list[str]] = Options.network,
52305288
child_hotkey_ss58: Optional[str] = typer.Option(
52315289
None,
5232-
"child-hotkey-ss58",
5290+
"--child-hotkey-ss58",
52335291
help="The hotkey SS58 to designate as child (not specifying will use the provided wallet's hotkey)",
52345292
prompt=False,
52355293
),
@@ -5306,7 +5364,7 @@ def stake_childkey_take(
53065364
subtensor=self.initialize_chain(network),
53075365
netuid=netuid,
53085366
take=take,
5309-
hotkey=hotkey,
5367+
hotkey=child_hotkey_ss58,
53105368
wait_for_inclusion=wait_for_inclusion,
53115369
wait_for_finalization=wait_for_finalization,
53125370
prompt=prompt,
@@ -7764,10 +7822,10 @@ def crowd_refund(
77647822
"""
77657823
Refund contributors of a non-finalized crowdloan.
77667824
7767-
Any account may call this once the crowdloan is no longer wanted. Each call
7768-
refunds up to the on-chain `RefundContributorsLimit` contributors (currently
7769-
50) excluding the creator. Run it repeatedly until everyone except the creator
7770-
has been reimbursed.
7825+
Only the creator may call this. Each call refunds up to the on-chain `RefundContributorsLimit` contributors
7826+
(currently 50) excluding the creator. Run it repeatedly until everyone except the creator has been reimbursed.
7827+
7828+
Contributors can call `btcli crowdloan withdraw` at will.
77717829
"""
77727830
self.verbosity_handler(quiet, verbose, json_output)
77737831

bittensor_cli/src/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ class RootSudoOnly(Enum):
635635
HYPERPARAMS = {
636636
# btcli name: (subtensor method, root-only enum)
637637
"rho": ("sudo_set_rho", RootSudoOnly.FALSE),
638-
"kappa": ("sudo_set_kappa", RootSudoOnly.FALSE),
638+
"kappa": ("sudo_set_kappa", RootSudoOnly.TRUE),
639639
"immunity_period": ("sudo_set_immunity_period", RootSudoOnly.FALSE),
640640
"min_allowed_weights": ("sudo_set_min_allowed_weights", RootSudoOnly.FALSE),
641641
"max_weights_limit": ("sudo_set_max_weight_limit", RootSudoOnly.FALSE),

bittensor_cli/src/bittensor/extrinsics/transfer.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,6 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]:
116116
)
117117
return False, None
118118
console.print(f"[dark_orange]Initiating transfer on network: {subtensor.network}")
119-
# Unlock wallet coldkey.
120-
if not unlock_key(wallet).success:
121-
return False, None
122119

123120
call_params: dict[str, Optional[Union[str, int]]] = {"dest": destination}
124121
if transfer_all:
@@ -175,18 +172,31 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]:
175172

176173
# Ask before moving on.
177174
if prompt:
175+
hk_owner = await subtensor.get_hotkey_owner(destination, check_exists=False)
176+
if hk_owner and hk_owner != destination:
177+
if not Confirm.ask(
178+
f"The destination appears to be a hotkey, owned by [bright_magenta]{hk_owner}[/bright_magenta]. "
179+
f"Only proceed if you are absolutely sure that [bright_magenta]{destination}[/bright_magenta] is the "
180+
f"correct destination.",
181+
default=False,
182+
):
183+
return False, None
178184
if not Confirm.ask(
179185
"Do you want to transfer:[bold white]\n"
180186
f" amount: [bright_cyan]{amount if not transfer_all else account_balance}[/bright_cyan]\n"
181187
f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : "
182-
f"[bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]"
188+
f"[bright_magenta]{wallet.coldkeypub.ss58_address}\n[/bright_magenta]"
183189
f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]\n"
184190
f"[bright_yellow]Transferring is not the same as staking. To instead stake, use "
185191
f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow].\n"
186192
f"Proceed with transfer?"
187193
):
188194
return False, None
189195

196+
# Unlock wallet coldkey.
197+
if not unlock_key(wallet).success:
198+
return False, None
199+
190200
with console.status(":satellite: Transferring...", spinner="earth"):
191201
success, block_hash, err_msg, ext_receipt = await do_transfer()
192202

bittensor_cli/src/bittensor/subtensor_interface.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,17 +1123,23 @@ async def get_hotkey_owner(
11231123
self,
11241124
hotkey_ss58: str,
11251125
block_hash: Optional[str] = None,
1126+
check_exists: bool = True,
11261127
) -> Optional[str]:
11271128
val = await self.query(
11281129
module="SubtensorModule",
11291130
storage_function="Owner",
11301131
params=[hotkey_ss58],
11311132
block_hash=block_hash,
11321133
)
1133-
if val:
1134-
exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash)
1134+
if check_exists:
1135+
if val:
1136+
exists = await self.does_hotkey_exist(
1137+
hotkey_ss58, block_hash=block_hash
1138+
)
1139+
else:
1140+
exists = False
11351141
else:
1136-
exists = False
1142+
exists = True
11371143
hotkey_owner = val if exists else None
11381144
return hotkey_owner
11391145

bittensor_cli/src/bittensor/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1496,7 +1496,7 @@ async def print_extrinsic_id(
14961496
Args:
14971497
extrinsic_receipt: AsyncExtrinsicReceipt object from a successful extrinsic submission.
14981498
"""
1499-
if extrinsic_receipt is None:
1499+
if extrinsic_receipt is None or not (await extrinsic_receipt.is_success):
15001500
return
15011501
substrate = extrinsic_receipt.substrate
15021502
ext_id = await extrinsic_receipt.get_extrinsic_identifier()

bittensor_cli/src/commands/stake/move.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,6 @@ async def move_stake(
566566
response = await subtensor.substrate.submit_extrinsic(
567567
extrinsic, wait_for_inclusion=True, wait_for_finalization=False
568568
)
569-
await print_extrinsic_id(response)
570569
ext_id = await response.get_extrinsic_identifier()
571570

572571
if not prompt:
@@ -580,6 +579,7 @@ async def move_stake(
580579
)
581580
return False, ""
582581
else:
582+
await print_extrinsic_id(response)
583583
console.print(
584584
":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]"
585585
)
@@ -755,7 +755,6 @@ async def transfer_stake(
755755
extrinsic, wait_for_inclusion=True, wait_for_finalization=False
756756
)
757757
ext_id = await response.get_extrinsic_identifier()
758-
await print_extrinsic_id(extrinsic)
759758

760759
if not prompt:
761760
console.print(":white_heavy_check_mark: [green]Sent[/green]")
@@ -767,7 +766,7 @@ async def transfer_stake(
767766
f"{format_error_message(await response.error_message)}"
768767
)
769768
return False, ""
770-
769+
await print_extrinsic_id(extrinsic)
771770
# Get and display new stake balances
772771
new_stake, new_dest_stake = await asyncio.gather(
773772
subtensor.get_stake(
@@ -933,7 +932,6 @@ async def swap_stake(
933932
wait_for_finalization=wait_for_finalization,
934933
)
935934
ext_id = await response.get_extrinsic_identifier()
936-
await print_extrinsic_id(response)
937935

938936
if not prompt:
939937
console.print(":white_heavy_check_mark: [green]Sent[/green]")
@@ -945,7 +943,7 @@ async def swap_stake(
945943
f"{format_error_message(await response.error_message)}"
946944
)
947945
return False, ""
948-
946+
await print_extrinsic_id(response)
949947
# Get and display new stake balances
950948
new_stake, new_dest_stake = await asyncio.gather(
951949
subtensor.get_stake(

0 commit comments

Comments
 (0)