meshtensor-cli 9.18.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. meshtensor_cli/__init__.py +22 -0
  2. meshtensor_cli/cli.py +10742 -0
  3. meshtensor_cli/doc_generation_helper.py +4 -0
  4. meshtensor_cli/src/__init__.py +1085 -0
  5. meshtensor_cli/src/commands/__init__.py +0 -0
  6. meshtensor_cli/src/commands/axon/__init__.py +0 -0
  7. meshtensor_cli/src/commands/axon/axon.py +132 -0
  8. meshtensor_cli/src/commands/crowd/__init__.py +0 -0
  9. meshtensor_cli/src/commands/crowd/contribute.py +621 -0
  10. meshtensor_cli/src/commands/crowd/contributors.py +200 -0
  11. meshtensor_cli/src/commands/crowd/create.py +783 -0
  12. meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
  13. meshtensor_cli/src/commands/crowd/refund.py +233 -0
  14. meshtensor_cli/src/commands/crowd/update.py +418 -0
  15. meshtensor_cli/src/commands/crowd/utils.py +124 -0
  16. meshtensor_cli/src/commands/crowd/view.py +991 -0
  17. meshtensor_cli/src/commands/governance/__init__.py +0 -0
  18. meshtensor_cli/src/commands/governance/governance.py +794 -0
  19. meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
  20. meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
  21. meshtensor_cli/src/commands/liquidity/utils.py +202 -0
  22. meshtensor_cli/src/commands/proxy.py +700 -0
  23. meshtensor_cli/src/commands/stake/__init__.py +0 -0
  24. meshtensor_cli/src/commands/stake/add.py +799 -0
  25. meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
  26. meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
  27. meshtensor_cli/src/commands/stake/claim.py +770 -0
  28. meshtensor_cli/src/commands/stake/list.py +738 -0
  29. meshtensor_cli/src/commands/stake/move.py +1211 -0
  30. meshtensor_cli/src/commands/stake/remove.py +1466 -0
  31. meshtensor_cli/src/commands/stake/wizard.py +323 -0
  32. meshtensor_cli/src/commands/subnets/__init__.py +0 -0
  33. meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
  34. meshtensor_cli/src/commands/subnets/price.py +733 -0
  35. meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
  36. meshtensor_cli/src/commands/sudo.py +1294 -0
  37. meshtensor_cli/src/commands/tc/__init__.py +0 -0
  38. meshtensor_cli/src/commands/tc/tc.py +190 -0
  39. meshtensor_cli/src/commands/treasury/__init__.py +0 -0
  40. meshtensor_cli/src/commands/treasury/treasury.py +194 -0
  41. meshtensor_cli/src/commands/view.py +354 -0
  42. meshtensor_cli/src/commands/wallets.py +2311 -0
  43. meshtensor_cli/src/commands/weights.py +467 -0
  44. meshtensor_cli/src/meshtensor/__init__.py +0 -0
  45. meshtensor_cli/src/meshtensor/balances.py +313 -0
  46. meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
  47. meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
  48. meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
  49. meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
  50. meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
  51. meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
  52. meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
  53. meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
  54. meshtensor_cli/src/meshtensor/minigraph.py +254 -0
  55. meshtensor_cli/src/meshtensor/networking.py +12 -0
  56. meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
  57. meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
  58. meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
  59. meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
  60. meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
  61. meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
  62. meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
  63. meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
  64. meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
  65. meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
  66. meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
  67. meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
  68. meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
  69. meshtensor_cli/src/meshtensor/utils.py +2007 -0
  70. meshtensor_cli/version.py +23 -0
  71. meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
  72. meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
  73. meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
  74. meshtensor_cli-9.18.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,799 @@
