charli3_dendrite 1.1.3.dev1__tar.gz → 1.1.3.dev2__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 (38) hide show
  1. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/PKG-INFO +3 -3
  2. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/README.md +2 -2
  3. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/pyproject.toml +2 -2
  4. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/cswap.py +2 -2
  5. charli3_dendrite-1.1.3.dev2/src/charli3_dendrite/dexs/ob/djed.py +946 -0
  6. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/LICENSE +0 -0
  7. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/__init__.py +0 -0
  8. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/backend/__init__.py +0 -0
  9. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/backend/backend_base.py +0 -0
  10. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/backend/blockfrost/__init__.py +0 -0
  11. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/backend/blockfrost/models.py +0 -0
  12. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/backend/dbsync/__init__.py +0 -0
  13. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/backend/dbsync/models.py +0 -0
  14. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/backend/ogmios_kupo/__init__.py +0 -0
  15. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/backend/ogmios_kupo/models.py +0 -0
  16. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/backend/utils.py +0 -0
  17. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dataclasses/__init__.py +0 -0
  18. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dataclasses/datums.py +0 -0
  19. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dataclasses/models.py +0 -0
  20. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/__init__.py +0 -0
  21. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/__init__.py +0 -0
  22. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/amm_base.py +0 -0
  23. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/amm_types.py +0 -0
  24. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/minswap.py +0 -0
  25. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/muesli.py +0 -0
  26. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/spectrum.py +0 -0
  27. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/splash.py +0 -0
  28. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/sundae.py +0 -0
  29. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/vyfi.py +0 -0
  30. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/amm/wingriders.py +0 -0
  31. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/core/__init__.py +0 -0
  32. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/core/base.py +0 -0
  33. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/core/errors.py +0 -0
  34. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/ob/__init__.py +0 -0
  35. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/ob/axo.py +0 -0
  36. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/ob/geniusyield.py +0 -0
  37. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/dexs/ob/ob_base.py +0 -0
  38. {charli3_dendrite-1.1.3.dev1 → charli3_dendrite-1.1.3.dev2}/src/charli3_dendrite/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: charli3_dendrite
3
- Version: 1.1.3.dev1
3
+ Version: 1.1.3.dev2
4
4
  Summary:
5
5
  Author: Elder Millenial
6
6
  Author-email: eldermillenial@protonmail.com
@@ -36,7 +36,7 @@ Charli3 Dendrite is a powerful Python SDK designed for seamless interaction with
36
36
 
37
37
  ## Key Features
38
38
 
39
- - 🔄 Multi-DEX Support: Integrate with Minswap, MuesliSwap, Spectrum, SundaeSwap, VyFi, GeniusYield, Splash, and WingRiders
39
+ - 🔄 Multi-DEX Support: Integrate with CSwap, Minswap, MuesliSwap, Spectrum, SundaeSwap, VyFi, GeniusYield, Splash, and WingRiders
40
40
  - 💧 Liquidity Pool Data: Fetch and analyze pool information across different DEXs
41
41
  - 💱 Swap Operations: Execute token swaps with ease
42
42
  - 🧩 Flexible Asset Handling: Manage various asset types and pool states efficiently
@@ -65,6 +65,7 @@ Charli3 Dendrite currently supports the following Cardano DEXs:
65
65
  - WingRiders
66
66
  - GeniusYield
67
67
  - Splash
68
+ - CSwap
68
69
 
69
70
  ### Deprecated DEXs
70
71
 
@@ -73,7 +74,6 @@ Charli3 Dendrite currently supports the following Cardano DEXs:
73
74
  ### Not Yet Implemented
74
75
 
75
76
  - CardanoSwaps
76
- - CSwap
77
77
  - Cerra
78
78
  - SaturnSwap
79
79
 
@@ -15,7 +15,7 @@ Charli3 Dendrite is a powerful Python SDK designed for seamless interaction with
15
15
 
16
16
  ## Key Features
17
17
 
18
- - 🔄 Multi-DEX Support: Integrate with Minswap, MuesliSwap, Spectrum, SundaeSwap, VyFi, GeniusYield, Splash, and WingRiders
18
+ - 🔄 Multi-DEX Support: Integrate with CSwap, Minswap, MuesliSwap, Spectrum, SundaeSwap, VyFi, GeniusYield, Splash, and WingRiders
19
19
  - 💧 Liquidity Pool Data: Fetch and analyze pool information across different DEXs
20
20
  - 💱 Swap Operations: Execute token swaps with ease
21
21
  - 🧩 Flexible Asset Handling: Manage various asset types and pool states efficiently
@@ -44,6 +44,7 @@ Charli3 Dendrite currently supports the following Cardano DEXs:
44
44
  - WingRiders
45
45
  - GeniusYield
46
46
  - Splash
47
+ - CSwap
47
48
 
48
49
  ### Deprecated DEXs
49
50
 
@@ -52,7 +53,6 @@ Charli3 Dendrite currently supports the following Cardano DEXs:
52
53
  ### Not Yet Implemented
53
54
 
54
55
  - CardanoSwaps
55
- - CSwap
56
56
  - Cerra
57
57
  - SaturnSwap
58
58
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "charli3_dendrite"
3
- version = "1.1.3-dev1"
3
+ version = "1.1.3-dev2"
4
4
  description = ""
5
5
  authors = ["Elder Millenial <eldermillenial@protonmail.com>"]
6
6
  readme = "README.md"
@@ -49,7 +49,7 @@ module = ["pycardano", "pycardano.*", "dotenv", "nox"]
49
49
  ignore_missing_imports = true
50
50
 
51
51
  [tool.bumpversion]
