Skip to content
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 9.7.0/2025-06-16

## What's Changed
* Add `SKIP_PULL` variable for conftest.py by @basfroman in https://github.com/opentensor/btcli/pull/502
* Feat: Adds netuid support in swap_hotkeys by @ibraheem-abe in https://github.com/opentensor/btcli/pull/505

**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.6.0...v9.7.0

## 9.6.0/2025-06-12

## What's Changed
Expand Down
12 changes: 10 additions & 2 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1897,6 +1897,8 @@ def wallet_swap_hotkey(
wallet_name: Optional[str] = Options.wallet_name,
wallet_path: Optional[str] = Options.wallet_path,
wallet_hotkey: Optional[str] = Options.wallet_hotkey,
netuid: Optional[int] = Options.netuid_not_req,
all_netuids: bool = Options.all_netuids,
network: Optional[list[str]] = Options.network,
destination_hotkey_name: Optional[str] = typer.Argument(
None, help="Destination hotkey name."
Expand All @@ -1917,12 +1919,14 @@ 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.
- Finally, note that this command requires a fee of 1 TAO for recycling and this fee is taken from your wallet (coldkeyA).

EXAMPLE

[green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey
[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)
original_wallet = self.wallet_ask(
wallet_name,
Expand All @@ -1946,7 +1950,7 @@ def wallet_swap_hotkey(
self.initialize_chain(network)
return self._run_command(
wallets.swap_hotkey(
original_wallet, new_wallet, self.subtensor, prompt, json_output
original_wallet, new_wallet, self.subtensor, netuid, prompt, json_output
)
)

Expand Down Expand Up @@ -5021,6 +5025,7 @@ def subnets_create(
description: Optional[str] = typer.Option(
None, "--description", help="Description"
),
logo_url: Optional[str] = typer.Option(None, "--logo-url", help="Logo URL"),
additional_info: Optional[str] = typer.Option(
None, "--additional-info", help="Additional information"
),
Expand Down Expand Up @@ -5063,6 +5068,7 @@ def subnets_create(
subnet_url=subnet_url,
discord=discord,
description=description,
logo_url=logo_url,
additional=additional_info,
)
self._run_command(
Expand Down Expand Up @@ -5186,6 +5192,7 @@ def subnets_set_identity(
description: Optional[str] = typer.Option(
None, "--description", help="Description"
),
logo_url: Optional[str] = typer.Option(None, "--logo-url", help="Logo URL"),
additional_info: Optional[str] = typer.Option(
None, "--additional-info", help="Additional information"
),
Expand Down Expand Up @@ -5237,6 +5244,7 @@ def subnets_set_identity(
subnet_url=subnet_url,
discord=discord,
description=description,
logo_url=logo_url,
additional=additional_info,
)

Expand Down
2 changes: 2 additions & 0 deletions bittensor_cli/src/bittensor/chain_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ class SubnetIdentity(InfoBase):
subnet_url: str
discord: str
description: str
logo_url: str
additional: str

@classmethod
Expand All @@ -641,6 +642,7 @@ def _fix_decoded(cls, decoded: dict) -> "SubnetIdentity":
subnet_url=bytes(decoded["subnet_url"]).decode(),
discord=bytes(decoded["discord"]).decode(),
description=bytes(decoded["description"]).decode(),
logo_url=bytes(decoded["logo_url"]).decode(),
additional=bytes(decoded["additional"]).decode(),
)

Expand Down
69 changes: 55 additions & 14 deletions bittensor_cli/src/bittensor/extrinsics/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1611,7 +1611,8 @@ def _update_curr_block(
"""
Update the current block data with the provided block information and difficulty.

This function updates the current block and its difficulty in a thread-safe manner. It sets the current block
This function updates the current block
and its difficulty in a thread-safe manner. It sets the current block
number, hashes the block with the hotkey, updates the current block bytes, and packs the difficulty.

:param curr_diff: Shared array to store the current difficulty.
Expand Down Expand Up @@ -1745,6 +1746,7 @@ async def swap_hotkey_extrinsic(
subtensor: "SubtensorInterface",
wallet: Wallet,
new_wallet: Wallet,
netuid: Optional[int] = None,
prompt: bool = False,
) -> bool:
"""
Expand All @@ -1756,37 +1758,76 @@ async def swap_hotkey_extrinsic(
netuids_registered = await subtensor.get_netuids_for_hotkey(
wallet.hotkey.ss58_address, block_hash=block_hash
)
if not len(netuids_registered) > 0:
netuids_registered_new_hotkey = await subtensor.get_netuids_for_hotkey(
new_wallet.hotkey.ss58_address, block_hash=block_hash
)

if netuid is not None and netuid not in netuids_registered:
err_console.print(
f":cross_mark: [red]Failed[/red]: Original hotkey {wallet.hotkey.ss58_address} is not registered on subnet {netuid}"
)
return False

elif not len(netuids_registered) > 0:
err_console.print(
f"Destination hotkey [dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] is not registered. "
f"Original hotkey [dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] is not registered on any subnet. "
f"Please register and try again"
)
return False

if netuid is not None:
if netuid in netuids_registered_new_hotkey:
err_console.print(
f":cross_mark: [red]Failed[/red]: New hotkey {new_wallet.hotkey.ss58_address} "
f"is already registered on subnet {netuid}"
)
return False
else:
if len(netuids_registered_new_hotkey) > 0:
err_console.print(
f":cross_mark: [red]Failed[/red]: New hotkey {new_wallet.hotkey.ss58_address} "
f"is already registered on subnet(s) {netuids_registered_new_hotkey}"
)
return False

if not unlock_key(wallet).success:
return False

if prompt:
# Prompt user for confirmation.
if not Confirm.ask(
f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t"
f"[dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] with hotkey \n\t"
f"[dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange]\n"
"This operation will cost [bold cyan]1 TAO t (recycled)[/bold cyan]"
):
if netuid is not None:
confirm_message = (
f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t"
f"[dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] with hotkey \n\t"
f"[dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] on subnet {netuid}\n"
"This operation will cost [bold cyan]1 TAO (recycled)[/bold cyan]"
)
else:
confirm_message = (
f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t"
f"[dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] with hotkey \n\t"
f"[dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] on all subnets\n"
"This operation will cost [bold cyan]1 TAO (recycled)[/bold cyan]"
)

if not Confirm.ask(confirm_message):
return False
print_verbose(
f"Swapping {wallet.name}'s hotkey ({wallet.hotkey.ss58_address}) with "
f"{new_wallet.name}s hotkey ({new_wallet.hotkey.ss58_address})"
f"{new_wallet.name}'s hotkey ({new_wallet.hotkey.ss58_address})"
)
with console.status(":satellite: Swapping hotkeys...", spinner="aesthetic"):
call_params = {
"hotkey": wallet.hotkey.ss58_address,
"new_hotkey": new_wallet.hotkey.ss58_address,
}
if netuid is not None:
call_params["netuid"] = netuid

call = await subtensor.substrate.compose_call(
call_module="SubtensorModule",
call_function="swap_hotkey",
call_params={
"hotkey": wallet.hotkey.ss58_address,
"new_hotkey": new_wallet.hotkey.ss58_address,
},
call_params=call_params,
)
success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet)

Expand Down
8 changes: 8 additions & 0 deletions bittensor_cli/src/bittensor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,7 @@ def prompt_for_subnet_identity(
subnet_url: Optional[str],
discord: Optional[str],
description: Optional[str],
logo_url: Optional[str],
additional: Optional[str],
):
"""
Expand Down Expand Up @@ -1227,6 +1228,13 @@ def prompt_for_subnet_identity(
lambda x: x and len(x.encode("utf-8")) > 1024,
"[red]Error:[/red] Description must be <= 1024 bytes.",
),
(
"logo_url",
"[blue]Logo URL [dim](optional)[/blue]",
logo_url,
lambda x: x and len(x.encode("utf-8")) > 1024,
"[red]Error:[/red] Logo URL must be <= 1024 bytes.",
),
(
"additional",
"[blue]Additional information [dim](optional)[/blue]",
Expand Down
6 changes: 6 additions & 0 deletions bittensor_cli/src/commands/subnets/subnets.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ async def _find_event_attributes_in_extrinsic_receipt(
"description": subnet_identity["description"].encode()
if subnet_identity.get("description")
else b"",
"logo_url": subnet_identity["logo_url"].encode()
if subnet_identity.get("logo_url")
else b"",
"additional": subnet_identity["additional"].encode()
if subnet_identity.get("additional")
else b"",
Expand Down Expand Up @@ -2207,6 +2210,7 @@ async def set_identity(
"subnet_url": subnet_identity.get("subnet_url", ""),
"discord": subnet_identity.get("discord", ""),
"description": subnet_identity.get("description", ""),
"logo_url": subnet_identity.get("logo_url", ""),
"additional": subnet_identity.get("additional", ""),
}

Expand Down Expand Up @@ -2252,6 +2256,7 @@ async def set_identity(
"subnet_url",
"discord",
"description",
"logo_url",
"additional",
]:
value = getattr(identity, key, None)
Expand Down Expand Up @@ -2301,6 +2306,7 @@ async def get_identity(
"subnet_url",
"discord",
"description",
"logo_url",
"additional",
]:
value = getattr(identity, key, None)
Expand Down
2 changes: 2 additions & 0 deletions bittensor_cli/src/commands/wallets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1632,6 +1632,7 @@ async def swap_hotkey(
original_wallet: Wallet,
new_wallet: Wallet,
subtensor: SubtensorInterface,
netuid: Optional[int],
prompt: bool,
json_output: bool,
):
Expand All @@ -1640,6 +1641,7 @@ async def swap_hotkey(
subtensor,
original_wallet,
new_wallet,
netuid=netuid,
prompt=prompt,
)
if json_output:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "bittensor-cli"
version = "9.6.0"
version = "9.7.0"
description = "Bittensor CLI"
readme = "README.md"
authors = [
Expand Down
7 changes: 7 additions & 0 deletions tests/e2e_tests/test_staking_sudo.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ def test_staking(local_chain, wallet_setup):
"A test subnet for e2e testing",
"--additional-info",
"Created by Alice",
"--logo-url",
"https://testsubnet.com/logo.png",
"--no-prompt",
"--json-output",
],
Expand Down Expand Up @@ -121,6 +123,8 @@ def test_staking(local_chain, wallet_setup):
"A test subnet for e2e testing",
"--additional-info",
"Created by Alice",
"--logo-url",
"https://testsubnet.com/logo.png",
"--no-prompt",
"--json-output",
],
Expand Down Expand Up @@ -198,6 +202,8 @@ def test_staking(local_chain, wallet_setup):
sn_discord := "alice#1234",
"--description",
sn_description := "A test subnet for e2e testing",
"--logo-url",
sn_logo_url := "https://testsubnet.com/logo.png",
"--additional-info",
sn_add_info := "Created by Alice",
"--json-output",
Expand Down Expand Up @@ -225,6 +231,7 @@ def test_staking(local_chain, wallet_setup):
assert get_identity_output["subnet_url"] == sn_url
assert get_identity_output["discord"] == sn_discord
assert get_identity_output["description"] == sn_description
assert get_identity_output["logo_url"] == sn_logo_url
assert get_identity_output["additional"] == sn_add_info

# Start emissions on SNs
Expand Down
4 changes: 4 additions & 0 deletions tests/e2e_tests/test_unstaking.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ def test_unstaking(local_chain, wallet_setup):
"A test subnet for e2e testing",
"--additional-info",
"Test subnet",
"--logo-url",
"https://testsubnet.com/logo.png",
"--no-prompt",
],
)
Expand Down Expand Up @@ -115,6 +117,8 @@ def test_unstaking(local_chain, wallet_setup):
"A test subnet for e2e testing",
"--additional-info",
"Test subnet",
"--logo-url",
"https://testsubnet.com/logo.png",
"--no-prompt",
],
)
Expand Down
17 changes: 15 additions & 2 deletions tests/e2e_tests/test_wallet_creations.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,13 @@ def test_wallet_balance_all(local_chain, wallet_setup, capfd):
result = exec_command(
command="wallet",
sub_command="balance",
extra_args=["--wallet-path", wallet_path, "--all"],
extra_args=[
"--wallet-path",
wallet_path,
"--all",
"--chain",
"ws://127.0.0.1:9945",
],
)

output = result.stdout
Expand All @@ -600,7 +606,14 @@ def test_wallet_balance_all(local_chain, wallet_setup, capfd):
json_results = exec_command(
"wallet",
"balance",
extra_args=["--wallet-path", wallet_path, "--all", "--json-output"],
extra_args=[
"--wallet-path",
wallet_path,
"--all",
"--json-output",
"--chain",
"ws://127.0.0.1:9945",
],
)
json_results_output = json.loads(json_results.stdout)
for wallet_name in wallet_names:
Expand Down
4 changes: 4 additions & 0 deletions tests/e2e_tests/test_wallet_interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def test_wallet_overview_inspect(local_chain, wallet_setup):
"test#1234",
"--description",
"A test subnet for e2e testing",
"--logo-url",
"https://testsubnet.com/logo.png",
"--additional-info",
"Test subnet",
"--no-prompt",
Expand Down Expand Up @@ -388,6 +390,8 @@ def test_wallet_identities(local_chain, wallet_setup):
"A test subnet for e2e testing",
"--additional-info",
"Created by Alice",
"--logo-url",
"https://testsubnet.com/logo.png",
"--no-prompt",
],
)
Expand Down
Loading