bittensor-cli 9.0.0rc4__py3-none-any.whl → 9.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import ast
2
+ from collections import namedtuple
2
3
  import math
3
4
  import os
4
5
  import sqlite3
@@ -13,7 +14,7 @@ import re
13
14
 
14
15
  from bittensor_wallet import Wallet, Keypair
15
16
  from bittensor_wallet.utils import SS58_FORMAT
16
- from bittensor_wallet.errors import KeyFileError
17
+ from bittensor_wallet.errors import KeyFileError, PasswordError
17
18
  from bittensor_wallet import utils
18
19
  from jinja2 import Template
19
20
  from markupsafe import Markup
@@ -31,12 +32,30 @@ from bittensor_cli.src import defaults, Constants
31
32
 
32
33
  if TYPE_CHECKING:
33
34
  from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters
34
- from async_substrate_interface.async_substrate import AsyncSubstrateInterface
35
35
 
36
36
  console = Console()
37
37
  err_console = Console(stderr=True)
38
38
  verbose_console = Console(quiet=True)
39
39
 
40
+ UnlockStatus = namedtuple("UnlockStatus", ["success", "message"])
41
+
42
+
43
+ class _Hotkey:
44
+ def __init__(self, hotkey_ss58=None):
45
+ self.ss58_address = hotkey_ss58
46
+
47
+
48
+ class WalletLike:
49
+ def __init__(self, name=None, hotkey_ss58=None, hotkey_str=None):
50
+ self.name = name
51
+ self.hotkey_ss58 = hotkey_ss58
52
+ self.hotkey_str = hotkey_str
53
+ self._hotkey = _Hotkey(hotkey_ss58)
54
+
55
+ @property
56
+ def hotkey(self):
57
+ return self._hotkey
58
+
40
59
 
41
60
  def print_console(message: str, colour: str, title: str, console: Console):
