defistream 1.0.0__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.
- defistream/__init__.py +81 -0
- defistream/client.py +380 -0
- defistream/exceptions.py +65 -0
- defistream/models.py +174 -0
- defistream/protocols.py +364 -0
- defistream/py.typed +0 -0
- defistream/query.py +477 -0
- defistream-1.0.0.dist-info/METADATA +490 -0
- defistream-1.0.0.dist-info/RECORD +11 -0
- defistream-1.0.0.dist-info/WHEEL +4 -0
- defistream-1.0.0.dist-info/licenses/LICENSE +21 -0
defistream/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DeFiStream Python Client
|
|
3
|
+
|
|
4
|
+
Official Python client for the DeFiStream API - access historical
|
|
5
|
+
DeFi events from 45+ EVM networks.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from defistream import DeFiStream
|
|
9
|
+
>>> client = DeFiStream(api_key="dsk_...")
|
|
10
|
+
>>>
|
|
11
|
+
>>> # Builder pattern
|
|
12
|
+
>>> query = client.erc20.transfers("USDT").network("ETH").start_block(24000000).end_block(24100000)
|
|
13
|
+
>>> transfers = query.as_dict()
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Save to file
|
|
16
|
+
>>> query.to_csv_file("transfers.csv")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .client import AsyncDeFiStream, DeFiStream
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
DeFiStreamError,
|
|
23
|
+
NotFoundError,
|
|
24
|
+
QuotaExceededError,
|
|
25
|
+
RateLimitError,
|
|
26
|
+
ServerError,
|
|
27
|
+
ValidationError,
|
|
28
|
+
)
|
|
29
|
+
from .models import (
|
|
30
|
+
AAVEBorrowEvent,
|
|
31
|
+
AAVEDepositEvent,
|
|
32
|
+
AAVELiquidationEvent,
|
|
33
|
+
AAVERepayEvent,
|
|
34
|
+
AAVEWithdrawEvent,
|
|
35
|
+
ERC20ApprovalEvent,
|
|
36
|
+
ERC20TransferEvent,
|
|
37
|
+
EventBase,
|
|
38
|
+
LidoDepositEvent,
|
|
39
|
+
LidoWithdrawEvent,
|
|
40
|
+
NativeTransferEvent,
|
|
41
|
+
ResponseMetadata,
|
|
42
|
+
UniswapBurnEvent,
|
|
43
|
+
UniswapMintEvent,
|
|
44
|
+
UniswapSwapEvent,
|
|
45
|
+
)
|
|
46
|
+
from .query import AsyncQueryBuilder, QueryBuilder
|
|
47
|
+
|
|
48
|
+
__version__ = "0.1.0"
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
# Clients
|
|
52
|
+
"DeFiStream",
|
|
53
|
+
"AsyncDeFiStream",
|
|
54
|
+
# Query builders
|
|
55
|
+
"QueryBuilder",
|
|
56
|
+
"AsyncQueryBuilder",
|
|
57
|
+
# Exceptions
|
|
58
|
+
"DeFiStreamError",
|
|
59
|
+
"AuthenticationError",
|
|
60
|
+
"QuotaExceededError",
|
|
61
|
+
"RateLimitError",
|
|
62
|
+
"ValidationError",
|
|
63
|
+
"NotFoundError",
|
|
64
|
+
"ServerError",
|
|
65
|
+
# Models
|
|
66
|
+
"EventBase",
|
|
67
|
+
"ResponseMetadata",
|
|
68
|
+
"ERC20TransferEvent",
|
|
69
|
+
"ERC20ApprovalEvent",
|
|
70
|
+
"NativeTransferEvent",
|
|
71
|
+
"AAVEDepositEvent",
|
|
72
|
+
"AAVEWithdrawEvent",
|
|
73
|
+
"AAVEBorrowEvent",
|
|
74
|
+
"AAVERepayEvent",
|
|
75
|
+
"AAVELiquidationEvent",
|
|
76
|
+
"UniswapSwapEvent",
|
|
77
|
+
"UniswapMintEvent",
|
|
78
|
+
"UniswapBurnEvent",
|
|
79
|
+
"LidoDepositEvent",
|
|
80
|
+
"LidoWithdrawEvent",
|
|
81
|
+
]
|
defistream/client.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""DeFiStream API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
DeFiStreamError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
QuotaExceededError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
ServerError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
from .models import ResponseMetadata
|
|
20
|
+
from .protocols import (
|
|
21
|
+
AAVEProtocol,
|
|
22
|
+
AsyncAAVEProtocol,
|
|
23
|
+
AsyncERC20Protocol,
|
|
24
|
+
AsyncLidoProtocol,
|
|
25
|
+
AsyncNativeTokenProtocol,
|
|
26
|
+
AsyncStaderProtocol,
|
|
27
|
+
AsyncThresholdProtocol,
|
|
28
|
+
AsyncUniswapProtocol,
|
|
29
|
+
ERC20Protocol,
|
|
30
|
+
LidoProtocol,
|
|
31
|
+
NativeTokenProtocol,
|
|
32
|
+
StaderProtocol,
|
|
33
|
+
ThresholdProtocol,
|
|
34
|
+
UniswapProtocol,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
DEFAULT_BASE_URL = "https://api.defistream.dev/v1"
|
|
38
|
+
DEFAULT_TIMEOUT = 60.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BaseClient:
|
|
42
|
+
"""Base client with shared functionality."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
api_key: str | None = None,
|
|
47
|
+
base_url: str | None = None,
|
|
48
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
49
|
+
max_retries: int = 3,
|
|
50
|
+
):
|
|
51
|
+
# Read API key from argument or environment
|
|
52
|
+
self.api_key = api_key or os.environ.get("DEFISTREAM_API_KEY")
|
|
53
|
+
if not self.api_key:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"API key required. Pass api_key or set DEFISTREAM_API_KEY environment variable."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.base_url = (base_url or os.environ.get("DEFISTREAM_BASE_URL", DEFAULT_BASE_URL)).rstrip("/")
|
|
59
|
+
self.timeout = timeout
|
|
60
|
+
self.max_retries = max_retries
|
|
61
|
+
self.last_response: ResponseMetadata = ResponseMetadata()
|
|
62
|
+
|
|
63
|
+
def _get_headers(self) -> dict[str, str]:
|
|
64
|
+
"""Get request headers."""
|
|
65
|
+
return {
|
|
66
|
+
"X-API-Key": self.api_key, # type: ignore
|
|
67
|
+
"Accept": "application/json",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def _parse_response_metadata(self, headers: httpx.Headers) -> ResponseMetadata:
|
|
71
|
+
"""Parse rate limit and quota info from response headers."""
|
|
72
|
+
return ResponseMetadata(
|
|
73
|
+
rate_limit=int(headers.get("X-RateLimit-Limit", 0)) or None,
|
|
74
|
+
quota_remaining=int(headers.get("X-RateLimit-Remaining", 0)) or None,
|
|
75
|
+
request_cost=int(headers.get("X-Request-Cost", 0)) or None,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
79
|
+
"""Handle error responses."""
|
|
80
|
+
status_code = response.status_code
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
data = response.json()
|
|
84
|
+
message = data.get("error", data.get("message", response.text))
|
|
85
|
+
error_code = data.get("code", "")
|
|
86
|
+
except Exception:
|
|
87
|
+
message = response.text
|
|
88
|
+
error_code = ""
|
|
89
|
+
|
|
90
|
+
if status_code == 401:
|
|
91
|
+
raise AuthenticationError(message, status_code, response)
|
|
92
|
+
|
|
93
|
+
if status_code == 403:
|
|
94
|
+
if error_code == "quota_exceeded":
|
|
95
|
+
# Try to extract remaining quota from response
|
|
96
|
+
remaining = 0
|
|
97
|
+
try:
|
|
98
|
+
remaining = int(data.get("remaining", 0))
|
|
99
|
+
except (ValueError, TypeError):
|
|
100
|
+
pass
|
|
101
|
+
raise QuotaExceededError(message, remaining=remaining, status_code=status_code, response=response)
|
|
102
|
+
raise AuthenticationError(message, status_code, response)
|
|
103
|
+
|
|
104
|
+
if status_code == 404:
|
|
105
|
+
raise NotFoundError(message, status_code, response)
|
|
106
|
+
|
|
107
|
+
if status_code == 429:
|
|
108
|
+
retry_after = response.headers.get("Retry-After")
|
|
109
|
+
raise RateLimitError(
|
|
110
|
+
message,
|
|
111
|
+
retry_after=float(retry_after) if retry_after else None,
|
|
112
|
+
status_code=status_code,
|
|
113
|
+
response=response,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if status_code == 400:
|
|
117
|
+
raise ValidationError(message, status_code, response)
|
|
118
|
+
|
|
119
|
+
if status_code >= 500:
|
|
120
|
+
raise ServerError(message, status_code, response)
|
|
121
|
+
|
|
122
|
+
raise DeFiStreamError(message, status_code, response)
|
|
123
|
+
|
|
124
|
+
def _process_response(
|
|
125
|
+
self,
|
|
126
|
+
response: httpx.Response,
|
|
127
|
+
as_dataframe: Literal["pandas", "polars"] | None = None,
|
|
128
|
+
output_file: str | None = None,
|
|
129
|
+
) -> list[dict[str, Any]] | Any:
|
|
130
|
+
"""Process response and optionally convert to DataFrame or save to file."""
|
|
131
|
+
self.last_response = self._parse_response_metadata(response.headers)
|
|
132
|
+
|
|
133
|
+
if response.status_code >= 400:
|
|
134
|
+
self._handle_error_response(response)
|
|
135
|
+
|
|
136
|
+
content_type = response.headers.get("Content-Type", "")
|
|
137
|
+
|
|
138
|
+
# Handle file output
|
|
139
|
+
if output_file:
|
|
140
|
+
if output_file.endswith(".parquet"):
|
|
141
|
+
with open(output_file, "wb") as f:
|
|
142
|
+
f.write(response.content)
|
|
143
|
+
else:
|
|
144
|
+
with open(output_file, "w") as f:
|
|
145
|
+
f.write(response.text)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
# Handle CSV response
|
|
149
|
+
if "text/csv" in content_type:
|
|
150
|
+
csv_text = response.text
|
|
151
|
+
if as_dataframe == "pandas":
|
|
152
|
+
import io
|
|
153
|
+
import pandas as pd
|
|
154
|
+
return pd.read_csv(io.StringIO(csv_text))
|
|
155
|
+
elif as_dataframe == "polars":
|
|
156
|
+
import io
|
|
157
|
+
import polars as pl
|
|
158
|
+
return pl.read_csv(io.StringIO(csv_text))
|
|
159
|
+
return csv_text
|
|
160
|
+
|
|
161
|
+
# Handle Parquet response
|
|
162
|
+
if "application/octet-stream" in content_type or "application/parquet" in content_type:
|
|
163
|
+
if as_dataframe == "pandas":
|
|
164
|
+
import io
|
|
165
|
+
import pandas as pd
|
|
166
|
+
df = pd.read_parquet(io.BytesIO(response.content))
|
|
167
|
+
if "time" in df.columns:
|
|
168
|
+
df["time"] = pd.to_datetime(df["time"], unit="s", utc=True)
|
|
169
|
+
return df
|
|
170
|
+
elif as_dataframe == "polars":
|
|
171
|
+
import io
|
|
172
|
+
import polars as pl
|
|
173
|
+
df = pl.read_parquet(io.BytesIO(response.content))
|
|
174
|
+
if "time" in df.columns:
|
|
175
|
+
df = df.with_columns(
|
|
176
|
+
pl.from_epoch("time", time_unit="s").dt.replace_time_zone("UTC")
|
|
177
|
+
)
|
|
178
|
+
return df
|
|
179
|
+
return response.content
|
|
180
|
+
|
|
181
|
+
# Handle JSON response
|
|
182
|
+
data = response.json()
|
|
183
|
+
|
|
184
|
+
if data.get("status") == "error":
|
|
185
|
+
raise DeFiStreamError(data.get("error", "Unknown error"))
|
|
186
|
+
|
|
187
|
+
events = data.get("events", [])
|
|
188
|
+
|
|
189
|
+
if as_dataframe == "pandas":
|
|
190
|
+
import pandas as pd
|
|
191
|
+
df = pd.DataFrame(events)
|
|
192
|
+
if "time" in df.columns:
|
|
193
|
+
df["time"] = pd.to_datetime(df["time"], unit="s", utc=True)
|
|
194
|
+
return df
|
|
195
|
+
elif as_dataframe == "polars":
|
|
196
|
+
import polars as pl
|
|
197
|
+
df = pl.DataFrame(events)
|
|
198
|
+
if "time" in df.columns:
|
|
199
|
+
df = df.with_columns(
|
|
200
|
+
pl.from_epoch("time", time_unit="s").dt.replace_time_zone("UTC")
|
|
201
|
+
)
|
|
202
|
+
return df
|
|
203
|
+
|
|
204
|
+
return events
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class DeFiStream(BaseClient):
|
|
208
|
+
"""
|
|
209
|
+
Synchronous DeFiStream API client with builder pattern.
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> from defistream import DeFiStream
|
|
213
|
+
>>> client = DeFiStream(api_key="dsk_...")
|
|
214
|
+
>>>
|
|
215
|
+
>>> # Builder pattern
|
|
216
|
+
>>> query = client.erc20.transfers("USDT").network("ETH").start_block(24000000).end_block(24100000)
|
|
217
|
+
>>> df = query.as_pandas()
|
|
218
|
+
>>>
|
|
219
|
+
>>> # Or chain everything
|
|
220
|
+
>>> transfers = client.erc20.transfers("USDT").network("ETH").start_block(24000000).end_block(24100000).as_dict()
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
api_key: str | None = None,
|
|
226
|
+
base_url: str | None = None,
|
|
227
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
228
|
+
max_retries: int = 3,
|
|
229
|
+
):
|
|
230
|
+
super().__init__(api_key, base_url, timeout, max_retries)
|
|
231
|
+
self._http_client: httpx.Client | None = None
|
|
232
|
+
|
|
233
|
+
# Protocol clients
|
|
234
|
+
self.erc20 = ERC20Protocol(self)
|
|
235
|
+
self.native_token = NativeTokenProtocol(self)
|
|
236
|
+
self.aave = AAVEProtocol(self)
|
|
237
|
+
self.uniswap = UniswapProtocol(self)
|
|
238
|
+
self.lido = LidoProtocol(self)
|
|
239
|
+
self.stader = StaderProtocol(self)
|
|
240
|
+
self.threshold = ThresholdProtocol(self)
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def _client(self) -> httpx.Client:
|
|
244
|
+
"""Lazy-initialize HTTP client."""
|
|
245
|
+
if self._http_client is None:
|
|
246
|
+
self._http_client = httpx.Client(
|
|
247
|
+
base_url=self.base_url,
|
|
248
|
+
headers=self._get_headers(),
|
|
249
|
+
timeout=self.timeout,
|
|
250
|
+
)
|
|
251
|
+
return self._http_client
|
|
252
|
+
|
|
253
|
+
def close(self) -> None:
|
|
254
|
+
"""Close the HTTP client."""
|
|
255
|
+
if self._http_client is not None:
|
|
256
|
+
self._http_client.close()
|
|
257
|
+
self._http_client = None
|
|
258
|
+
|
|
259
|
+
def __enter__(self) -> "DeFiStream":
|
|
260
|
+
return self
|
|
261
|
+
|
|
262
|
+
def __exit__(self, *args: Any) -> None:
|
|
263
|
+
self.close()
|
|
264
|
+
|
|
265
|
+
def _request(
|
|
266
|
+
self,
|
|
267
|
+
method: str,
|
|
268
|
+
path: str,
|
|
269
|
+
params: dict[str, Any] | None = None,
|
|
270
|
+
as_dataframe: Literal["pandas", "polars"] | None = None,
|
|
271
|
+
output_file: str | None = None,
|
|
272
|
+
) -> list[dict[str, Any]] | Any:
|
|
273
|
+
"""Make HTTP request."""
|
|
274
|
+
if params is None:
|
|
275
|
+
params = {}
|
|
276
|
+
|
|
277
|
+
if output_file:
|
|
278
|
+
if output_file.endswith(".parquet"):
|
|
279
|
+
params["format"] = "parquet"
|
|
280
|
+
elif output_file.endswith(".csv"):
|
|
281
|
+
params["format"] = "csv"
|
|
282
|
+
|
|
283
|
+
response = self._client.request(method, path, params=params)
|
|
284
|
+
return self._process_response(response, as_dataframe, output_file)
|
|
285
|
+
|
|
286
|
+
def decoders(self) -> list[str]:
|
|
287
|
+
"""Get list of available decoders."""
|
|
288
|
+
response = self._client.get("/decoders")
|
|
289
|
+
if response.status_code >= 400:
|
|
290
|
+
self._handle_error_response(response)
|
|
291
|
+
data = response.json()
|
|
292
|
+
return data.get("decoders", [])
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class AsyncDeFiStream(BaseClient):
|
|
296
|
+
"""
|
|
297
|
+
Asynchronous DeFiStream API client with builder pattern.
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> import asyncio
|
|
301
|
+
>>> from defistream import AsyncDeFiStream
|
|
302
|
+
>>>
|
|
303
|
+
>>> async def main():
|
|
304
|
+
... async with AsyncDeFiStream(api_key="dsk_...") as client:
|
|
305
|
+
... query = client.erc20.transfers("USDT").network("ETH").start_block(24000000).end_block(24100000)
|
|
306
|
+
... df = await query.as_pandas()
|
|
307
|
+
...
|
|
308
|
+
>>> asyncio.run(main())
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
def __init__(
|
|
312
|
+
self,
|
|
313
|
+
api_key: str | None = None,
|
|
314
|
+
base_url: str | None = None,
|
|
315
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
316
|
+
max_retries: int = 3,
|
|
317
|
+
):
|
|
318
|
+
super().__init__(api_key, base_url, timeout, max_retries)
|
|
319
|
+
self._http_client: httpx.AsyncClient | None = None
|
|
320
|
+
|
|
321
|
+
# Protocol clients (async versions)
|
|
322
|
+
self.erc20 = AsyncERC20Protocol(self)
|
|
323
|
+
self.native_token = AsyncNativeTokenProtocol(self)
|
|
324
|
+
self.aave = AsyncAAVEProtocol(self)
|
|
325
|
+
self.uniswap = AsyncUniswapProtocol(self)
|
|
326
|
+
self.lido = AsyncLidoProtocol(self)
|
|
327
|
+
self.stader = AsyncStaderProtocol(self)
|
|
328
|
+
self.threshold = AsyncThresholdProtocol(self)
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def _client(self) -> httpx.AsyncClient:
|
|
332
|
+
"""Lazy-initialize async HTTP client."""
|
|
333
|
+
if self._http_client is None:
|
|
334
|
+
self._http_client = httpx.AsyncClient(
|
|
335
|
+
base_url=self.base_url,
|
|
336
|
+
headers=self._get_headers(),
|
|
337
|
+
timeout=self.timeout,
|
|
338
|
+
)
|
|
339
|
+
return self._http_client
|
|
340
|
+
|
|
341
|
+
async def close(self) -> None:
|
|
342
|
+
"""Close the async HTTP client."""
|
|
343
|
+
if self._http_client is not None:
|
|
344
|
+
await self._http_client.aclose()
|
|
345
|
+
self._http_client = None
|
|
346
|
+
|
|
347
|
+
async def __aenter__(self) -> "AsyncDeFiStream":
|
|
348
|
+
return self
|
|
349
|
+
|
|
350
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
351
|
+
await self.close()
|
|
352
|
+
|
|
353
|
+
async def _request(
|
|
354
|
+
self,
|
|
355
|
+
method: str,
|
|
356
|
+
path: str,
|
|
357
|
+
params: dict[str, Any] | None = None,
|
|
358
|
+
as_dataframe: Literal["pandas", "polars"] | None = None,
|
|
359
|
+
output_file: str | None = None,
|
|
360
|
+
) -> list[dict[str, Any]] | Any:
|
|
361
|
+
"""Make async HTTP request."""
|
|
362
|
+
if params is None:
|
|
363
|
+
params = {}
|
|
364
|
+
|
|
365
|
+
if output_file:
|
|
366
|
+
if output_file.endswith(".parquet"):
|
|
367
|
+
params["format"] = "parquet"
|
|
368
|
+
elif output_file.endswith(".csv"):
|
|
369
|
+
params["format"] = "csv"
|
|
370
|
+
|
|
371
|
+
response = await self._client.request(method, path, params=params)
|
|
372
|
+
return self._process_response(response, as_dataframe, output_file)
|
|
373
|
+
|
|
374
|
+
async def decoders(self) -> list[str]:
|
|
375
|
+
"""Get list of available decoders."""
|
|
376
|
+
response = await self._client.get("/decoders")
|
|
377
|
+
if response.status_code >= 400:
|
|
378
|
+
self._handle_error_response(response)
|
|
379
|
+
data = response.json()
|
|
380
|
+
return data.get("decoders", [])
|
defistream/exceptions.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""DeFiStream API exceptions."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DeFiStreamError(Exception):
|
|
7
|
+
"""Base exception for DeFiStream API errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, status_code: int | None = None, response: Any = None):
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.message = message
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
self.response = response
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthenticationError(DeFiStreamError):
|
|
17
|
+
"""Raised when API key is invalid or missing."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class QuotaExceededError(DeFiStreamError):
|
|
23
|
+
"""Raised when account quota is exceeded."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
message: str,
|
|
28
|
+
remaining: int = 0,
|
|
29
|
+
status_code: int | None = None,
|
|
30
|
+
response: Any = None,
|
|
31
|
+
):
|
|
32
|
+
super().__init__(message, status_code, response)
|
|
33
|
+
self.remaining = remaining
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RateLimitError(DeFiStreamError):
|
|
37
|
+
"""Raised when rate limit is exceeded."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
message: str,
|
|
42
|
+
retry_after: float | None = None,
|
|
43
|
+
status_code: int | None = None,
|
|
44
|
+
response: Any = None,
|
|
45
|
+
):
|
|
46
|
+
super().__init__(message, status_code, response)
|
|
47
|
+
self.retry_after = retry_after
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ValidationError(DeFiStreamError):
|
|
51
|
+
"""Raised when request parameters are invalid."""
|
|
52
|
+
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class NotFoundError(DeFiStreamError):
|
|
57
|
+
"""Raised when a resource is not found."""
|
|
58
|
+
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ServerError(DeFiStreamError):
|
|
63
|
+
"""Raised when the server returns a 5xx error."""
|
|
64
|
+
|
|
65
|
+
pass
|
defistream/models.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Pydantic models for DeFiStream API responses."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ResponseMetadata(BaseModel):
|
|
8
|
+
"""Metadata from API response headers."""
|
|
9
|
+
|
|
10
|
+
rate_limit: int | None = None
|
|
11
|
+
quota_remaining: int | None = None
|
|
12
|
+
request_cost: int | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EventBase(BaseModel):
|
|
16
|
+
"""Base model for all events."""
|
|
17
|
+
|
|
18
|
+
block_number: int
|
|
19
|
+
time: str | None = None
|
|
20
|
+
# Verbose fields (only present when verbose=true)
|
|
21
|
+
name: str | None = None
|
|
22
|
+
network: str | None = None
|
|
23
|
+
tx_id: str | None = None
|
|
24
|
+
tx_hash: str | None = None
|
|
25
|
+
log_index: int | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ERC20TransferEvent(EventBase):
|
|
29
|
+
"""ERC20 Transfer event."""
|
|
30
|
+
|
|
31
|
+
sender: str = Field(alias="from_address", default="")
|
|
32
|
+
receiver: str = Field(alias="to_address", default="")
|
|
33
|
+
amount: float = 0.0
|
|
34
|
+
token_address: str | None = None
|
|
35
|
+
token_symbol: str | None = None
|
|
36
|
+
|
|
37
|
+
model_config = {"populate_by_name": True}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ERC20ApprovalEvent(EventBase):
|
|
41
|
+
"""ERC20 Approval event."""
|
|
42
|
+
|
|
43
|
+
owner: str = ""
|
|
44
|
+
spender: str = ""
|
|
45
|
+
amount: float = 0.0
|
|
46
|
+
token_address: str | None = None
|
|
47
|
+
token_symbol: str | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NativeTransferEvent(EventBase):
|
|
51
|
+
"""Native token (ETH/MATIC/etc) transfer event."""
|
|
52
|
+
|
|
53
|
+
sender: str = ""
|
|
54
|
+
receiver: str = ""
|
|
55
|
+
amount: float = 0.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AAVEDepositEvent(EventBase):
|
|
59
|
+
"""AAVE V3 Supply/Deposit event."""
|
|
60
|
+
|
|
61
|
+
user: str = ""
|
|
62
|
+
reserve: str = ""
|
|
63
|
+
amount: float = 0.0
|
|
64
|
+
on_behalf_of: str | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AAVEWithdrawEvent(EventBase):
|
|
68
|
+
"""AAVE V3 Withdraw event."""
|
|
69
|
+
|
|
70
|
+
user: str = ""
|
|
71
|
+
reserve: str = ""
|
|
72
|
+
amount: float = 0.0
|
|
73
|
+
to: str | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AAVEBorrowEvent(EventBase):
|
|
77
|
+
"""AAVE V3 Borrow event."""
|
|
78
|
+
|
|
79
|
+
user: str = ""
|
|
80
|
+
reserve: str = ""
|
|
81
|
+
amount: float = 0.0
|
|
82
|
+
interest_rate_mode: int | None = None
|
|
83
|
+
borrow_rate: float | None = None
|
|
84
|
+
on_behalf_of: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AAVERepayEvent(EventBase):
|
|
88
|
+
"""AAVE V3 Repay event."""
|
|
89
|
+
|
|
90
|
+
user: str = ""
|
|
91
|
+
reserve: str = ""
|
|
92
|
+
amount: float = 0.0
|
|
93
|
+
repayer: str | None = None
|
|
94
|
+
use_a_tokens: bool | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AAVELiquidationEvent(EventBase):
|
|
98
|
+
"""AAVE V3 Liquidation event."""
|
|
99
|
+
|
|
100
|
+
liquidator: str = ""
|
|
101
|
+
user: str = ""
|
|
102
|
+
collateral_asset: str = ""
|
|
103
|
+
debt_asset: str = ""
|
|
104
|
+
debt_to_cover: float = 0.0
|
|
105
|
+
liquidated_collateral_amount: float = 0.0
|
|
106
|
+
receive_a_token: bool | None = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class UniswapSwapEvent(EventBase):
|
|
110
|
+
"""Uniswap V3 Swap event."""
|
|
111
|
+
|
|
112
|
+
pool: str = ""
|
|
113
|
+
sender: str = ""
|
|
114
|
+
recipient: str = ""
|
|
115
|
+
amount0: float = 0.0
|
|
116
|
+
amount1: float = 0.0
|
|
117
|
+
sqrt_price_x96: int | None = None
|
|
118
|
+
liquidity: int | None = None
|
|
119
|
+
tick: int | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class UniswapMintEvent(EventBase):
|
|
123
|
+
"""Uniswap V3 Mint (add liquidity) event."""
|
|
124
|
+
|
|
125
|
+
pool: str = ""
|
|
126
|
+
owner: str = ""
|
|
127
|
+
tick_lower: int = 0
|
|
128
|
+
tick_upper: int = 0
|
|
129
|
+
amount: int = 0
|
|
130
|
+
amount0: float = 0.0
|
|
131
|
+
amount1: float = 0.0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class UniswapBurnEvent(EventBase):
|
|
135
|
+
"""Uniswap V3 Burn (remove liquidity) event."""
|
|
136
|
+
|
|
137
|
+
pool: str = ""
|
|
138
|
+
owner: str = ""
|
|
139
|
+
tick_lower: int = 0
|
|
140
|
+
tick_upper: int = 0
|
|
141
|
+
amount: int = 0
|
|
142
|
+
amount0: float = 0.0
|
|
143
|
+
amount1: float = 0.0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class LidoDepositEvent(EventBase):
|
|
147
|
+
"""Lido stETH deposit event."""
|
|
148
|
+
|
|
149
|
+
sender: str = ""
|
|
150
|
+
amount: float = 0.0
|
|
151
|
+
shares: float = 0.0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class LidoWithdrawEvent(EventBase):
|
|
155
|
+
"""Lido stETH withdrawal event."""
|
|
156
|
+
|
|
157
|
+
owner: str = ""
|
|
158
|
+
request_id: int = 0
|
|
159
|
+
amount: float = 0.0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class EventsResponse(BaseModel):
|
|
163
|
+
"""Standard events API response."""
|
|
164
|
+
|
|
165
|
+
status: Literal["success", "error"]
|
|
166
|
+
events: list[dict[str, Any]] = []
|
|
167
|
+
count: int = 0
|
|
168
|
+
error: str | None = None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class DecodersResponse(BaseModel):
|
|
172
|
+
"""Response from /decoders endpoint."""
|
|
173
|
+
|
|
174
|
+
decoders: list[str] = []
|