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.
- meshtensor_cli/__init__.py +22 -0
- meshtensor_cli/cli.py +10742 -0
- meshtensor_cli/doc_generation_helper.py +4 -0
- meshtensor_cli/src/__init__.py +1085 -0
- meshtensor_cli/src/commands/__init__.py +0 -0
- meshtensor_cli/src/commands/axon/__init__.py +0 -0
- meshtensor_cli/src/commands/axon/axon.py +132 -0
- meshtensor_cli/src/commands/crowd/__init__.py +0 -0
- meshtensor_cli/src/commands/crowd/contribute.py +621 -0
- meshtensor_cli/src/commands/crowd/contributors.py +200 -0
- meshtensor_cli/src/commands/crowd/create.py +783 -0
- meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
- meshtensor_cli/src/commands/crowd/refund.py +233 -0
- meshtensor_cli/src/commands/crowd/update.py +418 -0
- meshtensor_cli/src/commands/crowd/utils.py +124 -0
- meshtensor_cli/src/commands/crowd/view.py +991 -0
- meshtensor_cli/src/commands/governance/__init__.py +0 -0
- meshtensor_cli/src/commands/governance/governance.py +794 -0
- meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
- meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
- meshtensor_cli/src/commands/liquidity/utils.py +202 -0
- meshtensor_cli/src/commands/proxy.py +700 -0
- meshtensor_cli/src/commands/stake/__init__.py +0 -0
- meshtensor_cli/src/commands/stake/add.py +799 -0
- meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
- meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
- meshtensor_cli/src/commands/stake/claim.py +770 -0
- meshtensor_cli/src/commands/stake/list.py +738 -0
- meshtensor_cli/src/commands/stake/move.py +1211 -0
- meshtensor_cli/src/commands/stake/remove.py +1466 -0
- meshtensor_cli/src/commands/stake/wizard.py +323 -0
- meshtensor_cli/src/commands/subnets/__init__.py +0 -0
- meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
- meshtensor_cli/src/commands/subnets/price.py +733 -0
- meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
- meshtensor_cli/src/commands/sudo.py +1294 -0
- meshtensor_cli/src/commands/tc/__init__.py +0 -0
- meshtensor_cli/src/commands/tc/tc.py +190 -0
- meshtensor_cli/src/commands/treasury/__init__.py +0 -0
- meshtensor_cli/src/commands/treasury/treasury.py +194 -0
- meshtensor_cli/src/commands/view.py +354 -0
- meshtensor_cli/src/commands/wallets.py +2311 -0
- meshtensor_cli/src/commands/weights.py +467 -0
- meshtensor_cli/src/meshtensor/__init__.py +0 -0
- meshtensor_cli/src/meshtensor/balances.py +313 -0
- meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
- meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
- meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
- meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
- meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
- meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
- meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
- meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
- meshtensor_cli/src/meshtensor/minigraph.py +254 -0
- meshtensor_cli/src/meshtensor/networking.py +12 -0
- meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
- meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
- meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
- meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
- meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
- meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
- meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
- meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
- meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
- meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
- meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
- meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
- meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
- meshtensor_cli/src/meshtensor/utils.py +2007 -0
- meshtensor_cli/version.py +23 -0
- meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
- meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
- meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
- 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:]}"
|