charli3_dendrite 1.1.1.dev3__tar.gz → 1.1.1.dev4__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 (36) hide show
  1. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/PKG-INFO +1 -2
  2. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/pyproject.toml +1 -2
  3. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/backend/ogmios_kupo/__init__.py +23 -14
  4. charli3_dendrite-1.1.1.dev4/src/charli3_dendrite/dexs/amm/vyfi.py +415 -0
  5. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/ob/axo.py +131 -63
  6. charli3_dendrite-1.1.1.dev3/src/charli3_dendrite/dexs/amm/vyfi.py +0 -343
  7. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/LICENSE +0 -0
  8. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/README.md +0 -0
  9. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/__init__.py +0 -0
  10. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/backend/__init__.py +0 -0
  11. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/backend/backend_base.py +0 -0
  12. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/backend/blockfrost/__init__.py +0 -0
  13. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/backend/blockfrost/models.py +0 -0
  14. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/backend/dbsync/__init__.py +0 -0
  15. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/backend/dbsync/models.py +0 -0
  16. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/backend/ogmios_kupo/models.py +0 -0
  17. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/backend/utils.py +0 -0
  18. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dataclasses/__init__.py +0 -0
  19. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dataclasses/datums.py +0 -0
  20. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dataclasses/models.py +0 -0
  21. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/__init__.py +0 -0
  22. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/amm/__init__.py +0 -0
  23. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/amm/amm_base.py +0 -0
  24. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/amm/amm_types.py +0 -0
  25. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/amm/minswap.py +0 -0
  26. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/amm/muesli.py +0 -0
  27. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/amm/spectrum.py +0 -0
  28. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/amm/sundae.py +0 -0
  29. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/amm/wingriders.py +0 -0
  30. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/core/__init__.py +0 -0
  31. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/core/base.py +0 -0
  32. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/core/errors.py +0 -0
  33. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/ob/__init__.py +0 -0
  34. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/ob/geniusyield.py +0 -0
  35. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/dexs/ob/ob_base.py +0 -0
  36. {charli3_dendrite-1.1.1.dev3 → charli3_dendrite-1.1.1.dev4}/src/charli3_dendrite/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: charli3_dendrite
3
- Version: 1.1.1.dev3
3
+ Version: 1.1.1.dev4
4
4
  Summary:
5
5
  Author: Elder Millenial
6
6
  Author-email: eldermillenial@protonmail.com
@@ -10,7 +10,6 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Requires-Dist: blockfrost-python (>=0.6.0,<0.7.0)
13
- Requires-Dist: ogmios (==1.2.1)
14
13
  Requires-Dist: psycopg[binary,pool] (>=3.1.13,<4.0.0)
15
14
  Requires-Dist: pycardano (==0.11.1)
