bittensor-cli 9.2.0__py3-none-any.whl → 9.3.0__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,5 +1,6 @@
1
1
  import asyncio
2
2
  import itertools
3
+ import json
3
4
  import os
4
5
  from collections import defaultdict
5
6
  from typing import Generator, Optional
@@ -14,8 +15,9 @@ from rich.align import Align
14
15
  from rich.table import Column, Table
15
16
  from rich.tree import Tree
16
17
  from rich.padding import Padding
18
+ from rich.prompt import Confirm
17
19
 
18
- from bittensor_cli.src import COLOR_PALETTE
20
+ from bittensor_cli.src import COLOR_PALETTE, COLORS, Constants
19
21
  from bittensor_cli.src.bittensor import utils
20
22
  from bittensor_cli.src.bittensor.balances import Balance
21
23
  from bittensor_cli.src.bittensor.chain_data import (
@@ -34,6 +36,7 @@ from bittensor_cli.src.bittensor.utils import (
34
36
  console,
35
37
  convert_blocks_to_time,
36
38
  err_console,
39
+ json_console,
37
40
  print_error,
38
41
  print_verbose,
39
42
  get_all_wallets_for_path,
@@ -44,9 +47,78 @@ from bittensor_cli.src.bittensor.utils import (
44
47
  millify_tao,
45
48
  unlock_key,
46
49
  WalletLike,
50
+ blocks_to_duration,
51
+ decode_account_id,
47
52
  )
48
53
 
49
54
 
55
+ async def associate_hotkey(
56
+ wallet: Wallet,
57
+ subtensor: SubtensorInterface,
58
+ hotkey_ss58: str,
59
+ hotkey_display: str,
60
+ prompt: bool = False,
61
+ ):
62
+ """Associates a hotkey with a wallet"""
63
+
64
+ owner_ss58 = await subtensor.get_hotkey_owner(hotkey_ss58)
65
+ if owner_ss58:
66
+ if owner_ss58 == wallet.coldkeypub.ss58_address:
67
+ console.print(
68
+ f":white_heavy_check_mark: {hotkey_display.capitalize()} is already "
69
+ f"associated with \nwallet [blue]{wallet.name}[/blue], "
70
+ f"SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]"
71
+ )
72
+ return True
73
+ else:
74
+ owner_wallet = _get_wallet_by_ss58(wallet.path, owner_ss58)
75
+ wallet_name = owner_wallet.name if owner_wallet else "unknown wallet"
76
+ console.print(
77
+ f"[yellow]Warning[/yellow]: {hotkey_display.capitalize()} is already associated with \n"
78
+ f"wallet: [blue]{wallet_name}[/blue], SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]"
79
+ )
80
+ return False
81
+ else:
82
+ console.print(
83
+ f"{hotkey_display.capitalize()} is not associated with any wallet"
84
+ )
85
+
86
+ if prompt and not Confirm.ask("Do you want to continue with the association?"):
87
+ return False
88
+
89
+ if not unlock_key(wallet).success:
90
+ return False
91
+
92
+ call = await subtensor.substrate.compose_call(
93
+ call_module="SubtensorModule",
94
+ call_function="try_associate_hotkey",
95
+ call_params={
96
+ "hotkey": hotkey_ss58,
97
+ },
98
+ )
99
+
100
+ with console.status(":satellite: Associating hotkey on-chain..."):
101
+ success, err_msg = await subtensor.sign_and_send_extrinsic(
102
+ call,
103
+ wallet,
104
+ wait_for_inclusion=True,
105
+ wait_for_finalization=False,
106
+ )
107
+
108
+ if not success:
109
+ console.print(
110
+ f"[red]:cross_mark: Failed to associate hotkey: {err_msg}[/red]"
111
+ )
112
+ return False
113
+
114
+ console.print(
115
+ f":white_heavy_check_mark: Successfully associated {hotkey_display} with \n"
116
+ f"wallet [blue]{wallet.name}[/blue], "
117
+ f"SS58: [{COLORS.GENERAL.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.GENERAL.CK}]"
118
+ )
119
+ return True
120
+
121
+
50
122
  async def regen_coldkey(
51
123
  wallet: Wallet,
52
124
  mnemonic: Optional[str],
@@ -55,6 +127,7 @@ async def regen_coldkey(
55
127
  json_password: Optional[str] = "",
56
128
  use_password: Optional[bool] = True,
57
129
  overwrite: Optional[bool] = False,
130
+ json_output: bool = False,
58
131
  ):
59
132
  """Creates a new coldkey under this wallet"""
60
133
  json_str: Optional[str] = None
@@ -71,16 +144,41 @@ async def regen_coldkey(
71
144
  use_password=use_password,
72
145
  overwrite=overwrite,
73
146
  )
74
-
75
147
  if isinstance(new_wallet, Wallet):
76
148
  console.print(
77
149
  "\n✅ [dark_sea_green]Regenerated coldkey successfully!\n",
78
- f"[dark_sea_green]Wallet name: ({new_wallet.name}), path: ({new_wallet.path}), coldkey ss58: ({new_wallet.coldkeypub.ss58_address})",
150
+ f"[dark_sea_green]Wallet name: ({new_wallet.name}), "
151
+ f"path: ({new_wallet.path}), "
152
+ f"coldkey ss58: ({new_wallet.coldkeypub.ss58_address})",
79
153
  )
154
+ if json_output:
155
+ json_console.print(
156
+ json.dumps(
157
+ {
158
+ "success": True,
159
+ "data": {
160
+ "name": new_wallet.name,
161
+ "path": new_wallet.path,
162
+ "hotkey": new_wallet.hotkey_str,
163
+ "hotkey_ss58": new_wallet.hotkey.ss58_address,
164
+ "coldkey_ss58": new_wallet.coldkeypub.ss58_address,
165
+ },
166
+ "error": "",
167
+ }
168
+ )
169
+ )
80
170
  except ValueError:
81
171
  print_error("Mnemonic phrase is invalid")
172
+ if json_output:
173
+ json_console.print(
174
+ '{"success": false, "error": "Mnemonic phrase is invalid", "data": null}'
175
+ )
82
176
  except KeyFileError:
83
177
  print_error("KeyFileError: File is not writable")
178
+ if json_output:
179
+ json_console.print(
180
+ '{"success": false, "error": "Keyfile is not writable", "data": null}'
181
+ )
84
182
 
85
183
 
86
184
  async def regen_coldkey_pub(
@@ -88,6 +186,7 @@ async def regen_coldkey_pub(
88
186
  ss58_address: str,
89
187
  public_key_hex: str,
90
188
  overwrite: Optional[bool] = False,
189
+ json_output: bool = False,
91
190
  ):
92
191
  """Creates a new coldkeypub under this wallet."""
93
192
  try:
@@ -99,10 +198,31 @@ async def regen_coldkey_pub(
99
198
  if isinstance(new_coldkeypub, Wallet):
100
199
  console.print(
101
200
  "\n✅ [dark_sea_green]Regenerated coldkeypub successfully!\n",
102
- f"[dark_sea_green]Wallet name: ({new_coldkeypub.name}), path: ({new_coldkeypub.path}), coldkey ss58: ({new_coldkeypub.coldkeypub.ss58_address})",
201
+ f"[dark_sea_green]Wallet name: ({new_coldkeypub.name}), path: ({new_coldkeypub.path}), "
202
+ f"coldkey ss58: ({new_coldkeypub.coldkeypub.ss58_address})",
103
203
  )
204
+ if json_output:
205
+ json_console.print(
206
+ json.dumps(
207
+ {
208
+ "success": True,
209
+ "data": {
210
+ "name": new_coldkeypub.name,
211
+ "path": new_coldkeypub.path,
212
+ "hotkey": new_coldkeypub.hotkey_str,
213
+ "hotkey_ss58": new_coldkeypub.hotkey.ss58_address,
214
+ "coldkey_ss58": new_coldkeypub.coldkeypub.ss58_address,
215
+ },
216
+ "error": "",
217
+ }
218
+ )
219
+ )
104
220
  except KeyFileError:
105
221
  print_error("KeyFileError: File is not writable")
222
+ if json_output:
223
+ json_console.print(
224
+ '{"success": false, "error": "Keyfile is not writable", "data": null}'
225
+ )
106
226
 
107
227
 
108
228
  async def regen_hotkey(
@@ -113,6 +233,7 @@ async def regen_hotkey(
113
233
  json_password: Optional[str] = "",
114
234
  use_password: Optional[bool] = False,
115
235
  overwrite: Optional[bool] = False,
236
+ json_output: bool = False,
116
237
  ):
117
238
  """Creates a new hotkey under this wallet."""
118
239
  json_str: Optional[str] = None
@@ -134,13 +255,37 @@ async def regen_hotkey(
134
255
  if isinstance(new_hotkey_, Wallet):
135
256
  console.print(
136
257
  "\n✅ [dark_sea_green]Regenerated hotkey successfully!\n",
137
- f"[dark_sea_green]Wallet name: "
138
- f"({new_hotkey_.name}), path: ({new_hotkey_.path}), hotkey ss58: ({new_hotkey_.hotkey.ss58_address})",
258
+ f"[dark_sea_green]Wallet name: ({new_hotkey_.name}), path: ({new_hotkey_.path}), "
259
+ f"hotkey ss58: ({new_hotkey_.hotkey.ss58_address})",
139
260
  )
261
+ if json_output:
262
+ json_console.print(
263
+ json.dumps(
264
+ {
265
+ "success": True,
266
+ "data": {
267
+ "name": new_hotkey_.name,
268
+ "path": new_hotkey_.path,
269
+ "hotkey": new_hotkey_.hotkey_str,
270
+ "hotkey_ss58": new_hotkey_.hotkey.ss58_address,
271
+ "coldkey_ss58": new_hotkey_.coldkeypub.ss58_address,
272
+ },
273
+ "error": "",
274
+ }
275
+ )
276
+ )
140
277
  except ValueError:
141
278
  print_error("Mnemonic phrase is invalid")
279
+ if json_output:
280
+ json_console.print(
281
+ '{"success": false, "error": "Mnemonic phrase is invalid", "data": null}'
282
+ )
142
283
  except KeyFileError:
143
284
  print_error("KeyFileError: File is not writable")
285
+ if json_output:
286
+ json_console.print(
287
+ '{"success": false, "error": "Keyfile is not writable", "data": null}'
288
+ )
144
289
 
145
290
 
146
291
  async def new_hotkey(
@@ -149,6 +294,7 @@ async def new_hotkey(
149
294
  use_password: bool,
150
295
  uri: Optional[str] = None,
151
296
  overwrite: Optional[bool] = False,
297
+ json_output: bool = False,
152
298
  ):
153
299
  """Creates a new hotkey under this wallet."""
154
300
  try:
@@ -157,6 +303,7 @@ async def new_hotkey(
157
303
  keypair = Keypair.create_from_uri(uri)
158
304
  except Exception as e:
159
305
  print_error(f"Failed to create keypair from URI {uri}: {str(e)}")
306
+ return
160
307
  wallet.set_hotkey(keypair=keypair, encrypt=use_password)
161
308
  console.print(
162
309
  f"[dark_sea_green]Hotkey created from URI: {uri}[/dark_sea_green]"
@@ -168,8 +315,28 @@ async def new_hotkey(
168
315
  overwrite=overwrite,
169
316
  )
170
317
  console.print("[dark_sea_green]Hotkey created[/dark_sea_green]")
318
+ if json_output:
319
+ json_console.print(
320
+ json.dumps(
321
+ {
322
+ "success": True,
323
+ "data": {
324
+ "name": wallet.name,
325
+ "path": wallet.path,
326
+ "hotkey": wallet.hotkey_str,
327
+ "hotkey_ss58": wallet.hotkey.ss58_address,
328
+ "coldkey_ss58": wallet.coldkeypub.ss58_address,
329
+ },
330
+ "error": "",
331
+ }
332
+ )
333
+ )
171
334
  except KeyFileError:
172
335
  print_error("KeyFileError: File is not writable")
336
+ if json_output:
337
+ json_console.print(
338
+ '{"success": false, "error": "Keyfile is not writable", "data": null}'
339
+ )
173
340
 
174
341
 
175
342
  async def new_coldkey(
@@ -178,6 +345,7 @@ async def new_coldkey(
178
345
  use_password: bool,
179
346
  uri: Optional[str] = None,
180
347
  overwrite: Optional[bool] = False,
348
+ json_output: bool = False,
181
349
  ):
182
350
  """Creates a new coldkey under this wallet."""
183
351
  try:
@@ -198,8 +366,32 @@ async def new_coldkey(
198
366
  overwrite=overwrite,
199
367
  )
200
368
  console.print("[dark_sea_green]Coldkey created[/dark_sea_green]")
201
- except KeyFileError:
369
+ if json_output:
370
+ json_console.print(
371
+ json.dumps(
372
+ {
373
+ "success": True,
374
+ "data": {
375
+ "name": wallet.name,
376
+ "path": wallet.path,
377
+ "coldkey_ss58": wallet.coldkeypub.ss58_address,
378
+ },
379
+ "error": "",
380
+ }
381
+ )
382
+ )
383
+ except KeyFileError as e:
202
384
  print_error("KeyFileError: File is not writable")
385
+ if json_output:
386
+ json_console.print(
387
+ json.dumps(
388
+ {
389
+ "success": False,
390
+ "error": f"Keyfile is not writable: {e}",
391
+ "data": None,
392
+ }
393
+ )
394
+ )
203
395
 
204
396
 
205
397
  async def wallet_create(
@@ -208,16 +400,28 @@ async def wallet_create(
208
400
  use_password: bool = True,
209
401
  uri: Optional[str] = None,
210
402
  overwrite: Optional[bool] = False,
403
+ json_output: bool = False,
211
404
  ):
212
405
  """Creates a new wallet."""
406
+ output_dict = {"success": False, "error": "", "data": None}
213
407
  if uri:
214
408
  try:
215
409
  keypair = Keypair.create_from_uri(uri)
216
410
  wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False)
217
411
  wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False)
218
412
  wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=False)
413
+ output_dict["success"] = True
414
+ output_dict["data"] = {
415
+ "name": wallet.name,
416
+ "path": wallet.path,
417
+ "hotkey": wallet.hotkey_str,
418
+ "hotkey_ss58": wallet.hotkey.ss58_address,
419
+ "coldkey_ss58": wallet.coldkeypub.ss58_address,
420
+ }
219
421
  except Exception as e:
220
- print_error(f"Failed to create keypair from URI: {str(e)}")
422
+ err = f"Failed to create keypair from URI: {str(e)}"
423
+ print_error(err)
424
+ output_dict["error"] = err
221
425
  console.print(
222
426
  f"[dark_sea_green]Wallet created from URI: {uri}[/dark_sea_green]"
223
427
  )
@@ -229,9 +433,18 @@ async def wallet_create(
229
433
  overwrite=overwrite,
230
434
  )
231
435
  console.print("[dark_sea_green]Coldkey created[/dark_sea_green]")
436
+ output_dict["success"] = True
437
+ output_dict["data"] = {
438
+ "name": wallet.name,
439
+ "path": wallet.path,
440
+ "hotkey": wallet.hotkey_str,
441
+ "hotkey_ss58": wallet.hotkey.ss58_address,
442
+ "coldkey_ss58": wallet.coldkeypub.ss58_address,
443
+ }
232
444
  except KeyFileError:
233
- print_error("KeyFileError: File is not writable")
234
-
445
+ err = "KeyFileError: File is not writable"
446
+ print_error(err)
447
+ output_dict["error"] = err
235
448
  try:
236
449
  wallet.create_new_hotkey(
237
450
  n_words=n_words,
@@ -239,8 +452,20 @@ async def wallet_create(
239
452
  overwrite=overwrite,
240
453
  )
241
454
  console.print("[dark_sea_green]Hotkey created[/dark_sea_green]")
455
+ output_dict["success"] = True
456
+ output_dict["data"] = {
457
+ "name": wallet.name,
458
+ "path": wallet.path,
459
+ "hotkey": wallet.hotkey_str,
460
+ "hotkey_ss58": wallet.hotkey.ss58_address,
461
+ "coldkey_ss58": wallet.coldkeypub.ss58_address,
462
+ }
242
463
  except KeyFileError:
243
- print_error("KeyFileError: File is not writable")
464
+ err = "KeyFileError: File is not writable"
465
+ print_error(err)
466
+ output_dict["error"] = err
467
+ if json_output:
468
+ json_console.print(json.dumps(output_dict))
244
469
 
245
470
 
246
471
  def get_coldkey_wallets_for_path(path: str) -> list[Wallet]:
@@ -254,6 +479,15 @@ def get_coldkey_wallets_for_path(path: str) -> list[Wallet]:
254
479
  return wallets
255
480
 
256
481
 
482
+ def _get_wallet_by_ss58(path: str, ss58_address: str) -> Optional[Wallet]:
483
+ """Find a wallet by its SS58 address in the given path."""
484
+ ss58_addresses, wallet_names = _get_coldkey_ss58_addresses_for_path(path)
485
+ for wallet_name, addr in zip(wallet_names, ss58_addresses):
486
+ if addr == ss58_address:
487
+ return Wallet(path=path, name=wallet_name)
488
+ return None
489
+
490
+
257
491
  def _get_coldkey_ss58_addresses_for_path(path: str) -> tuple[list[str], list[str]]:
258
492
  """Get all coldkey ss58 addresses from path."""
259
493
 
@@ -280,6 +514,7 @@ async def wallet_balance(
280
514
  subtensor: SubtensorInterface,
281
515
  all_balances: bool,
282
516
  ss58_addresses: Optional[str] = None,
517
+ json_output: bool = False,
283
518
  ):
284
519
  """Retrieves the current balance of the specified wallet"""
285
520
  if ss58_addresses:
@@ -395,6 +630,31 @@ async def wallet_balance(
395
630
  )
396
631
  console.print(Padding(table, (0, 0, 0, 4)))
397
632
  await subtensor.substrate.close()
633
+ if json_output:
634
+ output_balances = {
635
+ key: {
636
+ "coldkey": value[0],
637
+ "free": value[1].tao,
638
+ "staked": value[2].tao,
639
+ "staked_with_slippage": value[3].tao,
640
+ "total": (value[1] + value[2]).tao,
641
+ "total_with_slippage": (value[1] + value[3]).tao,
642
+ }
643
+ for (key, value) in balances.items()
644
+ }
645
+ output_dict = {
646
+ "balances": output_balances,
647
+ "totals": {
648
+ "free": total_free_balance.tao,
649
+ "staked": total_staked_balance.tao,
650
+ "staked_with_slippage": total_staked_with_slippage.tao,
651
+ "total": (total_free_balance + total_staked_balance).tao,
652
+ "total_with_slippage": (
653
+ total_free_balance + total_staked_with_slippage
654
+ ).tao,
655
+ },
656
+ }
657
+ json_console.print(json.dumps(output_dict))
398
658
  return total_free_balance
399
659
 
400
660
 
@@ -526,7 +786,7 @@ async def wallet_history(wallet: Wallet):
526
786
  console.print(table)
527
787
 
528
788
 
529
- async def wallet_list(wallet_path: str):
789
+ async def wallet_list(wallet_path: str, json_output: bool):
530
790
  """Lists wallets."""
531
791
  wallets = utils.get_coldkey_wallets_for_path(wallet_path)
532
792
  print_verbose(f"Using wallets path: {wallet_path}")
@@ -534,6 +794,7 @@ async def wallet_list(wallet_path: str):
534
794
  err_console.print(f"[red]No wallets found in dir: {wallet_path}[/red]")
535
795
 
536
796
  root = Tree("Wallets")
797
+ main_data_dict = {"wallets": []}
537
798
  for wallet in wallets:
538
799
  if (
539
800
  wallet.coldkeypub_file.exists_on_device()
@@ -546,23 +807,39 @@ async def wallet_list(wallet_path: str):
546
807
  wallet_tree = root.add(
547
808
  f"[bold blue]Coldkey[/bold blue] [green]{wallet.name}[/green] ss58_address [green]{coldkeypub_str}[/green]"
548
809
  )
810
+ wallet_hotkeys = []
811
+ wallet_dict = {
812
+ "name": wallet.name,
813
+ "ss58_address": coldkeypub_str,
814
+ "hotkeys": wallet_hotkeys,
815
+ }
816
+ main_data_dict["wallets"].append(wallet_dict)
549
817
  hotkeys = utils.get_hotkey_wallets_for_wallet(
550
818
  wallet, show_nulls=True, show_encrypted=True
551
819
  )
552
820
  for hkey in hotkeys:
553
821
  data = f"[bold red]Hotkey[/bold red][green] {hkey}[/green] (?)"
822
+ hk_data = {"name": hkey.name, "ss58_address": "?"}
554
823
  if hkey:
555
824
  try:
556
- data = f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] ss58_address [green]{hkey.hotkey.ss58_address}[/green]\n"
825
+ data = (
826
+ f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] "
827
+ f"ss58_address [green]{hkey.hotkey.ss58_address}[/green]\n"
828
+ )
829
+ hk_data["name"] = hkey.hotkey_str
830
+ hk_data["ss58_address"] = hkey.hotkey.ss58_address
557
831
  except UnicodeDecodeError:
558
832
  pass
559
833
  wallet_tree.add(data)
834
+ wallet_hotkeys.append(hk_data)
560
835
 
561
836
  if not wallets:
562
837
  print_verbose(f"No wallets found in path: {wallet_path}")
563
838
  root.add("[bold red]No wallets found.")
564
-
565
- console.print(root)
839
+ if json_output:
840
+ json_console.print(json.dumps(main_data_dict))
841
+ else:
842
+ console.print(root)
566
843
 
567
844
 
568
845
  async def _get_total_balance(
@@ -639,23 +916,33 @@ async def overview(
639
916
  exclude_hotkeys: Optional[list[str]] = None,
640
917
  netuids_filter: Optional[list[int]] = None,
641
918
  verbose: bool = False,
919
+ json_output: bool = False,
642
920
  ):
643
921
  """Prints an overview for the wallet's coldkey."""
644
922
 
645
923
  total_balance = Balance(0)
646
924
 
647
- # We are printing for every coldkey.
648
- block_hash = await subtensor.substrate.get_chain_head()
649
- all_hotkeys, total_balance = await _get_total_balance(
650
- total_balance, subtensor, wallet, all_wallets, block_hash=block_hash
651
- )
652
- _dynamic_info = await subtensor.all_subnets()
653
- dynamic_info = {info.netuid: info for info in _dynamic_info}
654
-
655
925
  with console.status(
656
926
  f":satellite: Synchronizing with chain [white]{subtensor.network}[/white]",
657
927
  spinner="aesthetic",
658
928
  ) as status:
929
+ # We are printing for every coldkey.
930
+ block_hash = await subtensor.substrate.get_chain_head()
931
+ (
932
+ (all_hotkeys, total_balance),
933
+ _dynamic_info,
934
+ block,
935
+ all_netuids,
936
+ ) = await asyncio.gather(
937
+ _get_total_balance(
938
+ total_balance, subtensor, wallet, all_wallets, block_hash=block_hash
939
+ ),
940
+ subtensor.all_subnets(block_hash=block_hash),
941
+ subtensor.substrate.get_block_number(block_hash=block_hash),
942
+ subtensor.get_all_subnet_netuids(block_hash=block_hash),
943
+ )
944
+ dynamic_info = {info.netuid: info for info in _dynamic_info}
945
+
659
946
  # We are printing for a select number of hotkeys from all_hotkeys.
660
947
  if include_hotkeys or exclude_hotkeys:
661
948
  all_hotkeys = _get_hotkeys(include_hotkeys, exclude_hotkeys, all_hotkeys)
@@ -667,10 +954,6 @@ async def overview(
667
954
 
668
955
  # Pull neuron info for all keys.
669
956
  neurons: dict[str, list[NeuronInfoLite]] = {}
670
- block, all_netuids = await asyncio.gather(
671
- subtensor.substrate.get_block_number(None),
672
- subtensor.get_all_subnet_netuids(),
673
- )
674
957
 
675
958
  netuids = await subtensor.filter_netuids_by_registered_hotkeys(
676
959
  all_netuids, netuids_filter, all_hotkeys, reuse_block=True
@@ -705,16 +988,27 @@ async def overview(
705
988
  neurons = _process_neuron_results(results, neurons, netuids)
706
989
  # Setup outer table.
707
990
  grid = Table.grid(pad_edge=True)
991
+ data_dict = {
992
+ "wallet": "",
993
+ "network": subtensor.network,
994
+ "subnets": [],
995
+ "total_balance": 0.0,
996
+ }
708
997
 
709
998
  # Add title
710
999
  if not all_wallets:
711
1000
  title = "[underline dark_orange]Wallet[/underline dark_orange]\n"
712
- details = f"[bright_cyan]{wallet.name}[/bright_cyan] : [bright_magenta]{wallet.coldkeypub.ss58_address}[/bright_magenta]"
1001
+ details = (
1002
+ f"[bright_cyan]{wallet.name}[/bright_cyan] : "
1003
+ f"[bright_magenta]{wallet.coldkeypub.ss58_address}[/bright_magenta]"
1004
+ )
713
1005
  grid.add_row(Align(title, vertical="middle", align="center"))
714
1006
  grid.add_row(Align(details, vertical="middle", align="center"))
1007
+ data_dict["wallet"] = f"{wallet.name}|{wallet.coldkeypub.ss58_address}"
715
1008
  else:
716
1009
  title = "[underline dark_orange]All Wallets:[/underline dark_orange]"
717
1010
  grid.add_row(Align(title, vertical="middle", align="center"))
1011
+ data_dict["wallet"] = "All"
718
1012
 
719
1013
  grid.add_row(
720
1014
  Align(
@@ -732,6 +1026,14 @@ async def overview(
732
1026
  )
733
1027
  for netuid, subnet_tempo in zip(netuids, tempos):
734
1028
  table_data = []
1029
+ subnet_dict = {
1030
+ "netuid": netuid,
1031
+ "tempo": subnet_tempo,
1032
+ "neurons": [],
1033
+ "name": "",
1034
+ "symbol": "",
1035
+ }
1036
+ data_dict["subnets"].append(subnet_dict)
735
1037
  total_rank = 0.0
736
1038
  total_trust = 0.0
737
1039
  total_consensus = 0.0
@@ -786,6 +1088,26 @@ async def overview(
786
1088
  ),
787
1089
  nn.hotkey[:10],
788
1090
  ]
1091
+ neuron_dict = {
1092
+ "coldkey": hotwallet.name,
1093
+ "hotkey": hotwallet.hotkey_str,
1094
+ "uid": uid,
1095
+ "active": active,
1096
+ "stake": stake,
1097
+ "rank": rank,
1098
+ "trust": trust,
1099
+ "consensus": consensus,
1100
+ "incentive": incentive,
1101
+ "dividends": dividends,
1102
+ "emission": emission,
1103
+ "validator_trust": validator_trust,
1104
+ "validator_permit": validator_permit,
1105
+ "last_update": last_update,
1106
+ "axon": int_to_ip(nn.axon_info.ip) + ":" + str(nn.axon_info.port)
1107
+ if nn.axon_info.port != 0
1108
+ else None,
1109
+ "hotkey_ss58": nn.hotkey,
1110
+ }
789
1111
 
790
1112
  total_rank += rank
791
1113
  total_trust += trust
@@ -798,11 +1120,16 @@ async def overview(
798
1120
  total_neurons += 1
799
1121
 
800
1122
  table_data.append(row)
1123
+ subnet_dict["neurons"].append(neuron_dict)
801
1124
 
802
1125
  # Add subnet header
1126
+ sn_name = get_subnet_name(dynamic_info[netuid])
1127
+ sn_symbol = dynamic_info[netuid].symbol
803
1128
  grid.add_row(
804
- f"Subnet: [dark_orange]{netuid}: {get_subnet_name(dynamic_info[netuid])} {dynamic_info[netuid].symbol}[/dark_orange]"
1129
+ f"Subnet: [dark_orange]{netuid}: {sn_name} {sn_symbol}[/dark_orange]"
805
1130
  )
1131
+ subnet_dict["name"] = sn_name
1132
+ subnet_dict["symbol"] = sn_symbol
806
1133
  width = console.width
807
1134
  table = Table(
808
1135
  show_footer=False,
@@ -937,6 +1264,7 @@ async def overview(
937
1264
  caption = "\n[italic][dim][bright_cyan]Wallet balance: [dark_orange]\u03c4" + str(
938
1265
  total_balance.tao
939
1266
  )
1267
+ data_dict["total_balance"] = total_balance.tao
940
1268
  grid.add_row(Align(caption, vertical="middle", align="center"))
941
1269
 
942
1270
  if console.width < 150:
@@ -944,7 +1272,10 @@ async def overview(
944
1272
  "[yellow]Warning: Your terminal width might be too small to view all information clearly"
945
1273
  )
946
1274
  # Print the entire table/grid
947
- console.print(grid, width=None)
1275
+ if not json_output:
1276
+ console.print(grid, width=None)
1277
+ else:
1278
+ json_console.print(json.dumps(data_dict))
948
1279
 
949
1280
 
950
1281
  def _get_hotkeys(
@@ -1109,17 +1440,23 @@ async def transfer(
1109
1440
  destination: str,
1110
1441
  amount: float,
1111
1442
  transfer_all: bool,
1443
+ era: int,
1112
1444
  prompt: bool,
1445
+ json_output: bool,
1113
1446
  ):
1114
1447
  """Transfer token of amount to destination."""
1115
- await transfer_extrinsic(
1448
+ result = await transfer_extrinsic(
1116
1449
  subtensor=subtensor,
1117
1450
  wallet=wallet,
1118
1451
  destination=destination,
1119
1452
  amount=Balance.from_tao(amount),
1120
1453
  transfer_all=transfer_all,
1454
+ era=era,
1121
1455
  prompt=prompt,
1122
1456
  )
1457
+ if json_output:
1458
+ json_console.print(json.dumps({"success": result}))
1459
+ return result
1123
1460
 
1124
1461
 
1125
1462
  async def inspect(
@@ -1128,6 +1465,7 @@ async def inspect(
1128
1465
  netuids_filter: list[int],
1129
1466
  all_wallets: bool = False,
1130
1467
  ):
1468
+ # TODO add json_output when this is re-enabled and updated for dTAO
1131
1469
  def delegate_row_maker(
1132
1470
  delegates_: list[tuple[DelegateInfo, Balance]],
1133
1471
  ) -> Generator[list[str], None, None]:
@@ -1303,14 +1641,18 @@ async def swap_hotkey(
1303
1641
  new_wallet: Wallet,
1304
1642
  subtensor: SubtensorInterface,
1305
1643
  prompt: bool,
1644
+ json_output: bool,
1306
1645
  ):
1307
1646
  """Swap your hotkey for all registered axons on the network."""
1308
- return await swap_hotkey_extrinsic(
1647
+ result = await swap_hotkey_extrinsic(
1309
1648
  subtensor,
1310
1649
  original_wallet,
1311
1650
  new_wallet,
1312
1651
  prompt=prompt,
1313
1652
  )
1653
+ if json_output:
1654
+ json_console.print(json.dumps({"success": result}))
1655
+ return result
1314
1656
 
1315
1657
 
1316
1658
  def create_identity_table(title: str = None):
@@ -1349,9 +1691,10 @@ async def set_id(
1349
1691
  additional: str,
1350
1692
  github_repo: str,
1351
1693
  prompt: bool,
1694
+ json_output: bool = False,
1352
1695
  ):
1353
1696
  """Create a new or update existing identity on-chain."""
1354
-
1697
+ output_dict = {"success": False, "identity": None, "error": ""}
1355
1698
  identity_data = {
1356
1699
  "name": name.encode(),
1357
1700
  "url": web_url.encode(),
@@ -1378,20 +1721,31 @@ async def set_id(
1378
1721
 
1379
1722
  if not success:
1380
1723
  err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}")
1724
+ output_dict["error"] = err_msg
1725
+ if json_output:
1726
+ json_console.print(json.dumps(output_dict))
1381
1727
  return
1382
-
1383
- console.print(":white_heavy_check_mark: [dark_sea_green3]Success!")
1384
- identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address)
1728
+ else:
1729
+ console.print(":white_heavy_check_mark: [dark_sea_green3]Success!")
1730
+ output_dict["success"] = True
1731
+ identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address)
1385
1732
 
1386
1733
  table = create_identity_table(title="New on-chain Identity")
1387
1734
  table.add_row("Address", wallet.coldkeypub.ss58_address)
1388
1735
  for key, value in identity.items():
1389
1736
  table.add_row(key, str(value) if value else "~")
1390
-
1391
- return console.print(table)
1737
+ output_dict["identity"] = identity
1738
+ console.print(table)
1739
+ if json_output:
1740
+ json_console.print(json.dumps(output_dict))
1392
1741
 
1393
1742
 
1394
- async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = None):
1743
+ async def get_id(
1744
+ subtensor: SubtensorInterface,
1745
+ ss58_address: str,
1746
+ title: str = None,
1747
+ json_output: bool = False,
1748
+ ):
1395
1749
  with console.status(
1396
1750
  ":satellite: [bold green]Querying chain identity...", spinner="earth"
1397
1751
  ):
@@ -1403,6 +1757,8 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str =
1403
1757
  f" for [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]"
1404
1758
  f" on {subtensor}"
1405
1759
  )
1760
+ if json_output:
1761
+ json_console.print("{}")
1406
1762
  return {}
1407
1763
 
1408
1764
  table = create_identity_table(title)
@@ -1411,6 +1767,8 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str =
1411
1767
  table.add_row(key, str(value) if value else "~")
1412
1768
 
1413
1769
  console.print(table)
1770
+ if json_output:
1771
+ json_console.print(json.dumps(identity))
1414
1772
  return identity
1415
1773
 
1416
1774
 
@@ -1451,7 +1809,9 @@ async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface):
1451
1809
  )
1452
1810
 
1453
1811
 
1454
- async def sign(wallet: Wallet, message: str, use_hotkey: str):
1812
+ async def sign(
1813
+ wallet: Wallet, message: str, use_hotkey: str, json_output: bool = False
1814
+ ):
1455
1815
  """Sign a message using the provided wallet or hotkey."""
1456
1816
 
1457
1817
  if not use_hotkey:
@@ -1471,4 +1831,260 @@ async def sign(wallet: Wallet, message: str, use_hotkey: str):
1471
1831
 
1472
1832
  signed_message = keypair.sign(message.encode("utf-8")).hex()
1473
1833
  console.print("[dark_sea_green3]Message signed successfully:")
1834
+ if json_output:
1835
+ json_console.print(json.dumps({"signed_message": signed_message}))
1474
1836
  console.print(signed_message)
1837
+
1838
+
1839
+ async def schedule_coldkey_swap(
1840
+ wallet: Wallet,
1841
+ subtensor: SubtensorInterface,
1842
+ new_coldkey_ss58: str,
1843
+ force_swap: bool = False,
1844
+ ) -> bool:
1845
+ """Schedules a coldkey swap operation to be executed at a future block.
1846
+
1847
+ Args:
1848
+ wallet (Wallet): The wallet initiating the coldkey swap
1849
+ subtensor (SubtensorInterface): Connection to the Bittensor network
1850
+ new_coldkey_ss58 (str): SS58 address of the new coldkey
1851
+ force_swap (bool, optional): Whether to force the swap even if the new coldkey is already scheduled for a swap. Defaults to False.
1852
+ Returns:
1853
+ bool: True if the swap was scheduled successfully, False otherwise
1854
+ """
1855
+ if not is_valid_ss58_address(new_coldkey_ss58):
1856
+ print_error(f"Invalid SS58 address format: {new_coldkey_ss58}")
1857
+ return False
1858
+
1859
+ scheduled_coldkey_swap = await subtensor.get_scheduled_coldkey_swap()
1860
+ if wallet.coldkeypub.ss58_address in scheduled_coldkey_swap:
1861
+ print_error(
1862
+ f"Coldkey {wallet.coldkeypub.ss58_address} is already scheduled for a swap."
1863
+ )
1864
+ console.print("[dim]Use the force_swap (--force) flag to override this.[/dim]")
1865
+ if not force_swap:
1866
+ return False
1867
+ else:
1868
+ console.print(
1869
+ "[yellow]Continuing with the swap due to force_swap flag.[/yellow]\n"
1870
+ )
1871
+
1872
+ prompt = (
1873
+ "You are [red]swapping[/red] your [blue]coldkey[/blue] to a new address.\n"
1874
+ f"Current ss58: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]\n"
1875
+ f"New ss58: [{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]\n"
1876
+ "Are you sure you want to continue?"
1877
+ )
1878
+ if not Confirm.ask(prompt):
1879
+ return False
1880
+
1881
+ if not unlock_key(wallet).success:
1882
+ return False
1883
+
1884
+ block_pre_call, call = await asyncio.gather(
1885
+ subtensor.substrate.get_block_number(),
1886
+ subtensor.substrate.compose_call(
1887
+ call_module="SubtensorModule",
1888
+ call_function="schedule_swap_coldkey",
1889
+ call_params={
1890
+ "new_coldkey": new_coldkey_ss58,
1891
+ },
1892
+ ),
1893
+ )
1894
+
1895
+ with console.status(":satellite: Scheduling coldkey swap on-chain..."):
1896
+ success, err_msg = await subtensor.sign_and_send_extrinsic(
1897
+ call,
1898
+ wallet,
1899
+ wait_for_inclusion=True,
1900
+ wait_for_finalization=True,
1901
+ )
1902
+ block_post_call = await subtensor.substrate.get_block_number()
1903
+
1904
+ if not success:
1905
+ print_error(f"Failed to schedule coldkey swap: {err_msg}")
1906
+ return False
1907
+
1908
+ console.print(
1909
+ ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap"
1910
+ )
1911
+
1912
+ swap_info = await find_coldkey_swap_extrinsic(
1913
+ subtensor=subtensor,
1914
+ start_block=block_pre_call,
1915
+ end_block=block_post_call,
1916
+ wallet_ss58=wallet.coldkeypub.ss58_address,
1917
+ )
1918
+
1919
+ if not swap_info:
1920
+ console.print(
1921
+ "[yellow]Warning: Could not find the swap extrinsic in recent blocks"
1922
+ )
1923
+ return True
1924
+
1925
+ console.print(
1926
+ "\n[green]Coldkey swap details:[/green]"
1927
+ f"\nBlock number: {swap_info['block_num']}"
1928
+ f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]"
1929
+ f"\nDestination address: [{COLORS.G.CK}]{swap_info['dest_coldkey']}[/{COLORS.G.CK}]"
1930
+ f"\nThe swap will be completed at block: [green]{swap_info['execution_block']}[/green]"
1931
+ f"\n[dim]You can provide the block number to `btcli wallet swap-check`[/dim]"
1932
+ )
1933
+
1934
+
1935
+ async def find_coldkey_swap_extrinsic(
1936
+ subtensor: SubtensorInterface,
1937
+ start_block: int,
1938
+ end_block: int,
1939
+ wallet_ss58: str,
1940
+ ) -> dict:
1941
+ """Search for a coldkey swap event in a range of blocks.
1942
+
1943
+ Args:
1944
+ subtensor: SubtensorInterface for chain queries
1945
+ start_block: Starting block number to search
1946
+ end_block: Ending block number to search (inclusive)
1947
+ wallet_ss58: SS58 address of the signing wallet
1948
+
1949
+ Returns:
1950
+ dict: Contains the following keys if found:
1951
+ - block_num: Block number where swap was scheduled
1952
+ - dest_coldkey: SS58 address of destination coldkey
1953
+ - execution_block: Block number when swap will execute
1954
+ Empty dict if not found
1955
+ """
1956
+
1957
+ current_block, genesis_block = await asyncio.gather(
1958
+ subtensor.substrate.get_block_number(), subtensor.substrate.get_block_hash(0)
1959
+ )
1960
+ if (
1961
+ current_block - start_block > 300
1962
+ and genesis_block == Constants.genesis_block_hash_map["finney"]
1963
+ ):
1964
+ console.print("Querying archive node for coldkey swap events...")
1965
+ await subtensor.substrate.close()
1966
+ subtensor = SubtensorInterface("archive")
1967
+
1968
+ block_hashes = await asyncio.gather(
1969
+ *[
1970
+ subtensor.substrate.get_block_hash(block_num)
1971
+ for block_num in range(start_block, end_block + 1)
1972
+ ]
1973
+ )
1974
+ block_events = await asyncio.gather(
1975
+ *[
1976
+ subtensor.substrate.get_events(block_hash=block_hash)
1977
+ for block_hash in block_hashes
1978
+ ]
1979
+ )
1980
+
1981
+ for block_num, events in zip(range(start_block, end_block + 1), block_events):
1982
+ for event in events:
1983
+ if (
1984
+ event.get("event", {}).get("module_id") == "SubtensorModule"
1985
+ and event.get("event", {}).get("event_id") == "ColdkeySwapScheduled"
1986
+ ):
1987
+ attributes = event["event"].get("attributes", {})
1988
+ old_coldkey = decode_account_id(attributes["old_coldkey"][0])
1989
+
1990
+ if old_coldkey == wallet_ss58:
1991
+ return {
1992
+ "block_num": block_num,
1993
+ "dest_coldkey": decode_account_id(attributes["new_coldkey"][0]),
1994
+ "execution_block": attributes["execution_block"],
1995
+ }
1996
+
1997
+ return {}
1998
+
1999
+
2000
+ async def check_swap_status(
2001
+ subtensor: SubtensorInterface,
2002
+ origin_ss58: Optional[str] = None,
2003
+ expected_block_number: Optional[int] = None,
2004
+ ) -> None:
2005
+ """
2006
+ Check the status of a coldkey swap.
2007
+
2008
+ Args:
2009
+ subtensor: Connection to the network
2010
+ origin_ss58: The SS58 address of the original coldkey
2011
+ block_number: Optional block number where the swap was scheduled
2012
+ """
2013
+ scheduled_swaps = await subtensor.get_scheduled_coldkey_swap()
2014
+
2015
+ if not origin_ss58:
2016
+ if not scheduled_swaps:
2017
+ console.print("[yellow]No pending coldkey swaps found.[/yellow]")
2018
+ return
2019
+
2020
+ table = Table(
2021
+ Column(
2022
+ "Original Coldkey",
2023
+ justify="Left",
2024
+ style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"],
2025
+ no_wrap=True,
2026
+ ),
2027
+ Column("Status", style="dark_sea_green3"),
2028
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Pending Coldkey Swaps\n",
2029
+ show_header=True,
2030
+ show_edge=False,
2031
+ header_style="bold white",
2032
+ border_style="bright_black",
2033
+ style="bold",
2034
+ title_justify="center",
2035
+ show_lines=False,
2036
+ pad_edge=True,
2037
+ )
2038
+
2039
+ for coldkey in scheduled_swaps:
2040
+ table.add_row(coldkey, "Pending")
2041
+
2042
+ console.print(table)
2043
+ console.print(
2044
+ "\n[dim]Tip: Check specific swap details by providing the original coldkey SS58 address and the block number.[/dim]"
2045
+ )
2046
+ return
2047
+
2048
+ is_pending = origin_ss58 in scheduled_swaps
2049
+
2050
+ if not is_pending:
2051
+ console.print(
2052
+ f"[red]No pending swap found for coldkey:[/red] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]"
2053
+ )
2054
+ return
2055
+
2056
+ console.print(
2057
+ f"[green]Found pending swap for coldkey:[/green] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]"
2058
+ )
2059
+
2060
+ if expected_block_number is None:
2061
+ return
2062
+
2063
+ swap_info = await find_coldkey_swap_extrinsic(
2064
+ subtensor=subtensor,
2065
+ start_block=expected_block_number,
2066
+ end_block=expected_block_number,
2067
+ wallet_ss58=origin_ss58,
2068
+ )
2069
+
2070
+ if not swap_info:
2071
+ console.print(
2072
+ f"[yellow]Warning: Could not find swap extrinsic at block {expected_block_number}[/yellow]"
2073
+ )
2074
+ return
2075
+
2076
+ current_block = await subtensor.substrate.get_block_number()
2077
+ remaining_blocks = swap_info["execution_block"] - current_block
2078
+
2079
+ if remaining_blocks <= 0:
2080
+ console.print("[green]Swap period has completed![/green]")
2081
+ return
2082
+
2083
+ console.print(
2084
+ "\n[green]Coldkey swap details:[/green]"
2085
+ f"\nScheduled at block: {swap_info['block_num']}"
2086
+ f"\nOriginal address: [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]"
2087
+ f"\nDestination address: [{COLORS.G.CK}]{swap_info['dest_coldkey']}[/{COLORS.G.CK}]"
2088
+ f"\nCompletion block: {swap_info['execution_block']}"
2089
+ f"\nTime remaining: {blocks_to_duration(remaining_blocks)}"
2090
+ )