bittensor-cli 8.4.3__py3-none-any.whl → 9.0.0rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. bittensor_cli/__init__.py +2 -2
  2. bittensor_cli/cli.py +1508 -1385
  3. bittensor_cli/src/__init__.py +627 -197
  4. bittensor_cli/src/bittensor/balances.py +41 -8
  5. bittensor_cli/src/bittensor/chain_data.py +557 -428
  6. bittensor_cli/src/bittensor/extrinsics/registration.py +161 -47
  7. bittensor_cli/src/bittensor/extrinsics/root.py +14 -8
  8. bittensor_cli/src/bittensor/extrinsics/transfer.py +14 -21
  9. bittensor_cli/src/bittensor/minigraph.py +46 -8
  10. bittensor_cli/src/bittensor/subtensor_interface.py +572 -253
  11. bittensor_cli/src/bittensor/utils.py +326 -75
  12. bittensor_cli/src/commands/stake/__init__.py +154 -0
  13. bittensor_cli/src/commands/stake/children_hotkeys.py +121 -87
  14. bittensor_cli/src/commands/stake/move.py +1000 -0
  15. bittensor_cli/src/commands/stake/stake.py +1637 -1264
  16. bittensor_cli/src/commands/subnets/__init__.py +0 -0
  17. bittensor_cli/src/commands/subnets/price.py +867 -0
  18. bittensor_cli/src/commands/subnets/subnets.py +2055 -0
  19. bittensor_cli/src/commands/sudo.py +529 -26
  20. bittensor_cli/src/commands/wallets.py +234 -544
  21. bittensor_cli/src/commands/weights.py +15 -11
  22. {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0rc2.dist-info}/METADATA +7 -4
  23. bittensor_cli-9.0.0rc2.dist-info/RECORD +32 -0
  24. bittensor_cli/src/bittensor/async_substrate_interface.py +0 -2748
  25. bittensor_cli/src/commands/root.py +0 -1752
  26. bittensor_cli/src/commands/subnets.py +0 -897
  27. bittensor_cli-8.4.3.dist-info/RECORD +0 -31
  28. {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0rc2.dist-info}/WHEEL +0 -0
  29. {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0rc2.dist-info}/entry_points.txt +0 -0
  30. {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0rc2.dist-info}/top_level.txt +0 -0
@@ -1,1448 +1,1821 @@
1
1
  import asyncio
2
- import copy
3
- import json
4
- import sqlite3
5
- from contextlib import suppress
2
+ from functools import partial
6
3
 
7
- from typing import TYPE_CHECKING, Optional, Sequence, Union, cast
8
-
9
- from bittensor_wallet import Wallet
10
- from rich.prompt import Confirm
11
- from rich.table import Table, Column
4
+ from typing import TYPE_CHECKING, Optional
12
5
  import typer
13
6
 
14
-
7
+ from bittensor_wallet import Wallet
8
+ from bittensor_wallet.errors import KeyFileError
9
+ from rich.prompt import Confirm, FloatPrompt, Prompt
10
+ from rich.table import Table
11
+ from rich import box
12
+ from rich.progress import Progress, BarColumn, TextColumn
13
+ from rich.console import Group
14
+ from rich.live import Live
15
+ from async_substrate_interface.errors import SubstrateRequestException
16
+
17
+ from bittensor_cli.src import COLOR_PALETTE
15
18
  from bittensor_cli.src.bittensor.balances import Balance
19
+ from bittensor_cli.src.bittensor.chain_data import StakeInfo
16
20
  from bittensor_cli.src.bittensor.utils import (
21
+ # TODO add back in caching
17
22
  console,
18
- create_table,
19
23
  err_console,
20
24
  print_verbose,
21
25
  print_error,
22
- get_coldkey_wallets_for_path,
23
26
  get_hotkey_wallets_for_wallet,
24
27
  is_valid_ss58_address,
25
- get_metadata_table,
26
- update_metadata_table,
27
- render_tree,
28
- u16_normalized_float,
29
- validate_coldkey_presence,
30
- unlock_key,
28
+ format_error_message,
29
+ group_subnets,
30
+ millify_tao,
31
+ get_subnet_name,
31
32
  )
32
33
 
33
34
  if TYPE_CHECKING:
34
35
  from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
35
36
 
36
37
 
37
- # Helpers and Extrinsics
38
-
39
-
40
- async def _get_threshold_amount(
41
- subtensor: "SubtensorInterface", block_hash: str
42
- ) -> Balance:
43
- mrs = await subtensor.substrate.query(
44
- module="SubtensorModule",
45
- storage_function="NominatorMinRequiredStake",
46
- block_hash=block_hash,
47
- )
48
- min_req_stake: Balance = Balance.from_rao(mrs)
49
- return min_req_stake
50
-
51
-
52
- async def _check_threshold_amount(
38
+ async def stake_add(
39
+ wallet: Wallet,
53
40
  subtensor: "SubtensorInterface",
54
- sb: Balance,
55
- block_hash: str,
56
- min_req_stake: Optional[Balance] = None,
57
- ) -> tuple[bool, Balance]:
41
+ netuid: Optional[int],
42
+ stake_all: bool,
43
+ amount: float,
44
+ delegate: bool,
45
+ prompt: bool,
46
+ max_stake: float,
47
+ all_hotkeys: bool,
48
+ include_hotkeys: list[str],
49
+ exclude_hotkeys: list[str],
50
+ ):
58
51
  """
59
- Checks if the new stake balance will be above the minimum required stake threshold.
60
52
 
61
- :param sb: the balance to check for threshold limits.
53
+ Args:
54
+ wallet: wallet object
55
+ subtensor: SubtensorInterface object
56
+ netuid: the netuid to stake to (None indicates all subnets)
57
+ stake_all: whether to stake all available balance
58
+ amount: specified amount of balance to stake
59
+ delegate: whether to delegate stake, currently unused
60
+ prompt: whether to prompt the user
61
+ max_stake: maximum amount to stake (used in combination with stake_all), currently unused
62
+ all_hotkeys: whether to stake all hotkeys
63
+ include_hotkeys: list of hotkeys to include in staking process (if not specifying `--all`)
64
+ exclude_hotkeys: list of hotkeys to exclude in staking (if specifying `--all`)
65
+
66
+ Returns:
62
67
 
63
- :return: (success, threshold)
64
- `True` if the staking balance is above the threshold, or `False` if the staking balance is below the
65
- threshold.
66
- The threshold balance required to stake.
67
68
  """
68
- if not min_req_stake:
69
- min_req_stake = await _get_threshold_amount(subtensor, block_hash)
70
-
71
- if min_req_stake > sb:
72
- return False, min_req_stake
73
- else:
74
- return True, min_req_stake
75
-
69
+ netuids = (
70
+ [int(netuid)]
71
+ if netuid is not None
72
+ else await subtensor.get_all_subnet_netuids()
73
+ )
74
+ # Init the table.
75
+ table = Table(
76
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to: \nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n",
77
+ show_footer=True,
78
+ show_edge=False,
79
+ header_style="bold white",
80
+ border_style="bright_black",
81
+ style="bold",
82
+ title_justify="center",
83
+ show_lines=False,
84
+ pad_edge=True,
85
+ )
76
86
 
77
- async def add_stake_extrinsic(
78
- subtensor: "SubtensorInterface",
79
- wallet: Wallet,
80
- old_balance: Balance,
81
- hotkey_ss58: Optional[str] = None,
82
- amount: Optional[Balance] = None,
83
- wait_for_inclusion: bool = True,
84
- wait_for_finalization: bool = False,
85
- prompt: bool = False,
86
- ) -> bool:
87
- """
88
- Adds the specified amount of stake to passed hotkey `uid`.
89
-
90
- :param subtensor: the initialized SubtensorInterface object to use
91
- :param wallet: Bittensor wallet object.
92
- :param old_balance: the balance prior to the staking
93
- :param hotkey_ss58: The `ss58` address of the hotkey account to stake to defaults to the wallet's hotkey.
94
- :param amount: Amount to stake as Bittensor balance, `None` if staking all.
95
- :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns
96
- `False` if the extrinsic fails to enter the block within the timeout.
97
- :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
98
- or returns `False` if the extrinsic fails to be finalized within the timeout.
99
- :param prompt: If `True`, the call waits for confirmation from the user before proceeding.
100
-
101
- :return: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for
102
- finalization/inclusion, the response is `True`.
103
- """
87
+ # Determine the amount we are staking.
88
+ rows = []
89
+ stake_amount_balance = []
90
+ current_stake_balances = []
91
+ current_wallet_balance_ = await subtensor.get_balance(
92
+ wallet.coldkeypub.ss58_address
93
+ )
94
+ current_wallet_balance = current_wallet_balance_.set_unit(0)
95
+ remaining_wallet_balance = current_wallet_balance
96
+ max_slippage = 0.0
104
97
 
105
- # Decrypt keys,
106
- if not unlock_key(wallet).success:
107
- return False
98
+ hotkeys_to_stake_to: list[tuple[Optional[str], str]] = []
99
+ if all_hotkeys:
100
+ # Stake to all hotkeys.
101
+ all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet)
102
+ # Get the hotkeys to exclude. (d)efault to no exclusions.
103
+ # Exclude hotkeys that are specified.
104
+ hotkeys_to_stake_to = [
105
+ (wallet.hotkey_str, wallet.hotkey.ss58_address)
106
+ for wallet in all_hotkeys_
107
+ if wallet.hotkey_str not in exclude_hotkeys
108
+ ] # definitely wallets
108
109
 
109
- # Default to wallet's own hotkey if the value is not passed.
110
- if hotkey_ss58 is None:
111
- hotkey_ss58 = wallet.hotkey.ss58_address
110
+ elif include_hotkeys:
111
+ print_verbose("Staking to only included hotkeys")
112
+ # Stake to specific hotkeys.
113
+ for hotkey_ss58_or_hotkey_name in include_hotkeys:
114
+ if is_valid_ss58_address(hotkey_ss58_or_hotkey_name):
115
+ # If the hotkey is a valid ss58 address, we add it to the list.
116
+ hotkeys_to_stake_to.append((None, hotkey_ss58_or_hotkey_name))
117
+ else:
118
+ # If the hotkey is not a valid ss58 address, we assume it is a hotkey name.
119
+ # We then get the hotkey from the wallet and add it to the list.
120
+ wallet_ = Wallet(
121
+ path=wallet.path,
122
+ name=wallet.name,
123
+ hotkey=hotkey_ss58_or_hotkey_name,
124
+ )
125
+ hotkeys_to_stake_to.append(
126
+ (wallet_.hotkey_str, wallet_.hotkey.ss58_address)
127
+ )
128
+ else:
129
+ # Only config.wallet.hotkey is specified.
130
+ # so we stake to that single hotkey.
131
+ print_verbose(
132
+ f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})"
133
+ )
134
+ assert wallet.hotkey is not None
135
+ hotkey_ss58_or_name = wallet.hotkey.ss58_address
136
+ hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)]
112
137
 
113
- # Flag to indicate if we are using the wallet's own hotkey.
114
- own_hotkey: bool
138
+ starting_chain_head = await subtensor.substrate.get_chain_head()
139
+ _all_dynamic_info, stake_info_dict = await asyncio.gather(
140
+ subtensor.all_subnets(),
141
+ subtensor.get_stake_for_coldkey(
142
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
143
+ block_hash=starting_chain_head,
144
+ ),
145
+ )
146
+ all_dynamic_info = {di.netuid: di for di in _all_dynamic_info}
147
+ initial_stake_balances = {}
148
+ for hotkey_ss58 in [x[1] for x in hotkeys_to_stake_to]:
149
+ initial_stake_balances[hotkey_ss58] = {}
150
+ for netuid in netuids:
151
+ initial_stake_balances[hotkey_ss58][netuid] = Balance.from_rao(0)
152
+
153
+ for stake_info in stake_info_dict:
154
+ if stake_info.hotkey_ss58 in initial_stake_balances:
155
+ initial_stake_balances[stake_info.hotkey_ss58][stake_info.netuid] = (
156
+ stake_info.stake
157
+ )
115
158
 
116
- with console.status(
117
- f":satellite: Syncing with chain: [white]{subtensor}[/white] ...",
118
- spinner="aesthetic",
119
- ) as status:
120
- block_hash = await subtensor.substrate.get_chain_head()
121
- # Get hotkey owner
122
- print_verbose("Confirming hotkey owner", status)
123
- hotkey_owner = await subtensor.get_hotkey_owner(
124
- hotkey_ss58=hotkey_ss58, block_hash=block_hash
125
- )
126
- own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner
127
- if not own_hotkey:
128
- # This is not the wallet's own hotkey, so we are delegating.
129
- if not await subtensor.is_hotkey_delegate(
130
- hotkey_ss58, block_hash=block_hash
131
- ):
159
+ for hk_name, hk_ss58 in hotkeys_to_stake_to:
160
+ if not is_valid_ss58_address(hk_ss58):
161
+ print_error(
162
+ f"The entered hotkey ss58 address is incorrect: {hk_name} | {hk_ss58}"
163
+ )
164
+ return False
165
+ for hotkey in hotkeys_to_stake_to:
166
+ for netuid in netuids:
167
+ # Check that the subnet exists.
168
+ dynamic_info = all_dynamic_info.get(netuid)
169
+ if not dynamic_info:
170
+ err_console.print(f"Subnet with netuid: {netuid} does not exist.")
171
+ continue
172
+ current_stake_balances.append(initial_stake_balances[hotkey[1]][netuid])
173
+
174
+ # Get the amount.
175
+ amount_to_stake_as_balance = Balance(0)
176
+ if amount:
177
+ amount_to_stake_as_balance = Balance.from_tao(amount)
178
+ elif stake_all:
179
+ amount_to_stake_as_balance = current_wallet_balance / len(netuids)
180
+ elif not amount and not max_stake:
181
+ if Confirm.ask(f"Stake all: [bold]{remaining_wallet_balance}[/bold]?"):
182
+ amount_to_stake_as_balance = remaining_wallet_balance
183
+ else:
184
+ try:
185
+ amount = FloatPrompt.ask(
186
+ f"Enter amount to stake in {Balance.get_unit(0)} to subnet: {netuid}"
187
+ )
188
+ amount_to_stake_as_balance = Balance.from_tao(amount)
189
+ except ValueError:
190
+ err_console.print(
191
+ f":cross_mark:[red]Invalid amount: {amount}[/red]"
192
+ )
193
+ return False
194
+ stake_amount_balance.append(amount_to_stake_as_balance)
195
+
196
+ # Check enough to stake.
197
+ amount_to_stake_as_balance.set_unit(0)
198
+ if amount_to_stake_as_balance > remaining_wallet_balance:
132
199
  err_console.print(
133
- f"Hotkey {hotkey_ss58} is not a delegate on the chain."
200
+ f"[red]Not enough stake[/red]:[bold white]\n wallet balance:{remaining_wallet_balance} < "
201
+ f"staking amount: {amount_to_stake_as_balance}[/bold white]"
134
202
  )
135
203
  return False
204
+ remaining_wallet_balance -= amount_to_stake_as_balance
136
205
 