16
15
  Requires-Dist: pydantic (>=2.5.2,<3.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "charli3_dendrite"
3
- version = "1.1.1-dev3"
3
+ version = "1.1.1-dev4"
4
4
  description = ""
5
5
  authors = ["Elder Millenial <eldermillenial@protonmail.com>"]
6
6
  readme = "README.md"
@@ -14,7 +14,6 @@ psycopg = { extras = ["binary", "pool"], version = "^3.1.13" }
14
14
  python-dotenv = "0.21.1"
15
15
  pycardano = "0.11.1"
16
16
  blockfrost-python = "^0.6.0"
17
- ogmios = "1.2.1"
18
17
 
19
18
 
20
19
  [tool.poetry.group.dev.dependencies]
@@ -6,8 +6,8 @@ from typing import Optional
6
6
  from typing import Union
7
7
 
8
8
  import requests
9
- from ogmios import OgmiosChainContext # type: ignore
10
9
  from pycardano import Address # type: ignore
10
+ from pycardano import KupoOgmiosV6ChainContext
11
11
  from pycardano import Network # type: ignore
12
12
 
13
13
  from charli3_dendrite.backend.backend_base import AbstractBackend
@@ -52,12 +52,14 @@ class OgmiosKupoBackend(AbstractBackend):
52
52
  """
53
53
  _, ws_string = ogmios_url.split("ws://")
54
54
  self.ws_url, self.port = ws_string.split(":")
55
- self.ogmios_context = OgmiosChainContext(
55
+ self.ogmios_context = KupoOgmiosV6ChainContext(
56
56
  host=self.ws_url,
57
57
  port=int(self.port),
58
+ secure=False,
59
+ refetch_chain_tip_interval=None,
58
60
  network=network,
61
+ kupo_url=kupo_url,
59
62
  )
60
- self.kupo_url = kupo_url
61
63
 
62
64
  def _kupo_request(
63
65
  self,
@@ -76,7 +78,7 @@ class OgmiosKupoBackend(AbstractBackend):
76
78
  Raises:
77
79
  requests.exceptions.RequestException: If the request fails.
78
80
  """
79
- url = f"{self.kupo_url}/{endpoint}"
81
+ url = f"{self.ogmios_context._kupo_url}/{endpoint}"
80
82
  response = requests.get(url, params=params, timeout=10)
81
83
  response.raise_for_status()
82
84
  return KupoGenericResponse.model_validate(response.json())
@@ -122,8 +124,8 @@ class OgmiosKupoBackend(AbstractBackend):
122
124
  """Get pool UTXOs based on assets and addresses.
123
125
 
124
126
  Args:
127
+ addresses (list[str]): List of addresses to query.
125
128
  assets (Optional[list[str]]): List of asset IDs to filter by.
126
- addresses (Optional[list[str]]): List of addresses to query.
127
129
  limit (int): Maximum number of UTXOs to return.
128
130
  page (int): Page number for pagination.
129
131
  historical (bool): Whether to include historical data.
@@ -132,7 +134,7 @@ class OgmiosKupoBackend(AbstractBackend):
132
134
  PoolStateList: List of pool states.
133
135
  """
134
136
  pool_states = []
135
- if addresses is None:
137
+ if not addresses:
136
138
  return PoolStateList(root=[])
137
139
 
138
140
  for address in addresses:
@@ -140,15 +142,20 @@ class OgmiosKupoBackend(AbstractBackend):
140
142
  "limit": limit,
141
143
  "offset": page * limit,
142
144
  }
145
+ payment_cred = self.get_payment_credential(address)
143
146
  if assets:
144
- params["policy_id"] = assets[0][:POLICY_ID_LENGTH]
147
+ last_asset = assets[-1]
148
+ params["policy_id"] = last_asset[:POLICY_ID_LENGTH]
145
149
  params["asset_name"] = (
146
- assets[0][POLICY_ID_LENGTH:]
147
- if len(assets[0]) > POLICY_ID_LENGTH
150
+ last_asset[POLICY_ID_LENGTH:]
151
+ if len(last_asset) > POLICY_ID_LENGTH
148
152
  else None
149
153
  )
150
154
 
151
- matches = self._kupo_request(f"matches/{address}?unspent", params=params)
155
+ matches = self._kupo_request(
156
+ f"matches/{payment_cred}/*?unspent",
157
+ params=params,
158
+ )
152
159
  if isinstance(matches.root, list):
153
160
  for match in matches.root:
154
161
  pool_state = self._pool_state_from_kupo(match)
@@ -181,15 +188,17 @@ class OgmiosKupoBackend(AbstractBackend):
181
188
  "transaction_id": tx_hash,
182
189
  "order": "most_recent_first",
183
190
  }
191
+ payment_cred = self.get_payment_credential(address)
184
192
  if assets:
185
- params["policy_id"] = assets[0][:POLICY_ID_LENGTH]
193
+ last_asset = assets[-1]
194
+ params["policy_id"] = last_asset[:POLICY_ID_LENGTH]
186
195
  params["asset_name"] = (
187
- assets[0][POLICY_ID_LENGTH:]
188
- if len(assets[0]) > POLICY_ID_LENGTH
196
+ last_asset[POLICY_ID_LENGTH:]
197
+ if len(last_asset) > POLICY_ID_LENGTH
189
198
  else None
190
199
  )
191
200
 
192
- matches = self._kupo_request(f"matches/{address}", params=params)
201
+ matches = self._kupo_request(f"matches/{payment_cred}/*", params=params)
193
202
  if isinstance(matches.root, list):
194
203
  pool_states = []
195
204
  if matches.root:
@@ -0,0 +1,415 @@
1
+ """VyFi DEX Module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from collections import defaultdict
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+ from typing import ClassVar
11
+ from typing import Optional
12
+ from typing import Union
13
+
14
+ import requests
15
+ from pycardano import Address
16
+ from pycardano import PlutusData
17
+ from pycardano import VerificationKeyHash
18
+ from pydantic import BaseModel
19
+ from pydantic import Field
20
+
21
+ from charli3_dendrite.dataclasses.datums import OrderDatum
22
+ from charli3_dendrite.dataclasses.datums import PoolDatum
23
+ from charli3_dendrite.dataclasses.models import OrderType
24
+ from charli3_dendrite.dataclasses.models import PoolSelector
25
+ from charli3_dendrite.dexs.amm.amm_types import AbstractConstantProductPoolState
26
+ from charli3_dendrite.dexs.core.errors import NoAssetsError
27
+ from charli3_dendrite.dexs.core.errors import NotAPoolError
28
+ from charli3_dendrite.utility import Assets
29
+
30
+ POOL_REFRESH_INTERVAL = 3600
31
+ ADDRESS_HASH_LENGTH = 28
32
+ POLICY_ID_LENGTH = 56
33
+
34
+
35
+ @dataclass
36
+ class VyFiPoolDatum(PoolDatum):
37
+ """VyFi pool datum."""
38
+
39
+ token_a_fees: int
40
+ token_b_fees: int
41
+ lp_tokens: int
42
+
43
+ def pool_pair(self) -> Optional[Assets]:
44
+ """Return the pool pair assets."""
45
+ return None
46
+
47
+
48
+ @dataclass
49
+ class Deposit(PlutusData):
50
+ """Deposit assets into the pool."""
51
+
52
+ CONSTR_ID = 0
53
+ min_lp_receive: int
54
+
55
+
56
+ @dataclass
57
+ class WithdrawPair(PlutusData):
58
+ """Withdraw pair of assets."""
59
+
60
+ CONSTR_ID = 0
61
+ min_amount_a: int
62
+ min_amount_b: int
63
+
64
+
65
+ @dataclass
66
+ class Withdraw(PlutusData):
67
+ """Withdraw assets from the pool."""
68
+
69
+ CONSTR_ID = 1
70
+ min_lp_receive: WithdrawPair
71
+
72
+
73
+ @dataclass
74
+ class LPFlushA(PlutusData):
75
+ """Flush LP tokens from A."""
76
+
77
+ CONSTR_ID = 2
78
+
79
+
80
+ @dataclass
81
+ class AtoB(PlutusData):
82
+ """A to B swap direction."""
83
+
84
+ CONSTR_ID = 3
85
+ min_receive: int
86
+
87
+
88
+ @dataclass
89
+ class BtoA(PlutusData):
90
+ """B to A swap direction."""
91
+
92
+ CONSTR_ID = 4
93
+ min_receive: int
94
+
95
+
96
+ @dataclass
97
+ class ZapInA(PlutusData):
98
+ """Zap in A."""
99
+
100
+ CONSTR_ID = 5
101
+ min_lp_receive: int
102
+
103
+
104
+ @dataclass
105
+ class ZapInB(PlutusData):
106
+ """Zap in B."""
107
+
108
+ CONSTR_ID = 6
109
+ min_lp_receive: int
110
+
111
+
112
+ @dataclass
113
+ class VyFiOrderDatum(OrderDatum):
114
+ """VyFi order datum."""
115
+
116
+ address: bytes
117
+ order: Union[AtoB, BtoA, Deposit, LPFlushA, Withdraw, ZapInA, ZapInB]
118
+
119
+ @classmethod
120
+ def create_datum(
121
+ cls,
122
+ address_source: Address,
123
+ in_assets: Assets,
124
+ out_assets: Assets,
125
+ batcher_fee: Assets, # noqa: ARG003
126
+ deposit: Assets, # noqa: ARG003
127
+ address_target: Optional[Address] = None, # noqa: ARG003
128
+ datum_target: Optional[PlutusData] = None, # noqa: ARG003
129
+ ) -> VyFiOrderDatum:
130
+ """Create a new order datum."""
131
+ address_hash = (
132
+ address_source.payment_part.to_primitive()
133
+ + address_source.staking_part.to_primitive()
134
+ )
135
+
136
+ merged = in_assets + out_assets
137
+ if in_assets.unit() == merged.unit():
138
+ order = AtoB(min_receive=out_assets.quantity())
139
+ else:
140
+ order = BtoA(min_receive=out_assets.quantity())
141
+
142
+ return cls(address=address_hash, order=order)
143
+
144
+ def address_source(self) -> Address:
145
+ """Get the source address."""
146
+ payment_part = VerificationKeyHash.from_primitive(
147
+ self.address[:ADDRESS_HASH_LENGTH],
148
+ )
149
+ staking_part = (
150
+ VerificationKeyHash.from_primitive(self.address[ADDRESS_HASH_LENGTH:])
151
+ if len(self.address) > ADDRESS_HASH_LENGTH
152
+ else None
153
+ )
154
+ return Address(payment_part=payment_part, staking_part=staking_part)
155
+
156
+ def requested_amount(self) -> Assets:
157
+ """Get the requested amount."""
158
+ if isinstance(self.order, BtoA):
159
+ return Assets({"asset_a": self.order.min_receive})
160
+ if isinstance(self.order, AtoB):
161
+ return Assets({"asset_b": self.order.min_receive})
162
+ if isinstance(self.order, (ZapInA, ZapInB, Deposit)):
163
+ return Assets({"lp": self.order.min_lp_receive})
164
+ if isinstance(self.order, Withdraw):
165
+ return Assets(
166
+ {
167
+ "asset_a": self.order.min_lp_receive.min_amount_a,
168
+ "asset_b": self.order.min_lp_receive.min_amount_b,
169
+ },
170
+ )
171
+ return Assets()
172
+
173
+ def order_type(self) -> Optional[OrderType]:
174
+ """Get the order type."""
175
+ if isinstance(self.order, (BtoA, AtoB, ZapInA, ZapInB)):
176
+ return OrderType.swap
177
+ if isinstance(self.order, Deposit):
178
+ return OrderType.deposit
179
+ if isinstance(self.order, Withdraw):
180
+ return OrderType.withdraw
181
+ return None
182
+
183
+
184
+ class VyFiTokenDefinition(BaseModel):
185
+ """VyFi token definition."""
186
+
187
+ token_name: str = Field(alias="tokenName")
188
+ currency_symbol: str = Field(alias="currencySymbol")
189
+
190
+
191
+ class VyFiFees(BaseModel):
192
+ """VyFi fees."""
193
+
194
+ bar_fee: int = Field(alias="barFee")
195
+ process_fee: int = Field(alias="processFee")
196
+ liq_fee: int = Field(alias="liqFee")
197
+
198
+
199
+ class VyFiPoolTokens(BaseModel):
200
+ """VyFi pool tokens."""
201
+
202
+ a_asset: VyFiTokenDefinition = Field(alias="aAsset")
203
+ b_asset: VyFiTokenDefinition = Field(alias="bAsset")
204
+ main_nft: VyFiTokenDefinition = Field(alias="mainNFT")
205
+ operator_token: VyFiTokenDefinition = Field(alias="operatorToken")
206
+ lp_token_name: dict[str, str] = Field(alias="lpTokenName")
207
+ fees_settings: VyFiFees = Field(alias="feesSettings")
208
+ stake_key: Optional[str] = Field(alias="stakeKey")
209
+
210
+
211
+ class VyFiPoolDefinition(BaseModel):
212
+ """VyFi pool definition."""
213
+
214
+ units_pair: str = Field(alias="unitsPair")
215
+ pool_validator_utxo_address: str = Field(alias="poolValidatorUtxoAddress")
216
+ lp_policy_id_asset_id: str = Field(alias="lpPolicyId-assetId")
217
+ json_: VyFiPoolTokens = Field(alias="json")
218
+ pair: str
219
+ is_live: bool = Field(alias="isLive")
220
+ order_validator_utxo_address: str = Field(alias="orderValidatorUtxoAddress")
221
+
222
+ def __hash__(self) -> int:
223
+ """Make VyFiPoolDefinition hashable."""
224
+ return hash(
225
+ (
226
+ self.units_pair,
227
+ self.pool_validator_utxo_address,
228
+ self.order_validator_utxo_address,
229
+ ),
230
+ )
231
+
232
+
233
+ class VyFiCPPState(AbstractConstantProductPoolState):
234
+ """VyFi CPP state."""
235
+
236
+ _batcher = Assets(lovelace=1900000)
237
+ _deposit = Assets(lovelace=2000000)
238
+ _pools: ClassVar[Optional[dict[str, VyFiPoolDefinition]]] = None
239
+ _pools_refresh: ClassVar[float] = 0.0
240
+ lp_fee: int = 0
241
+ bar_fee: int = 0
242
+
243
+ @classmethod
244
+ def dex(cls) -> str:
245
+ """Get the DEX name."""
246
+ return "VyFi"
247
+
248
+ @classmethod
249
+ def pools(cls) -> dict[str, VyFiPoolDefinition]:
250
+ """Get the pools."""
251
+ if (
252
+ cls._pools is None
253
+ or (time.time() - cls._pools_refresh) > POOL_REFRESH_INTERVAL
254
+ ):
255
+ cls._refresh_pools()
256
+ return cls._pools or {}
257
+
258
+ @classmethod
259
+ def order_selector(cls) -> list[str]:
260
+ """Get order selector addresses."""
261
+ return [p.order_validator_utxo_address for p in cls.pools().values()]
262
+
263
+ @classmethod
264
+ def pool_selector(cls, assets: Optional[list[str]] = None) -> PoolSelector:
265
+ """Get a PoolSelector for VyFi pools, optionally filtered by assets."""
266
+ asset_to_pool = cls._create_asset_to_pool_mapping()
267
+ relevant_pools = cls._filter_relevant_pools(asset_to_pool, assets)
268
+ addresses = [pool.pool_validator_utxo_address for pool in relevant_pools]
269
+ return PoolSelector(addresses=addresses)
270
+
271
+ @classmethod
272
+ def _create_asset_to_pool_mapping(
273
+ cls,
274
+ ) -> defaultdict[str, list[VyFiPoolDefinition]]:
275
+ """Create a mapping of assets to pools."""
276
+ asset_to_pool: defaultdict[str, list[VyFiPoolDefinition]] = defaultdict(list)
277
+ for pool in cls.pools().values():
278
+ asset_a = cls._encode_asset(
279
+ pool.json_.a_asset.currency_symbol,
280
+ pool.json_.a_asset.token_name,
281
+ )
282
+ asset_b = cls._encode_asset(
283
+ pool.json_.b_asset.currency_symbol,
284
+ pool.json_.b_asset.token_name,
285
+ )
286
+ asset_to_pool[asset_a].append(pool)
287
+ asset_to_pool[asset_b].append(pool)
288
+ return asset_to_pool
289
+
290
+ @classmethod
291
+ def _filter_relevant_pools(
292
+ cls,
293
+ asset_to_pool: defaultdict[str, list[VyFiPoolDefinition]],
294
+ assets: Optional[list[str]],
295
+ ) -> set[VyFiPoolDefinition]:
296
+ """Filter relevant pools based on assets."""
297
+ if assets:
298
+ relevant_pools = set()
299
+ for asset in assets:
300
+ relevant_pools.update(asset_to_pool.get(asset, []))
301
+ else:
302
+ relevant_pools = set(cls.pools().values())
303
+ return relevant_pools
304
+
305
+ @staticmethod
306
+ def _encode_asset(policy_id: str, asset_name: str) -> str:
307
+ """Encode an asset by combining policy ID and hex-encoded asset name."""
308
+ encoded_name = asset_name.encode("utf-8").hex()
309
+ return policy_id + encoded_name
310
+
311
+ @staticmethod
312
+ def _decode_asset(encoded_asset: str) -> tuple[str, str]:
313
+ """Decode an encoded asset into policy ID and asset name."""
314
+ policy_id = encoded_asset[:POLICY_ID_LENGTH]
315
+ asset_name = bytes.fromhex(encoded_asset[POLICY_ID_LENGTH:]).decode("utf-8")
316
+ return policy_id, asset_name
317
+
318
+ @staticmethod
319
+ def _split_asset(asset: str) -> tuple[str, str]:
320
+ """Split an asset string into policy ID and asset name."""
321
+ if len(asset) == POLICY_ID_LENGTH: # Only policy ID
322
+ return asset, ""
323
+ return asset[:POLICY_ID_LENGTH], asset[POLICY_ID_LENGTH:]
324
+
325
+ @classmethod
326
+ def _refresh_pools(cls) -> None:
327
+ """Refresh the pools data from the API."""
328
+ try:
329
+ response = requests.get(
330
+ "https://api.vyfi.io/lp?networkId=1&v2=true",
331
+ timeout=10,
332
+ )
333
+ response.raise_for_status()
334
+ cls._pools = {}
335
+ for p in response.json():
336
+ p["json"] = json.loads(p["json"])
337
+ cls._pools[
338
+ p["json"]["mainNFT"]["currencySymbol"]
339
+ ] = VyFiPoolDefinition.model_validate(p)
340
+ cls._pools_refresh = time.time()
341
+ except requests.RequestException as e:
342
+ # Log the error or handle it as appropriate for your application
343
+ print(f"Error refreshing pools: {e}") # noqa: T201
344
+
345
+ @property
346
+ def swap_forward(self) -> bool:
347
+ """Check if swap is forward."""
348
+ return False
349
+
350
+ @property
351
+ def stake_address(self) -> Address:
352
+ """Get the stake address."""
353
+ return Address.from_primitive(
354
+ VyFiCPPState.pools()[self.pool_id].order_validator_utxo_address,
355
+ )
356
+
357
+ @classmethod
358
+ def order_datum_class(cls) -> type[VyFiOrderDatum]:
359
+ """Get the order datum class."""
360
+ return VyFiOrderDatum
361
+
362
+ @classmethod
363
+ def pool_datum_class(cls) -> type[VyFiPoolDatum]:
364
+ """Get the pool datum class."""
365
+ return VyFiPoolDatum
366
+
367
+ @property
368
+ def pool_id(self) -> str:
369
+ """Get a unique identifier for the pool."""
370
+ return self.pool_nft.unit()
371
+
372
+ @property
373
+ def volume_fee(self) -> int:
374
+ """Get the volume fee."""
375
+ return self.lp_fee + self.bar_fee
376
+
377
+ @classmethod
378
+ def extract_pool_nft(cls, values: dict[str, Any]) -> Optional[Assets]:
379
+ """Extract the dex nft from the UTXO."""
380
+ assets = values["assets"]
381
+
382
+ if "pool_nft" in values:
383
+ pool_nft = (
384
+ Assets(root=values["pool_nft"])
385
+ if isinstance(values["pool_nft"], dict)
386
+ else values["pool_nft"]
387
+ )
388
+ if not any(p in cls.pools() for p in values["pool_nft"]):
389
+ raise ValueError("Invalid pool NFT")
390
+ else:
391
+ nfts = [asset for asset, quantity in assets.items() if asset in cls.pools()]
392
+ if len(nfts) < 1:
393
+ if len(assets) == 0:
394
+ raise NoAssetsError(f"{cls.__name__}: No assets supplied.")
395
+ raise NotAPoolError(
396
+ f"{cls.__name__}: Pool must have one DEX NFT token.",
397
+ )
398
+ pool_nft = Assets(**{nfts[0]: assets.root.pop(nfts[0])})
399
+ values["pool_nft"] = pool_nft
400
+
401
+ values["lp_fee"] = cls.pools()[pool_nft.unit()].json_.fees_settings.liq_fee
402
+ values["bar_fee"] = cls.pools()[pool_nft.unit()].json_.fees_settings.bar_fee
403
+
404
+ return pool_nft
405
+
406
+ @classmethod
407
+ def post_init(cls, values: dict[str, Any]) -> None:
408
+ """Post-initialization processing."""
409
+ super().post_init(values)
410
+
411
+ assets = values["assets"]
412
+ datum = VyFiPoolDatum.from_cbor(values["datum_cbor"])
413
+
414
+ assets.root[assets.unit(0)] -= datum.token_a_fees
415
+ assets.root[assets.unit(1)] -= datum.token_b_fees