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.
@@ -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