Skip to content

Commit 85ab0ee

Browse files
authored
Merge pull request #686 from opentensor/release/9.14.2
Release/9.14.2
2 parents e193146 + 41950fb commit 85ab0ee

File tree

9 files changed

+311
-8
lines changed

9 files changed

+311
-8
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
# Changelog
2+
## 9.14.2 /2025-10-28
3+
* `stake remove --all` fails when unsuccessful by @thewhaleking in https://github.com/opentensor/btcli/pull/679
4+
* check subnet logo url by @thewhaleking in https://github.com/opentensor/btcli/pull/681
5+
* `st transfer` extrinsic id fix by @thewhaleking in https://github.com/opentensor/btcli/pull/685
6+
7+
**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.14.1...v9.14.2
8+
29
## 9.14.1 /2025-10-23
310
* Updates kappa to root sudo only in-line with devnet-ready by @thewhaleking in https://github.com/opentensor/btcli/pull/668
411
* Adds additional warnings for move vs transfer by @thewhaleking in https://github.com/opentensor/btcli/pull/672

bittensor_cli/src/bittensor/utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from functools import partial
1111
import re
1212

13+
import aiohttp
1314
from async_substrate_interface import AsyncExtrinsicReceipt
1415
from bittensor_wallet import Wallet, Keypair
1516
from bittensor_wallet.utils import SS58_FORMAT
@@ -1512,3 +1513,30 @@ async def print_extrinsic_id(
15121513
f":white_heavy_check_mark: Your extrinsic has been included as {ext_id}"
15131514
)
15141515
return
1516+
1517+
1518+
async def check_img_mimetype(img_url: str) -> tuple[bool, str, str]:
1519+
"""
1520+
Checks to see if the given URL is an image, as defined by its mimetype.
1521+
1522+
Args:
1523+
img_url: the URL to check
1524+
1525+
Returns:
1526+
tuple:
1527+
bool: True if the URL has a MIME type indicating image (e.g. 'image/...'), False otherwise.
1528+
str: MIME type of the URL.
1529+
str: error message if the URL could not be retrieved
1530+
1531+
"""
1532+
async with aiohttp.ClientSession() as session:
1533+
try:
1534+
async with session.get(img_url) as response:
1535+
if response.status != 200:
1536+
return False, "", "Could not fetch image"
1537+
elif "image/" not in response.content_type:
1538+
return False, response.content_type, ""
1539+
else:
1540+
return True, response.content_type, ""
1541+
except aiohttp.ClientError:
1542+
return False, "", "Could not fetch image"