137
- # Get hotkey take
138
- hk_result = await subtensor.substrate.query(
139
- module="SubtensorModule",
140
- storage_function="Delegates",
141
- params=[hotkey_ss58],
142
- block_hash=block_hash,
206
+ # Slippage warning
207
+ received_amount, _, slippage_pct_float = (
208
+ dynamic_info.tao_to_alpha_with_slippage(amount_to_stake_as_balance)
143
209
  )
144
- hotkey_take = u16_normalized_float(hk_result or 0)
145
- else:
146
- hotkey_take = None
147
-
148
- # Get current stake
149
- print_verbose("Fetching current stake", status)
150
- old_stake = await subtensor.get_stake_for_coldkey_and_hotkey(
151
- coldkey_ss58=wallet.coldkeypub.ss58_address,
152
- hotkey_ss58=hotkey_ss58,
153
- block_hash=block_hash,
154
- )
155
-
156
- print_verbose("Fetching existential deposit", status)
157
- # Grab the existential deposit.
158
- existential_deposit = await subtensor.get_existential_deposit()
159
-
160
- # Convert to bittensor.Balance
161
- if amount is None:
162
- # Stake it all.
163
- staking_balance = Balance.from_tao(old_balance.tao)
164
- else:
165
- staking_balance = Balance.from_tao(amount)
166
-
167
- # Leave existential balance to keep key alive.
168
- if staking_balance > old_balance - existential_deposit:
169
- # If we are staking all, we need to leave at least the existential deposit.
170
- staking_balance = old_balance - existential_deposit
171
- else:
172
- staking_balance = staking_balance
173
-
174
- # Check enough to stake.
175
- if staking_balance > old_balance:
176
- err_console.print(
177
- f":cross_mark: [red]Not enough stake[/red]:[bold white]\n"
178
- f"\tbalance:\t{old_balance}\n"
179
- f"\tamount:\t{staking_balance}\n"
180
- f"\tcoldkey:\t{wallet.name}[/bold white]"
181
- )
182
- return False
183
-
184
- # If nominating, we need to check if the new stake balance will be above the minimum required stake threshold.
185
- if not own_hotkey:
186
- new_stake_balance = old_stake + staking_balance
187
- print_verbose("Fetching threshold amount")
188
- is_above_threshold, threshold = await _check_threshold_amount(
189
- subtensor, new_stake_balance, block_hash
190
- )
191
- if not is_above_threshold:
192
- err_console.print(
193
- f":cross_mark: [red]New stake balance of {new_stake_balance} is below the minimum required nomination"
194
- f" stake threshold {threshold}.[/red]"
210
+ if dynamic_info.is_dynamic:
211
+ slippage_pct = f"{slippage_pct_float:.4f} %"
212
+ rate = str(1 / (float(dynamic_info.price) or 1))
213
+ else:
214
+ slippage_pct_float = 0
215
+ slippage_pct = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]"
216
+ rate = str(1)
217
+ max_slippage = max(slippage_pct_float, max_slippage)
218
+ rows.append(
219
+ (
220
+ str(netuid),
221
+ # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}",
222
+ f"{hotkey[1]}",
223
+ str(amount_to_stake_as_balance),
224
+ rate + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ",
225
+ str(received_amount.set_unit(netuid)),
226
+ str(slippage_pct),
227
+ )
195
228
  )
196
- return False
197
-
198
- # Ask before moving on.
229
+ table.add_column("Netuid", justify="center", style="grey89")
230
+ table.add_column(
231
+ "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
232
+ )
233
+ table.add_column(
234
+ f"Amount ({Balance.get_unit(0)})",
235
+ justify="center",
236
+ style=COLOR_PALETTE["POOLS"]["TAO"],
237
+ )
238
+ table.add_column(
239
+ f"Rate (per {Balance.get_unit(0)})",
240
+ justify="center",
241
+ style=COLOR_PALETTE["POOLS"]["RATE"],
242
+ )
243
+ table.add_column(
244
+ "Received",
245
+ justify="center",
246
+ style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"],
247
+ )
248
+ table.add_column(
249
+ "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"]
250
+ )
251
+ for row in rows:
252
+ table.add_row(*row)
253
+ console.print(table)
254
+ message = ""
255
+ if max_slippage > 5:
256
+ message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n"
257
+ message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n"
258
+ message += "-------------------------------------------------------------------------------------------------------------------\n"
259
+ console.print(message)
260
+ console.print(
261
+ """
262
+ [bold white]Description[/bold white]:
263
+ The table displays information about the stake operation you are about to perform.
264
+ The columns are as follows:
265
+ - [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to.
266
+ - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to.
267
+ - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey.
268
+ - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake.
269
+ - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage.
270
+ - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root).
271
+ """
272
+ )
199
273
  if prompt:
200
- if not own_hotkey:
201
- # We are delegating.
202
- if not Confirm.ask(
203
- f"Do you want to delegate:[bold white]\n"
204
- f"\tamount: {staking_balance}\n"
205
- f"\tto: {hotkey_ss58}\n"
206
- f"\ttake: {hotkey_take}\n[/bold white]"
207
- f"\towner: {hotkey_owner}\n"
208
- ):
209
- return False
210
- else:
211
- if not Confirm.ask(
212
- f"Do you want to stake:[bold white]\n"
213
- f"\tamount: {staking_balance}\n"
214
- f"\tto: {wallet.hotkey_str}\n"
215
- f"\taddress: {hotkey_ss58}[/bold white]\n"
216
- ):
217
- return False
274
+ if not Confirm.ask("Would you like to continue?"):
275
+ raise typer.Exit()
218
276
 
219
- with console.status(
220
- f":satellite: Staking to: [bold white]{subtensor}[/bold white] ...",
221
- spinner="earth",
277
+ async def send_extrinsic(
278
+ netuid_i, amount_, current, staking_address_ss58, status=None
222
279
  ):
280
+ err_out = partial(print_error, status=status)
281
+ failure_prelude = (
282
+ f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}"
283
+ )
223
284
  call = await subtensor.substrate.compose_call(
224
285
  call_module="SubtensorModule",
225
286
  call_function="add_stake",
226
- call_params={"hotkey": hotkey_ss58, "amount_staked": staking_balance.rao},
287
+ call_params={
288
+ "hotkey": staking_address_ss58,
289
+ "netuid": netuid_i,
290
+ "amount_staked": amount_.rao,
291
+ },
227
292
  )
228
- staking_response, err_msg = await subtensor.sign_and_send_extrinsic(
229
- call, wallet, wait_for_inclusion, wait_for_finalization
293
+ extrinsic = await subtensor.substrate.create_signed_extrinsic(
294
+ call=call, keypair=wallet.coldkey
230
295
  )
231
- if staking_response is True: # If we successfully staked.
232
- # We only wait here if we expect finalization.
233
- if not wait_for_finalization and not wait_for_inclusion:
234
- return True
235
-
236
- console.print(":white_heavy_check_mark: [green]Finalized[/green]")
237
- with console.status(
238
- f":satellite: Checking Balance on: [white]{subtensor}[/white] ..."
239
- ):
240
- new_block_hash = await subtensor.substrate.get_chain_head()
241
- new_balance, new_stake = await asyncio.gather(
242
- subtensor.get_balance(
243
- wallet.coldkeypub.ss58_address, block_hash=new_block_hash
244
- ),
245
- subtensor.get_stake_for_coldkey_and_hotkey(
246
- coldkey_ss58=wallet.coldkeypub.ss58_address,
247
- hotkey_ss58=hotkey_ss58,
248
- block_hash=new_block_hash,
249
- ),
250
- )
251
-
252
- console.print(
253
- f"Balance:\n"
254
- f"\t[blue]{old_balance}[/blue] :arrow_right: "
255
- f"[green]{new_balance[wallet.coldkeypub.ss58_address]}[/green]"
296
+ try:
297
+ response = await subtensor.substrate.submit_extrinsic(
298
+ extrinsic, wait_for_inclusion=True, wait_for_finalization=False
256
299
  )
257
- console.print(
258
- f"Stake:\n"
259
- f"\t[blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]"
300
+ except SubstrateRequestException as e:
301
+ err_out(
302
+ f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}"
260
303
  )
261
- return True
262
- else:
263
- err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
304
+ return
305
+ else:
306
+ await response.process_events()
307
+ if not await response.is_success:
308
+ err_out(
309
+ f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}"
310
+ )
311
+ else:
312
+ new_balance, stake_info_dict = await asyncio.gather(
313
+ subtensor.get_balance(wallet.coldkeypub.ss58_address),
314
+ subtensor.get_stake_for_coldkey(
315
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
316
+ ),
317
+ )
318
+ new_stake = Balance.from_rao(0)
319
+ for stake_info in stake_info_dict:
320
+ if (
321
+ stake_info.hotkey_ss58 == staking_address_ss58
322
+ and stake_info.netuid == netuid_i
323
+ ):
324
+ new_stake = stake_info.stake.set_unit(netuid_i)
325
+ break
326
+ console.print(":white_heavy_check_mark: [green]Finalized[/green]")
327
+ console.print(
328
+ f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
329
+ )
330
+ console.print(
331
+ f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] Stake:\n [blue]{current}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}"
332
+ )
333
+
334
+ # Perform staking operation.
335
+ try:
336
+ wallet.unlock_coldkey()
337
+ except KeyFileError:
338
+ err_console.print("Error decrypting coldkey (possibly incorrect password)")
264
339
  return False
340
+ extrinsics_coroutines = [
341
+ send_extrinsic(ni, am, curr, staking_address)
342
+ for i, (ni, am, curr) in enumerate(
343
+ zip(netuids, stake_amount_balance, current_stake_balances)
344
+ )
345
+ for _, staking_address in hotkeys_to_stake_to
346
+ ]
347
+ if len(extrinsics_coroutines) == 1:
348
+ with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."):
349
+ await extrinsics_coroutines[0]
350
+ else:
351
+ with console.status(":satellite: Checking transaction rate limit ..."):
352
+ tx_rate_limit_blocks = await subtensor.query(
353
+ module="SubtensorModule", storage_function="TxRateLimit"
354
+ )
355
+ netuid_hk_pairs = [(ni, hk) for ni in netuids for hk in hotkeys_to_stake_to]
356
+ for item, kp in zip(extrinsics_coroutines, netuid_hk_pairs):
357
+ ni, hk = kp
358
+ with console.status(
359
+ f"\n:satellite: Staking on netuid {ni} with hotkey {hk}... ..."
360
+ ):
361
+ await item
362
+ if tx_rate_limit_blocks > 0:
363
+ with console.status(
364
+ f":hourglass: [yellow]Waiting for tx rate limit:"
365
+ f" [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]"
366
+ ):
367
+ await asyncio.sleep(tx_rate_limit_blocks * 12) # 12 sec per block
265
368
 
266
369
 
267
- async def add_stake_multiple_extrinsic(
370
+ async def unstake_selection(
268
371
  subtensor: "SubtensorInterface",
269
372
  wallet: Wallet,
270
- old_balance: Balance,
271
- hotkey_ss58s: list[str],
272
- amounts: Optional[list[Balance]] = None,
273
- wait_for_inclusion: bool = True,
274
- wait_for_finalization: bool = False,
275
- prompt: bool = False,
276
- ) -> bool:
277
- """Adds stake to each ``hotkey_ss58`` in the list, using each amount, from a common coldkey.
278
-
279
- :param subtensor: The initialized SubtensorInterface object.
280
- :param wallet: Bittensor wallet object for the coldkey.
281
- :param old_balance: The balance of the wallet prior to staking.
282
- :param hotkey_ss58s: List of hotkeys to stake to.
283
- :param amounts: List of amounts to stake. If `None`, stake all to the first hotkey.
284
- :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns
285
- `False` if the extrinsic fails to enter the block within the timeout.
286
- :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
287
- or returns `False` if the extrinsic fails to be finalized within the timeout.
288
- :param prompt: If `True`, the call waits for confirmation from the user before proceeding.
289
-
290
- :return: success: `True` if extrinsic was finalized or included in the block. `True` if any wallet was staked. If
291
- we did not wait for finalization/inclusion, the response is `True`.
292
- """
293
-
294
- if len(hotkey_ss58s) == 0:
295
- return True
296
-
297
- if amounts is not None and len(amounts) != len(hotkey_ss58s):
298
- raise ValueError("amounts must be a list of the same length as hotkey_ss58s")
299
-
300
- new_amounts: Sequence[Optional[Balance]]
301
- if amounts is None:
302
- new_amounts = [None] * len(hotkey_ss58s)
303
- else:
304
- new_amounts = [Balance.from_tao(amount) for amount in amounts]
305
- if sum(amount.tao for amount in new_amounts) == 0:
306
- # Staking 0 tao
307
- return True
373
+ dynamic_info,
374
+ identities,
375
+ old_identities,
376
+ netuid: Optional[int] = None,
377
+ ):
378
+ stake_infos = await subtensor.get_stake_for_coldkey(
379
+ coldkey_ss58=wallet.coldkeypub.ss58_address
380
+ )
308
381
 
309
- # Decrypt coldkey.
310
- if not unlock_key(wallet).success:
311
- return False
382
+ if not stake_infos:
383
+ print_error("You have no stakes to unstake.")
384
+ raise typer.Exit()
312
385
 
313
- with console.status(
314
- f":satellite: Syncing with chain: [white]{subtensor}[/white] ..."
315
- ):
316
- block_hash = await subtensor.substrate.get_chain_head()
317
- old_stakes = await asyncio.gather(
318
- *[
319
- subtensor.get_stake_for_coldkey_and_hotkey(
320
- hk, wallet.coldkeypub.ss58_address, block_hash=block_hash
321
- )
322
- for hk in hotkey_ss58s
323
- ]
386
+ hotkey_stakes = {}
387
+ for stake_info in stake_infos:
388
+ if netuid is not None and stake_info.netuid != netuid:
389
+ continue
390
+ hotkey_ss58 = stake_info.hotkey_ss58
391
+ netuid_ = stake_info.netuid
392
+ stake_amount = stake_info.stake
393
+ if stake_amount.tao > 0:
394
+ hotkey_stakes.setdefault(hotkey_ss58, {})[netuid_] = stake_amount
395
+
396
+ if not hotkey_stakes:
397
+ if netuid is not None:
398
+ print_error(f"You have no stakes to unstake in subnet {netuid}.")
399
+ else:
400
+ print_error("You have no stakes to unstake.")
401
+ raise typer.Exit()
402
+
403
+ hotkeys_info = []
404
+ for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()):
405
+ if hk_identity := identities["hotkeys"].get(hotkey_ss58):
406
+ hotkey_name = hk_identity.get("identity", {}).get(
407
+ "name", ""
408
+ ) or hk_identity.get("display", "~")
409
+ elif old_identity := old_identities.get(hotkey_ss58):
410
+ hotkey_name = old_identity.display
411
+ else:
412
+ hotkey_name = "~"
413
+ # TODO: Add wallet ids here.
414
+
415
+ hotkeys_info.append(
416
+ {
417
+ "index": idx,
418
+ "identity": hotkey_name,
419
+ "netuids": list(netuid_stakes.keys()),
420
+ "hotkey_ss58": hotkey_ss58,
421
+ }
324
422
  )
325
423
 
326
- # Remove existential balance to keep key alive.
327
- ## Keys must maintain a balance of at least 1000 rao to stay alive.
328
- total_staking_rao = sum(
329
- [amount.rao if amount is not None else 0 for amount in new_amounts]
424
+ # Display existing hotkeys, id, and staked netuids.
425
+ subnet_filter = f" for Subnet {netuid}" if netuid is not None else ""
426
+ table = Table(
427
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes{subnet_filter}\n",
428
+ show_footer=True,
429
+ show_edge=False,
430
+ header_style="bold white",
431
+ border_style="bright_black",
432
+ style="bold",
433
+ title_justify="center",
434
+ show_lines=False,
435
+ pad_edge=True,
436
+ )
437
+ table.add_column("Index", justify="right")
438
+ table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"])
439
+ table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"])
440
+ table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"])
441
+
442
+ for hotkey_info in hotkeys_info:
443
+ index = str(hotkey_info["index"])
444
+ identity = hotkey_info["identity"]
445
+ netuids = group_subnets([n for n in hotkey_info["netuids"]])
446
+ hotkey_ss58 = hotkey_info["hotkey_ss58"]
447
+ table.add_row(index, identity, netuids, hotkey_ss58)
448
+
449
+ console.print("\n", table)
450
+
451
+ # Prompt to select hotkey to unstake.
452
+ hotkey_options = [str(hotkey_info["index"]) for hotkey_info in hotkeys_info]
453
+ hotkey_idx = Prompt.ask(
454
+ "\nEnter the index of the hotkey you want to unstake from",
455
+ choices=hotkey_options,
456
+ )
457
+ selected_hotkey_info = hotkeys_info[int(hotkey_idx)]
458
+ selected_hotkey_ss58 = selected_hotkey_info["hotkey_ss58"]
459
+ selected_hotkey_name = selected_hotkey_info["identity"]
460
+ netuid_stakes = hotkey_stakes[selected_hotkey_ss58]
461
+
462
+ # Display hotkey's staked netuids with amount.
463
+ table = Table(
464
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Stakes for hotkey \n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey_name}\n{selected_hotkey_ss58}\n",
465
+ show_footer=True,
466
+ show_edge=False,
467
+ header_style="bold white",
468
+ border_style="bright_black",
469
+ style="bold",
470
+ title_justify="center",
471
+ show_lines=False,
472
+ pad_edge=True,
473
+ )
474
+ table.add_column("Subnet", justify="right")
475
+ table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"])
476
+ table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"])
477
+ table.add_column(
478
+ f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)",
479
+ style=COLOR_PALETTE["POOLS"]["RATE"],
480
+ justify="left",
330
481
  )