42
61
  console.print(
@@ -196,13 +215,14 @@ def convert_root_weight_uids_and_vals_to_tensor(
196
215
 
197
216
 
198
217
  def get_hotkey_wallets_for_wallet(
199
- wallet: Wallet, show_nulls: bool = False
218
+ wallet: Wallet, show_nulls: bool = False, show_encrypted: bool = False
200
219
  ) -> list[Optional[Wallet]]:
201
220
  """
202
221
  Returns wallet objects with hotkeys for a single given wallet
203
222
 
204
223
  :param wallet: Wallet object to use for the path
205
224
  :param show_nulls: will add `None` into the output if a hotkey is encrypted or not on the device
225
+ :param show_encrypted: will add some basic info about the encrypted hotkey
206
226
 
207
227
  :return: a list of wallets (with Nones included for cases of a hotkey being encrypted or not on the device, if
208
228
  `show_nulls` is set to `True`)
@@ -218,12 +238,18 @@ def get_hotkey_wallets_for_wallet(
218
238
  hotkey_for_name = Wallet(path=str(wallet_path), name=wallet.name, hotkey=h_name)
219
239
  try:
220
240
  if (
221
- hotkey_for_name.hotkey_file.exists_on_device()
241
+ (exists := hotkey_for_name.hotkey_file.exists_on_device())
222
242
  and not hotkey_for_name.hotkey_file.is_encrypted()
223
243
  # and hotkey_for_name.coldkeypub.ss58_address
224
244
  and hotkey_for_name.hotkey.ss58_address
225
245
  ):
226
246
  hotkey_wallets.append(hotkey_for_name)
247
+ elif (
248
+ show_encrypted and exists and hotkey_for_name.hotkey_file.is_encrypted()
249
+ ):
250
+ hotkey_wallets.append(
251
+ WalletLike(str(wallet_path), "<ENCRYPTED>", h_name)
252
+ )
227
253
  elif show_nulls:
228
254
  hotkey_wallets.append(None)
229
255
  except (
@@ -240,11 +266,14 @@ def get_hotkey_wallets_for_wallet(
240
266
  def get_coldkey_wallets_for_path(path: str) -> list[Wallet]:
241
267
  """Gets all wallets with coldkeys from a given path"""
242
268
  wallet_path = Path(path).expanduser()
243
- wallets = [
244
- Wallet(name=directory.name, path=path)
245
- for directory in wallet_path.iterdir()
246
- if directory.is_dir()
247
- ]
269
+ try:
270
+ wallets = [
271
+ Wallet(name=directory.name, path=path)
272
+ for directory in wallet_path.iterdir()
273
+ if directory.is_dir()
274
+ ]
275
+ except FileNotFoundError:
276
+ wallets = []
248
277
  return wallets
249
278
 
250
279
 
@@ -448,16 +477,13 @@ def get_explorer_url_for_network(
448
477
  return explorer_urls
449
478
 
450
479
 
451
- def format_error_message(
452
- error_message: Union[dict, Exception], substrate: "AsyncSubstrateInterface"
453
- ) -> str:
480
+ def format_error_message(error_message: Union[dict, Exception]) -> str:
454
481
  """
455
482
  Formats an error message from the Subtensor error information for use in extrinsics.
456
483
 
457
484
  Args:
458
485
  error_message: A dictionary containing the error information from Subtensor, or a SubstrateRequestException
459
486
  containing dictionary literal args.
460
- substrate: The initialised SubstrateInterface object to use.
461
487
 
462
488
  Returns:
463
489
  str: A formatted error message string.
@@ -479,7 +505,7 @@ def format_error_message(
479
505
  elif all(x in d for x in ["code", "message", "data"]):
480
506
  new_error_message = d
481
507
  break
482
- except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
508
+ except ValueError:
483
509
  pass
484
510
  if new_error_message is None:
485
511
  return_val = " ".join(error_message.args)
@@ -500,7 +526,10 @@ def format_error_message(
500
526
 
501
527
  # subtensor custom error marker
502
528
  if err_data.startswith("Custom error:"):
503
- err_description = f"{err_data} | Please consult https://docs.bittensor.com/subtensor-nodes/subtensor-error-messages"
529
+ err_description = (
530
+ f"{err_data} | Please consult "
531
+ f"https://docs.bittensor.com/subtensor-nodes/subtensor-error-messages"
532
+ )
504
533
  else:
505
534
  err_description = err_data
506
535
 
@@ -535,7 +564,8 @@ def decode_hex_identity_dict(info_dictionary) -> dict[str, Any]:
535
564
  """
536
565
  Decodes hex-encoded strings in a dictionary.
537
566
 
538
- This function traverses the given dictionary, identifies hex-encoded strings, and decodes them into readable strings. It handles nested dictionaries and lists within the dictionary.
567
+ This function traverses the given dictionary, identifies hex-encoded strings, and decodes them into readable
568
+ strings. It handles nested dictionaries and lists within the dictionary.
539
569
 
540
570
  Args:
541
571
  info_dictionary (dict): The dictionary containing hex-encoded strings to decode.
@@ -557,7 +587,7 @@ def decode_hex_identity_dict(info_dictionary) -> dict[str, Any]:
557
587
  def get_decoded(data: str) -> str:
558
588
  """Decodes a hex-encoded string."""
559
589
  try:
560
- return bytes.fromhex(data[2:]).decode()
590
+ return hex_to_bytes(data).decode()
561
591
  except UnicodeDecodeError:
562
592
  print(f"Could not decode: {key}: {item}")
563
593
 
@@ -1096,6 +1126,7 @@ def prompt_for_identity(
1096
1126
 
1097
1127
 
1098
1128
  def prompt_for_subnet_identity(
1129
+ current_identity: dict,
1099
1130
  subnet_name: Optional[str],
1100
1131
  github_repo: Optional[str],
1101
1132
  subnet_contact: Optional[str],
@@ -1180,7 +1211,7 @@ def prompt_for_subnet_identity(
1180
1211
  prompt,
1181
1212
  rejection=rejection_func,
1182
1213
  rejection_text=rejection_msg,
1183
- default=None, # Maybe we can add some defaults later
1214
+ default=current_identity.get(key, ""),
1184
1215
  show_default=True,
1185
1216
  )
1186
1217
 
@@ -1262,11 +1293,14 @@ def is_linux():
1262
1293
  """Returns True if the operating system is Linux."""
1263
1294
  return platform.system().lower() == "linux"
1264
1295
 
1296
+
1265
1297
  def validate_rate_tolerance(value: Optional[float]) -> Optional[float]:
1266
1298
  """Validates rate tolerance input"""
1267
1299
  if value is not None:
1268
1300
  if value < 0:
1269
- raise typer.BadParameter("Rate tolerance cannot be negative (less than 0%).")
1301
+ raise typer.BadParameter(
1302
+ "Rate tolerance cannot be negative (less than 0%)."
1303
+ )
1270
1304
  if value > 1:
1271
1305
  raise typer.BadParameter("Rate tolerance cannot be greater than 1 (100%).")
1272
1306
  if value > 0.5:
@@ -1275,3 +1309,79 @@ def validate_rate_tolerance(value: Optional[float]) -> Optional[float]:
1275
1309
  "This may result in unfavorable transaction execution.[/yellow]"
1276
1310
  )
1277
1311
  return value
1312
+
1313
+
1314
+ def unlock_key(
1315
+ wallet: Wallet, unlock_type="cold", print_out: bool = True
1316
+ ) -> "UnlockStatus":
1317
+ """
1318
+ Attempts to decrypt a wallet's coldkey or hotkey
1319
+ Args:
1320
+ wallet: a Wallet object
1321
+ unlock_type: the key type, 'cold' or 'hot'
1322
+ print_out: whether to print out the error message to the err_console
1323
+
1324
+ Returns: UnlockStatus for success status of unlock, with error message if unsuccessful
1325
+
1326
+ """
1327
+ if unlock_type == "cold":
1328
+ unlocker = "unlock_coldkey"
1329
+ elif unlock_type == "hot":
1330
+ unlocker = "unlock_hotkey"
1331
+ else:
1332
+ raise ValueError(
1333
+ f"Invalid unlock type provided: {unlock_type}. Must be 'cold' or 'hot'."
1334
+ )
1335
+ try:
1336
+ getattr(wallet, unlocker)()
1337
+ return UnlockStatus(True, "")
1338
+ except PasswordError:
1339
+ err_msg = f"The password used to decrypt your {unlock_type.capitalize()}key Keyfile is invalid."
1340
+ if print_out:
1341
+ err_console.print(f":cross_mark: [red]{err_msg}[/red]")
1342
+ return UnlockStatus(False, err_msg)
1343
+ except KeyFileError:
1344
+ err_msg = f"{unlock_type.capitalize()}key Keyfile is corrupt, non-writable, or non-readable, or non-existent."
1345
+ if print_out:
1346
+ err_console.print(f":cross_mark: [red]{err_msg}[/red]")
1347
+ return UnlockStatus(False, err_msg)
1348
+
1349
+
1350
+ def hex_to_bytes(hex_str: str) -> bytes:
1351
+ """
1352
+ Converts a hex-encoded string into bytes. Handles 0x-prefixed and non-prefixed hex-encoded strings.
1353
+ """
1354
+ if hex_str.startswith("0x"):
1355
+ bytes_result = bytes.fromhex(hex_str[2:])
1356
+ else:
1357
+ bytes_result = bytes.fromhex(hex_str)
1358
+ return bytes_result
1359
+
1360
+
1361
+ def blocks_to_duration(blocks: int) -> str:
1362
+ """Convert blocks to human readable duration string using two largest units.
1363
+
1364
+ Args:
1365
+ blocks (int): Number of blocks (12s per block)
1366
+
1367
+ Returns:
1368
+ str: Duration string like '2d 5h', '3h 45m', '2m 10s', or '0s'
1369
+ """
1370
+ if blocks <= 0:
1371
+ return "0s"
1372
+
1373
+ seconds = blocks * 12
1374
+ intervals = [
1375
+ ("d", 86400), # 60 * 60 * 24
1376
+ ("h", 3600), # 60 * 60
1377
+ ("m", 60),
1378
+ ("s", 1),
1379
+ ]
1380
+ results = []
1381
+ for unit, seconds_per_unit in intervals:
1382
+ unit_count = seconds // seconds_per_unit
1383
+ seconds %= seconds_per_unit
1384
+ if unit_count > 0:
1385
+ results.append(f"{unit_count}{unit}")
1386
+ # Return only the first two non-zero units
1387
+ return " ".join(results[:2]) or "0s"
@@ -107,15 +107,13 @@ async def stake_add(
107
107
  )
108
108
  return
109
109
  else:
110
- err_out(
111
- f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}"
112
- )
110
+ err_out(f"\n{failure_prelude} with error: {format_error_message(e)}")
113
111
  return
114
112
  else:
115
113
  await response.process_events()
116
114
  if not await response.is_success:
117
115
  err_out(
118
- f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}"
116
+ f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}"
119
117
  )
120
118
  else:
121
119
  block_hash = await subtensor.substrate.get_chain_head()
@@ -180,15 +178,13 @@ async def stake_add(
180
178
  extrinsic, wait_for_inclusion=True, wait_for_finalization=False
181
179
  )
182
180
  except SubstrateRequestException as e:
183
- err_out(
184
- f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}"
185
- )
181
+ err_out(f"\n{failure_prelude} with error: {format_error_message(e)}")
186
182
  return
187
183
  else:
188
184
  await response.process_events()
189
185
  if not await response.is_success:
190
186
  err_out(
191
- f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}"
187
+ f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}"
192
188
  )
193
189
  else:
194
190
  new_balance, new_stake = await asyncio.gather(
@@ -2,7 +2,6 @@ import asyncio
2
2
  from typing import Optional
3
3
 
4
4
  from bittensor_wallet import Wallet
5
- from bittensor_wallet.errors import KeyFileError
6
5
  from rich.prompt import Confirm, Prompt, IntPrompt
7
6
  from rich.table import Table
8
7
  from rich.text import Text
@@ -19,6 +18,7 @@ from bittensor_cli.src.bittensor.utils import (
19
18
  u64_to_float,
20
19
  is_valid_ss58_address,
21
20
  format_error_message,
21
+ unlock_key,
22
22
  )
23
23
 
24
24
 
@@ -99,10 +99,8 @@ async def set_children_extrinsic(
99
99
  return False, "Operation Cancelled"
100
100
 
101
101
  # Decrypt coldkey.
102
- try:
103
- wallet.unlock_coldkey()
104
- except KeyFileError:
105
- return False, "There was an error unlocking your coldkey."
102
+ if not (unlock_status := unlock_key(wallet, print_out=False)).success:
103
+ return False, unlock_status.message
106
104
 
107
105
  with console.status(
108
106
  f":satellite: {operation} on [white]{subtensor.network}[/white] ..."
@@ -185,10 +183,8 @@ async def set_childkey_take_extrinsic(
185
183
  return False, "Operation Cancelled"
186
184
 
187
185
  # Decrypt coldkey.
188
- try:
189
- wallet.unlock_coldkey()
190
- except KeyFileError:
191
- return False, "There was an error unlocking your coldkey."
186
+ if not (unlock_status := unlock_key(wallet, print_out=False)).success:
187
+ return False, unlock_status.message
192
188
 
193
189
  with console.status(
194
190
  f":satellite: Setting childkey take on [white]{subtensor.network}[/white] ..."
@@ -239,7 +235,7 @@ async def set_childkey_take_extrinsic(
239
235
  except SubstrateRequestException as e:
240
236
  return (
241
237
  False,
242
- f"Exception occurred while setting childkey take: {format_error_message(e, subtensor.substrate)}",
238
+ f"Exception occurred while setting childkey take: {format_error_message(e)}",
243
239
  )
244
240
 
245
241
 
@@ -260,12 +256,10 @@ async def get_childkey_take(subtensor, hotkey: str, netuid: int) -> Optional[int
260
256
  params=[hotkey, netuid],
261
257
  )
262
258
  if childkey_take_:
263
- return int(childkey_take_.value)
259
+ return int(childkey_take_)
264
260
 
265
261
  except SubstrateRequestException as e:
266
- err_console.print(
267
- f"Error querying ChildKeys: {format_error_message(e, subtensor.substrate)}"
268
- )
262
+ err_console.print(f"Error querying ChildKeys: {format_error_message(e)}")
269
263
  return None
270
264
 
271
265
 
@@ -502,7 +496,7 @@ async def set_children(
502
496
  subtensor: "SubtensorInterface",
503
497
  children: list[str],
504
498
  proportions: list[float],
505
- netuid: Optional[int],
499
+ netuid: Optional[int] = None,
506
500
  wait_for_inclusion: bool = True,
507
501
  wait_for_finalization: bool = True,
508
502
  prompt: bool = True,
@@ -589,8 +583,8 @@ async def revoke_children(
589
583
  netuid: Optional[int] = None,
590
584
  wait_for_inclusion: bool = True,
591
585
  wait_for_finalization: bool = True,
586
+ prompt: bool = True,
592
587
  ):
593
- # TODO seek clarification on use of asking hotkey vs how we do it now
594
588
  """
595
589
  Revokes the children hotkeys associated with a given network identifier (netuid).
596
590
  """
@@ -601,7 +595,7 @@ async def revoke_children(
601
595
  netuid=netuid,
602
596
  hotkey=wallet.hotkey.ss58_address,
603
597
  children_with_proportions=[],
604
- prompt=True,
598
+ prompt=prompt,
605
599
  wait_for_inclusion=wait_for_inclusion,
606
600
  wait_for_finalization=wait_for_finalization,
607
601
  )
@@ -630,7 +624,7 @@ async def revoke_children(
630
624
  netuid=netuid,
631
625
  hotkey=wallet.hotkey.ss58_address,
632
626
  children_with_proportions=[],
633
- prompt=False,
627
+ prompt=prompt,
634
628
  wait_for_inclusion=True,
635
629
  wait_for_finalization=False,
636
630
  )
@@ -790,7 +784,7 @@ async def childkey_take(
790
784
  netuid=netuid,
791
785
  hotkey=wallet.hotkey.ss58_address,
792
786
  take=take,
793
- prompt=False,
787
+ prompt=prompt,
794
788
  wait_for_inclusion=True,
795
789
  wait_for_finalization=False,
796
790
  )