charli3_dendrite 1.4.16.dev0__tar.gz → 1.4.17.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 (92) hide show
  1. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/PKG-INFO +1 -1
  2. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/pyproject.toml +2 -2
  3. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/__init__.py +40 -0
  4. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/base.py +369 -0
  5. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/__init__.py +21 -0
  6. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/constants.py +157 -0
  7. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/datums.py +135 -0
  8. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/loader.py +233 -0
  9. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/market.py +89 -0
  10. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/math.py +255 -0
  11. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/__init__.py +1 -0
  12. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/aggregator.py +181 -0
  13. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/aggregator_datums.py +241 -0
  14. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/deployments.py +104 -0
  15. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/forward.py +357 -0
  16. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/leaves.py +356 -0
  17. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/locator.py +140 -0
  18. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/lp_pricing.py +102 -0
  19. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/recipes.json +837 -0
  20. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/redeemer.py +298 -0
  21. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/reproduce.py +213 -0
  22. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/oracles/structured_datums.py +303 -0
  23. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/state.py +253 -0
  24. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/__init__.py +49 -0
  25. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/_common.py +1462 -0
  26. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/build.py +562 -0
  27. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/builder.py +327 -0
  28. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/context.py +1146 -0
  29. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/create_loan.py +77 -0
  30. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/datum_synth.py +508 -0
  31. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/increase_loan.py +494 -0
  32. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/loan_ids.py +37 -0
  33. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/modify_collateral.py +371 -0
  34. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/oracle_redeemer.py +53 -0
  35. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/oracle_synth.py +45 -0
  36. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/redeemers.py +190 -0
  37. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/repay.py +665 -0
  38. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/topup_withdraw.py +369 -0
  39. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/danogo/transactions/verify.py +11 -0
  40. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/math.py +34 -0
  41. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/oracles/__init__.py +1 -0
  42. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/oracles/charli3.py +83 -0
  43. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/oracles/dex_pooled.py +51 -0
  44. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/oracles/fluid.py +136 -0
  45. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/oracles/models.py +168 -0
  46. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/oracles/orcfax.py +153 -0
  47. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/oracles/qtoken.py +50 -0
  48. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/registry.py +68 -0
  49. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/transactions/__init__.py +25 -0
  50. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/transactions/base.py +141 -0
  51. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/transactions/infra.py +241 -0
  52. charli3_dendrite-1.4.17.dev0/src/charli3_dendrite/lending/transactions/snapshot.py +47 -0
  53. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/utility.py +12 -0
  54. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/LICENSE +0 -0
  55. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/README.md +0 -0
  56. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/__init__.py +0 -0
  57. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/backend/__init__.py +0 -0
  58. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/backend/backend_base.py +0 -0
  59. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/backend/blockfrost/__init__.py +0 -0
  60. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/backend/blockfrost/models.py +0 -0
  61. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/backend/dbsync/__init__.py +0 -0
  62. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/backend/dbsync/models.py +0 -0
  63. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/backend/ogmios_kupo/__init__.py +0 -0
  64. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/backend/ogmios_kupo/models.py +0 -0
  65. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/backend/utils.py +0 -0
  66. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dataclasses/__init__.py +0 -0
  67. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dataclasses/datums.py +0 -0
  68. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dataclasses/models.py +0 -0
  69. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/__init__.py +0 -0
  70. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/__init__.py +0 -0
  71. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/amm_base.py +0 -0
  72. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/amm_types.py +0 -0
  73. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/cswap.py +0 -0
  74. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/dano.py +0 -0
  75. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/minswap.py +0 -0
  76. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/muesli.py +0 -0
  77. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/spectrum.py +0 -0
  78. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/splash.py +0 -0
  79. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/sundae.py +0 -0
  80. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/vyfi.py +0 -0
  81. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/amm/wingriders.py +0 -0
  82. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/core/__init__.py +0 -0
  83. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/core/base.py +0 -0
  84. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/core/errors.py +0 -0
  85. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/ob/__init__.py +0 -0
  86. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/ob/axo.py +0 -0
  87. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/ob/cardanoswaps.py +0 -0
  88. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/ob/chadswap.py +0 -0
  89. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/ob/djed.py +0 -0
  90. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/ob/geniusyield.py +0 -0
  91. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/ob/ob_base.py +0 -0
  92. {charli3_dendrite-1.4.16.dev0 → charli3_dendrite-1.4.17.dev0}/src/charli3_dendrite/dexs/ob/saturnswap.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: charli3_dendrite