331
- if total_staking_rao == 0:
332
- # Staking all to the first wallet.
333
- if old_balance.rao > 1000:
334
- old_balance -= Balance.from_rao(1000)
335
-
336
- elif total_staking_rao < 1000:
337
- # Staking less than 1000 rao to the wallets.
338
- pass
339
- else:
340
- # Staking more than 1000 rao to the wallets.
341
- ## Reduce the amount to stake to each wallet to keep the balance above 1000 rao.
342
- percent_reduction = 1 - (1000 / total_staking_rao)
343
- new_amounts = [
344
- Balance.from_tao(amount.tao * percent_reduction)
345
- for amount in cast(Sequence[Balance], new_amounts)
346
- ]
347
-
348
- successful_stakes = 0
349
- for idx, (hotkey_ss58, amount, old_stake) in enumerate(
350
- zip(hotkey_ss58s, new_amounts, old_stakes)
351
- ):
352
- staking_all = False
353
- # Convert to bittensor.Balance
354
- if amount is None:
355
- # Stake it all.
356
- staking_balance = Balance.from_tao(old_balance.tao)
357
- staking_all = True
358
- else:
359
- # Amounts are cast to balance earlier in the function
360
- assert isinstance(amount, Balance)
361
- staking_balance = amount
362
482
 
363
- # Check enough to stake
364
- if staking_balance > old_balance:
365
- err_console.print(
366
- f":cross_mark: [red]Not enough balance[/red]:"
367
- f" [green]{old_balance}[/green] to stake: [blue]{staking_balance}[/blue]"
368
- f" from coldkey: [white]{wallet.name}[/white]"
483
+ for netuid_, stake_amount in netuid_stakes.items():
484
+ symbol = dynamic_info[netuid_].symbol
485
+ rate = f"{dynamic_info[netuid_].price.tao:.4f} τ/{symbol}"
486
+ table.add_row(str(netuid_), symbol, str(stake_amount), rate)
487
+ console.print("\n", table, "\n")
488
+
489
+ # Ask which netuids to unstake from for the selected hotkey.
490
+ unstake_all = False
491
+ if netuid is not None:
492
+ selected_netuids = [netuid]
493
+ else:
494
+ while True:
495
+ netuid_input = Prompt.ask(
496
+ "\nEnter the netuids of the [blue]subnets to unstake[/blue] from (comma-separated), or '[blue]all[/blue]' to unstake from all",
497
+ default="all",
369
498
  )
370
- continue
371
499
 
372
- # Ask before moving on.
373
- if prompt:
374
- if not Confirm.ask(
375
- f"Do you want to stake:\n"
376
- f"\t[bold white]amount: {staking_balance}\n"
377
- f"\thotkey: {wallet.hotkey_str}[/bold white ]?"
378
- ):
379
- continue
500
+ if netuid_input.lower() == "all":
501
+ selected_netuids = list(netuid_stakes.keys())
502
+ unstake_all = True
503
+ break
504
+ else:
505
+ try:
506
+ netuid_list = [int(n.strip()) for n in netuid_input.split(",")]
507
+ invalid_netuids = [n for n in netuid_list if n not in netuid_stakes]
508
+ if invalid_netuids:
509
+ print_error(
510
+ f"The following netuids are invalid or not available: {', '.join(map(str, invalid_netuids))}. Please try again."
511
+ )
512
+ else:
513
+ selected_netuids = netuid_list
514
+ break
515
+ except ValueError:
516
+ print_error(
517
+ "Please enter valid netuids (numbers), separated by commas, or 'all'."
518
+ )
380
519
 
381
- call = await subtensor.substrate.compose_call(
382
- call_module="SubtensorModule",
383
- call_function="add_stake",
384
- call_params={"hotkey": hotkey_ss58, "amount_staked": staking_balance.rao},
385
- )
386
- staking_response, err_msg = await subtensor.sign_and_send_extrinsic(
387
- call, wallet, wait_for_inclusion, wait_for_finalization
520
+ hotkeys_to_unstake_from = []
521
+ for netuid_ in selected_netuids:
522
+ hotkeys_to_unstake_from.append(
523
+ (selected_hotkey_name, selected_hotkey_ss58, netuid_)
388
524
  )
389
-
390
- if staking_response is True: # If we successfully staked.
391
- # We only wait here if we expect finalization.
392
-
393
- if idx < len(hotkey_ss58s) - 1:
394
- # Wait for tx rate limit.
395
- tx_query = await subtensor.substrate.query(
396
- module="SubtensorModule",
397
- storage_function="TxRateLimit",
398
- block_hash=block_hash,
399
- )
400
- tx_rate_limit_blocks: int = tx_query
401
- if tx_rate_limit_blocks > 0:
402
- with console.status(
403
- f":hourglass: [yellow]Waiting for tx rate limit:"
404
- f" [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]"
405
- ):
406
- await asyncio.sleep(
407
- tx_rate_limit_blocks * 12
408
- ) # 12 seconds per block
409
-
410
- if not wait_for_finalization and not wait_for_inclusion:
411
- old_balance -= staking_balance
412
- successful_stakes += 1
413
- if staking_all:
414
- # If staked all, no need to continue
415
- break
416
-
417
- continue
418
-
419
- console.print(":white_heavy_check_mark: [green]Finalized[/green]")
420
-
421
- new_block_hash = await subtensor.substrate.get_chain_head()
422
- new_stake, new_balance_ = await asyncio.gather(
423
- subtensor.get_stake_for_coldkey_and_hotkey(
424
- coldkey_ss58=wallet.coldkeypub.ss58_address,
425
- hotkey_ss58=hotkey_ss58,
426
- block_hash=new_block_hash,
427
- ),
428
- subtensor.get_balance(
429
- wallet.coldkeypub.ss58_address, block_hash=new_block_hash
430
- ),
431
- )
432
- new_balance = new_balance_[wallet.coldkeypub.ss58_address]
433
- console.print(
434
- "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format(
435
- hotkey_ss58, old_stake, new_stake
525
+ return hotkeys_to_unstake_from, unstake_all
526
+
527
+
528
+ def ask_unstake_amount(
529
+ current_stake_balance: Balance,
530
+ netuid: int,
531
+ staking_address_name: str,
532
+ staking_address_ss58: str,
533
+ interactive: bool,
534
+ ) -> Optional[Balance]:
535
+ """Prompt the user to decide the amount to unstake."""
536
+ while True:
537
+ response = Prompt.ask(
538
+ f"Unstake all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
539
+ f" from [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{staking_address_name if staking_address_name else staking_address_ss58}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
540
+ f" on netuid: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]? [y/n/q]",
541
+ choices=["y", "n", "q"],
542
+ default="n" if interactive else "y",
543
+ show_choices=True,
544
+ ).lower()
545
+
546
+ if response == "q":
547
+ return None # Quit
548
+
549
+ elif response == "y":
550
+ return current_stake_balance
551
+
552
+ elif response == "n":
553
+ while True:
554
+ amount_input = Prompt.ask(
555
+ f"Enter amount to unstake in [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
556
+ f" from subnet: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
557
+ f" (Max: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}])"
436
558
  )
437
- )
438
- old_balance = new_balance
439
- successful_stakes += 1
440
- if staking_all:
441
- # If staked all, no need to continue
442
- break
559
+ if amount_input.lower() == "q":
560
+ return None # Quit
561
+
562
+ try:
563
+ amount_value = float(amount_input)
564
+ if amount_value <= 0:
565
+ console.print("[red]Amount must be greater than zero.[/red]")
566
+ continue # Re-prompt
567
+
568
+ amount_to_unstake = Balance.from_tao(amount_value)
569
+ amount_to_unstake.set_unit(netuid)
570
+ if amount_to_unstake > current_stake_balance:
571
+ console.print(
572
+ f"[red]Amount exceeds current stake balance of {current_stake_balance}.[/red]"
573
+ )
574
+ continue # Re-prompt
575
+
576
+ return amount_to_unstake
577
+
578
+ except ValueError:
579
+ console.print(
580
+ "[red]Invalid input. Please enter a numeric value or 'q' to quit.[/red]"
581
+ )
443
582
 
444
583
  else:
445
- err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
446
- continue
447
-
448
- if successful_stakes != 0:
449
- with console.status(
450
- f":satellite: Checking Balance on: ([white]{subtensor}[/white] ..."
451
- ):
452
- new_balance_ = await subtensor.get_balance(
453
- wallet.coldkeypub.ss58_address, reuse_block=False
454
- )
455
- new_balance = new_balance_[wallet.coldkeypub.ss58_address]
456
- console.print(
457
- f"Balance: [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance}[/green]"
458
- )
459
- return True
584
+ console.print("[red]Invalid input. Please enter 'y', 'n', or 'q'.[/red]")
460
585
 
461
- return False
462
586
 
463
-
464
- async def unstake_extrinsic(
465
- subtensor: "SubtensorInterface",
587
+ async def _unstake_all(
466
588
  wallet: Wallet,
467
- hotkey_ss58: Optional[str] = None,
468
- amount: Optional[Balance] = None,
469
- wait_for_inclusion: bool = True,
470
- wait_for_finalization: bool = False,
471
- prompt: bool = False,
589
+ subtensor: "SubtensorInterface",
590
+ unstake_all_alpha: bool = False,
591
+ prompt: bool = True,
472
592
  ) -> bool:
473
- """Removes stake into the wallet coldkey from the specified hotkey ``uid``.
474
-
475
- :param subtensor: the initialized SubtensorInterface object to use
476
- :param wallet: Bittensor wallet object.
477
- :param hotkey_ss58: The `ss58` address of the hotkey to unstake from. By default, the wallet hotkey is used.
478
- :param amount: Amount to stake as Bittensor balance, or `None` is unstaking all
479
- :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns
480
- `False` if the extrinsic fails to enter the block within the timeout.
481
- :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
482
- or returns `False` if the extrinsic fails to be finalized within the timeout.
483
- :param prompt: If `True`, the call waits for confirmation from the user before proceeding.
484
-
485
- :return: success: `True` if extrinsic was finalized or included in the block. If we did not wait for
486
- finalization/inclusion, the response is `True`.
487
- """
488
- # Decrypt coldkey
489
- if not unlock_key(wallet).success:
490
- return False
491
-
492
- if hotkey_ss58 is None:
493
- hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey.
593
+ """Unstakes all stakes from all hotkeys in all subnets."""
494
594
 
495
595
  with console.status(
496
- f":satellite: Syncing with chain: [white]{subtensor}[/white] ...",
497
- spinner="aesthetic",
498
- ) as status:
499
- print_verbose("Fetching balance and stake", status)
500
- block_hash = await subtensor.substrate.get_chain_head()
501
- old_balance, old_stake, hotkey_owner = await asyncio.gather(
502
- subtensor.get_balance(
503
- wallet.coldkeypub.ss58_address, block_hash=block_hash
504
- ),
505
- subtensor.get_stake_for_coldkey_and_hotkey(
506
- coldkey_ss58=wallet.coldkeypub.ss58_address,
507
- hotkey_ss58=hotkey_ss58,
508
- block_hash=block_hash,
509
- ),
510
- subtensor.get_hotkey_owner(hotkey_ss58, block_hash),
596
+ f"Retrieving stake information & identities from {subtensor.network}...",
597
+ spinner="earth",
598
+ ):
599
+ (
600
+ stake_info,
601
+ ck_hk_identities,
602
+ old_identities,
603
+ all_sn_dynamic_info_,
604
+ current_wallet_balance,
605
+ ) = await asyncio.gather(
606
+ subtensor.get_stake_for_coldkey(wallet.coldkeypub.ss58_address),
607
+ subtensor.fetch_coldkey_hotkey_identities(),
608
+ subtensor.get_delegate_identities(),
609
+ subtensor.all_subnets(),
610
+ subtensor.get_balance(wallet.coldkeypub.ss58_address),
511
611
  )
512
612
 
513
- own_hotkey: bool = wallet.coldkeypub.ss58_address == hotkey_owner
613
+ if unstake_all_alpha:
614
+ stake_info = [stake for stake in stake_info if stake.netuid != 0]
514
615
 
515
- # Convert to bittensor.Balance
516
- if amount is None:
517
- # Unstake it all.
518
- unstaking_balance = old_stake
519
- else:
520
- unstaking_balance = Balance.from_tao(amount)
521
-
522
- # Check enough to unstake.
523
- stake_on_uid = old_stake
524
- if unstaking_balance > stake_on_uid:
525
- err_console.print(
526
- f":cross_mark: [red]Not enough stake[/red]: "
527
- f"[green]{stake_on_uid}[/green] to unstake: "
528
- f"[blue]{unstaking_balance}[/blue] from hotkey:"
529
- f" [white]{wallet.hotkey_str}[/white]"
530
- )
531
- return False
616
+ if not stake_info:
617
+ console.print("[red]No stakes found to unstake[/red]")
618
+ return False
532
619
 
533
- print_verbose("Fetching threshold amount")
534
- # If nomination stake, check threshold.
535
- if not own_hotkey and not await _check_threshold_amount(
536
- subtensor=subtensor,
537
- sb=(stake_on_uid - unstaking_balance),
538
- block_hash=block_hash,
539
- ):
540
- console.print(
541
- ":warning: [yellow]This action will unstake the entire staked balance![/yellow]"
620
+ all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_}
621
+
622
+ # Calculate total value and slippage for all stakes
623
+ total_received_value = Balance(0)
624
+ table_title = (
625
+ "Unstaking Summary - All Stakes"
626
+ if not unstake_all_alpha
627
+ else "Unstaking Summary - All Alpha Stakes"
628
+ )
629
+ table = Table(
630
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{table_title}\nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n",
631
+ show_footer=True,
632
+ show_edge=False,
633
+ header_style="bold white",
634
+ border_style="bright_black",
635
+ style="bold",
636
+ title_justify="center",
637
+ show_lines=False,
638
+ pad_edge=True,
639
+ )
640
+ table.add_column("Netuid", justify="center", style="grey89")
641
+ table.add_column(
642
+ "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
542
643
  )
543
- unstaking_balance = stake_on_uid
644
+ table.add_column(
645
+ f"Current Stake ({Balance.get_unit(1)})",
646
+ justify="center",
647
+ style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"],
648
+ )
649
+ table.add_column(
650
+ f"Rate ({Balance.unit}/{Balance.get_unit(1)})",
651
+ justify="center",
652
+ style=COLOR_PALETTE["POOLS"]["RATE"],
653
+ )
654
+ table.add_column(
655
+ f"Recieved ({Balance.unit})",
656
+ justify="center",
657
+ style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"],
658
+ )
659
+ table.add_column(
660
+ "Slippage",
661
+ justify="center",
662
+ style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"],
663
+ )
664
+ max_slippage = 0.0
665
+ for stake in stake_info:
666
+ if stake.stake.rao == 0:
667
+ continue
544
668
 
545
- # Ask before moving on.
546
- if prompt:
547
- if not Confirm.ask(
548
- f"Do you want to unstake:\n"
549
- f"[bold white]\tamount: {unstaking_balance}\n"
550
- f"\thotkey: {wallet.hotkey_str}[/bold white ]?"
551
- ):
552
- return False
669
+ dynamic_info = all_sn_dynamic_info.get(stake.netuid)
670
+ stake_amount = stake.stake
671
+ received_amount, _, slippage_pct_float = (
672
+ dynamic_info.alpha_to_tao_with_slippage(stake_amount)
673
+ )
553
674
 
554
- with console.status(
555
- f":satellite: Unstaking from chain: [white]{subtensor}[/white] ...",
556
- spinner="earth",
675
+ total_received_value += received_amount
676
+
677
+ # Get hotkey identity
678
+ if hk_identity := ck_hk_identities["hotkeys"].get(stake.hotkey_ss58):
679
+ hotkey_name = hk_identity.get("identity", {}).get(
680
+ "name", ""
681
+ ) or hk_identity.get("display", "~")
682
+ hotkey_display = f"{hotkey_name}"
683
+ elif old_identity := old_identities.get(stake.hotkey_ss58):
684
+ hotkey_name = old_identity.display
685
+ hotkey_display = f"{hotkey_name}"
686
+ else:
687
+ hotkey_display = stake.hotkey_ss58
688
+
689
+ if dynamic_info.is_dynamic:
690
+ slippage_pct = f"{slippage_pct_float:.4f} %"
691
+ else:
692
+ slippage_pct_float = 0
693
+ slippage_pct = "[red]N/A[/red]"
694
+
695
+ max_slippage = max(max_slippage, slippage_pct_float)
696
+
697
+ table.add_row(
698
+ str(stake.netuid),
699
+ hotkey_display,
700
+ str(stake_amount),
701
+ str(float(dynamic_info.price))
702
+ + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})",
703
+ str(received_amount),
704
+ slippage_pct,
705
+ )
706
+ console.print(table)
707
+ message = ""
708
+ if max_slippage > 5:
709
+ message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n"
710
+ 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"
711
+ message += "-------------------------------------------------------------------------------------------------------------------\n"
712
+ console.print(message)
713
+
714
+ console.print(
715
+ f"Expected return after slippage: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}"
716
+ )
717
+
718
+ if prompt and not Confirm.ask(
719
+ "\nDo you want to proceed with unstaking everything?"
557
720
  ):
721
+ return False
722
+
723
+ try:
724
+ wallet.unlock_coldkey()
725
+ except KeyFileError:
726
+ err_console.print("Error decrypting coldkey (possibly incorrect password)")
727
+ return False
728
+
729
+ console_status = (
730
+ ":satellite: Unstaking all Alpha stakes..."
731
+ if unstake_all_alpha
732
+ else ":satellite: Unstaking all stakes..."
733
+ )
734
+ with console.status(console_status):
735
+ call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all"
558
736
  call = await subtensor.substrate.compose_call(
559
737
  call_module="SubtensorModule",
560
- call_function="remove_stake",
561
- call_params={
562
- "hotkey": hotkey_ss58,
563
- "amount_unstaked": unstaking_balance.rao,
564
- },
738
+ call_function=call_function,
739
+ call_params={"hotkey": wallet.hotkey.ss58_address},
565
740
  )
566
- staking_response, err_msg = await subtensor.sign_and_send_extrinsic(
567
- call, wallet, wait_for_inclusion, wait_for_finalization
741
+ success, error_message = await subtensor.sign_and_send_extrinsic(
742
+ call=call,
743
+ wallet=wallet,
744
+ wait_for_inclusion=True,
745
+ wait_for_finalization=False,
568
746
  )
569
747
 
570
- if staking_response is True: # If we successfully unstaked.
571
- # We only wait here if we expect finalization.
572
- if not wait_for_finalization and not wait_for_inclusion:
573
- return True
574
-
575
- console.print(":white_heavy_check_mark: [green]Finalized[/green]")
576
- with console.status(
577
- f":satellite: Checking Balance on: [white]{subtensor}[/white] ..."
578
- ):
579
- new_block_hash = await subtensor.substrate.get_chain_head()
580
- new_balance, new_stake = await asyncio.gather(
581
- subtensor.get_balance(
582
- wallet.coldkeypub.ss58_address, block_hash=new_block_hash
583
- ),
584
- subtensor.get_stake_for_coldkey_and_hotkey(
585
- hotkey_ss58, wallet.coldkeypub.ss58_address, new_block_hash
586
- ),
587
- )
588
- console.print(
589
- f"Balance:\n"
590
- f" [blue]{old_balance[wallet.coldkeypub.ss58_address]}[/blue] :arrow_right:"
591
- f" [green]{new_balance[wallet.coldkeypub.ss58_address]}[/green]"
748
+ if success:
749
+ success_message = (
750
+ ":white_heavy_check_mark: [green]Successfully unstaked all stakes[/green]"
751
+ if not unstake_all_alpha
752
+ else ":white_heavy_check_mark: [green]Successfully unstaked all Alpha stakes[/green]"
592
753
  )
754
+ console.print(success_message)
755
+ new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address)
593
756
  console.print(
594
- f"Stake:\n [blue]{old_stake}[/blue] :arrow_right: [green]{new_stake}[/green]"
757
+ f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
595
758
  )
596
759
  return True
597
- else:
598
- err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
599
- return False
760
+ else:
761
+ err_console.print(
762
+ f":cross_mark: [red]Failed to unstake[/red]: {error_message}"
763
+ )
764
+ return False
600
765
 
601
766
 
602
- async def unstake_multiple_extrinsic(
603
- subtensor: "SubtensorInterface",
767
+ async def unstake(
604
768
  wallet: Wallet,
605
- hotkey_ss58s: list[str],
606
- amounts: Optional[list[Union[Balance, float]]] = None,
607
- wait_for_inclusion: bool = True,
608
- wait_for_finalization: bool = False,
609
- prompt: bool = False,
610
- ) -> bool:
611
- """
612
- Removes stake from each `hotkey_ss58` in the list, using each amount, to a common coldkey.
613
-
614
- :param subtensor: the initialized SubtensorInterface object to use
615
- :param wallet: The wallet with the coldkey to unstake to.
616
- :param hotkey_ss58s: List of hotkeys to unstake from.
617
- :param amounts: List of amounts to unstake. If ``None``, unstake all.
618
- :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns
619
- `False` if the extrinsic fails to enter the block within the timeout.
620
- :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
621
- or returns `False` if the extrinsic fails to be finalized within the timeout.
622
- :param prompt: If `True`, the call waits for confirmation from the user before proceeding.
623
-
624
- :return: success: `True` if extrinsic was finalized or included in the block. Flag is `True` if any wallet was
625
- unstaked. If we did not wait for finalization/inclusion, the response is `True`.
626
- """
627
- if not isinstance(hotkey_ss58s, list) or not all(
628
- isinstance(hotkey_ss58, str) for hotkey_ss58 in hotkey_ss58s
629
- ):
630
- raise TypeError("hotkey_ss58s must be a list of str")
631
-
632
- if len(hotkey_ss58s) == 0:
633
- return True
769
+ subtensor: "SubtensorInterface",
770
+ hotkey_ss58_address: str,
771
+ all_hotkeys: bool,
772
+ include_hotkeys: list[str],
773
+ exclude_hotkeys: list[str],
774
+ amount: float,
775
+ keep_stake: float,
776
+ unstake_all: bool,
777
+ prompt: bool,
778
+ interactive: bool = False,
779
+ netuid: Optional[int] = None,
780
+ unstake_all_alpha: bool = False,
781
+ ):
782
+ """Unstake tokens from hotkey(s)."""
634
783
 
635
- if amounts is not None and len(amounts) != len(hotkey_ss58s):
636
- raise ValueError("amounts must be a list of the same length as hotkey_ss58s")
784
+ if unstake_all or unstake_all_alpha:
785
+ return await _unstake_all(wallet, subtensor, unstake_all_alpha, prompt)
637
786
 
638
- if amounts is not None and not all(
639
- isinstance(amount, (Balance, float)) for amount in amounts
787
+ unstake_all_from_hk = False
788
+ with console.status(
789
+ f"Retrieving subnet data & identities from {subtensor.network}...",
790
+ spinner="earth",
640
791
  ):
641
- raise TypeError(
642
- "amounts must be a [list of bittensor.Balance or float] or None"
792
+ all_sn_dynamic_info_, ck_hk_identities, old_identities = await asyncio.gather(
793
+ subtensor.all_subnets(),
794
+ subtensor.fetch_coldkey_hotkey_identities(),
795
+ subtensor.get_delegate_identities(),
796
+ )
797
+ all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_}
798
+
799
+ if interactive:
800
+ hotkeys_to_unstake_from, unstake_all_from_hk = await unstake_selection(
801
+ subtensor,
802
+ wallet,
803
+ all_sn_dynamic_info,
804
+ ck_hk_identities,
805
+ old_identities,
806
+ netuid=netuid,
643
807
  )
808
+ if not hotkeys_to_unstake_from:
809
+ console.print("[red]No unstake operations to perform.[/red]")
810
+ return False
811
+ netuids = list({netuid for _, _, netuid in hotkeys_to_unstake_from})
644
812
 
645
- new_amounts: Sequence[Optional[Balance]]
646
- if amounts is None:
647
- new_amounts = [None] * len(hotkey_ss58s)
648
813
  else:
649
- new_amounts = [
650
- Balance(amount) if not isinstance(amount, Balance) else amount
651
- for amount in (amounts or [None] * len(hotkey_ss58s))
652
- ]
653
- if sum(amount.tao for amount in new_amounts if amount is not None) == 0:
654
- return True
814
+ netuids = (
815
+ [int(netuid)]
816
+ if netuid is not None
817
+ else await subtensor.get_all_subnet_netuids()
818
+ )
655
819
 
656
- # Unlock coldkey.
657
- if not unlock_key(wallet).success:
658
- return False
820
+ # Get the hotkey_names (if any) and the hotkey_ss58s.
821
+ hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = []
822
+ if hotkey_ss58_address:
823
+ print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})")
824
+ # Unstake from specific hotkey.
825
+ hotkeys_to_unstake_from = [(None, hotkey_ss58_address)]
826
+ elif all_hotkeys:
827
+ print_verbose("Unstaking from all hotkeys")
828
+ # Unstake from all hotkeys.
829
+ all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet)
830
+ # Exclude hotkeys that are specified.
831
+ hotkeys_to_unstake_from = [
832
+ (wallet.hotkey_str, wallet.hotkey.ss58_address)
833
+ for wallet in all_hotkeys_
834
+ if wallet.hotkey_str not in exclude_hotkeys
835
+ ]
836
+ elif include_hotkeys:
837
+ print_verbose("Unstaking from included hotkeys")
838
+ # Unstake from specific hotkeys.
839
+ for hotkey_identifier in include_hotkeys:
840
+ if is_valid_ss58_address(hotkey_identifier):
841
+ # If the hotkey is a valid ss58 address, we add it to the list.
842
+ hotkeys_to_unstake_from.append((None, hotkey_identifier))
843
+ else:
844
+ # If the hotkey is not a valid ss58 address, we assume it is a hotkey name.
845
+ # We then get the hotkey from the wallet and add it to the list.
846
+ wallet_ = Wallet(
847
+ name=wallet.name,
848
+ path=wallet.path,
849
+ hotkey=hotkey_identifier,
850
+ )
851
+ hotkeys_to_unstake_from.append(
852
+ (wallet_.hotkey_str, wallet_.hotkey.ss58_address)
853
+ )
854
+ else:
855
+ # Only cli.config.wallet.hotkey is specified.
856
+ # So we unstake from that single hotkey.
857
+ print_verbose(
858
+ f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})"
859
+ )
860
+ assert wallet.hotkey is not None
861
+ hotkeys_to_unstake_from = [(wallet.hotkey_str, wallet.hotkey.ss58_address)]
659
862
 