1
+ import asyncio
2
+ from collections import defaultdict
3
+ from functools import partial
4
+
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ from async_substrate_interface import AsyncExtrinsicReceipt
8
+ from rich.table import Table
9
+ from rich.prompt import Prompt
10
+
11
+ from meshtensor_cli.src import COLOR_PALETTE
12
+ from meshtensor_cli.src.meshtensor.balances import Balance
13
+ from meshtensor_cli.src.meshtensor.extrinsics.mev_shield import (
14
+ extract_mev_shield_id,
15
+ wait_for_extrinsic_by_hash,
16
+ )
17
+ from meshtensor_cli.src.meshtensor.utils import (
18
+ confirm_action,
19
+ console,
20
+ get_hotkey_wallets_for_wallet,
21
+ is_valid_ss58_address,
22
+ print_error,
23
+ print_success,
24
+ print_verbose,
25
+ unlock_key,
26
+ json_console,
27
+ get_hotkey_pub_ss58,
28
+ print_extrinsic_id,
29
+ )
30
+ from meshtensor_wallet import Wallet
31
+
32
+ if TYPE_CHECKING:
33
+ from meshtensor_cli.src.meshtensor.meshtensor_interface import MeshtensorInterface
34
+
35
+
36
+ # Command
37
+ async def stake_add(
38
+ wallet: Wallet,
39
+ meshtensor: "MeshtensorInterface",
40
+ netuids: Optional[list[int]],
41
+ stake_all: bool,
42
+ amount: float,
43
+ prompt: bool,
44
+ decline: bool,
45
+ quiet: bool,
46
+ all_hotkeys: bool,
47
+ include_hotkeys: list[str],
48
+ exclude_hotkeys: list[str],
49
+ safe_staking: bool,
50
+ rate_tolerance: float,
51
+ allow_partial_stake: bool,
52
+ json_output: bool,
53
+ era: int,
54
+ mev_protection: bool,
55
+ proxy: Optional[str],
56
+ ):
57
+ """
58
+ Args:
59
+ wallet: wallet object
60
+ meshtensor: MeshtensorInterface object
61
+ netuids: the netuids to stake to (None indicates all subnets)
62
+ stake_all: whether to stake all available balance
63
+ amount: specified amount of balance to stake
64
+ prompt: whether to prompt the user
65
+ all_hotkeys: whether to stake all hotkeys
66
+ include_hotkeys: list of hotkeys to include in staking process (if not specifying `--all`)
67
+ exclude_hotkeys: list of hotkeys to exclude in staking (if specifying `--all`)
68
+ safe_staking: whether to use safe staking
69
+ rate_tolerance: rate tolerance percentage for stake operations
70
+ allow_partial_stake: whether to allow partial stake
71
+ json_output: whether to output stake info in JSON format
72
+ era: Blocks for which the transaction should be valid.
73
+ proxy: Optional proxy to use for staking.
74
+ mev_protection: If true, will encrypt the extrinsic behind the mev protection shield.
75
+
76
+ Returns:
77
+ bool: True if stake operation is successful, False otherwise
78
+ """
79
+
80
+ async def get_stake_extrinsic_fee(
81
+ netuid_: int,
82
+ amount_: Balance,
83
+ staking_address_: str,
84
+ safe_staking_: bool,
85
+ price_limit: Optional[Balance] = None,
86
+ ):
87
+ """
88
+ Quick method to get the extrinsic fee for adding stake depending on the args supplied.
89
+ Args:
90
+ netuid_: The netuid where the stake will be added
91
+ amount_: the amount of stake to add
92
+ staking_address_: the hotkey ss58 to stake to
93
+ safe_staking_: whether to use safe staking
94
+ price_limit: rate with tolerance
95
+
96
+ Returns:
97
+ Balance object representing the extrinsic fee for adding stake.
98
+ """
99
+ call_fn = "add_stake" if not safe_staking_ else "add_stake_limit"
100
+ call_params = {
101
+ "hotkey": staking_address_,
102
+ "netuid": netuid_,
103
+ "amount_staked": amount_.meshlet,
104
+ }
105
+ if safe_staking_:
106
+ call_params.update(
107
+ {
108
+ "limit_price": price_limit,
109
+ "allow_partial": allow_partial_stake,
110
+ }
111
+ )
112
+ call = await meshtensor.substrate.compose_call(
113
+ call_module="MeshtensorModule",
114
+ call_function=call_fn,
115
+ call_params=call_params,
116
+ )
117
+ return await meshtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy)
118
+
119
+ async def safe_stake_extrinsic(
120
+ netuid_: int,
121
+ amount_: Balance,
122
+ current_stake: Balance,
123
+ hotkey_ss58_: str,
124
+ price_limit: Balance,
125
+ status_=None,
126
+ ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
127
+ err_out = partial(print_error, status=status_)
128
+ failure_prelude = (
129
+ f":cross_mark: [red]Failed[/red] to stake {amount_} on Netuid {netuid_}"
130
+ )
131
+ current_balance, next_nonce, call = await asyncio.gather(
132
+ meshtensor.get_balance(coldkey_ss58),
133
+ meshtensor.substrate.get_account_next_index(signer_ss58),
134
+ meshtensor.substrate.compose_call(
135
+ call_module="MeshtensorModule",
136
+ call_function="add_stake_limit",
137
+ call_params={
138
+ "hotkey": hotkey_ss58_,
139
+ "netuid": netuid_,
140
+ "amount_staked": amount_.meshlet,
141
+ "limit_price": price_limit.meshlet,
142
+ "allow_partial": allow_partial_stake,
143
+ },
144
+ ),
145
+ )
146
+ success_, err_msg, response = await meshtensor.sign_and_send_extrinsic(
147
+ call=call,
148
+ wallet=wallet,
149
+ nonce=next_nonce,
150
+ era={"period": era},
151
+ proxy=proxy,
152
+ mev_protection=mev_protection,
153
+ )
154
+ if not success_:
155
+ if "Custom error: 8" in err_msg:
156
+ err_msg = (
157
+ f"{failure_prelude}: Price exceeded tolerance limit. "
158
+ f"Transaction rejected because partial staking is disabled. "
159
+ f"Either increase price tolerance or enable partial staking."
160
+ )
161
+ print_error("\n" + err_msg, status=status_)
162
+ else:
163
+ err_msg = f"{failure_prelude} with error: {err_msg}"
164
+ err_out("\n" + err_msg)
165
+ return False, err_msg, None
166
+ else:
167
+ if mev_protection:
168
+ inner_hash = err_msg
169
+ mev_shield_id = await extract_mev_shield_id(response)
170
+ mev_success, mev_error, response = await wait_for_extrinsic_by_hash(
171
+ meshtensor=meshtensor,
172
+ extrinsic_hash=inner_hash,
173
+ shield_id=mev_shield_id,
174
+ submit_block_hash=response.block_hash,
175
+ status=status_,
176
+ )
177
+ if not mev_success:
178
+ status_.stop()
179
+ err_msg = f"{failure_prelude}: {mev_error}"
180
+ err_out("\n" + err_msg)
181
+ return False, err_msg, None
182
+ if json_output:
183
+ # the rest of this checking is not necessary if using json_output
184
+ return True, "", response
185
+ await print_extrinsic_id(response)
186
+ block_hash = await meshtensor.substrate.get_chain_head()
187
+ new_balance, new_stake = await asyncio.gather(
188
+ meshtensor.get_balance(coldkey_ss58, block_hash),
189
+ meshtensor.get_stake(
190
+ hotkey_ss58=hotkey_ss58_,
191
+ coldkey_ss58=coldkey_ss58,
192
+ netuid=netuid_,
193
+ block_hash=block_hash,
194
+ ),
195
+ )
196
+ print_success(
197
+ f"[dark_sea_green3]Finalized. Stake added to netuid: {netuid_}[/dark_sea_green3]"
198
+ )
199
+ console.print(
200
+ f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: "
201
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
202
+ )
203
+
204
+ amount_staked = current_balance - new_balance
205
+ if allow_partial_stake and (amount_staked != amount_):
206
+ console.print(
207
+ "Partial stake transaction. Staked:\n"
208
+ f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}"
209
+ f"[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
210
+ f"instead of "
211
+ f"[blue]{amount_}[/blue]"
212
+ )
213
+
214
+ console.print(
215
+ f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]"
216
+ f"{netuid_}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] "
217
+ f"Stake:\n"
218
+ f" [blue]{current_stake}[/blue] "
219
+ f":arrow_right: "
220
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n"
221
+ )
222
+ return True, "", response
223
+
224
+ async def stake_extrinsic(
225
+ netuid_i, amount_, current, staking_address_ss58, status_=None
226
+ ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
227
+ err_out = partial(print_error, status=status_)
228
+ block_hash = await meshtensor.substrate.get_chain_head()
229
+ current_balance, next_nonce, call = await asyncio.gather(
230
+ meshtensor.get_balance(coldkey_ss58, block_hash=block_hash),
231
+ meshtensor.substrate.get_account_next_index(signer_ss58),
232
+ meshtensor.substrate.compose_call(
233
+ call_module="MeshtensorModule",
234
+ call_function="add_stake",
235
+ call_params={
236
+ "hotkey": staking_address_ss58,
237
+ "netuid": netuid_i,
238
+ "amount_staked": amount_.meshlet,
239
+ },
240
+ block_hash=block_hash,
241
+ ),
242
+ )
243
+ failure_prelude = (
244
+ f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}"
245
+ )
246
+ success_, err_msg, response = await meshtensor.sign_and_send_extrinsic(
247
+ call=call,
248
+ wallet=wallet,
249
+ nonce=next_nonce,
250
+ era={"period": era},
251
+ proxy=proxy,
252
+ mev_protection=mev_protection,
253
+ )
254
+ if not success_:
255
+ err_msg = f"{failure_prelude} with error: {err_msg}"
256
+ err_out("\n" + err_msg)
257
+ return False, err_msg, None
258
+ else:
259
+ if mev_protection:
260
+ inner_hash = err_msg
261
+ mev_shield_id = await extract_mev_shield_id(response)
262
+ mev_success, mev_error, response = await wait_for_extrinsic_by_hash(
263
+ meshtensor=meshtensor,
264
+ extrinsic_hash=inner_hash,
265
+ shield_id=mev_shield_id,
266
+ submit_block_hash=response.block_hash,
267
+ status=status_,
268
+ )
269
+ if not mev_success:
270
+ status_.stop()
271
+ err_msg = f"{failure_prelude}: {mev_error}"
272
+ err_out("\n" + err_msg)
273
+ return False, err_msg, None
274
+ if json_output:
275
+ # the rest of this is not necessary if using json_output
276
+ return True, "", response
277
+ await print_extrinsic_id(response)
278
+ new_block_hash = await meshtensor.substrate.get_chain_head()
279
+ new_balance, new_stake = await asyncio.gather(
280
+ meshtensor.get_balance(
281
+ wallet.coldkeypub.ss58_address, block_hash=new_block_hash
282
+ ),
283
+ meshtensor.get_stake(
284
+ hotkey_ss58=staking_address_ss58,
285
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
286
+ netuid=netuid_i,
287
+ block_hash=new_block_hash,
288
+ ),
289
+ )
290
+ print_success(
291
+ f"[dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]"
292
+ )
293
+ console.print(
294
+ f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: "
295
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}"
296
+ )
297
+ console.print(
298
+ f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]"
299
+ f"{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] "
300
+ f"Stake:\n"
301
+ f" [blue]{current}[/blue] "
302
+ f":arrow_right: "
303
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n"
304
+ )
305
+ return True, "", response
306
+
307
+ netuids = (
308
+ netuids if netuids is not None else await meshtensor.get_all_subnet_netuids()
309
+ )
310
+ coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address
311
+ signer_ss58 = wallet.coldkeypub.ss58_address
312
+
313
+ hotkeys_to_stake_to = _get_hotkeys_to_stake_to(
314
+ wallet=wallet,
315
+ all_hotkeys=all_hotkeys,
316
+ include_hotkeys=include_hotkeys,
317
+ exclude_hotkeys=exclude_hotkeys,
318
+ )
319
+
320
+ # Get subnet data and stake information for coldkey
321
+ chain_head = await meshtensor.substrate.get_chain_head()
322
+ _all_subnets, _stake_info, current_wallet_balance = await asyncio.gather(
323
+ meshtensor.all_subnets(block_hash=chain_head),
324
+ meshtensor.get_stake_for_coldkey(
325
+ coldkey_ss58=coldkey_ss58,
326
+ block_hash=chain_head,
327
+ ),
328
+ meshtensor.get_balance(coldkey_ss58, block_hash=chain_head),
329
+ )
330
+ all_subnets = {di.netuid: di for di in _all_subnets}
331
+
332
+ # Map current stake balances for hotkeys
333
+ hotkey_stake_map = {}
334
+ for _, hotkey_ss58 in hotkeys_to_stake_to:
335
+ hotkey_stake_map[hotkey_ss58] = {}
336
+ for netuid in netuids:
337
+ hotkey_stake_map[hotkey_ss58][netuid] = Balance.from_meshlet(0)
338
+
339
+ for stake_info in _stake_info:
340
+ if stake_info.hotkey_ss58 in hotkey_stake_map:
341
+ hotkey_stake_map[stake_info.hotkey_ss58][stake_info.netuid] = (
342
+ stake_info.stake
343
+ )
344
+
345
+ # Determine the amount we are staking.
346
+ rows = []
347
+ amounts_to_stake = []
348
+ current_stake_balances = []
349
+ prices_with_tolerance = []
350
+ remaining_wallet_balance = current_wallet_balance
351
+ max_slippage = 0.0
352
+
353
+ for hotkey in hotkeys_to_stake_to:
354
+ for netuid in netuids:
355
+ # Check that the subnet exists.
356
+ subnet_info = all_subnets.get(netuid)
357
+ if not subnet_info:
358
+ print_error(f"Subnet with netuid: {netuid} does not exist.")
359
+ continue
360
+ current_stake_balances.append(hotkey_stake_map[hotkey[1]][netuid])
361
+
362
+ # Get the amount.
363
+ amount_to_stake = Balance(0)
364
+ if amount:
365
+ amount_to_stake = Balance.from_tao(amount)
366
+ elif stake_all:
367
+ amount_to_stake = current_wallet_balance / len(netuids)
368
+ elif not amount:
369
+ amount_to_stake, _ = _prompt_stake_amount(
370
+ current_balance=remaining_wallet_balance,
371
+ netuid=netuid,
372
+ action_name="stake",
373
+ )
374
+ amounts_to_stake.append(amount_to_stake)
375
+
376
+ # Check enough to stake.
377
+ if amount_to_stake > remaining_wallet_balance:
378
+ print_error(
379
+ f"Not enough stake:[bold white]\n wallet balance:{remaining_wallet_balance} < "
380
+ f"staking amount: {amount_to_stake}[/bold white]"
381
+ )
382
+ return
383
+ remaining_wallet_balance -= amount_to_stake
384
+
385
+ # Calculate slippage
386
+ # TODO: Update for V3, slippage calculation is significantly different in v3
387
+ # try:
388
+ # received_amount, slippage_pct, slippage_pct_float, rate = (
389
+ # _calculate_slippage(subnet_info, amount_to_stake, stake_fee)
390
+ # )
391
+ # except ValueError:
392
+ # return False
393
+ #
394
+ # max_slippage = max(slippage_pct_float, max_slippage)
395
+
396
+ # Temporary workaround - calculations without slippage
397
+ current_price_float = float(subnet_info.price.tao)
398
+ rate = 1.0 / current_price_float
399
+
400
+ # If we are staking safe, add price tolerance
401
+ if safe_staking:
402
+ if subnet_info.is_dynamic:
403
+ price_with_tolerance = current_price_float * (1 + rate_tolerance)
404
+ _rate_with_tolerance = (
405
+ 1.0 / price_with_tolerance
406
+ ) # Rate only for display
407
+ rate_with_tolerance = f"{_rate_with_tolerance:.4f}"
408
+ price_with_tolerance = Balance.from_tao(
409
+ price_with_tolerance
410
+ ) # Actual price to pass to extrinsic
411
+ else:
412
+ rate_with_tolerance = "1"
413
+ price_with_tolerance = Balance.from_meshlet(1)
414
+ extrinsic_fee = await get_stake_extrinsic_fee(
415
+ netuid_=netuid,
416
+ amount_=amount_to_stake,
417
+ staking_address_=hotkey[1],
418
+ safe_staking_=safe_staking,
419
+ price_limit=price_with_tolerance,
420
+ )
421
+ prices_with_tolerance.append(price_with_tolerance)
422
+ row_extension = [
423
+ f"{rate_with_tolerance} {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ",
424
+ f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]"
425
+ # safe staking
426
+ f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]",
427
+ ]
428
+ else:
429
+ extrinsic_fee = await get_stake_extrinsic_fee(
430
+ netuid_=netuid,
431
+ amount_=amount_to_stake,
432
+ staking_address_=hotkey[1],
433
+ safe_staking_=safe_staking,
434
+ )
435
+ row_extension = []
436
+ # TODO this should be asyncio gathered before the for loop
437
+ amount_minus_fee = (
438
+ (amount_to_stake - extrinsic_fee) if not proxy else amount_to_stake
439
+ )
440
+ sim_swap = await meshtensor.sim_swap(
441
+ origin_netuid=0,
442
+ destination_netuid=netuid,
443
+ amount=amount_minus_fee.meshlet,
444
+ )
445
+ received_amount = sim_swap.alpha_amount
446
+ # Add rows for the table
447
+ base_row = [
448
+ str(netuid), # netuid
449
+ f"{hotkey[1]}", # hotkey
450
+ str(amount_to_stake), # amount
451
+ str(rate)
452
+ + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", # rate
453
+ str(received_amount.set_unit(netuid)), # received
454
+ str(sim_swap.tao_fee), # fee
455
+ str(extrinsic_fee),
456
+ # str(slippage_pct), # slippage
457
+ ] + row_extension
458
+ rows.append(tuple(base_row))
459
+
460
+ # Define and print stake table + slippage warning
461
+ table = _define_stake_table(wallet, meshtensor, safe_staking, rate_tolerance)
462
+ for row in rows:
463
+ table.add_row(*row)
464
+ _print_table_and_slippage(table, max_slippage, safe_staking)
465
+
466
+ if prompt:
467
+ if not confirm_action(
468
+ "Would you like to continue?", decline=decline, quiet=quiet
469
+ ):
470
+ return
471
+ if not unlock_key(wallet).success:
472
+ return
473
+
474
+ successes = defaultdict(dict)
475
+ error_messages = defaultdict(dict)
476
+ extrinsic_ids = defaultdict(dict)
477
+ with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ...") as status:
478
+ if safe_staking:
479
+ stake_coroutines = {}
480
+ for i, (ni, am, curr, price_with_tolerance) in enumerate(
481
+ zip(
482
+ netuids,
483
+ amounts_to_stake,
484
+ current_stake_balances,
485
+ prices_with_tolerance,
486
+ )
487
+ ):
488
+ for _, staking_address in hotkeys_to_stake_to:
489
+ # Regular extrinsic for root subnet
490
+ if ni == 0:
491
+ stake_coroutines[(ni, staking_address)] = stake_extrinsic(
492
+ netuid_i=ni,
493
+ amount_=am,
494
+ current=curr,
495
+ staking_address_ss58=staking_address,
496
+ status_=status,
497
+ )
498
+ else:
499
+ stake_coroutines[(ni, staking_address)] = safe_stake_extrinsic(
500
+ netuid_=ni,
501
+ amount_=am,
502
+ current_stake=curr,
503
+ hotkey_ss58_=staking_address,
504
+ price_limit=price_with_tolerance,
505
+ status_=status,
506
+ )
507
+ else:
508
+ stake_coroutines = {
509
+ (ni, staking_address): stake_extrinsic(
510
+ netuid_i=ni,
511
+ amount_=am,
512
+ current=curr,
513
+ staking_address_ss58=staking_address,
514
+ status_=status,
515
+ )
516
+ for i, (ni, am, curr) in enumerate(
517
+ zip(netuids, amounts_to_stake, current_stake_balances)
518
+ )
519
+ for _, staking_address in hotkeys_to_stake_to
520
+ }
521
+ # We can gather them all at once but balance reporting will be in race-condition.
522
+ for (ni, staking_address), coroutine in stake_coroutines.items():
523
+ success, er_msg, ext_receipt = await coroutine
524
+ successes[ni][staking_address] = success
525
+ error_messages[ni][staking_address] = er_msg
526
+ if success:
527
+ extrinsic_ids[ni][
528
+ staking_address
529
+ ] = await ext_receipt.get_extrinsic_identifier()
530
+ if json_output:
531
+ json_console.print_json(
532
+ data={
533
+ "staking_success": successes,
534
+ "error_messages": error_messages,
535
+ "extrinsic_ids": extrinsic_ids,
536
+ }
537
+ )
538
+
539
+
540
+ # Helper functions
541
+ def _prompt_stake_amount(
542
+ current_balance: Balance, netuid: int, action_name: str
543
+ ) -> tuple[Balance, bool]:
544
+ """Prompts user to input a stake amount with validation.
545
+
546
+ Args:
547
+ current_balance (Balance): The maximum available balance
548
+ netuid (int): The subnet id to get the correct unit
549
+ action_name (str): The name of the action (e.g. "transfer", "move", "unstake")
550
+
551
+ Returns:
552
+ tuple[Balance, bool]: (The amount to use as Balance object, whether all balance was selected)
553
+ """
554
+ while True:
555
+ amount_input = Prompt.ask(
556
+ f"\nEnter the amount to {action_name}"
557
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
558
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}](max: {current_balance})[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
559
+ f"or "
560
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]'all'[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
561
+ f"for entire balance"
562
+ )
563
+
564
+ if amount_input.lower() == "all":
565
+ return current_balance, True
566
+
567
+ try:
568
+ amount = float(amount_input)
569
+ if amount <= 0:
570
+ console.print("[red]Amount must be greater than 0[/red]")
571
+ continue
572
+ if amount > current_balance.tao:
573
+ console.print(
574
+ f"[red]Amount exceeds available balance of "
575
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_balance}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
576
+ f"[/red]"
577
+ )
578
+ continue
579
+ return Balance.from_tao(amount), False
580
+ except ValueError:
581
+ console.print("[red]Please enter a valid number or 'all'[/red]")
582
+ # will never return this, but fixes the type checker
583
+ return Balance(0), False
584
+
585
+
586
+ def _get_hotkeys_to_stake_to(
587
+ wallet: Wallet,
588
+ all_hotkeys: bool = False,
589
+ include_hotkeys: list[str] = None,
590
+ exclude_hotkeys: list[str] = None,
591
+ ) -> list[tuple[Optional[str], str]]:
592
+ """Get list of hotkeys to stake to based on input parameters.
593
+
594
+ Args:
595
+ wallet: The wallet containing hotkeys
596
+ all_hotkeys: If True, get all hotkeys from wallet except excluded ones
597
+ include_hotkeys: List of specific hotkeys to include (by name or ss58 address)
598
+ exclude_hotkeys: List of hotkeys to exclude when all_hotkeys is True
599
+
600
+ Returns:
601
+ List of tuples containing (hotkey_name, hotkey_ss58_address)
602
+ hotkey_name may be None if ss58 address was provided directly
603
+ """
604
+ if all_hotkeys:
605
+ # Stake to all hotkeys except excluded ones
606
+ all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet)
607
+ return [
608
+ (wallet.hotkey_str, get_hotkey_pub_ss58(wallet))
609
+ for wallet in all_hotkeys_
610
+ if wallet.hotkey_str not in (exclude_hotkeys or [])
611
+ ]
612
+
613
+ if include_hotkeys:
614
+ print_verbose("Staking to only included hotkeys")
615
+ # Stake to specific hotkeys
616
+ hotkeys = []
617
+ for hotkey_ss58_or_hotkey_name in include_hotkeys:
618
+ if is_valid_ss58_address(hotkey_ss58_or_hotkey_name):
619
+ # If valid ss58 address, add directly
620
+ hotkeys.append((None, hotkey_ss58_or_hotkey_name))
621
+ else:
622
+ # If hotkey name, get ss58 from wallet
623
+ wallet_ = Wallet(
624
+ path=wallet.path,
625
+ name=wallet.name,
626
+ hotkey=hotkey_ss58_or_hotkey_name,
627
+ )
628
+ hotkeys.append((wallet_.hotkey_str, get_hotkey_pub_ss58(wallet_)))
629
+
630
+ return hotkeys
631
+
632
+ # Default: stake to single hotkey from wallet
633
+ print_verbose(
634
+ f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})"
635
+ )
636
+ assert wallet.hotkey is not None
637
+ return [(None, get_hotkey_pub_ss58(wallet))]
638
+
639
+
640
+ def _define_stake_table(
641
+ wallet: Wallet,
642
+ meshtensor: "MeshtensorInterface",
643
+ safe_staking: bool,
644
+ rate_tolerance: float,
645
+ ) -> Table:
646
+ """Creates and initializes a table for displaying stake information.
647
+
648
+ Args:
649
+ wallet: The wallet being used for staking
650
+ meshtensor: The meshtensor interface
651
+
652
+ Returns:
653
+ Table: An initialized rich Table object with appropriate columns
654
+ """
655
+ table = Table(
656
+ title=f"\n[{COLOR_PALETTE.G.HEADER}]Staking to:\n"
657
+ f"Wallet: [{COLOR_PALETTE.G.CK}]{wallet.name}[/{COLOR_PALETTE.G.CK}], "
658
+ f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n"
659
+ f"Network: {meshtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n",
660
+ show_footer=True,
661
+ show_edge=False,
662
+ header_style="bold white",
663
+ border_style="bright_black",
664
+ style="bold",
665
+ title_justify="center",
666
+ show_lines=False,
667
+ pad_edge=True,
668
+ )
669
+
670
+ table.add_column("Netuid", justify="center", style="grey89")
671
+ table.add_column(
672
+ "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
673
+ )
674
+ table.add_column(
675
+ "Amount (τ)",
676
+ justify="center",
677
+ style=COLOR_PALETTE["POOLS"]["MESH"],
678
+ )
679
+ table.add_column(
680
+ "Rate (per τ)",
681
+ justify="center",
682
+ style=COLOR_PALETTE["POOLS"]["RATE"],
683
+ )
684
+ table.add_column(
685
+ "Est. Received",
686
+ justify="center",
687
+ style=COLOR_PALETTE["POOLS"]["MESH_EQUIV"],
688
+ )
689
+ table.add_column(
690
+ "Fee (τ)",
691
+ justify="center",
692
+ style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"],
693
+ )
694
+ table.add_column(
695
+ "Extrinsic Fee (τ)",
696
+ justify="center",
697
+ style=COLOR_PALETTE.STAKE.MESH,
698
+ )
699
+ # TODO: Uncomment when slippage is reimplemented for v3
700
+ # table.add_column(
701
+ # "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"]
702
+ # )
703
+
704
+ if safe_staking:
705
+ table.add_column(
706
+ f"Rate with tolerance: [blue]({rate_tolerance * 100}%)[/blue]",
707
+ justify="center",
708
+ style=COLOR_PALETTE["POOLS"]["RATE"],
709
+ )
710
+ table.add_column(
711
+ "Partial stake enabled",
712
+ justify="center",
713
+ style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"],
714
+ )
715
+ return table
716
+
717
+
718
+ def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: bool):
719
+ """Prints the stake table, slippage warning, and table description.
720
+
721
+ Args:
722
+ table: The rich Table object to print
723
+ max_slippage: The maximum slippage percentage across all operations
724
+ """
725
+ console.print(table)
726
+
727
+ # Greater than 5%
728
+ if max_slippage > 5:
729
+ message = (
730
+ f"[{COLOR_PALETTE.S.SLIPPAGE_TEXT}]" + ("-" * 115) + "\n"
731
+ f"[bold]WARNING:[/bold] The slippage on one of your operations is high: "
732
+ f"[{COLOR_PALETTE.S.SLIPPAGE_PERCENT}]{max_slippage} %[/{COLOR_PALETTE.S.SLIPPAGE_PERCENT}], "
733
+ f"this may result in a loss of funds.\n" + ("-" * 115) + "\n"
734
+ )
735
+
736
+ console.print(message)
737
+
738
+ # Table description
739
+ base_description = """
740
+ [bold white]Description[/bold white]:
741
+ The table displays information about the stake operation you are about to perform.
742
+ The columns are as follows:
743
+ - [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to.
744
+ - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to.
745
+ - [bold white]Amount[/bold white]: The MESH you are staking into this subnet onto this hotkey.
746
+ - [bold white]Rate[/bold white]: The rate of exchange between your MESH and the subnet's stake.
747
+ - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage."""
748
+ # - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root)."""
749
+
750
+ safe_staking_description = """
751
+ - [bold white]Rate Tolerance[/bold white]: Maximum acceptable alpha rate. If the rate exceeds this tolerance, the transaction will be limited or rejected.
752
+ - [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"""
753
+
754
+ console.print(base_description + (safe_staking_description if safe_staking else ""))
755
+
756
+
757
+ def _calculate_slippage(
758
+ subnet_info, amount: Balance, stake_fee: Balance
759
+ ) -> tuple[Balance, str, float, str]:
760
+ """Calculate slippage when adding stake.
761
+
762
+ Args:
763
+ subnet_info: Subnet dynamic info
764
+ amount: Amount being staked
765
+ stake_fee: Transaction fee for the stake operation
766
+
767
+ Returns:
768
+ tuple containing:
769
+ - received_amount: Amount received after slippage and fees
770
+ - slippage_str: Formatted slippage percentage string
771
+ - slippage_float: Raw slippage percentage value
772
+ - rate: Exchange rate string
773
+
774
+ TODO: Update to v3. This method only works for protocol-liquidity-only
775
+ mode (user liquidity disabled)
776
+ """
777
+ amount_after_fee = amount - stake_fee
778
+
779
+ if amount_after_fee < 0:
780
+ print_error("You don't have enough balance to cover the stake fee.")
781
+ raise ValueError()
782
+
783
+ received_amount, _, _ = subnet_info.tao_to_alpha_with_slippage(amount_after_fee)
784
+
785
+ if subnet_info.is_dynamic:
786
+ ideal_amount = subnet_info.tao_to_alpha(amount)
787
+ total_slippage = ideal_amount - received_amount
788
+ slippage_pct_float = 100 * (total_slippage.tao / ideal_amount.tao)
789
+ slippage_str = f"{slippage_pct_float:.4f} %"
790
+ rate = f"{(1 / subnet_info.price.tao or 1):.4f}"
791
+ else:
792
+ # TODO: Fix this. Slippage is always zero for static networks.
793
+ slippage_pct_float = (
794
+ 100 * float(stake_fee.tao) / float(amount.tao) if amount.tao != 0 else 0
795
+ )
796
+ slippage_str = f"{slippage_pct_float:.4f} %"
797
+ rate = "1"
798
+
799
+ return received_amount, slippage_str, slippage_pct_float, rate