Skip to content

Commit c587a38

Browse files
committed
Add non-indexed event arg filtering support for get_logs
- Contract event ``get_logs()`` has support for ``argument_filters`` but it only works on indexed event arguments. This feature works on unindexed args for ``create_filter()`` / ``build_filter()``. It is ideal to achieve some parity in the same argument for ``get_logs()`` by allowing it here as well.
1 parent 80a88e4 commit c587a38

File tree

5 files changed

+118
-22
lines changed

5 files changed

+118
-22
lines changed

docs/web3.contract.rst

+5-2
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ Each Contract Factory exposes the following methods.
261261
- ``fromBlock`` is a mandatory field. Defines the starting block (exclusive) filter block range. It can be either the starting block number, or 'latest' for the last mined block, or 'pending' for unmined transactions. In the case of ``fromBlock``, 'latest' and 'pending' set the 'latest' or 'pending' block as a static value for the starting filter block.
262262
- ``toBlock`` optional. Defaults to 'latest'. Defines the ending block (inclusive) in the filter block range. Special values 'latest' and 'pending' set a dynamic range that always includes the 'latest' or 'pending' blocks for the filter's upper block range.
263263
- ``address`` optional. Defaults to the contract address. The filter matches the event logs emanating from ``address``.
264-
- ``argument_filters``, optional. Expects a dictionary of argument names and values. When provided event logs are filtered for the event argument values. Event arguments can be both indexed or unindexed. Indexed values with be translated to their corresponding topic arguments. Unindexed arguments will be filtered using a regular expression.
264+
- ``argument_filters``, optional. Expects a dictionary of argument names and values. When provided event logs are filtered for the event argument values. Event arguments can be both indexed or unindexed. Indexed values will be translated to their corresponding topic arguments. Unindexed arguments will be filtered using a regular expression.
265265
- ``topics`` optional, accepts the standard JSON-RPC topics argument. See the JSON-RPC documentation for `eth_newFilter <https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter>`_ more information on the ``topics`` parameters.
266266

267267
.. py:classmethod:: Contract.events.your_event_name.build_filter()
@@ -933,12 +933,15 @@ For example:
933933
:noindex:
934934

935935
Fetches all logs for a given event within the specified block range or block hash.
936+
936937
``argument_filters`` is an optional dictionary argument that can be used to filter
937938
for logs where the event's argument values match the values provided in the
938939
dictionary. The keys must match the event argument names as they exist in the ABI.
939940
The values can either be a single value or a list of values to match against. If a
940941
list is provided, the logs will be filtered for any logs that match any of the
941-
values in the list.
942+
values in the list. Indexed arguments are filtered pre-call by building specific
943+
``topics`` to filter for. Non-indexed arguments are filtered by the library after
944+
the logs are fetched from the node.
942945

943946
.. code-block:: python
944947