52
- current_version = "1.1.3-dev1"
52
+ current_version = "1.1.3-dev2"
53
53
  parse = """(?x)
54
54
  (?P<major>\\d+)\\.
55
55
  (?P<minor>\\d+)\\.
@@ -53,7 +53,7 @@ class CSwapOrderDatum(OrderDatum):
53
53
  address: PlutusFullAddress # Field 0: Complex address structure
54
54
  target_assets: List[List[Union[bytes, int]]]
55
55
  input_assets: List[List[Union[bytes, int]]]
56
- order_type: Union[CSwapOrderSwapType | CSwapOrderZapInType | CSwapOrderZapOutType]
56
+ otype: Union[CSwapOrderSwapType | CSwapOrderZapInType | CSwapOrderZapOutType]
57
57
  slippage: int = 50
58
58
  platform_fee: int = 15
59
59
 
@@ -104,7 +104,7 @@ class CSwapOrderDatum(OrderDatum):
104
104
  address=full_address,
105
105
  target_assets=target_assets,
106
106
  input_assets=input_assets,
107
- order_type=CSwapOrderSwapType(),
107
+ otype=CSwapOrderSwapType(),
108
108
  slippage=50, # 0.5% default slippage
109
109
  platform_fee=15, # 0.15% platform fee
110
110
  )
@@ -0,0 +1,946 @@
1
+ """Djed/Shen Stablecoin Order Book Module.
2
+
3
+ This module provides order book functionality for Djed (collateralized stablecoin)
4
+ and Shen (liquidity token) operations, following the exact patterns established
5
+ by the GeniusYield implementation.
6
+ """
7
+
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import Union
11
+
12
+ from pycardano import Address
13
+ from pycardano import PlutusData
14
+ from pycardano import PlutusV1Script
15
+ from pycardano import PlutusV2Script
16
+ from pycardano import Redeemer
17
+ from pycardano import TransactionBuilder
18
+ from pycardano import TransactionId
19
+ from pycardano import TransactionInput
20
+ from pycardano import TransactionOutput
21
+ from pycardano import UTxO
22
+
23
+ from charli3_dendrite.backend import get_backend
24
+ from charli3_dendrite.dataclasses.datums import OrderDatum
25
+ from charli3_dendrite.dataclasses.datums import PlutusFullAddress
26
+ from charli3_dendrite.dataclasses.models import Assets
27
+ from charli3_dendrite.dataclasses.models import OrderType
28
+ from charli3_dendrite.dataclasses.models import PoolSelector
29
+ from charli3_dendrite.dexs.ob.ob_base import AbstractOrderBookState
30
+ from charli3_dendrite.dexs.ob.ob_base import AbstractOrderState
31
+ from charli3_dendrite.dexs.ob.ob_base import BuyOrderBook
32
+ from charli3_dendrite.dexs.ob.ob_base import OrderBookOrder
33
+ from charli3_dendrite.dexs.ob.ob_base import SellOrderBook
34
+ from charli3_dendrite.utility import DjedRational
35
+ from charli3_dendrite.utility import asset_to_value
36
+
37
+
38
+ @dataclass
39
+ class DjedRationalDatum(PlutusData):
40
+ """Plutus-compatible rational number for on-chain data."""
41
+
42
+ CONSTR_ID = 0
43
+ numerator: int
44
+ denominator: int
45
+
46
+
47
+ @dataclass
48
+ class DjedMintAction(PlutusData):
49
+ """Djed minting action in order datum."""
50
+
51
+ CONSTR_ID = 0
52
+ djed_amount: int
53
+ ada_amount: int
54
+
55
+
56
+ @dataclass
57
+ class DjedBurnAction(PlutusData):
58
+ """Djed burning action in order datum."""
59
+
60
+ CONSTR_ID = 1
61
+ djed_amount: int
62
+
63
+
64
+ @dataclass
65
+ class ShenMintAction(PlutusData):
66
+ """Shen minting action in order datum."""
67
+
68
+ CONSTR_ID = 2
69
+ shen_amount: int
70
+ ada_amount: int
71
+
72
+
73
+ @dataclass
74
+ class ShenBurnAction(PlutusData):
75
+ """Shen burning action in order datum."""
76
+
77
+ CONSTR_ID = 3
78
+ shen_amount: int
79
+
80
+
81
+ @dataclass
82
+ class DjedOrderDatum(OrderDatum):
83
+ """Djed/Shen order datum structure (following existing OrderDatum pattern)."""
84
+
85
+ CONSTR_ID = 0
86
+ action: Union[DjedMintAction, DjedBurnAction, ShenMintAction, ShenBurnAction]
87
+ owner_address: PlutusFullAddress
88
+ oracle_rate: DjedRationalDatum
89
+ creation_time: int
90
+ order_nft: bytes
91
+
92
+ def pool_pair(self) -> Assets | None:
93
+ """Return the asset pair for this order (required by OrderDatum interface)."""
94
+ if isinstance(self.action, (DjedMintAction, DjedBurnAction)):
95
+ # Djed <-> ADA pair (placeholder policy IDs)
96
+ return Assets(lovelace=0) + Assets(**{"djed_policy_id.djed_token_name": 0})
97
+ else: # Shen operations
98
+ # Shen <-> ADA pair (placeholder policy IDs)
99
+ return Assets(lovelace=0) + Assets(**{"shen_policy_id.shen_token_name": 0})
100
+
101
+ def address_source(self) -> str | None:
102
+ """Source address (required by OrderDatum interface)."""
103
+ return self.owner_address.to_address().encode("bech32")
104
+
105
+ def requested_amount(self) -> Assets:
106
+ """Return the requested amount for this order (required by OrderDatum interface)."""
107
+ if isinstance(self.action, DjedMintAction):
108
+ return Assets(**{"djed_policy_id.djed_token_name": self.action.djed_amount})
109
+ elif isinstance(self.action, DjedBurnAction):
110
+ # For burn, calculate ADA amount based on oracle rate
111
+ oracle_rate = DjedRational(
112
+ self.oracle_rate.numerator, self.oracle_rate.denominator
113
+ )
114
+ ada_amount = (
115
+ oracle_rate.invert().mul(DjedRational(self.action.djed_amount)).to_int()
116
+ )
117
+ return Assets(lovelace=ada_amount)
118
+ elif isinstance(self.action, ShenMintAction):
119
+ return Assets(**{"shen_policy_id.shen_token_name": self.action.shen_amount})
120
+ else: # ShenBurnAction
121
+ # For Shen burn, calculation is more complex and requires pool state
122
+ return Assets(lovelace=self.action.shen_amount) # Placeholder
123
+
124
+ def order_type(self) -> OrderType | None:
125
+ """Order type classification (required by OrderDatum interface)."""
126
+ if isinstance(self.action, (DjedMintAction, ShenMintAction)):
127
+ return OrderType.deposit # Minting = deposit operation
128
+ else:
129
+ return OrderType.swap # Burning = swap operation
130
+
131
+
132
+ # === SHARED BASE CLASS FOR COMMON FUNCTIONALITY ===
133
+
134
+
135
+ class DjedShenOrderStateBase(AbstractOrderState):
136
+ """Base class for Djed/Shen order states sharing common functionality.
137
+
138
+ Reduces code duplication between Djed and Shen implementations by providing
139
+ shared methods that follow the exact GeniusYield pattern.
140
+ """
141
+
142
+ tx_hash: str
143
+ tx_index: int
144
+ datum_cbor: str
145
+ datum_hash: str
146
+ inactive: bool = False
147
+
148
+ _batcher_fee: Assets = Assets(lovelace=2_000_000) # 2 ADA operator fee
149
+ _datum_parsed: PlutusData | None = None
150
+
151
+ @classmethod
152
+ def dex_policy(cls) -> list[str] | None:
153
+ """Djed/Shen order NFT policy IDs (following GeniusYield pattern)."""
154
+ return [
155
+ "djed_order_policy_mainnet_placeholder", # Replace with actual policy
156
+ "djed_order_policy_preprod_placeholder", # Replace with actual policy
157
+ ]
158
+
159
+ @property
160
+ def volume_fee(self) -> float:
161
+ """Fee percentage for operations (following GeniusYield pattern)."""
162
+ return 150 # 1.5% in basis points
163
+
164
+ @property
165
+ def reference_utxo(self) -> UTxO | None:
166
+ """Get reference UTxO for script validation (following GeniusYield pattern)."""
167
+ order_info = get_backend().get_pool_in_tx(
168
+ self.tx_hash,
169
+ assets=[self.dex_nft.unit()],
170
+ addresses=self.pool_selector().addresses,
171
+ )
172
+
173
+ script = get_backend().get_script_from_address(
174
+ Address.decode(order_info[0].address),
175
+ )
176
+
177
+ return UTxO(
178
+ input=TransactionInput(
179
+ TransactionId(bytes.fromhex(script.tx_hash)),
180
+ index=script.tx_index,
181
+ ),
182
+ output=TransactionOutput(
183
+ address=script.address,
184
+ amount=asset_to_value(script.assets),
185
+ script=PlutusV2Script(bytes.fromhex(script.script)),
186
+ ),
187
+ )
188
+
189
+ def _get_pool_utxo(self) -> UTxO:
190
+ """Get pool UTxO using backend (shared by both Djed and Shen)."""
191
+ pool_utxos = get_backend().get_pool_utxos(
192
+ addresses=["djed_pool_address_placeholder"], # Replace with actual
193
+ assets=["djed_pool_nft_placeholder"], # Replace with actual
194
+ limit=1,
195
+ historical=False,
196
+ )
197
+ if not pool_utxos:
198
+ raise RuntimeError("Pool UTxO not found")
199
+
200
+ pool_info = pool_utxos[0]
201
+ return UTxO(
202
+ input=TransactionInput(
203
+ TransactionId(bytes.fromhex(pool_info.tx_hash)),
204
+ index=pool_info.tx_index,
205
+ ),
206
+ output=TransactionOutput(
207
+ address=Address.decode(pool_info.address),
208
+ amount=asset_to_value(pool_info.assets),
209
+ ),
210
+ )
211
+
212
+ def _get_oracle_utxo(self) -> UTxO:
213
+ """Get oracle UTxO using backend (shared by both Djed and Shen)."""
214
+ oracle_utxos = get_backend().get_pool_utxos(
215
+ addresses=["djed_oracle_address_placeholder"], # Replace with actual
216
+ assets=["djed_oracle_nft_placeholder"], # Replace with actual
217
+ limit=1,
218
+ historical=False,
219
+ )
220
+ if not oracle_utxos:
221
+ raise RuntimeError("Oracle UTxO not found")
222
+
223
+ oracle_info = oracle_utxos[0]
224
+ return UTxO(
225
+ input=TransactionInput(
226
+ TransactionId(bytes.fromhex(oracle_info.tx_hash)),
227
+ index=oracle_info.tx_index,
228
+ ),
229
+ output=TransactionOutput(
230
+ address=Address.decode(oracle_info.address),
231
+ amount=asset_to_value(oracle_info.assets),
232
+ ),
233
+ )
234
+
235
+ @classmethod
236
+ def post_init(cls, values: dict[str, ...]):
237
+ """Post initialization validation (shared logic)."""
238
+ super().post_init(values)
239
+
240
+ # Parse and validate order datum
241
+ try:
242
+ datum = cls.order_datum_class().from_cbor(values["datum_cbor"])
243
+
244
+ # Check if order is expired (3-minute TTL)
245
+ current_time = int(time.time())
246
+ if current_time > datum.creation_time + 180: # 3 minutes
247
+ values["inactive"] = True
248
+
249
+ except Exception as e:
250
+ values["inactive"] = True
251
+
252
+ @classmethod
253
+ def order_selector(cls) -> list[str]:
254
+ """Order contract addresses (shared)."""
255
+ return [
256
+ "addr1_djed_order_mainnet_placeholder", # Replace with actual
257
+ "addr_test1_djed_order_preprod_placeholder", # Replace with actual
258
+ ]
259
+
260
+ @classmethod
261
+ def pool_selector(cls) -> PoolSelector:
262
+ """Pool selection for Djed/Shen orders (shared)."""
263
+ return PoolSelector(
264
+ addresses=[
265
+ "addr1_djed_pool_mainnet_placeholder", # Replace with actual
266
+ "addr_test1_djed_pool_preprod_placeholder", # Replace with actual
267
+ ],
268
+ )
269
+
270
+ @property
271
+ def swap_forward(self) -> bool:
272
+ """Returns if swap forwarding is enabled."""
273
+ return False
274
+
275
+ @property
276
+ def stake_address(self) -> Address | None:
277
+ """Return the staking address."""
278
+ return None
279
+
280
+ @classmethod
281
+ def order_datum_class(cls) -> type[PlutusData]:
282
+ """Returns data class used for handling order datums."""
283
+ return DjedOrderDatum
284
+
285
+ @classmethod
286
+ def default_script_class(cls) -> type[PlutusV1Script] | type[PlutusV2Script]:
287
+ """Get default script class."""
288
+ return PlutusV2Script
289
+
290
+ @property
291
+ def pool_id(self) -> str:
292
+ """A unique identifier for the pool or ob."""
293
+ return "Djed"
294
+
295
+
296
+ # === DJED-SPECIFIC ORDER STATE ===
297
+
298
+
299
+ class DjedOrderState(DjedShenOrderStateBase):
300
+ """Djed order state handling Djed mint/burn operations.
301
+
302
+ Inherits common functionality from DjedShenOrderStateBase and implements
303
+ Djed-specific pricing and transaction logic.
304
+ """
305
+
306
+ @classmethod
307
+ def dex(cls) -> str:
308
+ """Official dex name."""
309
+ return "Djed"
310
+
311
+ @property
312
+ def price(self) -> tuple[int, int]:
313
+ """Price for Djed operations (ADA per Djed)."""
314
+ oracle_rate_datum = self.order_datum.oracle_rate
315
+ oracle_rate = DjedRational(
316
+ oracle_rate_datum.numerator, oracle_rate_datum.denominator
317
+ )
318
+
319
+ if isinstance(self.order_datum.action, DjedMintAction):
320
+ # Djed mint: ADA per Djed (includes fees)
321
+ base_rate = oracle_rate.invert() # Convert Djed/ADA to ADA/Djed
322
+ fee_multiplier = DjedRational(1015, 1000) # 1.5% fee
323
+ final_rate = base_rate.mul(fee_multiplier)
324
+ return final_rate.to_tuple()
325
+ else: # DjedBurnAction
326
+ # Djed burn: ADA per Djed (after fees)
327
+ base_rate = oracle_rate.invert()
328
+ fee_multiplier = DjedRational(985, 1000) # 1.5% fee deduction
329
+ final_rate = base_rate.mul(fee_multiplier)
330
+ return final_rate.to_tuple()
331
+
332
+ @property
333
+ def available(self) -> Assets:
334
+ """Available amount for Djed orders."""
335
+ if isinstance(self.order_datum.action, DjedMintAction):
336
+ return Assets(
337
+ **{
338
+ "djed_policy_id.djed_token_name": self.order_datum.action.djed_amount
339
+ }
340
+ )
341
+ else: # DjedBurnAction
342
+ # Calculate ADA to return based on current oracle rate
343
+ ada_amount = self._calculate_ada_return(self.order_datum.action.djed_amount)
344
+ return Assets(lovelace=ada_amount)
345
+
346
+ def _calculate_ada_return(self, djed_amount: int) -> int:
347
+ """Calculate ADA to return for Djed burning."""
348
+ oracle_rate_datum = self.order_datum.oracle_rate
349
+ oracle_rate = DjedRational(
350
+ oracle_rate_datum.numerator, oracle_rate_datum.denominator
351
+ )
352
+
353
+ base_rate = oracle_rate.invert() # ADA per Djed
354
+ fee_multiplier = DjedRational(985, 1000) # 1.5% fee deduction
355
+ final_rate = base_rate.mul(fee_multiplier)
356
+
357
+ djed_rational = DjedRational(djed_amount, 1)
358
+ return final_rate.mul(djed_rational).to_int("ROUND_DOWN")
359
+
360
+ def swap_utxo(
361
+ self,
362
+ address_source: Address,
363
+ in_assets: Assets,
364
+ out_assets: Assets,
365
+ tx_builder: TransactionBuilder,
366
+ extra_assets: Assets | None = None,
367
+ address_target: Address | None = None,
368
+ datum_target: PlutusData | None = None,
369
+ ) -> tuple[TransactionOutput | None, PlutusData]:
370
+ """Build transaction for Djed order processing."""
371
+
372
+ # Get reference UTxOs (using shared methods)
373
+ pool_utxo = self._get_pool_utxo()
374
+ oracle_utxo = self._get_oracle_utxo()
375
+
376
+ # Add order UTxO as script input (following GeniusYield pattern)
377
+ assets = self.assets + Assets(**{self.dex_nft.unit(): 1})
378
+ order_utxo = UTxO(
379
+ TransactionInput(
380
+ transaction_id=TransactionId(bytes.fromhex(self.tx_hash)),
381
+ index=self.tx_index,
382
+ ),
383
+ output=TransactionOutput(
384
+ address=Address.decode(self.address),
385
+ amount=asset_to_value(assets),
386
+ datum_hash=self.order_datum.hash(),
387
+ ),
388
+ )
389
+
390
+ # Add script input with redeemer
391
+ if out_assets.quantity() < self.available.quantity():
392
+ redeemer = Redeemer(self._get_partial_redeemer(out_assets))
393
+ else:
394
+ redeemer = Redeemer(self._get_complete_redeemer())
395
+
396
+ tx_builder.add_script_input(
397
+ utxo=order_utxo,
398
+ script=self.reference_utxo,
399
+ redeemer=redeemer,
400
+ )
401
+
402
+ # Add reference inputs
403
+ tx_builder.reference_inputs.add(pool_utxo)
404
+ tx_builder.reference_inputs.add(oracle_utxo)
405
+
406
+ # Process based on Djed operation type
407
+ if isinstance(self.order_datum.action, DjedMintAction):
408
+ return self._process_djed_mint(tx_builder, in_assets, out_assets, pool_utxo)
409
+ else: # DjedBurnAction
410
+ return self._process_djed_burn(tx_builder, in_assets, out_assets, pool_utxo)
411
+
412
+ def _process_djed_mint(
413
+ self,
414
+ tx_builder: TransactionBuilder,
415
+ in_assets: Assets,
416
+ out_assets: Assets,
417
+ pool_utxo: UTxO,
418
+ ) -> tuple[TransactionOutput | None, PlutusData]:
419
+ """Process Djed minting order."""
420
+ # Update order datum if partial fill
421
+ order_datum = self.order_datum_class().from_cbor(self.order_datum.to_cbor())
422
+ order_datum.action.djed_amount -= out_assets.quantity()
423
+
424
+ # Update pool state with new Djed tokens
425
+ updated_assets = self.assets.copy()
426
+ updated_assets.root[in_assets.unit()] += in_assets.quantity()
427
+ updated_assets.root[out_assets.unit()] -= out_assets.quantity()
428
+ updated_assets += self._batcher_fee
429
+
430
+ if out_assets.quantity() < self.available.quantity():
431
+ # Partial fill - return updated order
432
+ txo = TransactionOutput(
433
+ address=Address.decode(self.address),
434
+ amount=asset_to_value(updated_assets),
435
+ datum_hash=order_datum.hash(),
436
+ )
437
+ else:
438
+ # Complete fill - pay user and close order
439
+ # Burn the beacon token
440
+ tx_builder.add_minting_script(
441
+ script=self.reference_utxo,
442
+ redeemer=Redeemer(PlutusData()), # Cancel redeemer placeholder
443
+ )
444
+ if tx_builder.mint is None:
445
+ tx_builder.mint = asset_to_value(
446
+ Assets(**{self.dex_nft.unit(): -1}),
447
+ ).multi_asset
448
+ else:
449
+ tx_builder.mint += asset_to_value(
450
+ Assets(**{self.dex_nft.unit(): -1}),
451
+ ).multi_asset
452
+
453
+ # Pay Djed tokens to user
454
+ payment_assets = Assets(**{out_assets.unit(): out_assets.quantity()})
455
+ payment_assets += Assets(lovelace=2_000_000) # Min ADA
456
+
457
+ txo = TransactionOutput(
458
+ address=order_datum.owner_address.to_address(),
459
+ amount=asset_to_value(payment_assets),
460
+ )
461
+
462
+ tx_builder.datums.update({order_datum.hash(): order_datum})
463
+ return txo, order_datum
464
+
465
+ def _process_djed_burn(
466
+ self,
467
+ tx_builder: TransactionBuilder,
468
+ in_assets: Assets,
469
+ out_assets: Assets,
470
+ pool_utxo: UTxO,
471
+ ) -> tuple[TransactionOutput | None, PlutusData]:
472
+ """Process Djed burning order."""
473
+ # Similar to mint but burning Djed for ADA
474
+ order_datum = self.order_datum_class().from_cbor(self.order_datum.to_cbor())
475
+ order_datum.action.djed_amount -= in_assets.quantity()
476
+
477
+ # Update pool state
478
+ updated_assets = self.assets.copy()
479
+ updated_assets.root[in_assets.unit()] -= in_assets.quantity()
480
+ updated_assets.root[out_assets.unit()] += out_assets.quantity()
481
+ updated_assets += self._batcher_fee
482
+
483
+ if in_assets.quantity() < self.available.quantity():
484
+ # Partial fill
485
+ txo = TransactionOutput(
486
+ address=Address.decode(self.address),
487
+ amount=asset_to_value(updated_assets),
488
+ datum_hash=order_datum.hash(),
489
+ )
490
+ else:
491
+ # Complete fill - close order and pay ADA
492
+ # Burn the beacon token
493
+ tx_builder.add_minting_script(
494
+ script=self.reference_utxo,
495
+ redeemer=Redeemer(PlutusData()),
496
+ )
497
+ if tx_builder.mint is None:
498
+ tx_builder.mint = asset_to_value(
499
+ Assets(**{self.dex_nft.unit(): -1}),
500
+ ).multi_asset
501
+ else:
502
+ tx_builder.mint += asset_to_value(
503
+ Assets(**{self.dex_nft.unit(): -1}),
504
+ ).multi_asset
505
+
506
+ # Pay ADA to user
507
+ payment_assets = Assets(lovelace=out_assets.quantity())
508
+
509
+ txo = TransactionOutput(
510
+ address=order_datum.owner_address.to_address(),
511
+ amount=asset_to_value(payment_assets),
512
+ )
513
+
514
+ tx_builder.datums.update({order_datum.hash(): order_datum})
515
+ return txo, order_datum
516
+
517
+ def _get_partial_redeemer(self, out_assets: Assets) -> PlutusData:
518
+ """Get redeemer for partial order processing."""
519
+ return PlutusData() # Placeholder - implement based on actual contract
520
+
521
+ def _get_complete_redeemer(self) -> PlutusData:
522
+ """Get redeemer for complete order processing."""
523
+ return PlutusData() # Placeholder - implement based on actual contract
524
+
525
+
526
+ # === SHEN-SPECIFIC ORDER STATE ===
527
+
528
+
529
+ class ShenOrderState(DjedShenOrderStateBase):
530
+ """Shen order state handling Shen mint/burn operations.
531
+
532
+ Inherits common functionality from DjedShenOrderStateBase and implements
533
+ Shen-specific pricing and transaction logic. Shen pricing is more complex
534
+ as it depends on pool reserves and collateral ratios.
535
+ """
536
+
537
+ @classmethod
538
+ def dex(cls) -> str:
539
+ """Official dex name."""
540
+ return "Shen"
541
+
542
+ @property
543
+ def price(self) -> tuple[int, int]:
544
+ """Price for Shen operations (more complex - requires pool state)."""
545
+ return self._calculate_shen_price()
546
+
547
+ @property
548
+ def available(self) -> Assets:
549
+ """Available amount for Shen orders."""
550
+ if isinstance(self.order_datum.action, ShenMintAction):
551
+ return Assets(
552
+ **{
553
+ "shen_policy_id.shen_token_name": self.order_datum.action.shen_amount
554
+ }
555
+ )
556
+ else: # ShenBurnAction
557
+ ada_amount = self._calculate_shen_ada_return(
558
+ self.order_datum.action.shen_amount
559
+ )
560
+ return Assets(lovelace=ada_amount)
561
+
562
+ def _calculate_shen_price(self) -> tuple[int, int]:
563
+ """Calculate Shen price based on current pool state.
564
+
565
+ Shen price is determined by the excess ADA reserves beyond what's needed
566
+ to back the Djed tokens. This is more complex than Djed pricing.
567
+ """
568
+ try:
569
+ # Get current pool state
570
+ pool_utxo = self._get_pool_utxo()
571
+ # TODO: Parse pool datum to get reserve amounts and Djed supply
572
+ # For now, return placeholder
573
+ return (1, 1) # ADA per Shen - placeholder
574
+ except Exception:
575
+ return (1, 1) # Fallback price
576
+
577
+ def _calculate_shen_ada_return(self, shen_amount: int) -> int:
578
+ """Calculate ADA to return for Shen burning.
579
+
580
+ Based on Shen's share of excess reserves beyond Djed backing.
581
+ """
582
+ try:
583
+ # Get current pool state and calculate Shen's share
584
+ # TODO: Implement complex calculation based on pool reserves
585
+ return shen_amount # Placeholder - 1:1 ratio
586
+ except Exception:
587
+ return shen_amount # Fallback
588
+
589
+ def swap_utxo(
590
+ self,
591
+ address_source: Address,
592
+ in_assets: Assets,
593
+ out_assets: Assets,
594
+ tx_builder: TransactionBuilder,
595
+ extra_assets: Assets | None = None,
596
+ address_target: Address | None = None,
597
+ datum_target: PlutusData | None = None,
598
+ ) -> tuple[TransactionOutput | None, PlutusData]:
599
+ """Build transaction for Shen order processing."""
600
+
601
+ # Get reference UTxOs (using shared methods)
602
+ pool_utxo = self._get_pool_utxo()
603
+ oracle_utxo = self._get_oracle_utxo()
604
+
605
+ # Similar structure to Djed but with Shen-specific logic
606
+ assets = self.assets + Assets(**{self.dex_nft.unit(): 1})
607
+ order_utxo = UTxO(
608
+ TransactionInput(
609
+ transaction_id=TransactionId(bytes.fromhex(self.tx_hash)),
610
+ index=self.tx_index,
611
+ ),
612
+ output=TransactionOutput(
613
+ address=Address.decode(self.address),
614
+ amount=asset_to_value(assets),
615
+ datum_hash=self.order_datum.hash(),
616
+ ),
617
+ )
618
+
619
+ # Add script input with redeemer
620
+ if out_assets.quantity() < self.available.quantity():
621
+ redeemer = Redeemer(PlutusData()) # Partial redeemer
622
+ else:
623
+ redeemer = Redeemer(PlutusData()) # Complete redeemer
624
+
625
+ tx_builder.add_script_input(
626
+ utxo=order_utxo,
627
+ script=self.reference_utxo,
628
+ redeemer=redeemer,
629
+ )
630
+
631
+ # Add reference inputs
632
+ tx_builder.reference_inputs.add(pool_utxo)
633
+ tx_builder.reference_inputs.add(oracle_utxo)
634
+
635
+ # Process based on Shen operation type
636
+ if isinstance(self.order_datum.action, ShenMintAction):
637
+ return self._process_shen_mint(tx_builder, in_assets, out_assets, pool_utxo)
638
+ else: # ShenBurnAction
639
+ return self._process_shen_burn(tx_builder, in_assets, out_assets, pool_utxo)
640
+
641
+ def _process_shen_mint(
642
+ self,
643
+ tx_builder: TransactionBuilder,
644
+ in_assets: Assets,
645
+ out_assets: Assets,
646
+ pool_utxo: UTxO,
647
+ ) -> tuple[TransactionOutput | None, PlutusData]:
648
+ """Process Shen minting order."""
649
+ # Similar to Djed mint but for Shen tokens
650
+ order_datum = self.order_datum_class().from_cbor(self.order_datum.to_cbor())
651
+ order_datum.action.shen_amount -= out_assets.quantity()
652
+
653
+ updated_assets = self.assets.copy()
654
+ updated_assets.root[in_assets.unit()] += in_assets.quantity()
655
+ updated_assets.root[out_assets.unit()] -= out_assets.quantity()
656
+ updated_assets += self._batcher_fee
657
+
658
+ if out_assets.quantity() < self.available.quantity():
659
+ txo = TransactionOutput(
660
+ address=Address.decode(self.address),
661
+ amount=asset_to_value(updated_assets),
662
+ datum_hash=order_datum.hash(),
663
+ )
664
+ else:
665
+ # Complete fill logic (similar to Djed)
666
+ tx_builder.add_minting_script(
667
+ script=self.reference_utxo,
668
+ redeemer=Redeemer(PlutusData()),
669
+ )
670
+ if tx_builder.mint is None:
671
+ tx_builder.mint = asset_to_value(
672
+ Assets(**{self.dex_nft.unit(): -1}),
673
+ ).multi_asset
674
+ else:
675
+ tx_builder.mint += asset_to_value(
676
+ Assets(**{self.dex_nft.unit(): -1}),
677
+ ).multi_asset
678
+
679
+ payment_assets = Assets(**{out_assets.unit(): out_assets.quantity()})
680
+ payment_assets += Assets(lovelace=2_000_000)
681
+
682
+ txo = TransactionOutput(
683
+ address=order_datum.owner_address.to_address(),
684
+ amount=asset_to_value(payment_assets),
685
+ )
686
+
687
+ tx_builder.datums.update({order_datum.hash(): order_datum})
688
+ return txo, order_datum
689
+
690
+ def _process_shen_burn(
691
+ self,
692
+ tx_builder: TransactionBuilder,
693
+ in_assets: Assets,
694
+ out_assets: Assets,
695
+ pool_utxo: UTxO,
696
+ ) -> tuple[TransactionOutput | None, PlutusData]:
697
+ """Process Shen burning order."""
698
+ # Similar to Djed burn but for Shen tokens
699
+ order_datum = self.order_datum_class().from_cbor(self.order_datum.to_cbor())
700
+ order_datum.action.shen_amount -= in_assets.quantity()
701
+
702
+ updated_assets = self.assets.copy()
703
+ updated_assets.root[in_assets.unit()] -= in_assets.quantity()
704
+ updated_assets.root[out_assets.unit()] += out_assets.quantity()
705
+ updated_assets += self._batcher_fee
706
+
707
+ if in_assets.quantity() < self.available.quantity():
708
+ txo = TransactionOutput(
709
+ address=Address.decode(self.address),
710
+ amount=asset_to_value(updated_assets),
711
+ datum_hash=order_datum.hash(),
712
+ )
713
+ else:
714
+ # Complete fill logic
715
+ tx_builder.add_minting_script(
716
+ script=self.reference_utxo,
717
+ redeemer=Redeemer(PlutusData()),
718
+ )
719
+ if tx_builder.mint is None:
720
+ tx_builder.mint = asset_to_value(
721
+ Assets(**{self.dex_nft.unit(): -1}),
722
+ ).multi_asset
723
+ else:
724
+ tx_builder.mint += asset_to_value(
725
+ Assets(**{self.dex_nft.unit(): -1}),
726
+ ).multi_asset
727
+
728
+ payment_assets = Assets(lovelace=out_assets.quantity())
729
+
730
+ txo = TransactionOutput(
731
+ address=order_datum.owner_address.to_address(),
732
+ amount=asset_to_value(payment_assets),
733
+ )
734
+
735
+ tx_builder.datums.update({order_datum.hash(): order_datum})
736
+ return txo, order_datum
737
+
738
+
739
+ # === SHARED ORDER BOOK BASE CLASS ===
740
+
741
+
742
+ class DjedShenOrderBookBase(AbstractOrderBookState):
743
+ """Base class for Djed/Shen order books sharing common functionality."""
744
+
745
+ fee: int = 150 # 1.5% fee in basis points
746
+ _deposit: Assets = Assets(lovelace=2_000_000)
747
+
748
+ @classmethod
749
+ def order_selector(cls) -> list[str]:
750
+ """Order selection information."""
751
+ return DjedShenOrderStateBase.order_selector()
752
+
753
+ @classmethod
754
+ def pool_selector(cls) -> PoolSelector:
755
+ """Pool selection information."""
756
+ return DjedShenOrderStateBase.pool_selector()
757
+
758
+ @property
759
+ def swap_forward(self) -> bool:
760
+ """Returns if swap forwarding is enabled."""
761
+ return True
762
+
763
+ @classmethod
764
+ def default_script_class(cls):
765
+ """Get default script class."""
766
+ return DjedShenOrderStateBase.default_script_class()
767
+
768
+ @classmethod
769
+ def order_datum_class(cls):
770
+ """Returns data class used for handling order datums."""
771
+ return DjedShenOrderStateBase.order_datum_class()
772
+
773
+ @property
774
+ def stake_address(self) -> Address | None:
775
+ """Return the staking address."""
776
+ return None
777
+
778
+
779
+ # === DJED ORDER BOOK ===
780
+
781
+
782
+ class DjedOrderBook(DjedShenOrderBookBase):
783
+ """Djed order book for Djed mint/burn operations."""
784
+
785
+ @classmethod
786
+ def get_book(
787
+ cls,
788
+ assets: Assets,
789
+ orders: list[DjedOrderState] | None = None,
790
+ ) -> "DjedOrderBook":
791
+ """Create Djed order book."""
792
+ if orders is None:
793
+ selector = DjedOrderState.pool_selector()
794
+
795
+ result = get_backend().get_pool_utxos(
796
+ limit=1000,
797
+ historical=False,
798
+ **selector.model_dump(),
799
+ )
800
+
801
+ # Filter for Djed orders only
802
+ orders = [
803
+ DjedOrderState.model_validate(r.model_dump())
804
+ for r in result
805
+ if cls._is_djed_order(r)
806
+ ]
807
+
808
+ # Sort into buy (mint) and sell (burn) orders
809
+ buy_orders = [] # Djed mint orders
810
+ sell_orders = [] # Djed burn orders
811
+
812
+ for order in orders:
813
+ if order.inactive:
814
+ continue
815
+
816
+ price = order.price[0] / order.price[1]
817
+ o = OrderBookOrder(
818
+ price=price,
819
+ quantity=int(order.available.quantity()),
820
+ state=order,
821
+ )
822
+
823
+ if isinstance(order.order_datum.action, DjedMintAction):
824
+ buy_orders.append(o) # Mint = Buy
825
+ else: # DjedBurnAction
826
+ sell_orders.append(o) # Burn = Sell
827
+
828
+ ob = DjedOrderBook(
829
+ assets=assets,
830
+ plutus_v2=True,
831
+ block_time=int(time.time()),
832
+ block_index=0,
833
+ sell_book_full=SellOrderBook(sell_orders),
834
+ buy_book_full=BuyOrderBook(buy_orders),
835
+ )
836
+
837
+ # Limit orders per transaction (following GeniusYield pattern)
838
+ ob.sell_book_full = ob.sell_book_full[:3]
839
+ ob.buy_book_full = ob.buy_book_full[:3]
840
+
841
+ return ob
842
+
843
+ @classmethod
844
+ def dex(cls) -> str:
845
+ """Official dex name."""
846
+ return "Djed"
847
+
848
+ @property
849
+ def pool_id(self) -> str:
850
+ """A unique identifier for the pool or ob."""
851
+ return "Djed"
852
+
853
+ @classmethod
854
+ def _is_djed_order(cls, order_data) -> bool:
855
+ """Check if order is a Djed order (not Shen)."""
856
+ try:
857
+ # Parse datum to check action type
858
+ datum = DjedOrderDatum.from_cbor(order_data.datum_cbor)
859
+ return isinstance(datum.action, (DjedMintAction, DjedBurnAction))
860
+ except Exception:
861
+ return False
862
+
863
+
864
+ # === SHEN ORDER BOOK ===
865
+
866
+
867
+ class ShenOrderBook(DjedShenOrderBookBase):
868
+ """Shen order book for Shen mint/burn operations."""
869
+
870
+ @classmethod
871
+ def get_book(
872
+ cls,
873
+ assets: Assets,
874
+ orders: list[ShenOrderState] | None = None,
875
+ ) -> "ShenOrderBook":
876
+ """Create Shen order book."""
877
+ if orders is None:
878
+ selector = ShenOrderState.pool_selector()
879
+
880
+ result = get_backend().get_pool_utxos(
881
+ limit=1000,
882
+ historical=False,
883
+ **selector.model_dump(),
884
+ )
885
+
886
+ # Filter for Shen orders only
887
+ orders = [
888
+ ShenOrderState.model_validate(r.model_dump())
889
+ for r in result
890
+ if cls._is_shen_order(r)
891
+ ]
892
+
893
+ # Sort into buy (mint) and sell (burn) orders
894
+ buy_orders = [] # Shen mint orders
895
+ sell_orders = [] # Shen burn orders
896
+
897
+ for order in orders:
898
+ if order.inactive:
899
+ continue
900
+
901
+ price = order.price[0] / order.price[1]
902
+ o = OrderBookOrder(
903
+ price=price,
904
+ quantity=int(order.available.quantity()),
905
+ state=order,
906
+ )
907
+
908
+ if isinstance(order.order_datum.action, ShenMintAction):
909
+ buy_orders.append(o) # Mint = Buy
910
+ else: # ShenBurnAction
911
+ sell_orders.append(o) # Burn = Sell
912
+
913
+ ob = ShenOrderBook(
914
+ assets=assets,
915
+ plutus_v2=True,
916
+ block_time=int(time.time()),
917
+ block_index=0,
918
+ sell_book_full=SellOrderBook(sell_orders),
919
+ buy_book_full=BuyOrderBook(buy_orders),
920
+ )
921
+
922
+ # Limit orders per transaction (following GeniusYield pattern)
923
+ ob.sell_book_full = ob.sell_book_full[:3]
924
+ ob.buy_book_full = ob.buy_book_full[:3]
925
+
926
+ return ob
927
+
928
+ @classmethod
929
+ def dex(cls) -> str:
930
+ """Official dex name."""
931
+ return "Shen"
932
+
933
+ @property
934
+ def pool_id(self) -> str:
935
+ """A unique identifier for the pool or ob."""
936
+ return "Shen"
937
+
938
+ @classmethod
939
+ def _is_shen_order(cls, order_data) -> bool:
940
+ """Check if order is a Shen order (not Djed)."""
941
+ try:
942
+ # Parse datum to check action type
943
+ datum = DjedOrderDatum.from_cbor(order_data.datum_cbor)
944
+ return isinstance(datum.action, (ShenMintAction, ShenBurnAction))
945
+ except Exception:
946
+ return False