660
863
  with console.status(
661
- f":satellite: Syncing with chain: [white]{subtensor}[/white] ..."
864
+ f"Retrieving stake data from {subtensor.network}...",
865
+ spinner="earth",
662
866
  ):
663
- block_hash = await subtensor.substrate.get_chain_head()
664
-
665
- old_balance_ = subtensor.get_balance(
666
- wallet.coldkeypub.ss58_address, block_hash=block_hash
667
- )
668
- old_stakes_ = asyncio.gather(
669
- *[
670
- subtensor.get_stake_for_coldkey_and_hotkey(
671
- h, wallet.coldkeypub.ss58_address, block_hash
672
- )
673
- for h in hotkey_ss58s
674
- ]
867
+ # Prepare unstaking transactions
868
+ unstake_operations = []
869
+ total_received_amount = Balance.from_tao(0)
870
+ current_wallet_balance: Balance = await subtensor.get_balance(
871
+ wallet.coldkeypub.ss58_address
675
872
  )
676
- hotkey_owners_ = asyncio.gather(
677
- *[subtensor.get_hotkey_owner(h, block_hash) for h in hotkey_ss58s]
873
+ max_float_slippage = 0
874
+
875
+ # Fetch stake balances
876
+ chain_head = await subtensor.substrate.get_chain_head()
877
+ stake_info_list = await subtensor.get_stake_for_coldkey(
878
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
879
+ block_hash=chain_head,
678
880
  )
881
+ stake_in_netuids = {}
882
+ for stake_info in stake_info_list:
883
+ if stake_info.hotkey_ss58 not in stake_in_netuids:
884
+ stake_in_netuids[stake_info.hotkey_ss58] = {}
885
+ stake_in_netuids[stake_info.hotkey_ss58][stake_info.netuid] = (
886
+ stake_info.stake
887
+ )
679
888
 
680
- old_balance, old_stakes, hotkey_owners, threshold = await asyncio.gather(
681
- old_balance_,
682
- old_stakes_,
683
- hotkey_owners_,
684
- _get_threshold_amount(subtensor, block_hash),
889
+ # Flag to check if user wants to quit
890
+ skip_remaining_subnets = False
891
+ if hotkeys_to_unstake_from:
892
+ console.print(
893
+ "[dark_sea_green3]Tip: Enter 'q' any time to skip further entries and process existing unstakes"
685
894
  )
686
- own_hotkeys = [
687
- wallet.coldkeypub.ss58_address == hotkey_owner
688
- for hotkey_owner in hotkey_owners
689
- ]
690
-
691
- successful_unstakes = 0
692
- for idx, (hotkey_ss58, amount, old_stake, own_hotkey) in enumerate(
693
- zip(hotkey_ss58s, new_amounts, old_stakes, own_hotkeys)
694
- ):
695
- # Covert to bittensor.Balance
696
- if amount is None:
697
- # Unstake it all.
698
- unstaking_balance = old_stake
895
+
896
+ # Iterate over hotkeys and netuids to collect unstake operations
897
+ unstake_all_hk_ss58 = None
898
+ for hotkey in hotkeys_to_unstake_from:
899
+ if skip_remaining_subnets:
900
+ break
901
+
902
+ if interactive:
903
+ staking_address_name, staking_address_ss58, netuid = hotkey
904
+ netuids_to_process = [netuid]
699
905
  else:
700
- unstaking_balance = amount
906
+ staking_address_name, staking_address_ss58 = hotkey
907
+ netuids_to_process = netuids
701
908
 
702
- # Check enough to unstake.
703
- stake_on_uid = old_stake
704
- if unstaking_balance > stake_on_uid:
705
- err_console.print(
706
- f":cross_mark: [red]Not enough stake[/red]:"
707
- f" [green]{stake_on_uid}[/green] to unstake:"
708
- f" [blue]{unstaking_balance}[/blue] from hotkey:"
709
- f" [white]{wallet.hotkey_str}[/white]"
909
+ initial_amount = amount
910
+
911
+ if len(netuids_to_process) > 1:
912
+ console.print(
913
+ "[dark_sea_green3]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes.\n"
710
914
  )
711
- continue
712
915
 
713
- # If nomination stake, check threshold.
714
- if (
715
- not own_hotkey
716
- and (
717
- await _check_threshold_amount(
718
- subtensor=subtensor,
719
- sb=(stake_on_uid - unstaking_balance),
720
- block_hash=block_hash,
721
- min_req_stake=threshold,
916
+ for netuid in netuids_to_process:
917
+ if skip_remaining_subnets:
918
+ break # Exit the loop over netuids
919
+
920
+ dynamic_info = all_sn_dynamic_info.get(netuid)
921
+ current_stake_balance = stake_in_netuids[staking_address_ss58][netuid]
922
+ if current_stake_balance.tao == 0:
923
+ continue # No stake to unstake
924
+
925
+ # Determine the amount we are unstaking.
926
+ if unstake_all_from_hk or unstake_all:
927
+ amount_to_unstake_as_balance = current_stake_balance
928
+ unstake_all_hk_ss58 = staking_address_ss58
929
+ elif initial_amount:
930
+ amount_to_unstake_as_balance = Balance.from_tao(initial_amount)
931
+ else:
932
+ amount_to_unstake_as_balance = ask_unstake_amount(
933
+ current_stake_balance,
934
+ netuid,
935
+ staking_address_name
936
+ if staking_address_name
937
+ else staking_address_ss58,
938
+ staking_address_ss58,
939
+ interactive,
722
940
  )
723
- )[0]
724
- is False
725
- ):
726
- console.print(
727
- ":warning: [yellow]This action will unstake the entire staked balance![/yellow]"
941
+ if amount_to_unstake_as_balance is None:
942
+ skip_remaining_subnets = True
943
+ break
944
+
945
+ # Check enough stake to remove.
946
+ amount_to_unstake_as_balance.set_unit(netuid)
947
+ if amount_to_unstake_as_balance > current_stake_balance:
948
+ err_console.print(
949
+ f"[red]Not enough stake to remove[/red]:\n Stake balance: [dark_orange]{current_stake_balance}[/dark_orange]"
950
+ f" < Unstaking amount: [dark_orange]{amount_to_unstake_as_balance}[/dark_orange]"
951
+ )
952
+ continue # Skip to the next subnet - useful when single amount is specified for all subnets
953
+
954
+ received_amount, _, slippage_pct_float = (
955
+ dynamic_info.alpha_to_tao_with_slippage(amount_to_unstake_as_balance)
728
956
  )
729
- unstaking_balance = stake_on_uid
730
-
731
- # Ask before moving on.
732
- if prompt:
733
- if not Confirm.ask(
734
- f"Do you want to unstake:\n"
735
- f"[bold white]\tamount: {unstaking_balance}\n"
736
- f"ss58: {hotkey_ss58}[/bold white ]?"
737
- ):
738
- continue
957
+ total_received_amount += received_amount
958
+ if dynamic_info.is_dynamic:
959
+ slippage_pct = f"{slippage_pct_float:.4f} %"
960
+ else:
961
+ slippage_pct_float = 0
962
+ slippage_pct = "[red]N/A[/red]"
963
+ max_float_slippage = max(max_float_slippage, slippage_pct_float)
739
964
 
740
- with console.status(
741
- f":satellite: Unstaking from chain: [white]{subtensor}[/white] ..."
742
- ):
965
+ unstake_operations.append(
966
+ {
967
+ "netuid": netuid,
968
+ "hotkey_name": staking_address_name
969
+ if staking_address_name
970
+ else staking_address_ss58,
971
+ "hotkey_ss58": staking_address_ss58,
972
+ "amount_to_unstake": amount_to_unstake_as_balance,
973
+ "current_stake_balance": current_stake_balance,
974
+ "received_amount": received_amount,
975
+ "slippage_pct": slippage_pct,
976
+ "slippage_pct_float": slippage_pct_float,
977
+ "dynamic_info": dynamic_info,
978
+ }
979
+ )
980
+
981
+ if not unstake_operations:
982
+ console.print("[red]No unstake operations to perform.[/red]")
983
+ return False
984
+
985
+ # Build the table
986
+ table = Table(
987
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking to: \nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}],"
988
+ f" Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n"
989
+ f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n",
990
+ show_footer=True,
991
+ show_edge=False,
992
+ header_style="bold white",
993
+ border_style="bright_black",
994
+ style="bold",
995
+ title_justify="center",
996
+ show_lines=False,
997
+ pad_edge=True,
998
+ )
999
+ table.add_column("Netuid", justify="center", style="grey89")
1000
+ table.add_column(
1001
+ "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
1002
+ )
1003
+ table.add_column(
1004
+ f"Amount ({Balance.get_unit(1)})",
1005
+ justify="center",
1006
+ style=COLOR_PALETTE["POOLS"]["TAO"],
1007
+ )
1008
+ table.add_column(
1009
+ f"Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})",
1010
+ justify="center",
1011
+ style=COLOR_PALETTE["POOLS"]["RATE"],
1012
+ )
1013
+ table.add_column(
1014
+ f"Received ({Balance.get_unit(0)})",
1015
+ justify="center",
1016
+ style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"],
1017
+ footer=f"{total_received_amount}",
1018
+ )
1019
+ table.add_column(
1020
+ "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"]
1021
+ )
1022
+
1023
+ for op in unstake_operations:
1024
+ dynamic_info = op["dynamic_info"]
1025
+ table.add_row(
1026
+ str(op["netuid"]),
1027
+ op["hotkey_name"],
1028
+ str(op["amount_to_unstake"]),
1029
+ str(float(dynamic_info.price))
1030
+ + f"({Balance.get_unit(0)}/{Balance.get_unit(op['netuid'])})",
1031
+ str(op["received_amount"]),
1032
+ op["slippage_pct"],
1033
+ )
1034
+
1035
+ console.print(table)
1036
+
1037
+ if max_float_slippage > 5:
1038
+ console.print(
1039
+ "\n"
1040
+ f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n"
1041
+ 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']}],"
1042
+ " this may result in a loss of funds.\n"
1043
+ f"-------------------------------------------------------------------------------------------------------------------\n"
1044
+ )
1045
+
1046
+ console.print(
1047
+ """
1048
+ [bold white]Description[/bold white]:
1049
+ The table displays information about the stake remove operation you are about to perform.
1050
+ The columns are as follows:
1051
+ - [bold white]Netuid[/bold white]: The netuid of the subnet you are unstaking from.
1052
+ - [bold white]Hotkey[/bold white]: The ss58 address or identity of the hotkey you are unstaking from.
1053
+ - [bold white]Amount[/bold white]: The stake amount you are removing from this key.
1054
+ - [bold white]Rate[/bold white]: The rate of exchange between TAO and the subnet's stake.
1055
+ - [bold white]Received[/bold white]: The amount of free balance TAO you will receive on this subnet after slippage.
1056
+ - [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root).
1057
+ """
1058
+ )
1059
+ if prompt:
1060
+ if not Confirm.ask("Would you like to continue?"):
1061
+ raise typer.Exit()
1062
+
1063
+ # Perform unstaking operations
1064
+ try:
1065
+ wallet.unlock_coldkey()
1066
+ except KeyFileError:
1067
+ err_console.print("Error decrypting coldkey (possibly incorrect password)")
1068
+ return False
1069
+
1070
+ with console.status("\n:satellite: Performing unstaking operations...") as status:
1071
+ if unstake_all_from_hk:
743
1072
  call = await subtensor.substrate.compose_call(
744
1073
  call_module="SubtensorModule",
745
- call_function="remove_stake",
1074
+ call_function="unstake_all",
746
1075
  call_params={
747
- "hotkey": hotkey_ss58,
748
- "amount_unstaked": unstaking_balance.rao,
1076
+ "hotkey": unstake_all_hk_ss58,
749
1077
  },
750
1078
  )
751
- staking_response, err_msg = await subtensor.sign_and_send_extrinsic(
752
- call, wallet, wait_for_inclusion, wait_for_finalization
1079
+ extrinsic = await subtensor.substrate.create_signed_extrinsic(
1080
+ call=call, keypair=wallet.coldkey
753
1081
  )
754
-
755
- if staking_response is True: # If we successfully unstaked.
756
- # We only wait here if we expect finalization.
757
-
758
- if idx < len(hotkey_ss58s) - 1:
759
- # Wait for tx rate limit.
760
- tx_query = await subtensor.substrate.query(
761
- module="SubtensorModule",
762
- storage_function="TxRateLimit",
763
- block_hash=block_hash,
1082
+ response = await subtensor.substrate.submit_extrinsic(
1083
+ extrinsic, wait_for_inclusion=True, wait_for_finalization=False
1084
+ )
1085
+ await response.process_events()
1086
+ if not await response.is_success:
1087
+ print_error(
1088
+ f":cross_mark: [red]Failed[/red] with error: "
1089
+ f"{format_error_message(await response.error_message, subtensor.substrate)}",
1090
+ status,
764
1091
  )
765
- tx_rate_limit_blocks: int = tx_query
766
-
767
- # TODO: Handle in-case we have fast blocks
768
- if tx_rate_limit_blocks > 0:
769
- console.print(
770
- ":hourglass: [yellow]Waiting for tx rate limit:"
771
- f" [white]{tx_rate_limit_blocks}[/white] blocks,"
772
- f" estimated time: [white]{tx_rate_limit_blocks * 12} [/white] seconds[/yellow]"
773
- )
774
- await asyncio.sleep(
775
- tx_rate_limit_blocks * 12
776
- ) # 12 seconds per block
777
-
778
- if not wait_for_finalization and not wait_for_inclusion:
779
- successful_unstakes += 1
780
- continue
781
-
782
- console.print(":white_heavy_check_mark: [green]Finalized[/green]")
783
- with console.status(
784
- f":satellite: Checking stake balance on: [white]{subtensor}[/white] ..."
785
- ):
786
- new_stake = await subtensor.get_stake_for_coldkey_and_hotkey(
787
- coldkey_ss58=wallet.coldkeypub.ss58_address,
788
- hotkey_ss58=hotkey_ss58,
789
- block_hash=(await subtensor.substrate.get_chain_head()),
1092
+ else:
1093
+ new_balance = await subtensor.get_balance(
1094
+ wallet.coldkeypub.ss58_address
790
1095
  )
1096
+ console.print(":white_heavy_check_mark: [green]Finalized[/green]")
791
1097
  console.print(
792
- "Stake ({}): [blue]{}[/blue] :arrow_right: [green]{}[/green]".format(
793
- hotkey_ss58, stake_on_uid, new_stake
794
- )
1098
+ f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
795
1099
  )
796
- successful_unstakes += 1
797
1100
  else:
798
- err_console.print(":cross_mark: [red]Failed[/red]: Unknown Error.")
799
- continue
800
-
801
- if successful_unstakes != 0:
802
- with console.status(
803
- f":satellite: Checking balance on: ([white]{subtensor}[/white] ..."
804
- ):
805
- new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address)
806
- console.print(
807
- f"Balance: [blue]{old_balance[wallet.coldkeypub.ss58_address]}[/blue]"
808
- f" :arrow_right: [green]{new_balance[wallet.coldkeypub.ss58_address]}[/green]"
809
- )
810
- return True
811
-
812
- return False
813
-
1101
+ for op in unstake_operations:
1102
+ netuid_i = op["netuid"]
1103
+ staking_address_name = op["hotkey_name"]
1104
+ staking_address_ss58 = op["hotkey_ss58"]
1105
+ amount = op["amount_to_unstake"]
1106
+ current_stake_balance = op["current_stake_balance"]
1107
+
1108
+ status.update(
1109
+ f"\n:satellite: Unstaking {amount} from {staking_address_name} on netuid: {netuid_i} ..."
1110
+ )
814
1111
 
815
- # Commands
1112
+ call = await subtensor.substrate.compose_call(
1113
+ call_module="SubtensorModule",
1114
+ call_function="remove_stake",
1115
+ call_params={
1116
+ "hotkey": staking_address_ss58,
1117
+ "netuid": netuid_i,
1118
+ "amount_unstaked": amount.rao,
1119
+ },
1120
+ )
1121
+ extrinsic = await subtensor.substrate.create_signed_extrinsic(
1122
+ call=call, keypair=wallet.coldkey
1123
+ )
1124
+ response = await subtensor.substrate.submit_extrinsic(
1125
+ extrinsic, wait_for_inclusion=True, wait_for_finalization=False
1126
+ )
1127
+ await response.process_events()
1128
+ if not await response.is_success:
1129
+ print_error(
1130
+ f":cross_mark: [red]Failed[/red] with error: "
1131
+ f"{format_error_message(await response.error_message, subtensor.substrate)}",
1132
+ status,
1133
+ )
1134
+ else:
1135
+ new_balance = await subtensor.get_balance(
1136
+ wallet.coldkeypub.ss58_address
1137
+ )
1138
+ new_stake_info = await subtensor.get_stake_for_coldkey(
1139
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
1140
+ )
1141
+ new_stake = Balance.from_rao(0)
1142
+ for stake_info in new_stake_info:
1143
+ if (
1144
+ stake_info.hotkey_ss58 == staking_address_ss58
1145
+ and stake_info.netuid == netuid_i
1146
+ ):
1147
+ new_stake = stake_info.stake.set_unit(netuid_i)
1148
+ break
1149
+ console.print(":white_heavy_check_mark: [green]Finalized[/green]")
1150
+ console.print(
1151
+ f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
1152
+ )
1153
+ console.print(
1154
+ f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]"
1155
+ f" Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}"
1156
+ )
1157
+ console.print(
1158
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed."
1159
+ )
816
1160
 
817
1161
 
818
- async def show(
1162
+ async def stake_list(
819
1163
  wallet: Wallet,
820
- subtensor: Optional["SubtensorInterface"],
821
- all_wallets: bool,
822
- reuse_last: bool,
823
- html_output: bool,
824
- no_cache: bool,
1164
+ coldkey_ss58: str,
1165
+ subtensor: "SubtensorInterface",
1166
+ live: bool = False,
1167
+ verbose: bool = False,
1168
+ prompt: bool = False,
825
1169
  ):
826
- """Show all stake accounts."""
1170
+ coldkey_address = coldkey_ss58 if coldkey_ss58 else wallet.coldkeypub.ss58_address
1171
+
1172
+ async def get_stake_data(block_hash: str = None):
1173
+ (
1174
+ sub_stakes,
1175
+ registered_delegate_info,
1176
+ _dynamic_info,
1177
+ ) = await asyncio.gather(
1178
+ subtensor.get_stake_for_coldkey(
1179
+ coldkey_ss58=coldkey_address, block_hash=block_hash
1180
+ ),
1181
+ subtensor.get_delegate_identities(block_hash=block_hash),
1182
+ subtensor.all_subnets(),
1183
+ )
1184
+ # sub_stakes = substakes[coldkey_address]
1185
+ dynamic_info = {info.netuid: info for info in _dynamic_info}
1186
+ return (
1187
+ sub_stakes,
1188
+ registered_delegate_info,
1189
+ dynamic_info,
1190
+ )
827
1191
 
828
- async def get_stake_accounts(
829
- wallet_, block_hash: str
830
- ) -> dict[str, Union[str, Balance, dict[str, Union[str, Balance]]]]:
831
- """Get stake account details for the given wallet.
1192
+ def define_table(
1193
+ hotkey_name: str,
1194
+ rows: list[list[str]],
1195
+ total_tao_ownership: Balance,
1196
+ total_tao_value: Balance,
1197
+ total_swapped_tao_value: Balance,
1198
+ live: bool = False,
1199
+ ):
1200
+ title = f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkey: {hotkey_name}\nNetwork: {subtensor.network}\n\n"
1201
+ # TODO: Add hint back in after adding columns descriptions
1202
+ # if not live:
1203
+ # title += f"[{COLOR_PALETTE['GENERAL']['HINT']}]See below for an explanation of the columns\n"
1204
+ table = Table(
1205
+ title=title,
1206
+ show_footer=True,
1207
+ show_edge=False,
1208
+ header_style="bold white",
1209
+ border_style="bright_black",
1210
+ style="bold",
1211
+ title_justify="center",
1212
+ show_lines=False,
1213
+ pad_edge=True,
1214
+ )
1215
+ table.add_column(
1216
+ "[white]Netuid",
1217
+ footer=f"{len(rows)}",
1218
+ footer_style="overline white",
1219
+ style="grey89",
1220
+ )
1221
+ table.add_column(
1222
+ "[white]Name",
1223
+ style="cyan",
1224
+ justify="left",
1225
+ no_wrap=True,
1226
+ )
1227
+ table.add_column(
1228
+ f"[white]Value \n({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})",
1229
+ footer_style="overline white",
1230
+ style=COLOR_PALETTE["STAKE"]["TAO"],
1231
+ justify="right",
1232
+ footer=f"τ {millify_tao(total_tao_value.tao)}"
1233
+ if not verbose
1234
+ else f"{total_tao_value}",
1235
+ )
1236
+ table.add_column(
1237
+ f"[white]Stake ({Balance.get_unit(1)})",
1238
+ footer_style="overline white",
1239
+ style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"],
1240
+ justify="center",
1241
+ )
1242
+ table.add_column(
1243
+ f"[white]Price \n({Balance.unit}_in/{Balance.get_unit(1)}_in)",
1244
+ footer_style="white",
1245
+ style=COLOR_PALETTE["POOLS"]["RATE"],
1246
+ justify="center",
1247
+ )
1248
+ table.add_column(
1249
+ f"[white]Swap ({Balance.get_unit(1)} -> {Balance.unit})",
1250
+ footer_style="overline white",
1251
+ style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"],
1252
+ justify="right",
1253
+ footer=f"τ {millify_tao(total_swapped_tao_value.tao)}"
1254
+ if not verbose
1255
+ else f"{total_swapped_tao_value}",
1256
+ )
1257
+ table.add_column(
1258
+ "[white]Registered",
1259
+ style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"],
1260
+ justify="right",
1261
+ )
1262
+ table.add_column(
1263
+ f"[white]Emission \n({Balance.get_unit(1)}/block)",
1264
+ style=COLOR_PALETTE["POOLS"]["EMISSION"],
1265
+ justify="right",
1266
+ )
1267
+ return table
832
1268
 
833
- :param wallet_: The wallet object to fetch the stake account details for.
1269
+ def create_table(hotkey_: str, substakes: list[StakeInfo]):
1270
+ name = (
1271
+ f"{registered_delegate_info[hotkey_].display} ({hotkey_})"
1272
+ if hotkey_ in registered_delegate_info
1273
+ else hotkey_
1274
+ )
1275
+ rows = []
1276
+ total_tao_ownership = Balance(0)
1277
+ total_tao_value = Balance(0)
1278
+ total_swapped_tao_value = Balance(0)
1279
+ root_stakes = [s for s in substakes if s.netuid == 0]
1280
+ other_stakes = sorted(
1281
+ [s for s in substakes if s.netuid != 0],
1282
+ key=lambda x: dynamic_info[x.netuid]
1283
+ .alpha_to_tao(Balance.from_rao(int(x.stake.rao)).set_unit(x.netuid))
1284
+ .tao,
1285
+ reverse=True,
1286
+ )
1287
+ sorted_substakes = root_stakes + other_stakes
1288
+ for substake_ in sorted_substakes:
1289
+ netuid = substake_.netuid
1290
+ pool = dynamic_info[netuid]
1291
+ symbol = f"{Balance.get_unit(netuid)}\u200e"
1292
+ # TODO: what is this price var for?
1293
+ price = (
1294
+ "{:.4f}{}".format(
1295
+ pool.price.__float__(), f" τ/{Balance.get_unit(netuid)}\u200e"
1296
+ )
1297
+ if pool.is_dynamic
1298
+ else (f" 1.0000 τ/{symbol} ")
1299
+ )
834
1300
 
835
- :return: A dictionary mapping SS58 addresses to their respective stake account details.
836
- """
1301
+ # Alpha value cell
1302
+ alpha_value = Balance.from_rao(int(substake_.stake.rao)).set_unit(netuid)
837
1303
 
838
- wallet_stake_accounts = {}
1304
+ # TAO value cell
1305
+ tao_value = pool.alpha_to_tao(alpha_value)
1306
+ total_tao_value += tao_value
839
1307
 
840
- # Get this wallet's coldkey balance.
841
- cold_balance_, stakes_from_hk, stakes_from_d = await asyncio.gather(
842
- subtensor.get_balance(
843
- wallet_.coldkeypub.ss58_address, block_hash=block_hash
844
- ),
845
- get_stakes_from_hotkeys(wallet_, block_hash=block_hash),
846
- get_stakes_from_delegates(wallet_),
847
- )
1308
+ # Swapped TAO value and slippage cell
1309
+ swapped_tao_value, _, slippage_percentage_ = (
1310
+ pool.alpha_to_tao_with_slippage(substake_.stake)
1311
+ )
1312
+ total_swapped_tao_value += swapped_tao_value
848
1313
 
849
- cold_balance = cold_balance_[wallet_.coldkeypub.ss58_address]
1314
+ # Slippage percentage cell
1315
+ if pool.is_dynamic:
1316
+ slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_percentage_:.3f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]"
1317
+ else:
1318
+ slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]0.000%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]"
850
1319
 
851
- # Populate the stake accounts with local hotkeys data.
852
- wallet_stake_accounts.update(stakes_from_hk)
1320
+ if netuid == 0:
1321
+ swap_value = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}] ({slippage_percentage})"
1322
+ else:
1323
+ swap_value = (
1324
+ f"τ {millify_tao(swapped_tao_value.tao)} ({slippage_percentage})"
1325
+ if not verbose
1326
+ else f"{swapped_tao_value} ({slippage_percentage})"
1327
+ )
853
1328
 
