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,1466 @@
1
+ import asyncio
2
+ import json
3
+ from functools import partial
4
+
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ from async_substrate_interface import AsyncExtrinsicReceipt
8
+ from meshtensor_wallet import Wallet
9
+ from rich.prompt import Prompt
10
+ from rich.table import Table
11
+
12
+ from meshtensor_cli.src import COLOR_PALETTE
13
+ from meshtensor_cli.src.meshtensor.extrinsics.mev_shield import (
14
+ extract_mev_shield_id,
15
+ wait_for_extrinsic_by_hash,
16
+ )
17
+ from meshtensor_cli.src.meshtensor.balances import Balance
18
+ from meshtensor_cli.src.meshtensor.utils import (
19
+ confirm_action,
20
+ console,
21
+ print_success,
22
+ print_verbose,
23
+ print_error,
24
+ get_hotkey_wallets_for_wallet,
25
+ is_valid_ss58_address,
26
+ format_error_message,
27
+ group_subnets,
28
+ unlock_key,
29
+ json_console,
30
+ get_hotkey_pub_ss58,
31
+ print_extrinsic_id,
32
+ )
33
+
34
+ if TYPE_CHECKING:
35
+ from meshtensor_cli.src.meshtensor.meshtensor_interface import MeshtensorInterface
36
+
37
+
38
+ # Commands
39
+ async def unstake(
40
+ wallet: Wallet,
41
+ meshtensor: "MeshtensorInterface",
42
+ hotkey_ss58_address: str,
43
+ all_hotkeys: bool,
44
+ include_hotkeys: list[str],
45
+ exclude_hotkeys: list[str],
46
+ amount: float,
47
+ prompt: bool,
48
+ decline: bool,
49
+ quiet: bool,
50
+ interactive: bool,
51
+ netuid: Optional[int],
52
+ safe_staking: bool,
53
+ rate_tolerance: float,
54
+ allow_partial_stake: bool,
55
+ json_output: bool,
56
+ era: int,
57
+ proxy: Optional[str],
58
+ mev_protection: bool,
59
+ ):
60
+ """Unstake from hotkey(s)."""
61
+ coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address
62
+ with console.status(
63
+ f"Retrieving subnet data & identities from {meshtensor.network}...",
64
+ spinner="earth",
65
+ ):
66
+ chain_head = await meshtensor.substrate.get_chain_head()
67
+ (
68
+ all_sn_dynamic_info_,
69
+ ck_hk_identities,
70
+ old_identities,
71
+ stake_infos,
72
+ ) = await asyncio.gather(
73
+ meshtensor.all_subnets(block_hash=chain_head),
74
+ meshtensor.fetch_coldkey_hotkey_identities(block_hash=chain_head),
75
+ meshtensor.get_delegate_identities(block_hash=chain_head),
76
+ meshtensor.get_stake_for_coldkey(coldkey_ss58, block_hash=chain_head),
77
+ )
78
+ all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_}
79
+
80
+ if interactive:
81
+ try:
82
+ hotkeys_to_unstake_from, unstake_all_from_hk = await _unstake_selection(
83
+ all_sn_dynamic_info,
84
+ ck_hk_identities,
85
+ old_identities,
86
+ stake_infos,
87
+ netuid=netuid,
88
+ )
89
+ except ValueError:
90
+ return False
91
+ if unstake_all_from_hk:
92
+ hotkey_to_unstake_all = hotkeys_to_unstake_from[0]
93
+ unstake_all_alpha = confirm_action(
94
+ "\nDo you want to:\n"
95
+ "[blue]Yes[/blue]: Unstake from all subnets and automatically re-stake to subnet 0 (root)\n"
96
+ "[blue]No[/blue]: Unstake everything (including subnet 0)",
97
+ default=True,
98
+ decline=decline,
99
+ quiet=quiet,
100
+ )
101
+ return await unstake_all(
102
+ wallet=wallet,
103
+ meshtensor=meshtensor,
104
+ hotkey_ss58_address=hotkey_to_unstake_all[1],
105
+ unstake_all_alpha=unstake_all_alpha,
106
+ prompt=prompt,
107
+ )
108
+
109
+ if not hotkeys_to_unstake_from:
110
+ console.print("[red]No unstake operations to perform.[/red]")
111
+ return False
112
+ netuids = list({netuid for _, _, netuid in hotkeys_to_unstake_from})
113
+
114
+ else:
115
+ netuids = (
116
+ [int(netuid)]
117
+ if netuid is not None
118
+ else await meshtensor.get_all_subnet_netuids()
119
+ )
120
+ hotkeys_to_unstake_from = _get_hotkeys_to_unstake(
121
+ wallet=wallet,
122
+ hotkey_ss58_address=hotkey_ss58_address,
123
+ all_hotkeys=all_hotkeys,
124
+ include_hotkeys=include_hotkeys,
125
+ exclude_hotkeys=exclude_hotkeys,
126
+ stake_infos=stake_infos,
127
+ identities=ck_hk_identities,
128
+ old_identities=old_identities,
129
+ )
130
+
131
+ with console.status(
132
+ f"Retrieving stake data from {meshtensor.network}...",
133
+ spinner="earth",
134
+ ):
135
+ stake_in_netuids = {}
136
+ for stake_info in stake_infos:
137
+ if stake_info.hotkey_ss58 not in stake_in_netuids:
138
+ stake_in_netuids[stake_info.hotkey_ss58] = {}
139
+ stake_in_netuids[stake_info.hotkey_ss58][stake_info.netuid] = (
140
+ stake_info.stake
141
+ )
142
+
143
+ # Flag to check if user wants to quit
144
+ skip_remaining_subnets = False
145
+ if len(netuids) > 1 and not amount:
146
+ console.print(
147
+ "[dark_sea_green3]Tip: Enter 'q' any time to stop going over "
148
+ "remaining subnets and process current unstakes.\n"
149
+ )
150
+
151
+ # Iterate over hotkeys and netuids to collect unstake operations
152
+ unstake_operations = []
153
+ total_received_amount = Balance.from_tao(0)
154
+ max_float_slippage = 0
155
+ table_rows = []
156
+ for hotkey in hotkeys_to_unstake_from:
157
+ if skip_remaining_subnets:
158
+ break
159
+
160
+ if interactive:
161
+ staking_address_name, staking_address_ss58, netuid = hotkey
162
+ netuids_to_process = [netuid]
163
+ else:
164
+ staking_address_name, staking_address_ss58, _ = hotkey
165
+ netuids_to_process = netuids
166
+
167
+ initial_amount = amount
168
+
169
+ for netuid in netuids_to_process:
170
+ if skip_remaining_subnets:
171
+ break # Exit the loop over netuids
172
+
173
+ subnet_info = all_sn_dynamic_info.get(netuid)
174
+ if staking_address_ss58 not in stake_in_netuids:
175
+ print_error(
176
+ f"No stake found for hotkey: {staking_address_ss58} on netuid: {netuid}"
177
+ )
178
+ continue # Skip to next hotkey
179
+
180
+ current_stake_balance = stake_in_netuids[staking_address_ss58].get(netuid)
181
+ if current_stake_balance is None or current_stake_balance.tao == 0:
182
+ print_error(
183
+ f"No stake to unstake from {staking_address_ss58} on netuid: {netuid}"
184
+ )
185
+ continue # No stake to unstake
186
+
187
+ # Determine the amount we are unstaking.
188
+ if initial_amount:
189
+ amount_to_unstake_as_balance = Balance.from_tao(initial_amount)
190
+ else:
191
+ amount_to_unstake_as_balance = _ask_unstake_amount(
192
+ current_stake_balance,
193
+ netuid,
194
+ staking_address_name
195
+ if staking_address_name
196
+ else staking_address_ss58,
197
+ staking_address_ss58,
198
+ )
199
+ if amount_to_unstake_as_balance is None:
200
+ skip_remaining_subnets = True
201
+ break
202
+
203
+ # Check enough stake to remove.
204
+ amount_to_unstake_as_balance.set_unit(netuid)
205
+ if amount_to_unstake_as_balance > current_stake_balance:
206
+ print_error(
207
+ f"Not enough stake to remove:\n"
208
+ f" Stake balance: [dark_orange]{current_stake_balance}[/dark_orange]"
209
+ f" < Unstaking amount: [dark_orange]{amount_to_unstake_as_balance}[/dark_orange]"
210
+ f" on netuid: {netuid}"
211
+ )
212
+ continue # Skip to the next subnet - useful when single amount is specified for all subnets
213
+
214
+ try:
215
+ current_price = subnet_info.price.tao
216
+ if safe_staking:
217
+ if subnet_info.is_dynamic:
218
+ price_with_tolerance = current_price * (1 - rate_tolerance)
219
+ rate_with_tolerance = price_with_tolerance
220
+ price_limit = Balance.from_tao(
221
+ rate_with_tolerance
222
+ ) # Actual price to pass to extrinsic
223
+ else:
224
+ price_limit = Balance.from_meshlet(1)
225
+ extrinsic_fee = await _get_extrinsic_fee(
226
+ "unstake_safe",
227
+ wallet,
228
+ meshtensor,
229
+ hotkey_ss58=staking_address_ss58,
230
+ amount=amount_to_unstake_as_balance,
231
+ netuid=netuid,
232
+ price_limit=price_limit,
233
+ allow_partial_stake=allow_partial_stake,
234
+ proxy=proxy,
235
+ )
236
+ else:
237
+ extrinsic_fee = await _get_extrinsic_fee(
238
+ "unstake",
239
+ wallet,
240
+ meshtensor,
241
+ hotkey_ss58=staking_address_ss58,
242
+ netuid=netuid,
243
+ amount=amount_to_unstake_as_balance,
244
+ proxy=proxy,
245
+ )
246
+ sim_swap = await meshtensor.sim_swap(
247
+ netuid, 0, amount_to_unstake_as_balance.meshlet
248
+ )
249
+ received_amount = sim_swap.tao_amount
250
+ if not proxy:
251
+ received_amount -= extrinsic_fee
252
+ except ValueError:
253
+ continue
254
+ total_received_amount += received_amount
255
+
256
+ base_unstake_op = {
257
+ "netuid": netuid,
258
+ "hotkey_name": staking_address_name
259
+ if staking_address_name
260
+ else staking_address_ss58,
261
+ "hotkey_ss58": staking_address_ss58,
262
+ "amount_to_unstake": amount_to_unstake_as_balance,
263
+ "current_stake_balance": current_stake_balance,
264
+ "received_amount": received_amount,
265
+ "dynamic_info": subnet_info,
266
+ }
267
+
268
+ base_table_row = [
269
+ str(netuid), # Netuid
270
+ staking_address_name, # Hotkey Name
271
+ str(amount_to_unstake_as_balance), # Amount to Unstake
272
+ f"{subnet_info.price.tao:.6f}"
273
+ + f"(τ/{Balance.get_unit(netuid)})", # Rate
274
+ str(sim_swap.alpha_fee), # Fee
275
+ str(extrinsic_fee), # Extrinsic fee
276
+ str(received_amount), # Received Amount
277
+ # slippage_pct, # Slippage Percent
278
+ ]
279
+
280
+ # Additional fields for safe unstaking
281
+ if safe_staking:
282
+ if subnet_info.is_dynamic:
283
+ price_with_tolerance = current_price * (1 - rate_tolerance)
284
+ rate_with_tolerance = price_with_tolerance
285
+ price_with_tolerance = Balance.from_tao(
286
+ rate_with_tolerance
287
+ ).meshlet # Actual price to pass to extrinsic
288
+ else:
289
+ rate_with_tolerance = 1
290
+ price_with_tolerance = 1
291
+
292
+ base_unstake_op["price_with_tolerance"] = price_with_tolerance
293
+ base_table_row.extend(
294
+ [
295
+ # Rate with tolerance
296
+ f"{rate_with_tolerance:.6f} {Balance.get_unit(0)}/{Balance.get_unit(netuid)}",
297
+ # Partial unstake
298
+ f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]"
299
+ f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]",
300
+ ]
301
+ )
302
+
303
+ unstake_operations.append(base_unstake_op)
304
+ table_rows.append(base_table_row)
305
+
306
+ if not unstake_operations:
307
+ console.print("[red]No unstake operations to perform.[/red]")
308
+ return False
309
+
310
+ table = _create_unstake_table(
311
+ wallet_name=wallet.name,
312
+ wallet_coldkey_ss58=coldkey_ss58,
313
+ network=meshtensor.network,
314
+ total_received_amount=total_received_amount,
315
+ safe_staking=safe_staking,
316
+ rate_tolerance=rate_tolerance,
317
+ )
318
+ for row in table_rows:
319
+ table.add_row(*row)
320
+
321
+ _print_table_and_slippage(table, max_float_slippage, safe_staking)
322
+ if prompt:
323
+ if not confirm_action(
324
+ "Would you like to continue?", decline=decline, quiet=quiet
325
+ ):
326
+ return False
327
+
328
+ # Execute extrinsics
329
+ if not unlock_key(wallet).success:
330
+ return False
331
+
332
+ successes = []
333
+ with console.status("\n:satellite: Performing unstaking operations...") as status:
334
+ for op in unstake_operations:
335
+ common_args = {
336
+ "wallet": wallet,
337
+ "meshtensor": meshtensor,
338
+ "netuid": op["netuid"],
339
+ "amount": op["amount_to_unstake"],
340
+ "hotkey_ss58": op["hotkey_ss58"],
341
+ "status": status,
342
+ "era": era,
343
+ "proxy": proxy,
344
+ "mev_protection": mev_protection,
345
+ }
346
+
347
+ if safe_staking and op["netuid"] != 0:
348
+ func = _safe_unstake_extrinsic
349
+ specific_args = {
350
+ "price_limit": op["price_with_tolerance"],
351
+ "allow_partial_stake": allow_partial_stake,
352
+ }
353
+ else:
354
+ func = _unstake_extrinsic
355
+ specific_args = {"current_stake": op["current_stake_balance"]}
356
+
357
+ suc, ext_receipt = await func(**common_args, **specific_args)
358
+ ext_id = await ext_receipt.get_extrinsic_identifier() if suc else None
359
+
360
+ successes.append(
361
+ {
362
+ "netuid": op["netuid"],
363
+ "hotkey_ss58": op["hotkey_ss58"],
364
+ "unstake_amount": op["amount_to_unstake"].tao,
365
+ "success": suc,
366
+ "extrinsic_identifier": ext_id,
367
+ }
368
+ )
369
+
370
+ console.print(
371
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed."
372
+ )
373
+ if json_output:
374
+ json_console.print_json(data=successes)
375
+ return True
376
+
377
+
378
+ async def unstake_all(
379
+ wallet: Wallet,
380
+ meshtensor: "MeshtensorInterface",
381
+ hotkey_ss58_address: str,
382
+ unstake_all_alpha: bool = False,
383
+ all_hotkeys: bool = False,
384
+ include_hotkeys: Optional[list[str]] = None,
385
+ exclude_hotkeys: Optional[list[str]] = None,
386
+ era: int = 3,
387
+ prompt: bool = True,
388
+ decline: bool = False,
389
+ quiet: bool = False,
390
+ json_output: bool = False,
391
+ proxy: Optional[str] = None,
392
+ mev_protection: bool = True,
393
+ ) -> None:
394
+ """Unstakes all stakes from all hotkeys in all subnets."""
395
+ include_hotkeys = include_hotkeys or []
396
+ exclude_hotkeys = exclude_hotkeys or []
397
+ coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address
398
+ with console.status(
399
+ f"Retrieving stake information & identities from {meshtensor.network}...",
400
+ spinner="earth",
401
+ ):
402
+ (
403
+ stake_info,
404
+ ck_hk_identities,
405
+ old_identities,
406
+ all_sn_dynamic_info_,
407
+ current_wallet_balance,
408
+ ) = await asyncio.gather(
409
+ meshtensor.get_stake_for_coldkey(coldkey_ss58),
410
+ meshtensor.fetch_coldkey_hotkey_identities(),
411
+ meshtensor.get_delegate_identities(),
412
+ meshtensor.all_subnets(),
413
+ meshtensor.get_balance(coldkey_ss58),
414
+ )
415
+
416
+ if all_hotkeys:
417
+ hotkeys = _get_hotkeys_to_unstake(
418
+ wallet,
419
+ hotkey_ss58_address=hotkey_ss58_address,
420
+ all_hotkeys=all_hotkeys,
421
+ include_hotkeys=include_hotkeys,
422
+ exclude_hotkeys=exclude_hotkeys,
423
+ stake_infos=stake_info,
424
+ identities=ck_hk_identities,
425
+ old_identities=old_identities,
426
+ )
427
+ elif not hotkey_ss58_address:
428
+ hotkeys = [(wallet.hotkey_str, get_hotkey_pub_ss58(wallet), None)]
429
+ else:
430
+ hotkeys = [(None, hotkey_ss58_address, None)]
431
+
432
+ hotkey_names = {ss58: name for name, ss58, _ in hotkeys if name is not None}
433
+ hotkey_ss58s = [item[1] for item in hotkeys]
434
+ stake_info = [
435
+ stake for stake in stake_info if stake.hotkey_ss58 in hotkey_ss58s
436
+ ]
437
+
438
+ if unstake_all_alpha:
439
+ stake_info = [stake for stake in stake_info if stake.netuid != 0]
440
+
441
+ if not stake_info:
442
+ console.print("[red]No stakes found to unstake[/red]")
443
+ return
444
+
445
+ all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_}
446
+
447
+ # Create table for unstaking all
448
+ table_title = (
449
+ "Unstaking Summary - All Stakes"
450
+ if not unstake_all_alpha
451
+ else "Unstaking Summary - All Alpha Stakes"
452
+ )
453
+ table = Table(
454
+ title=(
455
+ f"\n[{COLOR_PALETTE.G.HEADER}]{table_title}[/{COLOR_PALETTE.G.HEADER}]\n"
456
+ f"Wallet: [{COLOR_PALETTE.G.COLDKEY}]{wallet.name}[/{COLOR_PALETTE.G.COLDKEY}], "
457
+ f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{coldkey_ss58}[/{COLOR_PALETTE.G.CK}]\n"
458
+ f"Network: [{COLOR_PALETTE.G.HEADER}]{meshtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n"
459
+ ),
460
+ show_footer=True,
461
+ show_edge=False,
462
+ header_style="bold white",
463
+ border_style="bright_black",
464
+ style="bold",
465
+ title_justify="center",
466
+ show_lines=False,
467
+ pad_edge=True,
468
+ )
469
+ table.add_column("Netuid", justify="center", style="grey89")
470
+ table.add_column(
471
+ "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
472
+ )
473
+ table.add_column(
474
+ f"Current Stake ({Balance.get_unit(1)})",
475
+ justify="center",
476
+ style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"],
477
+ )
478
+ table.add_column(
479
+ f"Rate ({Balance.unit}/{Balance.get_unit(1)})",
480
+ justify="center",
481
+ style=COLOR_PALETTE["POOLS"]["RATE"],
482
+ )
483
+ table.add_column(
484
+ f"Fee ({Balance.get_unit(1)})",
485
+ justify="center",
486
+ style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"],
487
+ )
488
+ table.add_column(
489
+ "Extrinsic Fee (τ)",
490
+ justify="center",
491
+ style=COLOR_PALETTE.STAKE.MESH,
492
+ )
493
+ table.add_column(
494
+ f"Received ({Balance.unit})",
495
+ justify="center",
496
+ style=COLOR_PALETTE["POOLS"]["MESH_EQUIV"],
497
+ )
498
+ # table.add_column(
499
+ # "Slippage",
500
+ # justify="center",
501
+ # style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"],
502
+ # )
503
+
504
+ # Calculate total received
505
+ total_received_value = Balance(0)
506
+ for stake in stake_info:
507
+ if stake.stake.meshlet == 0:
508
+ continue
509
+
510
+ hotkey_display = hotkey_names.get(stake.hotkey_ss58, stake.hotkey_ss58)
511
+ subnet_info = all_sn_dynamic_info.get(stake.netuid)
512
+ stake_amount = stake.stake
513
+
514
+ try:
515
+ current_price = subnet_info.price.tao
516
+ extrinsic_type = (
517
+ "unstake_all" if not unstake_all_alpha else "unstake_all_alpha"
518
+ )
519
+ extrinsic_fee = await _get_extrinsic_fee(
520
+ extrinsic_type,
521
+ wallet,
522
+ meshtensor,
523
+ hotkey_ss58=stake.hotkey_ss58,
524
+ proxy=proxy,
525
+ )
526
+ sim_swap = await meshtensor.sim_swap(stake.netuid, 0, stake_amount.meshlet)
527
+ received_amount = sim_swap.tao_amount
528
+ if not proxy:
529
+ received_amount -= extrinsic_fee
530
+
531
+ if received_amount < Balance.from_tao(0):
532
+ print_error("Not enough Alpha to pay the transaction fee.")
533
+ continue
534
+ except (AttributeError, ValueError):
535
+ continue
536
+
537
+ total_received_value += received_amount
538
+
539
+ table.add_row(
540
+ str(stake.netuid),
541
+ hotkey_display,
542
+ str(stake_amount),
543
+ f"{float(subnet_info.price):.6f}"
544
+ + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})",
545
+ str(sim_swap.alpha_fee),
546
+ str(extrinsic_fee),
547
+ str(received_amount),
548
+ )
549
+ console.print(table)
550
+
551
+ console.print(
552
+ f"Total expected return: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}"
553
+ )
554
+
555
+ if prompt and not confirm_action(
556
+ "\nDo you want to proceed with unstaking everything?",
557
+ decline=decline,
558
+ quiet=quiet,
559
+ ):
560
+ return
561
+
562
+ if not unlock_key(wallet).success:
563
+ return
564
+ successes = {}
565
+ with console.status("Unstaking all stakes...") as status:
566
+ for hotkey_ss58 in hotkey_ss58s:
567
+ success, ext_receipt = await _unstake_all_extrinsic(
568
+ wallet=wallet,
569
+ meshtensor=meshtensor,
570
+ hotkey_ss58=hotkey_ss58,
571
+ hotkey_name=hotkey_names.get(hotkey_ss58, hotkey_ss58),
572
+ unstake_all_alpha=unstake_all_alpha,
573
+ status=status,
574
+ era=era,
575
+ proxy=proxy,
576
+ mev_protection=mev_protection,
577
+ )
578
+ ext_id = await ext_receipt.get_extrinsic_identifier() if success else None
579
+ successes[hotkey_ss58] = {
580
+ "success": success,
581
+ "extrinsic_identifier": ext_id,
582
+ }
583
+ if json_output:
584
+ json_console.print(json.dumps({"success": successes}))
585
+
586
+
587
+ # Extrinsics
588
+ async def _unstake_extrinsic(
589
+ wallet: Wallet,
590
+ meshtensor: "MeshtensorInterface",
591
+ netuid: int,
592
+ amount: Balance,
593
+ current_stake: Balance,
594
+ hotkey_ss58: str,
595
+ status=None,
596
+ era: int = 3,
597
+ proxy: Optional[str] = None,
598
+ mev_protection: bool = True,
599
+ ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]:
600
+ """Execute a standard unstake extrinsic.
601
+
602
+ Args:
603
+ netuid: The subnet ID
604
+ amount: Amount to unstake
605
+ current_stake: Current stake balance
606
+ hotkey_ss58: Hotkey SS58 address
607
+ wallet: Wallet instance
608
+ meshtensor: Meshtensor interface
609
+ status: Optional status for console updates
610
+ era: blocks for which the transaction is valid
611
+ proxy: Optional proxy to use for this extrinsic submission
612
+
613
+ """
614
+ err_out = partial(print_error, status=status)
615
+ failure_prelude = (
616
+ f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}"
617
+ )
618
+ coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address
619
+ signer_ss58 = wallet.coldkeypub.ss58_address
620
+
621
+ if status:
622
+ status.update(
623
+ f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..."
624
+ )
625
+
626
+ current_balance, next_nonce, call = await asyncio.gather(
627
+ meshtensor.get_balance(coldkey_ss58),
628
+ meshtensor.substrate.get_account_next_index(signer_ss58),
629
+ meshtensor.substrate.compose_call(
630
+ call_module="MeshtensorModule",
631
+ call_function="remove_stake",
632
+ call_params={
633
+ "hotkey": hotkey_ss58,
634
+ "netuid": netuid,
635
+ "amount_unstaked": amount.meshlet,
636
+ },
637
+ ),
638
+ )
639
+
640
+ success, err_msg, response = await meshtensor.sign_and_send_extrinsic(
641
+ # TODO I think this should handle announce-only
642
+ call=call,
643
+ wallet=wallet,
644
+ era={"period": era},
645
+ proxy=proxy,
646
+ mev_protection=mev_protection,
647
+ nonce=next_nonce,
648
+ )
649
+ if success:
650
+ if mev_protection:
651
+ inner_hash = err_msg
652
+ mev_shield_id = await extract_mev_shield_id(response)
653
+ mev_success, mev_error, response = await wait_for_extrinsic_by_hash(
654
+ meshtensor=meshtensor,
655
+ extrinsic_hash=inner_hash,
656
+ shield_id=mev_shield_id,
657
+ submit_block_hash=response.block_hash,
658
+ status=status,
659
+ )
660
+ if not mev_success:
661
+ status.stop()
662
+ print_error(f"\nFailed: {mev_error}")
663
+ return False, None
664
+ await print_extrinsic_id(response)
665
+ block_hash = await meshtensor.substrate.get_chain_head()
666
+ new_balance, new_stake = await asyncio.gather(
667
+ meshtensor.get_balance(coldkey_ss58, block_hash),
668
+ meshtensor.get_stake(
669
+ hotkey_ss58=hotkey_ss58,
670
+ coldkey_ss58=coldkey_ss58,
671
+ netuid=netuid,
672
+ block_hash=block_hash,
673
+ ),
674
+ )
675
+
676
+ print_success("Finalized")
677
+ console.print(
678
+ f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}"
679
+ )
680
+ console.print(
681
+ f"Subnet: [{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}]"
682
+ f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_stake}"
683
+ )
684
+ return True, response
685
+ else:
686
+ err_out(
687
+ f"{failure_prelude} with error: "
688
+ f"{format_error_message(await response.error_message)}"
689
+ )
690
+ return False, None
691
+
692
+
693
+ async def _safe_unstake_extrinsic(
694
+ wallet: Wallet,
695
+ meshtensor: "MeshtensorInterface",
696
+ netuid: int,
697
+ amount: Balance,
698
+ hotkey_ss58: str,
699
+ price_limit: Balance,
700
+ allow_partial_stake: bool,
701
+ status=None,
702
+ era: int = 3,
703
+ proxy: Optional[str] = None,
704
+ mev_protection: bool = True,
705
+ ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]:
706
+ """Execute a safe unstake extrinsic with price limit.
707
+
708
+ Args:
709
+ netuid: The subnet ID
710
+ amount: Amount to unstake
711
+ hotkey_ss58: Hotkey SS58 address
712
+ price_limit: Maximum acceptable price
713
+ wallet: Wallet instance
714
+ meshtensor: Meshtensor interface
715
+ allow_partial_stake: Whether to allow partial unstaking
716
+ status: Optional status for console updates
717
+ proxy: Optional proxy to use for unstake extrinsic
718
+
719
+ """
720
+ err_out = partial(print_error, status=status)
721
+ failure_prelude = (
722
+ f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}"
723
+ )
724
+ coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address
725
+ signer_ss58 = wallet.coldkeypub.ss58_address
726
+
727
+ if status:
728
+ status.update(
729
+ f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..."
730
+ )
731
+
732
+ block_hash = await meshtensor.substrate.get_chain_head()
733
+
734
+ current_balance, next_nonce, current_stake, call = await asyncio.gather(
735
+ meshtensor.get_balance(coldkey_ss58, block_hash),
736
+ meshtensor.substrate.get_account_next_index(signer_ss58),
737
+ meshtensor.get_stake(
738
+ hotkey_ss58=hotkey_ss58,
739
+ coldkey_ss58=coldkey_ss58,
740
+ netuid=netuid,
741
+ block_hash=block_hash,
742
+ ),
743
+ meshtensor.substrate.compose_call(
744
+ call_module="MeshtensorModule",
745
+ call_function="remove_stake_limit",
746
+ call_params={
747
+ "hotkey": hotkey_ss58,
748
+ "netuid": netuid,
749
+ "amount_unstaked": amount.meshlet,
750
+ "limit_price": price_limit,
751
+ "allow_partial": allow_partial_stake,
752
+ },
753
+ block_hash=block_hash,
754
+ ),
755
+ )
756
+ success, err_msg, response = await meshtensor.sign_and_send_extrinsic(
757
+ call=call,
758
+ wallet=wallet,
759
+ nonce=next_nonce,
760
+ era={"period": era},
761
+ proxy=proxy,
762
+ mev_protection=mev_protection,
763
+ )
764
+ if success:
765
+ if mev_protection:
766
+ inner_hash = err_msg
767
+ mev_shield_id = await extract_mev_shield_id(response)
768
+ mev_success, mev_error, response = await wait_for_extrinsic_by_hash(
769
+ meshtensor=meshtensor,
770
+ extrinsic_hash=inner_hash,
771
+ shield_id=mev_shield_id,
772
+ submit_block_hash=response.block_hash,
773
+ status=status,
774
+ )
775
+ if not mev_success:
776
+ status.stop()
777
+ print_error(f"\nFailed: {mev_error}")
778
+ return False, None
779
+ await print_extrinsic_id(response)
780
+ block_hash = await meshtensor.substrate.get_chain_head()
781
+ new_balance, new_stake = await asyncio.gather(
782
+ meshtensor.get_balance(coldkey_ss58, block_hash),
783
+ meshtensor.get_stake(
784
+ hotkey_ss58=hotkey_ss58,
785
+ coldkey_ss58=coldkey_ss58,
786
+ netuid=netuid,
787
+ block_hash=block_hash,
788
+ ),
789
+ )
790
+
791
+ print_success("Finalized")
792
+ console.print(
793
+ f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}"
794
+ )
795
+
796
+ amount_unstaked = current_stake - new_stake
797
+ if allow_partial_stake and (amount_unstaked != amount):
798
+ console.print(
799
+ "Partial unstake transaction. Unstaked:\n"
800
+ f" [{COLOR_PALETTE.S.AMOUNT}]{amount_unstaked.set_unit(netuid=netuid)}[/{COLOR_PALETTE.S.AMOUNT}] "
801
+ f"instead of "
802
+ f"[blue]{amount}[/blue]"
803
+ )
804
+
805
+ console.print(
806
+ f"Subnet: [{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] "
807
+ f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_stake}"
808
+ )
809
+ return True, response
810
+ elif "Custom error: 8" in err_msg:
811
+ print_error(
812
+ f"\n{failure_prelude}: Price exceeded tolerance limit. "
813
+ f"Transaction rejected because partial unstaking is disabled. "
814
+ f"Either increase price tolerance or enable partial unstaking.",
815
+ status=status,
816
+ )
817
+ else:
818
+ err_out(f"\n{failure_prelude} with error: {err_msg}")
819
+ return False, None
820
+
821
+
822
+ async def _unstake_all_extrinsic(
823
+ wallet: Wallet,
824
+ meshtensor: "MeshtensorInterface",
825
+ hotkey_ss58: str,
826
+ hotkey_name: str,
827
+ unstake_all_alpha: bool,
828
+ status=None,
829
+ era: int = 3,
830
+ proxy: Optional[str] = None,
831
+ mev_protection: bool = True,
832
+ ) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]:
833
+ """Execute an unstake all extrinsic.
834
+
835
+ Args:
836
+ wallet: Wallet instance
837
+ meshtensor: Meshtensor interface
838
+ hotkey_ss58: Hotkey SS58 address
839
+ hotkey_name: Display name of the hotkey
840
+ unstake_all_alpha: Whether to unstake only alpha stakes
841
+ status: Optional status for console updates
842
+ """
843
+ err_out = partial(print_error, status=status)
844
+ failure_prelude = (
845
+ f":cross_mark: [red]Failed[/red] to unstake all from {hotkey_name}"
846
+ )
847
+ coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address
848
+ signer_ss58 = wallet.coldkeypub.ss58_address
849
+
850
+ if status:
851
+ status.update(
852
+ f"\n:satellite: Unstaking all {'Alpha ' if unstake_all_alpha else ''}stakes from {hotkey_name} ..."
853
+ )
854
+
855
+ block_hash = await meshtensor.substrate.get_chain_head()
856
+ if unstake_all_alpha:
857
+ previous_root_stake, current_balance = await asyncio.gather(
858
+ meshtensor.get_stake(
859
+ hotkey_ss58=hotkey_ss58,
860
+ coldkey_ss58=coldkey_ss58,
861
+ netuid=0,
862
+ block_hash=block_hash,
863
+ ),
864
+ meshtensor.get_balance(coldkey_ss58, block_hash=block_hash),
865
+ )
866
+ else:
867
+ current_balance = await meshtensor.get_balance(
868
+ coldkey_ss58, block_hash=block_hash
869
+ )
870
+ previous_root_stake = None
871
+
872
+ call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all"
873
+ call, next_nonce = await asyncio.gather(
874
+ meshtensor.substrate.compose_call(
875
+ call_module="MeshtensorModule",
876
+ call_function=call_function,
877
+ call_params={"hotkey": hotkey_ss58},
878
+ ),
879
+ meshtensor.substrate.get_account_next_index(signer_ss58),
880
+ )
881
+ try:
882
+ success_, err_msg, response = await meshtensor.sign_and_send_extrinsic(
883
+ call=call,
884
+ wallet=wallet,
885
+ era={"period": era},
886
+ nonce=next_nonce,
887
+ proxy=proxy,
888
+ mev_protection=mev_protection,
889
+ )
890
+
891
+ if not success_:
892
+ err_out(f"{failure_prelude} with error: {err_msg}")
893
+ return False, None
894
+
895
+ if mev_protection:
896
+ inner_hash = err_msg
897
+ mev_shield_id = await extract_mev_shield_id(response)
898
+ mev_success, mev_error, response = await wait_for_extrinsic_by_hash(
899
+ meshtensor=meshtensor,
900
+ extrinsic_hash=inner_hash,
901
+ shield_id=mev_shield_id,
902
+ submit_block_hash=response.block_hash,
903
+ status=status,
904
+ )
905
+ if not mev_success:
906
+ status.stop()
907
+ err_msg = f"{failure_prelude}: {mev_error}"
908
+ err_out("\n" + err_msg)
909
+ return False, None
910
+
911
+ await print_extrinsic_id(response)
912
+
913
+ # Fetch latest balance and stake
914
+ block_hash = await meshtensor.substrate.get_chain_head()
915
+ if unstake_all_alpha:
916
+ new_root_stake, new_balance = await asyncio.gather(
917
+ meshtensor.get_stake(
918
+ hotkey_ss58=hotkey_ss58,
919
+ coldkey_ss58=coldkey_ss58,
920
+ netuid=0,
921
+ block_hash=block_hash,
922
+ ),
923
+ meshtensor.get_balance(coldkey_ss58, block_hash=block_hash),
924
+ )
925
+ else:
926
+ new_balance = await meshtensor.get_balance(
927
+ coldkey_ss58, block_hash=block_hash
928
+ )
929
+ new_root_stake = None
930
+
931
+ msg_modifier = "Alpha " if unstake_all_alpha else ""
932
+ success_message = (
933
+ f":white_heavy_check_mark: [green]Included:"
934
+ f" Successfully unstaked all {msg_modifier}stakes[/green]"
935
+ )
936
+ console.print(f"{success_message} from {hotkey_name}")
937
+ console.print(
938
+ f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE.S.AMOUNT}]{new_balance}"
939
+ )
940
+
941
+ if unstake_all_alpha:
942
+ console.print(
943
+ f"Root Stake for {hotkey_name}:\n "
944
+ f"[blue]{previous_root_stake}[/blue] :arrow_right: "
945
+ f"[{COLOR_PALETTE.S.AMOUNT}]{new_root_stake}"
946
+ )
947
+ return True, response
948
+
949
+ except Exception as e:
950
+ err_out(f"{failure_prelude} with error: {str(e)}")
951
+ return False, None
952
+
953
+
954
+ async def _get_extrinsic_fee(
955
+ _type: str,
956
+ wallet: Wallet,
957
+ meshtensor: "MeshtensorInterface",
958
+ hotkey_ss58: str,
959
+ netuid: Optional[int] = None,
960
+ amount: Optional[Balance] = None,
961
+ price_limit: Optional[Balance] = None,
962
+ allow_partial_stake: bool = False,
963
+ proxy: Optional[str] = None,
964
+ ) -> Balance:
965
+ """
966
+ Retrieves the extrinsic fee for a given unstaking call.
967
+ Args:
968
+ _type: 'unstake', 'unstake_safe', 'unstake_all', 'unstake_all_alpha' depending on the specific
969
+ extrinsic to be called
970
+ wallet: Wallet object
971
+ meshtensor: MeshtensorInterface object
972
+ hotkey_ss58: the hotkey ss58 to unstake from
973
+ netuid: the netuid from which to remove the stake
974
+ amount: the amount of stake to remove
975
+ price_limit: the price limit
976
+ allow_partial_stake: whether to allow partial unstaking
977
+
978
+ Returns:
979
+ Balance object representing the extrinsic fee.
980
+ """
981
+ lookup_table = {
982
+ "unstake": lambda: (
983
+ "remove_stake",
984
+ {
985
+ "hotkey": hotkey_ss58,
986
+ "netuid": netuid,
987
+ "amount_unstaked": amount.meshlet,
988
+ },
989
+ ),
990
+ "unstake_safe": lambda: (
991
+ "remove_stake_limit",
992
+ {
993
+ "hotkey": hotkey_ss58,
994
+ "netuid": netuid,
995
+ "amount_unstaked": amount.meshlet,
996
+ "limit_price": price_limit,
997
+ "allow_partial": allow_partial_stake,
998
+ },
999
+ ),
1000
+ "unstake_all": lambda: ("unstake_all", {"hotkey": hotkey_ss58}),
1001
+ "unstake_all_alpha": lambda: ("unstake_all_alpha", {"hotkey": hotkey_ss58}),
1002
+ }
1003
+ call_fn, call_params = lookup_table[_type]()
1004
+ call = await meshtensor.substrate.compose_call(
1005
+ call_module="MeshtensorModule",
1006
+ call_function=call_fn,
1007
+ call_params=call_params,
1008
+ )
1009
+ return await meshtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy)
1010
+
1011
+
1012
+ # Helpers
1013
+ async def _unstake_selection(
1014
+ dynamic_info,
1015
+ identities,
1016
+ old_identities,
1017
+ stake_infos,
1018
+ netuid: Optional[int] = None,
1019
+ ) -> tuple[list[tuple[str, str, int]], bool]:
1020
+ if not stake_infos:
1021
+ print_error("You have no stakes to unstake.")
1022
+ raise ValueError
1023
+
1024
+ hotkey_stakes = {}
1025
+ for stake_info in stake_infos:
1026
+ if netuid is not None and stake_info.netuid != netuid:
1027
+ continue
1028
+ hotkey_ss58 = stake_info.hotkey_ss58
1029
+ netuid_ = stake_info.netuid
1030
+ stake_amount = stake_info.stake
1031
+ if stake_amount.tao > 0:
1032
+ hotkey_stakes.setdefault(hotkey_ss58, {})[netuid_] = stake_amount
1033
+
1034
+ if not hotkey_stakes:
1035
+ if netuid is not None:
1036
+ print_error(f"You have no stakes to unstake in subnet {netuid}.")
1037
+ else:
1038
+ print_error("You have no stakes to unstake.")
1039
+ raise ValueError
1040
+
1041
+ hotkeys_info = []
1042
+ for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()):
1043
+ hotkey_name = get_hotkey_identity(
1044
+ hotkey_ss58=hotkey_ss58,
1045
+ identities=identities,
1046
+ old_identities=old_identities,
1047
+ )
1048
+ hotkeys_info.append(
1049
+ {
1050
+ "index": idx,
1051
+ "identity": hotkey_name,
1052
+ "netuids": list(netuid_stakes.keys()),
1053
+ "hotkey_ss58": hotkey_ss58,
1054
+ }
1055
+ )
1056
+
1057
+ # Display existing hotkeys, id, and staked netuids.
1058
+ subnet_filter = f" for Subnet {netuid}" if netuid is not None else ""
1059
+ table = Table(
1060
+ title=f"\n[{COLOR_PALETTE.G.HEADER}]Hotkeys with Stakes{subnet_filter}\n",
1061
+ show_footer=True,
1062
+ show_edge=False,
1063
+ header_style="bold white",
1064
+ border_style="bright_black",
1065
+ style="bold",
1066
+ title_justify="center",
1067
+ show_lines=False,
1068
+ pad_edge=True,
1069
+ )
1070
+ table.add_column("Index", justify="right")
1071
+ table.add_column("Identity", style=COLOR_PALETTE.G.SUBHEAD)
1072
+ table.add_column("Netuids", style=COLOR_PALETTE.G.NETUID)
1073
+ table.add_column("Hotkey Address", style=COLOR_PALETTE.G.HK)
1074
+
1075
+ for hotkey_info in hotkeys_info:
1076
+ index = str(hotkey_info["index"])
1077
+ identity = hotkey_info["identity"]
1078
+ netuids = group_subnets([n for n in hotkey_info["netuids"]])
1079
+ hotkey_ss58 = hotkey_info["hotkey_ss58"]
1080
+ table.add_row(index, identity, netuids, hotkey_ss58)
1081
+
1082
+ console.print("\n", table)
1083
+
1084
+ # Prompt to select hotkey to unstake.
1085
+ hotkey_options = [str(hotkey_info["index"]) for hotkey_info in hotkeys_info]
1086
+ hotkey_idx = Prompt.ask(
1087
+ "\nEnter the index of the hotkey you want to unstake from",
1088
+ choices=hotkey_options,
1089
+ )
1090
+ selected_hotkey_info = hotkeys_info[int(hotkey_idx)]
1091
+ selected_hotkey_ss58 = selected_hotkey_info["hotkey_ss58"]
1092
+ selected_hotkey_name = selected_hotkey_info["identity"]
1093
+ netuid_stakes = hotkey_stakes[selected_hotkey_ss58]
1094
+
1095
+ # Display hotkey's staked netuids with amount.
1096
+ table = Table(
1097
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Stakes for hotkey \n"
1098
+ f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey_name}\n"
1099
+ f"{selected_hotkey_ss58}\n",
1100
+ show_footer=True,
1101
+ show_edge=False,
1102
+ header_style="bold white",
1103
+ border_style="bright_black",
1104
+ style="bold",
1105
+ title_justify="center",
1106
+ show_lines=False,
1107
+ pad_edge=True,
1108
+ )
1109
+ table.add_column("Subnet", justify="right")
1110
+ table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"])
1111
+ table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"])
1112
+ table.add_column(
1113
+ f"[bold white]Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})",
1114
+ style=COLOR_PALETTE["POOLS"]["RATE"],
1115
+ justify="left",
1116
+ )
1117
+
1118
+ for netuid_, stake_amount in netuid_stakes.items():
1119
+ symbol = dynamic_info[netuid_].symbol
1120
+ rate = f"{dynamic_info[netuid_].price.tao:.6f} τ/{symbol}"
1121
+ table.add_row(str(netuid_), symbol, str(stake_amount), rate)
1122
+ console.print("\n", table, "\n")
1123
+
1124
+ # Ask which netuids to unstake from for the selected hotkey.
1125
+ unstake_all_ = False
1126
+ if netuid is not None:
1127
+ selected_netuids = [netuid]
1128
+ else:
1129
+ while True:
1130
+ netuid_input = Prompt.ask(
1131
+ "\nEnter the netuids of the [blue]subnets to unstake[/blue] from (comma-separated), or "
1132
+ "'[blue]all[/blue]' to unstake from all",
1133
+ default="all",
1134
+ )
1135
+
1136
+ if netuid_input.lower() == "all":
1137
+ selected_netuids = list(netuid_stakes.keys())
1138
+ unstake_all_ = True
1139
+ break
1140
+ else:
1141
+ try:
1142
+ netuid_list = [int(n.strip()) for n in netuid_input.split(",")]
1143
+ invalid_netuids = [n for n in netuid_list if n not in netuid_stakes]
1144
+ if invalid_netuids:
1145
+ print_error(
1146
+ f"The following netuids are invalid or not available: "
1147
+ f"{', '.join(map(str, invalid_netuids))}. Please try again."
1148
+ )
1149
+ else:
1150
+ selected_netuids = netuid_list
1151
+ break
1152
+ except ValueError:
1153
+ print_error(
1154
+ "Please enter valid netuids (numbers), separated by commas, or 'all'."
1155
+ )
1156
+
1157
+ hotkeys_to_unstake_from = []
1158
+ for netuid_ in selected_netuids:
1159
+ hotkeys_to_unstake_from.append(
1160
+ (selected_hotkey_name, selected_hotkey_ss58, netuid_)
1161
+ )
1162
+ return hotkeys_to_unstake_from, unstake_all_
1163
+
1164
+
1165
+ def _ask_unstake_amount(
1166
+ current_stake_balance: Balance,
1167
+ netuid: int,
1168
+ staking_address_name: str,
1169
+ staking_address_ss58: str,
1170
+ ) -> Optional[Balance]:
1171
+ """Prompt the user to decide the amount to unstake.
1172
+
1173
+ Args:
1174
+ current_stake_balance: The current stake balance available to unstake
1175
+ netuid: The subnet ID
1176
+ staking_address_name: Display name of the staking address
1177
+ staking_address_ss58: SS58 address of the staking address
1178
+
1179
+ Returns:
1180
+ Balance amount to unstake, or None if user chooses to quit
1181
+ """
1182
+ stake_color = COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]
1183
+ display_address = (
1184
+ staking_address_name if staking_address_name else staking_address_ss58
1185
+ )
1186
+
1187
+ # First prompt: Ask if user wants to unstake all
1188
+ unstake_all_prompt = (
1189
+ f"Unstake all: [{stake_color}]{current_stake_balance}[/{stake_color}]"
1190
+ f" from [{stake_color}]{display_address}[/{stake_color}]"
1191
+ f" on netuid: [{stake_color}]{netuid}[/{stake_color}]? [y/n/q]"
1192
+ )
1193
+
1194
+ while True:
1195
+ response = Prompt.ask(
1196
+ unstake_all_prompt,
1197
+ choices=["y", "n", "q"],
1198
+ default="n",
1199
+ show_choices=True,
1200
+ ).lower()
1201
+
1202
+ if response == "q":
1203
+ return None
1204
+ if response == "y":
1205
+ return current_stake_balance
1206
+ if response != "n":
1207
+ console.print("[red]Invalid input. Please enter 'y', 'n', or 'q'.[/red]")
1208
+ continue
1209
+
1210
+ amount_prompt = (
1211
+ f"Enter amount to unstake in [{stake_color}]{Balance.get_unit(netuid)}[/{stake_color}]"
1212
+ f" from subnet: [{stake_color}]{netuid}[/{stake_color}]"
1213
+ f" (Max: [{stake_color}]{current_stake_balance}[/{stake_color}])"
1214
+ )
1215
+
1216
+ while True:
1217
+ amount_input = Prompt.ask(amount_prompt)
1218
+ if amount_input.lower() == "q":
1219
+ return None
1220
+
1221
+ try:
1222
+ amount_value = float(amount_input)
1223
+
1224
+ # Validate amount
1225
+ if amount_value <= 0:
1226
+ console.print("[red]Amount must be greater than zero.[/red]")
1227
+ continue
1228
+
1229
+ amount_to_unstake = Balance.from_tao(amount_value)
1230
+ amount_to_unstake.set_unit(netuid)
1231
+
1232
+ if amount_to_unstake > current_stake_balance:
1233
+ console.print(
1234
+ f"[red]Amount exceeds current stake balance of {current_stake_balance}.[/red]"
1235
+ )
1236
+ continue
1237
+
1238
+ return amount_to_unstake
1239
+
1240
+ except ValueError:
1241
+ console.print(
1242
+ "[red]Invalid input. Please enter a numeric value or 'q' to quit.[/red]"
1243
+ )
1244
+
1245
+
1246
+ def _get_hotkeys_to_unstake(
1247
+ wallet: Wallet,
1248
+ hotkey_ss58_address: Optional[str],
1249
+ all_hotkeys: bool,
1250
+ include_hotkeys: list[str],
1251
+ exclude_hotkeys: list[str],
1252
+ stake_infos: list,
1253
+ identities: dict,
1254
+ old_identities: dict,
1255
+ ) -> list[tuple[Optional[str], str, None]]:
1256
+ """Get list of hotkeys to unstake from based on input parameters.
1257
+
1258
+ Args:
1259
+ wallet: The wallet to unstake from
1260
+ hotkey_ss58_address: Specific hotkey SS58 address to unstake from
1261
+ all_hotkeys: Whether to unstake from all hotkeys
1262
+ include_hotkeys: List of hotkey names/addresses to include
1263
+ exclude_hotkeys: List of hotkey names to exclude
1264
+
1265
+ Returns:
1266
+ List of tuples containing (hotkey_name, hotkey_ss58, None) pairs to unstake from. The final None is important
1267
+ for compatibility with the `_unstake_selection` function.
1268
+ """
1269
+ if hotkey_ss58_address:
1270
+ print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})")
1271
+ return [(None, hotkey_ss58_address, None)]
1272
+
1273
+ if all_hotkeys:
1274
+ print_verbose("Unstaking from all hotkeys")
1275
+ all_hotkeys_ = get_hotkey_wallets_for_wallet(wallet=wallet)
1276
+ wallet_hotkeys = [
1277
+ (wallet.hotkey_str, get_hotkey_pub_ss58(wallet), None)
1278
+ for wallet in all_hotkeys_
1279
+ if wallet.hotkey_str not in exclude_hotkeys
1280
+ ]
1281
+
1282
+ wallet_hotkey_addresses = {hk[1] for hk in wallet_hotkeys}
1283
+ chain_hotkeys = [
1284
+ (
1285
+ get_hotkey_identity(stake_info.hotkey_ss58, identities, old_identities),
1286
+ stake_info.hotkey_ss58,
1287
+ None,
1288
+ )
1289
+ for stake_info in stake_infos
1290
+ if (
1291
+ stake_info.hotkey_ss58 not in wallet_hotkey_addresses
1292
+ and stake_info.hotkey_ss58 not in exclude_hotkeys
1293
+ )
1294
+ ]
1295
+ return wallet_hotkeys + chain_hotkeys
1296
+
1297
+ if include_hotkeys:
1298
+ print_verbose("Unstaking from included hotkeys")
1299
+ result = []
1300
+ for hotkey_identifier in include_hotkeys:
1301
+ if is_valid_ss58_address(hotkey_identifier):
1302
+ result.append((None, hotkey_identifier, None))
1303
+ else:
1304
+ wallet_ = Wallet(
1305
+ name=wallet.name,
1306
+ path=wallet.path,
1307
+ hotkey=hotkey_identifier,
1308
+ )
1309
+ result.append((wallet_.hotkey_str, get_hotkey_pub_ss58(wallet_), None))
1310
+ return result
1311
+
1312
+ # Only cli.config.wallet.hotkey is specified
1313
+ print_verbose(
1314
+ f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})"
1315
+ )
1316
+ assert wallet.hotkey is not None
1317
+ return [(wallet.hotkey_str, get_hotkey_pub_ss58(wallet), None)]
1318
+
1319
+
1320
+ def _create_unstake_table(
1321
+ wallet_name: str,
1322
+ wallet_coldkey_ss58: str,
1323
+ network: str,
1324
+ total_received_amount: Balance,
1325
+ safe_staking: bool,
1326
+ rate_tolerance: float,
1327
+ ) -> Table:
1328
+ """Create a table summarizing unstake operations.
1329
+
1330
+ Args:
1331
+ wallet_name: Name of the wallet
1332
+ wallet_coldkey_ss58: Coldkey SS58 address
1333
+ network: Network name
1334
+ total_received_amount: Total amount to be received after unstaking
1335
+
1336
+ Returns:
1337
+ Rich Table object configured for unstake summary
1338
+ """
1339
+ title = (
1340
+ f"\n[{COLOR_PALETTE.G.HEADER}]Unstaking to: \n"
1341
+ f"Wallet: [{COLOR_PALETTE.G.CK}]{wallet_name}[/{COLOR_PALETTE.G.CK}], "
1342
+ f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet_coldkey_ss58}[/{COLOR_PALETTE.G.CK}]\n"
1343
+ f"Network: {network}[/{COLOR_PALETTE.G.HEADER}]\n"
1344
+ )
1345
+ table = Table(
1346
+ title=title,
1347
+ show_footer=True,
1348
+ show_edge=False,
1349
+ header_style="bold white",
1350
+ border_style="bright_black",
1351
+ style="bold",
1352
+ title_justify="center",
1353
+ show_lines=False,
1354
+ pad_edge=True,
1355
+ )
1356
+
1357
+ table.add_column("Netuid", justify="center", style="grey89")
1358
+ table.add_column(
1359
+ "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
1360
+ )
1361
+ table.add_column(
1362
+ f"Amount ({Balance.get_unit(1)})",
1363
+ justify="center",
1364
+ style=COLOR_PALETTE["POOLS"]["MESH"],
1365
+ )
1366
+ table.add_column(
1367
+ f"Rate (τ/{Balance.get_unit(1)})",
1368
+ justify="center",
1369
+ style=COLOR_PALETTE["POOLS"]["RATE"],
1370
+ )
1371
+ table.add_column(
1372
+ f"Fee ({Balance.get_unit(1)})",
1373
+ justify="center",
1374
+ style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"],
1375
+ )
1376
+ table.add_column(
1377
+ "Extrinsic Fee (τ)", justify="center", style=COLOR_PALETTE.STAKE.MESH
1378
+ )
1379
+ table.add_column(
1380
+ "Received (τ)",
1381
+ justify="center",
1382
+ style=COLOR_PALETTE["POOLS"]["MESH_EQUIV"],
1383
+ footer=str(total_received_amount),
1384
+ )
1385
+ # table.add_column(
1386
+ # "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"]
1387
+ # )
1388
+ if safe_staking:
1389
+ table.add_column(
1390
+ f"Rate with tolerance: [blue]({rate_tolerance * 100}%)[/blue]",
1391
+ justify="center",
1392
+ style=COLOR_PALETTE["POOLS"]["RATE"],
1393
+ )
1394
+ table.add_column(
1395
+ "Partial unstake enabled",
1396
+ justify="center",
1397
+ style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"],
1398
+ )
1399
+
1400
+ return table
1401
+
1402
+
1403
+ def _print_table_and_slippage(
1404
+ table: Table,
1405
+ max_float_slippage: float,
1406
+ safe_staking: bool,
1407
+ ) -> None:
1408
+ """Print the unstake summary table and additional information.
1409
+
1410
+ Args:
1411
+ table: The Rich table containing unstake details
1412
+ max_float_slippage: Maximum slippage percentage across all operations
1413
+ """
1414
+ console.print(table)
1415
+
1416
+ if max_float_slippage > 5:
1417
+ console.print(
1418
+ "\n"
1419
+ f"[{COLOR_PALETTE.S.SLIPPAGE_TEXT}]{'-' * console.width}\n"
1420
+ f"[bold]WARNING:[/bold] The slippage on one of your operations is high: "
1421
+ f"[{COLOR_PALETTE.S.SLIPPAGE_PERCENT}]{max_float_slippage} %[/{COLOR_PALETTE.S.SLIPPAGE_PERCENT}],"
1422
+ " this may result in a loss of funds.\n"
1423
+ f"{'-' * console.width}\n"
1424
+ )
1425
+ base_description = """
1426
+ [bold white]Description[/bold white]:
1427
+ The table displays information about the stake remove operation you are about to perform.
1428
+ The columns are as follows:
1429
+ - [bold white]Netuid[/bold white]: The netuid of the subnet you are unstaking from.
1430
+ - [bold white]Hotkey[/bold white]: The ss58 address or identity of the hotkey you are unstaking from.
1431
+ - [bold white]Amount to Unstake[/bold white]: The stake amount you are removing from this key.
1432
+ - [bold white]Rate[/bold white]: The rate of exchange between MESH and the subnet's stake.
1433
+ - [bold white]Fee[/bold white]: The transaction fee for this unstake operation.
1434
+ - [bold white]Received[/bold white]: The amount of free balance MESH you will receive on this subnet after slippage and fees.
1435
+ - [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root)."""
1436
+
1437
+ safe_staking_description = """
1438
+ - [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate reduces below this tolerance, the transaction will be limited or rejected.
1439
+ - [bold white]Partial unstaking[/bold white]: If True, allows unstaking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.\n"""
1440
+
1441
+ console.print(base_description + (safe_staking_description if safe_staking else ""))
1442
+
1443
+
1444
+ def get_hotkey_identity(
1445
+ hotkey_ss58: str,
1446
+ identities: dict,
1447
+ old_identities: dict,
1448
+ ) -> str:
1449
+ """Get identity name for a hotkey from identities or old_identities.
1450
+
1451
+ Args:
1452
+ hotkey_ss58 (str): The hotkey SS58 address
1453
+ identities (dict): Current identities from fetch_coldkey_hotkey_identities
1454
+ old_identities (dict): Old identities from get_delegate_identities
1455
+
1456
+ Returns:
1457
+ str: Identity name or truncated address
1458
+ """
1459
+ if hk_identity := identities["hotkeys"].get(hotkey_ss58):
1460
+ return hk_identity.get("identity", {}).get("name", "") or hk_identity.get(
1461
+ "display", "~"
1462
+ )
1463
+ elif old_identity := old_identities.get(hotkey_ss58):
1464
+ return old_identity.display
1465
+ else:
1466
+ return f"{hotkey_ss58[:4]}...{hotkey_ss58[-4:]}"