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.
Files changed (119) hide show
  1. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/PKG-INFO +1 -1
  2. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/pyproject.toml +2 -2
  3. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/loader.py +48 -11
  4. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/forward.py +0 -4
  5. charli3_dendrite-1.5.8.dev1/src/charli3_dendrite/lending/danogo/oracles/locator.py +300 -0
  6. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/state.py +1 -1
  7. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/state.py +2 -2
  8. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/models.py +49 -13
  9. charli3_dendrite-1.5.8.dev0/src/charli3_dendrite/lending/danogo/oracles/locator.py +0 -140
  10. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/LICENSE +0 -0
  11. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/README.md +0 -0
  12. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/__init__.py +0 -0
  13. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/__init__.py +0 -0
  14. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/backend_base.py +0 -0
  15. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/blockfrost/__init__.py +0 -0
  16. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/blockfrost/models.py +0 -0
  17. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/dbsync/__init__.py +0 -0
  18. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/dbsync/models.py +0 -0
  19. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/ogmios_kupo/__init__.py +0 -0
  20. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/ogmios_kupo/models.py +0 -0
  21. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/backend/utils.py +0 -0
  22. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dataclasses/__init__.py +0 -0
  23. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dataclasses/datums.py +0 -0
  24. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dataclasses/models.py +0 -0
  25. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/__init__.py +0 -0
  26. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/__init__.py +0 -0
  27. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/amm_base.py +0 -0
  28. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/amm_types.py +0 -0
  29. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/cswap.py +0 -0
  30. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/dano.py +0 -0
  31. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/minswap.py +0 -0
  32. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/muesli.py +0 -0
  33. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/spectrum.py +0 -0
  34. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/splash.py +0 -0
  35. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/sundae.py +0 -0
  36. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/vyfi.py +0 -0
  37. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/amm/wingriders.py +0 -0
  38. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/core/__init__.py +0 -0
  39. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/core/base.py +0 -0
  40. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/core/errors.py +0 -0
  41. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/__init__.py +0 -0
  42. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/axo.py +0 -0
  43. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/cardanoswaps.py +0 -0
  44. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/chadswap.py +0 -0
  45. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/djed.py +0 -0
  46. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/geniusyield.py +0 -0
  47. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/ob_base.py +0 -0
  48. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/dexs/ob/saturnswap.py +0 -0
  49. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/__init__.py +0 -0
  50. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/base.py +0 -0
  51. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/__init__.py +0 -0
  52. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/constants.py +0 -0
  53. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/datums.py +0 -0
  54. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/market.py +0 -0
  55. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/math.py +0 -0
  56. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/__init__.py +0 -0
  57. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/aggregator.py +0 -0
  58. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/aggregator_datums.py +0 -0
  59. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/deployments.py +0 -0
  60. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/leaves.py +0 -0
  61. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/lp_pricing.py +0 -0
  62. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/recipes.json +0 -0
  63. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/redeemer.py +0 -0
  64. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/reproduce.py +0 -0
  65. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/oracles/structured_datums.py +0 -0
  66. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/__init__.py +0 -0
  67. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/_common.py +0 -0
  68. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/build.py +0 -0
  69. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/builder.py +0 -0
  70. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/context.py +0 -0
  71. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/create_loan.py +0 -0
  72. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/datum_synth.py +0 -0
  73. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/increase_loan.py +0 -0
  74. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/loan_ids.py +0 -0
  75. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/modify_collateral.py +0 -0
  76. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/oracle_redeemer.py +0 -0
  77. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/oracle_synth.py +0 -0
  78. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/redeemers.py +0 -0
  79. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/repay.py +0 -0
  80. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/topup_withdraw.py +0 -0
  81. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/danogo/transactions/verify.py +0 -0
  82. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/__init__.py +0 -0
  83. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/constants.py +0 -0
  84. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/datums.py +0 -0
  85. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/loader.py +0 -0
  86. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/market.py +0 -0
  87. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/math.py +0 -0
  88. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/oracles/__init__.py +0 -0
  89. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/oracles/feeds.py +0 -0
  90. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/oracles/fluid_api.py +0 -0
  91. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/oracles/witness.py +0 -0
  92. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/__init__.py +0 -0
  93. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/_common.py +0 -0
  94. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/borrow.py +0 -0
  95. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/builder.py +0 -0
  96. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/change_collateral.py +0 -0
  97. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/context.py +0 -0
  98. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/datum_synth.py +0 -0
  99. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/lend.py +0 -0
  100. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/pool.py +0 -0
  101. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/recast.py +0 -0
  102. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/redeemers.py +0 -0
  103. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/repay.py +0 -0
  104. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/request.py +0 -0
  105. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/fluidtokens/transactions/resolve.py +0 -0
  106. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/math.py +0 -0
  107. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/__init__.py +0 -0
  108. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/charli3.py +0 -0
  109. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/dex_pooled.py +0 -0
  110. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/fluid.py +0 -0
  111. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/orcfax.py +0 -0
  112. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/oracles/qtoken.py +0 -0
  113. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/registry.py +0 -0
  114. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/transactions/__init__.py +0 -0
  115. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/transactions/base.py +0 -0
  116. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/transactions/infra.py +0 -0
  117. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/transactions/snapshot.py +0 -0
  118. {charli3_dendrite-1.5.8.dev0 → charli3_dendrite-1.5.8.dev1}/src/charli3_dendrite/lending/units.py +0 -0
  119. {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
  Metadata-Version: 2.4
2
2
  Name: charli3_dendrite
3
- Version: 1.5.8.dev0
3
+ Version: 1.5.8.dev1
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.5.8-dev0"
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-dev0"
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 pools by supply token so
209
- # loans (which only know their borrowed token) can find their market/pool.
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
- by_supply: dict[str, tuple[DanogoPoolState, DanogoMarket]] = {}
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
- by_supply[market.supply_token] = (pool, market)
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
- try:
223
- supply = loan.borrowed_unit
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 = ctx
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
- prices: dict[str, OraclePrice] = Field(default_factory=dict)
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`, keeping the one with the latest `valid_to`.
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
- existing = self.prices.get(price.token)
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[price.token] = price
160
+ self.prices[pair] = price
125
161
 
126
- def get(self, token: str) -> OraclePrice | None:
127
- """Return the stored price for `token`, or None if absent."""
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 `token`, or raise KeyError."""
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