3
- Version: 1.4.16.dev0
3
+ Version: 1.4.17.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.16-dev0"
3
+ version = "1.4.17-dev0"
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.4.16-dev0"
53
+ current_version = "1.4.17-dev0"
54
54
  parse = """(?x)
55
55
  (?P<major>\\d+)\\.
56
56
  (?P<minor>\\d+)\\.
@@ -0,0 +1,40 @@
1
+ """Read-only lending-protocol analytics (pools, loans, oracles).
2
+
3
+ Importing this package registers all oracle resolvers. Lending classes are
4
+ intentionally NOT exported from `charli3_dendrite/__init__.py` and do not
5
+ subclass DEX bases, so they never enter DEX discovery.
6
+ """
7
+
8
+ from charli3_dendrite.lending.base import AbstractLendingPoolState
9
+ from charli3_dendrite.lending.base import AbstractLoanState
10
+ from charli3_dendrite.lending.base import LendingBook
11
+ from charli3_dendrite.lending.base import LendingPriceBook
12
+ from charli3_dendrite.lending.oracles.charli3 import Charli3Resolver
13
+ from charli3_dendrite.lending.oracles.dex_pooled import DexPooledResolver
14
+ from charli3_dendrite.lending.oracles.fluid import FluidResolver
15
+ from charli3_dendrite.lending.oracles.models import OraclePrice
16
+ from charli3_dendrite.lending.oracles.models import OracleRef
17
+ from charli3_dendrite.lending.oracles.models import OracleSource
18
+ from charli3_dendrite.lending.oracles.models import PriceMap
19
+ from charli3_dendrite.lending.oracles.models import register_resolver
20
+ from charli3_dendrite.lending.oracles.orcfax import OrcfaxResolver
21
+ from charli3_dendrite.lending.oracles.qtoken import QTokenResolver
22
+
23
+ register_resolver(Charli3Resolver())
24
+ register_resolver(OrcfaxResolver())
25
+ register_resolver(QTokenResolver())
26
+ register_resolver(DexPooledResolver())
27
+ register_resolver(FluidResolver(OracleSource.FLUID_AGGREGATED))
28
+ register_resolver(FluidResolver(OracleSource.FLUID_POOLED))
29
+ register_resolver(FluidResolver(OracleSource.FLUID_DEDICATED))
30
+
31
+ __all__ = [
32
+ "AbstractLendingPoolState",
33
+ "AbstractLoanState",
34
+ "LendingBook",
35
+ "LendingPriceBook",
36
+ "OraclePrice",
37
+ "OracleRef",
38
+ "OracleSource",
39
+ "PriceMap",
40
+ ]
@@ -0,0 +1,369 @@
1
+ """Read-only lending pool/loan state bases + aggregation views."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC
6
+ from abc import abstractmethod
7
+ from collections.abc import Sequence
8
+ from decimal import Decimal
9
+ from typing import TYPE_CHECKING
10
+
11
+ from pycardano import Address
12
+ from pycardano import PlutusData
13
+ from pycardano import PlutusV1Script
14
+ from pycardano import PlutusV2Script
15
+ from pycardano import UTxO
16
+
17
+ from charli3_dendrite.dataclasses.models import Assets
18
+ from charli3_dendrite.dataclasses.models import DendriteBaseModel
19
+ from charli3_dendrite.dataclasses.models import PoolSelector
20
+ from charli3_dendrite.dataclasses.models import PoolStateInfo
21
+ from charli3_dendrite.dataclasses.models import PoolStateList
22
+ from charli3_dendrite.lending.oracles.models import OracleRef
23
+ from charli3_dendrite.lending.oracles.models import OracleSource
24
+ from charli3_dendrite.lending.oracles.models import PriceMap
25
+ from charli3_dendrite.lending.oracles.models import get_resolver
26
+
27
+ if TYPE_CHECKING:
28
+ from charli3_dendrite.backend.backend_base import AbstractBackend
29
+
30
+
31
+ class AbstractLendingPoolState(DendriteBaseModel, ABC):
32
+ """Snapshot of a lending pool/market (read-only)."""
33
+
34
+ assets: Assets
35
+ block_time: int
36
+ block_index: int
37
+ plutus_v2: bool
38
+ datum_cbor: str
39
+ datum_hash: str
40
+ tx_index: int
41
+ tx_hash: str
42
+ inactive: bool = False
43
+
44
+ _pool_datum_parsed: PlutusData | None = None
45
+
46
+ @classmethod
47
+ @abstractmethod
48
+ def protocol(cls) -> str:
49
+ """Official protocol name (e.g. 'Danogo', 'FluidTokens')."""
50
+ raise NotImplementedError
51
+
52
+ @classmethod
53
+ def pool_policy(cls) -> list[str] | None:
54
+ """Pool-NFT policy (or policy+name) used to recognize the pool."""
55
+ return None
56
+
57
+ @classmethod
58
+ @abstractmethod
59
+ def pool_datum_class(cls) -> type[PlutusData]:
60
+ """Plutus type for the pool datum."""
61
+ raise NotImplementedError
62
+
63
+ @classmethod
64
+ def oracle_datum_class(cls) -> type[PlutusData] | None:
65
+ """Optional protocol-owned oracle datum type."""
66
+ return None
67
+
68
+ @classmethod
69
+ @abstractmethod
70
+ def pool_selector(cls) -> PoolSelector:
71
+ """UTxO selector for this pool/market."""
72
+ raise NotImplementedError
73
+
74
+ @classmethod
75
+ def reference_utxo(cls) -> UTxO | None:
76
+ """Optional protocol reference-script UTxO."""
77
+ return None
78
+
79
+ @classmethod
80
+ def default_script_class(cls) -> type[PlutusV1Script | PlutusV2Script]:
81
+ """Default Plutus script version for this protocol."""
82
+ return PlutusV1Script
83
+
84
+ def script_class(self) -> type[PlutusV1Script | PlutusV2Script]:
85
+ """Plutus script version implied by this UTxO."""
86
+ return PlutusV2Script if self.plutus_v2 else PlutusV1Script
87
+
88
+ @property
89
+ @abstractmethod
90
+ def stake_address(self) -> Address:
91
+ """Stake address controlling this pool."""
92
+ raise NotImplementedError
93
+
94
+ @property
95
+ @abstractmethod
96
+ def pool_id(self) -> str:
97
+ """Stable identifier for this pool/market."""
98
+ raise NotImplementedError
99
+
100
+ @property
101
+ def pool_datum(self) -> PlutusData:
102
+ """Parsed pool datum (lazily decoded from CBOR)."""
103
+ if self._pool_datum_parsed is None:
104
+ self._pool_datum_parsed = self.pool_datum_class().from_cbor(self.datum_cbor)
105
+ return self._pool_datum_parsed
106
+
107
+ @property
108
+ @abstractmethod
109
+ def borrowable_unit(self) -> str:
110
+ """Unit that can be borrowed from this pool."""
111
+ raise NotImplementedError
112
+
113
+ @property
114
+ @abstractmethod
115
+ def utilization_ratio(self) -> Decimal:
116
+ """Fraction of supplied liquidity currently borrowed."""
117
+ raise NotImplementedError
118
+
119
+ def oracle_refs(self) -> list[OracleRef]:
120
+ """Oracle feeds this pool needs (e.g. alt-token rates). Default none."""
121
+ return []
122
+
123
+ @abstractmethod
124
+ def max_borrow_for_collateral_value(self, collateral_value_lovelace: int) -> int:
125
+ """Best-effort quote: collateral value -> borrowable size."""
126
+ raise NotImplementedError
127
+
128
+
129
+ class AbstractLoanState(DendriteBaseModel, ABC):
130
+ """Snapshot of a single borrower position (read-only)."""
131
+
132
+ assets: Assets
133
+ block_time: int
134
+ block_index: int
135
+ plutus_v2: bool
136
+ datum_cbor: str
137
+ datum_hash: str
138
+ tx_index: int
139
+ tx_hash: str
140
+ inactive: bool = False
141
+
142
+ _loan_datum_parsed: PlutusData | None = None
143
+
144
+ @classmethod
145
+ @abstractmethod
146
+ def protocol(cls) -> str:
147
+ """Official protocol name (e.g. 'Danogo', 'FluidTokens')."""
148
+ raise NotImplementedError
149
+
150
+ @classmethod
151
+ def protocol_nft_policy(cls) -> list[str] | None:
152
+ """Policy / policy+name prefixes for the loan-identity NFT."""
153
+ return None
154
+
155
+ @classmethod
156
+ @abstractmethod
157
+ def loan_datum_class(cls) -> type[PlutusData]:
158
+ """Plutus type for the loan datum."""
159
+ raise NotImplementedError
160
+
161
+ @classmethod
162
+ def oracle_datum_class(cls) -> type[PlutusData] | None:
163
+ """Optional protocol-owned oracle datum type."""
164
+ return None
165
+
166
+ @classmethod
167
+ @abstractmethod
168
+ def loan_selector(cls) -> PoolSelector:
169
+ """UTxO selector for loans (same shape as pool_selector)."""
170
+ raise NotImplementedError
171
+
172
+ @classmethod
173
+ def reference_utxo(cls) -> UTxO | None:
174
+ """Optional protocol reference-script UTxO."""
175
+ return None
176
+
177
+ @classmethod
178
+ def default_script_class(cls) -> type[PlutusV1Script | PlutusV2Script]:
179
+ """Default Plutus script version for this protocol."""
180
+ return PlutusV1Script
181
+
182
+ def script_class(self) -> type[PlutusV1Script | PlutusV2Script]:
183
+ """Plutus script version implied by this UTxO."""
184
+ return PlutusV2Script if self.plutus_v2 else PlutusV1Script
185
+
186
+ @property
187
+ @abstractmethod
188
+ def stake_address(self) -> Address:
189
+ """Stake address controlling this loan."""
190
+ raise NotImplementedError
191
+
192
+ @property
193
+ @abstractmethod
194
+ def loan_id(self) -> str:
195
+ """Stable identifier for this loan."""
196
+ raise NotImplementedError
197
+
198
+ @property
199
+ @abstractmethod
200
+ def pool_id(self) -> str:
201
+ """Identifier of the pool/market this loan belongs to."""
202
+ raise NotImplementedError
203
+
204
+ @property
205
+ def loan_datum(self) -> PlutusData:
206
+ """Parsed loan datum (lazily decoded from CBOR)."""
207
+ if self._loan_datum_parsed is None:
208
+ self._loan_datum_parsed = self.loan_datum_class().from_cbor(self.datum_cbor)
209
+ return self._loan_datum_parsed
210
+
211
+ @property
212
+ @abstractmethod
213
+ def borrowed_unit(self) -> str:
214
+ """Unit borrowed by this loan."""
215
+ raise NotImplementedError
216
+
217
+ def oracle_refs(self) -> list[OracleRef]:
218
+ """Oracle feeds needed to value this loan. Default none (fixed-ratio)."""
219
+ return []
220
+
221
+ @abstractmethod
222
+ def current_debt(self) -> int:
223
+ """Principal + accrued interest, in smallest units of borrowed asset."""
224
+ raise NotImplementedError
225
+
226
+ @abstractmethod
227
+ def collateral_value_lovelace(self, prices: PriceMap) -> int:
228
+ """Collateral value in lovelace at the given prices."""
229
+ raise NotImplementedError
230
+
231
+ @abstractmethod
232
+ def health_factor(self, prices: PriceMap) -> Decimal:
233
+ """Liquidation ratio; default rule liquidatable when <= 1."""
234
+ raise NotImplementedError
235
+
236
+ def is_liquidatable(self, prices: PriceMap) -> bool:
237
+ """Default: health_factor <= 1. Override if on-chain rule differs."""
238
+ return self.health_factor(prices) <= 1
239
+
240
+ def repay_amount_for_target_hf(self, target_hf: Decimal, prices: PriceMap) -> int:
241
+ """Repayment to reach `target_hf`.
242
+
243
+ Generic helper assuming the borrowed asset is denominated in lovelace
244
+ (i.e. collateral_value_lovelace and debt share units). Protocols whose
245
+ borrowed asset is not ADA should override.
246
+ """
247
+ if target_hf <= 0:
248
+ return self.current_debt()
249
+ debt = Decimal(self.current_debt())
250
+ max_safe_debt = Decimal(self.collateral_value_lovelace(prices)) / target_hf
251
+ if debt <= max_safe_debt:
252
+ return 0
253
+ return int((debt - max_safe_debt).to_integral_value(rounding="ROUND_CEILING"))
254
+
255
+
256
+ class LendingPriceBook:
257
+ """Batch oracle resolution for a set of loans/pools.
258
+
259
+ Selectors are de-duplicated within each source group, so each unique feed
260
+ is fetched once (one fetch per unique selector per source) no matter how
261
+ many loans reference it.
262
+ """
263
+
264
+ @staticmethod
265
+ def build(
266
+ items: Sequence[AbstractLoanState | AbstractLendingPoolState],
267
+ *,
268
+ backend: AbstractBackend,
269
+ at_ms: int | None = None, # noqa: ARG004
270
+ ) -> PriceMap:
271
+ """Resolve every source referenced by `items` into one `PriceMap`."""
272
+ refs: list[OracleRef] = []
273
+ for item in items:
274
+ refs.extend(item.oracle_refs())
275
+
276
+ by_source: dict[OracleSource, list[OracleRef]] = {}
277
+ for ref in refs:
278
+ by_source.setdefault(ref.source, []).append(ref)
279
+
280
+ price_map = PriceMap()
281
+ for source, group in by_source.items():
282
+ resolver = get_resolver(source)
283
+ seen: set[tuple] = set()
284
+ infos: list[PoolStateInfo] = []
285
+ for selector in resolver.selectors(group):
286
+ key = (
287
+ tuple(sorted(selector.addresses)),
288
+ tuple(sorted(selector.assets or ())),
289
+ )
290
+ if key in seen:
291
+ continue
292
+ seen.add(key)
293
+ infos.extend(list(backend.get_pool_utxos(**selector.model_dump())))
294
+ utxos = PoolStateList(root=infos)
295
+ for ref in group:
296
+ price = resolver.resolve(ref, utxos)
297
+ if price is not None:
298
+ price_map.add(price)
299
+ return price_map
300
+
301
+
302
+ class LendingBook:
303
+ """Aggregated, price-aware view of loans for bots and risk analytics."""
304
+
305
+ def __init__(
306
+ self,
307
+ loans_full: list[AbstractLoanState],
308
+ *,
309
+ block_time: int,
310
+ prices: PriceMap | None = None,
311
+ pool: AbstractLendingPoolState | None = None,
312
+ ) -> None:
313
+ """Store loans plus optional prices/pool context for aggregation."""
314
+ self.loans_full = list(loans_full)
315
+ self.block_time = block_time
316
+ self.prices = prices or PriceMap()
317
+ self.pool = pool
318
+
319
+ @classmethod
320
+ def from_loans(
321
+ cls,
322
+ loans: Sequence[AbstractLoanState],
323
+ *,
324
+ prices: PriceMap | None = None,
325
+ pool: AbstractLendingPoolState | None = None,
326
+ block_time: int | None = None,
327
+ ) -> LendingBook:
328
+ """Construct a book from `loans`, defaulting `block_time` to the latest."""
329
+ bt = (
330
+ block_time
331
+ if block_time is not None
332
+ else max((ln.block_time for ln in loans), default=0)
333
+ )
334
+ return cls(list(loans), block_time=bt, prices=prices, pool=pool)
335
+
336
+ def active_loans(self) -> list[AbstractLoanState]:
337
+ """Loans that are not marked inactive."""
338
+ return [ln for ln in self.loans_full if not ln.inactive]
339
+
340
+ def liquidatable_loans(self) -> list[AbstractLoanState]:
341
+ """Active loans whose health factor makes them liquidatable."""
342
+ return [ln for ln in self.active_loans() if ln.is_liquidatable(self.prices)]
343
+
344
+ def liquidation_candidates_by_urgency(self) -> list[AbstractLoanState]:
345
+ """Liquidatable loans sorted by ascending health factor."""
346
+ scored = [
347
+ (ln, ln.health_factor(self.prices)) for ln in self.liquidatable_loans()
348
+ ]
349
+ scored.sort(key=lambda item: (item[1], item[0].loan_id))
350
+ return [ln for ln, _ in scored]
351
+
352
+ def near_liquidation(
353
+ self,
354
+ buffer: Decimal = Decimal("0.05"),
355
+ ) -> list[AbstractLoanState]:
356
+ """Healthy active loans within `buffer` above the liquidation threshold."""
357
+ hi = Decimal(1) + buffer
358
+ scored = [
359
+ (ln, ln.health_factor(self.prices))
360
+ for ln in self.active_loans()
361
+ if not ln.is_liquidatable(self.prices)
362
+ ]
363
+ scored = [(ln, hf) for ln, hf in scored if hf <= hi]
364
+ scored.sort(key=lambda item: (item[1], item[0].loan_id))
365
+ return [ln for ln, _ in scored]
366
+
367
+ def by_pool(self, pool_id: str) -> list[AbstractLoanState]:
368
+ """Active loans belonging to `pool_id`."""
369
+ return [ln for ln in self.active_loans() if ln.pool_id == pool_id]
@@ -0,0 +1,21 @@
1
+ """Read-only Danogo Float-Rate Lending analytics.
2
+
3
+ Importing this module registers the Danogo aggregator resolver. Danogo classes do not
4
+ subclass DEX bases and are not exported from `charli3_dendrite/__init__.py`.
5
+ """
6
+
7
+ from charli3_dendrite.lending.danogo.loader import build_book
8
+ from charli3_dendrite.lending.danogo.market import DanogoMarket
9
+ from charli3_dendrite.lending.danogo.oracles.aggregator import DanogoAggregatorResolver
10
+ from charli3_dendrite.lending.danogo.state import DanogoLoanState
11
+ from charli3_dendrite.lending.danogo.state import DanogoPoolState
12
+ from charli3_dendrite.lending.oracles.models import register_resolver
13
+
14
+ register_resolver(DanogoAggregatorResolver())
15
+
16
+ __all__ = [
17
+ "DanogoLoanState",
18
+ "DanogoMarket",
19
+ "DanogoPoolState",
20
+ "build_book",
21
+ ]
@@ -0,0 +1,157 @@
1
+ """Danogo deployment anchors + on-chain address resolution.
2
+
3
+ Anchors are None until supplied. With an anchor, `resolve_addresses` reads the Protocol
4
+ Config UTxO's ProtocolDatum and derives the four script addresses.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+ from typing import TypedDict
11
+
12
+ from pycardano import Address
13
+ from pycardano import Network
14
+ from pycardano import ScriptHash
15
+
16
+ from charli3_dendrite.lending.danogo.datums import ProtocolDatum
17
+ from charli3_dendrite.lending.danogo.oracles.locator import LeafHandle
18
+
19
+ if TYPE_CHECKING:
20
+ from charli3_dendrite.backend.backend_base import AbstractBackend
21
+
22
+ # Deployment anchors (Cardano mainnet). NFT anchors are policy(56 hex)+asset_name(hex);
23
+ # the path anchor is policy-only. The Oracle Global Config NFT and the Oracle Path FTs
24
+ # share a single minting policy.
25
+ PROTOCOL_CONFIG_NFT: str | None = (
26
+ "bc2d3b7cf1009c788b1daf2208a38026ab20d7f8209d896ad1a09c14"
27
+ "e567b64450c406a17f869eb4e430f2e2a6c74d103edb988cb1bc1347de285c69"
28
+ )
29
+ ORACLE_GLOBAL_CONFIG_NFT: str | None = (
30
+ "bd23e2977937bb6054f625e5b566e28e294dcd8f7e8d0d33979ed90c"
31
+ "1e5e4d7ac0ba99f5f4b3d3c7764054ccce87ef497407785a4ee04d2beadb0b78"
32
+ )
33
+ ORACLE_PATH_FT_POLICY: str | None = (
34
+ "bd23e2977937bb6054f625e5b566e28e294dcd8f7e8d0d33979ed90c"
35
+ )
36
+
37
+ # Per-supply-token oracle routing anchor. Each supply token's price config is anchored
38
+ # to one Danogo reference pool: the `OraclePathDatum.anchor` (28 bytes) is the terminal
39
+ # bridge that converts every collateral price into the supply token. Many path-config
40
+ # UTxOs (across older config versions and other supply tokens) share the path FT policy;
41
+ # the create-loan oracle Withdraw validator only accepts the path datum anchored to the
42
+ # borrowed market's supply token. Keyed by supply-token unit ("lovelace" for ADA).
43
+ ORACLE_QUOTE_ANCHOR: dict[str, str] = {
44
+ "lovelace": "836cc68931c2e4e3e838602eca1902591d216837bafddfe6f0c8cb07",
45
+ # USDM-supply market routing anchor.
46
+ "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad0014df105553444d": (
47
+ "64e648e02d76567652d55e33106123f72ceac04bf3dcaf4f70737d58"
48
+ ),
49
+ # DjedMicroUSD-supply market routing anchor.
50
+ (
51
+ "8db269c3ec630e06ae29f74bc39edd1f87c819f1056206e879a1cd61"
52
+ "446a65644d6963726f555344"
53
+ ): "504791c3a0348e8ebadac99d5df8753b1b12e3849b8bd69653e5db92",
54
+ # USDC-supply market routing anchor.
55
+ (
56
+ "1f3aec8bfe7ea4fe14c5f121e2a92e301afe414147860d557cac7e34" "5553444378"
57
+ ): "853fca625371e99f0e0b94663f3513dd99ec141bac5dad8128b41db6",
58
+ # USDA-supply market routing anchor.
59
+ (
60
+ "fe7c786ab321f41c654ef6c1af7b3250a613c24e4213e0425a7ae456" "55534441"
61
+ ): "a8fe3b72f0195c5af7a9df18402ba5ec77938f5795da7bb738653c9d",
62
+ }
63
+
64
+ # Minswap V2 LP minting policy. The oracle path config encodes a Minswap LP source as
65
+ # the LP token *name* (32 bytes) under this policy; matching a borrowed collateral's
66
+ # resolved Minswap LP leaf to a path config's sources confirms the config prices it.
67
+ MINSWAP_LP_POLICY: str = "f5808c2c990d86da54bfc97d89cee6efa20cd8461616359478d96b4c"
68
+
69
+ # Curated cross-check oracle feeds per cross-quote supply token (keyed by supply-token
70
+ # unit). A cross-quote market's ``ada -> quote`` conversion is deviation-checked by the
71
+ # routing path config against independent external price feeds that the mined pricing
72
+ # recipes never reference (no recipe step names them), so the recipe-driven leaf
73
+ # resolution does not fetch them. The oracle ``Withdraw`` still walks them, so a
74
+ # fully-live cross-quote build must additionally fetch these feed UTxOs; the compose
75
+ # filter then references only the candidate whose rate is within the path config's
76
+ # deviation tolerance. Each handle is resolved through the same single-UTxO
77
+ # discriminator the recipe leaves use. Only cross-quote supply tokens (those with a
78
+ # canonical ``ada -> quote`` recipe) appear here; ADA-quote / single-hop markets need
79
+ # none.
80
+ CROSS_CHECK_SUPPLEMENTAL_HANDLES: dict[str, tuple[LeafHandle, ...]] = {
81
+ # USDCx-supply market: Indigo iUSD ``ada -> quote`` feed (a singleton UTxO pinned
82
+ # by its feed NFT at a stable script address), parsed as ``OracleUtxoType.TINDIGO``.
83
+ "1f3aec8bfe7ea4fe14c5f121e2a92e301afe414147860d557cac7e345553444378": (
84
+ LeafHandle(
85
+ otype="TINDIGO",
86
+ address="addr1wygyy4mdrh5kxsmm6ja4phxez3vh2mngz2muqmw4gw3n9jqdu67a0",
87
+ nft_policy="e3455f2715338b454fb853442f72dc03b98396854f97510027fe22ff",
88
+ nft_name="695553443230323231313138313935393036",
89
+ ),
90
+ ),
91
+ }
92
+
93
+ # Script credentials that hold the config/oracle reference UTxOs. Backends match
94
+ # UTxOs by payment credential and don't support NFT-only lookups, so resolution
95
+ # starts from these addresses: PROTOCOL_CONFIG_SKH holds the Protocol Config UTxO,
96
+ # and ORACLE_DATA_SKH holds the Oracle Global Config UTxO and the Oracle Path FTs.
97
+ PROTOCOL_CONFIG_SKH: str | None = (
98
+ "9a0054cf2ab678edcd7528d16a388e5b1cefa3af40d5b9634ab96de6"
99
+ )
100
+ ORACLE_DATA_SKH: str | None = "79a5116ee2ff13ffd53b2615568574fc804740a07bc2ff8930d2d6fb"
101
+
102
+
103
+ class DanogoScriptAddresses(TypedDict):
104
+ """The four Danogo script addresses derived from the ProtocolDatum."""
105
+
106
+ pool: str
107
+ loan: str
108
+ config_pool: str
109
+ oracle: str
110
+
111
+
112
+ def _addr(skh_hex: str) -> str:
113
+ # Returns a payment-credential (enterprise, no-stake) script *identity*. On-chain
114
+ # pool/loan UTxOs may sit at base addresses with a stake part, so callers must match
115
+ # by payment credential, not raw address-string equality (some backends match raw
116
+ # address strings).
117
+ return Address(
118
+ payment_part=ScriptHash(bytes.fromhex(skh_hex)),
119
+ network=Network.MAINNET,
120
+ ).encode()
121
+
122
+
123
+ def resolve_addresses(
124
+ backend: AbstractBackend,
125
+ *,
126
+ config_nft: str | None = None,
127
+ config_address: str | None = None,
128
+ ) -> DanogoScriptAddresses:
129
+ """Read ProtocolDatum at the Protocol Config UTxO; return the 4 script addresses.
130
+
131
+ Backends match UTxOs by payment credential and don't support NFT-only lookups, so
132
+ resolution needs the address that holds the Protocol Config NFT. It defaults to
133
+ `PROTOCOL_CONFIG_SKH`; pass `config_address` to point at another deployment.
134
+ """
135
+ nft = config_nft or PROTOCOL_CONFIG_NFT
136
+ if nft is None:
137
+ raise ValueError("PROTOCOL_CONFIG_NFT anchor not set")
138
+ if config_address is None and PROTOCOL_CONFIG_SKH is not None:
139
+ config_address = _addr(PROTOCOL_CONFIG_SKH)
140
+ addresses = [config_address] if config_address else []
141
+ rows = list(
142
+ backend.get_pool_utxos(
143
+ addresses=addresses,
144
+ assets=[nft],
145
+ limit=1,
146
+ historical=False,
147
+ ),
148
+ )
149
+ if not rows:
150
+ raise ValueError("Protocol Config UTxO not found for anchor")
151
+ d = ProtocolDatum.from_cbor(rows[0].datum_cbor)
152
+ return DanogoScriptAddresses(
153
+ pool=_addr(d.pool_skh.hex()),
154
+ loan=_addr(d.loan_skh.hex()),
155
+ config_pool=_addr(d.config_pool_skh.hex()),
156
+ oracle=_addr(d.oracle_skh.hex()),
157
+ )