meshtensor-cli 9.18.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.
Files changed (74) hide show
  1. meshtensor_cli/__init__.py +22 -0
  2. meshtensor_cli/cli.py +10742 -0
  3. meshtensor_cli/doc_generation_helper.py +4 -0
  4. meshtensor_cli/src/__init__.py +1085 -0
  5. meshtensor_cli/src/commands/__init__.py +0 -0
  6. meshtensor_cli/src/commands/axon/__init__.py +0 -0
  7. meshtensor_cli/src/commands/axon/axon.py +132 -0
  8. meshtensor_cli/src/commands/crowd/__init__.py +0 -0
  9. meshtensor_cli/src/commands/crowd/contribute.py +621 -0
  10. meshtensor_cli/src/commands/crowd/contributors.py +200 -0
  11. meshtensor_cli/src/commands/crowd/create.py +783 -0
  12. meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
  13. meshtensor_cli/src/commands/crowd/refund.py +233 -0
  14. meshtensor_cli/src/commands/crowd/update.py +418 -0
  15. meshtensor_cli/src/commands/crowd/utils.py +124 -0
  16. meshtensor_cli/src/commands/crowd/view.py +991 -0
  17. meshtensor_cli/src/commands/governance/__init__.py +0 -0
  18. meshtensor_cli/src/commands/governance/governance.py +794 -0
  19. meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
  20. meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
  21. meshtensor_cli/src/commands/liquidity/utils.py +202 -0
  22. meshtensor_cli/src/commands/proxy.py +700 -0
  23. meshtensor_cli/src/commands/stake/__init__.py +0 -0
  24. meshtensor_cli/src/commands/stake/add.py +799 -0
  25. meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
  26. meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
  27. meshtensor_cli/src/commands/stake/claim.py +770 -0
  28. meshtensor_cli/src/commands/stake/list.py +738 -0
  29. meshtensor_cli/src/commands/stake/move.py +1211 -0
  30. meshtensor_cli/src/commands/stake/remove.py +1466 -0
  31. meshtensor_cli/src/commands/stake/wizard.py +323 -0
  32. meshtensor_cli/src/commands/subnets/__init__.py +0 -0
  33. meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
  34. meshtensor_cli/src/commands/subnets/price.py +733 -0
  35. meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
  36. meshtensor_cli/src/commands/sudo.py +1294 -0
  37. meshtensor_cli/src/commands/tc/__init__.py +0 -0
  38. meshtensor_cli/src/commands/tc/tc.py +190 -0
  39. meshtensor_cli/src/commands/treasury/__init__.py +0 -0
  40. meshtensor_cli/src/commands/treasury/treasury.py +194 -0
  41. meshtensor_cli/src/commands/view.py +354 -0
  42. meshtensor_cli/src/commands/wallets.py +2311 -0
  43. meshtensor_cli/src/commands/weights.py +467 -0
  44. meshtensor_cli/src/meshtensor/__init__.py +0 -0
  45. meshtensor_cli/src/meshtensor/balances.py +313 -0
  46. meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
  47. meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
  48. meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
  49. meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
  50. meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
  51. meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
  52. meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
  53. meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
  54. meshtensor_cli/src/meshtensor/minigraph.py +254 -0
  55. meshtensor_cli/src/meshtensor/networking.py +12 -0
  56. meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
  57. meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
  58. meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
  59. meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
  60. meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
  61. meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
  62. meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
  63. meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
  64. meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
  65. meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
  66. meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
  67. meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
  68. meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
  69. meshtensor_cli/src/meshtensor/utils.py +2007 -0
  70. meshtensor_cli/version.py +23 -0
  71. meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
  72. meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
  73. meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
  74. meshtensor_cli-9.18.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,770 @@
