charli3_dendrite 1.5.0.dev1__tar.gz → 1.5.1.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.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/PKG-INFO +1 -1
  2. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/pyproject.toml +2 -2
  3. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/ob/saturnswap.py +407 -8
  4. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/LICENSE +0 -0
  5. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/README.md +0 -0
  6. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/__init__.py +0 -0
  7. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/backend/__init__.py +0 -0
  8. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/backend/backend_base.py +0 -0
  9. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/backend/blockfrost/__init__.py +0 -0
  10. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/backend/blockfrost/models.py +0 -0
  11. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/backend/dbsync/__init__.py +0 -0
  12. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/backend/dbsync/models.py +0 -0
  13. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/backend/ogmios_kupo/__init__.py +0 -0
  14. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/backend/ogmios_kupo/models.py +0 -0
  15. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/backend/utils.py +0 -0
  16. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dataclasses/__init__.py +0 -0
  17. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dataclasses/datums.py +0 -0
  18. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dataclasses/models.py +0 -0
  19. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/__init__.py +0 -0
  20. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/__init__.py +0 -0
  21. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/amm_base.py +0 -0
  22. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/amm_types.py +0 -0
  23. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/cswap.py +0 -0
  24. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/dano.py +0 -0
  25. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/minswap.py +0 -0
  26. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/muesli.py +0 -0
  27. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/spectrum.py +0 -0
  28. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/splash.py +0 -0
  29. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/sundae.py +0 -0
  30. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/vyfi.py +0 -0
  31. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/amm/wingriders.py +0 -0
  32. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/core/__init__.py +0 -0
  33. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/core/base.py +0 -0
  34. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/core/errors.py +0 -0
  35. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/ob/__init__.py +0 -0
  36. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/ob/axo.py +0 -0
  37. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/ob/cardanoswaps.py +0 -0
  38. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/ob/chadswap.py +0 -0
  39. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/ob/djed.py +0 -0
  40. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/ob/geniusyield.py +0 -0
  41. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/dexs/ob/ob_base.py +0 -0
  42. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/__init__.py +0 -0
  43. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/base.py +0 -0
  44. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/__init__.py +0 -0
  45. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/constants.py +0 -0
  46. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/datums.py +0 -0
  47. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/loader.py +0 -0
  48. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/market.py +0 -0
  49. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/math.py +0 -0
  50. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/__init__.py +0 -0
  51. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/aggregator.py +0 -0
  52. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/aggregator_datums.py +0 -0
  53. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/deployments.py +0 -0
  54. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/forward.py +0 -0
  55. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/leaves.py +0 -0
  56. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/locator.py +0 -0
  57. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/lp_pricing.py +0 -0
  58. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/recipes.json +0 -0
  59. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/redeemer.py +0 -0
  60. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/reproduce.py +0 -0
  61. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/oracles/structured_datums.py +0 -0
  62. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/state.py +0 -0
  63. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/__init__.py +0 -0
  64. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/_common.py +0 -0
  65. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/build.py +0 -0
  66. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/builder.py +0 -0
  67. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/context.py +0 -0
  68. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/create_loan.py +0 -0
  69. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/datum_synth.py +0 -0
  70. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/increase_loan.py +0 -0
  71. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/loan_ids.py +0 -0
  72. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/modify_collateral.py +0 -0
  73. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/oracle_redeemer.py +0 -0
  74. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/oracle_synth.py +0 -0
  75. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/redeemers.py +0 -0
  76. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/repay.py +0 -0
  77. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/topup_withdraw.py +0 -0
  78. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/danogo/transactions/verify.py +0 -0
  79. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/math.py +0 -0
  80. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/oracles/__init__.py +0 -0
  81. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/oracles/charli3.py +0 -0
  82. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/oracles/dex_pooled.py +0 -0
  83. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/oracles/fluid.py +0 -0
  84. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/oracles/models.py +0 -0
  85. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/oracles/orcfax.py +0 -0
  86. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/oracles/qtoken.py +0 -0
  87. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/registry.py +0 -0
  88. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/transactions/__init__.py +0 -0
  89. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/transactions/base.py +0 -0
  90. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/transactions/infra.py +0 -0
  91. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/src/charli3_dendrite/lending/transactions/snapshot.py +0 -0
  92. {charli3_dendrite-1.5.0.dev1 → charli3_dendrite-1.5.1.dev0}/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.0.dev1
