bittensor-cli 9.0.0rc4__py3-none-any.whl → 9.0.1__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"
@@ -108,14 +108,14 @@ async def stake_add(
108
108
  return
109
109
  else:
110
110
  err_out(
111
- f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}"
111
+ f"\n{failure_prelude} with error: {format_error_message(e)}"
112
112
  )
113
113
  return
114
114
  else:
115
115
  await response.process_events()
116
116
  if not await response.is_success:
117
117
  err_out(
118
- f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}"
118
+ f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}"
119
119
  )
120
120
  else:
121
121
  block_hash = await subtensor.substrate.get_chain_head()
@@ -181,14 +181,14 @@ async def stake_add(
181
181
  )
182
182
  except SubstrateRequestException as e:
183
183
  err_out(
184
- f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}"
184
+ f"\n{failure_prelude} with error: {format_error_message(e)}"
185
185
  )
186
186
  return
187
187
  else:
188
188
  await response.process_events()
189
189
  if not await response.is_success:
190
190
  err_out(
191
- f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}"
191
+ f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}"
192
192
  )
193
193
  else:
194
194
  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
 
@@ -263,9 +259,7 @@ async def get_childkey_take(subtensor, hotkey: str, netuid: int) -> Optional[int
263
259
  return int(childkey_take_.value)
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
  )
@@ -45,7 +45,7 @@ async def stake_list(
45
45
  coldkey_ss58=coldkey_address, block_hash=block_hash
46
46
  ),
47
47
  subtensor.get_delegate_identities(block_hash=block_hash),
48
- subtensor.all_subnets(),
48
+ subtensor.all_subnets(block_hash=block_hash),
49
49
  )
50
50
  # sub_stakes = substakes[coldkey_address]
51
51
  dynamic_info = {info.netuid: info for info in _dynamic_info}
@@ -199,7 +199,7 @@ async def stake_list(
199
199
  issuance = pool.alpha_out if pool.is_dynamic else tao_locked
200
200
 
201
201
  # Per block emission cell
202
- per_block_emission = substake_.emission.tao / pool.tempo
202
+ per_block_emission = substake_.emission.tao / (pool.tempo or 1)
203
203
  # Alpha ownership and TAO ownership cells
204
204
  if alpha_value.tao > 0.00009:
205
205
  if issuance.tao != 0:
@@ -319,7 +319,7 @@ async def stake_list(
319
319
  alpha_value = Balance.from_rao(int(substake.stake.rao)).set_unit(netuid)
320
320
  tao_value = pool.alpha_to_tao(alpha_value)
321
321
  total_tao_value += tao_value
322
- swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage(
322
+ swapped_tao_value, slippage, slippage_pct = pool.alpha_to_tao_with_slippage(
323
323
  substake.stake
324
324
  )
325
325
  total_swapped_tao_value += swapped_tao_value
@@ -341,7 +341,7 @@ async def stake_list(
341
341
  "price": pool.price.tao,
342
342
  "tao_value": tao_value.tao,
343
343
  "swapped_value": swapped_tao_value.tao,
344
- "emission": substake.emission.tao / pool.tempo,
344
+ "emission": substake.emission.tao / (pool.tempo or 1),
345
345
  "tao_ownership": tao_ownership.tao,
346
346
  }
347
347
 
@@ -376,15 +376,6 @@ async def stake_list(
376
376
  millify=True if not verbose else False,
377
377
  )
378
378
 
379
- if pool.is_dynamic:
380
- slippage_pct = (
381
- 100 * float(slippage) / float(slippage + swapped_tao_value)
382
- if slippage + swapped_tao_value != 0
383
- else 0
384
- )
385
- else:
386
- slippage_pct = 0
387
-
388
379
  if netuid != 0:
389
380
  swap_cell = (
390
381
  format_cell(
@@ -400,7 +391,7 @@ async def stake_list(
400
391
  else:
401
392
  swap_cell = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}] ({slippage_pct}%)"
402
393
 
403
- emission_value = substake.emission.tao / pool.tempo
394
+ emission_value = substake.emission.tao / (pool.tempo or 1)
404
395
  emission_cell = format_cell(
405
396
  emission_value,
406
397
  prev.get("emission"),
@@ -443,11 +434,12 @@ async def stake_list(
443
434
  return table, current_data
444
435
 
445
436
  # Main execution
437
+ block_hash = await subtensor.substrate.get_chain_head()
446
438
  (
447
439
  sub_stakes,
448
440
  registered_delegate_info,
449
441
  dynamic_info,
450
- ) = await get_stake_data()
442
+ ) = await get_stake_data(block_hash)
451
443
  balance = await subtensor.get_balance(coldkey_address)
452
444
 
453
445
  # Iterate over substakes and aggregate them by hotkey.
@@ -536,7 +528,7 @@ async def stake_list(
536
528
  table, current_data = create_live_table(
537
529
  selected_stakes,
538
530
  registered_delegate_info,
539
- dynamic_info,
531
+ dynamic_info_,
540
532
  hotkey_name,
541
533
  previous_data,
542
534
  )
@@ -812,7 +812,7 @@ async def transfer_stake(
812
812
  if not await response.is_success:
813
813
  err_console.print(
814
814
  f":cross_mark: [red]Failed[/red] with error: "
815
- f"{format_error_message(await response.error_message, subtensor.substrate)}"
815
+ f"{format_error_message(await response.error_message)}"
816
816
  )
817
817
  return False
818
818
 
@@ -971,7 +971,7 @@ async def swap_stake(
971
971
  if not await response.is_success:
972
972
  err_console.print(
973
973
  f":cross_mark: [red]Failed[/red] with error: "
974
- f"{format_error_message(await response.error_message, subtensor.substrate)}"
974
+ f"{format_error_message(await response.error_message)}"
975
975
  )
976
976
  return False
977
977
 
@@ -561,7 +561,7 @@ async def _unstake_extrinsic(
561
561
  if not await response.is_success:
562
562
  err_out(
563
563
  f"{failure_prelude} with error: "
564
- f"{format_error_message(await response.error_message, subtensor.substrate)}"
564
+ f"{format_error_message(await response.error_message)}"
565
565
  )
566
566
  return
567
567
 
@@ -667,14 +667,14 @@ async def _safe_unstake_extrinsic(
667
667
  return
668
668
  else:
669
669
  err_out(
670
- f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}"
670
+ f"\n{failure_prelude} with error: {format_error_message(e)}"
671
671
  )
672
672
  return
673
673
 
674
674
  await response.process_events()
675
675
  if not await response.is_success:
676
676
  err_out(
677
- f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}"
677
+ f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}"
678
678
  )
679
679
  return
680
680