bittensor-cli 9.0.0rc1__py3-none-any.whl → 9.0.0rc3__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.
- bittensor_cli/__init__.py +1 -1
- bittensor_cli/cli.py +440 -157
- bittensor_cli/src/__init__.py +4 -1
- bittensor_cli/src/bittensor/subtensor_interface.py +1 -1
- bittensor_cli/src/bittensor/utils.py +14 -0
- bittensor_cli/src/commands/stake/add.py +625 -0
- bittensor_cli/src/commands/stake/children_hotkeys.py +2 -4
- bittensor_cli/src/commands/stake/list.py +687 -0
- bittensor_cli/src/commands/stake/move.py +1 -1
- bittensor_cli/src/commands/stake/remove.py +1146 -0
- bittensor_cli/src/commands/subnets/subnets.py +20 -8
- bittensor_cli/src/commands/wallets.py +24 -32
- {bittensor_cli-9.0.0rc1.dist-info → bittensor_cli-9.0.0rc3.dist-info}/METADATA +2 -2
- {bittensor_cli-9.0.0rc1.dist-info → bittensor_cli-9.0.0rc3.dist-info}/RECORD +17 -15
- bittensor_cli/src/commands/stake/stake.py +0 -1821
- {bittensor_cli-9.0.0rc1.dist-info → bittensor_cli-9.0.0rc3.dist-info}/WHEEL +0 -0
- {bittensor_cli-9.0.0rc1.dist-info → bittensor_cli-9.0.0rc3.dist-info}/entry_points.txt +0 -0
- {bittensor_cli-9.0.0rc1.dist-info → bittensor_cli-9.0.0rc3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1146 @@
|
|
1
|
+
import asyncio
|
2
|
+
from functools import partial
|
3
|
+
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
5
|
+
import typer
|
6
|
+
|
7
|
+
from bittensor_wallet import Wallet
|
8
|
+
from bittensor_wallet.errors import KeyFileError
|
9
|
+
from rich.prompt import Confirm, Prompt
|
10
|
+
from rich.table import Table
|
11
|
+
|
12
|
+
from async_substrate_interface.errors import SubstrateRequestException
|
13
|
+
from bittensor_cli.src import COLOR_PALETTE
|
14
|
+
from bittensor_cli.src.bittensor.balances import Balance
|
15
|
+
from bittensor_cli.src.bittensor.utils import (
|
16
|
+
console,
|
17
|
+
err_console,
|
18
|
+
print_verbose,
|
19
|
+
print_error,
|
20
|
+
get_hotkey_wallets_for_wallet,
|
21
|
+
is_valid_ss58_address,
|
22
|
+
format_error_message,
|
23
|
+
group_subnets,
|
24
|
+
)
|
25
|
+
|
26
|
+
if TYPE_CHECKING:
|
27
|
+
from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
|
28
|
+
|
29
|
+
|
30
|
+
# Commands
|
31
|
+
async def unstake(
|
32
|
+
wallet: Wallet,
|
33
|
+
subtensor: "SubtensorInterface",
|
34
|
+
hotkey_ss58_address: str,
|
35
|
+
all_hotkeys: bool,
|
36
|
+
include_hotkeys: list[str],
|
37
|
+
exclude_hotkeys: list[str],
|
38
|
+
amount: float,
|
39
|
+
prompt: bool,
|
40
|
+
interactive: bool,
|
41
|
+
netuid: Optional[int],
|
42
|
+
safe_staking: bool,
|
43
|
+
rate_tolerance: float,
|
44
|
+
allow_partial_stake: bool,
|
45
|
+
):
|
46
|
+
"""Unstake from hotkey(s)."""
|
47
|
+
unstake_all_from_hk = False
|
48
|
+
with console.status(
|
49
|
+
f"Retrieving subnet data & identities from {subtensor.network}...",
|
50
|
+
spinner="earth",
|
51
|
+
):
|
52
|
+
all_sn_dynamic_info_, ck_hk_identities, old_identities = await asyncio.gather(
|
53
|
+
subtensor.all_subnets(),
|
54
|
+
subtensor.fetch_coldkey_hotkey_identities(),
|
55
|
+
subtensor.get_delegate_identities(),
|
56
|
+
)
|
57
|
+
all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_}
|
58
|
+
|
59
|
+
if interactive:
|
60
|
+
hotkeys_to_unstake_from, unstake_all_from_hk = await _unstake_selection(
|
61
|
+
subtensor,
|
62
|
+
wallet,
|
63
|
+
all_sn_dynamic_info,
|
64
|
+
ck_hk_identities,
|
65
|
+
old_identities,
|
66
|
+
netuid=netuid,
|
67
|
+
)
|
68
|
+
if unstake_all_from_hk:
|
69
|
+
hotkey_to_unstake_all = hotkeys_to_unstake_from[0]
|
70
|
+
unstake_all_alpha = Confirm.ask(
|
71
|
+
"\nUnstake [blue]all alpha stakes[/blue] and stake back to [blue]root[/blue]? (No will unstake everything)",
|
72
|
+
default=True,
|
73
|
+
)
|
74
|
+
return await unstake_all(
|
75
|
+
wallet=wallet,
|
76
|
+
subtensor=subtensor,
|
77
|
+
hotkey_ss58_address=hotkey_to_unstake_all[1],
|
78
|
+
unstake_all_alpha=unstake_all_alpha,
|
79
|
+
prompt=prompt,
|
80
|
+
)
|
81
|
+
|
82
|
+
if not hotkeys_to_unstake_from:
|
83
|
+
console.print("[red]No unstake operations to perform.[/red]")
|
84
|
+
return False
|
85
|
+
netuids = list({netuid for _, _, netuid in hotkeys_to_unstake_from})
|
86
|
+
|
87
|
+
else:
|
88
|
+
netuids = (
|
89
|
+
[int(netuid)]
|
90
|
+
if netuid is not None
|
91
|
+
else await subtensor.get_all_subnet_netuids()
|
92
|
+
)
|
93
|
+
hotkeys_to_unstake_from = _get_hotkeys_to_unstake(
|
94
|
+
wallet=wallet,
|
95
|
+
hotkey_ss58_address=hotkey_ss58_address,
|
96
|
+
all_hotkeys=all_hotkeys,
|
97
|
+
include_hotkeys=include_hotkeys,
|
98
|
+
exclude_hotkeys=exclude_hotkeys,
|
99
|
+
)
|
100
|
+
|
101
|
+
with console.status(
|
102
|
+
f"Retrieving stake data from {subtensor.network}...",
|
103
|
+
spinner="earth",
|
104
|
+
):
|
105
|
+
# Fetch stake balances
|
106
|
+
chain_head = await subtensor.substrate.get_chain_head()
|
107
|
+
stake_info_list = await subtensor.get_stake_for_coldkey(
|
108
|
+
coldkey_ss58=wallet.coldkeypub.ss58_address,
|
109
|
+
block_hash=chain_head,
|
110
|
+
)
|
111
|
+
stake_in_netuids = {}
|
112
|
+
for stake_info in stake_info_list:
|
113
|
+
if stake_info.hotkey_ss58 not in stake_in_netuids:
|
114
|
+
stake_in_netuids[stake_info.hotkey_ss58] = {}
|
115
|
+
stake_in_netuids[stake_info.hotkey_ss58][stake_info.netuid] = (
|
116
|
+
stake_info.stake
|
117
|
+
)
|
118
|
+
|
119
|
+
# Flag to check if user wants to quit
|
120
|
+
skip_remaining_subnets = False
|
121
|
+
if len(netuids) > 1 and not amount:
|
122
|
+
console.print(
|
123
|
+
"[dark_sea_green3]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes.\n"
|
124
|
+
)
|
125
|
+
|
126
|
+
# Iterate over hotkeys and netuids to collect unstake operations
|
127
|
+
unstake_operations = []
|
128
|
+
total_received_amount = Balance.from_tao(0)
|
129
|
+
max_float_slippage = 0
|
130
|
+
table_rows = []
|
131
|
+
for hotkey in hotkeys_to_unstake_from:
|
132
|
+
if skip_remaining_subnets:
|
133
|
+
break
|
134
|
+
|
135
|
+
if interactive:
|
136
|
+
staking_address_name, staking_address_ss58, netuid = hotkey
|
137
|
+
netuids_to_process = [netuid]
|
138
|
+
else:
|
139
|
+
staking_address_name, staking_address_ss58 = hotkey
|
140
|
+
netuids_to_process = netuids
|
141
|
+
|
142
|
+
initial_amount = amount
|
143
|
+
|
144
|
+
for netuid in netuids_to_process:
|
145
|
+
if skip_remaining_subnets:
|
146
|
+
break # Exit the loop over netuids
|
147
|
+
|
148
|
+
subnet_info = all_sn_dynamic_info.get(netuid)
|
149
|
+
if staking_address_ss58 not in stake_in_netuids:
|
150
|
+
print_error(
|
151
|
+
f"No stake found for hotkey: {staking_address_ss58} on netuid: {netuid}"
|
152
|
+
)
|
153
|
+
continue # Skip to next hotkey
|
154
|
+
|
155
|
+
current_stake_balance = stake_in_netuids[staking_address_ss58].get(netuid)
|
156
|
+
if current_stake_balance is None or current_stake_balance.tao == 0:
|
157
|
+
print_error(
|
158
|
+
f"No stake to unstake from {staking_address_ss58} on netuid: {netuid}"
|
159
|
+
)
|
160
|
+
continue # No stake to unstake
|
161
|
+
|
162
|
+
# Determine the amount we are unstaking.
|
163
|
+
if initial_amount:
|
164
|
+
amount_to_unstake_as_balance = Balance.from_tao(initial_amount)
|
165
|
+
else:
|
166
|
+
amount_to_unstake_as_balance = _ask_unstake_amount(
|
167
|
+
current_stake_balance,
|
168
|
+
netuid,
|
169
|
+
staking_address_name
|
170
|
+
if staking_address_name
|
171
|
+
else staking_address_ss58,
|
172
|
+
staking_address_ss58,
|
173
|
+
interactive,
|
174
|
+
)
|
175
|
+
if amount_to_unstake_as_balance is None:
|
176
|
+
skip_remaining_subnets = True
|
177
|
+
break
|
178
|
+
|
179
|
+
# Check enough stake to remove.
|
180
|
+
amount_to_unstake_as_balance.set_unit(netuid)
|
181
|
+
if amount_to_unstake_as_balance > current_stake_balance:
|
182
|
+
err_console.print(
|
183
|
+
f"[red]Not enough stake to remove[/red]:\n Stake balance: [dark_orange]{current_stake_balance}[/dark_orange]"
|
184
|
+
f" < Unstaking amount: [dark_orange]{amount_to_unstake_as_balance}[/dark_orange] on netuid: {netuid}"
|
185
|
+
)
|
186
|
+
continue # Skip to the next subnet - useful when single amount is specified for all subnets
|
187
|
+
|
188
|
+
received_amount, slippage_pct, slippage_pct_float = _calculate_slippage(
|
189
|
+
subnet_info=subnet_info, amount=amount_to_unstake_as_balance
|
190
|
+
)
|
191
|
+
total_received_amount += received_amount
|
192
|
+
max_float_slippage = max(max_float_slippage, slippage_pct_float)
|
193
|
+
|
194
|
+
base_unstake_op = {
|
195
|
+
"netuid": netuid,
|
196
|
+
"hotkey_name": staking_address_name
|
197
|
+
if staking_address_name
|
198
|
+
else staking_address_ss58,
|
199
|
+
"hotkey_ss58": staking_address_ss58,
|
200
|
+
"amount_to_unstake": amount_to_unstake_as_balance,
|
201
|
+
"current_stake_balance": current_stake_balance,
|
202
|
+
"received_amount": received_amount,
|
203
|
+
"slippage_pct": slippage_pct,
|
204
|
+
"slippage_pct_float": slippage_pct_float,
|
205
|
+
"dynamic_info": subnet_info,
|
206
|
+
}
|
207
|
+
|
208
|
+
base_table_row = [
|
209
|
+
str(netuid), # Netuid
|
210
|
+
staking_address_name, # Hotkey Name
|
211
|
+
str(amount_to_unstake_as_balance), # Amount to Unstake
|
212
|
+
str(subnet_info.price.tao)
|
213
|
+
+ f"({Balance.get_unit(0)}/{Balance.get_unit(netuid)})", # Rate
|
214
|
+
str(received_amount), # Received Amount
|
215
|
+
slippage_pct, # Slippage Percent
|
216
|
+
]
|
217
|
+
|
218
|
+
# Additional fields for safe unstaking
|
219
|
+
if safe_staking:
|
220
|
+
if subnet_info.is_dynamic:
|
221
|
+
rate = subnet_info.price.tao or 1
|
222
|
+
rate_with_tolerance = rate * (
|
223
|
+
1 - rate_tolerance
|
224
|
+
) # Rate only for display
|
225
|
+
price_with_tolerance = subnet_info.price.rao * (
|
226
|
+
1 - rate_tolerance
|
227
|
+
) # Actual price to pass to extrinsic
|
228
|
+
else:
|
229
|
+
rate_with_tolerance = 1
|
230
|
+
price_with_tolerance = 1
|
231
|
+
|
232
|
+
base_unstake_op["price_with_tolerance"] = price_with_tolerance
|
233
|
+
base_table_row.extend(
|
234
|
+
[
|
235
|
+
f"{rate_with_tolerance:.4f} {Balance.get_unit(0)}/{Balance.get_unit(netuid)}", # Rate with tolerance
|
236
|
+
f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", # Partial unstake
|
237
|
+
]
|
238
|
+
)
|
239
|
+
|
240
|
+
unstake_operations.append(base_unstake_op)
|
241
|
+
table_rows.append(base_table_row)
|
242
|
+
|
243
|
+
if not unstake_operations:
|
244
|
+
console.print("[red]No unstake operations to perform.[/red]")
|
245
|
+
return False
|
246
|
+
|
247
|
+
table = _create_unstake_table(
|
248
|
+
wallet_name=wallet.name,
|
249
|
+
wallet_coldkey_ss58=wallet.coldkeypub.ss58_address,
|
250
|
+
network=subtensor.network,
|
251
|
+
total_received_amount=total_received_amount,
|
252
|
+
safe_staking=safe_staking,
|
253
|
+
rate_tolerance=rate_tolerance,
|
254
|
+
)
|
255
|
+
for row in table_rows:
|
256
|
+
table.add_row(*row)
|
257
|
+
|
258
|
+
_print_table_and_slippage(table, max_float_slippage, safe_staking)
|
259
|
+
if prompt:
|
260
|
+
if not Confirm.ask("Would you like to continue?"):
|
261
|
+
raise typer.Exit()
|
262
|
+
|
263
|
+
# Execute extrinsics
|
264
|
+
try:
|
265
|
+
wallet.unlock_coldkey()
|
266
|
+
except KeyFileError:
|
267
|
+
err_console.print("Error decrypting coldkey (possibly incorrect password)")
|
268
|
+
return False
|
269
|
+
|
270
|
+
with console.status("\n:satellite: Performing unstaking operations...") as status:
|
271
|
+
if safe_staking:
|
272
|
+
for op in unstake_operations:
|
273
|
+
if op["netuid"] == 0:
|
274
|
+
await _unstake_extrinsic(
|
275
|
+
wallet=wallet,
|
276
|
+
subtensor=subtensor,
|
277
|
+
netuid=op["netuid"],
|
278
|
+
amount=op["amount_to_unstake"],
|
279
|
+
current_stake=op["current_stake_balance"],
|
280
|
+
hotkey_ss58=op["hotkey_ss58"],
|
281
|
+
status=status,
|
282
|
+
)
|
283
|
+
else:
|
284
|
+
await _safe_unstake_extrinsic(
|
285
|
+
wallet=wallet,
|
286
|
+
subtensor=subtensor,
|
287
|
+
netuid=op["netuid"],
|
288
|
+
amount=op["amount_to_unstake"],
|
289
|
+
current_stake=op["current_stake_balance"],
|
290
|
+
hotkey_ss58=op["hotkey_ss58"],
|
291
|
+
price_limit=op["price_with_tolerance"],
|
292
|
+
allow_partial_stake=allow_partial_stake,
|
293
|
+
status=status,
|
294
|
+
)
|
295
|
+
else:
|
296
|
+
for op in unstake_operations:
|
297
|
+
await _unstake_extrinsic(
|
298
|
+
wallet=wallet,
|
299
|
+
subtensor=subtensor,
|
300
|
+
netuid=op["netuid"],
|
301
|
+
amount=op["amount_to_unstake"],
|
302
|
+
current_stake=op["current_stake_balance"],
|
303
|
+
hotkey_ss58=op["hotkey_ss58"],
|
304
|
+
status=status,
|
305
|
+
)
|
306
|
+
console.print(
|
307
|
+
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed."
|
308
|
+
)
|
309
|
+
|
310
|
+
|
311
|
+
async def unstake_all(
|
312
|
+
wallet: Wallet,
|
313
|
+
subtensor: "SubtensorInterface",
|
314
|
+
hotkey_ss58_address: str,
|
315
|
+
unstake_all_alpha: bool = False,
|
316
|
+
prompt: bool = True,
|
317
|
+
) -> bool:
|
318
|
+
"""Unstakes all stakes from all hotkeys in all subnets."""
|
319
|
+
|
320
|
+
with console.status(
|
321
|
+
f"Retrieving stake information & identities from {subtensor.network}...",
|
322
|
+
spinner="earth",
|
323
|
+
):
|
324
|
+
(
|
325
|
+
stake_info,
|
326
|
+
ck_hk_identities,
|
327
|
+
old_identities,
|
328
|
+
all_sn_dynamic_info_,
|
329
|
+
current_wallet_balance,
|
330
|
+
) = await asyncio.gather(
|
331
|
+
subtensor.get_stake_for_coldkey(wallet.coldkeypub.ss58_address),
|
332
|
+
subtensor.fetch_coldkey_hotkey_identities(),
|
333
|
+
subtensor.get_delegate_identities(),
|
334
|
+
subtensor.all_subnets(),
|
335
|
+
subtensor.get_balance(wallet.coldkeypub.ss58_address),
|
336
|
+
)
|
337
|
+
if not hotkey_ss58_address:
|
338
|
+
hotkey_ss58_address = wallet.hotkey.ss58_address
|
339
|
+
stake_info = [
|
340
|
+
stake for stake in stake_info if stake.hotkey_ss58 == hotkey_ss58_address
|
341
|
+
]
|
342
|
+
|
343
|
+
if unstake_all_alpha:
|
344
|
+
stake_info = [stake for stake in stake_info if stake.netuid != 0]
|
345
|
+
|
346
|
+
if not stake_info:
|
347
|
+
console.print("[red]No stakes found to unstake[/red]")
|
348
|
+
return False
|
349
|
+
|
350
|
+
all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_}
|
351
|
+
|
352
|
+
# Create table for unstaking all
|
353
|
+
table_title = (
|
354
|
+
"Unstaking Summary - All Stakes"
|
355
|
+
if not unstake_all_alpha
|
356
|
+
else "Unstaking Summary - All Alpha Stakes"
|
357
|
+
)
|
358
|
+
table = Table(
|
359
|
+
title=(
|
360
|
+
f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{table_title}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n"
|
361
|
+
f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], "
|
362
|
+
f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n"
|
363
|
+
f"Network: [{COLOR_PALETTE['GENERAL']['HEADER']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n"
|
364
|
+
),
|
365
|
+
show_footer=True,
|
366
|
+
show_edge=False,
|
367
|
+
header_style="bold white",
|
368
|
+
border_style="bright_black",
|
369
|
+
style="bold",
|
370
|
+
title_justify="center",
|
371
|
+
show_lines=False,
|
372
|
+
pad_edge=True,
|
373
|
+
)
|
374
|
+
table.add_column("Netuid", justify="center", style="grey89")
|
375
|
+
table.add_column(
|
376
|
+
"Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
|
377
|
+
)
|
378
|
+
table.add_column(
|
379
|
+
f"Current Stake ({Balance.get_unit(1)})",
|
380
|
+
justify="center",
|
381
|
+
style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"],
|
382
|
+
)
|
383
|
+
table.add_column(
|
384
|
+
f"Rate ({Balance.unit}/{Balance.get_unit(1)})",
|
385
|
+
justify="center",
|
386
|
+
style=COLOR_PALETTE["POOLS"]["RATE"],
|
387
|
+
)
|
388
|
+
table.add_column(
|
389
|
+
f"Recieved ({Balance.unit})",
|
390
|
+
justify="center",
|
391
|
+
style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"],
|
392
|
+
)
|
393
|
+
table.add_column(
|
394
|
+
"Slippage",
|
395
|
+
justify="center",
|
396
|
+
style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"],
|
397
|
+
)
|
398
|
+
|
399
|
+
# Calculate slippage and total received
|
400
|
+
max_slippage = 0.0
|
401
|
+
total_received_value = Balance(0)
|
402
|
+
for stake in stake_info:
|
403
|
+
if stake.stake.rao == 0:
|
404
|
+
continue
|
405
|
+
|
406
|
+
# Get hotkey identity
|
407
|
+
if hk_identity := ck_hk_identities["hotkeys"].get(stake.hotkey_ss58):
|
408
|
+
hotkey_name = hk_identity.get("identity", {}).get(
|
409
|
+
"name", ""
|
410
|
+
) or hk_identity.get("display", "~")
|
411
|
+
hotkey_display = f"{hotkey_name}"
|
412
|
+
elif old_identity := old_identities.get(stake.hotkey_ss58):
|
413
|
+
hotkey_name = old_identity.display
|
414
|
+
hotkey_display = f"{hotkey_name}"
|
415
|
+
else:
|
416
|
+
hotkey_display = stake.hotkey_ss58
|
417
|
+
|
418
|
+
subnet_info = all_sn_dynamic_info.get(stake.netuid)
|
419
|
+
stake_amount = stake.stake
|
420
|
+
received_amount, slippage_pct, slippage_pct_float = _calculate_slippage(
|
421
|
+
subnet_info=subnet_info, amount=stake_amount
|
422
|
+
)
|
423
|
+
max_slippage = max(max_slippage, slippage_pct_float)
|
424
|
+
total_received_value += received_amount
|
425
|
+
|
426
|
+
table.add_row(
|
427
|
+
str(stake.netuid),
|
428
|
+
hotkey_display,
|
429
|
+
str(stake_amount),
|
430
|
+
str(float(subnet_info.price))
|
431
|
+
+ f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})",
|
432
|
+
str(received_amount),
|
433
|
+
slippage_pct,
|
434
|
+
)
|
435
|
+
console.print(table)
|
436
|
+
message = ""
|
437
|
+
if max_slippage > 5:
|
438
|
+
message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n"
|
439
|
+
message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n"
|
440
|
+
message += "-------------------------------------------------------------------------------------------------------------------\n"
|
441
|
+
console.print(message)
|
442
|
+
|
443
|
+
console.print(
|
444
|
+
f"Expected return after slippage: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}"
|
445
|
+
)
|
446
|
+
|
447
|
+
if prompt and not Confirm.ask(
|
448
|
+
"\nDo you want to proceed with unstaking everything?"
|
449
|
+
):
|
450
|
+
return False
|
451
|
+
|
452
|
+
try:
|
453
|
+
wallet.unlock_coldkey()
|
454
|
+
except KeyFileError:
|
455
|
+
err_console.print("Error decrypting coldkey (possibly incorrect password)")
|
456
|
+
return False
|
457
|
+
|
458
|
+
console_status = (
|
459
|
+
":satellite: Unstaking all Alpha stakes..."
|
460
|
+
if unstake_all_alpha
|
461
|
+
else ":satellite: Unstaking all stakes..."
|
462
|
+
)
|
463
|
+
previous_root_stake = await subtensor.get_stake(
|
464
|
+
hotkey_ss58=hotkey_ss58_address,
|
465
|
+
coldkey_ss58=wallet.coldkeypub.ss58_address,
|
466
|
+
netuid=0,
|
467
|
+
)
|
468
|
+
with console.status(console_status):
|
469
|
+
call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all"
|
470
|
+
call = await subtensor.substrate.compose_call(
|
471
|
+
call_module="SubtensorModule",
|
472
|
+
call_function=call_function,
|
473
|
+
call_params={"hotkey": hotkey_ss58_address},
|
474
|
+
)
|
475
|
+
success, error_message = await subtensor.sign_and_send_extrinsic(
|
476
|
+
call=call,
|
477
|
+
wallet=wallet,
|
478
|
+
wait_for_inclusion=True,
|
479
|
+
wait_for_finalization=False,
|
480
|
+
)
|
481
|
+
|
482
|
+
if success:
|
483
|
+
success_message = (
|
484
|
+
":white_heavy_check_mark: [green]Successfully unstaked all stakes[/green]"
|
485
|
+
if not unstake_all_alpha
|
486
|
+
else ":white_heavy_check_mark: [green]Successfully unstaked all Alpha stakes[/green]"
|
487
|
+
)
|
488
|
+
console.print(success_message)
|
489
|
+
new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address)
|
490
|
+
console.print(
|
491
|
+
f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
|
492
|
+
)
|
493
|
+
if unstake_all_alpha:
|
494
|
+
root_stake = await subtensor.get_stake(
|
495
|
+
hotkey_ss58=hotkey_ss58_address,
|
496
|
+
coldkey_ss58=wallet.coldkeypub.ss58_address,
|
497
|
+
netuid=0,
|
498
|
+
)
|
499
|
+
console.print(
|
500
|
+
f"Root Stake:\n [blue]{previous_root_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{root_stake}"
|
501
|
+
)
|
502
|
+
return True
|
503
|
+
else:
|
504
|
+
err_console.print(
|
505
|
+
f":cross_mark: [red]Failed to unstake[/red]: {error_message}"
|
506
|
+
)
|
507
|
+
return False
|
508
|
+
|
509
|
+
|
510
|
+
# Extrinsics
|
511
|
+
async def _unstake_extrinsic(
|
512
|
+
wallet: Wallet,
|
513
|
+
subtensor: "SubtensorInterface",
|
514
|
+
netuid: int,
|
515
|
+
amount: Balance,
|
516
|
+
current_stake: Balance,
|
517
|
+
hotkey_ss58: str,
|
518
|
+
status=None,
|
519
|
+
) -> None:
|
520
|
+
"""Execute a standard unstake extrinsic.
|
521
|
+
|
522
|
+
Args:
|
523
|
+
netuid: The subnet ID
|
524
|
+
amount: Amount to unstake
|
525
|
+
current_stake: Current stake balance
|
526
|
+
hotkey_ss58: Hotkey SS58 address
|
527
|
+
wallet: Wallet instance
|
528
|
+
subtensor: Subtensor interface
|
529
|
+
status: Optional status for console updates
|
530
|
+
"""
|
531
|
+
err_out = partial(print_error, status=status)
|
532
|
+
failure_prelude = (
|
533
|
+
f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}"
|
534
|
+
)
|
535
|
+
|
536
|
+
if status:
|
537
|
+
status.update(
|
538
|
+
f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..."
|
539
|
+
)
|
540
|
+
|
541
|
+
current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address)
|
542
|
+
call = await subtensor.substrate.compose_call(
|
543
|
+
call_module="SubtensorModule",
|
544
|
+
call_function="remove_stake",
|
545
|
+
call_params={
|
546
|
+
"hotkey": hotkey_ss58,
|
547
|
+
"netuid": netuid,
|
548
|
+
"amount_unstaked": amount.rao,
|
549
|
+
},
|
550
|
+
)
|
551
|
+
extrinsic = await subtensor.substrate.create_signed_extrinsic(
|
552
|
+
call=call, keypair=wallet.coldkey
|
553
|
+
)
|
554
|
+
|
555
|
+
try:
|
556
|
+
response = await subtensor.substrate.submit_extrinsic(
|
557
|
+
extrinsic, wait_for_inclusion=True, wait_for_finalization=False
|
558
|
+
)
|
559
|
+
await response.process_events()
|
560
|
+
|
561
|
+
if not await response.is_success:
|
562
|
+
err_out(
|
563
|
+
f"{failure_prelude} with error: "
|
564
|
+
f"{format_error_message(await response.error_message, subtensor.substrate)}"
|
565
|
+
)
|
566
|
+
return
|
567
|
+
|
568
|
+
# Fetch latest balance and stake
|
569
|
+
block_hash = await subtensor.substrate.get_chain_head()
|
570
|
+
new_balance, new_stake = await asyncio.gather(
|
571
|
+
subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash),
|
572
|
+
subtensor.get_stake(
|
573
|
+
hotkey_ss58=hotkey_ss58,
|
574
|
+
coldkey_ss58=wallet.coldkeypub.ss58_address,
|
575
|
+
netuid=netuid,
|
576
|
+
block_hash=block_hash,
|
577
|
+
),
|
578
|
+
)
|
579
|
+
|
580
|
+
console.print(":white_heavy_check_mark: [green]Finalized[/green]")
|
581
|
+
console.print(
|
582
|
+
f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
|
583
|
+
)
|
584
|
+
console.print(
|
585
|
+
f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]"
|
586
|
+
f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}"
|
587
|
+
)
|
588
|
+
|
589
|
+
except Exception as e:
|
590
|
+
err_out(f"{failure_prelude} with error: {str(e)}")
|
591
|
+
|
592
|
+
|
593
|
+
async def _safe_unstake_extrinsic(
|
594
|
+
wallet: Wallet,
|
595
|
+
subtensor: "SubtensorInterface",
|
596
|
+
netuid: int,
|
597
|
+
amount: Balance,
|
598
|
+
current_stake: Balance,
|
599
|
+
hotkey_ss58: str,
|
600
|
+
price_limit: Balance,
|
601
|
+
allow_partial_stake: bool,
|
602
|
+
status=None,
|
603
|
+
) -> None:
|
604
|
+
"""Execute a safe unstake extrinsic with price limit.
|
605
|
+
|
606
|
+
Args:
|
607
|
+
netuid: The subnet ID
|
608
|
+
amount: Amount to unstake
|
609
|
+
current_stake: Current stake balance
|
610
|
+
hotkey_ss58: Hotkey SS58 address
|
611
|
+
price_limit: Maximum acceptable price
|
612
|
+
wallet: Wallet instance
|
613
|
+
subtensor: Subtensor interface
|
614
|
+
allow_partial_stake: Whether to allow partial unstaking
|
615
|
+
status: Optional status for console updates
|
616
|
+
"""
|
617
|
+
err_out = partial(print_error, status=status)
|
618
|
+
failure_prelude = (
|
619
|
+
f":cross_mark: [red]Failed[/red] to unstake {amount} on Netuid {netuid}"
|
620
|
+
)
|
621
|
+
|
622
|
+
if status:
|
623
|
+
status.update(
|
624
|
+
f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..."
|
625
|
+
)
|
626
|
+
|
627
|
+
block_hash = await subtensor.substrate.get_chain_head()
|
628
|
+
|
629
|
+
current_balance, next_nonce, current_stake = await asyncio.gather(
|
630
|
+
subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash),
|
631
|
+
subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address),
|
632
|
+
subtensor.get_stake(
|
633
|
+
hotkey_ss58=hotkey_ss58,
|
634
|
+
coldkey_ss58=wallet.coldkeypub.ss58_address,
|
635
|
+
netuid=netuid,
|
636
|
+
),
|
637
|
+
)
|
638
|
+
|
639
|
+
call = await subtensor.substrate.compose_call(
|
640
|
+
call_module="SubtensorModule",
|
641
|
+
call_function="remove_stake_limit",
|
642
|
+
call_params={
|
643
|
+
"hotkey": hotkey_ss58,
|
644
|
+
"netuid": netuid,
|
645
|
+
"amount_unstaked": amount.rao,
|
646
|
+
"limit_price": price_limit,
|
647
|
+
"allow_partial": allow_partial_stake,
|
648
|
+
},
|
649
|
+
)
|
650
|
+
|
651
|
+
extrinsic = await subtensor.substrate.create_signed_extrinsic(
|
652
|
+
call=call, keypair=wallet.coldkey, nonce=next_nonce
|
653
|
+
)
|
654
|
+
|
655
|
+
try:
|
656
|
+
response = await subtensor.substrate.submit_extrinsic(
|
657
|
+
extrinsic, wait_for_inclusion=True, wait_for_finalization=False
|
658
|
+
)
|
659
|
+
except SubstrateRequestException as e:
|
660
|
+
if "Custom error: 8" in str(e):
|
661
|
+
print_error(
|
662
|
+
f"\n{failure_prelude}: Price exceeded tolerance limit. "
|
663
|
+
f"Transaction rejected because partial unstaking is disabled. "
|
664
|
+
f"Either increase price tolerance or enable partial unstaking.",
|
665
|
+
status=status,
|
666
|
+
)
|
667
|
+
return
|
668
|
+
else:
|
669
|
+
err_out(
|
670
|
+
f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}"
|
671
|
+
)
|
672
|
+
return
|
673
|
+
|
674
|
+
await response.process_events()
|
675
|
+
if not await response.is_success:
|
676
|
+
err_out(
|
677
|
+
f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}"
|
678
|
+
)
|
679
|
+
return
|
680
|
+
|
681
|
+
block_hash = await subtensor.substrate.get_chain_head()
|
682
|
+
new_balance, new_stake = await asyncio.gather(
|
683
|
+
subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash),
|
684
|
+
subtensor.get_stake(
|
685
|
+
hotkey_ss58=hotkey_ss58,
|
686
|
+
coldkey_ss58=wallet.coldkeypub.ss58_address,
|
687
|
+
netuid=netuid,
|
688
|
+
block_hash=block_hash,
|
689
|
+
),
|
690
|
+
)
|
691
|
+
|
692
|
+
console.print(":white_heavy_check_mark: [green]Finalized[/green]")
|
693
|
+
console.print(
|
694
|
+
f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
|
695
|
+
)
|
696
|
+
|
697
|
+
amount_unstaked = current_stake - new_stake
|
698
|
+
if allow_partial_stake and (amount_unstaked != amount):
|
699
|
+
console.print(
|
700
|
+
"Partial unstake transaction. Unstaked:\n"
|
701
|
+
f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_unstaked.set_unit(netuid=netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
|
702
|
+
f"instead of "
|
703
|
+
f"[blue]{amount}[/blue]"
|
704
|
+
)
|
705
|
+
|
706
|
+
console.print(
|
707
|
+
f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] "
|
708
|
+
f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}"
|
709
|
+
)
|
710
|
+
|
711
|
+
|
712
|
+
# Helpers
|
713
|
+
def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]:
|
714
|
+
"""Calculate slippage and received amount for unstaking operation.
|
715
|
+
|
716
|
+
Args:
|
717
|
+
dynamic_info: Subnet information containing price data
|
718
|
+
amount: Amount being unstaked
|
719
|
+
|
720
|
+
Returns:
|
721
|
+
tuple containing:
|
722
|
+
- received_amount: Balance after slippage
|
723
|
+
- slippage_pct: Formatted string of slippage percentage
|
724
|
+
- slippage_pct_float: Float value of slippage percentage
|
725
|
+
"""
|
726
|
+
received_amount, _, slippage_pct_float = subnet_info.alpha_to_tao_with_slippage(
|
727
|
+
amount
|
728
|
+
)
|
729
|
+
|
730
|
+
if subnet_info.is_dynamic:
|
731
|
+
slippage_pct = f"{slippage_pct_float:.4f} %"
|
732
|
+
else:
|
733
|
+
slippage_pct_float = 0
|
734
|
+
slippage_pct = "[red]N/A[/red]"
|
735
|
+
|
736
|
+
return received_amount, slippage_pct, slippage_pct_float
|
737
|
+
|
738
|
+
|
739
|
+
async def _unstake_selection(
|
740
|
+
subtensor: "SubtensorInterface",
|
741
|
+
wallet: Wallet,
|
742
|
+
dynamic_info,
|
743
|
+
identities,
|
744
|
+
old_identities,
|
745
|
+
netuid: Optional[int] = None,
|
746
|
+
):
|
747
|
+
stake_infos = await subtensor.get_stake_for_coldkey(
|
748
|
+
coldkey_ss58=wallet.coldkeypub.ss58_address
|
749
|
+
)
|
750
|
+
|
751
|
+
if not stake_infos:
|
752
|
+
print_error("You have no stakes to unstake.")
|
753
|
+
raise typer.Exit()
|
754
|
+
|
755
|
+
hotkey_stakes = {}
|
756
|
+
for stake_info in stake_infos:
|
757
|
+
if netuid is not None and stake_info.netuid != netuid:
|
758
|
+
continue
|
759
|
+
hotkey_ss58 = stake_info.hotkey_ss58
|
760
|
+
netuid_ = stake_info.netuid
|
761
|
+
stake_amount = stake_info.stake
|
762
|
+
if stake_amount.tao > 0:
|
763
|
+
hotkey_stakes.setdefault(hotkey_ss58, {})[netuid_] = stake_amount
|
764
|
+
|
765
|
+
if not hotkey_stakes:
|
766
|
+
if netuid is not None:
|
767
|
+
print_error(f"You have no stakes to unstake in subnet {netuid}.")
|
768
|
+
else:
|
769
|
+
print_error("You have no stakes to unstake.")
|
770
|
+
raise typer.Exit()
|
771
|
+
|
772
|
+
hotkeys_info = []
|
773
|
+
for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()):
|
774
|
+
if hk_identity := identities["hotkeys"].get(hotkey_ss58):
|
775
|
+
hotkey_name = hk_identity.get("identity", {}).get(
|
776
|
+
"name", ""
|
777
|
+
) or hk_identity.get("display", "~")
|
778
|
+
elif old_identity := old_identities.get(hotkey_ss58):
|
779
|
+
hotkey_name = old_identity.display
|
780
|
+
else:
|
781
|
+
hotkey_name = "~"
|
782
|
+
# TODO: Add wallet ids here.
|
783
|
+
|
784
|
+
hotkeys_info.append(
|
785
|
+
{
|
786
|
+
"index": idx,
|
787
|
+
"identity": hotkey_name,
|
788
|
+
"netuids": list(netuid_stakes.keys()),
|
789
|
+
"hotkey_ss58": hotkey_ss58,
|
790
|
+
}
|
791
|
+
)
|
792
|
+
|
793
|
+
# Display existing hotkeys, id, and staked netuids.
|
794
|
+
subnet_filter = f" for Subnet {netuid}" if netuid is not None else ""
|
795
|
+
table = Table(
|
796
|
+
title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes{subnet_filter}\n",
|
797
|
+
show_footer=True,
|
798
|
+
show_edge=False,
|
799
|
+
header_style="bold white",
|
800
|
+
border_style="bright_black",
|
801
|
+
style="bold",
|
802
|
+
title_justify="center",
|
803
|
+
show_lines=False,
|
804
|
+
pad_edge=True,
|
805
|
+
)
|
806
|
+
table.add_column("Index", justify="right")
|
807
|
+
table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"])
|
808
|
+
table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"])
|
809
|
+
table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"])
|
810
|
+
|
811
|
+
for hotkey_info in hotkeys_info:
|
812
|
+
index = str(hotkey_info["index"])
|
813
|
+
identity = hotkey_info["identity"]
|
814
|
+
netuids = group_subnets([n for n in hotkey_info["netuids"]])
|
815
|
+
hotkey_ss58 = hotkey_info["hotkey_ss58"]
|
816
|
+
table.add_row(index, identity, netuids, hotkey_ss58)
|
817
|
+
|
818
|
+
console.print("\n", table)
|
819
|
+
|
820
|
+
# Prompt to select hotkey to unstake.
|
821
|
+
hotkey_options = [str(hotkey_info["index"]) for hotkey_info in hotkeys_info]
|
822
|
+
hotkey_idx = Prompt.ask(
|
823
|
+
"\nEnter the index of the hotkey you want to unstake from",
|
824
|
+
choices=hotkey_options,
|
825
|
+
)
|
826
|
+
selected_hotkey_info = hotkeys_info[int(hotkey_idx)]
|
827
|
+
selected_hotkey_ss58 = selected_hotkey_info["hotkey_ss58"]
|
828
|
+
selected_hotkey_name = selected_hotkey_info["identity"]
|
829
|
+
netuid_stakes = hotkey_stakes[selected_hotkey_ss58]
|
830
|
+
|
831
|
+
# Display hotkey's staked netuids with amount.
|
832
|
+
table = Table(
|
833
|
+
title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Stakes for hotkey \n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey_name}\n{selected_hotkey_ss58}\n",
|
834
|
+
show_footer=True,
|
835
|
+
show_edge=False,
|
836
|
+
header_style="bold white",
|
837
|
+
border_style="bright_black",
|
838
|
+
style="bold",
|
839
|
+
title_justify="center",
|
840
|
+
show_lines=False,
|
841
|
+
pad_edge=True,
|
842
|
+
)
|
843
|
+
table.add_column("Subnet", justify="right")
|
844
|
+
table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"])
|
845
|
+
table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"])
|
846
|
+
table.add_column(
|
847
|
+
f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)",
|
848
|
+
style=COLOR_PALETTE["POOLS"]["RATE"],
|
849
|
+
justify="left",
|
850
|
+
)
|
851
|
+
|
852
|
+
for netuid_, stake_amount in netuid_stakes.items():
|
853
|
+
symbol = dynamic_info[netuid_].symbol
|
854
|
+
rate = f"{dynamic_info[netuid_].price.tao:.4f} τ/{symbol}"
|
855
|
+
table.add_row(str(netuid_), symbol, str(stake_amount), rate)
|
856
|
+
console.print("\n", table, "\n")
|
857
|
+
|
858
|
+
# Ask which netuids to unstake from for the selected hotkey.
|
859
|
+
unstake_all = False
|
860
|
+
if netuid is not None:
|
861
|
+
selected_netuids = [netuid]
|
862
|
+
else:
|
863
|
+
while True:
|
864
|
+
netuid_input = Prompt.ask(
|
865
|
+
"\nEnter the netuids of the [blue]subnets to unstake[/blue] from (comma-separated), or '[blue]all[/blue]' to unstake from all",
|
866
|
+
default="all",
|
867
|
+
)
|
868
|
+
|
869
|
+
if netuid_input.lower() == "all":
|
870
|
+
selected_netuids = list(netuid_stakes.keys())
|
871
|
+
unstake_all = True
|
872
|
+
break
|
873
|
+
else:
|
874
|
+
try:
|
875
|
+
netuid_list = [int(n.strip()) for n in netuid_input.split(",")]
|
876
|
+
invalid_netuids = [n for n in netuid_list if n not in netuid_stakes]
|
877
|
+
if invalid_netuids:
|
878
|
+
print_error(
|
879
|
+
f"The following netuids are invalid or not available: {', '.join(map(str, invalid_netuids))}. Please try again."
|
880
|
+
)
|
881
|
+
else:
|
882
|
+
selected_netuids = netuid_list
|
883
|
+
break
|
884
|
+
except ValueError:
|
885
|
+
print_error(
|
886
|
+
"Please enter valid netuids (numbers), separated by commas, or 'all'."
|
887
|
+
)
|
888
|
+
|
889
|
+
hotkeys_to_unstake_from = []
|
890
|
+
for netuid_ in selected_netuids:
|
891
|
+
hotkeys_to_unstake_from.append(
|
892
|
+
(selected_hotkey_name, selected_hotkey_ss58, netuid_)
|
893
|
+
)
|
894
|
+
return hotkeys_to_unstake_from, unstake_all
|
895
|
+
|
896
|
+
|
897
|
+
def _ask_unstake_amount(
|
898
|
+
current_stake_balance: Balance,
|
899
|
+
netuid: int,
|
900
|
+
staking_address_name: str,
|
901
|
+
staking_address_ss58: str,
|
902
|
+
interactive: bool,
|
903
|
+
) -> Optional[Balance]:
|
904
|
+
"""Prompt the user to decide the amount to unstake.
|
905
|
+
|
906
|
+
Args:
|
907
|
+
current_stake_balance: The current stake balance available to unstake
|
908
|
+
netuid: The subnet ID
|
909
|
+
staking_address_name: Display name of the staking address
|
910
|
+
staking_address_ss58: SS58 address of the staking address
|
911
|
+
interactive: Whether in interactive mode (affects default choice)
|
912
|
+
|
913
|
+
Returns:
|
914
|
+
Balance amount to unstake, or None if user chooses to quit
|
915
|
+
"""
|
916
|
+
stake_color = COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]
|
917
|
+
display_address = (
|
918
|
+
staking_address_name if staking_address_name else staking_address_ss58
|
919
|
+
)
|
920
|
+
|
921
|
+
# First prompt: Ask if user wants to unstake all
|
922
|
+
unstake_all_prompt = (
|
923
|
+
f"Unstake all: [{stake_color}]{current_stake_balance}[/{stake_color}]"
|
924
|
+
f" from [{stake_color}]{display_address}[/{stake_color}]"
|
925
|
+
f" on netuid: [{stake_color}]{netuid}[/{stake_color}]? [y/n/q]"
|
926
|
+
)
|
927
|
+
|
928
|
+
while True:
|
929
|
+
response = Prompt.ask(
|
930
|
+
unstake_all_prompt,
|
931
|
+
choices=["y", "n", "q"],
|
932
|
+
default="n",
|
933
|
+
show_choices=True,
|
934
|
+
).lower()
|
935
|
+
|
936
|
+
if response == "q":
|
937
|
+
return None
|
938
|
+
if response == "y":
|
939
|
+
return current_stake_balance
|
940
|
+
if response != "n":
|
941
|
+
console.print("[red]Invalid input. Please enter 'y', 'n', or 'q'.[/red]")
|
942
|
+
continue
|
943
|
+
|
944
|
+
amount_prompt = (
|
945
|
+
f"Enter amount to unstake in [{stake_color}]{Balance.get_unit(netuid)}[/{stake_color}]"
|
946
|
+
f" from subnet: [{stake_color}]{netuid}[/{stake_color}]"
|
947
|
+
f" (Max: [{stake_color}]{current_stake_balance}[/{stake_color}])"
|
948
|
+
)
|
949
|
+
|
950
|
+
while True:
|
951
|
+
amount_input = Prompt.ask(amount_prompt)
|
952
|
+
if amount_input.lower() == "q":
|
953
|
+
return None
|
954
|
+
|
955
|
+
try:
|
956
|
+
amount_value = float(amount_input)
|
957
|
+
|
958
|
+
# Validate amount
|
959
|
+
if amount_value <= 0:
|
960
|
+
console.print("[red]Amount must be greater than zero.[/red]")
|
961
|
+
continue
|
962
|
+
|
963
|
+
amount_to_unstake = Balance.from_tao(amount_value)
|
964
|
+
amount_to_unstake.set_unit(netuid)
|
965
|
+
|
966
|
+
if amount_to_unstake > current_stake_balance:
|
967
|
+
console.print(
|
968
|
+
f"[red]Amount exceeds current stake balance of {current_stake_balance}.[/red]"
|
969
|
+
)
|
970
|
+
continue
|
971
|
+
|
972
|
+
return amount_to_unstake
|
973
|
+
|
974
|
+
except ValueError:
|
975
|
+
console.print(
|
976
|
+
"[red]Invalid input. Please enter a numeric value or 'q' to quit.[/red]"
|
977
|
+
)
|
978
|
+
|
979
|
+
|
980
|
+
def _get_hotkeys_to_unstake(
|
981
|
+
wallet: Wallet,
|
982
|
+
hotkey_ss58_address: Optional[str],
|
983
|
+
all_hotkeys: bool,
|
984
|
+
include_hotkeys: list[str],
|
985
|
+
exclude_hotkeys: list[str],
|
986
|
+
) -> list[tuple[Optional[str], str]]:
|
987
|
+
"""Get list of hotkeys to unstake from based on input parameters.
|
988
|
+
|
989
|
+
Args:
|
990
|
+
wallet: The wallet to unstake from
|
991
|
+
hotkey_ss58_address: Specific hotkey SS58 address to unstake from
|
992
|
+
all_hotkeys: Whether to unstake from all hotkeys
|
993
|
+
include_hotkeys: List of hotkey names/addresses to include
|
994
|
+
exclude_hotkeys: List of hotkey names to exclude
|
995
|
+
|
996
|
+
Returns:
|
997
|
+
List of tuples containing (hotkey_name, hotkey_ss58) pairs to unstake from
|
998
|
+
"""
|
999
|
+
if hotkey_ss58_address:
|
1000
|
+
print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})")
|
1001
|
+
return [(None, hotkey_ss58_address)]
|
1002
|
+
|
1003
|
+
if all_hotkeys:
|
1004
|
+
print_verbose("Unstaking from all hotkeys")
|
1005
|
+
all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet)
|
1006
|
+
return [
|
1007
|
+
(wallet.hotkey_str, wallet.hotkey.ss58_address)
|
1008
|
+
for wallet in all_hotkeys_
|
1009
|
+
if wallet.hotkey_str not in exclude_hotkeys
|
1010
|
+
]
|
1011
|
+
|
1012
|
+
if include_hotkeys:
|
1013
|
+
print_verbose("Unstaking from included hotkeys")
|
1014
|
+
result = []
|
1015
|
+
for hotkey_identifier in include_hotkeys:
|
1016
|
+
if is_valid_ss58_address(hotkey_identifier):
|
1017
|
+
result.append((None, hotkey_identifier))
|
1018
|
+
else:
|
1019
|
+
wallet_ = Wallet(
|
1020
|
+
name=wallet.name,
|
1021
|
+
path=wallet.path,
|
1022
|
+
hotkey=hotkey_identifier,
|
1023
|
+
)
|
1024
|
+
result.append((wallet_.hotkey_str, wallet_.hotkey.ss58_address))
|
1025
|
+
return result
|
1026
|
+
|
1027
|
+
# Only cli.config.wallet.hotkey is specified
|
1028
|
+
print_verbose(
|
1029
|
+
f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})"
|
1030
|
+
)
|
1031
|
+
assert wallet.hotkey is not None
|
1032
|
+
return [(wallet.hotkey_str, wallet.hotkey.ss58_address)]
|
1033
|
+
|
1034
|
+
|
1035
|
+
def _create_unstake_table(
|
1036
|
+
wallet_name: str,
|
1037
|
+
wallet_coldkey_ss58: str,
|
1038
|
+
network: str,
|
1039
|
+
total_received_amount: Balance,
|
1040
|
+
safe_staking: bool,
|
1041
|
+
rate_tolerance: float,
|
1042
|
+
) -> Table:
|
1043
|
+
"""Create a table summarizing unstake operations.
|
1044
|
+
|
1045
|
+
Args:
|
1046
|
+
wallet_name: Name of the wallet
|
1047
|
+
wallet_coldkey_ss58: Coldkey SS58 address
|
1048
|
+
network: Network name
|
1049
|
+
total_received_amount: Total amount to be received after unstaking
|
1050
|
+
|
1051
|
+
Returns:
|
1052
|
+
Rich Table object configured for unstake summary
|
1053
|
+
"""
|
1054
|
+
title = (
|
1055
|
+
f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking to: \n"
|
1056
|
+
f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet_name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], "
|
1057
|
+
f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet_coldkey_ss58}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n"
|
1058
|
+
f"Network: {network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n"
|
1059
|
+
)
|
1060
|
+
table = Table(
|
1061
|
+
title=title,
|
1062
|
+
show_footer=True,
|
1063
|
+
show_edge=False,
|
1064
|
+
header_style="bold white",
|
1065
|
+
border_style="bright_black",
|
1066
|
+
style="bold",
|
1067
|
+
title_justify="center",
|
1068
|
+
show_lines=False,
|
1069
|
+
pad_edge=True,
|
1070
|
+
)
|
1071
|
+
|
1072
|
+
table.add_column("Netuid", justify="center", style="grey89")
|
1073
|
+
table.add_column(
|
1074
|
+
"Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
|
1075
|
+
)
|
1076
|
+
table.add_column(
|
1077
|
+
f"Amount ({Balance.get_unit(1)})",
|
1078
|
+
justify="center",
|
1079
|
+
style=COLOR_PALETTE["POOLS"]["TAO"],
|
1080
|
+
)
|
1081
|
+
table.add_column(
|
1082
|
+
f"Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})",
|
1083
|
+
justify="center",
|
1084
|
+
style=COLOR_PALETTE["POOLS"]["RATE"],
|
1085
|
+
)
|
1086
|
+
table.add_column(
|
1087
|
+
f"Received ({Balance.get_unit(0)})",
|
1088
|
+
justify="center",
|
1089
|
+
style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"],
|
1090
|
+
footer=str(total_received_amount),
|
1091
|
+
)
|
1092
|
+
table.add_column(
|
1093
|
+
"Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"]
|
1094
|
+
)
|
1095
|
+
if safe_staking:
|
1096
|
+
table.add_column(
|
1097
|
+
f"Rate with tolerance: [blue]({rate_tolerance*100}%)[/blue]",
|
1098
|
+
justify="center",
|
1099
|
+
style=COLOR_PALETTE["POOLS"]["RATE"],
|
1100
|
+
)
|
1101
|
+
table.add_column(
|
1102
|
+
"Partial unstake enabled",
|
1103
|
+
justify="center",
|
1104
|
+
style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"],
|
1105
|
+
)
|
1106
|
+
|
1107
|
+
return table
|
1108
|
+
|
1109
|
+
|
1110
|
+
def _print_table_and_slippage(
|
1111
|
+
table: Table,
|
1112
|
+
max_float_slippage: float,
|
1113
|
+
safe_staking: bool,
|
1114
|
+
) -> None:
|
1115
|
+
"""Print the unstake summary table and additional information.
|
1116
|
+
|
1117
|
+
Args:
|
1118
|
+
table: The Rich table containing unstake details
|
1119
|
+
max_float_slippage: Maximum slippage percentage across all operations
|
1120
|
+
"""
|
1121
|
+
console.print(table)
|
1122
|
+
|
1123
|
+
if max_float_slippage > 5:
|
1124
|
+
console.print(
|
1125
|
+
"\n"
|
1126
|
+
f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n"
|
1127
|
+
f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_float_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}],"
|
1128
|
+
" this may result in a loss of funds.\n"
|
1129
|
+
f"-------------------------------------------------------------------------------------------------------------------\n"
|
1130
|
+
)
|
1131
|
+
base_description = """
|
1132
|
+
[bold white]Description[/bold white]:
|
1133
|
+
The table displays information about the stake remove operation you are about to perform.
|
1134
|
+
The columns are as follows:
|
1135
|
+
- [bold white]Netuid[/bold white]: The netuid of the subnet you are unstaking from.
|
1136
|
+
- [bold white]Hotkey[/bold white]: The ss58 address or identity of the hotkey you are unstaking from.
|
1137
|
+
- [bold white]Amount to Unstake[/bold white]: The stake amount you are removing from this key.
|
1138
|
+
- [bold white]Rate[/bold white]: The rate of exchange between TAO and the subnet's stake.
|
1139
|
+
- [bold white]Received[/bold white]: The amount of free balance TAO you will receive on this subnet after slippage.
|
1140
|
+
- [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root)."""
|
1141
|
+
|
1142
|
+
safe_staking_description = """
|
1143
|
+
- [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate reduces below this tolerance, the transaction will be limited or rejected.
|
1144
|
+
- [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"""
|
1145
|
+
|
1146
|
+
console.print(base_description + (safe_staking_description if safe_staking else ""))
|