charli3_dendrite 1.4.7.dev0__tar.gz → 1.4.8.dev0__tar.gz

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 (41) hide show
  1. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/PKG-INFO +1 -1
  2. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/pyproject.toml +2 -2
  3. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/amm_types.py +145 -20
  4. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/dano.py +26 -139
  5. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/LICENSE +0 -0
  6. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/README.md +0 -0
  7. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/__init__.py +0 -0
  8. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/__init__.py +0 -0
  9. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/backend_base.py +0 -0
  10. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/blockfrost/__init__.py +0 -0
  11. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/blockfrost/models.py +0 -0
  12. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/dbsync/__init__.py +0 -0
  13. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/dbsync/models.py +0 -0
  14. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/ogmios_kupo/__init__.py +0 -0
  15. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/ogmios_kupo/models.py +0 -0
  16. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/utils.py +0 -0
  17. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dataclasses/__init__.py +0 -0
  18. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dataclasses/datums.py +0 -0
  19. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dataclasses/models.py +0 -0
  20. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/__init__.py +0 -0
  21. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/__init__.py +0 -0
  22. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/amm_base.py +0 -0
  23. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/cswap.py +0 -0
  24. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/minswap.py +0 -0
  25. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/muesli.py +0 -0
  26. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/spectrum.py +0 -0
  27. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/splash.py +0 -0
  28. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/sundae.py +0 -0
  29. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/vyfi.py +0 -0
  30. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/wingriders.py +0 -0
  31. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/core/__init__.py +0 -0
  32. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/core/base.py +0 -0
  33. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/core/errors.py +0 -0
  34. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/__init__.py +0 -0
  35. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/axo.py +0 -0
  36. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/chadswap.py +0 -0
  37. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/djed.py +0 -0
  38. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/geniusyield.py +0 -0
  39. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/ob_base.py +0 -0
  40. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/saturnswap.py +0 -0
  41. {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: charli3_dendrite
3
- Version: 1.4.7.dev0
3
+ Version: 1.4.8.dev0
4
4
  Summary:
5
5
  License-File: LICENSE
6
6
  Author: Elder Millenial
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "charli3_dendrite"
3
- version = "1.4.7-dev0"
3
+ version = "1.4.8-dev0"
4
4
  description = ""
5
5
  authors = ["Elder Millenial <eldermillenial@protonmail.com>"]
6
6
  readme = "README.md"
@@ -49,7 +49,7 @@ module = ["pycardano", "pycardano.*", "dotenv", "nox"]
49
49
  ignore_missing_imports = true
50
50
 
51
51
  [tool.bumpversion]
52
- current_version = "1.4.7-dev0"
52
+ current_version = "1.4.8-dev0"
53
53
  parse = """(?x)
54
54
  (?P<major>\\d+)\\.
55
55
  (?P<minor>\\d+)\\.
@@ -5,6 +5,7 @@ from typing import ClassVar
5
5
 
6
6
  from charli3_dendrite.dataclasses.models import Assets
7
7
  from charli3_dendrite.dexs.amm.amm_base import AbstractPoolState
8
+ from charli3_dendrite.dexs.core.errors import InvalidPoolError
8
9
 
9
10
  N_COINS = 2
10
11
 
@@ -269,7 +270,7 @@ class AbstractStableSwapPoolState(AbstractPoolState):
269
270
  ValueError: If the input asset is invalid or if multiple input
270
271
  assets are provided.
271
272
  """
272
- volume_fee: int = 0
273
+ volume_fee: int | float = 0
273
274
  if self.volume_fee is not None:
274
275
  if isinstance(self.volume_fee, (int, float)):
275
276
  volume_fee = self.volume_fee
@@ -336,7 +337,7 @@ class AbstractStableSwapPoolState(AbstractPoolState):
336
337
  ValueError: If the output asset is invalid or if multiple output
337
338
  assets are provided.
338
339
  """
339
- volume_fee: int = 0
340
+ volume_fee: int | float = 0
340
341
  if self.volume_fee is not None:
341
342
  if isinstance(self.volume_fee, (int, float)):
342
343
  volume_fee = self.volume_fee
@@ -390,48 +391,172 @@ class AbstractCommonStableSwapPoolState(AbstractStableSwapPoolState):
390
391
 
391
392
 
392
393
  class AbstractConstantLiquidityPoolState(AbstractPoolState):
393
- """Represents the state of a constant liquidity pool automated market maker (AMM).
394
-
395
- This class serves as a base for constant liquidity pool implementations, providing
396
- methods to calculate the input and output asset amounts for swaps.
394
+ """State of a single-band concentrated-liquidity ("constant liquidity") AMM pool.
395
+
396
+ A concentrated-liquidity position holds liquidity only within one
397
+ ``[sqrt_lower, sqrt_upper]`` price band. Inside the band the curve is a constant
398
+ product on VIRTUAL reserves ``(a_v, b_v)`` extrapolated from the band bounds; a
399
+ swap large enough to push the price past the band is capped at the band's
400
+ available output (a single UTxO cannot fill beyond its range). This base
401
+ implements that math against the standard ``reserve_a``/``reserve_b``/``unit_a``/
402
+ ``unit_b``/``volume_fee`` interface — exactly as
403
+ :class:`AbstractConstantProductPoolState` does for plain CPP and
404
+ :class:`AbstractStableSwapPoolState` does for stable swaps. Concrete CLMM DEXs
405
+ (Dano, Sundae V4) supply only the per-band datum specifics via the two hooks
406
+ below (``_sqrt_price_bounds`` and, where they net fees/carve-outs, the
407
+ ``reserve_a``/``reserve_b`` overrides), mirroring how the stable base exposes
408
+ ``amp``/``_get_ann``.
397
409
  """
398
410
 
411
+ fee_basis: int = 10000
412
+
413
+ # ── per-DEX hooks (the analogue of the stable base's amp/_get_ann) ────────
414
+ def _sqrt_price_bounds(self) -> tuple[tuple[int, int], tuple[int, int]]:
415
+ """Return ``((lower_num, lower_den), (upper_num, upper_den))``.
416
+
417
+ The square-root price band bounds as exact integer ratios, read from the
418
+ pool datum: ``lower`` is ``sqrt(P_a)``, ``upper`` is ``sqrt(P_b)``.
419
+ """
420
+ raise NotImplementedError
421
+
422
+ @property
423
+ def _lp_fee_rate(self) -> int:
424
+ """LP fee rate in ``fee_basis`` units — the only fee affecting swap output.
425
+
426
+ Defaults to ``volume_fee`` (which surfaces ``fee``); a concentrated-liquidity
427
+ position carries one symmetric LP fee. Override if a DEX encodes a per-side
428
+ fee list. Any platform/protocol fee skims the LP's cut, not the swapper's
429
+ output, so it is deliberately excluded here.
430
+ """
431
+ fee = self.volume_fee
432
+ if fee is None:
433
+ return 0
434
+ if isinstance(fee, (list, tuple)):
435
+ return int(fee[0])
436
+ return int(fee)
437
+
438
+ # ── generic single-band CLMM math ────────────────────────────────────────
439
+ def virtual_reserves(self) -> tuple[int, int]:
440
+ """Virtual reserves ``(a_v, b_v)`` of the active band (aligned to a, b).
441
+
442
+ The constant-product reserves the band's liquidity ``L`` is equivalent to
443
+ within ``[sqrt_lower, sqrt_upper]`` (the Uniswap-V3 single-band identity
444
+ ``a_v = a + L/sqrt(P_b)``, ``b_v = b + L*sqrt(P_a)``), solved in exact integer
445
+ arithmetic. The ``a_v / b_v`` ratio is the band's marginal price.
446
+ """
447
+ a, b = self.reserve_a, self.reserve_b
448
+ (pa_n, pa_d), (pb_n, pb_d) = self._sqrt_price_bounds()
449
+ den_ab = pa_d * pb_d
450
+ num_ab = pa_n * pb_n
451
+ diff = b * den_ab - a * num_ab
452
+ big = math.isqrt(diff * diff + 4 * a * b * pa_d * pa_d * pb_n * pb_n)
453
+ liq_num = b * den_ab + a * num_ab + big
454
+ liq_den = 2 * (pb_n * pa_d - pb_d * pa_n)
455
+ # ceilDiv for each virtual-reserve offset (``-(-n // d)``).
456
+ a_v = -(-(liq_num * pb_d) // (liq_den * pb_n)) + a
457
+ b_v = -(-(liq_num * pa_n) // (liq_den * pa_d)) + b
458
+ return a_v, b_v
459
+
399
460
  def get_amount_out(
400
461
  self,
401
462
  asset: Assets,
402
463
  precise: bool = True,
403
464
  ) -> tuple[Assets, float]:
404
- """Calculate the output amount for a given input in a constant liquidity pool.
465
+ """Output amount + price impact for an input ``asset`` (capped at the band).
405
466
 
406
467
  Args:
407
468
  asset (Assets): The input asset amount for the swap.
408
- precise (bool): If True: the output rounded to the nearest integer.
469
+ precise (bool): Accepted for interface parity; the output is always the
470
+ integer floor (a single band trades whole units).
409
471
 
410
472
  Returns:
411
- tuple[Assets, float]: Tuple containing the output asset and float value.
412
-
413
- Raises:
414
- NotImplementedError: This method is not implemented in the base class.
473
+ tuple[Assets, float]: The output asset and the price-impact ratio.
415
474
  """
416
- error_msg = "CLPP amount out is not yet implemented."
417
- raise NotImplementedError(error_msg)
475
+ if len(asset) != 1 or asset.unit() not in (self.unit_a, self.unit_b):
476
+ error_msg = f"Invalid input asset for pool: {asset}"
477
+ raise ValueError(error_msg)
478
+
479
+ a_v, b_v = self.virtual_reserves()
480
+ if asset.unit() == self.unit_a:
481
+ in_v, out_v, out_real, out_unit = a_v, b_v, self.reserve_b, self.unit_b
482
+ else:
483
+ in_v, out_v, out_real, out_unit = b_v, a_v, self.reserve_a, self.unit_a
484
+
485
+ amount_in = asset.quantity()
486
+ off_fee = self.fee_basis - self._lp_fee_rate
487
+ denominator = in_v * self.fee_basis + amount_in * off_fee
488
+ if denominator <= 0:
489
+ # Degenerate band (virtual in-reserve 0) probed with zero input: no
490
+ # output, no division. Guarding above the floor-division lets a
491
+ # parked-band probe return (0, 0.0) rather than ZeroDivisionError.
492
+ return Assets(**{out_unit: 0}), 0.0
493
+ numerator = out_v * denominator - in_v * out_v * self.fee_basis
494
+ # Capacity cap: one band holds only ``out_real`` of the output token; a
495
+ # larger swap would push price past the band, which this UTxO cannot fill.
496
+ # Cap the fill (order-book convention) rather than raise — callers route
497
+ # the remainder to other bands. A band parked at its edge has
498
+ # ``out_real == 0`` and correctly yields zero output.
499
+ expected_out = min(numerator // denominator, out_real)
500
+ out_assets = Assets(**{out_unit: expected_out})
501
+ if not precise:
502
+ out_assets.root[out_unit] = expected_out
503
+
504
+ if amount_in == 0 or expected_out == 0:
505
+ return out_assets, 0.0
506
+ spot = out_v / in_v
507
+ effective = expected_out / amount_in
508
+ return out_assets, 1.0 - (effective / spot)
418
509
 
419
510
  def get_amount_in(
420
511
  self,
421
512
  asset: Assets,
422
513
  precise: bool = True,
423
514
  ) -> tuple[Assets, float]:
424
- """Calculate input amount needed for desired output in constant liquidity pool.
515
+ """Minimum input + price impact to obtain a desired output ``asset``.
516
+
517
+ Algebraic inverse of :meth:`get_amount_out` in-band:
518
+ ``in_min = ceil(in_v * basis * out / ((out_v - out) * offFee))``.
425
519
 
426
520
  Args:
427
521
  asset (Assets): The desired output asset amount for the swap.
428
- precise (bool): If True: the output rounded to the nearest integer.
522
+ precise (bool): Accepted for interface parity; the input is always the
523
+ integer ceiling (the minimal whole-unit input).
429
524
 
430
525
  Returns:
431
- tuple[Assets, float]: Tuple containing required input asset and float value.
526
+ tuple[Assets, float]: The required input asset and the price-impact ratio.
432
527
 
433
528
  Raises:
434
- NotImplementedError: This method is not implemented in the base class.
529
+ InvalidPoolError: If the desired output exceeds the band's available
530
+ reserve (it cannot be filled from this UTxO).
435
531
  """
436
- error_msg = "CLPP amount in is not yet implemented."
437
- raise NotImplementedError(error_msg)
532
+ if len(asset) != 1 or asset.unit() not in (self.unit_a, self.unit_b):
533
+ error_msg = f"Invalid output asset for pool: {asset}"
534
+ raise ValueError(error_msg)
535
+ desired_out = asset.quantity()
536
+ if desired_out <= 0:
537
+ error_msg = "desired output must be positive"
538
+ raise ValueError(error_msg)
539
+
540
+ a_v, b_v = self.virtual_reserves()
541
+ off_fee = self.fee_basis - self._lp_fee_rate
542
+ if asset.unit() == self.unit_b:
543
+ out_real, in_v, out_v, in_unit = self.reserve_b, a_v, b_v, self.unit_a
544
+ else:
545
+ out_real, in_v, out_v, in_unit = self.reserve_a, b_v, a_v, self.unit_b
546
+ if desired_out >= out_real:
547
+ error_msg = (
548
+ f"Desired output {desired_out} exceeds available reserve {out_real}"
549
+ )
550
+ raise InvalidPoolError(error_msg)
551
+ amount_in = -(
552
+ -(in_v * self.fee_basis * desired_out) // ((out_v - desired_out) * off_fee)
553
+ )
554
+ in_assets = Assets(**{in_unit: amount_in})
555
+ if not precise:
556
+ in_assets.root[in_unit] = amount_in
557
+
558
+ if amount_in == 0:
559
+ return in_assets, 0.0
560
+ spot = out_v / in_v
561
+ effective = desired_out / amount_in
562
+ return in_assets, 1.0 - (effective / spot)
@@ -45,7 +45,7 @@ from charli3_dendrite.dataclasses.datums import OrderType
45
45
  from charli3_dendrite.dataclasses.datums import PoolDatum
46
46
  from charli3_dendrite.dataclasses.models import Assets
47
47
  from charli3_dendrite.dataclasses.models import PoolSelector
48
- from charli3_dendrite.dexs.amm.amm_base import AbstractPoolState
48
+ from charli3_dendrite.dexs.amm.amm_types import AbstractConstantLiquidityPoolState
49
49
  from charli3_dendrite.dexs.core.errors import InvalidPoolError
50
50
  from charli3_dendrite.utility import asset_to_value
51
51
 
@@ -361,8 +361,16 @@ class DanoOrderDatum(OrderDatum):
361
361
  raise NotImplementedError("Dano does not use order datums")
362
362
 
363
363
 
364
- class DanoCLMMState(AbstractPoolState):
365
- """State of a single Dano concentrated-liquidity pool."""
364
+ class DanoCLMMState(AbstractConstantLiquidityPoolState):
365
+ """State of a single Dano concentrated-liquidity pool.
366
+
367
+ The single-band CLMM math (``virtual_reserves`` + ``get_amount_out``/
368
+ ``get_amount_in`` + the capacity cap) is inherited from
369
+ :class:`AbstractConstantLiquidityPoolState`; this class supplies only the
370
+ Dano-specific datum parsing, active-reserve carve-outs, and direct-spend
371
+ tx-building. The LP fee is surfaced as ``fee`` in :meth:`post_init` so the
372
+ base reads it via ``volume_fee``/``_lp_fee_rate``.
373
+ """
366
374
 
367
375
  fee: int = 0 # populated from datum.lp_fee_rate in post_init
368
376
 
@@ -562,143 +570,22 @@ class DanoCLMMState(AbstractPoolState):
562
570
  """Active reserve of tokenY, net of platform fees."""
563
571
  return self.raw_y - self._datum.platform_fee_y
564
572
 
565
- # --- math (port of utils.ts:calculateConcentratedPoolSwap) -------------
566
-
567
- def _virtual_reserves(self) -> tuple[int, int]:
568
- """Compute virtual reserves (xV, yV) used by the CLMM invariant.
569
-
570
- Mirrors `calcLiquidity` + the xV/yV step in utils.ts.
571
- """
572
- d = self._datum
573
- x = self.reserve_a
574
- y = self.reserve_b
575
- pa_n, pa_d = d.sqrt_lower_price.numerator, d.sqrt_lower_price.denominator
576
- pb_n, pb_d = d.sqrt_upper_price.numerator, d.sqrt_upper_price.denominator
577
-
578
- den_a_den_b = pa_d * pb_d
579
- num_a_num_b = pa_n * pb_n
580
-
581
- diff = y * den_a_den_b - x * num_a_num_b
582
- big = isqrt(diff * diff + 4 * x * y * pa_d * pa_d * pb_n * pb_n)
583
-
584
- liq_num = y * den_a_den_b + x * num_a_num_b + big
585
- liq_den = 2 * (pb_n * pa_d - pb_d * pa_n)
586
-
587
- # ceilDiv equivalents
588
- x_v = -(-(liq_num * pb_d) // (liq_den * pb_n)) + x
589
- y_v = -(-(liq_num * pa_n) // (liq_den * pa_d)) + y
590
- return x_v, y_v
591
-
592
- def _swap_out(
593
- self,
594
- amount_in: int,
595
- in_virtual: int,
596
- out_virtual: int,
597
- out_real: int,
598
- ) -> tuple[int, int]:
599
- """Port of utils.ts:getPoolChange. Returns (out_amount, platform_fee)."""
600
- lp_fee = (amount_in * self._datum.lp_fee_rate) // FEE_BASIS
601
- platform_fee = (lp_fee * self.platform_fee_rate) // FEE_BASIS
602
- off_fee = FEE_BASIS - self._datum.lp_fee_rate
603
-
604
- denominator = in_virtual * FEE_BASIS + amount_in * off_fee
605
- numerator = out_virtual * denominator - in_virtual * out_virtual * FEE_BASIS
606
- expected_out = numerator // denominator
607
-
608
- # Concentrated-liquidity capacity cap. A single range holds only
609
- # ``out_real`` of the output token; a larger swap would push the price
610
- # past this range's ``[sqrt_lower, sqrt_upper]`` band, which this UTxO
611
- # cannot fill. Return the capacity (the maximum obtainable output)
612
- # rather than raising, mirroring the order-book convention
613
- # (``ob_base`` get_amount_out caps the fill at ``available``). Callers
614
- # route any remainder to other ranges, and recover the minimal input
615
- # for this capped output via ``get_amount_in``. A range parked at its
616
- # band edge has ``out_real == 0`` and correctly yields zero output.
617
- expected_out = min(expected_out, out_real)
618
-
619
- return expected_out, platform_fee
620
-
621
- def get_amount_out(self, asset: Assets) -> tuple[Assets, float]:
622
- """Return the output amount and price impact for a given input asset."""
623
- d = self._datum
624
- if len(asset) != 1 or asset.unit() not in (d.unit_x, d.unit_y):
625
- raise ValueError(f"Invalid input asset for pool: {asset}")
626
-
627
- x_v, y_v = self._virtual_reserves()
628
- if asset.unit() == d.unit_x:
629
- in_v, out_v, out_real, out_unit = x_v, y_v, self.reserve_b, d.unit_y
630
- else:
631
- in_v, out_v, out_real, out_unit = y_v, x_v, self.reserve_a, d.unit_x
632
-
633
- out_qty, _ = self._swap_out(asset.quantity(), in_v, out_v, out_real)
634
- out_assets = Assets(**{out_unit: out_qty})
635
-
636
- # Price impact: 1 - (effective_price / spot_price)
637
- # Approximated against virtual reserves.
638
- if asset.quantity() == 0 or out_qty == 0:
639
- return out_assets, 0.0
640
- spot = out_v / in_v
641
- effective = out_qty / asset.quantity()
642
- price_impact = 1.0 - (effective / spot)
643
- return out_assets, price_impact
644
-
645
- def get_amount_in(self, asset: Assets) -> tuple[Assets, float]:
646
- """Algebraic inverse of get_amount_out.
647
-
648
- Given a desired output ``asset`` quantity, return the minimum input
649
- amount required and the price-impact ratio.
650
-
651
- Derivation (from spec § Redeemer: Swap, X→Y direction):
652
- expected_out = Yv - floor(Xv*Yv*basis / (Xv*basis + dx*offFee))
653
- Solving for ``dx`` such that expected_out >= desired_out and
654
- rounding up gives:
655
- dx_min = ceil(Xv*basis*desired_out / ((Yv - desired_out) * offFee))
656
- The Y→X case is symmetric (swap the roles of Xv and Yv).
657
- """
573
+ # --- math (single-band CLMM curve inherited from the base) -------------
574
+ # ``virtual_reserves()`` + ``get_amount_out()``/``get_amount_in()`` + the
575
+ # capacity cap live on ``AbstractConstantLiquidityPoolState``; Dano supplies
576
+ # only the band bounds below. ``unit_a``/``unit_b`` == tokenX/tokenY and
577
+ # ``reserve_a``/``reserve_b`` are the active (carve-out-netted) reserves, so
578
+ # the base reproduces the original utils.ts:calculateConcentratedPoolSwap
579
+ # port exactly. The tx-build path (``compute_pool_change``) keeps its own
580
+ # virtual-reserve computation because it must net an in-tx staking reward.
581
+
582
+ def _sqrt_price_bounds(self) -> tuple[tuple[int, int], tuple[int, int]]:
583
+ """Band sqrt-price bounds: ``((lower_n, lower_d), (upper_n, upper_d))``."""
658
584
  d = self._datum
659
- if len(asset) != 1 or asset.unit() not in (d.unit_x, d.unit_y):
660
- raise ValueError(f"Invalid output asset for pool: {asset}")
661
- desired_out = asset.quantity()
662
- if desired_out <= 0:
663
- raise ValueError("desired output must be positive")
664
-
665
- x_v, y_v = self._virtual_reserves()
666
- pool_in_lp_x, pool_in_lp_y = self.active_liquidity()
667
- off_fee = FEE_BASIS - d.lp_fee_rate
668
-
669
- if asset.unit() == d.unit_y:
670
- # User wants Y out, must pay X in.
671
- if desired_out >= pool_in_lp_y:
672
- raise InvalidPoolError(
673
- f"Desired Y output {desired_out} exceeds available "
674
- f"Y reserve {pool_in_lp_y}",
675
- )
676
- in_v, out_v = x_v, y_v
677
- in_unit = d.unit_x
678
- amount_in = -(
679
- -(x_v * FEE_BASIS * desired_out) // ((y_v - desired_out) * off_fee)
680
- )
681
- else:
682
- # User wants X out, must pay Y in.
683
- if desired_out >= pool_in_lp_x:
684
- raise InvalidPoolError(
685
- f"Desired X output {desired_out} exceeds available "
686
- f"X reserve {pool_in_lp_x}",
687
- )
688
- in_v, out_v = y_v, x_v
689
- in_unit = d.unit_y
690
- amount_in = -(
691
- -(y_v * FEE_BASIS * desired_out) // ((x_v - desired_out) * off_fee)
692
- )
693
-
694
- in_assets = Assets(**{in_unit: amount_in})
695
-
696
- if amount_in == 0:
697
- return in_assets, 0.0
698
- spot = out_v / in_v
699
- effective = desired_out / amount_in
700
- price_impact = 1.0 - (effective / spot)
701
- return in_assets, price_impact
585
+ return (
586
+ (d.sqrt_lower_price.numerator, d.sqrt_lower_price.denominator),
587
+ (d.sqrt_upper_price.numerator, d.sqrt_upper_price.denominator),
588
+ )
702
589
 
703
590
  # --- spec-driven helpers ----------------------------------------------
704
591