Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1215d33
feature/fix-btcli-subnet-0-swap
nstankov-stkd Oct 20, 2025
55fc689
Merge branch 'opentensor:main' into main
nstankov-stkd Oct 20, 2025
adbb410
feature/fix-btcli-subnet-0-swap
nstankov-stkd Oct 20, 2025
e449831
ruff
nstankov-stkd Oct 20, 2025
51c3738
rm pr.md
nstankov-stkd Oct 20, 2025
6cd5f25
Updates kappa to root sudo only in-line with devnet-ready
thewhaleking Oct 21, 2025
43a5fc4
Merge pull request #668 from opentensor/chore/thewhaleking/update-kap…
thewhaleking Oct 21, 2025
97cfcc3
Childkey take was incorrectly labeled.
thewhaleking Oct 22, 2025
26b0e8a
Adds additional warnings for move vs transfer
thewhaleking Oct 23, 2025
c239c00
Checks if hotkey has owner and provides confirmation
thewhaleking Oct 23, 2025
ab41c23
Moves the unlock wallet fn after the confirmations/balance check
thewhaleking Oct 23, 2025
c912d6e
Merge pull request #672 from opentensor/chore/thewhaleking/additional…
thewhaleking Oct 23, 2025
3059692
Removes from changelog
thewhaleking Oct 23, 2025
9051a10
Merge pull request #669 from opentensor/fix/thewhaleking/broken-child…
thewhaleking Oct 23, 2025
3bfd513
Updates the help text of crownloan refund
thewhaleking Oct 23, 2025
1d12cc1
Ensures we exit gracefully if there's an error in connection.
thewhaleking Oct 23, 2025
39653e1
Ensure we don't print the extrinsic success message if the extrinsic …
thewhaleking Oct 23, 2025
814a050
Merge pull request #674 from opentensor/fix/thewhaleking/update-crowd…
thewhaleking Oct 23, 2025
1d08eb8
Merge pull request #673 from opentensor/nstankov-stkd/main
thewhaleking Oct 23, 2025
fceada9
Only raise typer.Exit on exception occurring
thewhaleking Oct 23, 2025
332aca0
Merge branch 'staging' into fix/thewhaleking/small-bug-fixes
thewhaleking Oct 23, 2025
b82e935
Merge pull request #675 from opentensor/fix/thewhaleking/small-bug-fixes
thewhaleking Oct 23, 2025
3be3dda
Adds wallet balance sorting
thewhaleking Oct 23, 2025
fa10630
Merge pull request #676 from opentensor/feat/thewhaleking/wallet-bala…
thewhaleking Oct 23, 2025
a12639b
changelog + version
thewhaleking Oct 23, 2025
a02599e
Merge pull request #677 from opentensor/changelog/9.14.1
thewhaleking Oct 23, 2025
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
# Changelog
## 9.14.1 /2025-10-23
* Updates kappa to root sudo only in-line with devnet-ready by @thewhaleking in https://github.com/opentensor/btcli/pull/668
* Adds additional warnings for move vs transfer by @thewhaleking in https://github.com/opentensor/btcli/pull/672
* Childkey take was incorrectly labeled. by @thewhaleking in https://github.com/opentensor/btcli/pull/669
* Updates the help text of crownloan refund by @thewhaleking in https://github.com/opentensor/btcli/pull/674
* Add a warn flag when --netuid 0 is used for btcli hotkey swap by @nstankov-stkd in https://github.com/opentensor/btcli/pull/666
* 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.
* Edge case bug fixes by @thewhaleking in https://github.com/opentensor/btcli/pull/675
* Adds wallet balance sorting by @thewhaleking in https://github.com/opentensor/btcli/pull/676

## New Contributors
* @nstankov-stkd made their first contribution in https://github.com/opentensor/btcli/pull/666

**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.14.0...v9.14.1

## 9.14.0 /2025-10-20
* Skips senate tests by @thewhaleking in https://github.com/opentensor/btcli/pull/658
* Feat/crowdloans by @ibraheem-abe in https://github.com/opentensor/btcli/pull/657
Expand Down
98 changes: 78 additions & 20 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
Constants,
COLORS,
HYPERPARAMS,
WalletOptions,
)
from bittensor_cli.src.bittensor import utils
from bittensor_cli.src.bittensor.balances import Balance
Expand Down Expand Up @@ -92,6 +93,7 @@
subnets,
mechanisms as subnet_mechanisms,
)
from bittensor_cli.src.commands.wallets import SortByBalance
from bittensor_cli.version import __version__, __version_as_int__

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