web3/_utils/filters.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ def construct_event_filter_params(
9898
if is_list_like(address):
9999
filter_params["address"] = [address] + [contract_address]
100100
elif is_string(address):
101-
filter_params["address"] = [address, contract_address]
101+
filter_params["address"] = (
102+
[address, contract_address]
103+
if address != contract_address
104+
else [address]
105+
)
102106
else:
103107
raise ValueError(
104108
f"Unsupported type for `address` parameter: {type(address)}"

web3/contract/async_contract.py

+30-10
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
ABIFunctionNotFound,
8484
NoABIFound,
8585
NoABIFunctionsFound,
86+
Web3ValidationError,
8687
)
8788
from web3.types import (
8889
ABI,
@@ -91,6 +92,9 @@
9192
EventData,
9293
TxParams,
9394
)
95+
from web3.utils import (
96+
get_abi_input_names,
97+
)
9498

9599
if TYPE_CHECKING:
96100
from ens import AsyncENS # noqa: F401
@@ -156,25 +160,41 @@ async def get_logs(
156160
157161
See also: :func:`web3.middleware.filter.local_filter_middleware`.
158162
159-
:param argument_filters:
163+
:param argument_filters: Filter by argument values. Indexed arguments are
164+
filtered by the node while non-indexed arguments are filtered by the library.
160165
:param fromBlock: block number or "latest", defaults to "latest"
161166
:param toBlock: block number or "latest". Defaults to "latest"
162-
:param blockHash: block hash. blockHash cannot be set at the
167+
:param block_hash: block hash. blockHash cannot be set at the
163168
same time as fromBlock or toBlock
164169
:yield: Tuple of :class:`AttributeDict` instances
165170
"""
166-
abi = self._get_event_abi()
171+
event_abi = self._get_event_abi()
172+
173+
# validate ``argument_filters`` if present
174+
if argument_filters is not None:
175+
event_arg_names = get_abi_input_names(event_abi)
176+
if not all(arg in event_arg_names for arg in argument_filters.keys()):
177+
raise Web3ValidationError(
178+
"When filtering by argument names, all argument names must be "
179+
"present in the contract's event ABI."
180+
)
181+
167182
# Call JSON-RPC API
168-
logs = await self.w3.eth.get_logs(
169-
self._get_event_filter_params(
170-
abi, argument_filters, fromBlock, toBlock, block_hash
171-
)
183+
_filter_params = self._get_event_filter_params(
184+
event_abi, argument_filters, fromBlock, toBlock, block_hash
172185
)
173186

174-
# Convert raw binary data to Python proxy objects as described by ABI
175-
return tuple( # type: ignore
176-
get_event_data(self.w3.codec, abi, entry) for entry in logs
187+
logs = await self.w3.eth.get_logs(_filter_params)
188+
# convert raw binary data to Python proxy objects as described by ABI:
189+
all_event_logs = tuple(
190+
get_event_data(self.w3.codec, event_abi, entry) for entry in logs
191+
)
192+
filtered_logs = self._process_get_logs_argument_filters(
193+
event_abi,
194+
all_event_logs,
195+
argument_filters,
177196
)
197+
return cast(Awaitable[Iterable[EventData]], filtered_logs)
178198

179199
@combomethod
180200
async def create_filter(

web3/contract/base_contract.py

+48-1
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def _get_event_filter_params(
224224

225225
# Construct JSON-RPC raw filter presentation based on human readable
226226
# Python descriptions. Namely, convert event names to their keccak signatures
227-
data_filter_set, event_filter_params = construct_event_filter_params(
227+
_, event_filter_params = construct_event_filter_params(
228228
abi,
229229
self.w3.codec,
230230
contract_address=self.address,
@@ -263,6 +263,53 @@ def check_for_forbidden_api_filter_arguments(
263263
"filters with the match_any method."
264264
)
265265

266+
@staticmethod
267+
def _process_get_logs_argument_filters(
268+
event_abi: ABIEvent,
269+
event_logs: Sequence[EventData],
270+
argument_filters: Optional[Dict[str, Any]],
271+
) -> Iterable[EventData]:
272+
if (
273+
argument_filters is None
274+
or len(event_logs) == 0
275+
or
276+
# If all args in ``argument_filters`` are indexed, then the logs are
277+
# already filtered by the node in the ``eth_getLogs`` call.
278+
all(
279+
input_arg["indexed"]
280+
for input_arg in event_abi["inputs"]
281+
if input_arg["name"] in argument_filters.keys()
282+
)
283+
):
284+
return event_logs
285+
286+
filtered_logs_by_non_indexed_args = []
287+
288+
for log in event_logs:
289+
match = False
290+
for arg, match_values in argument_filters.items():
291+
if not is_list_like(match_values):
292+
match_values = [match_values]
293+
294+
for abi_arg in event_abi["inputs"]:
295+
if abi_arg["name"] == arg:
296+
if (
297+
# isolate ``string`` values to support substrings
298+
abi_arg["type"] == "string"
299+
and any(val in log["args"][arg] for val in match_values)
300+
or (
301+
# otherwise, do direct value comparison
302+
abi_arg["type"] != "string"
303+
and log["args"][arg] in match_values
304+
)
305+
):
306+
filtered_logs_by_non_indexed_args.append(log)
307+
match = True
308+
break
309+
if match:
310+
break
311+
return filtered_logs_by_non_indexed_args
312+
266313
@combomethod
267314
def _set_up_filter_builder(
268315
self,

web3/contract/contract.py

+30-8
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
ABIFunctionNotFound,
8282
NoABIFound,
8383
NoABIFunctionsFound,
84+
Web3ValidationError,
8485
)
8586
from web3.types import (
8687
ABI,
@@ -89,6 +90,9 @@
8990
EventData,
9091
TxParams,
9192
)
93+
from web3.utils import (
94+
get_abi_input_names,
95+
)
9296

9397
if TYPE_CHECKING:
9498
from ens import ENS # noqa: F401
@@ -154,23 +158,41 @@ def get_logs(
154158
155159
See also: :func:`web3.middleware.filter.local_filter_middleware`.
156160
157-
:param argument_filters:
161+
:param argument_filters: Filter by argument values. Indexed arguments are
162+
filtered by the node while non-indexed arguments are filtered by the library.
158163
:param fromBlock: block number or "latest", defaults to "latest"
159164
:param toBlock: block number or "latest". Defaults to "latest"
160165
:param block_hash: block hash. block_hash cannot be set at the
161166
same time as fromBlock or toBlock
162167
:yield: Tuple of :class:`AttributeDict` instances
163168
"""
164-
abi = self._get_event_abi()
169+
event_abi = self._get_event_abi()
170+
171+
# validate ``argument_filters`` if present
172+
if argument_filters is not None:
173+
event_arg_names = get_abi_input_names(event_abi)
174+
if not all(arg in event_arg_names for arg in argument_filters.keys()):
175+
raise Web3ValidationError(
176+
"When filtering by argument names, all argument names must be "
177+
"present in the contract's event ABI."
178+
)
179+
165180
# Call JSON-RPC API
166-
logs = self.w3.eth.get_logs(
167-
self._get_event_filter_params(
168-
abi, argument_filters, fromBlock, toBlock, block_hash
169-
)
181+
_filter_params = self._get_event_filter_params(
182+
event_abi, argument_filters, fromBlock, toBlock, block_hash
170183
)
171184

172-
# Convert raw binary data to Python proxy objects as described by ABI
173-
return tuple(get_event_data(self.w3.codec, abi, entry) for entry in logs)
185+
logs = cast(Sequence[EventData], self.w3.eth.get_logs(_filter_params))
186+
# convert raw binary data to Python proxy objects as described by ABI:
187+
all_event_logs = tuple(
188+
get_event_data(self.w3.codec, event_abi, entry) for entry in logs
189+
)
190+
filtered_logs = self._process_get_logs_argument_filters(
191+
event_abi,
192+
all_event_logs,
193+
argument_filters,
194+
)
195+
return filtered_logs
174196

175197
@combomethod
176198
def create_filter(

0 commit comments

Comments
 (0)