854
- # Populate the stake accounts with delegations data.
855
- wallet_stake_accounts.update(stakes_from_d)
1329
+ # TAO locked cell
1330
+ tao_locked = pool.tao_in
856
1331
 
857
- return {
858
- "name": wallet_.name,
859
- "balance": cold_balance,
860
- "accounts": wallet_stake_accounts,
861
- }
1332
+ # Issuance cell
1333
+ issuance = pool.alpha_out if pool.is_dynamic else tao_locked
862
1334
 
863
- async def get_stakes_from_hotkeys(
864
- wallet_, block_hash: str
865
- ) -> dict[str, dict[str, Union[str, Balance]]]:
866
- """Fetch stakes from hotkeys for the provided wallet.
1335
+ # Per block emission cell
1336
+ per_block_emission = substake_.emission.tao / pool.tempo
1337
+ # Alpha ownership and TAO ownership cells
1338
+ if alpha_value.tao > 0.00009:
1339
+ if issuance.tao != 0:
1340
+ # TODO figure out why this alpha_ownership does nothing
1341
+ alpha_ownership = "{:.4f}".format(
1342
+ (alpha_value.tao / issuance.tao) * 100
1343
+ )
1344
+ tao_ownership = Balance.from_tao(
1345
+ (alpha_value.tao / issuance.tao) * tao_locked.tao
1346
+ )
1347
+ total_tao_ownership += tao_ownership
1348
+ else:
1349
+ # TODO what's this var for?
1350
+ alpha_ownership = "0.0000"
1351
+ tao_ownership = Balance.from_tao(0)
1352
+
1353
+ stake_value = (
1354
+ millify_tao(substake_.stake.tao)
1355
+ if not verbose
1356
+ else f"{substake_.stake.tao:,.4f}"
1357
+ )
1358
+ subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {get_subnet_name(dynamic_info[netuid])}"
867
1359
 
868
- :param wallet_: The wallet object to fetch the stakes for.
1360
+ rows.append(
1361
+ [
1362
+ str(netuid), # Number
1363
+ subnet_name_cell, # Symbol + name
1364
+ f"τ {millify_tao(tao_value.tao)}"
1365
+ if not verbose
1366
+ else f"{tao_value}", # Value (α x τ/α)
1367
+ f"{stake_value} {symbol}"
1368
+ if netuid != 0
1369
+ else f"{symbol} {stake_value}", # Stake (a)
1370
+ f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a)
1371
+ # f"τ {millify_tao(tao_ownership.tao)}" if not verbose else f"{tao_ownership}", # TAO equiv
1372
+ swap_value, # Swap(α) -> τ
1373
+ "YES"
1374
+ if substake_.is_registered
1375
+ else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registered
1376
+ str(Balance.from_tao(per_block_emission).set_unit(netuid)),
1377
+ # Removing this flag for now, TODO: Confirm correct values are here w.r.t CHKs
1378
+ # if substake_.is_registered
1379
+ # else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A", # Emission(α/block)
1380
+ ]
1381
+ )
1382
+ table = define_table(
1383
+ name, rows, total_tao_ownership, total_tao_value, total_swapped_tao_value
1384
+ )
1385
+ for row in rows:
1386
+ table.add_row(*row)
1387
+ console.print(table)
1388
+ return total_tao_ownership, total_tao_value
1389
+
1390
+ def create_live_table(
1391
+ substakes: list,
1392
+ registered_delegate_info: dict,
1393
+ dynamic_info: dict,
1394
+ hotkey_name: str,
1395
+ previous_data: Optional[dict] = None,
1396
+ ) -> tuple[Table, dict, Balance, Balance, Balance]:
1397
+ rows = []
1398
+ current_data = {}
869
1399
 
870
- :return: A dictionary of stakes related to hotkeys.
871
- """
1400
+ total_tao_ownership = Balance(0)
1401
+ total_tao_value = Balance(0)
1402
+ total_swapped_tao_value = Balance(0)
872
1403
 
