avantis-trader-sdk 0.8.12__py3-none-any.whl → 0.8.14__py3-none-any.whl

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.
@@ -10,5 +10,8 @@ MAINNET_ADDRESSES = {
10
10
  }
11
11
 
12
12
  AVANTIS_SOCKET_API = "https://socket-api-pub.avantisfi.com/socket-api/v1/data"
13
+ AVANTIS_CORE_API_BASE_URL = "https://core.avantisfi.com"
14
+ AVANTIS_FEED_V3_URL = "https://feed-v3.avantisfi.com"
15
+ PYTH_LAZER_SSE_URL = "https://pyth-lazer-proxy-3.dourolabs.app/v1/stream"
13
16
 
14
17
  CONTRACT_ADDRESSES = MAINNET_ADDRESSES
@@ -1,12 +1,19 @@
1
1
  import json
2
2
  import websockets
3
- from ..types import PriceFeedResponse, PriceFeedUpdatesResponse, PairInfoFeed
4
- from typing import List, Callable
3
+ from ..types import (
4
+ PriceFeedResponse,
5
+ PriceFeedUpdatesResponse,
6
+ PairInfoFeed,
7
+ FeedV3PriceResponse,
8
+ LazerPriceFeedResponse,
9
+ )
10
+ from typing import List, Callable, Optional
5
11
  import requests
6
12
  from pydantic import ValidationError
7
- from ..config import AVANTIS_SOCKET_API
13
+ from ..config import AVANTIS_SOCKET_API, AVANTIS_FEED_V3_URL, PYTH_LAZER_SSE_URL
8
14
  import asyncio
9
15
  from concurrent.futures import ThreadPoolExecutor
16
+ import aiohttp
10
17
 
11
18
 
12
19
  class FeedClient:
@@ -22,14 +29,21 @@ class FeedClient:
22
29
  hermes_url="https://hermes.pyth.network/v2/updates/price/latest",
23
30
  socket_api: str = AVANTIS_SOCKET_API,
24
31
  pair_fetcher: Callable = None,
32
+ feed_v3_url: str = AVANTIS_FEED_V3_URL,
33
+ lazer_sse_url: str = PYTH_LAZER_SSE_URL,
25
34
  ):
26
35
  """
27
36
  Constructor for the FeedClient class.
28
37
 
29
38
  Args:
30
- ws_url: Optional - The websocket URL to connect to.
31
- on_error: Optional callback for handling websocket errors.
32
- on_close: Optional callback for handling websocket close events.
39
+ ws_url: Optional - The websocket URL to connect to (Pyth Hermes).
40
+ on_error: Optional callback for handling websocket/SSE errors.
41
+ on_close: Optional callback for handling websocket/SSE close events.
42
+ hermes_url: Optional - The Hermes HTTP API URL.
43
+ socket_api: Optional - The Avantis socket API URL.
44
+ pair_fetcher: Optional - Custom pair fetcher function.
45
+ feed_v3_url: Optional - The feed-v3 API URL for price update data.
46
+ lazer_sse_url: Optional - The Pyth Lazer SSE URL for real-time prices.
33
47
  """
