bittensor-cli 9.0.0rc1__py3-none-any.whl → 9.0.0rc3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bittensor_cli/__init__.py +1 -1
- bittensor_cli/cli.py +440 -157
- bittensor_cli/src/__init__.py +4 -1
- bittensor_cli/src/bittensor/subtensor_interface.py +1 -1
- bittensor_cli/src/bittensor/utils.py +14 -0
- bittensor_cli/src/commands/stake/add.py +625 -0
- bittensor_cli/src/commands/stake/children_hotkeys.py +2 -4
- bittensor_cli/src/commands/stake/list.py +687 -0
- bittensor_cli/src/commands/stake/move.py +1 -1
- bittensor_cli/src/commands/stake/remove.py +1146 -0
- bittensor_cli/src/commands/subnets/subnets.py +20 -8
- bittensor_cli/src/commands/wallets.py +24 -32
- {bittensor_cli-9.0.0rc1.dist-info → bittensor_cli-9.0.0rc3.dist-info}/METADATA +2 -2
- {bittensor_cli-9.0.0rc1.dist-info → bittensor_cli-9.0.0rc3.dist-info}/RECORD +17 -15
- bittensor_cli/src/commands/stake/stake.py +0 -1821
- {bittensor_cli-9.0.0rc1.dist-info → bittensor_cli-9.0.0rc3.dist-info}/WHEEL +0 -0
- {bittensor_cli-9.0.0rc1.dist-info → bittensor_cli-9.0.0rc3.dist-info}/entry_points.txt +0 -0
- {bittensor_cli-9.0.0rc1.dist-info → bittensor_cli-9.0.0rc3.dist-info}/top_level.txt +0 -0
bittensor_cli/src/__init__.py
CHANGED
@@ -4,13 +4,14 @@ from typing import Any, Optional
|
|
4
4
|
|
5
5
|
|
6
6
|
class Constants:
|
7
|
-
networks = ["local", "finney", "test", "archive", "rao", "dev"]
|
7
|
+
networks = ["local", "finney", "test", "archive", "rao", "dev", "latent-lite"]
|
8
8
|
finney_entrypoint = "wss://entrypoint-finney.opentensor.ai:443"
|
9
9
|
finney_test_entrypoint = "wss://test.finney.opentensor.ai:443"
|
10
10
|
archive_entrypoint = "wss://archive.chain.opentensor.ai:443"
|
11
11
|
rao_entrypoint = "wss://rao.chain.opentensor.ai:443"
|
12
12
|
dev_entrypoint = "wss://dev.chain.opentensor.ai:443 "
|
13
13
|
local_entrypoint = "ws://127.0.0.1:9944"
|
14
|
+
latent_lite_entrypoint = "wss://lite.sub.latent.to:443"
|
14
15
|
network_map = {
|
15
16
|
"finney": finney_entrypoint,
|
16
17
|
"test": finney_test_entrypoint,
|
@@ -18,6 +19,7 @@ class Constants:
|
|
18
19
|
"local": local_entrypoint,
|
19
20
|
"dev": dev_entrypoint,
|
20
21
|
"rao": rao_entrypoint,
|
22
|
+
"latent-lite": latent_lite_entrypoint,
|
21
23
|
}
|
22
24
|
delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json"
|
23
25
|
|
@@ -65,6 +67,7 @@ class DelegatesDetails:
|
|
65
67
|
|
66
68
|
class Defaults:
|
67
69
|
netuid = 1
|
70
|
+
rate_tolerance = 0.005
|
68
71
|
|
69
72
|
class config:
|
70
73
|
base_path = "~/.bittensor"
|
@@ -1330,7 +1330,7 @@ class SubtensorInterface:
|
|
1330
1330
|
result = await self.query_runtime_api(
|
1331
1331
|
runtime_api="StakeInfoRuntimeApi",
|
1332
1332
|
method="get_stake_info_for_coldkeys",
|
1333
|
-
params=coldkey_ss58_list,
|
1333
|
+
params=[coldkey_ss58_list],
|
1334
1334
|
block_hash=block_hash,
|
1335
1335
|
)
|
1336
1336
|
if result is None:
|
@@ -1261,3 +1261,17 @@ def print_linux_dependency_message():
|
|
1261
1261
|
def is_linux():
|
1262
1262
|
"""Returns True if the operating system is Linux."""
|
1263
1263
|
return platform.system().lower() == "linux"
|
1264
|
+
|
1265
|
+
def validate_rate_tolerance(value: Optional[float]) -> Optional[float]:
|
1266
|
+
"""Validates rate tolerance input"""
|
1267
|
+
if value is not None:
|
1268
|
+
if value < 0:
|
1269
|
+
raise typer.BadParameter("Rate tolerance cannot be negative (less than 0%).")
|
1270
|
+
if value > 1:
|
1271
|
+
raise typer.BadParameter("Rate tolerance cannot be greater than 1 (100%).")
|
1272
|
+
if value > 0.5:
|
1273
|
+
console.print(
|
1274
|
+
f"[yellow]Warning: High rate tolerance of {value*100}% specified. "
|
1275
|
+
"This may result in unfavorable transaction execution.[/yellow]"
|
1276
|
+
)
|
1277
|
+
return value
|
@@ -0,0 +1,625 @@
|
|
1
|
+
import asyncio
|
2
|
+
from functools import partial
|
3
|
+
|
4
|
+
import typer
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
6
|
+
from rich.table import Table
|
7
|
+
from rich.prompt import Confirm, Prompt
|
8
|
+
|
9
|
+
from async_substrate_interface.errors import SubstrateRequestException
|
10
|
+
from bittensor_cli.src import COLOR_PALETTE
|
11
|
+
from bittensor_cli.src.bittensor.balances import Balance
|
12
|
+
from bittensor_cli.src.bittensor.utils import (
|
13
|
+
console,
|
14
|
+
err_console,
|
15
|
+
format_error_message,
|
16
|
+
get_hotkey_wallets_for_wallet,
|
17
|
+
is_valid_ss58_address,
|
18
|
+
print_error,
|
19
|
+
print_verbose,
|
20
|
+
)
|
21
|
+
from bittensor_wallet import Wallet
|
22
|
+
from bittensor_wallet.errors import KeyFileError
|
23
|
+
|
24
|
+
if TYPE_CHECKING:
|
25
|
+
from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
|
26
|
+
|
27
|
+
|
28
|
+
# Command
|
29
|
+
async def stake_add(
|
30
|
+
wallet: Wallet,
|
31
|
+
subtensor: "SubtensorInterface",
|
32
|
+
netuid: Optional[int],
|
33
|
+
stake_all: bool,
|
34
|
+
amount: float,
|
35
|
+
prompt: bool,
|
36
|
+
all_hotkeys: bool,
|
37
|
+
include_hotkeys: list[str],
|
38
|
+
exclude_hotkeys: list[str],
|
39
|
+
safe_staking: bool,
|
40
|
+
rate_tolerance: float,
|
41
|
+
allow_partial_stake: bool,
|
42
|
+
):
|
43
|
+
"""
|
44
|
+
Args:
|
45
|
+
wallet: wallet object
|
46
|
+
subtensor: SubtensorInterface object
|
47
|
+
netuid: the netuid to stake to (None indicates all subnets)
|
48
|
+
stake_all: whether to stake all available balance
|
49
|
+
amount: specified amount of balance to stake
|
50
|
+
delegate: whether to delegate stake, currently unused
|
51
|
+
prompt: whether to prompt the user
|
52
|
+
max_stake: maximum amount to stake (used in combination with stake_all), currently unused
|
53
|
+
all_hotkeys: whether to stake all hotkeys
|
54
|
+
include_hotkeys: list of hotkeys to include in staking process (if not specifying `--all`)
|
55
|
+
exclude_hotkeys: list of hotkeys to exclude in staking (if specifying `--all`)
|
56
|
+
safe_staking: whether to use safe staking
|
57
|
+
rate_tolerance: rate tolerance percentage for stake operations
|
58
|
+
allow_partial_stake: whether to allow partial stake
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
bool: True if stake operation is successful, False otherwise
|
62
|
+
"""
|
63
|
+
|
64
|
+
async def safe_stake_extrinsic(
|
65
|
+
netuid: int,
|
66
|
+
amount: Balance,
|
67
|
+
current_stake: Balance,
|
68
|
+
hotkey_ss58: str,
|
69
|
+
price_limit: Balance,
|
70
|
+
wallet: Wallet,
|
71
|
+
subtensor: "SubtensorInterface",
|
72
|
+
status=None,
|
73
|
+
) -> None:
|
74
|
+
err_out = partial(print_error, status=status)
|
75
|
+
failure_prelude = (
|
76
|
+
f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid}"
|
77
|
+
)
|
78
|
+
current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address)
|
79
|
+
next_nonce = await subtensor.substrate.get_account_next_index(
|
80
|
+
wallet.coldkeypub.ss58_address
|
81
|
+
)
|
82
|
+
call = await subtensor.substrate.compose_call(
|
83
|
+
call_module="SubtensorModule",
|
84
|
+
call_function="add_stake_limit",
|
85
|
+
call_params={
|
86
|
+
"hotkey": hotkey_ss58,
|
87
|
+
"netuid": netuid,
|
88
|
+
"amount_staked": amount.rao,
|
89
|
+
"limit_price": price_limit,
|
90
|
+
"allow_partial": allow_partial_stake,
|
91
|
+
},
|
92
|
+
)
|
93
|
+
extrinsic = await subtensor.substrate.create_signed_extrinsic(
|
94
|
+
call=call, keypair=wallet.coldkey, nonce=next_nonce
|
95
|
+
)
|
96
|
+
try:
|
97
|
+
response = await subtensor.substrate.submit_extrinsic(
|
98
|
+
extrinsic, wait_for_inclusion=True, wait_for_finalization=False
|
99
|
+
)
|
100
|
+
except SubstrateRequestException as e:
|
101
|
+
if "Custom error: 8" in str(e):
|
102
|
+
print_error(
|
103
|
+
f"\n{failure_prelude}: Price exceeded tolerance limit. "
|
104
|
+
f"Transaction rejected because partial staking is disabled. "
|
105
|
+
f"Either increase price tolerance or enable partial staking.",
|
106
|
+
status=status,
|
107
|
+
)
|
108
|
+
return
|
109
|
+
else:
|
110
|
+
err_out(
|
111
|
+
f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}"
|
112
|
+
)
|
113
|
+
return
|
114
|
+
else:
|
115
|
+
await response.process_events()
|
116
|
+
if not await response.is_success:
|
117
|
+
err_out(
|
118
|
+
f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}"
|
119
|
+
)
|
120
|
+
else:
|
121
|
+
block_hash = await subtensor.substrate.get_chain_head()
|
122
|
+
new_balance, new_stake = await asyncio.gather(
|
123
|
+
subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash),
|
124
|
+
subtensor.get_stake(
|
125
|
+
hotkey_ss58=hotkey_ss58,
|
126
|
+
coldkey_ss58=wallet.coldkeypub.ss58_address,
|
127
|
+
netuid=netuid,
|
128
|
+
block_hash=block_hash,
|
129
|
+
),
|
130
|
+
)
|
131
|
+
console.print(
|
132
|
+
f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid}[/dark_sea_green3]"
|
133
|
+
)
|
134
|
+
console.print(
|
135
|
+
f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
|
136
|
+
)
|
137
|
+
|
138
|
+
amount_staked = current_balance - new_balance
|
139
|
+
if allow_partial_stake and (amount_staked != amount):
|
140
|
+
console.print(
|
141
|
+
"Partial stake transaction. Staked:\n"
|
142
|
+
f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
|
143
|
+
f"instead of "
|
144
|
+
f"[blue]{amount}[/blue]"
|
145
|
+
)
|
146
|
+
|
147
|
+
console.print(
|
148
|
+
f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] "
|
149
|
+
f"Stake:\n"
|
150
|
+
f" [blue]{current_stake}[/blue] "
|
151
|
+
f":arrow_right: "
|
152
|
+
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n"
|
153
|
+
)
|
154
|
+
|
155
|
+
async def stake_extrinsic(
|
156
|
+
netuid_i, amount_, current, staking_address_ss58, status=None
|
157
|
+
):
|
158
|
+
err_out = partial(print_error, status=status)
|
159
|
+
current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address)
|
160
|
+
failure_prelude = (
|
161
|
+
f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}"
|
162
|
+
)
|
163
|
+
next_nonce = await subtensor.substrate.get_account_next_index(
|
164
|
+
wallet.coldkeypub.ss58_address
|
165
|
+
)
|
166
|
+
call = await subtensor.substrate.compose_call(
|
167
|
+
call_module="SubtensorModule",
|
168
|
+
call_function="add_stake",
|
169
|
+
call_params={
|
170
|
+
"hotkey": staking_address_ss58,
|
171
|
+
"netuid": netuid_i,
|
172
|
+
"amount_staked": amount_.rao,
|
173
|
+
},
|
174
|
+
)
|
175
|
+
extrinsic = await subtensor.substrate.create_signed_extrinsic(
|
176
|
+
call=call, keypair=wallet.coldkey, nonce=next_nonce
|
177
|
+
)
|
178
|
+
try:
|
179
|
+
response = await subtensor.substrate.submit_extrinsic(
|
180
|
+
extrinsic, wait_for_inclusion=True, wait_for_finalization=False
|
181
|
+
)
|
182
|
+
except SubstrateRequestException as e:
|
183
|
+
err_out(
|
184
|
+
f"\n{failure_prelude} with error: {format_error_message(e, subtensor.substrate)}"
|
185
|
+
)
|
186
|
+
return
|
187
|
+
else:
|
188
|
+
await response.process_events()
|
189
|
+
if not await response.is_success:
|
190
|
+
err_out(
|
191
|
+
f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}"
|
192
|
+
)
|
193
|
+
else:
|
194
|
+
new_balance, new_stake = await asyncio.gather(
|
195
|
+
subtensor.get_balance(wallet.coldkeypub.ss58_address),
|
196
|
+
subtensor.get_stake(
|
197
|
+
hotkey_ss58=staking_address_ss58,
|
198
|
+
coldkey_ss58=wallet.coldkeypub.ss58_address,
|
199
|
+
netuid=netuid_i,
|
200
|
+
),
|
201
|
+
)
|
202
|
+
console.print(
|
203
|
+
f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]"
|
204
|
+
)
|
205
|
+
console.print(
|
206
|
+
f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
|
207
|
+
)
|
208
|
+
console.print(
|
209
|
+
f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] "
|
210
|
+
f"Stake:\n"
|
211
|
+
f" [blue]{current}[/blue] "
|
212
|
+
f":arrow_right: "
|
213
|
+
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n"
|
214
|
+
)
|
215
|
+
|
216
|
+
netuids = (
|
217
|
+
[int(netuid)]
|
218
|
+
if netuid is not None
|
219
|
+
else await subtensor.get_all_subnet_netuids()
|
220
|
+
)
|
221
|
+
|
222
|
+
hotkeys_to_stake_to = _get_hotkeys_to_stake_to(
|
223
|
+
wallet=wallet,
|
224
|
+
all_hotkeys=all_hotkeys,
|
225
|
+
include_hotkeys=include_hotkeys,
|
226
|
+
exclude_hotkeys=exclude_hotkeys,
|
227
|
+
)
|
228
|
+
|
229
|
+
# Get subnet data and stake information for coldkey
|
230
|
+
chain_head = await subtensor.substrate.get_chain_head()
|
231
|
+
_all_subnets, _stake_info, current_wallet_balance = await asyncio.gather(
|
232
|
+
subtensor.all_subnets(),
|
233
|
+
subtensor.get_stake_for_coldkey(
|
234
|
+
coldkey_ss58=wallet.coldkeypub.ss58_address,
|
235
|
+
block_hash=chain_head,
|
236
|
+
),
|
237
|
+
subtensor.get_balance(wallet.coldkeypub.ss58_address),
|
238
|
+
)
|
239
|
+
all_subnets = {di.netuid: di for di in _all_subnets}
|
240
|
+
|
241
|
+
# Map current stake balances for hotkeys
|
242
|
+
hotkey_stake_map = {}
|
243
|
+
for _, hotkey_ss58 in hotkeys_to_stake_to:
|
244
|
+
hotkey_stake_map[hotkey_ss58] = {}
|
245
|
+
for netuid in netuids:
|
246
|
+
hotkey_stake_map[hotkey_ss58][netuid] = Balance.from_rao(0)
|
247
|
+
|
248
|
+
for stake_info in _stake_info:
|
249
|
+
if stake_info.hotkey_ss58 in hotkey_stake_map:
|
250
|
+
hotkey_stake_map[stake_info.hotkey_ss58][stake_info.netuid] = (
|
251
|
+
stake_info.stake
|
252
|
+
)
|
253
|
+
|
254
|
+
# Determine the amount we are staking.
|
255
|
+
rows = []
|
256
|
+
amounts_to_stake = []
|
257
|
+
current_stake_balances = []
|
258
|
+
prices_with_tolerance = []
|
259
|
+
remaining_wallet_balance = current_wallet_balance
|
260
|
+
max_slippage = 0.0
|
261
|
+
|
262
|
+
for hotkey in hotkeys_to_stake_to:
|
263
|
+
for netuid in netuids:
|
264
|
+
# Check that the subnet exists.
|
265
|
+
subnet_info = all_subnets.get(netuid)
|
266
|
+
if not subnet_info:
|
267
|
+
err_console.print(f"Subnet with netuid: {netuid} does not exist.")
|
268
|
+
continue
|
269
|
+
current_stake_balances.append(hotkey_stake_map[hotkey[1]][netuid])
|
270
|
+
|
271
|
+
# Get the amount.
|
272
|
+
amount_to_stake = Balance(0)
|
273
|
+
if amount:
|
274
|
+
amount_to_stake = Balance.from_tao(amount)
|
275
|
+
elif stake_all:
|
276
|
+
amount_to_stake = current_wallet_balance / len(netuids)
|
277
|
+
elif not amount:
|
278
|
+
amount_to_stake, _ = _prompt_stake_amount(
|
279
|
+
current_balance=remaining_wallet_balance,
|
280
|
+
netuid=netuid,
|
281
|
+
action_name="stake",
|
282
|
+
)
|
283
|
+
amounts_to_stake.append(amount_to_stake)
|
284
|
+
|
285
|
+
# Check enough to stake.
|
286
|
+
if amount_to_stake > remaining_wallet_balance:
|
287
|
+
err_console.print(
|
288
|
+
f"[red]Not enough stake[/red]:[bold white]\n wallet balance:{remaining_wallet_balance} < "
|
289
|
+
f"staking amount: {amount_to_stake}[/bold white]"
|
290
|
+
)
|
291
|
+
return False
|
292
|
+
remaining_wallet_balance -= amount_to_stake
|
293
|
+
|
294
|
+
# Calculate slippage
|
295
|
+
received_amount, slippage_pct, slippage_pct_float, rate = (
|
296
|
+
_calculate_slippage(subnet_info, amount_to_stake)
|
297
|
+
)
|
298
|
+
max_slippage = max(slippage_pct_float, max_slippage)
|
299
|
+
|
300
|
+
# Add rows for the table
|
301
|
+
base_row = [
|
302
|
+
str(netuid), # netuid
|
303
|
+
f"{hotkey[1]}", # hotkey
|
304
|
+
str(amount_to_stake), # amount
|
305
|
+
str(rate)
|
306
|
+
+ f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", # rate
|
307
|
+
str(received_amount.set_unit(netuid)), # received
|
308
|
+
str(slippage_pct), # slippage
|
309
|
+
]
|
310
|
+
|
311
|
+
# If we are staking safe, add price tolerance
|
312
|
+
if safe_staking:
|
313
|
+
if subnet_info.is_dynamic:
|
314
|
+
rate = 1 / subnet_info.price.tao or 1
|
315
|
+
_rate_with_tolerance = rate * (
|
316
|
+
1 + rate_tolerance
|
317
|
+
) # Rate only for display
|
318
|
+
rate_with_tolerance = f"{_rate_with_tolerance:.4f}"
|
319
|
+
price_with_tolerance = subnet_info.price.rao * (
|
320
|
+
1 + rate_tolerance
|
321
|
+
) # Actual price to pass to extrinsic
|
322
|
+
else:
|
323
|
+
rate_with_tolerance = "1"
|
324
|
+
price_with_tolerance = Balance.from_rao(1)
|
325
|
+
prices_with_tolerance.append(price_with_tolerance)
|
326
|
+
|
327
|
+
base_row.extend(
|
328
|
+
[
|
329
|
+
f"{rate_with_tolerance} {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ",
|
330
|
+
f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", # safe staking
|
331
|
+
]
|
332
|
+
)
|
333
|
+
|
334
|
+
rows.append(tuple(base_row))
|
335
|
+
|
336
|
+
# Define and print stake table + slippage warning
|
337
|
+
table = _define_stake_table(wallet, subtensor, safe_staking, rate_tolerance)
|
338
|
+
for row in rows:
|
339
|
+
table.add_row(*row)
|
340
|
+
_print_table_and_slippage(table, max_slippage, safe_staking)
|
341
|
+
|
342
|
+
if prompt:
|
343
|
+
if not Confirm.ask("Would you like to continue?"):
|
344
|
+
raise typer.Exit()
|
345
|
+
try:
|
346
|
+
wallet.unlock_coldkey()
|
347
|
+
except KeyFileError:
|
348
|
+
err_console.print("Error decrypting coldkey (possibly incorrect password)")
|
349
|
+
return False
|
350
|
+
|
351
|
+
if safe_staking:
|
352
|
+
stake_coroutines = []
|
353
|
+
for i, (ni, am, curr, price_with_tolerance) in enumerate(
|
354
|
+
zip(
|
355
|
+
netuids, amounts_to_stake, current_stake_balances, prices_with_tolerance
|
356
|
+
)
|
357
|
+
):
|
358
|
+
for _, staking_address in hotkeys_to_stake_to:
|
359
|
+
# Regular extrinsic for root subnet
|
360
|
+
if ni == 0:
|
361
|
+
stake_coroutines.append(
|
362
|
+
stake_extrinsic(
|
363
|
+
netuid_i=ni,
|
364
|
+
amount_=am,
|
365
|
+
current=curr,
|
366
|
+
staking_address_ss58=staking_address,
|
367
|
+
)
|
368
|
+
)
|
369
|
+
else:
|
370
|
+
stake_coroutines.append(
|
371
|
+
safe_stake_extrinsic(
|
372
|
+
netuid=ni,
|
373
|
+
amount=am,
|
374
|
+
current_stake=curr,
|
375
|
+
hotkey_ss58=staking_address,
|
376
|
+
price_limit=price_with_tolerance,
|
377
|
+
wallet=wallet,
|
378
|
+
subtensor=subtensor,
|
379
|
+
)
|
380
|
+
)
|
381
|
+
else:
|
382
|
+
stake_coroutines = [
|
383
|
+
stake_extrinsic(
|
384
|
+
netuid_i=ni,
|
385
|
+
amount_=am,
|
386
|
+
current=curr,
|
387
|
+
staking_address_ss58=staking_address,
|
388
|
+
)
|
389
|
+
for i, (ni, am, curr) in enumerate(
|
390
|
+
zip(netuids, amounts_to_stake, current_stake_balances)
|
391
|
+
)
|
392
|
+
for _, staking_address in hotkeys_to_stake_to
|
393
|
+
]
|
394
|
+
|
395
|
+
with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."):
|
396
|
+
# We can gather them all at once but balance reporting will be in race-condition.
|
397
|
+
for coroutine in stake_coroutines:
|
398
|
+
await coroutine
|
399
|
+
|
400
|
+
|
401
|
+
# Helper functions
|
402
|
+
def _prompt_stake_amount(
|
403
|
+
current_balance: Balance, netuid: int, action_name: str
|
404
|
+
) -> tuple[Balance, bool]:
|
405
|
+
"""Prompts user to input a stake amount with validation.
|
406
|
+
|
407
|
+
Args:
|
408
|
+
current_balance (Balance): The maximum available balance
|
409
|
+
netuid (int): The subnet id to get the correct unit
|
410
|
+
action_name (str): The name of the action (e.g. "transfer", "move", "unstake")
|
411
|
+
|
412
|
+
Returns:
|
413
|
+
tuple[Balance, bool]: (The amount to use as Balance object, whether all balance was selected)
|
414
|
+
"""
|
415
|
+
while True:
|
416
|
+
amount_input = Prompt.ask(
|
417
|
+
f"\nEnter the amount to {action_name}"
|
418
|
+
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
|
419
|
+
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
|
420
|
+
f"or "
|
421
|
+
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]'all'[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
|
422
|
+
f"for entire balance"
|
423
|
+
)
|
424
|
+
|
425
|
+
if amount_input.lower() == "all":
|
426
|
+
return current_balance, True
|
427
|
+
|
428
|
+
try:
|
429
|
+
amount = float(amount_input)
|
430
|
+
if amount <= 0:
|
431
|
+
console.print("[red]Amount must be greater than 0[/red]")
|
432
|
+
continue
|
433
|
+
if amount > current_balance.tao:
|
434
|
+
console.print(
|
435
|
+
f"[red]Amount exceeds available balance of "
|
436
|
+
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
|
437
|
+
f"[/red]"
|
438
|
+
)
|
439
|
+
continue
|
440
|
+
return Balance.from_tao(amount), False
|
441
|
+
except ValueError:
|
442
|
+
console.print("[red]Please enter a valid number or 'all'[/red]")
|
443
|
+
|
444
|
+
|
445
|
+
def _get_hotkeys_to_stake_to(
|
446
|
+
wallet: Wallet,
|
447
|
+
all_hotkeys: bool = False,
|
448
|
+
include_hotkeys: list[str] = None,
|
449
|
+
exclude_hotkeys: list[str] = None,
|
450
|
+
) -> list[tuple[Optional[str], str]]:
|
451
|
+
"""Get list of hotkeys to stake to based on input parameters.
|
452
|
+
|
453
|
+
Args:
|
454
|
+
wallet: The wallet containing hotkeys
|
455
|
+
all_hotkeys: If True, get all hotkeys from wallet except excluded ones
|
456
|
+
include_hotkeys: List of specific hotkeys to include (by name or ss58 address)
|
457
|
+
exclude_hotkeys: List of hotkeys to exclude when all_hotkeys is True
|
458
|
+
|
459
|
+
Returns:
|
460
|
+
List of tuples containing (hotkey_name, hotkey_ss58_address)
|
461
|
+
hotkey_name may be None if ss58 address was provided directly
|
462
|
+
"""
|
463
|
+
if all_hotkeys:
|
464
|
+
# Stake to all hotkeys except excluded ones
|
465
|
+
all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet)
|
466
|
+
return [
|
467
|
+
(wallet.hotkey_str, wallet.hotkey.ss58_address)
|
468
|
+
for wallet in all_hotkeys_
|
469
|
+
if wallet.hotkey_str not in (exclude_hotkeys or [])
|
470
|
+
]
|
471
|
+
|
472
|
+
if include_hotkeys:
|
473
|
+
print_verbose("Staking to only included hotkeys")
|
474
|
+
# Stake to specific hotkeys
|
475
|
+
hotkeys = []
|
476
|
+
for hotkey_ss58_or_hotkey_name in include_hotkeys:
|
477
|
+
if is_valid_ss58_address(hotkey_ss58_or_hotkey_name):
|
478
|
+
# If valid ss58 address, add directly
|
479
|
+
hotkeys.append((None, hotkey_ss58_or_hotkey_name))
|
480
|
+
else:
|
481
|
+
# If hotkey name, get ss58 from wallet
|
482
|
+
wallet_ = Wallet(
|
483
|
+
path=wallet.path,
|
484
|
+
name=wallet.name,
|
485
|
+
hotkey=hotkey_ss58_or_hotkey_name,
|
486
|
+
)
|
487
|
+
hotkeys.append((wallet_.hotkey_str, wallet_.hotkey.ss58_address))
|
488
|
+
|
489
|
+
return hotkeys
|
490
|
+
|
491
|
+
# Default: stake to single hotkey from wallet
|
492
|
+
print_verbose(
|
493
|
+
f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})"
|
494
|
+
)
|
495
|
+
assert wallet.hotkey is not None
|
496
|
+
return [(None, wallet.hotkey.ss58_address)]
|
497
|
+
|
498
|
+
|
499
|
+
def _define_stake_table(
|
500
|
+
wallet: Wallet,
|
501
|
+
subtensor: "SubtensorInterface",
|
502
|
+
safe_staking: bool,
|
503
|
+
rate_tolerance: float,
|
504
|
+
) -> Table:
|
505
|
+
"""Creates and initializes a table for displaying stake information.
|
506
|
+
|
507
|
+
Args:
|
508
|
+
wallet: The wallet being used for staking
|
509
|
+
subtensor: The subtensor interface
|
510
|
+
|
511
|
+
Returns:
|
512
|
+
Table: An initialized rich Table object with appropriate columns
|
513
|
+
"""
|
514
|
+
table = Table(
|
515
|
+
title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to:\n"
|
516
|
+
f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], "
|
517
|
+
f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n"
|
518
|
+
f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n",
|
519
|
+
show_footer=True,
|
520
|
+
show_edge=False,
|
521
|
+
header_style="bold white",
|
522
|
+
border_style="bright_black",
|
523
|
+
style="bold",
|
524
|
+
title_justify="center",
|
525
|
+
show_lines=False,
|
526
|
+
pad_edge=True,
|
527
|
+
)
|
528
|
+
|
529
|
+
table.add_column("Netuid", justify="center", style="grey89")
|
530
|
+
table.add_column(
|
531
|
+
"Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
|
532
|
+
)
|
533
|
+
table.add_column(
|
534
|
+
f"Amount ({Balance.get_unit(0)})",
|
535
|
+
justify="center",
|
536
|
+
style=COLOR_PALETTE["POOLS"]["TAO"],
|
537
|
+
)
|
538
|
+
table.add_column(
|
539
|
+
f"Rate (per {Balance.get_unit(0)})",
|
540
|
+
justify="center",
|
541
|
+
style=COLOR_PALETTE["POOLS"]["RATE"],
|
542
|
+
)
|
543
|
+
table.add_column(
|
544
|
+
"Received",
|
545
|
+
justify="center",
|
546
|
+
style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"],
|
547
|
+
)
|
548
|
+
table.add_column(
|
549
|
+
"Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"]
|
550
|
+
)
|
551
|
+
|
552
|
+
if safe_staking:
|
553
|
+
table.add_column(
|
554
|
+
f"Rate with tolerance: [blue]({rate_tolerance*100}%)[/blue]",
|
555
|
+
justify="center",
|
556
|
+
style=COLOR_PALETTE["POOLS"]["RATE"],
|
557
|
+
)
|
558
|
+
table.add_column(
|
559
|
+
"Partial stake enabled",
|
560
|
+
justify="center",
|
561
|
+
style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"],
|
562
|
+
)
|
563
|
+
return table
|
564
|
+
|
565
|
+
|
566
|
+
def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: bool):
|
567
|
+
"""Prints the stake table, slippage warning, and table description.
|
568
|
+
|
569
|
+
Args:
|
570
|
+
table: The rich Table object to print
|
571
|
+
max_slippage: The maximum slippage percentage across all operations
|
572
|
+
"""
|
573
|
+
console.print(table)
|
574
|
+
|
575
|
+
# Greater than 5%
|
576
|
+
if max_slippage > 5:
|
577
|
+
message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n"
|
578
|
+
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"
|
579
|
+
message += "-------------------------------------------------------------------------------------------------------------------\n"
|
580
|
+
console.print(message)
|
581
|
+
|
582
|
+
# Table description
|
583
|
+
base_description = """
|
584
|
+
[bold white]Description[/bold white]:
|
585
|
+
The table displays information about the stake operation you are about to perform.
|
586
|
+
The columns are as follows:
|
587
|
+
- [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to.
|
588
|
+
- [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to.
|
589
|
+
- [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey.
|
590
|
+
- [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake.
|
591
|
+
- [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage.
|
592
|
+
- [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root)."""
|
593
|
+
|
594
|
+
safe_staking_description = """
|
595
|
+
- [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate exceeds this tolerance, the transaction will be limited or rejected.
|
596
|
+
- [bold white]Partial staking[/bold white]: If True, allows staking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.\n"""
|
597
|
+
|
598
|
+
console.print(base_description + (safe_staking_description if safe_staking else ""))
|
599
|
+
|
600
|
+
|
601
|
+
def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]:
|
602
|
+
"""Calculate slippage when adding stake.
|
603
|
+
|
604
|
+
Args:
|
605
|
+
subnet_info: Subnet dynamic info
|
606
|
+
amount: Amount being staked
|
607
|
+
|
608
|
+
Returns:
|
609
|
+
tuple containing:
|
610
|
+
- received_amount: Amount received after slippage
|
611
|
+
- slippage_str: Formatted slippage percentage string
|
612
|
+
- slippage_float: Raw slippage percentage value
|
613
|
+
"""
|
614
|
+
received_amount, _, slippage_pct_float = subnet_info.tao_to_alpha_with_slippage(
|
615
|
+
amount
|
616
|
+
)
|
617
|
+
if subnet_info.is_dynamic:
|
618
|
+
slippage_str = f"{slippage_pct_float:.4f} %"
|
619
|
+
rate = f"{(1 / subnet_info.price.tao or 1):.4f}"
|
620
|
+
else:
|
621
|
+
slippage_pct_float = 0
|
622
|
+
slippage_str = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]"
|
623
|
+
rate = "1"
|
624
|
+
|
625
|
+
return received_amount, slippage_str, slippage_pct_float, rate
|