dexscreen 0.0.1__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.
- dexscreen/__init__.py +31 -0
- dexscreen/api/__init__.py +3 -0
- dexscreen/api/client.py +672 -0
- dexscreen/config/__init__.py +0 -0
- dexscreen/core/__init__.py +27 -0
- dexscreen/core/http.py +460 -0
- dexscreen/core/models.py +106 -0
- dexscreen/stream/__init__.py +3 -0
- dexscreen/stream/polling.py +462 -0
- dexscreen/utils/__init__.py +4 -0
- dexscreen/utils/browser_selector.py +57 -0
- dexscreen/utils/filters.py +226 -0
- dexscreen/utils/ratelimit.py +65 -0
- dexscreen-0.0.1.dist-info/METADATA +278 -0
- dexscreen-0.0.1.dist-info/RECORD +17 -0
- dexscreen-0.0.1.dist-info/WHEEL +4 -0
- dexscreen-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
from .http import HttpClientCffi
|
2
|
+
from .models import (
|
3
|
+
BaseToken,
|
4
|
+
Liquidity,
|
5
|
+
OrderInfo,
|
6
|
+
PairTransactionCounts,
|
7
|
+
PriceChangePeriods,
|
8
|
+
TokenInfo,
|
9
|
+
TokenLink,
|
10
|
+
TokenPair,
|
11
|
+
TransactionCount,
|
12
|
+
VolumeChangePeriods,
|
13
|
+
)
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"BaseToken",
|
17
|
+
"HttpClientCffi",
|
18
|
+
"Liquidity",
|
19
|
+
"OrderInfo",
|
20
|
+
"PairTransactionCounts",
|
21
|
+
"PriceChangePeriods",
|
22
|
+
"TokenInfo",
|
23
|
+
"TokenLink",
|
24
|
+
"TokenPair",
|
25
|
+
"TransactionCount",
|
26
|
+
"VolumeChangePeriods",
|
27
|
+
]
|
dexscreen/core/http.py
ADDED
@@ -0,0 +1,460 @@
|
|
1
|
+
"""
|
2
|
+
Enhanced with realworld browser impersonation and custom configuration support
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import contextlib
|
7
|
+
from datetime import datetime, timedelta
|
8
|
+
from enum import Enum
|
9
|
+
from threading import Lock
|
10
|
+
from typing import Any, Literal, Optional, Union
|
11
|
+
|
12
|
+
import orjson
|
13
|
+
from curl_cffi.requests import AsyncSession, Session
|
14
|
+
|
15
|
+
from ..utils.browser_selector import get_random_browser
|
16
|
+
from ..utils.ratelimit import RateLimiter
|
17
|
+
|
18
|
+
# Type alias for HTTP methods
|
19
|
+
HttpMethod = Literal["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "TRACE"]
|
20
|
+
|
21
|
+
|
22
|
+
class SessionState(Enum):
|
23
|
+
"""Session state"""
|
24
|
+
|
25
|
+
ACTIVE = "active" # Active, accepting new requests
|
26
|
+
DRAINING = "draining" # Draining, not accepting new requests
|
27
|
+
STANDBY = "standby" # Standby, ready to take over
|
28
|
+
CLOSED = "closed" # Closed
|
29
|
+
|
30
|
+
|
31
|
+
class HttpClientCffi:
|
32
|
+
"""HTTP client with curl_cffi for bypassing anti-bot measures
|
33
|
+
|
34
|
+
Features:
|
35
|
+
- Session reuse for better performance (avoid TLS handshake)
|
36
|
+
- Zero-downtime configuration updates
|
37
|
+
- Graceful session switching
|
38
|
+
- Automatic connection warm-up
|
39
|
+
"""
|
40
|
+
|
41
|
+
def __init__(
|
42
|
+
self,
|
43
|
+
calls: int,
|
44
|
+
period: int,
|
45
|
+
base_url: str = "https://api.dexscreener.com/",
|
46
|
+
client_kwargs: Optional[dict[str, Any]] = None,
|
47
|
+
warmup_url: str = "/latest/dex/tokens/solana?limit=1",
|
48
|
+
):
|
49
|
+
"""
|
50
|
+
Initialize HTTP client with rate limiting and browser impersonation.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
calls: Maximum number of calls allowed
|
54
|
+
period: Time period in seconds
|
55
|
+
base_url: Base URL for API requests
|
56
|
+
client_kwargs: Optional kwargs to pass to curl_cffi Session/AsyncSession.
|
57
|
+
Common options include:
|
58
|
+
- impersonate: Browser to impersonate (default: "realworld")
|
59
|
+
- proxies: Proxy configuration
|
60
|
+
- timeout: Request timeout
|
61
|
+
- headers: Additional headers
|
62
|
+
- verify: SSL verification
|
63
|
+
warmup_url: URL path for warming up new sessions
|
64
|
+
"""
|
65
|
+
self._limiter = RateLimiter(calls, period)
|
66
|
+
self.base_url = base_url
|
67
|
+
self.warmup_url = warmup_url
|
68
|
+
|
69
|
+
# Setup client kwargs with defaults
|
70
|
+
self.client_kwargs = client_kwargs or {}
|
71
|
+
# Use our custom realworld browser selection if not specified
|
72
|
+
if "impersonate" not in self.client_kwargs:
|
73
|
+
self.client_kwargs["impersonate"] = get_random_browser()
|
74
|
+
|
75
|
+
# Thread lock for safe updates
|
76
|
+
self._lock = Lock()
|
77
|
+
|
78
|
+
# Session management
|
79
|
+
# Primary session
|
80
|
+
self._primary_session: Optional[AsyncSession] = None
|
81
|
+
self._primary_state = SessionState.CLOSED
|
82
|
+
self._primary_requests = 0 # Active request count
|
83
|
+
|
84
|
+
# Secondary session for hot switching
|
85
|
+
self._secondary_session: Optional[AsyncSession] = None
|
86
|
+
self._secondary_state = SessionState.CLOSED
|
87
|
+
self._secondary_requests = 0
|
88
|
+
|
89
|
+
# Sync sessions
|
90
|
+
self._sync_primary: Optional[Session] = None
|
91
|
+
self._sync_secondary: Optional[Session] = None
|
92
|
+
|
93
|
+
# Async lock for session switching
|
94
|
+
self._switch_lock = asyncio.Lock()
|
95
|
+
|
96
|
+
# Statistics
|
97
|
+
self._stats = {
|
98
|
+
"switches": 0,
|
99
|
+
"failed_requests": 0,
|
100
|
+
"successful_requests": 0,
|
101
|
+
"last_switch": None,
|
102
|
+
}
|
103
|
+
|
104
|
+
def _create_absolute_url(self, relative: str) -> str:
|
105
|
+
base = self.base_url.rstrip("/")
|
106
|
+
relative = relative.lstrip("/")
|
107
|
+
return f"{base}/{relative}"
|
108
|
+
|
109
|
+
async def _ensure_active_session(self) -> AsyncSession:
|
110
|
+
"""Ensure there's an active session"""
|
111
|
+
async with self._switch_lock:
|
112
|
+
# If primary session is not active, create it
|
113
|
+
if self._primary_state != SessionState.ACTIVE and self._primary_session is None:
|
114
|
+
self._primary_session = AsyncSession(**self.client_kwargs)
|
115
|
+
# Warm up connection
|
116
|
+
warmup_success = False
|
117
|
+
try:
|
118
|
+
warmup_url = self._create_absolute_url(self.warmup_url)
|
119
|
+
response = await self._primary_session.get(warmup_url)
|
120
|
+
if response.status_code == 200:
|
121
|
+
warmup_success = True
|
122
|
+
except Exception:
|
123
|
+
pass # Warmup failure doesn't affect usage
|
124
|
+
|
125
|
+
# Only activate if warmup succeeded
|
126
|
+
if warmup_success:
|
127
|
+
self._primary_state = SessionState.ACTIVE
|
128
|
+
else:
|
129
|
+
# Keep trying with the session even if warmup failed
|
130
|
+
# This maintains backward compatibility
|
131
|
+
self._primary_state = SessionState.ACTIVE
|
132
|
+
|
133
|
+
if self._primary_session is None:
|
134
|
+
raise RuntimeError("Failed to create primary session")
|
135
|
+
return self._primary_session
|
136
|
+
|
137
|
+
def _ensure_sync_session(self) -> Session:
|
138
|
+
"""Ensure there's a sync session"""
|
139
|
+
with self._lock:
|
140
|
+
if self._sync_primary is None:
|
141
|
+
self._sync_primary = Session(**self.client_kwargs)
|
142
|
+
# Warm up
|
143
|
+
try:
|
144
|
+
warmup_url = self._create_absolute_url(self.warmup_url)
|
145
|
+
response = self._sync_primary.get(warmup_url)
|
146
|
+
# Check if warmup was successful
|
147
|
+
if response.status_code != 200:
|
148
|
+
pass # Log warning in production
|
149
|
+
except Exception:
|
150
|
+
pass
|
151
|
+
|
152
|
+
return self._sync_primary
|
153
|
+
|
154
|
+
def request(self, method: HttpMethod, url: str, **kwargs) -> Union[list, dict, None]:
|
155
|
+
"""
|
156
|
+
Synchronous request with rate limiting and browser impersonation.
|
157
|
+
|
158
|
+
Args:
|
159
|
+
method: HTTP method (GET, POST, etc.)
|
160
|
+
url: Relative URL path
|
161
|
+
**kwargs: Additional request kwargs
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
Parsed JSON response
|
165
|
+
"""
|
166
|
+
url = self._create_absolute_url(url)
|
167
|
+
|
168
|
+
with self._limiter:
|
169
|
+
try:
|
170
|
+
# Use persistent session
|
171
|
+
session = self._ensure_sync_session()
|
172
|
+
response = session.request(method, url, **kwargs) # type: ignore
|
173
|
+
response.raise_for_status()
|
174
|
+
|
175
|
+
# Check if response is JSON
|
176
|
+
content_type = response.headers.get("content-type", "")
|
177
|
+
if "application/json" in content_type:
|
178
|
+
# Use orjson for better performance
|
179
|
+
return orjson.loads(response.content)
|
180
|
+
else:
|
181
|
+
# Non-JSON response (e.g., HTML error page)
|
182
|
+
return None
|
183
|
+
except Exception:
|
184
|
+
return None
|
185
|
+
|
186
|
+
async def request_async(self, method: HttpMethod, url: str, **kwargs) -> Union[list, dict, None]:
|
187
|
+
"""
|
188
|
+
Asynchronous request with rate limiting and browser impersonation.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
method: HTTP method (GET, POST, etc.)
|
192
|
+
url: Relative URL path
|
193
|
+
**kwargs: Additional request kwargs
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
Parsed JSON response
|
197
|
+
"""
|
198
|
+
url = self._create_absolute_url(url)
|
199
|
+
|
200
|
+
async with self._limiter:
|
201
|
+
# Get active session
|
202
|
+
session = await self._ensure_active_session()
|
203
|
+
|
204
|
+
# Track active requests
|
205
|
+
with self._lock:
|
206
|
+
self._primary_requests += 1
|
207
|
+
|
208
|
+
try:
|
209
|
+
response = await session.request(method, url, **kwargs) # type: ignore
|
210
|
+
response.raise_for_status()
|
211
|
+
|
212
|
+
# Statistics
|
213
|
+
with self._lock:
|
214
|
+
self._stats["successful_requests"] += 1
|
215
|
+
|
216
|
+
# Parse response
|
217
|
+
content_type = response.headers.get("content-type", "")
|
218
|
+
if "application/json" in content_type:
|
219
|
+
# Use orjson for better performance
|
220
|
+
return orjson.loads(response.content)
|
221
|
+
else:
|
222
|
+
return None
|
223
|
+
|
224
|
+
except Exception:
|
225
|
+
with self._lock:
|
226
|
+
self._stats["failed_requests"] += 1
|
227
|
+
|
228
|
+
# Try failover to secondary session if available
|
229
|
+
if self._secondary_state == SessionState.ACTIVE:
|
230
|
+
return await self._failover_request(method, url, **kwargs)
|
231
|
+
|
232
|
+
return None
|
233
|
+
|
234
|
+
finally:
|
235
|
+
# Decrease request count
|
236
|
+
with self._lock:
|
237
|
+
self._primary_requests -= 1
|
238
|
+
|
239
|
+
async def _failover_request(self, method: HttpMethod, url: str, **kwargs) -> Union[list, dict, None]:
|
240
|
+
"""Failover to secondary session"""
|
241
|
+
if self._secondary_session and self._secondary_state == SessionState.ACTIVE:
|
242
|
+
try:
|
243
|
+
with self._lock:
|
244
|
+
self._secondary_requests += 1
|
245
|
+
|
246
|
+
response = await self._secondary_session.request(method, url, **kwargs) # type: ignore
|
247
|
+
response.raise_for_status()
|
248
|
+
|
249
|
+
content_type = response.headers.get("content-type", "")
|
250
|
+
if "application/json" in content_type:
|
251
|
+
# Use orjson for better performance
|
252
|
+
return orjson.loads(response.content)
|
253
|
+
else:
|
254
|
+
return None
|
255
|
+
|
256
|
+
finally:
|
257
|
+
with self._lock:
|
258
|
+
self._secondary_requests -= 1
|
259
|
+
|
260
|
+
return None
|
261
|
+
|
262
|
+
async def _perform_switch(self):
|
263
|
+
"""Perform hot switch between sessions"""
|
264
|
+
# 1. Promote secondary to active
|
265
|
+
self._secondary_state = SessionState.ACTIVE
|
266
|
+
|
267
|
+
# 2. Mark primary as draining
|
268
|
+
if self._primary_state == SessionState.ACTIVE:
|
269
|
+
self._primary_state = SessionState.DRAINING
|
270
|
+
|
271
|
+
# 3. Swap references
|
272
|
+
old_primary = self._primary_session
|
273
|
+
old_primary_requests = self._primary_requests
|
274
|
+
|
275
|
+
self._primary_session = self._secondary_session
|
276
|
+
self._primary_requests = self._secondary_requests
|
277
|
+
self._primary_state = SessionState.ACTIVE
|
278
|
+
|
279
|
+
self._secondary_session = old_primary
|
280
|
+
self._secondary_requests = old_primary_requests
|
281
|
+
self._secondary_state = SessionState.DRAINING
|
282
|
+
|
283
|
+
# 4. Async cleanup of old session
|
284
|
+
if old_primary:
|
285
|
+
asyncio.create_task(self._graceful_close_session(old_primary, lambda: self._secondary_requests))
|
286
|
+
|
287
|
+
async def _graceful_close_session(self, session: AsyncSession, get_request_count):
|
288
|
+
"""Gracefully close session after requests complete"""
|
289
|
+
# Wait for ongoing requests to complete (max 30 seconds)
|
290
|
+
start_time = datetime.now()
|
291
|
+
timeout = timedelta(seconds=30)
|
292
|
+
|
293
|
+
while get_request_count() > 0:
|
294
|
+
if datetime.now() - start_time > timeout:
|
295
|
+
break
|
296
|
+
|
297
|
+
await asyncio.sleep(0.1)
|
298
|
+
|
299
|
+
# Close session
|
300
|
+
with contextlib.suppress(Exception):
|
301
|
+
await session.close()
|
302
|
+
|
303
|
+
def set_impersonate(self, browser: str):
|
304
|
+
"""
|
305
|
+
Change browser impersonation.
|
306
|
+
|
307
|
+
NOTE: This method only updates the configuration for future requests.
|
308
|
+
It does NOT trigger a hot-switch of existing sessions.
|
309
|
+
Use update_config() for hot-switching with zero downtime.
|
310
|
+
|
311
|
+
Args:
|
312
|
+
browser: Browser to impersonate. Options include:
|
313
|
+
- "chrome136", "chrome134", etc.: Specific Chrome versions
|
314
|
+
- "safari180", "safari184", etc.: Specific Safari versions
|
315
|
+
- "firefox133", "firefox135", etc.: Specific Firefox versions
|
316
|
+
Note: "realworld" is replaced by our custom browser selector
|
317
|
+
"""
|
318
|
+
# Update client kwargs for future sessions
|
319
|
+
with self._lock:
|
320
|
+
self.client_kwargs["impersonate"] = browser
|
321
|
+
|
322
|
+
async def update_config(self, new_kwargs: dict[str, Any], replace: bool = False):
|
323
|
+
"""
|
324
|
+
Hot update configuration with zero downtime.
|
325
|
+
Creates new session with new config and gracefully switches.
|
326
|
+
|
327
|
+
Args:
|
328
|
+
new_kwargs: New configuration options
|
329
|
+
replace: If True, replace entire config. If False (default), merge with existing.
|
330
|
+
"""
|
331
|
+
# Don't lock here - we want requests to continue
|
332
|
+
# Prepare new config
|
333
|
+
if replace:
|
334
|
+
# Complete replacement
|
335
|
+
config = new_kwargs.copy()
|
336
|
+
else:
|
337
|
+
# Merge with existing
|
338
|
+
config = self.client_kwargs.copy()
|
339
|
+
config.update(new_kwargs)
|
340
|
+
|
341
|
+
# Handle special case: if proxy is None, remove it
|
342
|
+
if "proxy" in new_kwargs and new_kwargs["proxy"] is None:
|
343
|
+
config.pop("proxy", None)
|
344
|
+
config.pop("proxies", None)
|
345
|
+
|
346
|
+
if "impersonate" not in config:
|
347
|
+
config["impersonate"] = get_random_browser()
|
348
|
+
|
349
|
+
# Create new session (secondary) without blocking
|
350
|
+
new_session = AsyncSession(**config)
|
351
|
+
|
352
|
+
# Warm up new connection in background
|
353
|
+
warmup_success = False
|
354
|
+
try:
|
355
|
+
warmup_url = self._create_absolute_url(self.warmup_url)
|
356
|
+
response = await new_session.get(warmup_url)
|
357
|
+
# Only consider warmup successful if we get 200 OK
|
358
|
+
if response.status_code == 200:
|
359
|
+
warmup_success = True
|
360
|
+
except Exception:
|
361
|
+
pass
|
362
|
+
|
363
|
+
# Only proceed with switch if warmup was successful
|
364
|
+
if warmup_success:
|
365
|
+
# Now acquire lock for the actual switch
|
366
|
+
async with self._switch_lock:
|
367
|
+
# Store the new session
|
368
|
+
self._secondary_session = new_session
|
369
|
+
self._secondary_state = SessionState.STANDBY
|
370
|
+
|
371
|
+
# Perform switch
|
372
|
+
await self._perform_switch()
|
373
|
+
|
374
|
+
# Update config
|
375
|
+
self.client_kwargs = config
|
376
|
+
|
377
|
+
# Statistics
|
378
|
+
with self._lock:
|
379
|
+
self._stats["switches"] += 1
|
380
|
+
self._stats["last_switch"] = datetime.now()
|
381
|
+
else:
|
382
|
+
# Clean up failed session
|
383
|
+
with contextlib.suppress(Exception):
|
384
|
+
await new_session.close()
|
385
|
+
|
386
|
+
def update_client_kwargs(self, new_kwargs: dict[str, Any], merge: bool = True):
|
387
|
+
"""
|
388
|
+
Update client configuration at runtime.
|
389
|
+
|
390
|
+
Args:
|
391
|
+
new_kwargs: New configuration options to apply
|
392
|
+
merge: If True, merge with existing kwargs. If False, replace entirely.
|
393
|
+
|
394
|
+
Example:
|
395
|
+
# Update proxy
|
396
|
+
client.update_client_kwargs({"proxies": {"https": "http://new-proxy:8080"}})
|
397
|
+
|
398
|
+
# Change impersonation
|
399
|
+
client.update_client_kwargs({"impersonate": "safari184"})
|
400
|
+
|
401
|
+
# Add custom headers
|
402
|
+
client.update_client_kwargs({"headers": {"X-Custom": "value"}})
|
403
|
+
|
404
|
+
# Replace all kwargs
|
405
|
+
client.update_client_kwargs({
|
406
|
+
"impersonate": "firefox135",
|
407
|
+
"timeout": 30,
|
408
|
+
"verify": False
|
409
|
+
}, merge=False)
|
410
|
+
"""
|
411
|
+
with self._lock:
|
412
|
+
if merge:
|
413
|
+
self.client_kwargs.update(new_kwargs)
|
414
|
+
else:
|
415
|
+
self.client_kwargs = new_kwargs.copy()
|
416
|
+
|
417
|
+
# Ensure we have browser impersonation
|
418
|
+
if "impersonate" not in self.client_kwargs:
|
419
|
+
self.client_kwargs["impersonate"] = get_random_browser()
|
420
|
+
|
421
|
+
def get_current_config(self) -> dict[str, Any]:
|
422
|
+
"""
|
423
|
+
Get a copy of current client configuration.
|
424
|
+
|
425
|
+
Returns:
|
426
|
+
Current client_kwargs dictionary
|
427
|
+
"""
|
428
|
+
with self._lock:
|
429
|
+
return self.client_kwargs.copy()
|
430
|
+
|
431
|
+
def get_stats(self) -> dict[str, Any]:
|
432
|
+
"""
|
433
|
+
Get statistics.
|
434
|
+
|
435
|
+
Returns:
|
436
|
+
Statistics dictionary with switches, requests, etc.
|
437
|
+
"""
|
438
|
+
with self._lock:
|
439
|
+
return self._stats.copy()
|
440
|
+
|
441
|
+
async def close(self):
|
442
|
+
"""
|
443
|
+
Close all sessions gracefully.
|
444
|
+
"""
|
445
|
+
tasks = []
|
446
|
+
|
447
|
+
if self._primary_session:
|
448
|
+
tasks.append(self._primary_session.close())
|
449
|
+
|
450
|
+
if self._secondary_session:
|
451
|
+
tasks.append(self._secondary_session.close())
|
452
|
+
|
453
|
+
if self._sync_primary:
|
454
|
+
self._sync_primary.close()
|
455
|
+
|
456
|
+
if self._sync_secondary:
|
457
|
+
self._sync_secondary.close()
|
458
|
+
|
459
|
+
if tasks:
|
460
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
dexscreen/core/models.py
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
import datetime as dt
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
5
|
+
|
6
|
+
# Base configuration for all models
|
7
|
+
base_config = ConfigDict(
|
8
|
+
# Standard Pydantic config
|
9
|
+
populate_by_name=True,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class BaseToken(BaseModel):
|
14
|
+
model_config = base_config
|
15
|
+
|
16
|
+
address: str
|
17
|
+
name: str
|
18
|
+
symbol: str
|
19
|
+
|
20
|
+
|
21
|
+
class TransactionCount(BaseModel):
|
22
|
+
model_config = base_config
|
23
|
+
|
24
|
+
buys: int
|
25
|
+
sells: int
|
26
|
+
|
27
|
+
|
28
|
+
class PairTransactionCounts(BaseModel):
|
29
|
+
model_config = base_config
|
30
|
+
|
31
|
+
m5: TransactionCount
|
32
|
+
h1: TransactionCount
|
33
|
+
h6: TransactionCount
|
34
|
+
h24: TransactionCount
|
35
|
+
|
36
|
+
|
37
|
+
class _TimePeriodsFloat(BaseModel):
|
38
|
+
model_config = base_config
|
39
|
+
|
40
|
+
m5: Optional[float] = 0.0
|
41
|
+
h1: Optional[float] = 0.0
|
42
|
+
h6: Optional[float] = 0.0
|
43
|
+
h24: Optional[float] = 0.0
|
44
|
+
|
45
|
+
|
46
|
+
class VolumeChangePeriods(_TimePeriodsFloat): ...
|
47
|
+
|
48
|
+
|
49
|
+
class PriceChangePeriods(_TimePeriodsFloat): ...
|
50
|
+
|
51
|
+
|
52
|
+
class Liquidity(BaseModel):
|
53
|
+
model_config = base_config
|
54
|
+
|
55
|
+
usd: Optional[float] = None
|
56
|
+
base: float
|
57
|
+
quote: float
|
58
|
+
|
59
|
+
|
60
|
+
class TokenPair(BaseModel):
|
61
|
+
model_config = base_config
|
62
|
+
|
63
|
+
chain_id: str = Field(..., alias="chainId")
|
64
|
+
dex_id: str = Field(..., alias="dexId")
|
65
|
+
url: str = Field(...)
|
66
|
+
pair_address: str = Field(..., alias="pairAddress")
|
67
|
+
base_token: BaseToken = Field(..., alias="baseToken")
|
68
|
+
quote_token: BaseToken = Field(..., alias="quoteToken")
|
69
|
+
price_native: float = Field(..., alias="priceNative")
|
70
|
+
price_usd: Optional[float] = Field(None, alias="priceUsd")
|
71
|
+
transactions: PairTransactionCounts = Field(..., alias="txns")
|
72
|
+
volume: VolumeChangePeriods
|
73
|
+
price_change: PriceChangePeriods = Field(..., alias="priceChange")
|
74
|
+
liquidity: Optional[Liquidity] = None
|
75
|
+
fdv: Optional[float] = 0.0
|
76
|
+
pair_created_at: Optional[dt.datetime] = Field(None, alias="pairCreatedAt")
|
77
|
+
|
78
|
+
|
79
|
+
class TokenLink(BaseModel):
|
80
|
+
model_config = base_config
|
81
|
+
|
82
|
+
type: Optional[str] = None
|
83
|
+
label: Optional[str] = None
|
84
|
+
url: Optional[str] = None
|
85
|
+
|
86
|
+
|
87
|
+
class TokenInfo(BaseModel):
|
88
|
+
model_config = base_config
|
89
|
+
|
90
|
+
url: str
|
91
|
+
chain_id: str = Field(..., alias="chainId")
|
92
|
+
token_address: str = Field(..., alias="tokenAddress")
|
93
|
+
amount: float = 0.0 # Not sure if this is the best logic
|
94
|
+
total_amount: float = Field(0.0, alias="totalAmount")
|
95
|
+
icon: Optional[str] = None
|
96
|
+
header: Optional[str] = None
|
97
|
+
description: Optional[str] = None
|
98
|
+
links: list[TokenLink] = []
|
99
|
+
|
100
|
+
|
101
|
+
class OrderInfo(BaseModel):
|
102
|
+
model_config = base_config
|
103
|
+
|
104
|
+
type: str
|
105
|
+
status: str
|
106
|
+
payment_timestamp: int = Field(..., alias="paymentTimestamp")
|