34
48
  if (
35
49
  ws_url is not None
@@ -40,11 +54,15 @@ class FeedClient:
40
54
 
41
55
  self.ws_url = ws_url
42
56
  self.hermes_url = hermes_url
57
+ self.feed_v3_url = feed_v3_url
58
+ self.lazer_sse_url = lazer_sse_url
43
59
  self.pair_feeds = {}
44
60
  self.feed_pairs = {}
45
61
  self.price_feed_callbacks = {}
62
+ self.lazer_callbacks = {}
46
63
  self._socket = None
47
64
  self._connected = False
65
+ self._lazer_connected = False
48
66
  self._on_error = on_error
49
67
  self._on_close = on_close
50
68
  self.socket_api = socket_api
@@ -122,7 +140,7 @@ class FeedClient:
122
140
  return pairs
123
141
  except (requests.RequestException, ValidationError) as e:
124
142
  print(f"Error fetching pair feeds: {e}")
125
- return []
143
+ return []
126
144
 
127
145
  def load_pair_feeds(self):
128
146
  """
@@ -130,6 +148,9 @@ class FeedClient:
130
148
  """
131
149
 
132
150
  try:
151
+ if self.pair_feeds:
152
+ return
153
+
133
154
  try:
134
155
  asyncio.get_running_loop()
135
156
  except RuntimeError:
@@ -231,6 +252,9 @@ class FeedClient:
231
252
  Returns:
232
253
  A PriceFeedUpdatesResponse object containing the latest price updates.
233
254
  """
255
+ if not self.pair_feeds:
256
+ self.load_pair_feeds()
257
+
234
258
  url = self.hermes_url
235
259
 
236
260
  feedIds = []
@@ -261,3 +285,75 @@ class FeedClient:
261
285
  return PriceFeedUpdatesResponse(**data)
262
286
  else:
263
287
  response.raise_for_status()
288
+
289
+ async def get_price_update_data(self, pair_index: int) -> FeedV3PriceResponse:
290
+ """
291
+ Retrieves price update data from the feed-v3 API for a specific pair.
292
+
293
+ This returns both core (Pyth Hermes) and pro (Pyth Lazer) price data,
294
+ including the priceUpdateData bytes needed for contract calls.
295
+
296
+ Args:
297
+ pair_index: The pair index to get price update data for.
298
+
299
+ Returns:
300
+ A FeedV3PriceResponse containing core and pro price data.
301
+
302
+ Raises:
303
+ requests.HTTPError: If the API request fails.
304
+ """
305
+ url = f"{self.feed_v3_url}/v2/pairs/{pair_index}/price-update-data"
306
+ response = requests.get(url, timeout=10)
307
+ response.raise_for_status()
308
+ data = response.json()
309
+ return FeedV3PriceResponse(**data)
310
+
311
+ async def listen_for_lazer_price_updates(
312
+ self,
313
+ lazer_feed_ids: List[int],
314
+ callback: Callable[[LazerPriceFeedResponse], None],
315
+ ):
316
+ """
317
+ Listens for real-time price updates from the Pyth Lazer SSE stream.
318
+
319
+ This is the Pyth Pro alternative to the WebSocket-based listen_for_price_updates.
320
+
321
+ Args:
322
+ lazer_feed_ids: List of Lazer feed IDs to subscribe to.
323
+ callback: Callback function to handle price updates.
324
+
325
+ Raises:
326
+ Exception: If an error occurs while listening for price updates.
327
+ """
328
+ params = "&".join([f"price_feed_ids={fid}" for fid in lazer_feed_ids])
329
+ url = f"{self.lazer_sse_url}?{params}"
330
+
331
+ try:
332
+ async with aiohttp.ClientSession() as session:
333
+ async with session.get(url) as response:
334
+ self._lazer_connected = True
335
+ async for line in response.content:
336
+ line = line.decode("utf-8").strip()
337
+ if line.startswith("data:"):
338
+ try:
339
+ data = json.loads(line[5:].strip())
340
+ price_response = LazerPriceFeedResponse(**data)
341
+ callback(price_response)
342
+ except json.JSONDecodeError as e:
343
+ if self._on_error:
344
+ self._on_error(e)
345
+ except ValidationError as e:
346
+ if self._on_error:
347
+ self._on_error(e)
348
+ except aiohttp.ClientError as e:
349
+ self._lazer_connected = False
350
+ if self._on_error:
351
+ self._on_error(e)
352
+ else:
353
+ raise e
354
+ except Exception as e:
355
+ self._lazer_connected = False
356
+ if self._on_close:
357
+ self._on_close(e)
358
+ else:
359
+ raise e
@@ -28,6 +28,7 @@ class PairsCache:
28
28
  self._pair_mapping = {}
29
29
 
30
30
  self._pair_info_from_socket_cache = {}
31
+ self._socket_info_cache = {}
31
32
 
32
33
  async def get_pairs_info(self, force_update=False):
33
34
  """
@@ -87,10 +88,13 @@ class PairsCache:
87
88
 
88
89
  return self._pair_info_cache
89
90
 
90
- async def get_pair_info_from_socket(self, pair_index=None):
91
+ async def get_pair_info_from_socket(self, pair_index=None, use_cache=False):
91
92
  """
92
93
  Retrieves the pair information from the socket.
93
94
  """
95
+ if not use_cache and self._pair_info_from_socket_cache:
96
+ return self._pair_info_from_socket_cache[str(pair_index)]
97
+
94
98
  if not self.socket_api:
95
99
  raise ValueError("socket_api is not set")
96
100
  try:
@@ -98,7 +102,8 @@ class PairsCache:
98
102
  response.raise_for_status()
99
103
 
100
104
  result = response.json()
101
- pairs = result["data"]["pairInfos"]
105
+ self._socket_info_cache = result["data"]
106
+ pairs = self._socket_info_cache["pairInfos"]
102
107
  self._pair_info_from_socket_cache = pairs
103
108
  except (requests.RequestException, ValidationError) as e:
104
109
  print(f"Error fetching pair feeds: {e}")
@@ -108,6 +113,28 @@ class PairsCache:
108
113
  return self._pair_info_from_socket_cache[str(pair_index)]
109
114
  return self._pair_info_from_socket_cache
110
115
 
116
+ async def get_info_from_socket(self, force_update=False):
117
+ """
118
+ Retrieves the socket information.
119
+ """
120
+ if not force_update and self._socket_info_cache:
121
+ return self._socket_info_cache
122
+
123
+ if not self.socket_api:
124
+ raise ValueError("socket_api is not set")
125
+ try:
126
+ response = requests.get(self.socket_api)
127
+ response.raise_for_status()
128
+
129
+ result = response.json()
130
+ self._socket_info_cache = result["data"]
131
+ self._pair_info_from_socket_cache = self._socket_info_cache["pairInfos"]
132
+ except (requests.RequestException, ValidationError) as e:
133
+ print(f"Error fetching pair feeds: {e}")
134
+ return {}
135
+
136
+ return self._socket_info_cache
137
+
111
138
  async def get_pairs_count(self):
112
139
  """
113
140
  Retrieves the number of pairs from the blockchain.
@@ -160,3 +187,22 @@ class PairsCache:
160
187
  """
161
188
  pairs_info = await self.get_pairs_info()
162
189
  return pairs_info[pair_index].from_ + "/" + pairs_info[pair_index].to
190
+
191
+ async def get_lazer_feed_id(self, pair_index: int) -> int:
192
+ """
193
+ Retrieves the Pyth Lazer feed ID for a pair.
194
+
195
+ Args:
196
+ pair_index: The pair index.
197
+
198
+ Returns:
199
+ The Lazer feed ID as an integer.
200
+
201
+ Raises:
202
+ ValueError: If the pair does not have a Lazer feed configured.
203
+ """
204
+ pair_info = await self.get_pair_info_from_socket(pair_index)
205
+ lazer_feed = pair_info.get("lazerFeed")
206
+ if not lazer_feed:
207
+ raise ValueError(f"Pair {pair_index} does not have a Lazer feed configured")
208
+ return lazer_feed.get("feedId")