3
+ Version: 1.5.1.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.5.0-dev1"
3
+ version = "1.5.1-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.5.0-dev1"
53
+ current_version = "1.5.1-dev0"
54
54
  parse = """(?x)
55
55
  (?P<major>\\d+)\\.
56
56
  (?P<minor>\\d+)\\.
@@ -17,6 +17,7 @@ from pycardano import PaymentSigningKey
17
17
  from pycardano import PaymentVerificationKey
18
18
  from pycardano import PlutusData
19
19
  from pycardano import PlutusV2Script
20
+ from pycardano import PlutusV3Script
20
21
  from pycardano import Redeemer
21
22
  from pycardano import TransactionBuilder
22
23
  from pycardano import TransactionId
@@ -63,6 +64,12 @@ SATURNSWAP_LEGACY_ORDER_ADDRESS = (
63
64
  "addr1zyd0sj57d9lpu7cy9g9qdurpazqc9l4eaxk6j59nd2gkh4"
64
65
  "275jq4yvpskgayj55xegdp30g5rfynax66r8vgn9fldndsqzf5tn"
65
66
  )
67
+ # V3 (PlutusV3) contract; script hash
68
+ # 6023f59dce0064f1d6d27594dbea25bc4305a9f6a10f3a064037553a.
69
+ SATURNSWAP_V3_ORDER_ADDRESS = (
70
+ "addr1z9sz8avaecqxfuwk6f6efkl2yk7yxpdf76ss7wsxgqm42w"
71
+ "h2l9cdyhc0eja9mxq0lgeer90edhlfymnxv2ym3szcetqsp0ume8"
72
+ )
66
73
 
67
74
  # When the protocol shares its authorize hot-key, set this env var to the signing
68
75
  # key and dendrite builds the fee-free authorized fill on either contract.
@@ -199,6 +206,199 @@ class SaturnSwapSwapDatum(OrderDatum):
199
206
  return OrderType.swap
200
207
 
201
208
 
209
+ @dataclass
210
+ class SaturnSwapOutputReferenceV3(PlutusData):
211
+ """Flat OutputReference: Constr0[tx_id: bytes(32), index]. No TxId wrapper."""
212
+
213
+ CONSTR_ID = 0
214
+ tx_id: bytes
215
+ index: int
216
+
217
+
218
+ @dataclass
219
+ class SaturnSwapPaymentDatumV3(PlutusData):
220
+ """PaymentDatum { output_reference } with the flat V3 OutputReference."""
221
+
222
+ CONSTR_ID = 0
223
+ output_reference: SaturnSwapOutputReferenceV3
224
+
225
+
226
+ @dataclass
227
+ class SaturnSwapCoverage(PlutusData):
228
+ """Aegis coverage { vault, premium_bps, policy_ref }."""
229
+
230
+ CONSTR_ID = 0
231
+ vault: PlutusFullAddress
232
+ premium_bps: int
233
+ policy_ref: SaturnSwapOutputReferenceV3
234
+
235
+
236
+ @dataclass
237
+ class SaturnSwapSomeCoverage(PlutusData):
238
+ """Some(Coverage) wrapper for Option<Coverage>."""
239
+
240
+ CONSTR_ID = 0
241
+ value: SaturnSwapCoverage
242
+
243
+
244
+ @dataclass
245
+ class SaturnSwapSwapDatumV3(OrderDatum):
246
+ """V3 SwapDatum (11 fields).
247
+
248
+ Extends the V2 layout with ``min_partial_fill`` and optional Aegis
249
+ ``coverage``, and uses the flat :class:`SaturnSwapOutputReferenceV3`.
250
+ """
251
+
252
+ CONSTR_ID = 0
253
+ owner: PlutusFullAddress
254
+ policy_id_sell: bytes
255
+ asset_name_sell: bytes
256
+ amount_sell: int
257
+ policy_id_buy: bytes
258
+ asset_name_buy: bytes
259
+ amount_buy: int
260
+ valid_before_time: Union[PlutusNone, SaturnSwapSomeInt]
261
+ output_reference: SaturnSwapOutputReferenceV3
262
+ min_partial_fill: int
263
+ coverage: Union[SaturnSwapSomeCoverage, PlutusNone]
264
+
265
+ def pool_pair(self) -> Assets | None:
266
+ """Return the asset pair for this swap datum."""
267
+ sell_unit = (
268
+ "lovelace"
269
+ if self.policy_id_sell == b""
270
+ else self.policy_id_sell.hex() + self.asset_name_sell.hex()
271
+ )
272
+ buy_unit = (
273
+ "lovelace"
274
+ if self.policy_id_buy == b""
275
+ else self.policy_id_buy.hex() + self.asset_name_buy.hex()
276
+ )
277
+ return Assets(**{sell_unit: 0}) + Assets(**{buy_unit: 0})
278
+
279
+ def address_source(self) -> str | None:
280
+ """Return the maker address as a bech32 string."""
281
+ return self.owner.to_address().encode()
282
+
283
+ def requested_amount(self) -> Assets:
284
+ """Return the requested buy asset amount."""
285
+ buy_unit = (
286
+ "lovelace"
287
+ if self.policy_id_buy == b""
288
+ else self.policy_id_buy.hex() + self.asset_name_buy.hex()
289
+ )
290
+ return Assets(**{buy_unit: self.amount_buy})
291
+
292
+ def order_type(self) -> OrderType | None:
293
+ """Return the order type classification."""
294
+ return OrderType.swap
295
+
296
+ def is_covered(self) -> bool:
297
+ """Return whether the order carries Aegis coverage."""
298
+ return isinstance(self.coverage, SaturnSwapSomeCoverage)
299
+
300
+ def premium_bps(self) -> int | None:
301
+ """Coverage premium in basis points, or None when uncovered."""
302
+ return self.coverage.value.premium_bps if self.is_covered() else None
303
+
304
+ def coverage_vault(self) -> str | None:
305
+ """Aegis vault bech32 address, or None when uncovered."""
306
+ if not self.is_covered():
307
+ return None
308
+ return self.coverage.value.vault.to_address().encode()
309
+
310
+ def premium_for_fill(self, user_sell_amount: int) -> int:
311
+ """Out-of-pocket premium (buy asset) for a fill of ``user_sell_amount``.
312
+
313
+ ``max(1, user_sell_amount * premium_bps // 10000)`` for covered orders;
314
+ ``0`` when uncovered.
315
+ """
316
+ if not self.is_covered():
317
+ return 0
318
+ base = (user_sell_amount * self.coverage.value.premium_bps) // 10_000
319
+ return max(1, base)
320
+
321
+ def check_min_partial_fill(self, user_sell_amount: int) -> None:
322
+ """Reject a partial fill below the min_partial_fill floor.
323
+
324
+ A partial fill (``user_sell_amount < amount_buy``) must deliver at least
325
+ ``min_partial_fill`` of the buy asset; a full fill is always allowed.
326
+ """
327
+ is_partial = user_sell_amount < self.amount_buy
328
+ if is_partial and user_sell_amount < self.min_partial_fill:
329
+ msg = (
330
+ f"partial fill {user_sell_amount} below min_partial_fill "
331
+ f"{self.min_partial_fill}"
332
+ )
333
+ raise ValueError(msg)
334
+
335
+ def build_relist_datum(
336
+ self,
337
+ spent_tx_hash: str,
338
+ spent_index: int,
339
+ user_sell_amount: int,
340
+ ) -> "SaturnSwapSwapDatumV3":
341
+ """Continuation (relist) datum for a partial fill of ``user_sell_amount``.
342
+
343
+ Owner, pair, ``valid_before_time``, ``min_partial_fill`` and ``coverage``
344
+ carry forward unchanged; the flat ``output_reference`` points to the spent
345
+ order. Mirrors the residual-amount math in
346
+ :meth:`SaturnSwapV3OrderState.swap_utxo` (including the ADA-sell carve).
347
+ """
348
+ two_ada = 2_000_000
349
+ new_amount_buy = self.amount_buy - user_sell_amount
350
+ new_amount_sell = _ratio_amount(
351
+ self.amount_buy,
352
+ new_amount_buy,
353
+ self.amount_sell,
354
+ )
355
+ if self.policy_id_sell == b"" and new_amount_sell > two_ada:
356
+ new_amount_sell -= two_ada
357
+ new_amount_buy = _ratio_amount(
358
+ self.amount_sell,
359
+ new_amount_sell,
360
+ self.amount_buy,
361
+ )
362
+ return SaturnSwapSwapDatumV3(
363
+ owner=self.owner,
364
+ policy_id_sell=self.policy_id_sell,
365
+ asset_name_sell=self.asset_name_sell,
366
+ amount_sell=new_amount_sell,
367
+ policy_id_buy=self.policy_id_buy,
368
+ asset_name_buy=self.asset_name_buy,
369
+ amount_buy=new_amount_buy,
370
+ valid_before_time=self.valid_before_time,
371
+ output_reference=SaturnSwapOutputReferenceV3(
372
+ tx_id=bytes.fromhex(spent_tx_hash),
373
+ index=spent_index,
374
+ ),
375
+ min_partial_fill=self.min_partial_fill,
376
+ coverage=self.coverage,
377
+ )
378
+
379
+ def premium_payment(
380
+ self,
381
+ user_sell_amount: int,
382
+ ) -> tuple[str, Assets] | None:
383
+ """Aegis premium owed for a fill: ``(vault_bech32, buy-asset premium)``.
384
+
385
+ ``None`` for uncovered orders. The premium is out-of-pocket for the filler,
386
+ denominated in the BUY asset, and paid to the coverage vault as a real
387
+ output the validator requires on a covered fill.
388
+ """
389
+ if not self.is_covered():
390
+ return None
391
+ buy_unit = (
392
+ "lovelace"
393
+ if self.policy_id_buy == b""
394
+ else self.policy_id_buy.hex() + self.asset_name_buy.hex()
395
+ )
396
+ return (
397
+ self.coverage.value.vault.to_address().encode(),
398
+ Assets(**{buy_unit: self.premium_for_fill(user_sell_amount)}),
399
+ )
400
+
401
+
202
402
  @dataclass
203
403
  class SaturnSwapSwapAction(PlutusData):
204
404
  """SwapAction(user_sell_amount, input_index, output_index)."""
@@ -243,12 +443,20 @@ class SaturnSwapSwapAction(PlutusData):
243
443
  input_index = input_utxo.input.index
244
444
 
245
445
  for i, txo in enumerate(tx_builder.outputs):
246
- if not isinstance(txo.datum, SaturnSwapPaymentDatum):
446
+ datum = txo.datum
447
+ if isinstance(datum, SaturnSwapPaymentDatum):
448
+ # V2: nested OutputReference -> TxId -> bytes.
449
+ ref_tx_id = datum.output_reference.tx_id.value
450
+ ref_index = datum.output_reference.index
451
+ elif isinstance(datum, SaturnSwapPaymentDatumV3):
452
+ # V3: flat OutputReference (tx_id is bare bytes).
453
+ ref_tx_id = datum.output_reference.tx_id
454
+ ref_index = datum.output_reference.index
455
+ else:
247
456
  continue
248
- output_ref = txo.datum.output_reference
249
457
  if (
250
- output_ref.tx_id.value == bytes.fromhex(str(input_tx_id))
251
- and output_ref.index == input_index
458
+ ref_tx_id == bytes.fromhex(str(input_tx_id))
459
+ and ref_index == input_index
252
460
  and txo.address == owner_address
253
461
  ):
254
462
  self.output_index = i
@@ -311,8 +519,8 @@ class _SaturnSwapOrderStateBase(AbstractOrderState):
311
519
  return SaturnSwapSwapDatum
312
520
 
313
521
  @classmethod
314
- def default_script_class(cls) -> type[PlutusV2Script]:
315
- """Return default script type."""
522
+ def default_script_class(cls) -> type[PlutusV2Script] | type[PlutusV3Script]:
523
+ """Return default script type (V2 base; the V3 leaf overrides)."""
316
524
  return PlutusV2Script
317
525
 
318
526
  @property
@@ -464,7 +672,7 @@ class _SaturnSwapOrderStateBase(AbstractOrderState):
464
672
  tx_builder: TransactionBuilder,
465
673
  sell_unit: str,
466
674
  new_amount_sell: int,
467
- payment_datum: "SaturnSwapPaymentDatum",
675
+ payment_datum: "SaturnSwapPaymentDatum | SaturnSwapPaymentDatumV3",
468
676
  ) -> None:
469
677
  """Add the taker-fee output, or require the authorize hot-key signature.
470
678
 
@@ -699,9 +907,200 @@ class SaturnSwapLegacyOrderState(_SaturnSwapOrderStateBase):
699
907
  return [SATURNSWAP_LEGACY_ORDER_ADDRESS]
700
908
 
701
909
 
910
+ class SaturnSwapV3OrderState(_SaturnSwapOrderStateBase):
911
+ """V3 (PlutusV3) SaturnSwap order state — 1% taker-fee contract.
912
+
913
+ Orders resting at :data:`SATURNSWAP_V3_ORDER_ADDRESS` (script hash
914
+ ``6023f59d…``). Same 1% non-auth taker fee as the live V2 contract; the datum
915
+ is the 11-field :class:`SaturnSwapSwapDatumV3` (flat OutputReference +
916
+ ``min_partial_fill`` + optional coverage) and the script is PlutusV3.
917
+ """
918
+
919
+ TAKER_FEE_BPS: ClassVar[int] = SATURNSWAP_TAKER_FEE_BPS
920
+
921
+ @classmethod
922
+ def dex(cls) -> str:
923
+ """Return the DEX name."""
924
+ return "SaturnSwap"
925
+
926
+ @classmethod
927
+ def order_selector(cls) -> list[str]:
928
+ """Return order script addresses (V3 contract)."""
929
+ return [SATURNSWAP_V3_ORDER_ADDRESS]
930
+
931
+ @classmethod
932
+ def order_datum_class(cls) -> type[PlutusData]:
933
+ """Return the V3 order datum class."""
934
+ return SaturnSwapSwapDatumV3
935
+
936
+ @classmethod
937
+ def default_script_class(cls) -> type[PlutusV3Script]:
938
+ """V3 orders are spent via a PlutusV3 reference script."""
939
+ return PlutusV3Script
940
+
941
+ def swap_utxo(
942
+ self,
943
+ address_source: Address,
944
+ in_assets: Assets,
945
+ out_assets: Assets,
946
+ tx_builder: TransactionBuilder,
947
+ extra_assets: Assets | None = None,
948
+ address_target: Address | None = None,
949
+ datum_target: PlutusData | None = None,
950
+ ) -> tuple[TransactionOutput | None, PlutusData]:
951
+ """Build a V3 taker-fill for this order.
952
+
953
+ Uses the flat :class:`SaturnSwapPaymentDatumV3` and the V3 relist datum
954
+ (:meth:`SaturnSwapSwapDatumV3.build_relist_datum`), enforces the
955
+ ``min_partial_fill`` floor, and preserves the authorized fee-free path.
956
+ Covered orders require the out-of-pocket premium output and are handled
957
+ separately.
958
+
959
+ TODO(dry): the owner-output / fee / partial-residual scaffold mirrors the
960
+ V2 base ``swap_utxo``; fold the shared body into an order-book mixin so V2
961
+ and V3 stop duplicating it.
962
+ """
963
+ two_ada = 2_000_000
964
+ user_sell_amount = int(in_assets.quantity())
965
+
966
+ # Reject a partial below the on-chain min_partial_fill floor.
967
+ self.order_datum.check_min_partial_fill(user_sell_amount)
968
+
969
+ if self.reference_utxo is not None:
970
+ tx_builder.reference_inputs.add(self.reference_utxo)
971
+
972
+ order_address = (
973
+ get_backend()
974
+ .get_pool_in_tx(
975
+ self.tx_hash,
976
+ addresses=self.pool_selector().addresses,
977
+ )[0]
978
+ .address
979
+ )
980
+ input_utxo = UTxO(
981
+ TransactionInput(
982
+ transaction_id=TransactionId(bytes.fromhex(self.tx_hash)),
983
+ index=self.tx_index,
984
+ ),
985
+ output=TransactionOutput(
986
+ address=order_address,
987
+ amount=asset_to_value(self.assets),
988
+ datum_hash=self.order_datum.hash(),
989
+ ),
990
+ )
991
+ script_input_lovelace = input_utxo.output.amount.coin
992
+
993
+ # Flat V3 payment datum stamped on every output (owner/fee/relist).
994
+ output_ref = SaturnSwapOutputReferenceV3(
995
+ tx_id=bytes.fromhex(self.tx_hash),
996
+ index=self.tx_index,
997
+ )
998
+ payment_datum = SaturnSwapPaymentDatumV3(output_reference=output_ref)
999
+
1000
+ partial = user_sell_amount < self.order_datum.amount_buy
1001
+ sell_unit = out_assets.unit()
1002
+ buy_unit = in_assets.unit()
1003
+ sell_is_ada = sell_unit == "lovelace"
1004
+
1005
+ owner_address = self.order_datum.owner.to_address()
1006
+ owner_assets = Assets(**{buy_unit: user_sell_amount})
1007
+ new_amount_sell = _ratio_amount(
1008
+ self.order_datum.amount_buy,
1009
+ user_sell_amount,
1010
+ self.order_datum.amount_sell,
1011
+ )
1012
+ if partial and sell_is_ada and new_amount_sell > two_ada:
1013
+ owner_assets.root["lovelace"] = (
1014
+ owner_assets.root.get("lovelace", 0) + two_ada
1015
+ )
1016
+ elif not partial and not sell_is_ada:
1017
+ owner_assets.root["lovelace"] = (
1018
+ owner_assets.root.get("lovelace", 0) + script_input_lovelace
1019
+ )
1020
+
1021
+ owner_output = TransactionOutput(
1022
+ address=owner_address,
1023
+ amount=asset_to_value(owner_assets),
1024
+ datum=payment_datum,
1025
+ )
1026
+ owner_output.amount.coin = max(
1027
+ owner_output.amount.coin,
1028
+ min_lovelace(tx_builder.context, output=owner_output),
1029
+ )
1030
+ tx_builder.add_output(owner_output)
1031
+
1032
+ self._add_fee_or_authorize(
1033
+ tx_builder,
1034
+ sell_unit,
1035
+ new_amount_sell,
1036
+ payment_datum,
1037
+ )
1038
+
1039
+ # Covered orders owe an out-of-pocket premium output (buy asset) to the
1040
+ # Aegis vault; the validator requires it and enforces vault distinctness.
1041
+ premium = self.order_datum.premium_payment(user_sell_amount)
1042
+ if premium is not None:
1043
+ vault_bech32, premium_assets = premium
1044
+ vault_address = Address.decode(vault_bech32)
1045
+ if vault_address.payment_part == owner_address.payment_part:
1046
+ msg = "coverage vault must differ from the order owner"
1047
+ raise ValueError(msg)
1048
+ premium_output = TransactionOutput(
1049
+ address=vault_address,
1050
+ amount=asset_to_value(premium_assets),
1051
+ datum=payment_datum,
1052
+ )
1053
+ premium_output.amount.coin = max(
1054
+ premium_output.amount.coin,
1055
+ min_lovelace(tx_builder.context, output=premium_output),
1056
+ )
1057
+ tx_builder.add_output(premium_output)
1058
+
1059
+ action = SaturnSwapSwapAction(
1060
+ user_sell_amount=user_sell_amount,
1061
+ input_index=0,
1062
+ output_index=0,
1063
+ )
1064
+ redeemer = Redeemer(action)
1065
+ tx_builder.add_script_input(
1066
+ utxo=input_utxo,
1067
+ script=self.reference_utxo,
1068
+ redeemer=redeemer,
1069
+ )
1070
+ tx_builder.datums.update({self.order_datum.hash(): self.order_datum})
1071
+
1072
+ if partial:
1073
+ new_datum = self.order_datum.build_relist_datum(
1074
+ self.tx_hash,
1075
+ self.tx_index,
1076
+ user_sell_amount,
1077
+ )
1078
+ residual_assets = Assets(**{sell_unit: new_datum.amount_sell})
1079
+ residual_output = TransactionOutput(
1080
+ address=order_address,
1081
+ amount=asset_to_value(residual_assets),
1082
+ datum=new_datum,
1083
+ )
1084
+ if sell_is_ada:
1085
+ residual_output.amount.coin = max(
1086
+ residual_output.amount.coin,
1087
+ min_lovelace(tx_builder.context, output=residual_output),
1088
+ )
1089
+ else:
1090
+ residual_output.amount.coin = max(
1091
+ script_input_lovelace,
1092
+ min_lovelace(tx_builder.context, output=residual_output),
1093
+ )
1094
+ tx_builder.datums.update({new_datum.hash(): new_datum})
1095
+ return residual_output, new_datum
1096
+
1097
+ return None, self.order_datum
1098
+
1099
+
702
1100
  # Concrete SaturnSwap contracts, newest first. The order book walks all of these
703
- # so both the live 1% contract and the legacy 4% contract are aggregated.
1101
+ # so the V3, live 1%, and legacy 4% contracts are aggregated.
704
1102
  _SATURNSWAP_ORDER_STATE_CLASSES: list[type[_SaturnSwapOrderStateBase]] = [
1103
+ SaturnSwapV3OrderState,
705
1104
  SaturnSwapOrderState,
706
1105
  SaturnSwapLegacyOrderState,
707
1106
  ]