bittensor_cli/src/commands/stake/move.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ async def transfer_stake(
766766
f"{format_error_message(await response.error_message)}"
767767
)
768768
return False, ""
769-
await print_extrinsic_id(extrinsic)
769+
await print_extrinsic_id(response)
770770
# Get and display new stake balances
771771
new_stake, new_dest_stake = await asyncio.gather(
772772
subtensor.get_stake(

bittensor_cli/src/commands/stake/remove.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ async def unstake_all(
547547
status=status,
548548
era=era,
549549
)
550-
ext_id = await ext_receipt.get_extrinsic_identifier() if successes else None
550+
ext_id = await ext_receipt.get_extrinsic_identifier() if success else None
551551
successes[hotkey_ss58] = {
552552
"success": success,
553553
"extrinsic_identifier": ext_id,

bittensor_cli/src/commands/subnets/subnets.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
json_console,
3939
get_hotkey_pub_ss58,
4040
print_extrinsic_id,
41+
check_img_mimetype,
4142
)
4243

4344
if TYPE_CHECKING:
@@ -2257,7 +2258,28 @@ async def set_identity(
22572258
) -> tuple[bool, Optional[str]]:
22582259
"""Set identity information for a subnet"""
22592260

2260-
if not await subtensor.subnet_exists(netuid):
2261+
if prompt and (logo_url := subnet_identity.get("logo_url")):
2262+
sn_exists, img_validation = await asyncio.gather(
2263+
subtensor.subnet_exists(netuid),
2264+
check_img_mimetype(subnet_identity["logo_url"]),
2265+
)
2266+
img_valid, content_type, err_msg = img_validation
2267+
if not img_valid:
2268+
confirmation_msg = f"Are you sure you want to use [blue]{logo_url}[/blue] as your image URL?"
2269+
if err_msg:
2270+
if not Confirm.ask(f"{err_msg}\n{confirmation_msg}"):
2271+
return False, None
2272+
else:
2273+
if not Confirm.ask(
2274+
f"The provided image's MIME type is {content_type}, which is not recognized as a valid"
2275+
f" image MIME type.\n{confirmation_msg}"
2276+
):
2277+
return False, None
2278+
2279+
else:
2280+
sn_exists = await subtensor.subnet_exists(netuid)
2281+
2282+
if not sn_exists:
22612283
err_console.print(f"Subnet {netuid} does not exist")
22622284
return False, None
22632285

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "bittensor-cli"
7-
version = "9.14.1"
7+
version = "9.14.2"
88
description = "Bittensor CLI"
99
readme = "README.md"
1010
authors = [
@@ -16,14 +16,14 @@ requires-python = ">=3.9,<3.14"
1616
dependencies = [
1717
"wheel",
1818
"async-substrate-interface>=1.5.2",
19-
"aiohttp~=3.10.2",
19+
"aiohttp~=3.13",
2020
"backoff~=2.2.1",
2121
"GitPython>=3.0.0",
2222
"netaddr~=1.3.0",
2323
"numpy>=2.0.1,<3.0.0",
2424
"Jinja2",
2525
"pycryptodome>=3.0.0,<4.0.0",
26-
"PyYAML~=6.0.1",
26+
"PyYAML~=6.0",
2727
"rich>=13.7,<15.0",
2828
"scalecodec==1.2.12",
2929
"typer>=0.16",
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import json
2+
from unittest.mock import MagicMock, AsyncMock, patch
3+
4+
5+
"""
6+
Verify commands:
7+
* btcli s create
8+
* btcli s set-identity
9+
* btcli s get-identity
10+
"""
11+
12+
13+
def test_set_id(local_chain, wallet_setup):
14+
"""
15+
Tests that the user is prompted to confirm that the incorrect text/html URL is
16+
indeed the one they wish to set as their logo URL, and that when the MIME type is 'image/jpeg'
17+
they are not given this prompt.
18+
"""
19+
wallet_path_alice = "//Alice"
20+
netuid = 2
21+
22+
# Create wallet for Alice
23+
keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup(
24+
wallet_path_alice
25+
)
26+
# Register a subnet with sudo as Alice
27+
result = exec_command_alice(
28+
command="subnets",
29+
sub_command="create",
30+
extra_args=[
31+
"--wallet-path",
32+
wallet_path_alice,
33+
"--chain",
34+
"ws://127.0.0.1:9945",
35+
"--wallet-name",
36+
wallet_alice.name,
37+
"--wallet-hotkey",
38+
wallet_alice.hotkey_str,
39+
"--subnet-name",
40+
"Test Subnet",
41+
"--repo",
42+
"https://github.com/username/repo",
43+
"--contact",
44+
"alice@opentensor.dev",
45+
"--url",
46+
"https://testsubnet.com",
47+
"--discord",
48+
"alice#1234",
49+
"--description",
50+
"A test subnet for e2e testing",
51+
"--additional-info",
52+
"Created by Alice",
53+
"--logo-url",
54+
"https://testsubnet.com/logo.png",
55+
"--no-prompt",
56+
"--json-output",
57+
],
58+
)
59+
result_output = json.loads(result.stdout)
60+
assert result_output["success"] is True
61+
62+
mock_response = MagicMock()
63+
mock_response.status = 200
64+
mock_response.content_type = "text/html" # bad MIME type
65+
mock_response.__aenter__ = AsyncMock(return_value=mock_response)
66+
mock_response.__aexit__ = AsyncMock(return_value=None)
67+
68+
mock_session = MagicMock()
69+
mock_session.get = MagicMock(return_value=mock_response)
70+
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
71+
mock_session.__aexit__ = AsyncMock(return_value=None)
72+
73+
with patch("aiohttp.ClientSession", return_value=mock_session):
74+
set_identity = exec_command_alice(
75+
"subnets",
76+
"set-identity",
77+
extra_args=[
78+
"--wallet-path",
79+
wallet_path_alice,
80+
"--wallet-name",
81+
wallet_alice.name,
82+
"--hotkey",
83+
wallet_alice.hotkey_str,
84+
"--chain",
85+
"ws://127.0.0.1:9945",
86+
"--netuid",
87+
str(netuid),
88+
"--subnet-name",
89+
sn_name := "Test Subnet",
90+
"--github-repo",
91+
sn_github := "https://github.com/username/repo",
92+
"--subnet-contact",
93+
sn_contact := "alice@opentensor.dev",
94+
"--subnet-url",
95+
sn_url := "https://testsubnet.com",
96+
"--discord",
97+
sn_discord := "alice#1234",
98+
"--description",
99+
sn_description := "A test subnet for e2e testing",
100+
"--logo-url",
101+
sn_logo_url := "https://testsubnet.com/logo.png",
102+
"--additional-info",
103+
sn_add_info := "Created by Alice",
104+
"--prompt",
105+
],
106+
inputs=["Y", "Y"],
107+
)
108+
assert (
109+
f"Are you sure you want to use {sn_logo_url} as your image URL?"
110+
in set_identity.stdout
111+
)
112+
get_identity = exec_command_alice(
113+
"subnets",
114+
"get-identity",
115+
extra_args=[
116+
"--chain",
117+
"ws://127.0.0.1:9945",
118+
"--netuid",
119+
netuid,
120+
"--json-output",
121+
],
122+
)
123+
get_identity_output = json.loads(get_identity.stdout)
124+
assert get_identity_output["subnet_name"] == sn_name
125+
assert get_identity_output["github_repo"] == sn_github
126+
assert get_identity_output["subnet_contact"] == sn_contact
127+
assert get_identity_output["subnet_url"] == sn_url
128+
assert get_identity_output["discord"] == sn_discord
129+
assert get_identity_output["description"] == sn_description
130+
assert get_identity_output["logo_url"] == sn_logo_url
131+
assert get_identity_output["additional"] == sn_add_info
132+
133+
mock_response = MagicMock()
134+
mock_response.status = 200
135+
mock_response.content_type = "image/jpeg" # good MIME type
136+
mock_response.__aenter__ = AsyncMock(return_value=mock_response)
137+
mock_response.__aexit__ = AsyncMock(return_value=None)
138+
139+
mock_session = MagicMock()
140+
mock_session.get = MagicMock(return_value=mock_response)
141+
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
142+
mock_session.__aexit__ = AsyncMock(return_value=None)
143+
with patch("aiohttp.ClientSession", return_value=mock_session):
144+
set_identity = exec_command_alice(
145+
"subnets",
146+
"set-identity",
147+
extra_args=[
148+
"--wallet-path",
149+
wallet_path_alice,
150+
"--wallet-name",
151+
wallet_alice.name,
152+
"--hotkey",
153+
wallet_alice.hotkey_str,
154+
"--chain",
155+
"ws://127.0.0.1:9945",
156+
"--netuid",
157+
str(netuid),
158+
"--subnet-name",
159+
sn_name := "Test Subnet",
160+
"--github-repo",
161+
sn_github := "https://github.com/username/repo",
162+
"--subnet-contact",
163+
sn_contact := "alice@opentensor.dev",
164+
"--subnet-url",
165+
sn_url := "https://testsubnet.com",
166+
"--discord",
167+
sn_discord := "alice#1234",
168+
"--description",
169+
sn_description := "A test subnet for e2e testing",
170+
"--logo-url",
171+
sn_logo_url := "https://testsubnet.com/logo.png",
172+
"--additional-info",
173+
sn_add_info := "Created by Alice",
174+
"--prompt",
175+
],
176+
inputs=["Y"],
177+
)
178+
assert (
179+
f"Are you sure you want to use {sn_logo_url} as your image URL?"
180+
not in set_identity.stdout
181+
)
182+
get_identity = exec_command_alice(
183+
"subnets",
184+
"get-identity",
185+
extra_args=[
186+
"--chain",
187+
"ws://127.0.0.1:9945",
188+
"--netuid",
189+
netuid,
190+
"--json-output",
191+
],
192+
)
193+
get_identity_output = json.loads(get_identity.stdout)
194+
assert get_identity_output["subnet_name"] == sn_name
195+
assert get_identity_output["github_repo"] == sn_github
196+
assert get_identity_output["subnet_contact"] == sn_contact
197+
assert get_identity_output["subnet_url"] == sn_url
198+
assert get_identity_output["discord"] == sn_discord
199+
assert get_identity_output["description"] == sn_description
200+
assert get_identity_output["logo_url"] == sn_logo_url
201+
assert get_identity_output["additional"] == sn_add_info

tests/e2e_tests/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ def __call__(
2828
self,
2929
command: str,
3030
sub_command: str,
31-
extra_args: Optional[list[str]],
32-
inputs: Optional[list[str]],
31+
extra_args: Optional[list[str]] = None,
32+
inputs: Optional[list[str]] = None,
3333
) -> Result: ...
3434

3535

0 commit comments

Comments
 (0)