diff --git a/docs/web3.contract.rst b/docs/web3.contract.rst index 25341b2be8..56fcbf18e3 100644 --- a/docs/web3.contract.rst +++ b/docs/web3.contract.rst @@ -261,7 +261,7 @@ Each Contract Factory exposes the following methods. - ``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. - ``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. - ``address`` optional. Defaults to the contract address. The filter matches the event logs emanating from ``address``. - - ``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. + - ``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. - ``topics`` optional, accepts the standard JSON-RPC topics argument. See the JSON-RPC documentation for `eth_newFilter `_ more information on the ``topics`` parameters. .. py:classmethod:: Contract.events.your_event_name.build_filter() diff --git a/web3/_utils/filters.py b/web3/_utils/filters.py index bea39bbb98..1fd03e1f05 100644 --- a/web3/_utils/filters.py +++ b/web3/_utils/filters.py @@ -98,7 +98,11 @@ def construct_event_filter_params( if is_list_like(address): filter_params["address"] = [address] + [contract_address] elif is_string(address): - filter_params["address"] = [address, contract_address] + filter_params["address"] = ( + [address, contract_address] + if address != contract_address + else [address] + ) else: raise ValueError( f"Unsupported type for `address` parameter: {type(address)}" diff --git a/web3/contract/base_contract.py b/web3/contract/base_contract.py index b8a2cc417f..f63b583dc0 100644 --- a/web3/contract/base_contract.py +++ b/web3/contract/base_contract.py @@ -224,7 +224,7 @@ def _get_event_filter_params( # Construct JSON-RPC raw filter presentation based on human readable # Python descriptions. Namely, convert event names to their keccak signatures - data_filter_set, event_filter_params = construct_event_filter_params( + _, event_filter_params = construct_event_filter_params( abi, self.w3.codec, contract_address=self.address, diff --git a/web3/contract/contract.py b/web3/contract/contract.py index ef0941bc3d..69dd7896e9 100644 --- a/web3/contract/contract.py +++ b/web3/contract/contract.py @@ -17,6 +17,7 @@ ) from eth_utils import ( combomethod, + is_list_like, ) from eth_utils.toolz import ( partial, @@ -89,6 +90,9 @@ EventData, TxParams, ) +from web3.utils import ( + get_abi_input_names, +) if TYPE_CHECKING: from ens import ENS # noqa: F401 @@ -161,16 +165,65 @@ def get_logs( same time as fromBlock or toBlock :yield: Tuple of :class:`AttributeDict` instances """ - abi = self._get_event_abi() + event_abi = self._get_event_abi() + # Call JSON-RPC API - logs = self.w3.eth.get_logs( - self._get_event_filter_params( - abi, argument_filters, fromBlock, toBlock, block_hash - ) + _filter_params = self._get_event_filter_params( + event_abi, argument_filters, fromBlock, toBlock, block_hash + ) + + logs = cast(Sequence[EventData], self.w3.eth.get_logs(_filter_params)) + # convert raw binary data to Python proxy objects as described by ABI: + event_logs = tuple( + get_event_data(self.w3.codec, event_abi, entry) for entry in logs ) - # Convert raw binary data to Python proxy objects as described by ABI - return tuple(get_event_data(self.w3.codec, abi, entry) for entry in logs) + event_arg_names = get_abi_input_names(event_abi) + + if argument_filters is not None: + # handle argument filtering for un-indexed args + if not all(arg in event_arg_names for arg in argument_filters.keys()): + raise TypeError( + "When filtering by argument names, all argument names must be " + "present in the contract's event ABI." + ) + + if ( + len(event_logs) > 0 + # if all args in ``argument_filters`` are indexed, then the logs + # are already filtered by the node in the ``eth_getLogs`` call above. + and not all( + input_arg["indexed"] + for input_arg in event_abi["inputs"] + if input_arg["name"] in argument_filters.keys() + ) + ): + filtered_logs_by_non_indexed_args = [] + + for log in event_logs: + for arg, match_values in argument_filters.items(): + if not is_list_like(match_values): + match_values = [match_values] + + for abi_arg in event_abi["inputs"]: + if abi_arg["name"] == arg: + if ( + # isolate ``string`` values to support substrings + abi_arg["type"] == "string" + and any( + val in log["args"][arg] for val in match_values + ) + or ( + # otherwise, do direct value comparison + abi_arg["type"] != "string" + and log["args"][arg] in match_values + ) + ): + filtered_logs_by_non_indexed_args.append(log) + + return filtered_logs_by_non_indexed_args + + return event_logs @combomethod def create_filter(