charli3_dendrite 1.5.8.dev0__tar.gz → 1.5.8.dev1__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.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/PKG-INFO +1 -1
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/pyproject.toml +2 -2
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/loader.py +48 -11
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/forward.py +0 -4
- charli3_dendrite-1.5.8.dev1/src/charli3_dendrite/lending/danogo/oracles/locator.py +300 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/state.py +1 -1
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/state.py +2 -2
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/models.py +49 -13
- charli3_dendrite-1.5.8.dev0/src/charli3_dendrite/lending/danogo/oracles/locator.py +0 -140
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/LICENSE +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/README.md +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/backend_base.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/blockfrost/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/blockfrost/models.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/dbsync/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/dbsync/models.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/ogmios_kupo/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/ogmios_kupo/models.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/utils.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dataclasses/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dataclasses/datums.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dataclasses/models.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/amm_base.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/amm_types.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/cswap.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/dano.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/minswap.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/muesli.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/spectrum.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/splash.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/sundae.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/vyfi.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/wingriders.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/core/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/core/base.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/core/errors.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/axo.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/cardanoswaps.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/chadswap.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/djed.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/geniusyield.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/ob_base.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/saturnswap.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/base.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/constants.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/datums.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/market.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/math.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/aggregator.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/aggregator_datums.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/deployments.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/leaves.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/lp_pricing.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/recipes.json +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/redeemer.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/reproduce.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/structured_datums.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/_common.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/build.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/builder.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/context.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/create_loan.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/datum_synth.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/increase_loan.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/loan_ids.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/modify_collateral.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/oracle_redeemer.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/oracle_synth.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/redeemers.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/repay.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/topup_withdraw.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/verify.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/constants.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/datums.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/loader.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/market.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/math.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/oracles/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/oracles/feeds.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/oracles/fluid_api.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/oracles/witness.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/_common.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/borrow.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/builder.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/change_collateral.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/context.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/datum_synth.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/lend.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/pool.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/recast.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/redeemers.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/repay.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/request.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/resolve.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/math.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/charli3.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/dex_pooled.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/fluid.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/orcfax.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/qtoken.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/registry.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/transactions/__init__.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/transactions/base.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/transactions/infra.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/transactions/snapshot.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/units.py +0 -0
- {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/utility.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "charli3_dendrite"
|
|
3
|
-
version = "1.5.8-
|
|
3
|
+
version = "1.5.8-dev1"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["Elder Millenial <eldermillenial@protonmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -50,7 +50,7 @@ module = ["pycardano", "pycardano.*", "dotenv", "nox", "uplc", "uplc.*"]
|
|
|
50
50
|
ignore_missing_imports = true
|
|
51
51
|
|
|
52
52
|
[tool.bumpversion]
|
|
53
|
-
current_version = "1.5.8-
|
|
53
|
+
current_version = "1.5.8-dev1"
|
|
54
54
|
parse = """(?x)
|
|
55
55
|
(?P<major>\\d+)\\.
|
|
56
56
|
(?P<minor>\\d+)\\.
|
|
@@ -28,7 +28,10 @@ from charli3_dendrite.lending.danogo.state import DanogoLoanState
|
|
|
28
28
|
from charli3_dendrite.lending.danogo.state import DanogoPoolState
|
|
29
29
|
|
|
30
30
|
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Collection
|
|
32
|
+
|
|
31
33
|
from charli3_dendrite.backend.backend_base import AbstractBackend
|
|
34
|
+
from charli3_dendrite.dataclasses.models import Assets
|
|
32
35
|
from charli3_dendrite.dataclasses.models import PoolStateInfo
|
|
33
36
|
from charli3_dendrite.lending.oracles.models import PriceMap
|
|
34
37
|
|
|
@@ -103,6 +106,29 @@ def _pool_nft_name(info: PoolStateInfo, policy: str) -> str | None:
|
|
|
103
106
|
return None
|
|
104
107
|
|
|
105
108
|
|
|
109
|
+
def _loan_market_name(
|
|
110
|
+
assets: Assets,
|
|
111
|
+
loan_policy: str,
|
|
112
|
+
valid_names: Collection[str],
|
|
113
|
+
) -> str | None:
|
|
114
|
+
"""Pool-NFT name (hex) of the market a loan belongs to, from its loan-identity NFT.
|
|
115
|
+
|
|
116
|
+
Each loan UTxO carries a loan token minted under the loan script hash whose asset
|
|
117
|
+
name IS the pool-NFT name of the loan's market (`create_loan` mints it that way).
|
|
118
|
+
Mirroring `_pool_nft_name`, this reads that name directly off the loan rather than
|
|
119
|
+
guessing the market from the borrowed supply token (several markets can share a
|
|
120
|
+
supply token) or the collateral (several markets can accept the same collateral).
|
|
121
|
+
Returns the name only when it is a known market/pool name (in `valid_names`), else
|
|
122
|
+
None (the loan is skipped, matching the existing skip-on-parse-failure degrade).
|
|
123
|
+
"""
|
|
124
|
+
for unit, qty in assets.root.items():
|
|
125
|
+
if unit != "lovelace" and unit.startswith(loan_policy) and qty == 1:
|
|
126
|
+
name = unit[len(loan_policy) :]
|
|
127
|
+
if name in valid_names:
|
|
128
|
+
return name
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
106
132
|
def fetch_markets(
|
|
107
133
|
backend: AbstractBackend,
|
|
108
134
|
addresses: DanogoScriptAddresses,
|
|
@@ -193,9 +219,22 @@ def snapshot(
|
|
|
193
219
|
if prices is None:
|
|
194
220
|
try:
|
|
195
221
|
from charli3_dendrite.lending.danogo.oracles import forward
|
|
222
|
+
from charli3_dendrite.lending.danogo.oracles.locator import (
|
|
223
|
+
augment_registry_with_bond_dtokens,
|
|
224
|
+
)
|
|
196
225
|
from charli3_dendrite.lending.danogo.oracles.locator import load_registry
|
|
197
226
|
|
|
198
227
|
registry = load_registry()
|
|
228
|
+
# Bond dTokens (a pool's own redeemable dToken) are accepted as collateral
|
|
229
|
+
# but are not mined for every (dToken, quote); synthesize their pricing
|
|
230
|
+
# recipes from the live pool/market state before resolving, so a dToken
|
|
231
|
+
# collateral prices instead of collapsing the loan's health factor.
|
|
232
|
+
augment_registry_with_bond_dtokens(
|
|
233
|
+
registry,
|
|
234
|
+
pool_address=addresses["pool"],
|
|
235
|
+
config_pool_cred=_payment_cred_hex(addresses["config_pool"]),
|
|
236
|
+
markets=markets,
|
|
237
|
+
)
|
|
199
238
|
pairs = {
|
|
200
239
|
(collateral, market.supply_token)
|
|
201
240
|
for market in markets.values()
|
|
@@ -205,28 +244,26 @@ def snapshot(
|
|
|
205
244
|
except Exception: # noqa: BLE001 - pricing outage must not crash the snapshot
|
|
206
245
|
prices = PriceMap()
|
|
207
246
|
|
|
208
|
-
# Pair pool <-> market (same pool-NFT name) and index
|
|
209
|
-
#
|
|
247
|
+
# Pair pool <-> market (same pool-NFT name) and index BY THAT NAME. Each loan is
|
|
248
|
+
# stitched to the market its own loan-identity NFT names -- not one guessed from its
|
|
249
|
+
# borrowed supply token or collateral, both of which several markets can share.
|
|
210
250
|
pools: list[DanogoPoolState] = []
|
|
211
|
-
|
|
251
|
+
by_name: dict[str, tuple[DanogoPoolState, DanogoMarket]] = {}
|
|
212
252
|
for name, pool in pools_by_name.items():
|
|
213
253
|
market = markets.get(name)
|
|
214
254
|
if market is None:
|
|
215
255
|
continue
|
|
216
256
|
pool.attach_market(market)
|
|
217
257
|
pools.append(pool)
|
|
218
|
-
|
|
258
|
+
by_name[name] = (pool, market)
|
|
219
259
|
|
|
260
|
+
loan_policy = _payment_cred_hex(addresses["loan"])
|
|
220
261
|
stitched: list[DanogoLoanState] = []
|
|
221
262
|
for loan in loans:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
except (ValueError, IndexError, TypeError, AttributeError):
|
|
225
|
-
continue
|
|
226
|
-
ctx = by_supply.get(supply)
|
|
227
|
-
if ctx is None:
|
|
263
|
+
loan_market = _loan_market_name(loan.assets, loan_policy, by_name)
|
|
264
|
+
if loan_market is None:
|
|
228
265
|
continue
|
|
229
|
-
pool, market =
|
|
266
|
+
pool, market = by_name[loan_market]
|
|
230
267
|
loan.attach_context(pool=pool, market=market, now_ms=now_ms)
|
|
231
268
|
stitched.append(loan)
|
|
232
269
|
|
|
@@ -296,10 +296,6 @@ def resolve_prices(
|
|
|
296
296
|
Emits ``OraclePrice(token=..., quote=quote, ...)``. A pair with no recipe, a
|
|
297
297
|
missing leaf, or a non-reproducing walk is simply omitted (collateral stays unpriced
|
|
298
298
|
-> not-liquidatable). Never raises into the caller.
|
|
299
|
-
|
|
300
|
-
NOTE: ``PriceMap`` keys by token only, so a collateral used across markets with
|
|
301
|
-
different supply tokens can hold only one quote. That's a pre-existing PriceMap
|
|
302
|
-
constraint, not handled here.
|
|
303
299
|
"""
|
|
304
300
|
pm = PriceMap()
|
|
305
301
|
for collateral, quote in pairs:
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Observation-based oracle recipes: re-price a collateral from live leaves.
|
|
2
|
+
|
|
3
|
+
Danogo stores no on-chain price; a collateral's price is reproduced from a fixed
|
|
4
|
+
set of source-leaf UTxOs (each contributing a forward rate, optionally inverted,
|
|
5
|
+
scaled by a power of ten). A `Recipe` is that frozen, ordered description for one
|
|
6
|
+
collateral, learned from a historical price-gen tx and replayed live (read each
|
|
7
|
+
handle's current UTxO, then `forward.replay_recipe`). `LeafHandle` says *where* to
|
|
8
|
+
re-read a leaf; `RecipeStep` pairs a handle with its orientation/scale; the registry
|
|
9
|
+
the loader consults is keyed `f"{collateral_unit}|{quote_unit}"` -> `Recipe`.
|
|
10
|
+
`load_registry` reads the packaged `recipes.json`; the db-sync miner that fills it
|
|
11
|
+
lives in `tests/lending/danogo/fixtures/_capture_recipes.py`.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from importlib.resources import files
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from pycardano import Address
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from charli3_dendrite.lending.danogo.market import DanogoMarket
|
|
26
|
+
|
|
27
|
+
# The Danogo pool redemption leaf kind (an ``OracleUtxoType`` member name). Named as a
|
|
28
|
+
# literal here to keep the recipe assembler free of the oracle-redeemer import; it is
|
|
29
|
+
# the same string the mined recipes and `forward.leaf_rate` dispatch on.
|
|
30
|
+
_BOND_DTOKEN_OTYPE = "TDANOGO_POOL"
|
|
31
|
+
# ADA is the universal intermediate quote for cross-quote composition (mirrors
|
|
32
|
+
# `forward.INTERMEDIATE_QUOTE`; redeclared to avoid importing the forward module, which
|
|
33
|
+
# imports this one).
|
|
34
|
+
_INTERMEDIATE_QUOTE = "lovelace"
|
|
35
|
+
# On-chain unit = 28-byte policy id (56 hex chars) followed by the asset name (hex).
|
|
36
|
+
_POLICY_ID_HEX_LEN = 56
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _payment_cred_hex(address: str) -> str:
|
|
40
|
+
"""Payment-credential (script hash) hex of an enterprise/base address."""
|
|
41
|
+
return Address.decode(address).payment_part.payload.hex()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class LeafHandle:
|
|
46
|
+
"""Where to re-read a source leaf live: parser type + address + identifying NFT.
|
|
47
|
+
|
|
48
|
+
`nft_policy`/`nft_name` (hex) identify the specific UTxO at `address` when the
|
|
49
|
+
address holds several; both empty means "the only relevant UTxO at the address".
|
|
50
|
+
`datum_key` is a further discriminator for kinds that share one marker token across
|
|
51
|
+
many feeds (e.g. Liqwid V2 oracles): the stable asset-identity parsed from the leaf
|
|
52
|
+
datum (see `forward.leaf_discriminator`). Empty when the NFT alone pins the UTxO.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
otype: str
|
|
56
|
+
address: str
|
|
57
|
+
nft_policy: str
|
|
58
|
+
nft_name: str
|
|
59
|
+
datum_key: str = ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class RecipeStep:
|
|
64
|
+
"""One leaf's contribution: its handle, whether to invert, and its power-of-ten."""
|
|
65
|
+
|
|
66
|
+
handle: LeafHandle
|
|
67
|
+
is_reverse: bool
|
|
68
|
+
scale_exp: int
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class Recipe:
|
|
73
|
+
"""Ordered steps whose composed product is one collateral's price."""
|
|
74
|
+
|
|
75
|
+
steps: tuple[RecipeStep, ...]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def recipe_from_observation(leaves: Sequence[Mapping]) -> Recipe:
|
|
79
|
+
"""Build a `Recipe` from a captured case's ordered `leaves`.
|
|
80
|
+
|
|
81
|
+
See ``forward_pricing.json``; each leaf mapping must carry `otype`, `address`,
|
|
82
|
+
`nft_policy`, `nft_name`, `is_reverse`, `scale_exp` (and optionally `datum_key`).
|
|
83
|
+
"""
|
|
84
|
+
steps = tuple(
|
|
85
|
+
RecipeStep(
|
|
86
|
+
handle=LeafHandle(
|
|
87
|
+
otype=lf["otype"],
|
|
88
|
+
address=lf["address"],
|
|
89
|
+
nft_policy=lf["nft_policy"],
|
|
90
|
+
nft_name=lf["nft_name"],
|
|
91
|
+
datum_key=lf.get("datum_key", ""),
|
|
92
|
+
),
|
|
93
|
+
is_reverse=bool(lf["is_reverse"]),
|
|
94
|
+
scale_exp=int(lf["scale_exp"]),
|
|
95
|
+
)
|
|
96
|
+
for lf in leaves
|
|
97
|
+
)
|
|
98
|
+
return Recipe(steps=steps)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def registry_to_dict(registry: Mapping[str, Recipe]) -> dict:
|
|
102
|
+
"""Serialize `{unit: Recipe}` to a JSON-safe dict."""
|
|
103
|
+
return {
|
|
104
|
+
unit: {
|
|
105
|
+
"steps": [
|
|
106
|
+
{
|
|
107
|
+
"otype": s.handle.otype,
|
|
108
|
+
"address": s.handle.address,
|
|
109
|
+
"nft_policy": s.handle.nft_policy,
|
|
110
|
+
"nft_name": s.handle.nft_name,
|
|
111
|
+
**({"datum_key": s.handle.datum_key} if s.handle.datum_key else {}),
|
|
112
|
+
"is_reverse": s.is_reverse,
|
|
113
|
+
"scale_exp": s.scale_exp,
|
|
114
|
+
}
|
|
115
|
+
for s in recipe.steps
|
|
116
|
+
],
|
|
117
|
+
}
|
|
118
|
+
for unit, recipe in registry.items()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_registry() -> dict[str, Recipe]:
|
|
123
|
+
"""Load the packaged ``{collateral|quote: Recipe}`` registry; ``{}`` if unavailable.
|
|
124
|
+
|
|
125
|
+
The registry is package data (``recipes.json``) mined offline from historical
|
|
126
|
+
price-gen transactions. Missing/unreadable data degrades to an empty registry so
|
|
127
|
+
the loader prices nothing rather than crashing.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
text = (
|
|
131
|
+
files("charli3_dendrite.lending.danogo.oracles")
|
|
132
|
+
.joinpath("recipes.json")
|
|
133
|
+
.read_text()
|
|
134
|
+
)
|
|
135
|
+
except (FileNotFoundError, ModuleNotFoundError, OSError):
|
|
136
|
+
return {}
|
|
137
|
+
try:
|
|
138
|
+
return registry_from_dict(json.loads(text))
|
|
139
|
+
except (ValueError, KeyError, TypeError):
|
|
140
|
+
return {}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def registry_from_dict(data: Mapping[str, Mapping]) -> dict[str, Recipe]:
|
|
144
|
+
"""Inverse of `registry_to_dict`."""
|
|
145
|
+
out: dict[str, Recipe] = {}
|
|
146
|
+
for unit, rec in data.items():
|
|
147
|
+
steps = tuple(
|
|
148
|
+
RecipeStep(
|
|
149
|
+
handle=LeafHandle(
|
|
150
|
+
otype=s["otype"],
|
|
151
|
+
address=s["address"],
|
|
152
|
+
nft_policy=s["nft_policy"],
|
|
153
|
+
nft_name=s["nft_name"],
|
|
154
|
+
datum_key=s.get("datum_key", ""),
|
|
155
|
+
),
|
|
156
|
+
is_reverse=bool(s["is_reverse"]),
|
|
157
|
+
scale_exp=int(s["scale_exp"]),
|
|
158
|
+
)
|
|
159
|
+
for s in rec["steps"]
|
|
160
|
+
)
|
|
161
|
+
out[unit] = Recipe(steps=steps)
|
|
162
|
+
return out
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _reverse_steps(steps: tuple[RecipeStep, ...]) -> tuple[RecipeStep, ...]:
|
|
166
|
+
"""Reverse a recipe's steps so the composed rate is inverted.
|
|
167
|
+
|
|
168
|
+
Each step contributes ``rate**(±1) * 10**scale_exp``; the reciprocal of the whole
|
|
169
|
+
product flips every step's orientation and negates its scale, in reverse order. The
|
|
170
|
+
leaf handles are unchanged, so the reversed steps resolve to the same live UTxOs.
|
|
171
|
+
"""
|
|
172
|
+
return tuple(
|
|
173
|
+
RecipeStep(
|
|
174
|
+
handle=s.handle,
|
|
175
|
+
is_reverse=not s.is_reverse,
|
|
176
|
+
scale_exp=-s.scale_exp,
|
|
177
|
+
)
|
|
178
|
+
for s in reversed(steps)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def synthesize_bond_dtoken_recipe(
|
|
183
|
+
collateral: str,
|
|
184
|
+
quote: str,
|
|
185
|
+
*,
|
|
186
|
+
pool_address: str,
|
|
187
|
+
config_pool_cred: str,
|
|
188
|
+
markets: Mapping[str, DanogoMarket],
|
|
189
|
+
registry: Mapping[str, Recipe],
|
|
190
|
+
) -> Recipe | None:
|
|
191
|
+
"""Assemble a live pricing `Recipe` for a Danogo bond-dToken collateral, or None.
|
|
192
|
+
|
|
193
|
+
A bond dToken's on-chain unit is ``<pool script hash> + N`` where ``N`` is a live
|
|
194
|
+
pool/market NFT name; the dToken's own pool redeems it into the market's supply
|
|
195
|
+
token at ``total_supply / circulating_dtoken`` (the ``TDANOGO_POOL`` leaf, priced
|
|
196
|
+
bit-exact by `reproduce`/`leaves`). This ASSEMBLES the recipe that reprices such a
|
|
197
|
+
dToken into `quote`; it computes no rate -- `forward.replay_recipe` /
|
|
198
|
+
`forward.compose_cross_quote` do, over the same leaf handles the mined recipes use.
|
|
199
|
+
|
|
200
|
+
`collateral` is recognised as a bond dToken only when its policy id is the
|
|
201
|
+
deployment's pool validator hash (derived from `pool_address`, resolved from
|
|
202
|
+
on-chain config -- never hardcoded) AND its asset name is a known pool/market name
|
|
203
|
+
(``markets`` keyed by ``N``). Requiring the policy, not the name alone, rejects an
|
|
204
|
+
unrelated token that happens to reuse a pool/market name as its asset name, while
|
|
205
|
+
staying deployment-agnostic (no hardcoded policy, no pool from another deployment).
|
|
206
|
+
Four cases:
|
|
207
|
+
|
|
208
|
+
* ``underlying == quote`` -- the single redemption step prices the dToken directly.
|
|
209
|
+
* a mined ``underlying|quote`` recipe exists -- append the redemption to it.
|
|
210
|
+
* ``quote == ADA`` (``underlying != ADA``) -- ``dToken -> underlying -> ADA``: the
|
|
211
|
+
redemption times the reversed ``lovelace|underlying`` recipe, priced in ADA.
|
|
212
|
+
* cross-quote (the registry has ``lovelace|underlying`` and ``lovelace|quote``) --
|
|
213
|
+
``dToken -> underlying -> ADA -> quote`` shaped for `compose_cross_quote`'s
|
|
214
|
+
pool-leading branch (the redemption leads, the reversed ``lovelace|underlying``
|
|
215
|
+
values it in ADA, and a trailing bridge step is dropped in favour of the resolved
|
|
216
|
+
``lovelace|quote`` intermediate).
|
|
217
|
+
* otherwise None -- no fabricated price; the collateral stays unpriced
|
|
218
|
+
(not-liquidatable), the existing safe degrade.
|
|
219
|
+
"""
|
|
220
|
+
# A bond dToken is minted under the Danogo pool validator hash: its unit is
|
|
221
|
+
# ``<pool script hash> + <pool/market name>``. Require BOTH the policy id (the pool
|
|
222
|
+
# validator hash, from `pool_address`) and a known pool/market name, so an unrelated
|
|
223
|
+
# token that merely reuses a pool/market name is not mistaken for a bond dToken.
|
|
224
|
+
name = collateral[_POLICY_ID_HEX_LEN:]
|
|
225
|
+
market = markets.get(name)
|
|
226
|
+
if market is None or collateral[:_POLICY_ID_HEX_LEN] != _payment_cred_hex(
|
|
227
|
+
pool_address,
|
|
228
|
+
):
|
|
229
|
+
return None
|
|
230
|
+
redemption_step = RecipeStep(
|
|
231
|
+
handle=LeafHandle(
|
|
232
|
+
otype=_BOND_DTOKEN_OTYPE,
|
|
233
|
+
address=pool_address,
|
|
234
|
+
nft_policy=config_pool_cred,
|
|
235
|
+
nft_name=name,
|
|
236
|
+
datum_key="",
|
|
237
|
+
),
|
|
238
|
+
is_reverse=False,
|
|
239
|
+
scale_exp=0,
|
|
240
|
+
)
|
|
241
|
+
underlying = market.supply_token
|
|
242
|
+
if underlying == quote:
|
|
243
|
+
return Recipe(steps=(redemption_step,))
|
|
244
|
+
conv = registry.get(f"{underlying}|{quote}")
|
|
245
|
+
if conv is not None:
|
|
246
|
+
return Recipe(steps=(*conv.steps, redemption_step))
|
|
247
|
+
|
|
248
|
+
# Compose through ADA: value the dToken in ADA via its redemption then the reversed
|
|
249
|
+
# ADA->underlying recipe (ADA is the universal intermediate). No bridge -> unpriced.
|
|
250
|
+
under_from_ada = registry.get(f"{_INTERMEDIATE_QUOTE}|{underlying}")
|
|
251
|
+
if under_from_ada is None:
|
|
252
|
+
return None
|
|
253
|
+
underlying_to_ada = _reverse_steps(under_from_ada.steps)
|
|
254
|
+
if quote == _INTERMEDIATE_QUOTE:
|
|
255
|
+
# Target IS ADA: dToken -> underlying -> ADA, priced single-hop in ADA.
|
|
256
|
+
steps = (redemption_step, *underlying_to_ada)
|
|
257
|
+
elif (ada_to_quote := registry.get(f"{_INTERMEDIATE_QUOTE}|{quote}")) is not None:
|
|
258
|
+
# Cross-quote pool-leading shape: (redemption, underlying->ADA, dropped bridge).
|
|
259
|
+
# compose_cross_quote values the collateral in ADA over all-but-the-last steps,
|
|
260
|
+
# and takes ADA->quote from the separately-resolved lovelace|quote intermediate,
|
|
261
|
+
# dropping the trailing step -- it only has to resolve live; its rate is unused.
|
|
262
|
+
steps = (redemption_step, *underlying_to_ada, ada_to_quote.steps[0])
|
|
263
|
+
else:
|
|
264
|
+
return None
|
|
265
|
+
return Recipe(steps=steps)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def augment_registry_with_bond_dtokens(
|
|
269
|
+
registry: dict[str, Recipe],
|
|
270
|
+
*,
|
|
271
|
+
pool_address: str,
|
|
272
|
+
config_pool_cred: str,
|
|
273
|
+
markets: Mapping[str, DanogoMarket],
|
|
274
|
+
) -> dict[str, Recipe]:
|
|
275
|
+
"""Add synthesized bond-dToken recipes to `registry` in place; return it.
|
|
276
|
+
|
|
277
|
+
For every market's accepted collateral that is a bond dToken with no mined recipe
|
|
278
|
+
for that market's quote, synthesize one (`synthesize_bond_dtoken_recipe`) and
|
|
279
|
+
register it under ``f"{collateral}|{quote}"``. Additive: mined recipes are never
|
|
280
|
+
overwritten, and a collateral with no composable path is left unpriced. Both live
|
|
281
|
+
seams that consult the registry -- `loader.snapshot` and the tx-builder leaf
|
|
282
|
+
resolution -- call this so they price bond dTokens identically.
|
|
283
|
+
"""
|
|
284
|
+
for market in markets.values():
|
|
285
|
+
quote = market.supply_token
|
|
286
|
+
for collateral in market.collaterals:
|
|
287
|
+
key = f"{collateral}|{quote}"
|
|
288
|
+
if key in registry:
|
|
289
|
+
continue
|
|
290
|
+
recipe = synthesize_bond_dtoken_recipe(
|
|
291
|
+
collateral,
|
|
292
|
+
quote,
|
|
293
|
+
pool_address=pool_address,
|
|
294
|
+
config_pool_cred=config_pool_cred,
|
|
295
|
+
markets=markets,
|
|
296
|
+
registry=registry,
|
|
297
|
+
)
|
|
298
|
+
if recipe is not None:
|
|
299
|
+
registry[key] = recipe
|
|
300
|
+
return registry
|
|
@@ -177,7 +177,7 @@ class DanogoLoanState(AbstractLoanState):
|
|
|
177
177
|
terms: list[tuple[int, int, int, int]] = []
|
|
178
178
|
has_missing = False
|
|
179
179
|
for unit, amount in self._collateral_units().items():
|
|
180
|
-
price = prices.get(unit)
|
|
180
|
+
price = prices.get(unit, quote=market.supply_token)
|
|
181
181
|
if price is None or price.num <= 0 or price.denom <= 0:
|
|
182
182
|
has_missing = True
|
|
183
183
|
continue
|
|
@@ -259,7 +259,7 @@ class FluidLoanState(AbstractLoanState):
|
|
|
259
259
|
"""
|
|
260
260
|
unit = self._collateral_unit()
|
|
261
261
|
amount = self.assets[unit]
|
|
262
|
-
price = prices.get(unit)
|
|
262
|
+
price = prices.get(unit, quote=self.borrowed_unit)
|
|
263
263
|
if price is None or price.num <= 0 or price.denom <= 0:
|
|
264
264
|
return 0
|
|
265
265
|
return amount * price.num // price.denom
|
|
@@ -299,7 +299,7 @@ class FluidLoanState(AbstractLoanState):
|
|
|
299
299
|
we do NOT flag it (avoids false positives during oracle outages).
|
|
300
300
|
"""
|
|
301
301
|
unit = self._collateral_unit()
|
|
302
|
-
price = prices.get(unit)
|
|
302
|
+
price = prices.get(unit, quote=self.borrowed_unit)
|
|
303
303
|
if price is None or price.num <= 0 or price.denom <= 0:
|
|
304
304
|
return False
|
|
305
305
|
return self.health_factor(prices) <= 1
|
|
@@ -13,6 +13,8 @@ from typing import Protocol
|
|
|
13
13
|
from typing import runtime_checkable
|
|
14
14
|
|
|
15
15
|
from pydantic import Field
|
|
16
|
+
from pydantic import field_serializer
|
|
17
|
+
from pydantic import field_validator
|
|
16
18
|
|
|
17
19
|
from charli3_dendrite.dataclasses.models import DendriteBaseModel
|
|
18
20
|
from charli3_dendrite.dataclasses.models import PoolSelector
|
|
@@ -104,34 +106,68 @@ class OracleRef(DendriteBaseModel):
|
|
|
104
106
|
|
|
105
107
|
|
|
106
108
|
class PriceMap(DendriteBaseModel):
|
|
107
|
-
"""token -> freshest OraclePrice.
|
|
109
|
+
"""(token, quote) -> freshest OraclePrice.
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
A collateral can be priced under more than one market quote (e.g. ADA priced
|
|
112
|
+
into several supply tokens), so the identity is the price's own ``(token,
|
|
113
|
+
quote)`` pair, not the token alone. Keying by token alone let same-token,
|
|
114
|
+
different-quote prices collide, making reads non-deterministic; keying by the
|
|
115
|
+
pair keeps each quote's price distinct.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
prices: dict[tuple[str, str], OraclePrice] = Field(default_factory=dict)
|
|
119
|
+
|
|
120
|
+
# The `(token, quote)` tuple keys have no symmetric JSON form: pydantic dumps a
|
|
121
|
+
# tuple key as ``"token,quote"`` but cannot parse it back. Encode each key as
|
|
122
|
+
# ``"token|quote"`` on dump and split it back on validate so both `model_dump`/
|
|
123
|
+
# `model_validate` and `model_dump_json`/`model_validate_json` round-trip. Units are
|
|
124
|
+
# hex or "lovelace" and never contain ``|``.
|
|
125
|
+
@field_serializer("prices")
|
|
126
|
+
def _serialize_prices(
|
|
127
|
+
self,
|
|
128
|
+
prices: dict[tuple[str, str], OraclePrice],
|
|
129
|
+
) -> dict[str, OraclePrice]:
|
|
130
|
+
return {f"{token}|{quote}": price for (token, quote), price in prices.items()}
|
|
131
|
+
|
|
132
|
+
@field_validator("prices", mode="before")
|
|
133
|
+
@classmethod
|
|
134
|
+
def _parse_price_keys(cls, value: object) -> object:
|
|
135
|
+
if not isinstance(value, dict):
|
|
136
|
+
return value
|
|
137
|
+
parsed: dict[object, object] = {}
|
|
138
|
+
for key, price in value.items():
|
|
139
|
+
if isinstance(key, str):
|
|
140
|
+
token, quote = key.split("|", 1)
|
|
141
|
+
parsed[(token, quote)] = price
|
|
142
|
+
else:
|
|
143
|
+
parsed[key] = price
|
|
144
|
+
return parsed
|
|
110
145
|
|
|
111
146
|
def add(self, price: OraclePrice) -> None:
|
|
112
|
-
"""Insert `price
|
|
147
|
+
"""Insert `price` under its ``(token, quote)``, keeping the latest `valid_to`.
|
|
113
148
|
|
|
114
149
|
A `valid_to` of `None` means "no expiry" and is treated as the freshest
|
|
115
150
|
possible value (`+inf`), so an open-ended price is never overwritten by
|
|
116
|
-
a dated one and always overwrites a dated one for the same token
|
|
151
|
+
a dated one and always overwrites a dated one for the same ``(token, quote)``.
|
|
117
152
|
"""
|
|
118
153
|
|
|
119
154
|
def _key(p: OraclePrice) -> float:
|
|
120
155
|
return float("inf") if p.valid_to is None else p.valid_to
|
|
121
156
|
|
|
122
|
-
|
|
157
|
+
pair = (price.token, price.quote)
|
|
158
|
+
existing = self.prices.get(pair)
|
|
123
159
|
if existing is None or _key(price) >= _key(existing):
|
|
124
|
-
self.prices[
|
|
160
|
+
self.prices[pair] = price
|
|
125
161
|
|
|
126
|
-
def get(self, token: str) -> OraclePrice | None:
|
|
127
|
-
"""Return the stored price for
|
|
128
|
-
return self.prices.get(token)
|
|
162
|
+
def get(self, token: str, quote: str = "lovelace") -> OraclePrice | None:
|
|
163
|
+
"""Return the stored price for ``(token, quote)``, or None if absent."""
|
|
164
|
+
return self.prices.get((token, quote))
|
|
129
165
|
|
|
130
|
-
def require(self, token: str) -> OraclePrice:
|
|
131
|
-
"""Return the stored price for
|
|
132
|
-
price = self.prices.get(token)
|
|
166
|
+
def require(self, token: str, quote: str = "lovelace") -> OraclePrice:
|
|
167
|
+
"""Return the stored price for ``(token, quote)``, or raise KeyError."""
|
|
168
|
+
price = self.prices.get((token, quote))
|
|
133
169
|
if price is None:
|
|
134
|
-
raise KeyError(f"no price for {token}")
|
|
170
|
+
raise KeyError(f"no price for {token} in {quote}")
|
|
135
171
|
return price
|
|
136
172
|
|
|
137
173
|
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
"""Observation-based oracle recipes: re-price a collateral from live leaves.
|
|
2
|
-
|
|
3
|
-
Danogo stores no on-chain price; a collateral's price is reproduced from a fixed
|
|
4
|
-
set of source-leaf UTxOs (each contributing a forward rate, optionally inverted,
|
|
5
|
-
scaled by a power of ten). A `Recipe` is that frozen, ordered description for one
|
|
6
|
-
collateral, learned from a historical price-gen tx and replayed live (read each
|
|
7
|
-
handle's current UTxO, then `forward.replay_recipe`). `LeafHandle` says *where* to
|
|
8
|
-
re-read a leaf; `RecipeStep` pairs a handle with its orientation/scale; the registry
|
|
9
|
-
the loader consults is keyed `f"{collateral_unit}|{quote_unit}"` -> `Recipe`.
|
|
10
|
-
`load_registry` reads the packaged `recipes.json`; the db-sync miner that fills it
|
|
11
|
-
lives in `tests/lending/danogo/fixtures/_capture_recipes.py`.
|
|
12
|
-
"""
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import json
|
|
16
|
-
from collections.abc import Mapping
|
|
17
|
-
from collections.abc import Sequence
|
|
18
|
-
from dataclasses import dataclass
|
|
19
|
-
from importlib.resources import files
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@dataclass(frozen=True)
|
|
23
|
-
class LeafHandle:
|
|
24
|
-
"""Where to re-read a source leaf live: parser type + address + identifying NFT.
|
|
25
|
-
|
|
26
|
-
`nft_policy`/`nft_name` (hex) identify the specific UTxO at `address` when the
|
|
27
|
-
address holds several; both empty means "the only relevant UTxO at the address".
|
|
28
|
-
`datum_key` is a further discriminator for kinds that share one marker token across
|
|
29
|
-
many feeds (e.g. Liqwid V2 oracles): the stable asset-identity parsed from the leaf
|
|
30
|
-
datum (see `forward.leaf_discriminator`). Empty when the NFT alone pins the UTxO.
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
otype: str
|
|
34
|
-
address: str
|
|
35
|
-
nft_policy: str
|
|
36
|
-
nft_name: str
|
|
37
|
-
datum_key: str = ""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@dataclass(frozen=True)
|
|
41
|
-
class RecipeStep:
|
|
42
|
-
"""One leaf's contribution: its handle, whether to invert, and its power-of-ten."""
|
|
43
|
-
|
|
44
|
-
handle: LeafHandle
|
|
45
|
-
is_reverse: bool
|
|
46
|
-
scale_exp: int
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@dataclass(frozen=True)
|
|
50
|
-
class Recipe:
|
|
51
|
-
"""Ordered steps whose composed product is one collateral's price."""
|
|
52
|
-
|
|
53
|
-
steps: tuple[RecipeStep, ...]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def recipe_from_observation(leaves: Sequence[Mapping]) -> Recipe:
|
|
57
|
-
"""Build a `Recipe` from a captured case's ordered `leaves`.
|
|
58
|
-
|
|
59
|
-
See ``forward_pricing.json``; each leaf mapping must carry `otype`, `address`,
|
|
60
|
-
`nft_policy`, `nft_name`, `is_reverse`, `scale_exp` (and optionally `datum_key`).
|
|
61
|
-
"""
|
|
62
|
-
steps = tuple(
|
|
63
|
-
RecipeStep(
|
|
64
|
-
handle=LeafHandle(
|
|
65
|
-
otype=lf["otype"],
|
|
66
|
-
address=lf["address"],
|
|
67
|
-
nft_policy=lf["nft_policy"],
|
|
68
|
-
nft_name=lf["nft_name"],
|
|
69
|
-
datum_key=lf.get("datum_key", ""),
|
|
70
|
-
),
|
|
71
|
-
is_reverse=bool(lf["is_reverse"]),
|
|
72
|
-
scale_exp=int(lf["scale_exp"]),
|
|
73
|
-
)
|
|
74
|
-
for lf in leaves
|
|
75
|
-
)
|
|
76
|
-
return Recipe(steps=steps)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def registry_to_dict(registry: Mapping[str, Recipe]) -> dict:
|
|
80
|
-
"""Serialize `{unit: Recipe}` to a JSON-safe dict."""
|
|
81
|
-
return {
|
|
82
|
-
unit: {
|
|
83
|
-
"steps": [
|
|
84
|
-
{
|
|
85
|
-
"otype": s.handle.otype,
|
|
86
|
-
"address": s.handle.address,
|
|
87
|
-
"nft_policy": s.handle.nft_policy,
|
|
88
|
-
"nft_name": s.handle.nft_name,
|
|
89
|
-
**({"datum_key": s.handle.datum_key} if s.handle.datum_key else {}),
|
|
90
|
-
"is_reverse": s.is_reverse,
|
|
91
|
-
"scale_exp": s.scale_exp,
|
|
92
|
-
}
|
|
93
|
-
for s in recipe.steps
|
|
94
|
-
],
|
|
95
|
-
}
|
|
96
|
-
for unit, recipe in registry.items()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def load_registry() -> dict[str, Recipe]:
|
|
101
|
-
"""Load the packaged ``{collateral|quote: Recipe}`` registry; ``{}`` if unavailable.
|
|
102
|
-
|
|
103
|
-
The registry is package data (``recipes.json``) mined offline from historical
|
|
104
|
-
price-gen transactions. Missing/unreadable data degrades to an empty registry so
|
|
105
|
-
the loader prices nothing rather than crashing.
|
|
106
|
-
"""
|
|
107
|
-
try:
|
|
108
|
-
text = (
|
|
109
|
-
files("charli3_dendrite.lending.danogo.oracles")
|
|
110
|
-
.joinpath("recipes.json")
|
|
111
|
-
.read_text()
|
|
112
|
-
)
|
|
113
|
-
except (FileNotFoundError, ModuleNotFoundError, OSError):
|
|
114
|
-
return {}
|
|
115
|
-
try:
|
|
116
|
-
return registry_from_dict(json.loads(text))
|
|
117
|
-
except (ValueError, KeyError, TypeError):
|
|
118
|
-
return {}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def registry_from_dict(data: Mapping[str, Mapping]) -> dict[str, Recipe]:
|
|
122
|
-
"""Inverse of `registry_to_dict`."""
|
|
123
|
-
out: dict[str, Recipe] = {}
|
|
124
|
-
for unit, rec in data.items():
|
|
125
|
-
steps = tuple(
|
|
126
|
-
RecipeStep(
|
|
127
|
-
handle=LeafHandle(
|
|
128
|
-
otype=s["otype"],
|
|
129
|
-
address=s["address"],
|
|
130
|
-
nft_policy=s["nft_policy"],
|
|
131
|
-
nft_name=s["nft_name"],
|
|
132
|
-
datum_key=s.get("datum_key", ""),
|
|
133
|
-
),
|
|
134
|
-
is_reverse=bool(s["is_reverse"]),
|
|
135
|
-
scale_exp=int(s["scale_exp"]),
|
|
136
|
-
)
|
|
137
|
-
for s in rec["steps"]
|
|
138
|
-
)
|
|
139
|
-
out[unit] = Recipe(steps=steps)
|
|
140
|
-
return out
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/__init__.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/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.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/cswap.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/dano.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/minswap.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/muesli.py
RENAMED
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/splash.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/sundae.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/vyfi.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/core/base.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/core/errors.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/__init__.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/axo.py
RENAMED
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/chadswap.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/djed.py
RENAMED
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/ob_base.py
RENAMED
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/__init__.py
RENAMED
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/base.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/math.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/units.py
RENAMED
|
File without changes
|
|
File without changes
|