bittensor-cli 9.7.1__py3-none-any.whl → 9.8.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.
@@ -0,0 +1,628 @@
1
+ import asyncio
2
+ import json
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ from rich.prompt import Confirm
6
+ from rich.table import Column, Table
7
+
8
+ from bittensor_cli.src import COLORS
9
+ from bittensor_cli.src.bittensor.utils import (
10
+ unlock_key,
11
+ console,
12
+ err_console,
13
+ json_console,
14
+ )
15
+ from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float
16
+ from bittensor_cli.src.commands.liquidity.utils import (
17
+ LiquidityPosition,
18
+ calculate_fees,
19
+ get_fees,
20
+ price_to_tick,
21
+ tick_to_price,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ from bittensor_wallet import Wallet
26
+ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
27
+
28
+
29
+ async def add_liquidity_extrinsic(
30
+ subtensor: "SubtensorInterface",
31
+ wallet: "Wallet",
32
+ hotkey_ss58: str,
33
+ netuid: int,
34
+ liquidity: Balance,
35
+ price_low: Balance,
36
+ price_high: Balance,
37
+ wait_for_inclusion: bool = True,
38
+ wait_for_finalization: bool = False,
39
+ ) -> tuple[bool, str]:
40
+ """
41
+ Adds liquidity to the specified price range.
42
+
43
+ Arguments:
44
+ subtensor: The Subtensor client instance used for blockchain interaction.
45
+ wallet: The wallet used to sign the extrinsic (must be unlocked).
46
+ hotkey_ss58: the SS58 of the hotkey to use for this transaction.
47
+ netuid: The UID of the target subnet for which the call is being initiated.
48
+ liquidity: The amount of liquidity to be added.
49
+ price_low: The lower bound of the price tick range.
50
+ price_high: The upper bound of the price tick range.
51
+ wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True.
52
+ wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False.
53
+
54
+ Returns:
55
+ Tuple[bool, str]:
56
+ - True and a success message if the extrinsic is successfully submitted or processed.
57
+ - False and an error message if the submission fails or the wallet cannot be unlocked.
58
+
59
+ Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call
60
+ `toggle_user_liquidity_extrinsic` to enable/disable user liquidity.
61
+ """
62
+ if not (unlock := unlock_key(wallet)).success:
63
+ return False, unlock.message
64
+
65
+ tick_low = price_to_tick(price_low.tao)
66
+ tick_high = price_to_tick(price_high.tao)
67
+
68
+ call = await subtensor.substrate.compose_call(
69
+ call_module="Swap",
70
+ call_function="add_liquidity",
71
+ call_params={
72
+ "hotkey": hotkey_ss58,
73
+ "netuid": netuid,
74
+ "tick_low": tick_low,
75
+ "tick_high": tick_high,
76
+ "liquidity": liquidity.rao,
77
+ },
78
+ )
79
+
80
+ return await subtensor.sign_and_send_extrinsic(
81
+ call=call,
82
+ wallet=wallet,
83
+ wait_for_inclusion=wait_for_inclusion,
84
+ wait_for_finalization=wait_for_finalization,
85
+ )
86
+
87
+
88
+ async def modify_liquidity_extrinsic(
89
+ subtensor: "SubtensorInterface",
90
+ wallet: "Wallet",
91
+ hotkey_ss58: str,
92
+ netuid: int,
93
+ position_id: int,
94
+ liquidity_delta: Balance,
95
+ wait_for_inclusion: bool = True,
96
+ wait_for_finalization: bool = False,
97
+ ) -> tuple[bool, str]:
98
+ """Modifies liquidity in liquidity position by adding or removing liquidity from it.
99
+
100
+ Arguments:
101
+ subtensor: The Subtensor client instance used for blockchain interaction.
102
+ wallet: The wallet used to sign the extrinsic (must be unlocked).
103
+ hotkey_ss58: the SS58 of the hotkey to use for this transaction.
104
+ netuid: The UID of the target subnet for which the call is being initiated.
105
+ position_id: The id of the position record in the pool.
106
+ liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative).
107
+ wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True.
108
+ wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False.
109
+
110
+ Returns:
111
+ Tuple[bool, str]:
112
+ - True and a success message if the extrinsic is successfully submitted or processed.
113
+ - False and an error message if the submission fails or the wallet cannot be unlocked.
114
+
115
+ Note: Modifying is allowed even when user liquidity is enabled in specified subnet.
116
+ Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity.
117
+ """
118
+ if not (unlock := unlock_key(wallet)).success:
119
+ return False, unlock.message
120
+
121
+ call = await subtensor.substrate.compose_call(
122
+ call_module="Swap",
123
+ call_function="modify_position",
124
+ call_params={
125
+ "hotkey": hotkey_ss58,
126
+ "netuid": netuid,
127
+ "position_id": position_id,
128
+ "liquidity_delta": liquidity_delta.rao,
129
+ },
130
+ )
131
+
132
+ return await subtensor.sign_and_send_extrinsic(
133
+ call=call,
134
+ wallet=wallet,
135
+ wait_for_inclusion=wait_for_inclusion,
136
+ wait_for_finalization=wait_for_finalization,
137
+ )
138
+
139
+
140
+ async def remove_liquidity_extrinsic(
141
+ subtensor: "SubtensorInterface",
142
+ wallet: "Wallet",
143
+ hotkey_ss58: str,
144
+ netuid: int,
145
+ position_id: int,
146
+ wait_for_inclusion: bool = True,
147
+ wait_for_finalization: bool = False,
148
+ ) -> tuple[bool, str]:
149
+ """Remove liquidity and credit balances back to wallet's hotkey stake.
150
+
151
+ Arguments:
152
+ subtensor: The Subtensor client instance used for blockchain interaction.
153
+ wallet: The wallet used to sign the extrinsic (must be unlocked).
154
+ hotkey_ss58: the SS58 of the hotkey to use for this transaction.
155
+ netuid: The UID of the target subnet for which the call is being initiated.
156
+ position_id: The id of the position record in the pool.
157
+ wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True.
158
+ wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False.
159
+
160
+ Returns:
161
+ Tuple[bool, str]:
162
+ - True and a success message if the extrinsic is successfully submitted or processed.
163
+ - False and an error message if the submission fails or the wallet cannot be unlocked.
164
+
165
+ Note: Adding is allowed even when user liquidity is enabled in specified subnet.
166
+ Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity.
167
+ """
168
+ if not (unlock := unlock_key(wallet)).success:
169
+ return False, unlock.message
170
+
171
+ call = await subtensor.substrate.compose_call(
172
+ call_module="Swap",
173
+ call_function="remove_liquidity",
174
+ call_params={
175
+ "hotkey": hotkey_ss58,
176
+ "netuid": netuid,
177
+ "position_id": position_id,
178
+ },
179
+ )
180
+
181
+ return await subtensor.sign_and_send_extrinsic(
182
+ call=call,
183
+ wallet=wallet,
184
+ wait_for_inclusion=wait_for_inclusion,
185
+ wait_for_finalization=wait_for_finalization,
186
+ )
187
+
188
+
189
+ async def toggle_user_liquidity_extrinsic(
190
+ subtensor: "SubtensorInterface",
191
+ wallet: "Wallet",
192
+ netuid: int,
193
+ enable: bool,
194
+ wait_for_inclusion: bool = True,
195
+ wait_for_finalization: bool = False,
196
+ ) -> tuple[bool, str]:
197
+ """Allow to toggle user liquidity for specified subnet.
198
+
199
+ Arguments:
200
+ subtensor: The Subtensor client instance used for blockchain interaction.
201
+ wallet: The wallet used to sign the extrinsic (must be unlocked).
202
+ netuid: The UID of the target subnet for which the call is being initiated.
203
+ enable: Boolean indicating whether to enable user liquidity.
204
+ wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True.
205
+ wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False.
206
+
207
+ Returns:
208
+ Tuple[bool, str]:
209
+ - True and a success message if the extrinsic is successfully submitted or processed.
210
+ - False and an error message if the submission fails or the wallet cannot be unlocked.
211
+ """
212
+ if not (unlock := unlock_key(wallet)).success:
213
+ return False, unlock.message
214
+
215
+ call = await subtensor.substrate.compose_call(
216
+ call_module="Swap",
217
+ call_function="toggle_user_liquidity",
218
+ call_params={"netuid": netuid, "enable": enable},
219
+ )
220
+
221
+ return await subtensor.sign_and_send_extrinsic(
222
+ call=call,
223
+ wallet=wallet,
224
+ wait_for_inclusion=wait_for_inclusion,
225
+ wait_for_finalization=wait_for_finalization,
226
+ )
227
+
228
+
229
+ # Command
230
+ async def add_liquidity(
231
+ subtensor: "SubtensorInterface",
232
+ wallet: "Wallet",
233
+ hotkey_ss58: str,
234
+ netuid: Optional[int],
235
+ liquidity: Optional[float],
236
+ price_low: Optional[float],
237
+ price_high: Optional[float],
238
+ prompt: bool,
239
+ json_output: bool,
240
+ ) -> tuple[bool, str]:
241
+ """Add liquidity position to provided subnet."""
242
+ # Check wallet access
243
+ if not unlock_key(wallet).success:
244
+ return False
245
+
246
+ # Check that the subnet exists.
247
+ if not await subtensor.subnet_exists(netuid=netuid):
248
+ return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}."
249
+
250
+ if prompt:
251
+ console.print(
252
+ "You are about to add a LiquidityPosition with:\n"
253
+ f"\tliquidity: {liquidity}\n"
254
+ f"\tprice low: {price_low}\n"
255
+ f"\tprice high: {price_high}\n"
256
+ f"\tto SN: {netuid}\n"
257
+ f"\tusing wallet with name: {wallet.name}"
258
+ )
259
+
260
+ if not Confirm.ask("Would you like to continue?"):
261
+ return False, "User cancelled operation."
262
+
263
+ success, message = await add_liquidity_extrinsic(
264
+ subtensor=subtensor,
265
+ wallet=wallet,
266
+ hotkey_ss58=hotkey_ss58,
267
+ netuid=netuid,
268
+ liquidity=liquidity,
269
+ price_low=price_low,
270
+ price_high=price_high,
271
+ )
272
+ if json_output:
273
+ json_console.print(json.dumps({"success": success, "message": message}))
274
+ else:
275
+ if success:
276
+ console.print(
277
+ "[green]LiquidityPosition has been successfully added.[/green]"
278
+ )
279
+ else:
280
+ err_console.print(f"[red]Error: {message}[/red]")
281
+
282
+
283
+ async def get_liquidity_list(
284
+ subtensor: "SubtensorInterface",
285
+ wallet: "Wallet",
286
+ netuid: Optional[int],
287
+ ) -> tuple[bool, str, list]:
288
+ """
289
+ Args:
290
+ wallet: wallet object
291
+ subtensor: SubtensorInterface object
292
+ netuid: the netuid to stake to (None indicates all subnets)
293
+
294
+ Returns:
295
+ Tuple of (success, error message, liquidity list)
296
+ """
297
+
298
+ if not await subtensor.subnet_exists(netuid=netuid):
299
+ return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}.", []
300
+
301
+ if not await subtensor.is_subnet_active(netuid=netuid):
302
+ return False, f"Subnet with netuid: {netuid} is not active in {subtensor}.", []
303
+
304
+ block_hash = await subtensor.substrate.get_chain_head()
305
+ (
306
+ positions_response,
307
+ fee_global_tao,
308
+ fee_global_alpha,
309
+ current_sqrt_price,
310
+ ) = await asyncio.gather(
311
+ subtensor.substrate.query_map(
312
+ module="Swap",
313
+ storage_function="Positions",
314
+ params=[netuid, wallet.coldkeypub.ss58_address],
315
+ block_hash=block_hash,
316
+ ),
317
+ subtensor.query(
318
+ module="Swap",
319
+ storage_function="FeeGlobalTao",
320
+ params=[netuid],
321
+ block_hash=block_hash,
322
+ ),
323
+ subtensor.query(
324
+ module="Swap",
325
+ storage_function="FeeGlobalAlpha",
326
+ params=[netuid],
327
+ block_hash=block_hash,
328
+ ),
329
+ subtensor.query(
330
+ module="Swap",
331
+ storage_function="AlphaSqrtPrice",
332
+ params=[netuid],
333
+ block_hash=block_hash,
334
+ ),
335
+ )
336
+
337
+ current_sqrt_price = fixed_to_float(current_sqrt_price)
338
+ fee_global_tao = fixed_to_float(fee_global_tao)
339
+ fee_global_alpha = fixed_to_float(fee_global_alpha)
340
+
341
+ current_price = current_sqrt_price * current_sqrt_price
342
+ current_tick = price_to_tick(current_price)
343
+
344
+ preprocessed_positions = []
345
+ positions_futures = []
346
+
347
+ async for _, p in positions_response:
348
+ position = p.value
349
+ tick_index_low = position.get("tick_low")[0]
350
+ tick_index_high = position.get("tick_high")[0]
351
+ preprocessed_positions.append((position, tick_index_low, tick_index_high))
352
+
353
+ # Get ticks for the position (for below/above fees)
354
+ positions_futures.append(
355
+ asyncio.gather(
356
+ subtensor.query(
357
+ module="Swap",
358
+ storage_function="Ticks",
359
+ params=[netuid, tick_index_low],
360
+ block_hash=block_hash,
361
+ ),
362
+ subtensor.query(
363
+ module="Swap",
364
+ storage_function="Ticks",
365
+ params=[netuid, tick_index_high],
366
+ block_hash=block_hash,
367
+ ),
368
+ )
369
+ )
370
+
371
+ awaited_futures = await asyncio.gather(*positions_futures)
372
+
373
+ positions = []
374
+
375
+ for (position, tick_index_low, tick_index_high), (tick_low, tick_high) in zip(
376
+ preprocessed_positions, awaited_futures
377
+ ):
378
+ tao_fees_below_low = get_fees(
379
+ current_tick=current_tick,
380
+ tick=tick_low,
381
+ tick_index=tick_index_low,
382
+ quote=True,
383
+ global_fees_tao=fee_global_tao,
384
+ global_fees_alpha=fee_global_alpha,
385
+ above=False,
386
+ )
387
+ tao_fees_above_high = get_fees(
388
+ current_tick=current_tick,
389
+ tick=tick_high,
390
+ tick_index=tick_index_high,
391
+ quote=True,
392
+ global_fees_tao=fee_global_tao,
393
+ global_fees_alpha=fee_global_alpha,
394
+ above=True,
395
+ )
396
+ alpha_fees_below_low = get_fees(
397
+ current_tick=current_tick,
398
+ tick=tick_low,
399
+ tick_index=tick_index_low,
400
+ quote=False,
401
+ global_fees_tao=fee_global_tao,
402
+ global_fees_alpha=fee_global_alpha,
403
+ above=False,
404
+ )
405
+ alpha_fees_above_high = get_fees(
406
+ current_tick=current_tick,
407
+ tick=tick_high,
408
+ tick_index=tick_index_high,
409
+ quote=False,
410
+ global_fees_tao=fee_global_tao,
411
+ global_fees_alpha=fee_global_alpha,
412
+ above=True,
413
+ )
414
+
415
+ # Get position accrued fees
416
+ fees_tao, fees_alpha = calculate_fees(
417
+ position=position,
418
+ global_fees_tao=fee_global_tao,
419
+ global_fees_alpha=fee_global_alpha,
420
+ tao_fees_below_low=tao_fees_below_low,
421
+ tao_fees_above_high=tao_fees_above_high,
422
+ alpha_fees_below_low=alpha_fees_below_low,
423
+ alpha_fees_above_high=alpha_fees_above_high,
424
+ netuid=netuid,
425
+ )
426
+
427
+ lp = LiquidityPosition(
428
+ **{
429
+ "id": position.get("id")[0],
430
+ "price_low": Balance.from_tao(
431
+ tick_to_price(position.get("tick_low")[0])
432
+ ),
433
+ "price_high": Balance.from_tao(
434
+ tick_to_price(position.get("tick_high")[0])
435
+ ),
436
+ "liquidity": Balance.from_rao(position.get("liquidity")),
437
+ "fees_tao": fees_tao,
438
+ "fees_alpha": fees_alpha,
439
+ "netuid": position.get("netuid"),
440
+ }
441
+ )
442
+ positions.append(lp)
443
+
444
+ return True, "", positions
445
+
446
+
447
+ async def show_liquidity_list(
448
+ subtensor: "SubtensorInterface",
449
+ wallet: "Wallet",
450
+ netuid: int,
451
+ json_output: bool = False,
452
+ ):
453
+ current_price_, (success, err_msg, positions) = await asyncio.gather(
454
+ subtensor.subnet(netuid=netuid), get_liquidity_list(subtensor, wallet, netuid)
455
+ )
456
+ if not success:
457
+ if json_output:
458
+ json_console.print(
459
+ json.dumps({"success": success, "err_msg": err_msg, "positions": []})
460
+ )
461
+ return False
462
+ else:
463
+ err_console.print(f"Error: {err_msg}")
464
+ return False
465
+ liquidity_table = Table(
466
+ Column("ID", justify="center"),
467
+ Column("Liquidity", justify="center"),
468
+ Column("Alpha", justify="center"),
469
+ Column("Tao", justify="center"),
470
+ Column("Price low", justify="center"),
471
+ Column("Price high", justify="center"),
472
+ Column("Fee TAO", justify="center"),
473
+ Column("Fee Alpha", justify="center"),
474
+ title=f"\n[{COLORS.G.HEADER}]{'Liquidity Positions of '}{wallet.name} wallet in SN #{netuid}\n"
475
+ "Alpha and Tao columns are respective portions of liquidity.",
476
+ show_footer=False,
477
+ show_edge=True,
478
+ header_style="bold white",
479
+ border_style="bright_black",
480
+ style="bold",
481
+ title_justify="center",
482
+ show_lines=False,
483
+ pad_edge=True,
484
+ )
485
+ json_table = []
486
+ current_price = current_price_.price
487
+ lp: LiquidityPosition
488
+ for lp in positions:
489
+ alpha, tao = lp.to_token_amounts(current_price)
490
+ liquidity_table.add_row(
491
+ str(lp.id),
492
+ str(lp.liquidity.tao),
493
+ str(alpha),
494
+ str(tao),
495
+ str(lp.price_low),
496
+ str(lp.price_high),
497
+ str(lp.fees_tao),
498
+ str(lp.fees_alpha),
499
+ )
500
+ json_table.append(
501
+ {
502
+ "id": lp.id,
503
+ "liquidity": lp.liquidity.tao,
504
+ "token_amounts": {"alpha": alpha.tao, "tao": tao.tao},
505
+ "price_low": lp.price_low.tao,
506
+ "price_high": lp.price_high.tao,
507
+ "fees_tao": lp.fees_tao.tao,
508
+ "fees_alpha": lp.fees_alpha.tao,
509
+ "netuid": lp.netuid,
510
+ }
511
+ )
512
+ if not json_output:
513
+ console.print(liquidity_table)
514
+ else:
515
+ json_console.print(
516
+ json.dumps({"success": True, "err_msg": "", "positions": json_table})
517
+ )
518
+
519
+
520
+ async def remove_liquidity(
521
+ subtensor: "SubtensorInterface",
522
+ wallet: "Wallet",
523
+ hotkey_ss58: str,
524
+ netuid: int,
525
+ position_id: Optional[int] = None,
526
+ prompt: Optional[bool] = None,
527
+ all_liquidity_ids: Optional[bool] = None,
528
+ json_output: bool = False,
529
+ ) -> tuple[bool, str]:
530
+ """Remove liquidity position from provided subnet."""
531
+ if not await subtensor.subnet_exists(netuid=netuid):
532
+ return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}."
533
+
534
+ if all_liquidity_ids:
535
+ success, msg, positions = await get_liquidity_list(subtensor, wallet, netuid)
536
+ if not success:
537
+ if json_output:
538
+ return json_console.print(
539
+ {"success": False, "err_msg": msg, "positions": positions}
540
+ )
541
+ else:
542
+ return err_console.print(f"Error: {msg}")
543
+ else:
544
+ position_ids = [p.id for p in positions]
545
+ else:
546
+ position_ids = [position_id]
547
+
548
+ if prompt:
549
+ console.print("You are about to remove LiquidityPositions with:")
550
+ console.print(f"\tSubnet: {netuid}")
551
+ console.print(f"\tWallet name: {wallet.name}")
552
+ for pos in position_ids:
553
+ console.print(f"\tPosition id: {pos}")
554
+
555
+ if not Confirm.ask("Would you like to continue?"):
556
+ return False, "User cancelled operation."
557
+
558
+ results = await asyncio.gather(
559
+ *[
560
+ remove_liquidity_extrinsic(
561
+ subtensor=subtensor,
562
+ wallet=wallet,
563
+ hotkey_ss58=hotkey_ss58,
564
+ netuid=netuid,
565
+ position_id=pos_id,
566
+ )
567
+ for pos_id in position_ids
568
+ ]
569
+ )
570
+ if not json_output:
571
+ for (success, msg), posid in zip(results, position_ids):
572
+ if success:
573
+ console.print(f"[green] Position {posid} has been removed.")
574
+ else:
575
+ err_console.print(f"[red] Error removing {posid}: {msg}")
576
+ else:
577
+ json_table = {}
578
+ for (success, msg), posid in zip(results, position_ids):
579
+ json_table[posid] = {"success": success, "err_msg": msg}
580
+ json_console.print(json.dumps(json_table))
581
+
582
+
583
+ async def modify_liquidity(
584
+ subtensor: "SubtensorInterface",
585
+ wallet: "Wallet",
586
+ hotkey_ss58: str,
587
+ netuid: int,
588
+ position_id: int,
589
+ liquidity_delta: Optional[float],
590
+ prompt: Optional[bool] = None,
591
+ json_output: bool = False,
592
+ ) -> bool:
593
+ """Modify liquidity position in provided subnet."""
594
+ if not await subtensor.subnet_exists(netuid=netuid):
595
+ err_msg = f"Subnet with netuid: {netuid} does not exist in {subtensor}."
596
+ if json_output:
597
+ json_console.print(json.dumps({"success": False, "err_msg": err_msg}))
598
+ else:
599
+ err_console.print(err_msg)
600
+ return False
601
+
602
+ if prompt:
603
+ console.print(
604
+ "You are about to modify a LiquidityPosition with:"
605
+ f"\tSubnet: {netuid}\n"
606
+ f"\tPosition id: {position_id}\n"
607
+ f"\tWallet name: {wallet.name}\n"
608
+ f"\tLiquidity delta: {liquidity_delta}"
609
+ )
610
+
611
+ if not Confirm.ask("Would you like to continue?"):
612
+ return False
613
+
614
+ success, msg = await modify_liquidity_extrinsic(
615
+ subtensor=subtensor,
616
+ wallet=wallet,
617
+ hotkey_ss58=hotkey_ss58,
618
+ netuid=netuid,
619
+ position_id=position_id,
620
+ liquidity_delta=liquidity_delta,
621
+ )
622
+ if json_output:
623
+ json_console.print(json.dumps({"success": success, "err_msg": msg}))
624
+ else:
625
+ if success:
626
+ console.print(f"[green] Position {position_id} has been modified.")
627
+ else:
628
+ err_console.print(f"[red] Error modifying {position_id}: {msg}")