873
- async def get_all_neurons_for_pubkey(hk):
874
- netuids = await subtensor.get_netuids_for_hotkey(hk, block_hash=block_hash)
875
- uid_query = await asyncio.gather(
876
- *[
877
- subtensor.substrate.query(
878
- module="SubtensorModule",
879
- storage_function="Uids",
880
- params=[netuid, hk],
881
- block_hash=block_hash,
1404
+ def format_cell(
1405
+ value, previous_value, unit="", unit_first=False, precision=4, millify=False
1406
+ ):
1407
+ if previous_value is not None:
1408
+ change = value - previous_value
1409
+ if abs(change) > 10 ** (-precision):
1410
+ formatted_change = (
1411
+ f"{change:.{precision}f}"
1412
+ if not millify
1413
+ else f"{millify_tao(change)}"
882
1414
  )
883
- for netuid in netuids
884
- ]
885
- )
886
- uids = [_result for _result in uid_query]
887
- neurons = await asyncio.gather(
888
- *[
889
- subtensor.neuron_for_uid(uid, net)
890
- for (uid, net) in zip(uids, netuids)
891
- ]
1415
+ change_text = (
1416
+ f" [pale_green3](+{formatted_change})[/pale_green3]"
1417
+ if change > 0
1418
+ else f" [hot_pink3]({formatted_change})[/hot_pink3]"
1419
+ )
1420
+ else:
1421
+ change_text = ""
1422
+ else:
1423
+ change_text = ""
1424
+ formatted_value = (
1425
+ f"{value:,.{precision}f}" if not millify else f"{millify_tao(value)}"
892
1426
  )
893
- return neurons
894
-
895
- async def get_emissions_and_stake(hk: str):
896
- neurons, stake = await asyncio.gather(
897
- get_all_neurons_for_pubkey(hk),
898
- subtensor.substrate.query(
899
- module="SubtensorModule",
900
- storage_function="Stake",
901
- params=[hk, wallet_.coldkeypub.ss58_address],
902
- block_hash=block_hash,
903
- ),
1427
+ return (
1428
+ f"{formatted_value} {unit}{change_text}"
1429
+ if not unit_first
1430
+ else f"{unit} {formatted_value}{change_text}"
904
1431
  )
905
- emission_ = sum([n.emission for n in neurons]) if neurons else 0.0
906
- return emission_, Balance.from_rao(stake) if stake else Balance(0)
907
1432
 
908
- hotkeys = cast(list[Wallet], get_hotkey_wallets_for_wallet(wallet_))
909
- stakes = {}
910
- query = await asyncio.gather(
911
- *[get_emissions_and_stake(hot.hotkey.ss58_address) for hot in hotkeys]
1433
+ # Sort subnets by value
1434
+ root_stakes = [s for s in substakes if s.netuid == 0]
1435
+ other_stakes = sorted(
1436
+ [s for s in substakes if s.netuid != 0],
1437
+ key=lambda x: dynamic_info[x.netuid]
1438
+ .alpha_to_tao(Balance.from_rao(int(x.stake.rao)).set_unit(x.netuid))
1439
+ .tao,
1440
+ reverse=True,
912
1441
  )
913
- for hot, (emission, hotkey_stake) in zip(hotkeys, query):
914
- stakes[hot.hotkey.ss58_address] = {
915
- "name": hot.hotkey_str,
916
- "stake": hotkey_stake,
917
- "rate": emission,
918
- }
919
- return stakes
1442
+ sorted_substakes = root_stakes + other_stakes
920
1443
 
921
- async def get_stakes_from_delegates(
922
- wallet_,
923
- ) -> dict[str, dict[str, Union[str, Balance]]]:
924
- """Fetch stakes from delegates for the provided wallet.
925
-
926
- :param wallet_: The wallet object to fetch the stakes for.
1444
+ # Process each stake
1445
+ for substake in sorted_substakes:
1446
+ netuid = substake.netuid
1447
+ pool = dynamic_info.get(netuid)
1448
+ if substake.stake.rao == 0 or not pool:
1449
+ continue
927
1450
 
928
- :return: A dictionary of stakes related to delegates.
929
- """
930
- delegates = await subtensor.get_delegated(
931
- coldkey_ss58=wallet_.coldkeypub.ss58_address, block_hash=None
932
- )
933
- stakes = {}
934
- for dele, staked in delegates:
935
- for nom in dele.nominators:
936
- if nom[0] == wallet_.coldkeypub.ss58_address:
937
- delegate_name = (
938
- registered_delegate_info[dele.hotkey_ss58].display
939
- if dele.hotkey_ss58 in registered_delegate_info
940
- else None
941
- )
942
- stakes[dele.hotkey_ss58] = {
943
- "name": delegate_name if delegate_name else dele.hotkey_ss58,
944
- "stake": nom[1],
945
- "rate": dele.total_daily_return.tao
946
- * (nom[1] / dele.total_stake.tao),
947
- }
948
- return stakes
949
-
950
- async def get_all_wallet_accounts(
951
- block_hash: str,
952
- ) -> list[dict[str, Union[str, Balance, dict[str, Union[str, Balance]]]]]:
953
- """Fetch stake accounts for all provided wallets using a ThreadPool.
954
-
955
- :param block_hash: The block hash to fetch the stake accounts for.
956
-
957
- :return: A list of dictionaries, each dictionary containing stake account details for each wallet.
958
- """
1451
+ # Calculate base values
1452
+ symbol = f"{Balance.get_unit(netuid)}\u200e"
1453
+ alpha_value = Balance.from_rao(int(substake.stake.rao)).set_unit(netuid)
1454
+ tao_value = pool.alpha_to_tao(alpha_value)
1455
+ total_tao_value += tao_value
1456
+ swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage(
1457
+ substake.stake
1458
+ )
1459
+ total_swapped_tao_value += swapped_tao_value
1460
+
1461
+ # Calculate TAO ownership
1462
+ tao_locked = pool.tao_in
1463
+ issuance = pool.alpha_out if pool.is_dynamic else tao_locked
1464
+ if alpha_value.tao > 0.00009 and issuance.tao != 0:
1465
+ tao_ownership = Balance.from_tao(
1466
+ (alpha_value.tao / issuance.tao) * tao_locked.tao
1467
+ )
1468
+ total_tao_ownership += tao_ownership
1469
+ else:
1470
+ tao_ownership = Balance.from_tao(0)
1471
+
1472
+ # Store current values for future delta tracking
1473
+ current_data[netuid] = {
1474
+ "stake": alpha_value.tao,
1475
+ "price": pool.price.tao,
1476
+ "tao_value": tao_value.tao,
1477
+ "swapped_value": swapped_tao_value.tao,
1478
+ "emission": substake.emission.tao / pool.tempo,
1479
+ "tao_ownership": tao_ownership.tao,
1480
+ }
959
1481
 
960
- accounts_ = await asyncio.gather(
961
- *[get_stake_accounts(w, block_hash=block_hash) for w in wallets]
962
- )
963
- return accounts_
964
-
965
- if not reuse_last:
966
- cast("SubtensorInterface", subtensor)
967
- if all_wallets:
968
- wallets = get_coldkey_wallets_for_path(wallet.path)
969
- valid_wallets, invalid_wallets = validate_coldkey_presence(wallets)
970
- wallets = valid_wallets
971
- for invalid_wallet in invalid_wallets:
972
- print_error(f"No coldkeypub found for wallet: ({invalid_wallet.name})")
973
- else:
974
- wallets = [wallet]
1482
+ # Get previous values for delta tracking
1483
+ prev = previous_data.get(netuid, {}) if previous_data else {}
1484
+ unit_first = True if netuid == 0 else False
1485
+
1486
+ stake_cell = format_cell(
1487
+ alpha_value.tao,
1488
+ prev.get("stake"),
1489
+ unit=symbol,
1490
+ unit_first=unit_first,
1491
+ precision=4,
1492
+ millify=True if not verbose else False,
1493
+ )
975
1494
 
976
- with console.status(
977
- ":satellite: Retrieving account data...", spinner="aesthetic"
978
- ):
979
- block_hash_ = await subtensor.substrate.get_chain_head()
980
- registered_delegate_info = await subtensor.get_delegate_identities(
981
- block_hash=block_hash_
1495
+ rate_cell = format_cell(
1496
+ pool.price.tao,
1497
+ prev.get("price"),
1498
+ unit=f"τ/{symbol}",
1499
+ unit_first=False,
1500
+ precision=5,
1501
+ millify=True if not verbose else False,
982
1502
  )
983
- accounts = await get_all_wallet_accounts(block_hash=block_hash_)
984
1503
 
985
- total_stake: float = 0.0
986
- total_balance: float = 0.0
987
- total_rate: float = 0.0
988
- rows = []
989
- db_rows = []
990
- for acc in accounts:
991
- cast(str, acc["name"])
992
- cast(Balance, acc["balance"])
993
- rows.append([acc["name"], str(acc["balance"]), "", "", ""])
994
- db_rows.append(
995
- [acc["name"], float(acc["balance"]), None, None, None, None, 0]
1504
+ exchange_cell = format_cell(
1505
+ tao_value.tao,
1506
+ prev.get("tao_value"),
1507
+ unit="τ",
1508
+ unit_first=True,
1509
+ precision=4,
1510
+ millify=True if not verbose else False,
996
1511
  )
997
- total_balance += cast(Balance, acc["balance"]).tao
998
- for key, value in cast(dict, acc["accounts"]).items():
999
- if value["name"] and value["name"] != key:
1000
- account_display_name = f"{value['name']}"
1001
- else:
1002
- account_display_name = "(~)"
1003
- rows.append(
1004
- [
1005
- "",
1006
- "",
1007
- account_display_name,
1008
- key,
1009
- str(value["stake"]),
1010
- str(value["rate"]),
1011
- ]
1512
+
1513
+ if pool.is_dynamic:
1514
+ slippage_pct = (
1515
+ 100 * float(slippage) / float(slippage + swapped_tao_value)
1516
+ if slippage + swapped_tao_value != 0
1517
+ else 0
1012
1518
  )
1013
- db_rows.append(
1014
- [
1015
- acc["name"],
1016
- None,
1017
- value["name"],
1018
- float(value["stake"]),
1019
- float(value["rate"]),
1020
- key,
1021
- 1,
1022
- ]
1519
+ else:
1520
+ slippage_pct = 0
1521
+
1522
+ if netuid != 0:
1523
+ swap_cell = (
1524
+ format_cell(
1525
+ swapped_tao_value.tao,
1526
+ prev.get("swapped_value"),
1527
+ unit="τ",
1528
+ unit_first=True,
1529
+ precision=4,
1530
+ millify=True if not verbose else False,
1531
+ )
1532
+ + f" ({slippage_pct:.2f}%)"
1023
1533
  )
1024
- total_stake += cast(Balance, value["stake"]).tao
1025
- total_rate += float(value["rate"])
1026
- metadata = {
1027
- "total_stake": "\u03c4{:.5f}".format(total_stake),
1028
- "total_balance": "\u03c4{:.5f}".format(total_balance),
1029
- "total_rate": "\u03c4{:.5f}/d".format(total_rate),
1030
- "rows": json.dumps(rows),
1031
- }
1032
- if not no_cache:
1033
- create_table(
1034
- "stakeshow",
1035
- [
1036
- ("COLDKEY", "TEXT"),
1037
- ("BALANCE", "REAL"),
1038
- ("ACCOUNT", "TEXT"),
1039
- ("STAKE", "REAL"),
1040
- ("RATE", "REAL"),
1041
- ("HOTKEY", "TEXT"),
1042
- ("CHILD", "INTEGER"),
1043
- ],
1044
- db_rows,
1534
+ else:
1535
+ swap_cell = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}] ({slippage_pct}%)"
1536
+
1537
+ emission_value = substake.emission.tao / pool.tempo
1538
+ emission_cell = format_cell(
1539
+ emission_value,
1540
+ prev.get("emission"),
1541
+ unit=symbol,
1542
+ unit_first=unit_first,
1543
+ precision=4,
1045
1544
  )
1046
- update_metadata_table("stakeshow", metadata)
1047
- else:
1048
- try:
1049
- metadata = get_metadata_table("stakeshow")
1050
- rows = json.loads(metadata["rows"])
1051
- except sqlite3.OperationalError:
1052
- err_console.print(
1053
- "[red]Error[/red] Unable to retrieve table data. This is usually caused by attempting to use "
1054
- "`--reuse-last` before running the command a first time. In rare cases, this could also be due to "
1055
- "a corrupted database. Re-run the command (do not use `--reuse-last`) and see if that resolves your "
1056
- "issue."
1545
+ subnet_name_cell = (
1546
+ f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]"
1547
+ f" {get_subnet_name(dynamic_info[netuid])}"
1057
1548
  )
1058
- return
1059
- if not html_output:
1060
- table = Table(
1061
- Column("[bold white]Coldkey", style="dark_orange", ratio=1),
1062
- Column(
1063
- "[bold white]Balance",
1064
- metadata["total_balance"],
1065
- style="dark_sea_green",
1066
- ratio=1,
1067
- ),
1068
- Column("[bold white]Account", style="bright_cyan", ratio=3),
1069
- Column("[bold white]Hotkey", ratio=7, no_wrap=True, style="bright_magenta"),
1070
- Column(
1071
- "[bold white]Stake",
1072
- metadata["total_stake"],
1073
- style="light_goldenrod2",
1074
- ratio=1,
1075
- ),
1076
- Column(
1077
- "[bold white]Rate /d",
1078
- metadata["total_rate"],
1079
- style="rgb(42,161,152)",
1080
- ratio=1,
1081
- ),
1082
- title=f"[underline dark_orange]Stake Show[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n",
1083
- show_footer=True,
1084
- show_edge=False,
1085
- expand=False,
1086
- border_style="bright_black",
1549
+
1550
+ rows.append(
1551
+ [
1552
+ str(netuid), # Netuid
1553
+ subnet_name_cell,
1554
+ exchange_cell, # Exchange value
1555
+ stake_cell, # Stake amount
1556
+ rate_cell, # Rate
1557
+ swap_cell, # Swap value with slippage
1558
+ "YES"
1559
+ if substake.is_registered
1560
+ else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registration status
1561
+ emission_cell, # Emission rate
1562
+ ]
1563
+ )
1564
+
1565
+ table = define_table(
1566
+ hotkey_name,
1567
+ rows,
1568
+ total_tao_ownership,
1569
+ total_tao_value,
1570
+ total_swapped_tao_value,
1571
+ live=True,
1087
1572
  )
1088
1573
 
1089
- for i, row in enumerate(rows):
1090
- is_last_row = i + 1 == len(rows)
1574
+ for row in rows:
1091
1575
  table.add_row(*row)
1092
1576
 
1093
- # If last row or new coldkey starting next
1094
- if is_last_row or (rows[i + 1][0] != ""):
1095
- table.add_row(end_section=True)
1096
- console.print(table)
1577
+ return table, current_data
1097
1578
 
1098
- else:
1099
- render_tree(
1100
- "stakeshow",
1101
- f"Stakes | Total Balance: {metadata['total_balance']} - Total Stake: {metadata['total_stake']} "
1102
- f"Total Rate: {metadata['total_rate']}",
1103
- [
1104
- {"title": "Coldkey", "field": "COLDKEY"},
1105
- {
1106
- "title": "Balance",
1107
- "field": "BALANCE",
1108
- "formatter": "money",
1109
- "formatterParams": {"symbol": "τ", "precision": 5},
1110
- },
1111
- {
1112
- "title": "Account",
1113
- "field": "ACCOUNT",
1114
- "width": 425,
1115
- },
1116
- {
1117
- "title": "Stake",
1118
- "field": "STAKE",
1119
- "formatter": "money",
1120
- "formatterParams": {"symbol": "τ", "precision": 5},
1121
- },
1122
- {
1123
- "title": "Daily Rate",
1124
- "field": "RATE",
1125
- "formatter": "money",
1126
- "formatterParams": {"symbol": "τ", "precision": 5},
1127
- },
1128
- {
1129
- "title": "Hotkey",
1130
- "field": "HOTKEY",
1131
- "width": 425,
1132
- },
1133
- ],
1134
- 0,
1135
- )
1579
+ # Main execution
1580
+ (
1581
+ sub_stakes,
1582
+ registered_delegate_info,
1583
+ dynamic_info,
1584
+ ) = await get_stake_data()
1585
+ balance = await subtensor.get_balance(coldkey_address)
1136
1586
 
1587
+ # Iterate over substakes and aggregate them by hotkey.
1588
+ hotkeys_to_substakes: dict[str, list[StakeInfo]] = {}
1137
1589
 
1138
- async def stake_add(
1139
- wallet: Wallet,
1140
- subtensor: "SubtensorInterface",
1141
- amount: float,
1142
- stake_all: bool,
1143
- max_stake: float,
1144
- include_hotkeys: list[str],
1145
- exclude_hotkeys: list[str],
1146
- all_hotkeys: bool,
1147
- prompt: bool,
1148
- hotkey_ss58: Optional[str] = None,
1149
- ) -> None:
1150
- """Stake token of amount to hotkey(s)."""
1590
+ for substake in sub_stakes:
1591
+ hotkey = substake.hotkey_ss58
1592
+ if substake.stake.rao == 0:
1593
+ continue
1594
+ if hotkey not in hotkeys_to_substakes:
1595
+ hotkeys_to_substakes[hotkey] = []
1596
+ hotkeys_to_substakes[hotkey].append(substake)
1151
1597
 
1152
- async def is_hotkey_registered_any(hk: str, bh: str) -> bool:
1153
- return len(await subtensor.get_netuids_for_hotkey(hk, bh)) > 0
1598
+ if not hotkeys_to_substakes:
1599
+ print_error(f"No stakes found for coldkey ss58: ({coldkey_address})")
1600
+ raise typer.Exit()
1154
1601
 
1155
- # Get the hotkey_names (if any) and the hotkey_ss58s.
1156
- hotkeys_to_stake_to: list[tuple[Optional[str], str]] = []
1157
- if hotkey_ss58:
1158
- if not is_valid_ss58_address(hotkey_ss58):
1159
- print_error("The entered ss58 address is incorrect")
1160
- typer.Exit()
1161
-
1162
- # Stake to specific hotkey.
1163
- hotkeys_to_stake_to = [(None, hotkey_ss58)]
1164
- elif all_hotkeys:
1165
- # Stake to all hotkeys.
1166
- all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet)
1167
- # Get the hotkeys to exclude. (d)efault to no exclusions.
1168
- # Exclude hotkeys that are specified.
1169
- hotkeys_to_stake_to = [
1170
- (wallet.hotkey_str, wallet.hotkey.ss58_address)
1171
- for wallet in all_hotkeys_
1172
- if wallet.hotkey_str not in exclude_hotkeys
1173
- and wallet.hotkey.ss58_address not in exclude_hotkeys
1174
- ] # definitely wallets
1175
-
1176
- elif include_hotkeys:
1177
- print_verbose("Staking to only included hotkeys")
1178
- # Stake to specific hotkeys.
1179
- for hotkey_ss58_or_hotkey_name in include_hotkeys:
1180
- if is_valid_ss58_address(hotkey_ss58_or_hotkey_name):
1181
- # If the hotkey is a valid ss58 address, we add it to the list.
1182
- hotkeys_to_stake_to.append((None, hotkey_ss58_or_hotkey_name))
1183
- else:
1184
- # If the hotkey is not a valid ss58 address, we assume it is a hotkey name.
1185
- # We then get the hotkey from the wallet and add it to the list.
1186
- wallet_ = Wallet(
1187
- path=wallet.path,
1188
- name=wallet.name,
1189
- hotkey=hotkey_ss58_or_hotkey_name,
1190
- )
1191
- hotkeys_to_stake_to.append(
1192
- (wallet_.hotkey_str, wallet_.hotkey.ss58_address)
1602
+ if live:
1603
+ # Select one hokkey for live monitoring
1604
+ if len(hotkeys_to_substakes) > 1:
1605
+ console.print(
1606
+ "\n[bold]Multiple hotkeys found. Please select one for live monitoring:[/bold]"
1607
+ )
1608
+ for idx, hotkey in enumerate(hotkeys_to_substakes.keys()):
1609
+ name = (
1610
+ f"{registered_delegate_info[hotkey].display} ({hotkey})"
1611
+ if hotkey in registered_delegate_info
1612
+ else hotkey
1193
1613
  )
1194
- else:
1195
- # Only config.wallet.hotkey is specified.
1196
- # so we stake to that single hotkey.
1197
- print_verbose(
1198
- f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})"
1199
- )
1200
- assert wallet.hotkey is not None
1201
- hotkey_ss58_or_name = wallet.hotkey.ss58_address
1202
- hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)]
1614
+ console.print(f"[{idx}] [{COLOR_PALETTE['GENERAL']['HEADER']}]{name}")
1203
1615
 
1204
- try:
1205
- # Get coldkey balance
1206
- print_verbose("Fetching coldkey balance")
1207
- wallet_balance_: dict[str, Balance] = await subtensor.get_balance(
1208
- wallet.coldkeypub.ss58_address
1209
- )
1210
- block_hash = subtensor.substrate.last_block_hash
1211
- wallet_balance: Balance = wallet_balance_[wallet.coldkeypub.ss58_address]
1212
- old_balance = copy.copy(wallet_balance)
1213
- final_hotkeys: list[tuple[Optional[str], str]] = []
1214
- final_amounts: list[Union[float, Balance]] = []
1215
- hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58)
1216
-
1217
- print_verbose("Checking if hotkeys are registered")
1218
- registered_ = asyncio.gather(
1219
- *[is_hotkey_registered_any(h[1], block_hash) for h in hotkeys_to_stake_to]
1220
- )
1221
- if max_stake:
1222
- hotkey_stakes_ = asyncio.gather(
1223
- *[
1224
- subtensor.get_stake_for_coldkey_and_hotkey(
1225
- hotkey_ss58=h[1],
1226
- coldkey_ss58=wallet.coldkeypub.ss58_address,
1227
- block_hash=block_hash,
1228
- )
1229
- for h in hotkeys_to_stake_to
1230
- ]
1616
+ selected_idx = Prompt.ask(
1617
+ "Enter hotkey index",
1618
+ choices=[str(i) for i in range(len(hotkeys_to_substakes))],
1231
1619
  )
1620
+ selected_hotkey = list(hotkeys_to_substakes.keys())[int(selected_idx)]
1621
+ selected_stakes = hotkeys_to_substakes[selected_hotkey]
1232
1622
  else:
1623
+ selected_hotkey = list(hotkeys_to_substakes.keys())[0]
1624
+ selected_stakes = hotkeys_to_substakes[selected_hotkey]
1233
1625
 
1234
- async def null():
1235
- return [None] * len(hotkeys_to_stake_to)
1626
+ hotkey_name = (
1627
+ f"{registered_delegate_info[selected_hotkey].display} ({selected_hotkey})"
1628
+ if selected_hotkey in registered_delegate_info
1629
+ else selected_hotkey
1630
+ )
1236
1631
 
1237
- hotkey_stakes_ = null()
1238
- registered: list[bool]
1239
- hotkey_stakes: list[Optional[Balance]]
1240
- registered, hotkey_stakes = await asyncio.gather(registered_, hotkey_stakes_)
1632
+ refresh_interval = 10 # seconds
1633
+ progress = Progress(
1634
+ TextColumn("[progress.description]{task.description}"),
1635
+ BarColumn(bar_width=20),
1636
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
1637
+ console=console,
1638
+ )
1639
+ progress_task = progress.add_task("Updating: ", total=refresh_interval)
1640
+
1641
+ previous_block = None
1642
+ current_block = None
1643
+ previous_data = None
1644
+
1645
+ with Live(console=console, screen=True, auto_refresh=True) as live:
1646
+ try:
1647
+ while True:
1648
+ block_hash = await subtensor.substrate.get_chain_head()
1649
+ (
1650
+ sub_stakes,
1651
+ registered_delegate_info,
1652
+ dynamic_info_,
1653
+ ) = await get_stake_data(block_hash)
1654
+ selected_stakes = [
1655
+ stake
1656
+ for stake in sub_stakes
1657
+ if stake.hotkey_ss58 == selected_hotkey
1658
+ ]
1241
1659
 
1242
- for hotkey, reg, hotkey_stake in zip(
1243
- hotkeys_to_stake_to, registered, hotkey_stakes
1244
- ):
1245
- if not reg:
1246
- # Hotkey is not registered.
1247
- if len(hotkeys_to_stake_to) == 1:
1248
- # Only one hotkey, error
1249
- err_console.print(
1250
- f"[red]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Aborting.[/red]"
1660
+ block_number = await subtensor.substrate.get_block_number(None)
1661
+
1662
+ previous_block = current_block
1663
+ current_block = block_number
1664
+ new_blocks = (
1665
+ "N/A"
1666
+ if previous_block is None
1667
+ else str(current_block - previous_block)
1251
1668
  )
1252
- raise ValueError
1253
- else:
1254
- # Otherwise, print warning and skip
1255
- console.print(
1256
- f"[yellow]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Skipping.[/yellow]"
1669
+
1670
+ table, current_data = create_live_table(
1671
+ selected_stakes,
1672
+ registered_delegate_info,
1673
+ dynamic_info,
1674
+ hotkey_name,
1675
+ previous_data,
1257
1676
  )
1258
- continue
1259
-
1260
- stake_amount_tao: float = amount
1261
- if max_stake:
1262
- stake_amount_tao = max_stake - hotkey_stake.tao
1263
-
1264
- # If the max_stake is greater than the current wallet balance, stake the entire balance.
1265
- stake_amount_tao = min(stake_amount_tao, wallet_balance.tao)
1266
- if (
1267
- stake_amount_tao <= 0.00001
1268
- ): # Threshold because of fees, might create a loop otherwise
1269
- # Skip hotkey if max_stake is less than current stake.
1270
- continue
1271
- wallet_balance = Balance.from_tao(wallet_balance.tao - stake_amount_tao)
1272
-
1273
- if wallet_balance.tao < 0:
1274
- # No more balance to stake.
1275
- break
1276
1677
 
1277
- final_amounts.append(stake_amount_tao)
1278
- final_hotkeys.append(hotkey) # add both the name and the ss58 address.
1678
+ previous_data = current_data
1679
+ progress.reset(progress_task)
1680
+ start_time = asyncio.get_event_loop().time()
1279
1681
 
1280
- if len(final_hotkeys) == 0:
1281
- # No hotkeys to stake to.
1282
- err_console.print(
1283
- "Not enough balance to stake to any hotkeys or max_stake is less than current stake."
1284
- )
1285
- raise ValueError
1286
-
1287
- if len(final_hotkeys) == 1:
1288
- # do regular stake
1289
- await add_stake_extrinsic(
1290
- subtensor,
1291
- wallet=wallet,
1292
- old_balance=old_balance,
1293
- hotkey_ss58=final_hotkeys[0][1],
1294
- amount=None if stake_all else final_amounts[0],
1295
- wait_for_inclusion=True,
1296
- prompt=prompt,
1297
- )
1298
- else:
1299
- await add_stake_multiple_extrinsic(
1300
- subtensor,
1301
- wallet=wallet,
1302
- old_balance=old_balance,
1303
- hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys],
1304
- amounts=None if stake_all else final_amounts,
1305
- wait_for_inclusion=True,
1306
- prompt=prompt,
1307
- )
1308
- except ValueError:
1309
- pass
1682
+ block_info = (
1683
+ f"Previous: [dark_sea_green]{previous_block}[/dark_sea_green] "
1684
+ f"Current: [dark_sea_green]{current_block}[/dark_sea_green] "
1685
+ f"Diff: [dark_sea_green]{new_blocks}[/dark_sea_green]"
1686
+ )
1310
1687
 
1688
+ message = f"\nLive stake view - Press [bold red]Ctrl+C[/bold red] to exit\n{block_info}"
1689
+ live_render = Group(message, progress, table)
1690
+ live.update(live_render)
1311
1691
 
1312
- async def unstake(
1313
- wallet: Wallet,
1314
- subtensor: "SubtensorInterface",
1315
- hotkey_ss58_address: str,
1316
- all_hotkeys: bool,
1317
- include_hotkeys: list[str],
1318
- exclude_hotkeys: list[str],
1319
- amount: float,
1320
- keep_stake: float,
1321
- unstake_all: bool,
1322
- prompt: bool,
1323
- ):
1324
- """Unstake token of amount from hotkey(s)."""
1325
-
1326
- # Get the hotkey_names (if any) and the hotkey_ss58s.
1327
- hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = []
1328
- if hotkey_ss58_address:
1329
- print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})")
1330
- # Unstake to specific hotkey.
1331
- hotkeys_to_unstake_from = [(None, hotkey_ss58_address)]
1332
- elif all_hotkeys:
1333
- print_verbose("Unstaking from all hotkeys")
1334
- # Unstake to all hotkeys.
1335
- all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet)
1336
- # Exclude hotkeys that are specified.
1337
- hotkeys_to_unstake_from = [
1338
- (wallet.hotkey_str, wallet.hotkey.ss58_address)
1339
- for wallet in all_hotkeys_
1340
- if wallet.hotkey_str not in exclude_hotkeys
1341
- and wallet.hotkey.ss58_address not in hotkeys_to_unstake_from
1342
- ] # definitely wallets
1692
+ while not progress.finished:
1693
+ await asyncio.sleep(0.1)
1694
+ elapsed = asyncio.get_event_loop().time() - start_time
1695
+ progress.update(
1696
+ progress_task, completed=min(elapsed, refresh_interval)
1697
+ )
1698
+
1699
+ except KeyboardInterrupt:
1700
+ console.print("\n[bold]Stopped live updates[/bold]")
1701
+ return
1343
1702
 
1344
- elif include_hotkeys:
1345
- print_verbose("Unstaking from included hotkeys")
1346
- # Unstake to specific hotkeys.
1347
- for hotkey_ss58_or_hotkey_name in include_hotkeys:
1348
- if is_valid_ss58_address(hotkey_ss58_or_hotkey_name):
1349
- # If the hotkey is a valid ss58 address, we add it to the list.
1350
- hotkeys_to_unstake_from.append((None, hotkey_ss58_or_hotkey_name))
1351
- else:
1352
- # If the hotkey is not a valid ss58 address, we assume it is a hotkey name.
1353
- # We then get the hotkey from the wallet and add it to the list.
1354
- wallet_ = Wallet(
1355
- name=wallet.name,
1356
- path=wallet.path,
1357
- hotkey=hotkey_ss58_or_hotkey_name,
1358
- )
1359
- hotkeys_to_unstake_from.append(
1360
- (wallet_.hotkey_str, wallet_.hotkey.ss58_address)
1361
- )
1362
1703
  else:
1363
- # Only cli.config.wallet.hotkey is specified.
1364
- # so we stake to that single hotkey.
1365
- print_verbose(
1366
- f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})"
1704
+ # Iterate over each hotkey and make a table
1705
+ counter = 0
1706
+ num_hotkeys = len(hotkeys_to_substakes)
1707
+ all_hotkeys_total_global_tao = Balance(0)
1708
+ all_hotkeys_total_tao_value = Balance(0)
1709
+ for hotkey in hotkeys_to_substakes.keys():
1710
+ counter += 1
1711
+ stake, value = create_table(hotkey, hotkeys_to_substakes[hotkey])
1712
+ all_hotkeys_total_global_tao += stake
1713
+ all_hotkeys_total_tao_value += value
1714
+
1715
+ if num_hotkeys > 1 and counter < num_hotkeys and prompt:
1716
+ console.print("\nPress Enter to continue to the next hotkey...")
1717
+ input()
1718
+
1719
+ total_tao_value = (
1720
+ f"τ {millify_tao(all_hotkeys_total_tao_value.tao)}"
1721
+ if not verbose
1722
+ else all_hotkeys_total_tao_value
1367
1723
  )
1368
- assert wallet.hotkey is not None
1369
- hotkeys_to_unstake_from = [(None, wallet.hotkey.ss58_address)]
1370
-
1371
- final_hotkeys: list[tuple[str, str]] = []
1372
- final_amounts: list[Union[float, Balance]] = []
1373
- hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58)
1374
- with suppress(ValueError):
1375
- with console.status(
1376
- f":satellite:Syncing with chain {subtensor}", spinner="earth"
1377
- ) as status:
1378
- print_verbose("Fetching stake", status)
1379
- block_hash = await subtensor.substrate.get_chain_head()
1380
- hotkey_stakes = await asyncio.gather(
1381
- *[
1382
- subtensor.get_stake_for_coldkey_and_hotkey(
1383
- hotkey_ss58=hotkey[1],
1384
- coldkey_ss58=wallet.coldkeypub.ss58_address,
1385
- block_hash=block_hash,
1386
- )
1387
- for hotkey in hotkeys_to_unstake_from
1388
- ]
1724
+ total_tao_ownership = (
1725
+ f"τ {millify_tao(all_hotkeys_total_global_tao.tao)}"
1726
+ if not verbose
1727
+ else all_hotkeys_total_global_tao
1728
+ )
1729
+
1730
+ console.print("\n\n")
1731
+ console.print(
1732
+ f"Wallet:\n"
1733
+ f" Coldkey SS58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{coldkey_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n"
1734
+ f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n"
1735
+ f" Total TAO ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_ownership}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n"
1736
+ f" Total Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]"
1737
+ )
1738
+ if not sub_stakes:
1739
+ console.print(
1740
+ f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})"
1389
1741
  )
