bittensor-cli 8.2.0rc12__py3-none-any.whl → 8.2.0rc14__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.
@@ -0,0 +1,992 @@
1
+ import asyncio
2
+
3
+ from typing import TYPE_CHECKING
4
+ import typer
5
+
6
+ from bittensor_wallet import Wallet
7
+ from bittensor_wallet.errors import KeyFileError
8
+ from rich.table import Table
9
+ from rich.prompt import Confirm, Prompt
10
+
11
+ from bittensor_cli.src import COLOR_PALETTE
12
+ from bittensor_cli.src.bittensor.balances import Balance
13
+ from bittensor_cli.src.bittensor.utils import (
14
+ console,
15
+ err_console,
16
+ print_error,
17
+ format_error_message,
18
+ group_subnets,
19
+ get_subnet_name,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
24
+
25
+ MIN_STAKE_FEE = Balance.from_rao(500_000)
26
+
27
+
28
+ # Helpers
29
+ async def display_stake_movement_cross_subnets(
30
+ subtensor: "SubtensorInterface",
31
+ origin_netuid: int,
32
+ destination_netuid: int,
33
+ origin_hotkey: str,
34
+ destination_hotkey: str,
35
+ amount_to_move: Balance,
36
+ ) -> tuple[Balance, float, str, str]:
37
+ """Calculate and display slippage information"""
38
+
39
+ if origin_netuid == destination_netuid:
40
+ subnet = await subtensor.subnet(origin_netuid)
41
+ received_amount_tao = subnet.alpha_to_tao(amount_to_move)
42
+ received_amount_tao -= MIN_STAKE_FEE
43
+ received_amount = subnet.tao_to_alpha(received_amount_tao)
44
+ slippage_pct_float = (
45
+ 100 * float(MIN_STAKE_FEE) / float(MIN_STAKE_FEE + received_amount_tao)
46
+ if received_amount_tao != 0
47
+ else 0
48
+ )
49
+ slippage_pct = f"{slippage_pct_float:.4f}%"
50
+ price = Balance.from_tao(1).set_unit(origin_netuid)
51
+ price_str = (
52
+ str(float(price.tao))
53
+ + f"{Balance.get_unit(origin_netuid)}/{Balance.get_unit(origin_netuid)}"
54
+ )
55
+ else:
56
+ dynamic_origin, dynamic_destination = await asyncio.gather(
57
+ subtensor.subnet(origin_netuid),
58
+ subtensor.subnet(destination_netuid),
59
+ )
60
+ price = (
61
+ float(dynamic_origin.price) * 1 / (float(dynamic_destination.price) or 1)
62
+ )
63
+ received_amount_tao, _, slippage_pct_float = (
64
+ dynamic_origin.alpha_to_tao_with_slippage(amount_to_move)
65
+ )
66
+ received_amount_tao -= MIN_STAKE_FEE
67
+ received_amount, _, slippage_pct_float = (
68
+ dynamic_destination.tao_to_alpha_with_slippage(received_amount_tao)
69
+ )
70
+ received_amount.set_unit(destination_netuid)
71
+ slippage_pct = f"{slippage_pct_float:.4f} %"
72
+ price_str = (
73
+ str(float(price))
74
+ + f"{Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)}"
75
+ )
76
+
77
+ # Create and display table
78
+ table = Table(
79
+ title=(
80
+ f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]"
81
+ f"Moving stake from: "
82
+ f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})"
83
+ f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] "
84
+ f"to: "
85
+ f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})"
86
+ f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\nNetwork: {subtensor.network}\n"
87
+ f"[/{COLOR_PALETTE['GENERAL']['HEADER']}]"
88
+ ),
89
+ show_footer=True,
90
+ show_edge=False,
91
+ header_style="bold white",
92
+ border_style="bright_black",
93
+ style="bold",
94
+ title_justify="center",
95
+ show_lines=False,
96
+ pad_edge=True,
97
+ )
98
+
99
+ table.add_column(
100
+ "origin netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]
101
+ )
102
+ table.add_column(
103
+ "origin hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
104
+ )
105
+ table.add_column(
106
+ "dest netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]
107
+ )
108
+ table.add_column(
109
+ "dest hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
110
+ )
111
+ table.add_column(
112
+ f"amount ({Balance.get_unit(origin_netuid)})",
113
+ justify="center",
114
+ style=COLOR_PALETTE["STAKE"]["TAO"],
115
+ )
116
+ table.add_column(
117
+ f"rate ({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})",
118
+ justify="center",
119
+ style=COLOR_PALETTE["POOLS"]["RATE"],
120
+ )
121
+ table.add_column(
122
+ f"received ({Balance.get_unit(destination_netuid)})",
123
+ justify="center",
124
+ style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"],
125
+ )
126
+ table.add_column(
127
+ "slippage",
128
+ justify="center",
129
+ style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"],
130
+ )
131
+
132
+ table.add_row(
133
+ f"{Balance.get_unit(origin_netuid)}({origin_netuid})",
134
+ f"{origin_hotkey[:3]}...{origin_hotkey[-3:]}",
135
+ f"{Balance.get_unit(destination_netuid)}({destination_netuid})",
136
+ f"{destination_hotkey[:3]}...{destination_hotkey[-3:]}",
137
+ str(amount_to_move),
138
+ price_str,
139
+ str(received_amount),
140
+ str(slippage_pct),
141
+ )
142
+
143
+ console.print(table)
144
+ # console.print(
145
+ # f"[dim]A fee of {MIN_STAKE_FEE} applies.[/dim]"
146
+ # )
147
+
148
+ # Display slippage warning if necessary
149
+ if slippage_pct_float > 5:
150
+ message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n"
151
+ message += f"[bold]WARNING:\tSlippage is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_pct}[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.[/bold] \n"
152
+ message += "-------------------------------------------------------------------------------------------------------------------\n"
153
+ console.print(message)
154
+
155
+ return received_amount, slippage_pct_float, slippage_pct, price_str
156
+
157
+
158
+ def prompt_stake_amount(
159
+ current_balance: Balance, netuid: int, action_name: str
160
+ ) -> tuple[Balance, bool]:
161
+ """Prompts user to input a stake amount with validation.
162
+
163
+ Args:
164
+ current_balance (Balance): The maximum available balance
165
+ netuid (int): The subnet id to get the correct unit
166
+ action_name (str): The name of the action (e.g. "transfer", "move", "unstake")
167
+
168
+ Returns:
169
+ tuple[Balance, bool]: (The amount to use as Balance object, whether all balance was selected)
170
+ """
171
+ while True:
172
+ amount_input = Prompt.ask(
173
+ f"\nEnter amount to {action_name} from "
174
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
175
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
176
+ f"or "
177
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]'all'[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
178
+ f"for entire balance"
179
+ )
180
+
181
+ if amount_input.lower() == "all":
182
+ return current_balance, True
183
+
184
+ try:
185
+ amount = float(amount_input)
186
+ if amount <= 0:
187
+ console.print("[red]Amount must be greater than 0[/red]")
188
+ continue
189
+ if amount > current_balance.tao:
190
+ console.print(
191
+ f"[red]Amount exceeds available balance of "
192
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
193
+ f"[/red]"
194
+ )
195
+ continue
196
+ return Balance.from_tao(amount), False
197
+ except ValueError:
198
+ console.print("[red]Please enter a valid number or 'all'[/red]")
199
+
200
+
201
+ async def stake_move_selection(
202
+ subtensor: "SubtensorInterface",
203
+ wallet: Wallet,
204
+ ):
205
+ """Selection interface for moving stakes between hotkeys and subnets."""
206
+ stakes, ck_hk_identities, old_identities = await asyncio.gather(
207
+ subtensor.get_stake_for_coldkey(coldkey_ss58=wallet.coldkeypub.ss58_address),
208
+ subtensor.fetch_coldkey_hotkey_identities(),
209
+ subtensor.get_delegate_identities(),
210
+ )
211
+
212
+ hotkey_stakes = {}
213
+ for stake in stakes:
214
+ if stake.stake.tao > 0:
215
+ hotkey = stake.hotkey_ss58
216
+ netuid = stake.netuid
217
+ stake_balance = stake.stake
218
+ hotkey_stakes.setdefault(hotkey, {})[netuid] = stake_balance
219
+
220
+ if not hotkey_stakes:
221
+ print_error("You have no stakes to move.")
222
+ raise typer.Exit()
223
+
224
+ # Display hotkeys with stakes
225
+ table = Table(
226
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes\n",
227
+ show_footer=True,
228
+ show_edge=False,
229
+ header_style="bold white",
230
+ border_style="bright_black",
231
+ style="bold",
232
+ title_justify="center",
233
+ show_lines=False,
234
+ pad_edge=True,
235
+ )
236
+ table.add_column("Index", justify="right")
237
+ table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"])
238
+ table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"])
239
+ table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"])
240
+
241
+ hotkeys_info = []
242
+ for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()):
243
+ if hk_identity := ck_hk_identities["hotkeys"].get(hotkey_ss58):
244
+ hotkey_name = hk_identity.get("identity", {}).get(
245
+ "name", ""
246
+ ) or hk_identity.get("display", "~")
247
+ elif old_identity := old_identities.get(hotkey_ss58):
248
+ hotkey_name = old_identity.display
249
+ else:
250
+ hotkey_name = "~"
251
+ hotkeys_info.append(
252
+ {
253
+ "index": idx,
254
+ "identity": hotkey_name,
255
+ "hotkey_ss58": hotkey_ss58,
256
+ "netuids": list(netuid_stakes.keys()),
257
+ "stakes": netuid_stakes,
258
+ }
259
+ )
260
+ table.add_row(
261
+ str(idx),
262
+ hotkey_name,
263
+ group_subnets([n for n in netuid_stakes.keys()]),
264
+ hotkey_ss58,
265
+ )
266
+
267
+ console.print("\n", table)
268
+
269
+ # Select origin hotkey
270
+ origin_idx = Prompt.ask(
271
+ "\nEnter the index of the hotkey you want to move stake from",
272
+ choices=[str(i) for i in range(len(hotkeys_info))],
273
+ )
274
+ origin_hotkey_info = hotkeys_info[int(origin_idx)]
275
+ origin_hotkey_ss58 = origin_hotkey_info["hotkey_ss58"]
276
+
277
+ # Display available netuids for selected hotkey
278
+ table = Table(
279
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Available Stakes for Hotkey\n[/{COLOR_PALETTE['GENERAL']['HEADER']}]"
280
+ f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{origin_hotkey_ss58}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n",
281
+ show_edge=False,
282
+ header_style="bold white",
283
+ border_style="bright_black",
284
+ title_justify="center",
285
+ width=len(origin_hotkey_ss58) + 20,
286
+ )
287
+ table.add_column("Index", justify="right")
288
+ table.add_column("Netuid", style="cyan")
289
+ table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"])
290
+
291
+ available_netuids = []
292
+ for idx, netuid in enumerate(origin_hotkey_info["netuids"]):
293
+ stake = origin_hotkey_info["stakes"][netuid]
294
+ if stake.tao > 0:
295
+ available_netuids.append(netuid)
296
+ table.add_row(str(idx), str(netuid), str(stake))
297
+
298
+ console.print("\n", table)
299
+
300
+ # Select origin netuid
301
+ netuid_idx = Prompt.ask(
302
+ "\nEnter the index of the subnet you want to move stake from",
303
+ choices=[str(i) for i in range(len(available_netuids))],
304
+ )
305
+ origin_netuid = available_netuids[int(netuid_idx)]
306
+ origin_stake = origin_hotkey_info["stakes"][origin_netuid]
307
+
308
+ # Ask for amount to move
309
+ amount, stake_all = prompt_stake_amount(origin_stake, origin_netuid, "move")
310
+
311
+ all_subnets = sorted(await subtensor.get_all_subnet_netuids())
312
+ destination_netuid = Prompt.ask(
313
+ "\nEnter the netuid of the subnet you want to move stake to"
314
+ + f" ([dim]{group_subnets(all_subnets)}[/dim])",
315
+ choices=[str(netuid) for netuid in all_subnets],
316
+ show_choices=False,
317
+ )
318
+
319
+ return {
320
+ "origin_hotkey": origin_hotkey_ss58,
321
+ "origin_netuid": origin_netuid,
322
+ "amount": amount.tao,
323
+ "stake_all": stake_all,
324
+ "destination_netuid": int(destination_netuid),
325
+ }
326
+
327
+
328
+ async def stake_transfer_selection(
329
+ wallet: Wallet,
330
+ subtensor: "SubtensorInterface",
331
+ ):
332
+ """Selection interface for transferring stakes."""
333
+ (
334
+ stakes,
335
+ all_netuids,
336
+ all_subnets,
337
+ ) = await asyncio.gather(
338
+ subtensor.get_stake_for_coldkey(coldkey_ss58=wallet.coldkeypub.ss58_address),
339
+ subtensor.get_all_subnet_netuids(),
340
+ subtensor.all_subnets(),
341
+ )
342
+ all_netuids = sorted(all_netuids)
343
+ all_subnets = {di.netuid: di for di in all_subnets}
344
+
345
+ available_stakes = {}
346
+ for stake in stakes:
347
+ if stake.stake.tao > 0 and stake.hotkey_ss58 == wallet.hotkey.ss58_address:
348
+ available_stakes[stake.netuid] = {
349
+ "hotkey_ss58": stake.hotkey_ss58,
350
+ "stake": stake.stake,
351
+ "is_registered": stake.is_registered,
352
+ }
353
+
354
+ if not available_stakes:
355
+ console.print("[red]No stakes available to transfer.[/red]")
356
+ return None
357
+
358
+ table = Table(
359
+ title=(
360
+ f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]"
361
+ f"Available Stakes to Transfer\n"
362
+ f"for wallet hotkey:\n"
363
+ f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey_str}: {wallet.hotkey.ss58_address}"
364
+ f"[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n"
365
+ ),
366
+ show_edge=False,
367
+ header_style="bold white",
368
+ border_style="bright_black",
369
+ title_justify="center",
370
+ width=len(wallet.hotkey_str + wallet.hotkey.ss58_address) + 10,
371
+ )
372
+
373
+ table.add_column("Index", justify="right", style="cyan")
374
+ table.add_column("Netuid")
375
+ table.add_column("Name", style="cyan", justify="left")
376
+ table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"])
377
+ table.add_column("Registered", justify="center")
378
+
379
+ for idx, (netuid, stake_info) in enumerate(available_stakes.items()):
380
+ subnet_name_cell = (
381
+ f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{all_subnets[netuid].symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]"
382
+ f" {get_subnet_name(all_subnets[netuid])}"
383
+ )
384
+ table.add_row(
385
+ str(idx),
386
+ str(netuid),
387
+ subnet_name_cell,
388
+ str(stake_info["stake"]),
389
+ "[dark_sea_green3]YES"
390
+ if stake_info["is_registered"]
391
+ else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO",
392
+ )
393
+
394
+ console.print(table)
395
+
396
+ if not available_stakes:
397
+ console.print("[red]No stakes available to transfer.[/red]")
398
+ return None
399
+
400
+ # Prompt to select index of stake to transfer
401
+ selection = Prompt.ask(
402
+ "\nEnter the index of the stake you want to transfer",
403
+ choices=[str(i) for i in range(len(available_stakes))],
404
+ )
405
+ selected_netuid = list(available_stakes.keys())[int(selection)]
406
+ selected_stake = available_stakes[selected_netuid]
407
+
408
+ # Prompt for amount
409
+ stake_balance = selected_stake["stake"]
410
+ amount, _ = prompt_stake_amount(stake_balance, selected_netuid, "transfer")
411
+
412
+ # Prompt for destination subnet
413
+ destination_netuid = Prompt.ask(
414
+ "\nEnter the netuid of the subnet you want to move stake to"
415
+ + f" ([dim]{group_subnets(all_netuids)}[/dim])",
416
+ choices=[str(netuid) for netuid in all_netuids],
417
+ show_choices=False,
418
+ )
419
+
420
+ return {
421
+ "origin_netuid": selected_netuid,
422
+ "amount": amount.tao,
423
+ "destination_netuid": int(destination_netuid),
424
+ }
425
+
426
+
427
+ async def stake_swap_selection(
428
+ subtensor: "SubtensorInterface",
429
+ wallet: Wallet,
430
+ ) -> dict:
431
+ """Selection interface for swapping stakes between subnets."""
432
+ block_hash = await subtensor.substrate.get_chain_head()
433
+ stakes, all_subnets = await asyncio.gather(
434
+ subtensor.get_stake_for_coldkey(
435
+ coldkey_ss58=wallet.coldkeypub.ss58_address, block_hash=block_hash
436
+ ),
437
+ subtensor.all_subnets(block_hash=block_hash),
438
+ )
439
+ subnet_dict = {di.netuid: di for di in all_subnets}
440
+
441
+ # Filter stakes for this hotkey
442
+ hotkey_stakes = {}
443
+ for stake in stakes:
444
+ if stake.hotkey_ss58 == wallet.hotkey.ss58_address and stake.stake.tao > 0:
445
+ hotkey_stakes[stake.netuid] = {
446
+ "stake": stake.stake,
447
+ "is_registered": stake.is_registered,
448
+ }
449
+
450
+ if not hotkey_stakes:
451
+ print_error(f"No stakes found for hotkey: {wallet.hotkey_str}")
452
+ raise typer.Exit()
453
+
454
+ # Display available stakes
455
+ table = Table(
456
+ title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Available Stakes for Hotkey\n[/{COLOR_PALETTE['GENERAL']['HEADER']}]"
457
+ f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey_str}: {wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n",
458
+ show_edge=False,
459
+ header_style="bold white",
460
+ border_style="bright_black",
461
+ title_justify="center",
462
+ width=len(wallet.hotkey.ss58_address) + 20,
463
+ )
464
+
465
+ table.add_column("Index", justify="right", style="cyan")
466
+ table.add_column("Netuid", style=COLOR_PALETTE["GENERAL"]["NETUID"])
467
+ table.add_column("Name", style="cyan", justify="left")
468
+ table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"])
469
+ table.add_column("Registered", justify="center")
470
+
471
+ available_netuids = []
472
+ for idx, (netuid, stake_info) in enumerate(sorted(hotkey_stakes.items())):
473
+ subnet_info = subnet_dict[netuid]
474
+ subnet_name_cell = (
475
+ f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet_info.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]"
476
+ f" {get_subnet_name(subnet_info)}"
477
+ )
478
+
479
+ available_netuids.append(netuid)
480
+ table.add_row(
481
+ str(idx),
482
+ str(netuid),
483
+ subnet_name_cell,
484
+ str(stake_info["stake"]),
485
+ "[dark_sea_green3]YES"
486
+ if stake_info["is_registered"]
487
+ else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO",
488
+ )
489
+
490
+ console.print("\n", table)
491
+
492
+ # Select origin netuid
493
+ origin_idx = Prompt.ask(
494
+ "\nEnter the index of the subnet you want to swap stake from",
495
+ choices=[str(i) for i in range(len(available_netuids))],
496
+ )
497
+ origin_netuid = available_netuids[int(origin_idx)]
498
+ origin_stake = hotkey_stakes[origin_netuid]["stake"]
499
+
500
+ # Ask for amount to swap
501
+ amount, all_balance = prompt_stake_amount(origin_stake, origin_netuid, "swap")
502
+
503
+ all_netuids = sorted(await subtensor.get_all_subnet_netuids())
504
+ destination_choices = [
505
+ str(netuid) for netuid in all_netuids if netuid != origin_netuid
506
+ ]
507
+ destination_netuid = Prompt.ask(
508
+ "\nEnter the netuid of the subnet you want to swap stake to"
509
+ + f" ([dim]{group_subnets(all_netuids)}[/dim])",
510
+ choices=destination_choices,
511
+ show_choices=False,
512
+ )
513
+
514
+ return {
515
+ "origin_netuid": origin_netuid,
516
+ "amount": amount.tao,
517
+ "destination_netuid": int(destination_netuid),
518
+ }
519
+
520
+
521
+ # Commands
522
+ async def move_stake(
523
+ subtensor: "SubtensorInterface",
524
+ wallet: Wallet,
525
+ origin_netuid: int,
526
+ origin_hotkey: str,
527
+ destination_netuid: int,
528
+ destination_hotkey: str,
529
+ amount: float,
530
+ stake_all: bool,
531
+ interactive_selection: bool = False,
532
+ prompt: bool = True,
533
+ ):
534
+ if interactive_selection:
535
+ selection = await stake_move_selection(subtensor, wallet)
536
+ origin_hotkey = selection["origin_hotkey"]
537
+ origin_netuid = selection["origin_netuid"]
538
+ amount = selection["amount"]
539
+ stake_all = selection["stake_all"]
540
+ destination_netuid = selection["destination_netuid"]
541
+
542
+ # Get the wallet stake balances.
543
+ block_hash = await subtensor.substrate.get_chain_head()
544
+ origin_stake_balance, destination_stake_balance = await asyncio.gather(
545
+ subtensor.get_stake(
546
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
547
+ hotkey_ss58=origin_hotkey,
548
+ netuid=origin_netuid,
549
+ block_hash=block_hash,
550
+ ),
551
+ subtensor.get_stake(
552
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
553
+ hotkey_ss58=destination_hotkey,
554
+ netuid=destination_netuid,
555
+ block_hash=block_hash,
556
+ ),
557
+ )
558
+
559
+ if origin_stake_balance.tao == 0:
560
+ print_error(
561
+ f"Your balance is "
562
+ f"[{COLOR_PALETTE['POOLS']['TAO']}]0[/{COLOR_PALETTE['POOLS']['TAO']}] "
563
+ f"in Netuid: "
564
+ f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]"
565
+ )
566
+ raise typer.Exit()
567
+
568
+ console.print(
569
+ f"\nOrigin Netuid: "
570
+ f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], "
571
+ f"Origin stake: "
572
+ f"[{COLOR_PALETTE['POOLS']['TAO']}]{origin_stake_balance}[/{COLOR_PALETTE['POOLS']['TAO']}]"
573
+ )
574
+ console.print(
575
+ f"Destination netuid: "
576
+ f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{destination_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], "
577
+ f"Destination stake: "
578
+ f"[{COLOR_PALETTE['POOLS']['TAO']}]{destination_stake_balance}[/{COLOR_PALETTE['POOLS']['TAO']}]\n"
579
+ )
580
+
581
+ # Determine the amount we are moving.
582
+ amount_to_move_as_balance = None
583
+ if amount:
584
+ amount_to_move_as_balance = Balance.from_tao(amount)
585
+ elif stake_all:
586
+ amount_to_move_as_balance = origin_stake_balance
587
+ else:
588
+ amount_to_move_as_balance, _ = prompt_stake_amount(
589
+ origin_stake_balance, origin_netuid, "move"
590
+ )
591
+
592
+ # Check enough to move.
593
+ amount_to_move_as_balance.set_unit(origin_netuid)
594
+ if amount_to_move_as_balance > origin_stake_balance:
595
+ err_console.print(
596
+ f"[red]Not enough stake[/red]:\n"
597
+ f" Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
598
+ f" < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
599
+ )
600
+ return False
601
+
602
+ # Slippage warning
603
+ if prompt:
604
+ await display_stake_movement_cross_subnets(
605
+ subtensor=subtensor,
606
+ origin_netuid=origin_netuid,
607
+ destination_netuid=destination_netuid,
608
+ origin_hotkey=origin_hotkey,
609
+ destination_hotkey=destination_hotkey,
610
+ amount_to_move=amount_to_move_as_balance,
611
+ )
612
+ if not Confirm.ask("Would you like to continue?"):
613
+ raise typer.Exit()
614
+
615
+ # Perform moving operation.
616
+ try:
617
+ wallet.unlock_coldkey()
618
+ except KeyFileError:
619
+ err_console.print("Error decrypting coldkey (possibly incorrect password)")
620
+ return False
621
+ with console.status(
622
+ f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: [blue]{origin_netuid}[/blue] \nto "
623
+ f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..."
624
+ ):
625
+ call = await subtensor.substrate.compose_call(
626
+ call_module="SubtensorModule",
627
+ call_function="move_stake",
628
+ call_params={
629
+ "origin_hotkey": origin_hotkey,
630
+ "origin_netuid": origin_netuid,
631
+ "destination_hotkey": destination_hotkey,
632
+ "destination_netuid": destination_netuid,
633
+ "alpha_amount": amount_to_move_as_balance.rao,
634
+ },
635
+ )
636
+ extrinsic = await subtensor.substrate.create_signed_extrinsic(
637
+ call=call, keypair=wallet.coldkey
638
+ )
639
+ response = await subtensor.substrate.submit_extrinsic(
640
+ extrinsic, wait_for_inclusion=True, wait_for_finalization=False
641
+ )
642
+
643
+ if not prompt:
644
+ console.print(":white_heavy_check_mark: [green]Sent[/green]")
645
+ return True
646
+ else:
647
+ await response.process_events()
648
+ if not await response.is_success:
649
+ err_console.print(
650
+ f"\n:cross_mark: [red]Failed[/red] with error:"
651
+ f" {format_error_message( await response.error_message, subtensor.substrate)}"
652
+ )
653
+ return
654
+ else:
655
+ console.print(
656
+ ":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]"
657
+ )
658
+ block_hash = await subtensor.substrate.get_chain_head()
659
+ (
660
+ new_origin_stake_balance,
661
+ new_destination_stake_balance,
662
+ ) = await asyncio.gather(
663
+ subtensor.get_stake(
664
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
665
+ hotkey_ss58=origin_hotkey,
666
+ netuid=origin_netuid,
667
+ block_hash=block_hash,
668
+ ),
669
+ subtensor.get_stake(
670
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
671
+ hotkey_ss58=destination_hotkey,
672
+ netuid=destination_netuid,
673
+ block_hash=block_hash,
674
+ ),
675
+ )
676
+
677
+ console.print(
678
+ f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: "
679
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_origin_stake_balance}"
680
+ )
681
+ console.print(
682
+ f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: "
683
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}"
684
+ )
685
+ return
686
+
687
+
688
+ async def transfer_stake(
689
+ wallet: Wallet,
690
+ subtensor: "SubtensorInterface",
691
+ amount: float,
692
+ origin_netuid: int,
693
+ dest_netuid: int,
694
+ dest_coldkey_ss58: str,
695
+ interactive_selection: bool = False,
696
+ prompt: bool = True,
697
+ ) -> bool:
698
+ """Transfers stake from one network to another.
699
+
700
+ Args:
701
+ wallet (Wallet): Bittensor wallet object.
702
+ subtensor (SubtensorInterface): Subtensor interface instance.
703
+ amount (float): Amount to transfer.
704
+ origin_netuid (int): The netuid to transfer stake from.
705
+ dest_netuid (int): The netuid to transfer stake to.
706
+ dest_coldkey_ss58 (str): The destination coldkey to transfer stake to.
707
+ interactive_selection (bool): If true, prompts for selection of origin and destination subnets.
708
+ prompt (bool): If true, prompts for confirmation before executing transfer.
709
+
710
+ Returns:
711
+ bool: True if transfer was successful, False otherwise.
712
+ """
713
+ if interactive_selection:
714
+ selection = await stake_transfer_selection(wallet, subtensor)
715
+ origin_netuid = selection["origin_netuid"]
716
+ amount = selection["amount"]
717
+ dest_netuid = selection["destination_netuid"]
718
+
719
+ # Check if both subnets exist
720
+ block_hash = await subtensor.substrate.get_chain_head()
721
+ dest_exists, origin_exists = await asyncio.gather(
722
+ subtensor.subnet_exists(netuid=dest_netuid, block_hash=block_hash),
723
+ subtensor.subnet_exists(netuid=origin_netuid, block_hash=block_hash),
724
+ )
725
+ if not dest_exists:
726
+ err_console.print(f"[red]Subnet {dest_netuid} does not exist[/red]")
727
+ return False
728
+
729
+ if not origin_exists:
730
+ err_console.print(f"[red]Subnet {origin_netuid} does not exist[/red]")
731
+ return False
732
+
733
+ # Get current stake balances
734
+ hotkey_ss58 = wallet.hotkey.ss58_address
735
+ with console.status(f"Retrieving stake data from {subtensor.network}..."):
736
+ current_stake = await subtensor.get_stake(
737
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
738
+ hotkey_ss58=hotkey_ss58,
739
+ netuid=origin_netuid,
740
+ )
741
+ current_dest_stake = await subtensor.get_stake(
742
+ coldkey_ss58=dest_coldkey_ss58,
743
+ hotkey_ss58=hotkey_ss58,
744
+ netuid=dest_netuid,
745
+ )
746
+ amount_to_transfer = Balance.from_tao(amount).set_unit(origin_netuid)
747
+
748
+ # Check if enough stake to transfer
749
+ if amount_to_transfer > current_stake:
750
+ err_console.print(
751
+ f"[red]Not enough stake to transfer[/red]:\n"
752
+ f"Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < "
753
+ f"Transfer amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_transfer}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
754
+ )
755
+ return False
756
+
757
+ # Slippage warning
758
+ if prompt:
759
+ await display_stake_movement_cross_subnets(
760
+ subtensor=subtensor,
761
+ origin_netuid=origin_netuid,
762
+ destination_netuid=dest_netuid,
763
+ origin_hotkey=hotkey_ss58,
764
+ destination_hotkey=hotkey_ss58,
765
+ amount_to_move=amount_to_transfer,
766
+ )
767
+
768
+ if not Confirm.ask("Would you like to continue?"):
769
+ raise typer.Exit()
770
+
771
+ # Perform transfer operation
772
+ try:
773
+ wallet.unlock_coldkey()
774
+ except KeyFileError:
775
+ err_console.print("Error decrypting coldkey (possibly incorrect password)")
776
+ return False
777
+
778
+ with console.status("\n:satellite: Transferring stake ..."):
779
+ call = await subtensor.substrate.compose_call(
780
+ call_module="SubtensorModule",
781
+ call_function="transfer_stake",
782
+ call_params={
783
+ "destination_coldkey": dest_coldkey_ss58,
784
+ "hotkey": hotkey_ss58,
785
+ "origin_netuid": origin_netuid,
786
+ "destination_netuid": dest_netuid,
787
+ "alpha_amount": amount_to_transfer.rao,
788
+ },
789
+ )
790
+
791
+ extrinsic = await subtensor.substrate.create_signed_extrinsic(
792
+ call=call, keypair=wallet.coldkey
793
+ )
794
+
795
+ response = await subtensor.substrate.submit_extrinsic(
796
+ extrinsic, wait_for_inclusion=True, wait_for_finalization=False
797
+ )
798
+
799
+ if not prompt:
800
+ console.print(":white_heavy_check_mark: [green]Sent[/green]")
801
+ return True
802
+
803
+ await response.process_events()
804
+ if not await response.is_success:
805
+ err_console.print(
806
+ f":cross_mark: [red]Failed[/red] with error: "
807
+ f"{format_error_message(await response.error_message, subtensor.substrate)}"
808
+ )
809
+ return False
810
+
811
+ # Get and display new stake balances
812
+ new_stake, new_dest_stake = await asyncio.gather(
813
+ subtensor.get_stake(
814
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
815
+ hotkey_ss58=hotkey_ss58,
816
+ netuid=origin_netuid,
817
+ ),
818
+ subtensor.get_stake(
819
+ coldkey_ss58=dest_coldkey_ss58,
820
+ hotkey_ss58=hotkey_ss58,
821
+ netuid=dest_netuid,
822
+ ),
823
+ )
824
+
825
+ console.print(
826
+ f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: "
827
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}"
828
+ )
829
+ console.print(
830
+ f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: "
831
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}"
832
+ )
833
+ return True
834
+
835
+
836
+ async def swap_stake(
837
+ wallet: Wallet,
838
+ subtensor: "SubtensorInterface",
839
+ origin_netuid: int,
840
+ destination_netuid: int,
841
+ amount: float,
842
+ swap_all: bool = False,
843
+ interactive_selection: bool = False,
844
+ prompt: bool = True,
845
+ wait_for_inclusion: bool = True,
846
+ wait_for_finalization: bool = False,
847
+ ) -> bool:
848
+ """Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership.
849
+
850
+ Args:
851
+ wallet (Wallet): The wallet to swap stake from.
852
+ subtensor (SubtensorInterface): Subtensor interface instance.
853
+ hotkey_ss58 (str): The SS58 address of the hotkey whose stake is being swapped.
854
+ origin_netuid (int): The netuid from which stake is removed.
855
+ destination_netuid (int): The netuid to which stake is added.
856
+ amount (float): The amount to swap.
857
+ interactive_selection (bool): If true, prompts for selection of origin and destination subnets.
858
+ prompt (bool): If true, prompts for confirmation before executing swap.
859
+ wait_for_inclusion (bool): If true, waits for the transaction to be included in a block.
860
+ wait_for_finalization (bool): If true, waits for the transaction to be finalized.
861
+
862
+ Returns:
863
+ bool: True if the swap was successful, False otherwise.
864
+ """
865
+ hotkey_ss58 = wallet.hotkey.ss58_address
866
+ if interactive_selection:
867
+ selection = await stake_swap_selection(subtensor, wallet)
868
+ origin_netuid = selection["origin_netuid"]
869
+ amount = selection["amount"]
870
+ destination_netuid = selection["destination_netuid"]
871
+
872
+ # Check if both subnets exist
873
+ block_hash = await subtensor.substrate.get_chain_head()
874
+ dest_exists, origin_exists = await asyncio.gather(
875
+ subtensor.subnet_exists(netuid=destination_netuid, block_hash=block_hash),
876
+ subtensor.subnet_exists(netuid=origin_netuid, block_hash=block_hash),
877
+ )
878
+ if not dest_exists:
879
+ err_console.print(f"[red]Subnet {destination_netuid} does not exist[/red]")
880
+ return False
881
+
882
+ if not origin_exists:
883
+ err_console.print(f"[red]Subnet {origin_netuid} does not exist[/red]")
884
+ return False
885
+
886
+ # Get current stake balances
887
+ with console.status(f"Retrieving stake data from {subtensor.network}..."):
888
+ current_stake = await subtensor.get_stake(
889
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
890
+ hotkey_ss58=hotkey_ss58,
891
+ netuid=origin_netuid,
892
+ )
893
+ current_dest_stake = await subtensor.get_stake(
894
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
895
+ hotkey_ss58=hotkey_ss58,
896
+ netuid=destination_netuid,
897
+ )
898
+
899
+ if swap_all:
900
+ amount_to_swap = Balance.from_tao(current_stake).set_unit(origin_netuid)
901
+ else:
902
+ amount_to_swap = Balance.from_tao(amount).set_unit(origin_netuid)
903
+
904
+ # Check if enough stake to swap
905
+ if amount_to_swap > current_stake:
906
+ err_console.print(
907
+ f"[red]Not enough stake to swap[/red]:\n"
908
+ f"Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < "
909
+ f"Swap amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_swap}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
910
+ )
911
+ return False
912
+
913
+ # Slippage warning
914
+ if prompt:
915
+ await display_stake_movement_cross_subnets(
916
+ subtensor=subtensor,
917
+ origin_netuid=origin_netuid,
918
+ destination_netuid=destination_netuid,
919
+ origin_hotkey=hotkey_ss58,
920
+ destination_hotkey=hotkey_ss58,
921
+ amount_to_move=amount_to_swap,
922
+ )
923
+
924
+ if not Confirm.ask("Would you like to continue?"):
925
+ raise typer.Exit()
926
+
927
+ # Perform swap operation
928
+ try:
929
+ wallet.unlock_coldkey()
930
+ except KeyFileError:
931
+ err_console.print("Error decrypting coldkey (possibly incorrect password)")
932
+ return False
933
+
934
+ with console.status(
935
+ f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] to netuid [blue]{destination_netuid}[/blue]..."
936
+ ):
937
+ call = await subtensor.substrate.compose_call(
938
+ call_module="SubtensorModule",
939
+ call_function="swap_stake",
940
+ call_params={
941
+ "hotkey": hotkey_ss58,
942
+ "origin_netuid": origin_netuid,
943
+ "destination_netuid": destination_netuid,
944
+ "alpha_amount": amount_to_swap.rao,
945
+ },
946
+ )
947
+
948
+ extrinsic = await subtensor.substrate.create_signed_extrinsic(
949
+ call=call, keypair=wallet.coldkey
950
+ )
951
+
952
+ response = await subtensor.substrate.submit_extrinsic(
953
+ extrinsic,
954
+ wait_for_inclusion=wait_for_inclusion,
955
+ wait_for_finalization=wait_for_finalization,
956
+ )
957
+
958
+ if not prompt:
959
+ console.print(":white_heavy_check_mark: [green]Sent[/green]")
960
+ return True
961
+
962
+ await response.process_events()
963
+ if not await response.is_success:
964
+ err_console.print(
965
+ f":cross_mark: [red]Failed[/red] with error: "
966
+ f"{format_error_message(await response.error_message, subtensor.substrate)}"
967
+ )
968
+ return False
969
+
970
+ # Get and display new stake balances
971
+ new_stake, new_dest_stake = await asyncio.gather(
972
+ subtensor.get_stake(
973
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
974
+ hotkey_ss58=hotkey_ss58,
975
+ netuid=origin_netuid,
976
+ ),
977
+ subtensor.get_stake(
978
+ coldkey_ss58=wallet.coldkeypub.ss58_address,
979
+ hotkey_ss58=hotkey_ss58,
980
+ netuid=destination_netuid,
981
+ ),
982
+ )
983
+
984
+ console.print(
985
+ f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: "
986
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}"
987
+ )
988
+ console.print(
989
+ f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: "
990
+ f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}"
991
+ )
992
+ return True