defistream 1.0.6__tar.gz → 1.1.0__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.
- defistream-1.1.0/.claude/settings.local.json +16 -0
- {defistream-1.0.6 → defistream-1.1.0}/PKG-INFO +59 -1
- {defistream-1.0.6 → defistream-1.1.0}/README.md +58 -0
- {defistream-1.0.6 → defistream-1.1.0}/pyproject.toml +1 -1
- {defistream-1.0.6 → defistream-1.1.0}/src/defistream/__init__.py +1 -1
- {defistream-1.0.6 → defistream-1.1.0}/src/defistream/query.py +158 -24
- {defistream-1.0.6 → defistream-1.1.0}/tests/test_client.py +332 -0
- {defistream-1.0.6 → defistream-1.1.0}/.gitignore +0 -0
- {defistream-1.0.6 → defistream-1.1.0}/LICENSE +0 -0
- {defistream-1.0.6 → defistream-1.1.0}/src/defistream/client.py +0 -0
- {defistream-1.0.6 → defistream-1.1.0}/src/defistream/exceptions.py +0 -0
- {defistream-1.0.6 → defistream-1.1.0}/src/defistream/models.py +0 -0
- {defistream-1.0.6 → defistream-1.1.0}/src/defistream/protocols.py +0 -0
- {defistream-1.0.6 → defistream-1.1.0}/src/defistream/py.typed +0 -0
- {defistream-1.0.6 → defistream-1.1.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(git log:*)",
|
|
5
|
+
"Bash(python -m pytest:*)",
|
|
6
|
+
"Bash(python3 -m pytest:*)",
|
|
7
|
+
"Bash(git add:*)",
|
|
8
|
+
"Bash(git commit -m \"$\\(cat <<''EOF''\nAdd label/category filters and multi-value support to query builders\n\nAdd involving, involving_label, involving_category, sender_label,\nsender_category, receiver_label, receiver_category methods to both\nQueryBuilder and AsyncQueryBuilder. Update sender/receiver/from_address/\nto_address to accept varargs for multi-value filtering. Add SQL safety\nvalidation for label/category params and mutual exclusivity enforcement\nbetween involving* and sender*/receiver* filter slots.\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
9
|
+
"Bash(python3 -m build:*)",
|
|
10
|
+
"Bash(python3:*)",
|
|
11
|
+
"Bash(pip3 install:*)",
|
|
12
|
+
"Bash(/home/mvp/Running/Horatio_Chain_Scan/DeFiStream/python-client/.venv/bin/pip install:*)",
|
|
13
|
+
"Bash(/home/mvp/Running/Horatio_Chain_Scan/DeFiStream/python-client/.venv/bin/python:*)"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: defistream
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Python client for the DeFiStream API
|
|
5
5
|
Project-URL: Homepage, https://defistream.dev
|
|
6
6
|
Project-URL: Documentation, https://docs.defistream.dev
|
|
@@ -204,6 +204,47 @@ df = (
|
|
|
204
204
|
)
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
+
### Label & Category Filters
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
# Get USDT transfers involving Binance wallets
|
|
211
|
+
df = (
|
|
212
|
+
client.erc20.transfers("USDT")
|
|
213
|
+
.network("ETH")
|
|
214
|
+
.block_range(21000000, 21010000)
|
|
215
|
+
.involving_label("Binance")
|
|
216
|
+
.as_df()
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Get USDT transfers FROM exchanges TO DeFi protocols
|
|
220
|
+
df = (
|
|
221
|
+
client.erc20.transfers("USDT")
|
|
222
|
+
.network("ETH")
|
|
223
|
+
.block_range(21000000, 21010000)
|
|
224
|
+
.sender_category("exchange")
|
|
225
|
+
.receiver_category("defi")
|
|
226
|
+
.as_df()
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Get AAVE deposits involving exchange addresses
|
|
230
|
+
df = (
|
|
231
|
+
client.aave.deposits()
|
|
232
|
+
.network("ETH")
|
|
233
|
+
.block_range(21000000, 21010000)
|
|
234
|
+
.involving_category("exchange")
|
|
235
|
+
.as_df()
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Get native ETH transfers FROM Binance or Coinbase (multi-value)
|
|
239
|
+
df = (
|
|
240
|
+
client.native_token.transfers()
|
|
241
|
+
.network("ETH")
|
|
242
|
+
.block_range(21000000, 21010000)
|
|
243
|
+
.sender_label("Binance,Coinbase")
|
|
244
|
+
.as_df()
|
|
245
|
+
)
|
|
246
|
+
```
|
|
247
|
+
|
|
207
248
|
### Verbose Mode
|
|
208
249
|
|
|
209
250
|
By default, responses omit metadata fields to reduce payload size. Use `.verbose()` to include all fields:
|
|
@@ -423,6 +464,23 @@ print(f"Request cost: {client.last_response.request_cost}")
|
|
|
423
464
|
| `.symbol1(sym)` | Uniswap | Second token symbol (required) |
|
|
424
465
|
| `.fee(tier)` | Uniswap | Fee tier: 100, 500, 3000, 10000 (required) |
|
|
425
466
|
|
|
467
|
+
### Address Label & Category Filters
|
|
468
|
+
|
|
469
|
+
Filter events by entity names or categories using the labels database. Available on all protocols.
|
|
470
|
+
|
|
471
|
+
| Method | Protocols | Description |
|
|
472
|
+
|--------|-----------|-------------|
|
|
473
|
+
| `.involving_label(label)` | All | Filter where any involved address matches a label substring (e.g., "Binance") |
|
|
474
|
+
| `.involving_category(cat)` | All | Filter where any involved address matches a category (e.g., "exchange") |
|
|
475
|
+
| `.sender_label(label)` | ERC20, Native | Filter sender by label substring |
|
|
476
|
+
| `.sender_category(cat)` | ERC20, Native | Filter sender by category |
|
|
477
|
+
| `.receiver_label(label)` | ERC20, Native | Filter receiver by label substring |
|
|
478
|
+
| `.receiver_category(cat)` | ERC20, Native | Filter receiver by category |
|
|
479
|
+
|
|
480
|
+
**Multi-value support:** Comma-separated values are accepted (e.g., `"Binance,Coinbase"`).
|
|
481
|
+
|
|
482
|
+
**Mutual exclusivity:** Within each slot (involving/sender/receiver), only one of address/label/category can be set. `involving*` filters cannot be combined with `sender*`/`receiver*` filters.
|
|
483
|
+
|
|
426
484
|
### Terminal Methods
|
|
427
485
|
|
|
428
486
|
| Method | Description |
|
|
@@ -170,6 +170,47 @@ df = (
|
|
|
170
170
|
)
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
### Label & Category Filters
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
# Get USDT transfers involving Binance wallets
|
|
177
|
+
df = (
|
|
178
|
+
client.erc20.transfers("USDT")
|
|
179
|
+
.network("ETH")
|
|
180
|
+
.block_range(21000000, 21010000)
|
|
181
|
+
.involving_label("Binance")
|
|
182
|
+
.as_df()
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Get USDT transfers FROM exchanges TO DeFi protocols
|
|
186
|
+
df = (
|
|
187
|
+
client.erc20.transfers("USDT")
|
|
188
|
+
.network("ETH")
|
|
189
|
+
.block_range(21000000, 21010000)
|
|
190
|
+
.sender_category("exchange")
|
|
191
|
+
.receiver_category("defi")
|
|
192
|
+
.as_df()
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Get AAVE deposits involving exchange addresses
|
|
196
|
+
df = (
|
|
197
|
+
client.aave.deposits()
|
|
198
|
+
.network("ETH")
|
|
199
|
+
.block_range(21000000, 21010000)
|
|
200
|
+
.involving_category("exchange")
|
|
201
|
+
.as_df()
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Get native ETH transfers FROM Binance or Coinbase (multi-value)
|
|
205
|
+
df = (
|
|
206
|
+
client.native_token.transfers()
|
|
207
|
+
.network("ETH")
|
|
208
|
+
.block_range(21000000, 21010000)
|
|
209
|
+
.sender_label("Binance,Coinbase")
|
|
210
|
+
.as_df()
|
|
211
|
+
)
|
|
212
|
+
```
|
|
213
|
+
|
|
173
214
|
### Verbose Mode
|
|
174
215
|
|
|
175
216
|
By default, responses omit metadata fields to reduce payload size. Use `.verbose()` to include all fields:
|
|
@@ -389,6 +430,23 @@ print(f"Request cost: {client.last_response.request_cost}")
|
|
|
389
430
|
| `.symbol1(sym)` | Uniswap | Second token symbol (required) |
|
|
390
431
|
| `.fee(tier)` | Uniswap | Fee tier: 100, 500, 3000, 10000 (required) |
|
|
391
432
|
|
|
433
|
+
### Address Label & Category Filters
|
|
434
|
+
|
|
435
|
+
Filter events by entity names or categories using the labels database. Available on all protocols.
|
|
436
|
+
|
|
437
|
+
| Method | Protocols | Description |
|
|
438
|
+
|--------|-----------|-------------|
|
|
439
|
+
| `.involving_label(label)` | All | Filter where any involved address matches a label substring (e.g., "Binance") |
|
|
440
|
+
| `.involving_category(cat)` | All | Filter where any involved address matches a category (e.g., "exchange") |
|
|
441
|
+
| `.sender_label(label)` | ERC20, Native | Filter sender by label substring |
|
|
442
|
+
| `.sender_category(cat)` | ERC20, Native | Filter sender by category |
|
|
443
|
+
| `.receiver_label(label)` | ERC20, Native | Filter receiver by label substring |
|
|
444
|
+
| `.receiver_category(cat)` | ERC20, Native | Filter receiver by category |
|
|
445
|
+
|
|
446
|
+
**Multi-value support:** Comma-separated values are accepted (e.g., `"Binance,Coinbase"`).
|
|
447
|
+
|
|
448
|
+
**Mutual exclusivity:** Within each slot (involving/sender/receiver), only one of address/label/category can be set. `involving*` filters cannot be combined with `sender*`/`receiver*` filters.
|
|
449
|
+
|
|
392
450
|
### Terminal Methods
|
|
393
451
|
|
|
394
452
|
| Method | Description |
|
|
@@ -4,10 +4,84 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
|
+
from .exceptions import ValidationError
|
|
8
|
+
|
|
7
9
|
if TYPE_CHECKING:
|
|
8
10
|
from .client import BaseClient
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
# Label/category parameter names that need SQL safety checks
|
|
14
|
+
_LABEL_CATEGORY_PARAMS = frozenset({
|
|
15
|
+
"involving_label", "involving_category",
|
|
16
|
+
"sender_label", "sender_category",
|
|
17
|
+
"receiver_label", "receiver_category",
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
# Mutual exclusivity groups: within each slot, only one key is allowed
|
|
21
|
+
_INVOLVING_SLOT = ("involving", "involving_label", "involving_category")
|
|
22
|
+
_SENDER_SLOT = ("sender", "sender_label", "sender_category")
|
|
23
|
+
_RECEIVER_SLOT = ("receiver", "receiver_label", "receiver_category")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _normalize_multi(*args: str) -> str:
|
|
27
|
+
"""Join varargs into a comma-separated string.
|
|
28
|
+
|
|
29
|
+
Accepts one or more strings. If a single pre-joined string is passed
|
|
30
|
+
(e.g. "a,b") it passes through unchanged.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If no arguments are provided.
|
|
34
|
+
"""
|
|
35
|
+
if not args:
|
|
36
|
+
raise ValueError("At least one value is required")
|
|
37
|
+
return ",".join(args)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _validate_sql_safety(params: dict[str, Any]) -> None:
|
|
41
|
+
"""Reject label/category values containing quotes or backslashes.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValidationError: If any label/category value contains ' or \\.
|
|
45
|
+
"""
|
|
46
|
+
for key in _LABEL_CATEGORY_PARAMS:
|
|
47
|
+
value = params.get(key)
|
|
48
|
+
if value is not None and isinstance(value, str):
|
|
49
|
+
if "'" in value or "\\" in value:
|
|
50
|
+
raise ValidationError(
|
|
51
|
+
f"Parameter '{key}' contains unsafe characters (single quote or backslash)"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _validate_mutual_exclusivity(params: dict[str, Any]) -> None:
|
|
56
|
+
"""Enforce mutual exclusivity between filter slots.
|
|
57
|
+
|
|
58
|
+
Within each slot (involving, sender, receiver) only one key is allowed.
|
|
59
|
+
Cross-slot: involving* cannot combine with sender* or receiver*.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValidationError: If conflicting parameters are present.
|
|
63
|
+
"""
|
|
64
|
+
def _check_slot(slot: tuple[str, ...], slot_name: str) -> str | None:
|
|
65
|
+
present = [k for k in slot if k in params]
|
|
66
|
+
if len(present) > 1:
|
|
67
|
+
raise ValidationError(
|
|
68
|
+
f"Cannot combine {present[0]} with {present[1]} — "
|
|
69
|
+
f"pick one {slot_name} filter"
|
|
70
|
+
)
|
|
71
|
+
return present[0] if present else None
|
|
72
|
+
|
|
73
|
+
involving_key = _check_slot(_INVOLVING_SLOT, "involving")
|
|
74
|
+
sender_key = _check_slot(_SENDER_SLOT, "sender")
|
|
75
|
+
receiver_key = _check_slot(_RECEIVER_SLOT, "receiver")
|
|
76
|
+
|
|
77
|
+
if involving_key and (sender_key or receiver_key):
|
|
78
|
+
other = sender_key or receiver_key
|
|
79
|
+
raise ValidationError(
|
|
80
|
+
f"Cannot combine {involving_key} with {other} — "
|
|
81
|
+
f"use involving* or sender*/receiver*, not both"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
11
85
|
class QueryBuilder:
|
|
12
86
|
"""
|
|
13
87
|
Builder for constructing and executing DeFiStream API queries.
|
|
@@ -78,21 +152,49 @@ class QueryBuilder:
|
|
|
78
152
|
return self._copy_with(since=start, until=end)
|
|
79
153
|
|
|
80
154
|
# ERC20 and Native Token filters
|
|
81
|
-
def sender(self,
|
|
82
|
-
"""Filter by sender address (ERC20, Native Token)."""
|
|
83
|
-
return self._copy_with(sender=
|
|
155
|
+
def sender(self, *addresses: str) -> "QueryBuilder":
|
|
156
|
+
"""Filter by sender address (ERC20, Native Token). Accepts multiple addresses."""
|
|
157
|
+
return self._copy_with(sender=_normalize_multi(*addresses))
|
|
158
|
+
|
|
159
|
+
def receiver(self, *addresses: str) -> "QueryBuilder":
|
|
160
|
+
"""Filter by receiver address (ERC20, Native Token). Accepts multiple addresses."""
|
|
161
|
+
return self._copy_with(receiver=_normalize_multi(*addresses))
|
|
162
|
+
|
|
163
|
+
def from_address(self, *addresses: str) -> "QueryBuilder":
|
|
164
|
+
"""Filter by sender address (alias for sender). Accepts multiple addresses."""
|
|
165
|
+
return self.sender(*addresses)
|
|
166
|
+
|
|
167
|
+
def to_address(self, *addresses: str) -> "QueryBuilder":
|
|
168
|
+
"""Filter by receiver address (alias for receiver). Accepts multiple addresses."""
|
|
169
|
+
return self.receiver(*addresses)
|
|
170
|
+
|
|
171
|
+
def involving(self, *addresses: str) -> "QueryBuilder":
|
|
172
|
+
"""Filter by any involved address (all protocols). Accepts multiple addresses."""
|
|
173
|
+
return self._copy_with(involving=_normalize_multi(*addresses))
|
|
174
|
+
|
|
175
|
+
def involving_label(self, *labels: str) -> "QueryBuilder":
|
|
176
|
+
"""Filter by label on any involved address (all protocols)."""
|
|
177
|
+
return self._copy_with(involving_label=_normalize_multi(*labels))
|
|
178
|
+
|
|
179
|
+
def involving_category(self, *categories: str) -> "QueryBuilder":
|
|
180
|
+
"""Filter by category on any involved address (all protocols)."""
|
|
181
|
+
return self._copy_with(involving_category=_normalize_multi(*categories))
|
|
84
182
|
|
|
85
|
-
def
|
|
86
|
-
"""Filter by
|
|
87
|
-
return self._copy_with(
|
|
183
|
+
def sender_label(self, *labels: str) -> "QueryBuilder":
|
|
184
|
+
"""Filter sender by label (ERC20, Native Token)."""
|
|
185
|
+
return self._copy_with(sender_label=_normalize_multi(*labels))
|
|
88
186
|
|
|
89
|
-
def
|
|
90
|
-
"""Filter by
|
|
91
|
-
return self.
|
|
187
|
+
def sender_category(self, *categories: str) -> "QueryBuilder":
|
|
188
|
+
"""Filter sender by category (ERC20, Native Token)."""
|
|
189
|
+
return self._copy_with(sender_category=_normalize_multi(*categories))
|
|
92
190
|
|
|
93
|
-
def
|
|
94
|
-
"""Filter by
|
|
95
|
-
return self.
|
|
191
|
+
def receiver_label(self, *labels: str) -> "QueryBuilder":
|
|
192
|
+
"""Filter receiver by label (ERC20, Native Token)."""
|
|
193
|
+
return self._copy_with(receiver_label=_normalize_multi(*labels))
|
|
194
|
+
|
|
195
|
+
def receiver_category(self, *categories: str) -> "QueryBuilder":
|
|
196
|
+
"""Filter receiver by category (ERC20, Native Token)."""
|
|
197
|
+
return self._copy_with(receiver_category=_normalize_multi(*categories))
|
|
96
198
|
|
|
97
199
|
def min_amount(self, amount: float) -> "QueryBuilder":
|
|
98
200
|
"""Filter by minimum amount (ERC20, Native Token)."""
|
|
@@ -134,6 +236,8 @@ class QueryBuilder:
|
|
|
134
236
|
def _build_params(self) -> dict[str, Any]:
|
|
135
237
|
"""Build the final query parameters."""
|
|
136
238
|
params = self._params.copy()
|
|
239
|
+
_validate_sql_safety(params)
|
|
240
|
+
_validate_mutual_exclusivity(params)
|
|
137
241
|
if self._verbose:
|
|
138
242
|
params["verbose"] = "true"
|
|
139
243
|
return params
|
|
@@ -294,21 +398,49 @@ class AsyncQueryBuilder:
|
|
|
294
398
|
return self._copy_with(since=start, until=end)
|
|
295
399
|
|
|
296
400
|
# ERC20 and Native Token filters
|
|
297
|
-
def sender(self,
|
|
298
|
-
"""Filter by sender address (ERC20, Native Token)."""
|
|
299
|
-
return self._copy_with(sender=
|
|
401
|
+
def sender(self, *addresses: str) -> "AsyncQueryBuilder":
|
|
402
|
+
"""Filter by sender address (ERC20, Native Token). Accepts multiple addresses."""
|
|
403
|
+
return self._copy_with(sender=_normalize_multi(*addresses))
|
|
404
|
+
|
|
405
|
+
def receiver(self, *addresses: str) -> "AsyncQueryBuilder":
|
|
406
|
+
"""Filter by receiver address (ERC20, Native Token). Accepts multiple addresses."""
|
|
407
|
+
return self._copy_with(receiver=_normalize_multi(*addresses))
|
|
408
|
+
|
|
409
|
+
def from_address(self, *addresses: str) -> "AsyncQueryBuilder":
|
|
410
|
+
"""Filter by sender address (alias for sender). Accepts multiple addresses."""
|
|
411
|
+
return self.sender(*addresses)
|
|
412
|
+
|
|
413
|
+
def to_address(self, *addresses: str) -> "AsyncQueryBuilder":
|
|
414
|
+
"""Filter by receiver address (alias for receiver). Accepts multiple addresses."""
|
|
415
|
+
return self.receiver(*addresses)
|
|
416
|
+
|
|
417
|
+
def involving(self, *addresses: str) -> "AsyncQueryBuilder":
|
|
418
|
+
"""Filter by any involved address (all protocols). Accepts multiple addresses."""
|
|
419
|
+
return self._copy_with(involving=_normalize_multi(*addresses))
|
|
420
|
+
|
|
421
|
+
def involving_label(self, *labels: str) -> "AsyncQueryBuilder":
|
|
422
|
+
"""Filter by label on any involved address (all protocols)."""
|
|
423
|
+
return self._copy_with(involving_label=_normalize_multi(*labels))
|
|
424
|
+
|
|
425
|
+
def involving_category(self, *categories: str) -> "AsyncQueryBuilder":
|
|
426
|
+
"""Filter by category on any involved address (all protocols)."""
|
|
427
|
+
return self._copy_with(involving_category=_normalize_multi(*categories))
|
|
428
|
+
|
|
429
|
+
def sender_label(self, *labels: str) -> "AsyncQueryBuilder":
|
|
430
|
+
"""Filter sender by label (ERC20, Native Token)."""
|
|
431
|
+
return self._copy_with(sender_label=_normalize_multi(*labels))
|
|
300
432
|
|
|
301
|
-
def
|
|
302
|
-
"""Filter by
|
|
303
|
-
return self._copy_with(
|
|
433
|
+
def sender_category(self, *categories: str) -> "AsyncQueryBuilder":
|
|
434
|
+
"""Filter sender by category (ERC20, Native Token)."""
|
|
435
|
+
return self._copy_with(sender_category=_normalize_multi(*categories))
|
|
304
436
|
|
|
305
|
-
def
|
|
306
|
-
"""Filter by
|
|
307
|
-
return self.
|
|
437
|
+
def receiver_label(self, *labels: str) -> "AsyncQueryBuilder":
|
|
438
|
+
"""Filter receiver by label (ERC20, Native Token)."""
|
|
439
|
+
return self._copy_with(receiver_label=_normalize_multi(*labels))
|
|
308
440
|
|
|
309
|
-
def
|
|
310
|
-
"""Filter by
|
|
311
|
-
return self.
|
|
441
|
+
def receiver_category(self, *categories: str) -> "AsyncQueryBuilder":
|
|
442
|
+
"""Filter receiver by category (ERC20, Native Token)."""
|
|
443
|
+
return self._copy_with(receiver_category=_normalize_multi(*categories))
|
|
312
444
|
|
|
313
445
|
def min_amount(self, amount: float) -> "AsyncQueryBuilder":
|
|
314
446
|
"""Filter by minimum amount (ERC20, Native Token)."""
|
|
@@ -350,6 +482,8 @@ class AsyncQueryBuilder:
|
|
|
350
482
|
def _build_params(self) -> dict[str, Any]:
|
|
351
483
|
"""Build the final query parameters."""
|
|
352
484
|
params = self._params.copy()
|
|
485
|
+
_validate_sql_safety(params)
|
|
486
|
+
_validate_mutual_exclusivity(params)
|
|
353
487
|
if self._verbose:
|
|
354
488
|
params["verbose"] = "true"
|
|
355
489
|
return params
|
|
@@ -400,3 +400,335 @@ class TestAsyncTerminalMethods:
|
|
|
400
400
|
client = AsyncDeFiStream(api_key="dsk_test")
|
|
401
401
|
query = client.erc20.transfers()
|
|
402
402
|
assert hasattr(query, "as_file")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class TestLabelCategoryMethods:
|
|
406
|
+
"""Test label and category filter methods."""
|
|
407
|
+
|
|
408
|
+
def test_involving_sets_param(self):
|
|
409
|
+
"""involving() should set involving param."""
|
|
410
|
+
client = DeFiStream(api_key="dsk_test")
|
|
411
|
+
query = client.erc20.transfers().involving("0xA")
|
|
412
|
+
assert query._params["involving"] == "0xA"
|
|
413
|
+
|
|
414
|
+
def test_involving_label_sets_param(self):
|
|
415
|
+
"""involving_label() should set involving_label param."""
|
|
416
|
+
client = DeFiStream(api_key="dsk_test")
|
|
417
|
+
query = client.erc20.transfers().involving_label("Binance")
|
|
418
|
+
assert query._params["involving_label"] == "Binance"
|
|
419
|
+
|
|
420
|
+
def test_involving_category_sets_param(self):
|
|
421
|
+
"""involving_category() should set involving_category param."""
|
|
422
|
+
client = DeFiStream(api_key="dsk_test")
|
|
423
|
+
query = client.erc20.transfers().involving_category("CEX")
|
|
424
|
+
assert query._params["involving_category"] == "CEX"
|
|
425
|
+
|
|
426
|
+
def test_sender_label_sets_param(self):
|
|
427
|
+
"""sender_label() should set sender_label param."""
|
|
428
|
+
client = DeFiStream(api_key="dsk_test")
|
|
429
|
+
query = client.erc20.transfers().sender_label("Binance")
|
|
430
|
+
assert query._params["sender_label"] == "Binance"
|
|
431
|
+
|
|
432
|
+
def test_sender_category_sets_param(self):
|
|
433
|
+
"""sender_category() should set sender_category param."""
|
|
434
|
+
client = DeFiStream(api_key="dsk_test")
|
|
435
|
+
query = client.erc20.transfers().sender_category("CEX")
|
|
436
|
+
assert query._params["sender_category"] == "CEX"
|
|
437
|
+
|
|
438
|
+
def test_receiver_label_sets_param(self):
|
|
439
|
+
"""receiver_label() should set receiver_label param."""
|
|
440
|
+
client = DeFiStream(api_key="dsk_test")
|
|
441
|
+
query = client.erc20.transfers().receiver_label("Coinbase")
|
|
442
|
+
assert query._params["receiver_label"] == "Coinbase"
|
|
443
|
+
|
|
444
|
+
def test_receiver_category_sets_param(self):
|
|
445
|
+
"""receiver_category() should set receiver_category param."""
|
|
446
|
+
client = DeFiStream(api_key="dsk_test")
|
|
447
|
+
query = client.erc20.transfers().receiver_category("DEX")
|
|
448
|
+
assert query._params["receiver_category"] == "DEX"
|
|
449
|
+
|
|
450
|
+
def test_label_chaining(self):
|
|
451
|
+
"""Label methods should chain with other methods."""
|
|
452
|
+
client = DeFiStream(api_key="dsk_test")
|
|
453
|
+
query = (
|
|
454
|
+
client.erc20.transfers("USDT")
|
|
455
|
+
.network("ETH")
|
|
456
|
+
.sender_label("Binance")
|
|
457
|
+
.block_range(21000000, 21010000)
|
|
458
|
+
)
|
|
459
|
+
assert query._params["sender_label"] == "Binance"
|
|
460
|
+
assert query._params["network"] == "ETH"
|
|
461
|
+
assert query._params["block_start"] == 21000000
|
|
462
|
+
|
|
463
|
+
def test_label_immutability(self):
|
|
464
|
+
"""Label methods should return new instances (immutability)."""
|
|
465
|
+
client = DeFiStream(api_key="dsk_test")
|
|
466
|
+
query1 = client.erc20.transfers()
|
|
467
|
+
query2 = query1.sender_label("Binance")
|
|
468
|
+
assert query1 is not query2
|
|
469
|
+
assert "sender_label" not in query1._params
|
|
470
|
+
assert query2._params["sender_label"] == "Binance"
|
|
471
|
+
|
|
472
|
+
def test_sender_and_receiver_labels_together(self):
|
|
473
|
+
"""sender_label and receiver_label should work together."""
|
|
474
|
+
client = DeFiStream(api_key="dsk_test")
|
|
475
|
+
query = client.erc20.transfers().sender_label("Binance").receiver_label("Coinbase")
|
|
476
|
+
params = query._build_params()
|
|
477
|
+
assert params["sender_label"] == "Binance"
|
|
478
|
+
assert params["receiver_label"] == "Coinbase"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class TestMultiValueSupport:
|
|
482
|
+
"""Test multi-value support for address and label/category filters."""
|
|
483
|
+
|
|
484
|
+
def test_sender_varargs_join(self):
|
|
485
|
+
"""sender() with multiple args should join with comma."""
|
|
486
|
+
client = DeFiStream(api_key="dsk_test")
|
|
487
|
+
query = client.erc20.transfers().sender("0xA", "0xB", "0xC")
|
|
488
|
+
assert query._params["sender"] == "0xA,0xB,0xC"
|
|
489
|
+
|
|
490
|
+
def test_receiver_varargs_join(self):
|
|
491
|
+
"""receiver() with multiple args should join with comma."""
|
|
492
|
+
client = DeFiStream(api_key="dsk_test")
|
|
493
|
+
query = client.erc20.transfers().receiver("0xA", "0xB")
|
|
494
|
+
assert query._params["receiver"] == "0xA,0xB"
|
|
495
|
+
|
|
496
|
+
def test_from_address_varargs_join(self):
|
|
497
|
+
"""from_address() with multiple args should join with comma."""
|
|
498
|
+
client = DeFiStream(api_key="dsk_test")
|
|
499
|
+
query = client.erc20.transfers().from_address("0xA", "0xB")
|
|
500
|
+
assert query._params["sender"] == "0xA,0xB"
|
|
501
|
+
|
|
502
|
+
def test_to_address_varargs_join(self):
|
|
503
|
+
"""to_address() with multiple args should join with comma."""
|
|
504
|
+
client = DeFiStream(api_key="dsk_test")
|
|
505
|
+
query = client.erc20.transfers().to_address("0xA", "0xB")
|
|
506
|
+
assert query._params["receiver"] == "0xA,0xB"
|
|
507
|
+
|
|
508
|
+
def test_involving_varargs_join(self):
|
|
509
|
+
"""involving() with multiple args should join with comma."""
|
|
510
|
+
client = DeFiStream(api_key="dsk_test")
|
|
511
|
+
query = client.erc20.transfers().involving("0xA", "0xB")
|
|
512
|
+
assert query._params["involving"] == "0xA,0xB"
|
|
513
|
+
|
|
514
|
+
def test_involving_label_varargs_join(self):
|
|
515
|
+
"""involving_label() with multiple args should join with comma."""
|
|
516
|
+
client = DeFiStream(api_key="dsk_test")
|
|
517
|
+
query = client.erc20.transfers().involving_label("Binance", "Coinbase")
|
|
518
|
+
assert query._params["involving_label"] == "Binance,Coinbase"
|
|
519
|
+
|
|
520
|
+
def test_involving_category_varargs_join(self):
|
|
521
|
+
"""involving_category() with multiple args should join with comma."""
|
|
522
|
+
client = DeFiStream(api_key="dsk_test")
|
|
523
|
+
query = client.erc20.transfers().involving_category("CEX", "DEX")
|
|
524
|
+
assert query._params["involving_category"] == "CEX,DEX"
|
|
525
|
+
|
|
526
|
+
def test_sender_label_varargs_join(self):
|
|
527
|
+
"""sender_label() with multiple args should join with comma."""
|
|
528
|
+
client = DeFiStream(api_key="dsk_test")
|
|
529
|
+
query = client.erc20.transfers().sender_label("Binance", "Coinbase")
|
|
530
|
+
assert query._params["sender_label"] == "Binance,Coinbase"
|
|
531
|
+
|
|
532
|
+
def test_pre_joined_string_passthrough(self):
|
|
533
|
+
"""A single pre-joined string should pass through unchanged."""
|
|
534
|
+
client = DeFiStream(api_key="dsk_test")
|
|
535
|
+
query = client.erc20.transfers().sender("0xA,0xB,0xC")
|
|
536
|
+
assert query._params["sender"] == "0xA,0xB,0xC"
|
|
537
|
+
|
|
538
|
+
def test_single_string_backward_compat(self):
|
|
539
|
+
"""Single string arg should still work (backward compatibility)."""
|
|
540
|
+
client = DeFiStream(api_key="dsk_test")
|
|
541
|
+
query = client.erc20.transfers().sender("0xA")
|
|
542
|
+
assert query._params["sender"] == "0xA"
|
|
543
|
+
|
|
544
|
+
def test_sender_no_args_raises(self):
|
|
545
|
+
"""sender() with no args should raise ValueError."""
|
|
546
|
+
client = DeFiStream(api_key="dsk_test")
|
|
547
|
+
with pytest.raises(ValueError, match="At least one value is required"):
|
|
548
|
+
client.erc20.transfers().sender()
|
|
549
|
+
|
|
550
|
+
def test_involving_label_no_args_raises(self):
|
|
551
|
+
"""involving_label() with no args should raise ValueError."""
|
|
552
|
+
client = DeFiStream(api_key="dsk_test")
|
|
553
|
+
with pytest.raises(ValueError, match="At least one value is required"):
|
|
554
|
+
client.erc20.transfers().involving_label()
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class TestMutualExclusivity:
|
|
558
|
+
"""Test mutual exclusivity validation between filter slots."""
|
|
559
|
+
|
|
560
|
+
def test_involving_vs_involving_label_raises(self):
|
|
561
|
+
"""involving + involving_label should raise ValidationError."""
|
|
562
|
+
client = DeFiStream(api_key="dsk_test")
|
|
563
|
+
query = client.erc20.transfers().involving("0xA").involving_label("Binance")
|
|
564
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
565
|
+
query._build_params()
|
|
566
|
+
|
|
567
|
+
def test_involving_vs_involving_category_raises(self):
|
|
568
|
+
"""involving + involving_category should raise ValidationError."""
|
|
569
|
+
client = DeFiStream(api_key="dsk_test")
|
|
570
|
+
query = client.erc20.transfers().involving("0xA").involving_category("CEX")
|
|
571
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
572
|
+
query._build_params()
|
|
573
|
+
|
|
574
|
+
def test_involving_label_vs_involving_category_raises(self):
|
|
575
|
+
"""involving_label + involving_category should raise ValidationError."""
|
|
576
|
+
client = DeFiStream(api_key="dsk_test")
|
|
577
|
+
query = client.erc20.transfers().involving_label("Binance").involving_category("CEX")
|
|
578
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
579
|
+
query._build_params()
|
|
580
|
+
|
|
581
|
+
def test_sender_vs_sender_label_raises(self):
|
|
582
|
+
"""sender + sender_label should raise ValidationError."""
|
|
583
|
+
client = DeFiStream(api_key="dsk_test")
|
|
584
|
+
query = client.erc20.transfers().sender("0xA").sender_label("Binance")
|
|
585
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
586
|
+
query._build_params()
|
|
587
|
+
|
|
588
|
+
def test_sender_vs_sender_category_raises(self):
|
|
589
|
+
"""sender + sender_category should raise ValidationError."""
|
|
590
|
+
client = DeFiStream(api_key="dsk_test")
|
|
591
|
+
query = client.erc20.transfers().sender("0xA").sender_category("CEX")
|
|
592
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
593
|
+
query._build_params()
|
|
594
|
+
|
|
595
|
+
def test_receiver_vs_receiver_label_raises(self):
|
|
596
|
+
"""receiver + receiver_label should raise ValidationError."""
|
|
597
|
+
client = DeFiStream(api_key="dsk_test")
|
|
598
|
+
query = client.erc20.transfers().receiver("0xA").receiver_label("Coinbase")
|
|
599
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
600
|
+
query._build_params()
|
|
601
|
+
|
|
602
|
+
def test_receiver_vs_receiver_category_raises(self):
|
|
603
|
+
"""receiver + receiver_category should raise ValidationError."""
|
|
604
|
+
client = DeFiStream(api_key="dsk_test")
|
|
605
|
+
query = client.erc20.transfers().receiver("0xA").receiver_category("CEX")
|
|
606
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
607
|
+
query._build_params()
|
|
608
|
+
|
|
609
|
+
def test_involving_with_sender_raises(self):
|
|
610
|
+
"""involving + sender should raise ValidationError (cross-slot)."""
|
|
611
|
+
client = DeFiStream(api_key="dsk_test")
|
|
612
|
+
query = client.erc20.transfers().involving("0xA").sender("0xB")
|
|
613
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
614
|
+
query._build_params()
|
|
615
|
+
|
|
616
|
+
def test_involving_label_with_receiver_label_raises(self):
|
|
617
|
+
"""involving_label + receiver_label should raise ValidationError (cross-slot)."""
|
|
618
|
+
client = DeFiStream(api_key="dsk_test")
|
|
619
|
+
query = client.erc20.transfers().involving_label("Binance").receiver_label("Coinbase")
|
|
620
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
621
|
+
query._build_params()
|
|
622
|
+
|
|
623
|
+
def test_involving_category_with_sender_category_raises(self):
|
|
624
|
+
"""involving_category + sender_category should raise ValidationError (cross-slot)."""
|
|
625
|
+
client = DeFiStream(api_key="dsk_test")
|
|
626
|
+
query = client.erc20.transfers().involving_category("CEX").sender_category("DEX")
|
|
627
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
628
|
+
query._build_params()
|
|
629
|
+
|
|
630
|
+
def test_sender_and_receiver_valid(self):
|
|
631
|
+
"""sender + receiver should be valid (different slots, no involving)."""
|
|
632
|
+
client = DeFiStream(api_key="dsk_test")
|
|
633
|
+
query = client.erc20.transfers().sender("0xA").receiver("0xB")
|
|
634
|
+
params = query._build_params()
|
|
635
|
+
assert params["sender"] == "0xA"
|
|
636
|
+
assert params["receiver"] == "0xB"
|
|
637
|
+
|
|
638
|
+
def test_sender_label_and_receiver_category_valid(self):
|
|
639
|
+
"""sender_label + receiver_category should be valid."""
|
|
640
|
+
client = DeFiStream(api_key="dsk_test")
|
|
641
|
+
query = client.erc20.transfers().sender_label("Binance").receiver_category("DEX")
|
|
642
|
+
params = query._build_params()
|
|
643
|
+
assert params["sender_label"] == "Binance"
|
|
644
|
+
assert params["receiver_category"] == "DEX"
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
class TestSQLSafetyValidation:
|
|
648
|
+
"""Test SQL safety validation for label/category params."""
|
|
649
|
+
|
|
650
|
+
def test_single_quote_in_label_raises(self):
|
|
651
|
+
"""Label with single quote should raise ValidationError."""
|
|
652
|
+
client = DeFiStream(api_key="dsk_test")
|
|
653
|
+
query = client.erc20.transfers().involving_label("Binance'; DROP TABLE--")
|
|
654
|
+
with pytest.raises(ValidationError, match="unsafe characters"):
|
|
655
|
+
query._build_params()
|
|
656
|
+
|
|
657
|
+
def test_backslash_in_label_raises(self):
|
|
658
|
+
"""Label with backslash should raise ValidationError."""
|
|
659
|
+
client = DeFiStream(api_key="dsk_test")
|
|
660
|
+
query = client.erc20.transfers().sender_label("Binance\\x00")
|
|
661
|
+
with pytest.raises(ValidationError, match="unsafe characters"):
|
|
662
|
+
query._build_params()
|
|
663
|
+
|
|
664
|
+
def test_single_quote_in_category_raises(self):
|
|
665
|
+
"""Category with single quote should raise ValidationError."""
|
|
666
|
+
client = DeFiStream(api_key="dsk_test")
|
|
667
|
+
query = client.erc20.transfers().receiver_category("CEX'")
|
|
668
|
+
with pytest.raises(ValidationError, match="unsafe characters"):
|
|
669
|
+
query._build_params()
|
|
670
|
+
|
|
671
|
+
def test_backslash_in_category_raises(self):
|
|
672
|
+
"""Category with backslash should raise ValidationError."""
|
|
673
|
+
client = DeFiStream(api_key="dsk_test")
|
|
674
|
+
query = client.erc20.transfers().involving_category("CEX\\")
|
|
675
|
+
with pytest.raises(ValidationError, match="unsafe characters"):
|
|
676
|
+
query._build_params()
|
|
677
|
+
|
|
678
|
+
def test_clean_label_passes(self):
|
|
679
|
+
"""Clean label values should pass validation."""
|
|
680
|
+
client = DeFiStream(api_key="dsk_test")
|
|
681
|
+
query = client.erc20.transfers().involving_label("Binance 14")
|
|
682
|
+
params = query._build_params()
|
|
683
|
+
assert params["involving_label"] == "Binance 14"
|
|
684
|
+
|
|
685
|
+
def test_address_params_not_checked(self):
|
|
686
|
+
"""Address params (sender, receiver) should not be SQL-checked."""
|
|
687
|
+
client = DeFiStream(api_key="dsk_test")
|
|
688
|
+
# Addresses can contain any characters — only label/category is checked
|
|
689
|
+
query = client.erc20.transfers().sender("0x'abc")
|
|
690
|
+
params = query._build_params()
|
|
691
|
+
assert params["sender"] == "0x'abc"
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class TestAsyncLabelCategoryMethods:
|
|
695
|
+
"""Test label/category methods on AsyncQueryBuilder."""
|
|
696
|
+
|
|
697
|
+
def test_async_involving_label(self):
|
|
698
|
+
"""Async involving_label should set param."""
|
|
699
|
+
client = AsyncDeFiStream(api_key="dsk_test")
|
|
700
|
+
query = client.erc20.transfers().involving_label("Binance")
|
|
701
|
+
assert isinstance(query, AsyncQueryBuilder)
|
|
702
|
+
assert query._params["involving_label"] == "Binance"
|
|
703
|
+
|
|
704
|
+
def test_async_sender_multi_value(self):
|
|
705
|
+
"""Async sender with multiple args should join."""
|
|
706
|
+
client = AsyncDeFiStream(api_key="dsk_test")
|
|
707
|
+
query = client.erc20.transfers().sender("0xA", "0xB")
|
|
708
|
+
assert query._params["sender"] == "0xA,0xB"
|
|
709
|
+
|
|
710
|
+
def test_async_mutual_exclusivity(self):
|
|
711
|
+
"""Async builder should enforce mutual exclusivity."""
|
|
712
|
+
client = AsyncDeFiStream(api_key="dsk_test")
|
|
713
|
+
query = client.erc20.transfers().involving("0xA").sender("0xB")
|
|
714
|
+
with pytest.raises(ValidationError, match="Cannot combine"):
|
|
715
|
+
query._build_params()
|
|
716
|
+
|
|
717
|
+
def test_async_sql_safety(self):
|
|
718
|
+
"""Async builder should enforce SQL safety."""
|
|
719
|
+
client = AsyncDeFiStream(api_key="dsk_test")
|
|
720
|
+
query = client.erc20.transfers().sender_label("test'injection")
|
|
721
|
+
with pytest.raises(ValidationError, match="unsafe characters"):
|
|
722
|
+
query._build_params()
|
|
723
|
+
|
|
724
|
+
def test_async_label_chaining(self):
|
|
725
|
+
"""Async label methods should chain correctly."""
|
|
726
|
+
client = AsyncDeFiStream(api_key="dsk_test")
|
|
727
|
+
query = (
|
|
728
|
+
client.erc20.transfers("USDT")
|
|
729
|
+
.network("ETH")
|
|
730
|
+
.sender_label("Binance", "Coinbase")
|
|
731
|
+
.receiver_category("DEX")
|
|
732
|
+
)
|
|
733
|
+
assert query._params["sender_label"] == "Binance,Coinbase"
|
|
734
|
+
assert query._params["receiver_category"] == "DEX"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|