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,1211 @@
1
+ import asyncio
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from meshtensor_wallet import Wallet
7
+ from rich.table import Table
8
+ from rich.prompt import Prompt
9
+
10
+ from meshtensor_cli.src import COLOR_PALETTE
11
+ from meshtensor_cli.src.meshtensor.balances import Balance
12
+ from meshtensor_cli.src.meshtensor.extrinsics.mev_shield import (
13
+ extract_mev_shield_id,
14
+ wait_for_extrinsic_by_hash,
15
+ )
16
+ from meshtensor_cli.src.meshtensor.utils import (
17
+ confirm_action,
18
+ console,
19
+ print_error,
20
+ group_subnets,
21
+ get_subnet_name,
22
+ print_success,
23
+ unlock_key,
24
+ get_hotkey_pub_ss58,
25
+ print_extrinsic_id,
26
+ )
27
+
28
+ if TYPE_CHECKING:
29
+ from meshtensor_cli.src.meshtensor.meshtensor_interface import MeshtensorInterface
30
+ from meshtensor_cli.src.meshtensor.chain_data import DynamicInfo
31
+
32
+ MIN_STAKE_FEE = Balance.from_meshlet(50_000)
33
+
34
+
35
+ # Helpers
36
+ @dataclass(frozen=True)
37
+ class MovementPricing:
38
+ origin_subnet: "DynamicInfo"
39
+ destination_subnet: "DynamicInfo"
40
+ rate: float
41
+ rate_with_tolerance: Optional[float]
42
+
43
+
44
+ async def get_movement_pricing(
45
+ meshtensor: "MeshtensorInterface",
46
+ origin_netuid: int,
47
+ destination_netuid: int,
48
+ safe_staking: bool = False,
49
+ rate_tolerance: Optional[float] = None,
50
+ ) -> MovementPricing:
51
+ """
52
+ Returns pricing information for stake movement commands based on the origin and destination subnets.
53
+
54
+ Args:
55
+ meshtensor: MeshtensorInterface instance.
56
+ origin_netuid: The netuid of the origin subnet.
57
+ destination_netuid: The netuid of the destination subnet.
58
+ safe_staking: Whether to enable safe staking with slippage protection.
59
+ rate_tolerance: The accepted rate tolerance (slippage) for safe staking.
60
+
61
+ Returns:
62
+ MovementPricing: Object containing pricing details like rates and limits.
63
+ """
64
+ if origin_netuid == destination_netuid:
65
+ subnet = await meshtensor.subnet(origin_netuid)
66
+ return MovementPricing(
67
+ origin_subnet=subnet,
68
+ destination_subnet=subnet,
69
+ rate=1.0,
70
+ rate_with_tolerance=1.0 if safe_staking else None,
71
+ )
72
+
73
+ origin_subnet, destination_subnet = await asyncio.gather(
74
+ meshtensor.subnet(origin_netuid),
75
+ meshtensor.subnet(destination_netuid),
76
+ )
77
+ price_origin = origin_subnet.price.tao
78
+ price_destination = destination_subnet.price.tao
79
+ rate = price_origin / (price_destination or 1)
80
+ rate_with_tolerance = None
81
+ if safe_staking:
82
+ limit_rate = rate * (1 - rate_tolerance)
83
+ rate_with_tolerance = limit_rate
84
+
85
+ return MovementPricing(
86
+ origin_subnet=origin_subnet,
87
+ destination_subnet=destination_subnet,
88
+ rate=rate,
89
+ rate_with_tolerance=rate_with_tolerance,
90
+ )
91
+
92
+
93
+ async def display_stake_movement_cross_subnets(
94
+ meshtensor: "MeshtensorInterface",
95
+ origin_netuid: int,
96
+ destination_netuid: int,
97
+ origin_hotkey: str,
98
+ destination_hotkey: str,
99
+ amount_to_move: Balance,
100
+ pricing: MovementPricing,
101
+ stake_fee: Balance,
102
+ extrinsic_fee: Balance,
103
+ safe_staking: bool = False,
104
+ rate_tolerance: Optional[float] = None,
105
+ allow_partial_stake: bool = False,
106
+ proxy: Optional[str] = None,
107
+ ) -> tuple[Balance, str]:
108
+ """Calculate and display stake movement information.
109
+
110
+ Args:
111
+ meshtensor: MeshtensorInterface instance.
112
+ origin_netuid: The netuid of the origin subnet.
113
+ destination_netuid: The netuid of the destination subnet.
114
+ origin_hotkey: The origin hotkey SS58 address.
115
+ destination_hotkey: The destination hotkey SS58 address.
116
+ amount_to_move: The amount of stake to move/swap.
117
+ pricing: Pricing information including rates and limits.
118
+ stake_fee: The fee for the stake transaction.
119
+ extrinsic_fee: The fee for the extrinsic execution.
120
+ safe_staking: Whether to enable safe staking.
121
+ rate_tolerance: The accepted rate tolerance.
122
+ allow_partial_stake: Whether to allow partial execution if the full amount cannot be staked within limits.
123
+ proxy: Optional proxy address.
124
+
125
+ Returns:
126
+ tuple[Balance, str]: The estimated amount received and the formatted price string.
127
+ """
128
+
129
+ if origin_netuid == destination_netuid:
130
+ subnet = pricing.origin_subnet
131
+ received_amount_tao = subnet.alpha_to_tao(amount_to_move - stake_fee)
132
+ if not proxy:
133
+ received_amount_tao -= extrinsic_fee
134
+ received_amount = subnet.tao_to_alpha(received_amount_tao)
135
+
136
+ if received_amount < Balance.from_tao(0).set_unit(destination_netuid):
137
+ print_error(
138
+ f"Not enough Alpha to pay the transaction fee. The fee is {stake_fee}, "
139
+ f"which would set the total received to {received_amount}."
140
+ )
141
+ raise ValueError
142
+
143
+ price = subnet.price.tao
144
+ price_str = (
145
+ str(float(price))
146
+ + f"({Balance.get_unit(0)}/{Balance.get_unit(origin_netuid)})"
147
+ )
148
+ else:
149
+ dynamic_origin = pricing.origin_subnet
150
+ dynamic_destination = pricing.destination_subnet
151
+ received_amount_tao = (
152
+ dynamic_origin.alpha_to_tao(amount_to_move - stake_fee) - extrinsic_fee
153
+ )
154
+ received_amount = dynamic_destination.tao_to_alpha(received_amount_tao)
155
+ received_amount.set_unit(destination_netuid)
156
+
157
+ if received_amount < Balance.from_tao(0).set_unit(destination_netuid):
158
+ print_error(
159
+ f"Not enough Alpha to pay the transaction fee. The fee is {stake_fee}, "
160
+ f"which would set the total received to {received_amount}."
161
+ )
162
+ raise ValueError
163
+
164
+ price_str = (
165
+ f"{pricing.rate:.5f}"
166
+ + f"({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})"
167
+ )
168
+
169
+ # Create and display table
170
+ table = Table(
171
+ title=(
172
+ f"\n[{COLOR_PALETTE.G.HEADER}]"
173
+ f"Moving stake from: "
174
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})"
175
+ f"[/{COLOR_PALETTE.G.SUBHEAD}] "
176
+ f"to: "
177
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})"
178
+ f"[/{COLOR_PALETTE.G.SUBHEAD}]\nNetwork: {meshtensor.network}\n"
179
+ f"[/{COLOR_PALETTE.G.HEADER}]"
180
+ ),
181
+ show_footer=True,
182
+ show_edge=False,
183
+ header_style="bold white",
184
+ border_style="bright_black",
185
+ style="bold",
186
+ title_justify="center",
187
+ show_lines=False,
188
+ pad_edge=True,
189
+ )
190
+
191
+ table.add_column(
192
+ "origin netuid",
193
+ justify="center",
194
+ style=COLOR_PALETTE["GENERAL"]["SYMBOL"],
195
+ max_width=14,
196
+ )
197
+ table.add_column(
198
+ "origin hotkey",
199
+ justify="center",
200
+ style=COLOR_PALETTE["GENERAL"]["HOTKEY"],
201
+ max_width=15,
202
+ )
203
+ table.add_column(
204
+ "dest netuid",
205
+ justify="center",
206
+ style=COLOR_PALETTE["GENERAL"]["SYMBOL"],
207
+ max_width=12,
208
+ )
209
+ table.add_column(
210
+ "dest hotkey",
211
+ justify="center",
212
+ style=COLOR_PALETTE["GENERAL"]["HOTKEY"],
213
+ max_width=15,
214
+ )
215
+ table.add_column(
216
+ f"amount ({Balance.get_unit(origin_netuid)})",
217
+ justify="center",
218
+ style=COLOR_PALETTE["STAKE"]["MESH"],
219
+ max_width=18,
220
+ )
221
+ table.add_column(
222
+ f"rate ({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})",
223
+ justify="center",
224
+ style=COLOR_PALETTE["POOLS"]["RATE"],
225
+ max_width=20,
226
+ )
227
+ table.add_column(
228
+ f"received ({Balance.get_unit(destination_netuid)})",
229
+ justify="center",
230
+ style=COLOR_PALETTE["POOLS"]["MESH_EQUIV"],
231
+ max_width=18,
232
+ )
233
+ table.add_column(
234
+ f"Fee ({Balance.get_unit(origin_netuid)})",
235
+ justify="center",
236
+ style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"],
237
+ max_width=15,
238
+ )
239
+ table.add_column(
240
+ "Extrinsic Fee (τ)",
241
+ justify="center",
242
+ style=COLOR_PALETTE.STAKE.MESH,
243
+ max_width=18,
244
+ )
245
+ if safe_staking:
246
+ table.add_column(
247
+ f"Rate with tolerance: [blue]({rate_tolerance * 100}%)[/blue]",
248
+ justify="center",
249
+ style=COLOR_PALETTE["POOLS"]["RATE"],
250
+ )
251
+ table.add_column(
252
+ "Partial stake enabled",
253
+ justify="center",
254
+ style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"],
255
+ )
256
+
257
+ row = [
258
+ f"{Balance.get_unit(origin_netuid)}({origin_netuid})",
259
+ f"{origin_hotkey[:3]}...{origin_hotkey[-3:]}",
260
+ f"{Balance.get_unit(destination_netuid)}({destination_netuid})",
261
+ f"{destination_hotkey[:3]}...{destination_hotkey[-3:]}",
262
+ str(amount_to_move),
263
+ price_str,
264
+ str(received_amount),
265
+ str(stake_fee.set_unit(origin_netuid)),
266
+ str(extrinsic_fee),
267
+ ]
268
+ if safe_staking:
269
+ rate_with_tolerance_str = (
270
+ f"{pricing.rate_with_tolerance:.5f}"
271
+ + f"({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})"
272
+ )
273
+ row.extend(
274
+ [
275
+ rate_with_tolerance_str,
276
+ "Yes" if allow_partial_stake else "No",
277
+ ]
278
+ )
279
+ table.add_row(*row)
280
+
281
+ console.print(table)
282
+
283
+ return received_amount, price_str
284
+
285
+
286
+ def prompt_stake_amount(
287
+ current_balance: Balance, netuid: int, action_name: str
288
+ ) -> tuple[Balance, bool]:
289
+ """Prompts user to input a stake amount with validation.
290
+
291
+ Args:
292
+ current_balance (Balance): The maximum available balance
293
+ netuid (int): The subnet id to get the correct unit
294
+ action_name (str): The name of the action (e.g. "transfer", "move", "unstake")
295
+
296
+ Returns:
297
+ tuple[Balance, bool]: (The amount to use as Balance object, whether all balance was selected)
298
+ """
299
+ while True:
300
+ amount_input = Prompt.ask(
301
+ f"\nEnter the amount to {action_name} "
302
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
303
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}](max: {current_balance})[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
304
+ f"or "
305
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]'all'[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
306
+ f"for entire balance"
307
+ )
308
+
309
+ if amount_input.lower() == "all":
310
+ return current_balance, True
311
+
312
+ try:
313
+ amount = float(amount_input)
314
+ if amount <= 0:
315
+ console.print("[red]Amount must be greater than 0[/red]")
316
+ continue
317
+ if amount > current_balance.tao:
318
+ console.print(
319
+ f"[red]Amount exceeds available balance of "
320
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_balance}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
321
+ f"[/red]"
322
+ )
323
+ continue
324
+ return Balance.from_tao(amount), False
325
+ except ValueError:
326
+ console.print("[red]Please enter a valid number or 'all'[/red]")
327
+ # can never return this, but fixes the type checker
328
+ return Balance(0), False
329
+
330
+
331
+ async def stake_move_transfer_selection(
332
+ meshtensor: "MeshtensorInterface",
333
+ wallet: Wallet,
334
+ ):
335
+ """Selection interface for moving stakes between hotkeys and subnets."""
336
+ stakes, ck_hk_identities, old_identities = await asyncio.gather(
337
+ meshtensor.get_stake_for_coldkey(coldkey_ss58=wallet.coldkeypub.ss58_address),
338
+ meshtensor.fetch_coldkey_hotkey_identities(),
339
+ meshtensor.get_delegate_identities(),
340
+ )
341
+
342
+ hotkey_stakes = {}
343
+ for stake in stakes:
344
+ if stake.stake.tao > 0:
345
+ hotkey = stake.hotkey_ss58
346
+ netuid = stake.netuid
347
+ stake_balance = stake.stake
348
+ hotkey_stakes.setdefault(hotkey, {})[netuid] = stake_balance
349
+
350
+ if not hotkey_stakes:
351
+ print_error("You have no stakes to move.")
352
+ raise ValueError
353
+
354
+ # Display hotkeys with stakes
355
+ table = Table(
356
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes\n",
357
+ show_footer=True,
358
+ show_edge=False,
359
+ header_style="bold white",
360
+ border_style="bright_black",
361
+ style="bold",
362
+ title_justify="center",
363
+ show_lines=False,
364
+ pad_edge=True,
365
+ )
366
+ table.add_column("Index", justify="right")
367
+ table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"])
368
+ table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"])
369
+ table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"])
370
+
371
+ hotkeys_info = []
372
+ for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()):
373
+ if hk_identity := ck_hk_identities["hotkeys"].get(hotkey_ss58):
374
+ hotkey_name = hk_identity.get("identity", {}).get(
375
+ "name", ""
376
+ ) or hk_identity.get("display", "~")
377
+ elif old_identity := old_identities.get(hotkey_ss58):
378
+ hotkey_name = old_identity.display
379
+ else:
380
+ hotkey_name = "~"
381
+ hotkeys_info.append(
382
+ {
383
+ "index": idx,
384
+ "identity": hotkey_name,
385
+ "hotkey_ss58": hotkey_ss58,
386
+ "netuids": list(netuid_stakes.keys()),
387
+ "stakes": netuid_stakes,
388
+ }
389
+ )
390
+ table.add_row(
391
+ str(idx),
392
+ hotkey_name,
393
+ group_subnets([n for n in netuid_stakes.keys()]),
394
+ hotkey_ss58,
395
+ )
396
+
397
+ console.print("\n", table)
398
+
399
+ # Select origin hotkey
400
+ origin_idx = Prompt.ask(
401
+ "\nEnter the index of the hotkey you want to move stake from",
402
+ choices=[str(i) for i in range(len(hotkeys_info))],
403
+ )
404
+ origin_hotkey_info = hotkeys_info[int(origin_idx)]
405
+ origin_hotkey_ss58 = origin_hotkey_info["hotkey_ss58"]
406
+
407
+ # Display available netuids for selected hotkey
408
+ table = Table(
409
+ title=f"\n[{COLOR_PALETTE.G.HEADER}]Available Stakes for Hotkey\n[/{COLOR_PALETTE.G.HEADER}]"
410
+ f"[{COLOR_PALETTE.G.HK}]{origin_hotkey_ss58}[/{COLOR_PALETTE.G.HK}]\n",
411
+ show_edge=False,
412
+ header_style="bold white",
413
+ border_style="bright_black",
414
+ title_justify="center",
415
+ width=len(origin_hotkey_ss58) + 20,
416
+ )
417
+ table.add_column("Netuid", style="cyan")
418
+ table.add_column("Stake Amount", style=COLOR_PALETTE.STAKE.STAKE_AMOUNT)
419
+
420
+ available_netuids = []
421
+ for netuid in origin_hotkey_info["netuids"]:
422
+ stake = origin_hotkey_info["stakes"][netuid]
423
+ if stake.tao > 0:
424
+ available_netuids.append(netuid)
425
+ table.add_row(str(netuid), str(stake))
426
+
427
+ console.print("\n", table)
428
+
429
+ # Select origin netuid
430
+ origin_netuid = Prompt.ask(
431
+ "\nEnter the netuid you want to move stake from",
432
+ choices=[str(netuid) for netuid in available_netuids],
433
+ )
434
+ origin_netuid = int(origin_netuid)
435
+ origin_stake = origin_hotkey_info["stakes"][origin_netuid]
436
+
437
+ # Ask for amount to move
438
+ amount, stake_all = prompt_stake_amount(origin_stake, origin_netuid, "move")
439
+
440
+ all_subnets = sorted(await meshtensor.get_all_subnet_netuids())
441
+ destination_netuid = Prompt.ask(
442
+ "\nEnter the netuid of the subnet you want to move stake to"
443
+ + f" ([dim]{group_subnets(all_subnets)}[/dim])",
444
+ choices=[str(netuid) for netuid in all_subnets],
445
+ show_choices=False,
446
+ )
447
+
448
+ return {
449
+ "origin_hotkey": origin_hotkey_ss58,
450
+ "origin_netuid": origin_netuid,
451
+ "amount": amount.tao,
452
+ "stake_all": stake_all,
453
+ "destination_netuid": int(destination_netuid),
454
+ }
455
+
456
+
457
+ async def stake_swap_selection(
458
+ meshtensor: "MeshtensorInterface",
459
+ wallet: Wallet,
460
+ ) -> dict:
461
+ """Selection interface for swapping stakes between subnets."""
462
+ block_hash = await meshtensor.substrate.get_chain_head()
463
+ stakes, all_subnets = await asyncio.gather(
464
+ meshtensor.get_stake_for_coldkey(
465
+ coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=block_hash
466
+ ),
467
+ meshtensor.all_subnets(block_hash=block_hash),
468
+ )
469
+ subnet_dict = {di.netuid: di for di in all_subnets}
470
+
471
+ # Filter stakes for this hotkey
472
+ hotkey_stakes = {}
473
+ hotkey_ss58 = get_hotkey_pub_ss58(wallet)
474
+ for stake in stakes:
475
+ if stake.hotkey_ss58 == hotkey_ss58 and stake.stake.tao > 0:
476
+ hotkey_stakes[stake.netuid] = {
477
+ "stake": stake.stake,
478
+ "is_registered": stake.is_registered,
479
+ }
480
+
481
+ if not hotkey_stakes:
482
+ print_error(f"No stakes found for hotkey: {wallet.hotkey_str}")
483
+ raise ValueError
484
+
485
+ # Display available stakes
486
+ table = Table(
487
+ title=f"\n[{COLOR_PALETTE.G.HEADER}]Available Stakes for Hotkey\n[/{COLOR_PALETTE.G.HEADER}]"
488
+ f"[{COLOR_PALETTE.G.HK}]{wallet.hotkey_str}: {hotkey_ss58}[/{COLOR_PALETTE.G.HK}]\n",
489
+ show_edge=False,
490
+ header_style="bold white",
491
+ border_style="bright_black",
492
+ title_justify="center",
493
+ width=len(hotkey_ss58) + 20,
494
+ )
495
+
496
+ table.add_column("Netuid", style=COLOR_PALETTE["GENERAL"]["NETUID"])
497
+ table.add_column("Name", style="cyan", justify="left")
498
+ table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"])
499
+ table.add_column("Registered", justify="center")
500
+
501
+ available_netuids = []
502
+ for netuid, stake_info in sorted(hotkey_stakes.items()):
503
+ subnet_info = subnet_dict[netuid]
504
+ subnet_name_cell = (
505
+ f"[{COLOR_PALETTE.G.SYM}]{subnet_info.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE.G.SYM}]"
506
+ f" {get_subnet_name(subnet_info)}"
507
+ )
508
+
509
+ available_netuids.append(netuid)
510
+ table.add_row(
511
+ str(netuid),
512
+ subnet_name_cell,
513
+ str(stake_info["stake"]),
514
+ "[dark_sea_green3]YES"
515
+ if stake_info["is_registered"]
516
+ else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO",
517
+ )
518
+
519
+ console.print("\n", table)
520
+
521
+ # Select origin netuid
522
+ origin_netuid = Prompt.ask(
523
+ "\nEnter the netuid of the subnet you want to swap stake from"
524
+ + f" ([dim]{group_subnets(sorted(available_netuids))}[/dim])",
525
+ choices=[str(netuid) for netuid in available_netuids],
526
+ show_choices=False,
527
+ )
528
+ origin_netuid = int(origin_netuid)
529
+ origin_stake = hotkey_stakes[origin_netuid]["stake"]
530
+
531
+ # Ask for amount to swap
532
+ amount, _ = prompt_stake_amount(origin_stake, origin_netuid, "swap")
533
+
534
+ all_netuids = sorted(await meshtensor.get_all_subnet_netuids())
535
+ destination_netuids = [netuid for netuid in all_netuids if netuid != origin_netuid]
536
+ destination_choices = [str(netuid) for netuid in destination_netuids]
537
+ destination_netuid = Prompt.ask(
538
+ "\nEnter the netuid of the subnet you want to swap stake to"
539
+ + f" ([dim]{group_subnets(destination_netuids)}[/dim])",
540
+ choices=destination_choices,
541
+ show_choices=False,
542
+ )
543
+
544
+ return {
545
+ "origin_netuid": origin_netuid,
546
+ "amount": amount.tao,
547
+ "destination_netuid": int(destination_netuid),
548
+ }
549
+
550
+
551
+ # Commands
552
+ async def move_stake(
553
+ meshtensor: "MeshtensorInterface",
554
+ wallet: Wallet,
555
+ origin_netuid: int,
556
+ origin_hotkey: str,
557
+ destination_netuid: int,
558
+ destination_hotkey: str,
559
+ amount: float,
560
+ stake_all: bool,
561
+ era: int,
562
+ interactive_selection: bool = False,
563
+ prompt: bool = True,
564
+ decline: bool = False,
565
+ quiet: bool = False,
566
+ proxy: Optional[str] = None,
567
+ mev_protection: bool = True,
568
+ ) -> tuple[bool, str]:
569
+ if interactive_selection:
570
+ try:
571
+ selection = await stake_move_transfer_selection(meshtensor, wallet)
572
+ except ValueError:
573
+ return False, ""
574
+ origin_hotkey = selection["origin_hotkey"]
575
+ origin_netuid = selection["origin_netuid"]
576
+ amount = selection["amount"]
577
+ stake_all = selection["stake_all"]
578
+ destination_netuid = selection["destination_netuid"]
579
+
580
+ # Get the wallet stake balances.
581
+ block_hash = await meshtensor.substrate.get_chain_head()
582
+ # TODO should this use `proxy if proxy else wallet.coldkeypub.ss58_address`?
583
+ origin_stake_balance, destination_stake_balance = await asyncio.gather(
584
+ meshtensor.get_stake(
585
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
586
+ hotkey_ss58=origin_hotkey,
587
+ netuid=origin_netuid,
588
+ block_hash=block_hash,
589
+ ),
590
+ meshtensor.get_stake(
591
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
592
+ hotkey_ss58=destination_hotkey,
593
+ netuid=destination_netuid,
594
+ block_hash=block_hash,
595
+ ),
596
+ )
597
+
598
+ if origin_stake_balance.tao == 0:
599
+ print_error(
600
+ f"Your balance is "
601
+ f"[{COLOR_PALETTE.POOLS.MESH}]0[/{COLOR_PALETTE.POOLS.MESH}] "
602
+ f"in Netuid: "
603
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{origin_netuid}[/{COLOR_PALETTE.G.SUBHEAD}]"
604
+ )
605
+ return False, ""
606
+
607
+ console.print(
608
+ f"\nOrigin Netuid: "
609
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{origin_netuid}[/{COLOR_PALETTE.G.SUBHEAD}], "
610
+ f"Origin stake: "
611
+ f"[{COLOR_PALETTE.POOLS.MESH}]{origin_stake_balance}[/{COLOR_PALETTE.POOLS.MESH}]"
612
+ )
613
+ console.print(
614
+ f"Destination netuid: "
615
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{destination_netuid}[/{COLOR_PALETTE.G.SUBHEAD}], "
616
+ f"Destination stake: "
617
+ f"[{COLOR_PALETTE.POOLS.MESH}]{destination_stake_balance}[/{COLOR_PALETTE.POOLS.MESH}]\n"
618
+ )
619
+
620
+ # Determine the amount we are moving.
621
+ if amount:
622
+ amount_to_move_as_balance = Balance.from_tao(amount)
623
+ elif stake_all:
624
+ amount_to_move_as_balance = origin_stake_balance
625
+ else:
626
+ amount_to_move_as_balance, _ = prompt_stake_amount(
627
+ origin_stake_balance, origin_netuid, "move"
628
+ )
629
+
630
+ # Check enough to move.
631
+ amount_to_move_as_balance.set_unit(origin_netuid)
632
+ if amount_to_move_as_balance > origin_stake_balance:
633
+ print_error(
634
+ f"Not enough stake:\n"
635
+ f" Stake balance: [{COLOR_PALETTE.S.AMOUNT}]{origin_stake_balance}[/{COLOR_PALETTE.S.AMOUNT}]"
636
+ f" < Moving amount: [{COLOR_PALETTE.S.AMOUNT}]{amount_to_move_as_balance}[/{COLOR_PALETTE.S.AMOUNT}]"
637
+ )
638
+ return False, ""
639
+
640
+ call = await meshtensor.substrate.compose_call(
641
+ call_module="MeshtensorModule",
642
+ call_function="move_stake",
643
+ call_params={
644
+ "origin_hotkey": origin_hotkey,
645
+ "origin_netuid": origin_netuid,
646
+ "destination_hotkey": destination_hotkey,
647
+ "destination_netuid": destination_netuid,
648
+ "alpha_amount": amount_to_move_as_balance.meshlet,
649
+ },
650
+ )
651
+ pricing, sim_swap, extrinsic_fee, next_nonce = await asyncio.gather(
652
+ get_movement_pricing(
653
+ meshtensor=meshtensor,
654
+ origin_netuid=origin_netuid,
655
+ destination_netuid=destination_netuid,
656
+ ),
657
+ meshtensor.sim_swap(
658
+ origin_netuid=origin_netuid,
659
+ destination_netuid=destination_netuid,
660
+ amount=amount_to_move_as_balance.meshlet,
661
+ ),
662
+ meshtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy),
663
+ # TODO verify if this should be proxy or signer
664
+ meshtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address),
665
+ )
666
+
667
+ # Display stake movement details
668
+ if prompt:
669
+ try:
670
+ await display_stake_movement_cross_subnets(
671
+ meshtensor=meshtensor,
672
+ origin_netuid=origin_netuid,
673
+ destination_netuid=destination_netuid,
674
+ origin_hotkey=origin_hotkey,
675
+ destination_hotkey=destination_hotkey,
676
+ amount_to_move=amount_to_move_as_balance,
677
+ pricing=pricing,
678
+ stake_fee=sim_swap.alpha_fee
679
+ if origin_netuid != 0
680
+ else sim_swap.tao_fee,
681
+ extrinsic_fee=extrinsic_fee,
682
+ proxy=proxy,
683
+ )
684
+ except ValueError:
685
+ return False, ""
686
+ if not confirm_action(
687
+ "Would you like to continue?", decline=decline, quiet=quiet
688
+ ):
689
+ return False, ""
690
+
691
+ # Perform moving operation.
692
+ if not unlock_key(wallet).success:
693
+ return False, ""
694
+ with console.status(
695
+ f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: "
696
+ f"[blue]{origin_netuid}[/blue] \nto "
697
+ f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..."
698
+ ) as status:
699
+ success_, err_msg, response = await meshtensor.sign_and_send_extrinsic(
700
+ call=call,
701
+ wallet=wallet,
702
+ era={"period": era},
703
+ proxy=proxy,
704
+ mev_protection=mev_protection,
705
+ nonce=next_nonce,
706
+ )
707
+
708
+ ext_id = await response.get_extrinsic_identifier() if response else ""
709
+ if success_:
710
+ if mev_protection:
711
+ inner_hash = err_msg
712
+ mev_shield_id = await extract_mev_shield_id(response)
713
+ mev_success, mev_error, response = await wait_for_extrinsic_by_hash(
714
+ meshtensor=meshtensor,
715
+ extrinsic_hash=inner_hash,
716
+ shield_id=mev_shield_id,
717
+ submit_block_hash=response.block_hash,
718
+ status=status,
719
+ )
720
+ if not mev_success:
721
+ status.stop()
722
+ print_error(f"\nFailed: {mev_error}")
723
+ return False, ""
724
+ await print_extrinsic_id(response)
725
+ if not prompt:
726
+ print_success("Sent")
727
+ return True, ext_id
728
+ else:
729
+ print_success("[dark_sea_green3]Stake moved.[/dark_sea_green3]")
730
+ block_hash = await meshtensor.substrate.get_chain_head()
731
+ (
732
+ new_origin_stake_balance,
733
+ new_destination_stake_balance,
734
+ ) = await asyncio.gather(
735
+ meshtensor.get_stake(
736
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
737
+ hotkey_ss58=origin_hotkey,
738
+ netuid=origin_netuid,
739
+ block_hash=block_hash,
740
+ ),
741
+ meshtensor.get_stake(
742
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
743
+ hotkey_ss58=destination_hotkey,
744
+ netuid=destination_netuid,
745
+ block_hash=block_hash,
746
+ ),
747
+ )
748
+
749
+ console.print(
750
+ f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: "
751
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_origin_stake_balance}"
752
+ )
753
+ console.print(
754
+ f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: "
755
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}"
756
+ )
757
+ return True, ext_id
758
+ else:
759
+ print_error(f"\nFailed with error: {err_msg}")
760
+ return False, ""
761
+
762
+
763
+ async def transfer_stake(
764
+ wallet: Wallet,
765
+ meshtensor: "MeshtensorInterface",
766
+ amount: float,
767
+ origin_hotkey: str,
768
+ origin_netuid: int,
769
+ dest_netuid: int,
770
+ dest_coldkey_ss58: str,
771
+ era: int,
772
+ interactive_selection: bool = False,
773
+ stake_all: bool = False,
774
+ prompt: bool = True,
775
+ decline: bool = False,
776
+ quiet: bool = False,
777
+ proxy: Optional[str] = None,
778
+ mev_protection: bool = True,
779
+ ) -> tuple[bool, str]:
780
+ """Transfers stake from one network to another.
781
+
782
+ Args:
783
+ wallet: Meshtensor wallet object.
784
+ meshtensor: Meshtensor interface instance.
785
+ amount: Amount to transfer.
786
+ origin_hotkey: The hotkey SS58 to transfer the stake from.
787
+ origin_netuid: The netuid to transfer stake from.
788
+ dest_netuid: The netuid to transfer stake to.
789
+ dest_coldkey_ss58: The destination coldkey to transfer stake to.
790
+ interactive_selection: If true, prompts for selection of origin and destination subnets.
791
+ prompt: If true, prompts for confirmation before executing transfer.
792
+ era: number of blocks for which the extrinsic should be valid
793
+ stake_all: If true, transfer all stakes.
794
+ proxy: Optional proxy to use for this extrinsic
795
+ mev_protection: If true, will encrypt the extrinsic behind the mev protection shield.
796
+
797
+ Returns:
798
+ tuple:
799
+ bool: True if transfer was successful, False otherwise.
800
+ str: error message
801
+ """
802
+ if interactive_selection:
803
+ selection = await stake_move_transfer_selection(meshtensor, wallet)
804
+ origin_netuid = selection["origin_netuid"]
805
+ amount = selection["amount"]
806
+ dest_netuid = selection["destination_netuid"]
807
+ stake_all = selection["stake_all"]
808
+ origin_hotkey = selection["origin_hotkey"]
809
+
810
+ # Check if both subnets exist
811
+ block_hash = await meshtensor.substrate.get_chain_head()
812
+ dest_exists, origin_exists = await asyncio.gather(
813
+ meshtensor.subnet_exists(netuid=dest_netuid, block_hash=block_hash),
814
+ meshtensor.subnet_exists(netuid=origin_netuid, block_hash=block_hash),
815
+ )
816
+ if not dest_exists:
817
+ print_error(f"Subnet {dest_netuid} does not exist")
818
+ return False, ""
819
+
820
+ if not origin_exists:
821
+ print_error(f"Subnet {origin_netuid} does not exist")
822
+ return False, ""
823
+
824
+ # Get current stake balances
825
+ with console.status(f"Retrieving stake data from {meshtensor.network}..."):
826
+ # TODO should use proxy for these checks?
827
+ current_stake = await meshtensor.get_stake(
828
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
829
+ hotkey_ss58=origin_hotkey,
830
+ netuid=origin_netuid,
831
+ )
832
+ current_dest_stake = await meshtensor.get_stake(
833
+ coldkey_ss58=dest_coldkey_ss58,
834
+ hotkey_ss58=origin_hotkey,
835
+ netuid=dest_netuid,
836
+ )
837
+
838
+ if current_stake.tao == 0:
839
+ print_error(
840
+ f"No stake found for hotkey: {origin_hotkey} on netuid: {origin_netuid}"
841
+ )
842
+ return False, ""
843
+
844
+ if amount:
845
+ amount_to_transfer = Balance.from_tao(amount).set_unit(origin_netuid)
846
+ elif stake_all:
847
+ amount_to_transfer = current_stake
848
+ else:
849
+ amount_to_transfer, _ = prompt_stake_amount(
850
+ current_stake, origin_netuid, "transfer"
851
+ )
852
+
853
+ # Check if enough stake to transfer
854
+ if amount_to_transfer > current_stake:
855
+ print_error(
856
+ f"Not enough stake to transfer:\n"
857
+ f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < "
858
+ f"Transfer amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_transfer}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
859
+ )
860
+ return False, ""
861
+
862
+ call = await meshtensor.substrate.compose_call(
863
+ call_module="MeshtensorModule",
864
+ call_function="transfer_stake",
865
+ call_params={
866
+ "destination_coldkey": dest_coldkey_ss58,
867
+ "hotkey": origin_hotkey,
868
+ "origin_netuid": origin_netuid,
869
+ "destination_netuid": dest_netuid,
870
+ "alpha_amount": amount_to_transfer.meshlet,
871
+ },
872
+ )
873
+ pricing, sim_swap, extrinsic_fee, next_nonce = await asyncio.gather(
874
+ get_movement_pricing(
875
+ meshtensor=meshtensor,
876
+ origin_netuid=origin_netuid,
877
+ destination_netuid=dest_netuid,
878
+ ),
879
+ meshtensor.sim_swap(
880
+ origin_netuid=origin_netuid,
881
+ destination_netuid=dest_netuid,
882
+ amount=amount_to_transfer.meshlet,
883
+ ),
884
+ meshtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy),
885
+ meshtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address),
886
+ )
887
+
888
+ # Display stake movement details
889
+ if prompt:
890
+ try:
891
+ await display_stake_movement_cross_subnets(
892
+ meshtensor=meshtensor,
893
+ origin_netuid=origin_netuid,
894
+ destination_netuid=dest_netuid,
895
+ origin_hotkey=origin_hotkey,
896
+ destination_hotkey=origin_hotkey,
897
+ amount_to_move=amount_to_transfer,
898
+ pricing=pricing,
899
+ stake_fee=sim_swap.alpha_fee
900
+ if origin_netuid != 0
901
+ else sim_swap.tao_fee,
902
+ extrinsic_fee=extrinsic_fee,
903
+ proxy=proxy,
904
+ )
905
+ except ValueError:
906
+ return False, ""
907
+
908
+ if not confirm_action(
909
+ "Would you like to continue?", decline=decline, quiet=quiet
910
+ ):
911
+ return False, ""
912
+
913
+ # Perform transfer operation
914
+ if not unlock_key(wallet).success:
915
+ return False, ""
916
+
917
+ with console.status("\n:satellite: Transferring stake ...") as status:
918
+ success_, err_msg, response = await meshtensor.sign_and_send_extrinsic(
919
+ call=call,
920
+ wallet=wallet,
921
+ era={"period": era},
922
+ proxy=proxy,
923
+ mev_protection=mev_protection,
924
+ nonce=next_nonce,
925
+ )
926
+
927
+ if success_:
928
+ if mev_protection:
929
+ inner_hash = err_msg
930
+ mev_shield_id = await extract_mev_shield_id(response)
931
+ mev_success, mev_error, response = await wait_for_extrinsic_by_hash(
932
+ meshtensor=meshtensor,
933
+ extrinsic_hash=inner_hash,
934
+ shield_id=mev_shield_id,
935
+ submit_block_hash=response.block_hash,
936
+ status=status,
937
+ )
938
+ if not mev_success:
939
+ status.stop()
940
+ print_error(f"\nFailed: {mev_error}")
941
+ return False, ""
942
+ await print_extrinsic_id(response)
943
+ ext_id = await response.get_extrinsic_identifier()
944
+ if not prompt:
945
+ print_success("Sent")
946
+ return True, ext_id
947
+ else:
948
+ # Get and display new stake balances
949
+ new_stake, new_dest_stake = await asyncio.gather(
950
+ meshtensor.get_stake(
951
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
952
+ hotkey_ss58=origin_hotkey,
953
+ netuid=origin_netuid,
954
+ ),
955
+ meshtensor.get_stake(
956
+ coldkey_ss58=dest_coldkey_ss58,
957
+ hotkey_ss58=origin_hotkey,
958
+ netuid=dest_netuid,
959
+ ),
960
+ )
961
+
962
+ console.print(
963
+ f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: "
964
+ f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}"
965
+ )
966
+ console.print(
967
+ f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: "
968
+ f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}"
969
+ )
970
+ return True, ext_id
971
+
972
+ else:
973
+ print_error(f"Failed with error: {err_msg}")
974
+ return False, ""
975
+
976
+
977
+ async def swap_stake(
978
+ wallet: Wallet,
979
+ meshtensor: "MeshtensorInterface",
980
+ origin_netuid: int,
981
+ destination_netuid: int,
982
+ amount: float,
983
+ safe_staking: bool,
984
+ rate_tolerance: float,
985
+ allow_partial_stake: bool,
986
+ swap_all: bool = False,
987
+ era: int = 3,
988
+ proxy: Optional[str] = None,
989
+ interactive_selection: bool = False,
990
+ prompt: bool = True,
991
+ decline: bool = False,
992
+ quiet: bool = False,
993
+ wait_for_inclusion: bool = True,
994
+ wait_for_finalization: bool = False,
995
+ mev_protection: bool = True,
996
+ ) -> tuple[bool, str]:
997
+ """Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership.
998
+
999
+ Args:
1000
+ wallet: The wallet to swap stake from.
1001
+ meshtensor: Meshtensor interface instance.
1002
+ origin_netuid: The netuid from which stake is removed.
1003
+ destination_netuid: The netuid to which stake is added.
1004
+ amount: The amount to swap.
1005
+ safe_staking: Whether to use safe staking with slippage limits.
1006
+ rate_tolerance: The maximum slippage tolerance (e.g., 0.05 for 5%).
1007
+ allow_partial_stake: Whether to execute the swap partially if the full amount exceeds slippage limits.
1008
+ swap_all: Whether to swap all stakes.
1009
+ era: The period (number of blocks) that the extrinsic is valid for
1010
+ proxy: Optional proxy to use for this extrinsic submission
1011
+ interactive_selection: If true, prompts for selection of origin and destination subnets.
1012
+ prompt: If true, prompts for confirmation before executing swap.
1013
+ wait_for_inclusion: If true, waits for the transaction to be included in a block.
1014
+ wait_for_finalization: If true, waits for the transaction to be finalized.
1015
+ mev_protection: If true, will encrypt the extrinsic behind the mev protection shield.
1016
+
1017
+ Returns:
1018
+ (success, extrinsic_identifier):
1019
+ success is True if the swap was successful, False otherwise.
1020
+ extrinsic_identifier if the extrinsic was successfully included
1021
+ """
1022
+ hotkey_ss58 = get_hotkey_pub_ss58(wallet)
1023
+ if interactive_selection:
1024
+ try:
1025
+ selection = await stake_swap_selection(meshtensor, wallet)
1026
+ except ValueError:
1027
+ return False, ""
1028
+ origin_netuid = selection["origin_netuid"]
1029
+ amount = selection["amount"]
1030
+ destination_netuid = selection["destination_netuid"]
1031
+
1032
+ # Check if both subnets exist
1033
+ block_hash = await meshtensor.substrate.get_chain_head()
1034
+ dest_exists, origin_exists = await asyncio.gather(
1035
+ meshtensor.subnet_exists(netuid=destination_netuid, block_hash=block_hash),
1036
+ meshtensor.subnet_exists(netuid=origin_netuid, block_hash=block_hash),
1037
+ )
1038
+ if not dest_exists:
1039
+ print_error(f"Subnet {destination_netuid} does not exist")
1040
+ return False, ""
1041
+
1042
+ if not origin_exists:
1043
+ print_error(f"Subnet {origin_netuid} does not exist")
1044
+ return False, ""
1045
+
1046
+ # Get current stake balances
1047
+ with console.status(f"Retrieving stake data from {meshtensor.network}..."):
1048
+ current_stake = await meshtensor.get_stake(
1049
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
1050
+ hotkey_ss58=hotkey_ss58,
1051
+ netuid=origin_netuid,
1052
+ )
1053
+ current_dest_stake = await meshtensor.get_stake(
1054
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
1055
+ hotkey_ss58=hotkey_ss58,
1056
+ netuid=destination_netuid,
1057
+ )
1058
+
1059
+ if swap_all:
1060
+ amount_to_swap = current_stake.set_unit(origin_netuid)
1061
+ else:
1062
+ amount_to_swap = Balance.from_tao(amount).set_unit(origin_netuid)
1063
+
1064
+ # Check if enough stake to swap
1065
+ if amount_to_swap > current_stake:
1066
+ print_error(
1067
+ f"Not enough stake to swap:\n"
1068
+ f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < "
1069
+ f"Swap amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_swap}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
1070
+ )
1071
+ return False, ""
1072
+
1073
+ pricing = await get_movement_pricing(
1074
+ meshtensor=meshtensor,
1075
+ origin_netuid=origin_netuid,
1076
+ destination_netuid=destination_netuid,
1077
+ safe_staking=safe_staking,
1078
+ rate_tolerance=rate_tolerance,
1079
+ )
1080
+
1081
+ call_fn = "swap_stake"
1082
+ call_params = {
1083
+ "hotkey": hotkey_ss58,
1084
+ "origin_netuid": origin_netuid,
1085
+ "destination_netuid": destination_netuid,
1086
+ "alpha_amount": amount_to_swap.meshlet,
1087
+ }
1088
+ if safe_staking:
1089
+ if pricing.rate_with_tolerance is None:
1090
+ print_error("Failed to compute a rate with tolerance for safe staking.")
1091
+ return False, ""
1092
+ limit_price = Balance.from_tao(pricing.rate_with_tolerance)
1093
+ call_fn = "swap_stake_limit"
1094
+ call_params.update(
1095
+ {
1096
+ "limit_price": limit_price.meshlet,
1097
+ "allow_partial": allow_partial_stake,
1098
+ }
1099
+ )
1100
+
1101
+ call = await meshtensor.substrate.compose_call(
1102
+ call_module="MeshtensorModule",
1103
+ call_function=call_fn,
1104
+ call_params=call_params,
1105
+ )
1106
+ sim_swap, extrinsic_fee, next_nonce = await asyncio.gather(
1107
+ meshtensor.sim_swap(
1108
+ origin_netuid=origin_netuid,
1109
+ destination_netuid=destination_netuid,
1110
+ amount=amount_to_swap.meshlet,
1111
+ ),
1112
+ meshtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy),
1113
+ meshtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address),
1114
+ )
1115
+
1116
+ # Display stake movement details
1117
+ if prompt:
1118
+ try:
1119
+ await display_stake_movement_cross_subnets(
1120
+ meshtensor=meshtensor,
1121
+ origin_netuid=origin_netuid,
1122
+ destination_netuid=destination_netuid,
1123
+ origin_hotkey=hotkey_ss58,
1124
+ destination_hotkey=hotkey_ss58,
1125
+ amount_to_move=amount_to_swap,
1126
+ pricing=pricing,
1127
+ stake_fee=sim_swap.alpha_fee
1128
+ if origin_netuid != 0
1129
+ else sim_swap.tao_fee,
1130
+ extrinsic_fee=extrinsic_fee,
1131
+ safe_staking=safe_staking,
1132
+ rate_tolerance=rate_tolerance,
1133
+ allow_partial_stake=allow_partial_stake,
1134
+ proxy=proxy,
1135
+ )
1136
+ except ValueError:
1137
+ return False, ""
1138
+
1139
+ if not confirm_action(
1140
+ "Would you like to continue?", decline=decline, quiet=quiet
1141
+ ):
1142
+ return False, ""
1143
+
1144
+ # Perform swap operation
1145
+ if not unlock_key(wallet).success:
1146
+ return False, ""
1147
+
1148
+ with console.status(
1149
+ f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] "
1150
+ f"to netuid [blue]{destination_netuid}[/blue]..."
1151
+ ) as status:
1152
+ success_, err_msg, response = await meshtensor.sign_and_send_extrinsic(
1153
+ call=call,
1154
+ wallet=wallet,
1155
+ era={"period": era},
1156
+ proxy=proxy,
1157
+ wait_for_finalization=wait_for_finalization,
1158
+ wait_for_inclusion=wait_for_inclusion,
1159
+ mev_protection=mev_protection,
1160
+ nonce=next_nonce,
1161
+ )
1162
+
1163
+ ext_id = await response.get_extrinsic_identifier()
1164
+
1165
+ if success_:
1166
+ if mev_protection:
1167
+ inner_hash = err_msg
1168
+ mev_shield_id = await extract_mev_shield_id(response)
1169
+ mev_success, mev_error, response = await wait_for_extrinsic_by_hash(
1170
+ meshtensor=meshtensor,
1171
+ extrinsic_hash=inner_hash,
1172
+ shield_id=mev_shield_id,
1173
+ submit_block_hash=response.block_hash,
1174
+ status=status,
1175
+ )
1176
+ if not mev_success:
1177
+ status.stop()
1178
+ print_error(f"\nFailed: {mev_error}")
1179
+ return False, ""
1180
+ await print_extrinsic_id(response)
1181
+ if not prompt:
1182
+ print_success("Sent")
1183
+ return True, await response.get_extrinsic_identifier()
1184
+ else:
1185
+ # Get and display new stake balances
1186
+ new_stake, new_dest_stake = await asyncio.gather(
1187
+ meshtensor.get_stake(
1188
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
1189
+ hotkey_ss58=hotkey_ss58,
1190
+ netuid=origin_netuid,
1191
+ ),
1192
+ meshtensor.get_stake(
1193
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
1194
+ hotkey_ss58=hotkey_ss58,
1195
+ netuid=destination_netuid,
1196
+ ),
1197
+ )
1198
+
1199
+ console.print(
1200
+ f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: "
1201
+ f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}"
1202
+ )
1203
+ console.print(
1204
+ f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: "
1205
+ f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}"
1206
+ )
1207
+ return True, ext_id
1208
+
1209
+ else:
1210
+ print_error(f"Failed with error: {err_msg}")
1211
+ return False, ""