1390
- for hotkey, hotkey_stake in zip(hotkeys_to_unstake_from, hotkey_stakes):
1391
- unstake_amount_tao: float = amount
1392
-
1393
- if unstake_all:
1394
- unstake_amount_tao = hotkey_stake.tao
1395
- if keep_stake:
1396
- # Get the current stake of the hotkey from this coldkey.
1397
- unstake_amount_tao = hotkey_stake.tao - keep_stake
1398
- amount = unstake_amount_tao
1399
- if unstake_amount_tao < 0:
1400
- # Skip if max_stake is greater than current stake.
1401
- continue
1742
+ else:
1743
+ # TODO: Temporarily returning till we update docs
1744
+ return
1745
+ display_table = Prompt.ask(
1746
+ "\nPress Enter to view column descriptions or type 'q' to skip:",
1747
+ choices=["", "q"],
1748
+ default="",
1749
+ show_choices=True,
1750
+ ).lower()
1751
+
1752
+ if display_table == "q":
1753
+ console.print(
1754
+ f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]Column descriptions skipped."
1755
+ )
1402
1756
  else:
1403
- if unstake_amount_tao > hotkey_stake.tao:
1404
- # Skip if the specified amount is greater than the current stake.
1405
- continue
1406
-
1407
- final_amounts.append(unstake_amount_tao)
1408
- final_hotkeys.append(hotkey) # add both the name and the ss58 address.
1757
+ header = """
1758
+ [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows:
1759
+ """
1760
+ console.print(header)
1761
+ description_table = Table(
1762
+ show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True
1763
+ )
1409
1764
 
