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.
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/PKG-INFO +1 -1
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/pyproject.toml +2 -2
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/amm_types.py +145 -20
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/dano.py +26 -139
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/LICENSE +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/README.md +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/__init__.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/__init__.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/backend_base.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/blockfrost/__init__.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/blockfrost/models.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/dbsync/__init__.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/dbsync/models.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/ogmios_kupo/__init__.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/ogmios_kupo/models.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/utils.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dataclasses/__init__.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dataclasses/datums.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dataclasses/models.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/__init__.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/__init__.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/amm_base.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/cswap.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/minswap.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/muesli.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/spectrum.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/splash.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/sundae.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/vyfi.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/wingriders.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/core/__init__.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/core/base.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/core/errors.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/__init__.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/axo.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/chadswap.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/djed.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/geniusyield.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/ob_base.py +0 -0
- {charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/saturnswap.py +0 -0
- {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
|
[tool.poetry]
|
|
2
2
|
name = "charli3_dendrite"
|
|
3
|
-
version = "1.4.
|
|
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.
|
|
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
|
-
"""
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
"""
|
|
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):
|
|
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]:
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
"""
|
|
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):
|
|
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]:
|
|
526
|
+
tuple[Assets, float]: The required input asset and the price-impact ratio.
|
|
432
527
|
|
|
433
528
|
Raises:
|
|
434
|
-
|
|
529
|
+
InvalidPoolError: If the desired output exceeds the band's available
|
|
530
|
+
reserve (it cannot be filled from this UTxO).
|
|
435
531
|
"""
|
|
436
|
-
|
|
437
|
-
|
|
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)
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/dano.py
RENAMED
|
@@ -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.
|
|
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(
|
|
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 (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/__init__.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/backend/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/cswap.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/minswap.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/muesli.py
RENAMED
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/splash.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/sundae.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/amm/vyfi.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/core/base.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/core/errors.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/__init__.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/axo.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/chadswap.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/djed.py
RENAMED
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.4.7.dev0 → charli3_dendrite-1.4.8.dev0}/src/charli3_dendrite/dexs/ob/ob_base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|