async def _run():
initiated = False
exception_occurred = False
try:
if self.subtensor:
await self.subtensor.substrate.initialize()
Expand All @@ -1311,6 +1314,7 @@ async def _run():
except (ConnectionRefusedError, ssl.SSLError, InvalidHandshake):
err_console.print(f"Unable to connect to the chain: {self.subtensor}")
verbose_console.print(traceback.format_exc())
exception_occurred = True
except (
ConnectionClosed,
SubstrateRequestException,
Expand All @@ -1322,22 +1326,25 @@ async def _run():
elif isinstance(e, RuntimeError):
pass # Temporarily to handle loop bound issues
verbose_console.print(traceback.format_exc())
exception_occurred = True
except Exception as e:
err_console.print(f"An unknown error has occurred: {e}")
verbose_console.print(traceback.format_exc())
exception_occurred = True
finally:
if initiated is False:
asyncio.create_task(cmd).cancel()
if (
exit_early is True
): # temporarily to handle multiple run commands in one session
try:
if self.subtensor:
if self.subtensor:
try:
await self.subtensor.substrate.close()
except Exception as e: # ensures we always exit cleanly
if not isinstance(e, (typer.Exit, RuntimeError)):
err_console.print(f"An unknown error has occurred: {e}")
if exception_occurred:
raise typer.Exit()
except Exception as e: # ensures we always exit cleanly
if not isinstance(e, (typer.Exit, RuntimeError)):
err_console.print(f"An unknown error has occurred: {e}")

return self.event_loop.run_until_complete(_run())

Expand Down Expand Up @@ -1910,7 +1917,7 @@ def wallet_ask(
wallet_name: Optional[str],
wallet_path: Optional[str],
wallet_hotkey: Optional[str],
ask_for: Optional[list[Literal[WO.NAME, WO.PATH, WO.HOTKEY]]] = None,
ask_for: Optional[list[WalletOptions]] = None,
validate: WV = WV.WALLET,
return_wallet_and_hotkey: bool = False,
) -> Union[Wallet, tuple[Wallet, str]]:
Expand Down Expand Up @@ -2286,15 +2293,44 @@ def wallet_swap_hotkey(

- Make sure that your original key pair (coldkeyA, hotkeyA) is already registered.
- Make sure that you use a newly created hotkeyB in this command. A hotkeyB that is already registered cannot be used in this command.
- 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.
- If NO netuid is specified, the swap will be initiated for ALL subnets (recommended for most users).
- If a SPECIFIC netuid is specified (e.g., --netuid 1), the swap will only affect that particular subnet.
- 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.
- Finally, note that this command requires a fee of 1 TAO for recycling and this fee is taken from your wallet (coldkeyA).

EXAMPLE

Full swap across all subnets (recommended):
[green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey

Swap for a specific subnet only:
[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)
self.verbosity_handler(quiet, verbose, json_output)

# Warning for netuid 0 - only swaps on root network, not a full swap
if netuid == 0 and prompt:
console.print(
"\n[bold yellow]⚠️ WARNING: Using --netuid 0 for swap_hotkey[/bold yellow]\n"
)
console.print(
"[yellow]Specifying --netuid 0 will ONLY swap the hotkey on the root network (netuid 0).[/yellow]\n"
)
console.print(
"[yellow]It will NOT move child hotkey delegation mappings on root.[/yellow]\n"
)
console.print(
f"[bold green]btcli wallet swap_hotkey {destination_hotkey_name or '<destination_hotkey>'} "
f"--wallet-name {wallet_name or '<wallet_name>'} "
f"--wallet-hotkey {wallet_hotkey or '<original_hotkey>'}[/bold green]\n"
)

if not Confirm.ask(
"Are you SURE you want to proceed with --netuid 0 (only root network swap)?",
default=False,
):
return
original_wallet = self.wallet_ask(
wallet_name,
wallet_path,
Expand Down Expand Up @@ -3132,6 +3168,11 @@ def wallet_balance(
"-a",
help="Whether to display the balances for all the wallets.",
),
sort_by: Optional[wallets.SortByBalance] = typer.Option(
None,
"--sort",
help="When using `--all`, sorts the wallets by a given column",
),
network: Optional[list[str]] = Options.network,
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
Expand Down Expand Up @@ -3231,7 +3272,7 @@ def wallet_balance(
subtensor = self.initialize_chain(network)
return self._run_command(
wallets.wallet_balance(
wallet, subtensor, all_balances, ss58_addresses, json_output
wallet, subtensor, all_balances, ss58_addresses, sort_by, json_output
)
)

Expand Down Expand Up @@ -4573,9 +4614,17 @@ def stake_move(
[green]$[/green] btcli stake move
"""
self.verbosity_handler(quiet, verbose, json_output)
console.print(
"[dim]This command moves stake from one hotkey to another hotkey while keeping the same coldkey.[/dim]"
)
if prompt:
if not Confirm.ask(
"This transaction will [bold]move stake[/bold] to another hotkey while keeping the same "
"coldkey ownership. Do you wish to continue? ",
default=False,
):
raise typer.Exit()
else:
console.print(
"[dim]This command moves stake from one hotkey to another hotkey while keeping the same coldkey.[/dim]"
)
if not destination_hotkey:
dest_wallet_or_ss58 = Prompt.ask(
"Enter the [blue]destination wallet[/blue] where destination hotkey is located or "
Expand Down Expand Up @@ -4770,9 +4819,18 @@ def stake_transfer(
[green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2
"""
self.verbosity_handler(quiet, verbose, json_output)
console.print(
"[dim]This command transfers stake from one coldkey to another while keeping the same hotkey.[/dim]"
)
if prompt:
if not Confirm.ask(
"This transaction will [bold]transfer ownership[/bold] from one coldkey to another, in subnets "
"which have enabled it. You should ensure that the destination coldkey is "
"[bold]not a validator hotkey[/bold] before continuing. Do you wish to continue?",
default=False,
):
raise typer.Exit()
else:
console.print(
"[dim]This command transfers stake from one coldkey to another while keeping the same hotkey.[/dim]"
)

if not dest_ss58:
dest_ss58 = Prompt.ask(
Expand Down Expand Up @@ -5229,7 +5287,7 @@ def stake_childkey_take(
network: Optional[list[str]] = Options.network,
child_hotkey_ss58: Optional[str] = typer.Option(
None,
"child-hotkey-ss58",
"--child-hotkey-ss58",
help="The hotkey SS58 to designate as child (not specifying will use the provided wallet's hotkey)",
prompt=False,
),
Expand Down Expand Up @@ -5306,7 +5364,7 @@ def stake_childkey_take(
subtensor=self.initialize_chain(network),
netuid=netuid,
take=take,
hotkey=hotkey,
hotkey=child_hotkey_ss58,
wait_for_inclusion=wait_for_inclusion,
wait_for_finalization=wait_for_finalization,
prompt=prompt,
Expand Down Expand Up @@ -7764,10 +7822,10 @@ def crowd_refund(
"""
Refund contributors of a non-finalized crowdloan.

Any account may call this once the crowdloan is no longer wanted. Each call
refunds up to the on-chain `RefundContributorsLimit` contributors (currently
50) excluding the creator. Run it repeatedly until everyone except the creator
has been reimbursed.
Only the creator may call this. Each call refunds up to the on-chain `RefundContributorsLimit` contributors
(currently 50) excluding the creator. Run it repeatedly until everyone except the creator has been reimbursed.

Contributors can call `btcli crowdloan withdraw` at will.
"""
self.verbosity_handler(quiet, verbose, json_output)

Expand Down
2 changes: 1 addition & 1 deletion bittensor_cli/src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ class RootSudoOnly(Enum):
HYPERPARAMS = {
# btcli name: (subtensor method, root-only enum)
"rho": ("sudo_set_rho", RootSudoOnly.FALSE),
"kappa": ("sudo_set_kappa", RootSudoOnly.FALSE),
"kappa": ("sudo_set_kappa", RootSudoOnly.TRUE),
"immunity_period": ("sudo_set_immunity_period", RootSudoOnly.FALSE),
"min_allowed_weights": ("sudo_set_min_allowed_weights", RootSudoOnly.FALSE),
"max_weights_limit": ("sudo_set_max_weight_limit", RootSudoOnly.FALSE),
Expand Down
18 changes: 14 additions & 4 deletions bittensor_cli/src/bittensor/extrinsics/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,6 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]:
)
return False, None
console.print(f"[dark_orange]Initiating transfer on network: {subtensor.network}")
# Unlock wallet coldkey.
if not unlock_key(wallet).success:
return False, None

call_params: dict[str, Optional[Union[str, int]]] = {"dest": destination}
if transfer_all:
Expand Down Expand Up @@ -175,18 +172,31 @@ async def do_transfer() -> tuple[bool, str, str, 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 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 "
f"correct destination.",
default=False,
):
return False, None
if not Confirm.ask(
"Do you want to transfer:[bold white]\n"
f" amount: [bright_cyan]{amount if not transfer_all else account_balance}[/bright_cyan]\n"
f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : "
f"[bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]"
f"[bright_magenta]{wallet.coldkeypub.ss58_address}\n[/bright_magenta]"
f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]\n"
f"[bright_yellow]Transferring is not the same as staking. To instead stake, use "
f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow].\n"
f"Proceed with transfer?"
):
return False, None

# Unlock wallet coldkey.
if not unlock_key(wallet).success:
return False, None

with console.status(":satellite: Transferring...", spinner="earth"):
success, block_hash, err_msg, ext_receipt = await do_transfer()

Expand Down
12 changes: 9 additions & 3 deletions bittensor_cli/src/bittensor/subtensor_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1123,17 +1123,23 @@ async def get_hotkey_owner(
self,
hotkey_ss58: str,
block_hash: Optional[str] = None,
check_exists: bool = True,
) -> Optional[str]:
val = await self.query(
module="SubtensorModule",
storage_function="Owner",
params=[hotkey_ss58],
block_hash=block_hash,
)
if val:
exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash)
if check_exists:
if val:
exists = await self.does_hotkey_exist(
hotkey_ss58, block_hash=block_hash
)
else:
exists = False
else:
exists = False
exists = True
hotkey_owner = val if exists else None
return hotkey_owner

Expand Down
2 changes: 1 addition & 1 deletion bittensor_cli/src/bittensor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1496,7 +1496,7 @@ async def print_extrinsic_id(
Args:
extrinsic_receipt: AsyncExtrinsicReceipt object from a successful extrinsic submission.
"""
if extrinsic_receipt is None:
if extrinsic_receipt is None or not (await extrinsic_receipt.is_success):
return
substrate = extrinsic_receipt.substrate
ext_id = await extrinsic_receipt.get_extrinsic_identifier()
Expand Down
8 changes: 3 additions & 5 deletions bittensor_cli/src/commands/stake/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,6 @@ async def move_stake(
response = await subtensor.substrate.submit_extrinsic(
extrinsic, wait_for_inclusion=True, wait_for_finalization=False
)
await print_extrinsic_id(response)
ext_id = await response.get_extrinsic_identifier()

if not prompt:
Expand All @@ -580,6 +579,7 @@ async def move_stake(
)
return False, ""
else:
await print_extrinsic_id(response)
console.print(
":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]"
)
Expand Down Expand Up @@ -755,7 +755,6 @@ async def transfer_stake(
extrinsic, wait_for_inclusion=True, wait_for_finalization=False
)
ext_id = await response.get_extrinsic_identifier()
await print_extrinsic_id(extrinsic)

if not prompt:
console.print(":white_heavy_check_mark: [green]Sent[/green]")
Expand All @@ -767,7 +766,7 @@ async def transfer_stake(
f"{format_error_message(await response.error_message)}"
)
return False, ""

await print_extrinsic_id(extrinsic)
# Get and display new stake balances
new_stake, new_dest_stake = await asyncio.gather(
subtensor.get_stake(
Expand Down Expand Up @@ -933,7 +932,6 @@ async def swap_stake(
wait_for_finalization=wait_for_finalization,
)
ext_id = await response.get_extrinsic_identifier()
await print_extrinsic_id(response)

if not prompt:
console.print(":white_heavy_check_mark: [green]Sent[/green]")
Expand All @@ -945,7 +943,7 @@ async def swap_stake(
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(
Expand Down
Loading
Loading