1410
- if len(final_hotkeys) == 0:
1411
- # No hotkeys to unstake from.
1412
- err_console.print(
1413
- "Not enough stake to unstake from any hotkeys or max_stake is more than current stake."
1414
- )
1415
- return None
1765
+ fields = [
1766
+ ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."),
1767
+ (
1768
+ "[bold tan]Symbol[/bold tan]",
1769
+ "The symbol for the subnet's dynamic TAO token.",
1770
+ ),
1771
+ (
1772
+ "[bold tan]Stake (α)[/bold tan]",
1773
+ "The stake amount this hotkey holds in the subnet, expressed in subnet's alpha token currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#staking[/blue].",
1774
+ ),
1775
+ (
1776
+ "[bold tan]TAO Reserves (τ_in)[/bold tan]",
1777
+ 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#subnet-pool[/blue].',
1778
+ ),
1779
+ (
1780
+ "[bold tan]Alpha Reserves (α_in)[/bold tan]",
1781
+ "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#subnet-pool[/blue].",
1782
+ ),
1783
+ (
1784
+ "[bold tan]RATE (τ_in/α_in)[/bold tan]",
1785
+ "Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].",
1786
+ ),
1787
+ (
1788
+ "[bold tan]Alpha out (α_out)[/bold tan]",
1789
+ "Total stake in the subnet, expressed in subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out",
1790
+ ),
1791
+ (
1792
+ "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]",
1793
+ 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#local-weight-or-tao-equiv-%CF%84_in-x-%CE%B1%CE%B1_out[/blue].',
1794
+ ),
1795
+ (
1796
+ "[bold tan]Exchange Value (α x τ/α)[/bold tan]",
1797
+ "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#exchange-value-%CE%B1-x-%CF%84%CE%B1[/blue].",
1798
+ ),
1799
+ (
1800
+ "[bold tan]Swap (α → τ)[/bold tan]",
1801
+ "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#swap-%CE%B1--%CF%84[/blue].",
1802
+ ),
1803
+ (
1804
+ "[bold tan]Registered[/bold tan]",
1805
+ "Indicates if the hotkey is registered in this subnet or not. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].",
1806
+ ),
1807
+ (
1808
+ "[bold tan]Emission (α/block)[/bold tan]",
1809
+ "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://docs.bittensor.com/dynamic-tao/dtao-guide#emissions[/blue].",
1810
+ ),
1811
+ ]
1416
1812
 
1417
- # Ask to unstake
1418
- if prompt:
1419
- if not Confirm.ask(
1420
- f"Do you want to unstake from the following keys to {wallet.name}:\n"
1421
- + "".join(
1422
- [
1423
- f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: "
1424
- f"{f'{amount} {Balance.unit}' if amount else 'All'}[/bold white]\n"
1425
- for hotkey, amount in zip(final_hotkeys, final_amounts)
1426
- ]
1813
+ description_table.add_column(
1814
+ "Field",
1815
+ no_wrap=True,
1816
+ style="bold tan",
1427
1817
  )
1428
- ):
1429
- return None
1430
- if len(final_hotkeys) == 1:
1431
- # do regular unstake
1432
- await unstake_extrinsic(
1433
- subtensor,
1434
- wallet=wallet,
1435
- hotkey_ss58=final_hotkeys[0][1],
1436
- amount=None if unstake_all else final_amounts[0],
1437
- wait_for_inclusion=True,
1438
- prompt=prompt,
1439
- )
1440
- else:
1441
- await unstake_multiple_extrinsic(
1442
- subtensor,
1443
- wallet=wallet,
1444
- hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys],
1445
- amounts=None if unstake_all else final_amounts,
1446
- wait_for_inclusion=True,
1447
- prompt=prompt,
1448
- )
1818
+ description_table.add_column("Description", overflow="fold")
1819
+ for field_name, description in fields:
1820
+ description_table.add_row(field_name, description)
1821
+ console.print(description_table)