1
+ import asyncio
2
+ import json
3
+ from enum import Enum
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from meshtensor_wallet import Wallet
7
+ from rich.prompt import Prompt
8
+ from rich.panel import Panel
9
+ from rich.table import Table, Column
10
+ from rich import box
11
+
12
+ from meshtensor_cli.src import COLORS
13
+ from meshtensor_cli.src.meshtensor.balances import Balance
14
+ from meshtensor_cli.src.meshtensor.utils import (
15
+ confirm_action,
16
+ console,
17
+ print_error,
18
+ print_success,
19
+ unlock_key,
20
+ print_extrinsic_id,
21
+ json_console,
22
+ millify_tao,
23
+ group_subnets,
24
+ parse_subnet_range,
25
+ )
26
+
27
+ if TYPE_CHECKING:
28
+ from meshtensor_cli.src.meshtensor.meshtensor_interface import MeshtensorInterface
29
+
30
+
31
+ class ClaimType(Enum):
32
+ Keep = "Keep"
33
+ Swap = "Swap"
34
+
35
+
36
+ async def set_claim_type(
37
+ wallet: Wallet,
38
+ meshtensor: "MeshtensorInterface",
39
+ claim_type: Optional[ClaimType],
40
+ proxy: Optional[str],
41
+ netuids: Optional[str] = None,
42
+ prompt: bool = True,
43
+ decline: bool = False,
44
+ quiet: bool = False,
45
+ json_output: bool = False,
46
+ ) -> tuple[bool, str, Optional[str]]:
47
+ """
48
+ Sets the root claim type for the coldkey.
49
+
50
+ Root claim types control how staking emissions are handled on the ROOT network (subnet 0):
51
+ - "Swap": Future Root Alpha Emissions are swapped to MESH at claim time and added to root stake
52
+ - "Keep": Future Root Alpha Emissions are kept as Alpha tokens
53
+ - "KeepSubnets": Specific subnets kept as Alpha, rest swapped to MESH
54
+
55
+ Args:
56
+ wallet: Meshtensor wallet object
57
+ meshtensor: MeshtensorInterface object
58
+ claim_type: Claim type ("Keep" or "Swap"). If omitted, user will be prompted.
59
+ proxy: Optional proxy to use with this extrinsic submission.
60
+ netuids: Optional string of subnet IDs (e.g., "1-5,10,20-30"). Will be parsed internally.
61
+ prompt: Whether to prompt for user confirmation
62
+ json_output: Whether to output JSON
63
+
64
+ Returns:
65
+ tuple[bool, str, Optional[str]]: Tuple containing:
66
+ - bool: True if successful, False otherwise
67
+ - str: Error message if failed
68
+ - Optional[str]: Extrinsic identifier if successful
69
+ """
70
+
71
+ if claim_type is not None:
72
+ claim_type = claim_type.value
73
+
74
+ current_claim_info, all_netuids = await asyncio.gather(
75
+ meshtensor.get_coldkey_claim_type(coldkey_ss58=wallet.coldkeypub.ss58_address),
76
+ meshtensor.get_all_subnet_netuids(),
77
+ )
78
+ all_subnets = sorted([n for n in all_netuids if n != 0])
79
+
80
+ selected_netuids = None
81
+ if netuids is not None:
82
+ try:
83
+ selected_netuids = parse_subnet_range(
84
+ netuids, total_subnets=len(all_subnets)
85
+ )
86
+ except ValueError as e:
87
+ msg = f"Invalid netuid format: {e}"
88
+ print_error(msg)
89
+ if json_output:
90
+ json_console.print(json.dumps({"success": False, "message": msg}))
91
+ return False, msg, None
92
+
93
+ claim_table = Table(
94
+ Column("[bold white]Coldkey", style=COLORS.GENERAL.COLDKEY, justify="left"),
95
+ Column(
96
+ "[bold white]Current Type", style=COLORS.GENERAL.SUBHEADING, justify="left"
97
+ ),
98
+ show_header=True,
99
+ border_style="bright_black",
100
+ box=box.SIMPLE,
101
+ title=f"\n[{COLORS.GENERAL.HEADER}]Current Root Claim Type[/{COLORS.GENERAL.HEADER}]",
102
+ )
103
+ claim_table.add_row(
104
+ wallet.coldkeypub.ss58_address,
105
+ _format_claim_type_display(current_claim_info, all_subnets),
106
+ )
107
+ console.print(claim_table)
108
+
109
+ # Full wizard
110
+ if claim_type is None and selected_netuids is None:
111
+ new_claim_info = await _ask_for_claim_types(
112
+ wallet, meshtensor, all_subnets, decline=decline, quiet=quiet
113
+ )
114
+ if new_claim_info is None:
115
+ msg = "Operation cancelled."
116
+ console.print(f"[yellow]{msg}[/yellow]")
117
+ if json_output:
118
+ json_console.print(
119
+ json.dumps(
120
+ {
121
+ "success": False,
122
+ "message": msg,
123
+ "extrinsic_identifier": None,
124
+ }
125
+ )
126
+ )
127
+ return False, msg, None
128
+
129
+ # Keep netuids passed thru the cli and assume Keep type
130
+ elif claim_type is None and selected_netuids is not None:
131
+ new_claim_info = {"type": "KeepSubnets", "subnets": selected_netuids}
132
+
133
+ else:
134
+ # Netuids passed with Keep type
135
+ if selected_netuids is not None and claim_type == "Keep":
136
+ new_claim_info = {"type": "KeepSubnets", "subnets": selected_netuids}
137
+
138
+ # Netuids passed with Swap type
139
+ elif selected_netuids is not None and claim_type == "Swap":
140
+ keep_subnets = [n for n in all_subnets if n not in selected_netuids]
141
+ invalid = [n for n in selected_netuids if n not in all_subnets]
142
+ if invalid:
143
+ msg = f"Invalid subnets (not available): {group_subnets(invalid)}"
144
+ print_error(msg)
145
+ if json_output:
146
+ json_console.print(json.dumps({"success": False, "message": msg}))
147
+ return False, msg, None
148
+
149
+ if not keep_subnets:
150
+ new_claim_info = {"type": "Swap"}
151
+ elif set(keep_subnets) == set(all_subnets):
152
+ new_claim_info = {"type": "Keep"}
153
+ else:
154
+ new_claim_info = {"type": "KeepSubnets", "subnets": keep_subnets}
155
+ else:
156
+ new_claim_info = {"type": claim_type}
157
+
158
+ if _claim_types_equal(current_claim_info, new_claim_info):
159
+ if new_claim_info["type"] == "KeepSubnets":
160
+ msg = f"Claim type already set to {_format_claim_type_display(new_claim_info)}. \nNo change needed."
161
+ console.print(msg)
162
+ if json_output:
163
+ json_console.print(
164
+ json.dumps(
165
+ {
166
+ "success": True,
167
+ "message": msg,
168
+ "extrinsic_identifier": None,
169
+ }
170
+ )
171
+ )
172
+ return True, msg, None
173
+
174
+ if prompt:
175
+ console.print(
176
+ Panel(
177
+ f"[{COLORS.GENERAL.HEADER}]Confirm Claim Type Change[/{COLORS.GENERAL.HEADER}]\n\n"
178
+ f"FROM: {_format_claim_type_display(current_claim_info, all_subnets)}\n\n"
179
+ f"TO: {_format_claim_type_display(new_claim_info, all_subnets)}"
180
+ )
181
+ )
182
+
183
+ if not confirm_action(
184
+ "\nProceed with this change?", decline=decline, quiet=quiet
185
+ ):
186
+ msg = "Operation cancelled."
187
+ console.print(f"[yellow]{msg}[/yellow]")
188
+ if json_output:
189
+ json_console.print(json.dumps({"success": False, "message": msg}))
190
+ return False, msg, None
191
+
192
+ if not (unlock := unlock_key(wallet)).success:
193
+ msg = f"Failed to unlock wallet: {unlock.message}"
194
+ print_error(msg)
195
+ if json_output:
196
+ json_console.print(json.dumps({"success": False, "message": msg}))
197
+ return False, msg, None
198
+
199
+ with console.status(":satellite: Setting root claim type...", spinner="earth"):
200
+ claim_type_param = _prepare_claim_type_args(new_claim_info)
201
+ call = await meshtensor.substrate.compose_call(
202
+ call_module="MeshtensorModule",
203
+ call_function="set_root_claim_type",
204
+ call_params={"new_root_claim_type": claim_type_param},
205
+ )
206
+ success, err_msg, ext_receipt = await meshtensor.sign_and_send_extrinsic(
207
+ call, wallet, proxy=proxy
208
+ )
209
+
210
+ if success:
211
+ ext_id = await ext_receipt.get_extrinsic_identifier()
212
+ msg = "Successfully changed claim type"
213
+ print_success(msg)
214
+ await print_extrinsic_id(ext_receipt)
215
+ if json_output:
216
+ json_console.print(
217
+ json.dumps(
218
+ {
219
+ "success": True,
220
+ "message": msg,
221
+ "extrinsic_identifier": ext_id,
222
+ }
223
+ )
224
+ )
225
+ return True, msg, ext_id
226
+ else:
227
+ msg = f"Failed to set claim type: {err_msg}"
228
+ print_error(msg)
229
+ if json_output:
230
+ json_console.print(json.dumps({"success": False, "message": msg}))
231
+ return False, msg, None
232
+
233
+
234
+ async def process_pending_claims(
235
+ wallet: Wallet,
236
+ meshtensor: "MeshtensorInterface",
237
+ netuids: Optional[list[int]] = None,
238
+ proxy: Optional[str] = None,
239
+ prompt: bool = True,
240
+ decline: bool = False,
241
+ quiet: bool = False,
242
+ json_output: bool = False,
243
+ verbose: bool = False,
244
+ ) -> tuple[bool, str, Optional[str]]:
245
+ """Claims root network emissions for the coldkey across specified subnets"""
246
+
247
+ coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address
248
+ with console.status(":satellite: Discovering claimable emissions..."):
249
+ block_hash = await meshtensor.substrate.get_chain_head()
250
+ all_stakes, identities = await asyncio.gather(
251
+ meshtensor.get_stake_for_coldkey(
252
+ coldkey_ss58=coldkey_ss58, block_hash=block_hash
253
+ ),
254
+ meshtensor.query_all_identities(block_hash=block_hash),
255
+ )
256
+ if not all_stakes:
257
+ msg = "No stakes found for this coldkey"
258
+ console.print(f"[yellow]{msg}[/yellow]")
259
+ if json_output:
260
+ json_console.print(
261
+ json.dumps(
262
+ {
263
+ "success": True,
264
+ "message": msg,
265
+ "extrinsic_identifier": None,
266
+ "netuids": [],
267
+ }
268
+ )
269
+ )
270
+ return True, msg, None
271
+
272
+ current_stakes = {
273
+ (stake.hotkey_ss58, stake.netuid): stake for stake in all_stakes
274
+ }
275
+ claimable_by_hotkey = await meshtensor.get_claimable_stakes_for_coldkey(
276
+ coldkey_ss58=coldkey_ss58,
277
+ stakes_info=all_stakes,
278
+ block_hash=block_hash,
279
+ )
280
+ hotkey_owner_tasks = [
281
+ meshtensor.get_hotkey_owner(
282
+ hotkey, check_exists=False, block_hash=block_hash
283
+ )
284
+ for hotkey in claimable_by_hotkey.keys()
285
+ ]
286
+ hotkey_owners = await asyncio.gather(*hotkey_owner_tasks)
287
+ hotkey_to_owner = dict(zip(claimable_by_hotkey.keys(), hotkey_owners))
288
+
289
+ # Consolidate data
290
+ claimable_stake_info = {}
291
+ for vali_hotkey, claimable_stakes in claimable_by_hotkey.items():
292
+ vali_coldkey = hotkey_to_owner.get(vali_hotkey, "~")
293
+ vali_identity = identities.get(vali_coldkey, {}).get("name", "~")
294
+ for netuid, claimable_stake in claimable_stakes.items():
295
+ if claimable_stake.meshlet > 0:
296
+ if netuid not in claimable_stake_info:
297
+ claimable_stake_info[netuid] = {}
298
+ current_stake = (
299
+ stake_info.stake
300
+ if (stake_info := current_stakes.get((vali_hotkey, netuid)))
301
+ else Balance.from_meshlet(0).set_unit(netuid)
302
+ )
303
+ claimable_stake_info[netuid][vali_hotkey] = {
304
+ "claimable": claimable_stake,
305
+ "stake": current_stake,
306
+ "coldkey": vali_coldkey,
307
+ "identity": vali_identity,
308
+ }
309
+
310
+ if netuids:
311
+ claimable_stake_info = {
312
+ netuid: hotkeys_info
313
+ for netuid, hotkeys_info in claimable_stake_info.items()
314
+ if netuid in netuids
315
+ }
316
+
317
+ if not claimable_stake_info:
318
+ msg = "No claimable emissions found"
319
+ console.print(f"[yellow]{msg}[/yellow]")
320
+ if json_output:
321
+ json_console.print(
322
+ json.dumps(
323
+ {
324
+ "success": True,
325
+ "message": msg,
326
+ "extrinsic_identifier": None,
327
+ "netuids": netuids,
328
+ }
329
+ )
330
+ )
331
+ return True, msg, None
332
+
333
+ _print_claimable_table(wallet, claimable_stake_info, verbose)
334
+ selected_netuids = (
335
+ netuids if netuids else _prompt_claim_selection(claimable_stake_info)
336
+ )
337
+
338
+ call = await meshtensor.substrate.compose_call(
339
+ call_module="MeshtensorModule",
340
+ call_function="claim_root",
341
+ call_params={"subnets": selected_netuids},
342
+ )
343
+ extrinsic_fee = await meshtensor.get_extrinsic_fee(
344
+ call, wallet.coldkeypub, proxy=proxy
345
+ )
346
+ console.print(
347
+ f"\n[dim]Estimated extrinsic fee: {extrinsic_fee.tao:.9f} τ"
348
+ + (" (paid by signer account)" if proxy else "")
349
+ )
350
+
351
+ if prompt:
352
+ if not confirm_action("Do you want to proceed?", decline=decline, quiet=quiet):
353
+ msg = "Operation cancelled by user"
354
+ console.print(f"[yellow]{msg}[/yellow]")
355
+ if json_output:
356
+ json_console.print(
357
+ json.dumps(
358
+ {
359
+ "success": False,
360
+ "message": msg,
361
+ "extrinsic_identifier": None,
362
+ "netuids": selected_netuids,
363
+ }
364
+ )
365
+ )
366
+ return False, msg, None
367
+
368
+ if not (unlock := unlock_key(wallet)).success:
369
+ msg = f"Failed to unlock wallet: {unlock.message}"
370
+ print_error(msg)
371
+ if json_output:
372
+ json_console.print(
373
+ json.dumps(
374
+ {
375
+ "success": False,
376
+ "message": msg,
377
+ "extrinsic_identifier": None,
378
+ "netuids": selected_netuids,
379
+ }
380
+ )
381
+ )
382
+ return False, msg, None
383
+
384
+ with console.status(
385
+ f":satellite: Claiming root emissions for {len(selected_netuids)} subnet(s)...",
386
+ spinner="earth",
387
+ ):
388
+ success, err_msg, ext_receipt = await meshtensor.sign_and_send_extrinsic(
389
+ call, wallet, proxy=proxy
390
+ )
391
+ if success:
392
+ ext_id = await ext_receipt.get_extrinsic_identifier()
393
+ msg = f"Successfully claimed root emissions for {len(selected_netuids)} subnet(s)"
394
+ console.print(f"[dark_sea_green3]{msg}[/dark_sea_green3]")
395
+ await print_extrinsic_id(ext_receipt)
396
+ if json_output:
397
+ json_console.print(
398
+ json.dumps(
399
+ {
400
+ "success": True,
401
+ "message": msg,
402
+ "extrinsic_identifier": ext_id,
403
+ "netuids": selected_netuids,
404
+ }
405
+ )
406
+ )
407
+ return True, msg, ext_id
408
+ else:
409
+ msg = f"Failed to claim root emissions: {err_msg}"
410
+ print_error(msg)
411
+ if json_output:
412
+ json_console.print(
413
+ json.dumps(
414
+ {
415
+ "success": False,
416
+ "message": msg,
417
+ "extrinsic_identifier": None,
418
+ "netuids": selected_netuids,
419
+ }
420
+ )
421
+ )
422
+ return False, msg, None
423
+
424
+
425
+ def _prompt_claim_selection(claimable_stake: dict) -> Optional[list[int]]:
426
+ """Prompts user to select up to 5 netuids to claim from"""
427
+
428
+ available_netuids = sorted(claimable_stake.keys())
429
+ while True:
430
+ netuid_input = Prompt.ask(
431
+ "Enter up to 5 netuids to claim from (comma-separated)",
432
+ default=",".join(str(n) for n in available_netuids),
433
+ )
434
+
435
+ try:
436
+ if "," in netuid_input:
437
+ selected = [int(n.strip()) for n in netuid_input.split(",")]
438
+ else:
439
+ selected = [int(netuid_input.strip())]
440
+ except ValueError:
441
+ print_error("Invalid input. Please enter numbers only.")
442
+ continue
443
+
444
+ if len(selected) > 5:
445
+ print_error(
446
+ f"You selected {len(selected)} netuids. Maximum is 5. Please try again."
447
+ )
448
+ continue
449
+
450
+ if len(selected) == 0:
451
+ print_error("Please select at least one netuid.")
452
+ continue
453
+
454
+ invalid_netuids = [n for n in selected if n not in available_netuids]
455
+ if invalid_netuids:
456
+ print_error(f"Invalid netuids: {', '.join(map(str, invalid_netuids))}")
457
+ continue
458
+
459
+ selected = list(dict.fromkeys(selected))
460
+
461
+ return selected
462
+
463
+
464
+ def _print_claimable_table(
465
+ wallet: Wallet, claimable_stake: dict, verbose: bool = False
466
+ ):
467
+ """Prints claimable stakes table grouped by netuid"""
468
+
469
+ table = Table(
470
+ show_header=True,
471
+ show_footer=False,
472
+ show_edge=True,
473
+ border_style="bright_black",
474
+ box=box.SIMPLE,
475
+ pad_edge=False,
476
+ title=f"\n[{COLORS.GENERAL.HEADER}]Claimable emissions for coldkey: {wallet.coldkeypub.ss58_address}",
477
+ )
478
+
479
+ table.add_column("Netuid", style=COLORS.GENERAL.NETUID, justify="center")
480
+ table.add_column("Current Stake", style=COLORS.GENERAL.SUBHEADING, justify="right")
481
+ table.add_column("Claimable", style=COLORS.GENERAL.SUCCESS, justify="right")
482
+ table.add_column("Hotkey", style=COLORS.GENERAL.HOTKEY, justify="left")
483
+ table.add_column("Identity", style=COLORS.GENERAL.SUBHEADING, justify="left")
484
+
485
+ for netuid in sorted(claimable_stake.keys()):
486
+ hotkeys_info = claimable_stake[netuid]
487
+ first_row = True
488
+
489
+ for hotkey, info in hotkeys_info.items():
490
+ hotkey_display = hotkey if verbose else f"{hotkey[:8]}...{hotkey[-8:]}"
491
+ netuid_display = str(netuid) if first_row else ""
492
+
493
+ stake_display = info["stake"]
494
+ stake_formatted = (
495
+ f"{stake_display.tao:.4f} {stake_display.unit}"
496
+ if verbose
497
+ else f"{millify_tao(stake_display.tao)} {stake_display.unit}"
498
+ )
499
+
500
+ claimable_display = info["claimable"]
501
+ claimable_formatted = (
502
+ f"{claimable_display.tao:.4f} {claimable_display.unit}"
503
+ if verbose
504
+ else f"{millify_tao(claimable_display.tao)} {claimable_display.unit}"
505
+ )
506
+ table.add_row(
507
+ netuid_display,
508
+ stake_formatted,
509
+ claimable_formatted,
510
+ hotkey_display,
511
+ info.get("identity", "~"),
512
+ )
513
+ first_row = False
514
+
515
+ console.print(table)
516
+
517
+
518
+ async def _ask_for_claim_types(
519
+ wallet: Wallet,
520
+ meshtensor: "MeshtensorInterface",
521
+ all_subnets: list,
522
+ decline: bool = False,
523
+ quiet: bool = False,
524
+ ) -> Optional[dict]:
525
+ """
526
+ Interactive prompts for claim type selection.
527
+
528
+ Flow:
529
+ 1. Ask "Keep or Swap?"
530
+ 2. Ask "All subnets?"
531
+ - If yes → return simple type (Keep or Swap)
532
+ - If no → enter subnet selection
533
+
534
+ Returns:
535
+ dict: Selected claim type, or None if cancelled
536
+ """
537
+
538
+ console.print("\n")
539
+ console.print(
540
+ Panel(
541
+ f"[{COLORS.GENERAL.HEADER}]Root Claim Type Selection[/{COLORS.GENERAL.HEADER}]\n\n"
542
+ "Configure how your root network emissions are claimed.\n\n"
543
+ "[yellow]Options:[/yellow]\n"
544
+ " • [green]Swap[/green] - Convert emissions to MESH\n"
545
+ " • [green]Keep[/green] - Keep emissions as Alpha\n"
546
+ " • [green]Keep Specific[/green] - Keep selected subnets, swap others\n",
547
+ )
548
+ )
549
+
550
+ primary_choice = Prompt.ask(
551
+ "\nSelect new root claim type",
552
+ choices=["keep", "swap", "cancel"],
553
+ default="cancel",
554
+ )
555
+ if primary_choice == "cancel":
556
+ return None
557
+
558
+ apply_to_all = confirm_action(
559
+ f"\nSet {primary_choice.capitalize()} to ALL subnets?",
560
+ default=True,
561
+ decline=decline,
562
+ quiet=quiet,
563
+ )
564
+
565
+ if apply_to_all:
566
+ return {"type": primary_choice.capitalize()}
567
+
568
+ if primary_choice == "keep":
569
+ console.print(
570
+ "\nYou can select which subnets to KEEP as Alpha (others will be swapped to MESH).\n"
571
+ )
572
+ else:
573
+ console.print(
574
+ "\nYou can select which subnets to SWAP to MESH (others will be kept as Alpha).\n"
575
+ )
576
+
577
+ return await _prompt_claim_netuids(
578
+ wallet,
579
+ meshtensor,
580
+ all_subnets,
581
+ mode=primary_choice,
582
+ decline=decline,
583
+ quiet=quiet,
584
+ )
585
+
586
+
587
+ async def _prompt_claim_netuids(
588
+ wallet: Wallet,
589
+ meshtensor: "MeshtensorInterface",
590
+ all_subnets: list,
591
+ mode: str = "keep",
592
+ decline: bool = False,
593
+ quiet: bool = False,
594
+ ) -> Optional[dict]:
595
+ """
596
+ Interactive subnet selection.
597
+
598
+ Args:
599
+ mode: "keep" to select subnets to keep as Alpha, "swap" to select subnets to swap to MESH
600
+
601
+ Returns:
602
+ dict: KeepSubnets claim type or None if cancelled
603
+ """
604
+
605
+ if not all_subnets:
606
+ console.print("[yellow]No subnets available.[/yellow]")
607
+ return {"type": "Swap"}
608
+
609
+ if mode == "keep":
610
+ action = "KEEP as Alpha"
611
+ else:
612
+ action = "SWAP to MESH"
613
+
614
+ console.print(
615
+ Panel(
616
+ f"[{COLORS.GENERAL.HEADER}]Subnet Selection[/{COLORS.GENERAL.HEADER}]\n\n"
617
+ f"[bold]Available subnets:[/bold] {group_subnets(sorted(all_subnets))}\n"
618
+ f"[dim]Total: {len(all_subnets)} subnets[/dim]\n\n"
619
+ "[yellow]Input examples:[/yellow]\n"
620
+ " • [cyan]1-10[/cyan] - Range from 1 to 10\n"
621
+ " • [cyan]1, 5, 10[/cyan] - Specific subnets\n"
622
+ " • [cyan]1-10, 20-30, 50[/cyan] - Mixed"
623
+ )
624
+ )
625
+
626
+ while True:
627
+ subnet_input = Prompt.ask(
628
+ f"\nEnter subnets to {action} [dim]{group_subnets(sorted(all_subnets))}",
629
+ default="",
630
+ )
631
+
632
+ if not subnet_input.strip():
633
+ print_error("No subnets entered. Please try again.")
634
+ continue
635
+
636
+ try:
637
+ selected = parse_subnet_range(subnet_input, total_subnets=len(all_subnets))
638
+ invalid = [s for s in selected if s not in all_subnets]
639
+ if invalid:
640
+ print_error(
641
+ f"Invalid subnets (not available): {group_subnets(invalid)}"
642
+ )
643
+ print_error("[yellow]Please try again.[/yellow]")
644
+ continue
645
+
646
+ if mode == "keep":
647
+ keep_subnets = selected
648
+ else:
649
+ keep_subnets = [n for n in all_subnets if n not in selected]
650
+
651
+ if _preview_subnet_selection(
652
+ keep_subnets, all_subnets, decline=decline, quiet=quiet
653
+ ):
654
+ if not keep_subnets:
655
+ return {"type": "Swap"}
656
+ elif set(keep_subnets) == set(all_subnets):
657
+ return {"type": "Keep"}
658
+ else:
659
+ return {"type": "KeepSubnets", "subnets": keep_subnets}
660
+ else:
661
+ console.print(
662
+ "[yellow]Selection cancelled. Starting over...[/yellow]\n"
663
+ )
664
+ return await _prompt_claim_netuids(
665
+ wallet, meshtensor, all_subnets, mode=mode
666
+ )
667
+
668
+ except ValueError as e:
669
+ print_error(f"Invalid subnet selection: {e}\nPlease try again.")
670
+
671
+
672
+ def _preview_subnet_selection(
673
+ keep_subnets: list[int],
674
+ all_subnets: list[int],
675
+ decline: bool = False,
676
+ quiet: bool = False,
677
+ ) -> bool:
678
+ """Show preview and ask for confirmation."""
679
+
680
+ swap_subnets = [n for n in all_subnets if n not in keep_subnets]
681
+ preview_content = (
682
+ f"[{COLORS.GENERAL.HEADER}]Preview Your Selection[/{COLORS.GENERAL.HEADER}]\n\n"
683
+ )
684
+
685
+ if keep_subnets:
686
+ preview_content += (
687
+ f"[green]✓ Keep as Alpha:[/green] {group_subnets(keep_subnets)}\n"
688
+ f"[dim] ({len(keep_subnets)} subnet{'s' if len(keep_subnets) != 1 else ''})[/dim]"
689
+ )
690
+ else:
691
+ preview_content += "[dim]No subnets kept as Alpha[/dim]"
692
+
693
+ if swap_subnets:
694
+ preview_content += (
695
+ f"\n\n[yellow]⟳ Swap to MESH:[/yellow] {group_subnets(swap_subnets)}\n"
696
+ f"[dim] ({len(swap_subnets)} subnet{'s' if len(swap_subnets) != 1 else ''})[/dim]"
697
+ )
698
+ else:
699
+ preview_content += "\n\n[dim]No subnets swapped to MESH[/dim]"
700
+
701
+ console.print(Panel(preview_content))
702
+
703
+ return confirm_action(
704
+ "\nIs this correct?", default=True, decline=decline, quiet=quiet
705
+ )
706
+
707
+
708
+ def _format_claim_type_display(
709
+ claim_info: dict, all_subnets: Optional[list[int]] = None
710
+ ) -> str:
711
+ """
712
+ Format claim type for human-readable display.
713
+
714
+ Args:
715
+ claim_info: Claim type information dict
716
+ all_subnets: Optional list of all available subnets (for showing swap info)
717
+ """
718
+
719
+ claim_type = claim_info["type"]
720
+ if claim_type == "Swap":
721
+ return "[yellow]Swap All[/yellow]"
722
+
723
+ elif claim_type == "Keep":
724
+ return "[dark_sea_green3]Keep All[/dark_sea_green3]"
725
+
726
+ elif claim_type == "KeepSubnets":
727
+ subnets = claim_info["subnets"]
728
+ subnet_display = group_subnets(subnets)
729
+
730
+ result = (
731
+ f"[cyan]Keep Specific[/cyan]\n[green] ✓ Keep:[/green] {subnet_display}"
732
+ )
733
+ if all_subnets:
734
+ swap_subnets = [n for n in all_subnets if n not in subnets]
735
+ if swap_subnets:
736
+ swap_display = group_subnets(swap_subnets)
737
+ result += f"\n[yellow] ⟳ Swap:[/yellow] {swap_display}"
738
+
739
+ return result
740
+ else:
741
+ return "[red]Unknown[/red]"
742
+
743
+
744
+ def _claim_types_equal(claim1: dict, claim2: dict) -> bool:
745
+ """Check if two claim type configs are equivalent."""
746
+
747
+ if claim1["type"] != claim2["type"]:
748
+ return False
749
+
750
+ if claim1["type"] == "KeepSubnets":
751
+ subnets1 = sorted(claim1.get("subnets", []))
752
+ subnets2 = sorted(claim2.get("subnets", []))
753
+ return subnets1 == subnets2
754
+
755
+ return True
756
+
757
+
758
+ def _prepare_claim_type_args(claim_info: dict) -> dict:
759
+ """Convert claim type arguments for chain call"""
760
+
761
+ claim_type = claim_info["type"]
762
+ if claim_type == "Swap":
763
+ return {"Swap": None}
764
+ elif claim_type == "Keep":
765
+ return {"Keep": None}
766
+ elif claim_type == "KeepSubnets":
767
+ subnets = claim_info["subnets"]
768
+ return {"KeepSubnets": {"subnets": subnets}}
769
+ else:
770
+ raise ValueError(f"Unknown claim type: {claim_type}")