Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unindexed event arg filtering support for get_logs #3078

Merged
merged 6 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/web3.contract.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter>`_ more information on the ``topics`` parameters.

.. py:classmethod:: Contract.events.your_event_name.build_filter()
Expand Down Expand Up @@ -933,12 +933,15 @@ For example:
:noindex:

Fetches all logs for a given event within the specified block range or block hash.

``argument_filters`` is an optional dictionary argument that can be used to filter
for logs where the event's argument values match the values provided in the
dictionary. The keys must match the event argument names as they exist in the ABI.
The values can either be a single value or a list of values to match against. If a
list is provided, the logs will be filtered for any logs that match any of the
values in the list.
values in the list. Indexed arguments are filtered pre-call by building specific
``topics`` to filter for. Non-indexed arguments are filtered by the library after
the logs are fetched from the node.

.. code-block:: python

Expand Down
1 change: 1 addition & 0 deletions newsfragments/3078.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enable filtering by non-indexed arguments for contract event ``get_logs()``.
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def emitter_contract_data():


class LogFunctions:
# These appear to be for a very specific test and this doesn't need to be updated
# for every event in the emitter contract. That ends up breaking that test.
LogAnonymous = 0
LogNoArguments = 1
LogSingleArg = 2
Expand Down Expand Up @@ -99,6 +101,7 @@ class LogTopics:
"LogAddressNotIndexed(address,address)"
)
LogStructArgs = event_signature_to_log_topic("LogStructArgs(uint256,tuple)")
LogIndexedAndNotIndexed = event_signature_to_log_topic("LogIndexedAndNotIndexed()")


@pytest.fixture(scope="session")
Expand Down
201 changes: 199 additions & 2 deletions tests/core/filtering/test_contract_get_logs.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import pytest

from web3._utils.contract_sources.contract_data._custom_contract_data import (
EMITTER_ENUM,
)
from web3.exceptions import (
Web3ValidationError,
)


def test_contract_get_available_events(
emitter,
):
"""We can iterate over available contract events"""
contract = emitter
events = list(contract.events)
assert len(events) == 19
assert len(events) == len(EMITTER_ENUM)


def test_contract_get_logs_all(
Expand Down Expand Up @@ -87,6 +94,93 @@ def test_contract_get_logs_argument_filter(
assert len(partial_logs) == 4


def test_get_logs_argument_filters_indexed_and_non_indexed_args(emitter):
emitter.functions.logIndexedAndNotIndexedArgs(
f"0xdead{'00' * 17}01", # indexed address
101, # indexed uint256
f"0xBEEf{'00' * 17}01", # non-indexed address
201, # non-indexed uint256
"This is the FIRST string arg.", # non-indexed string
).transact()
emitter.functions.logIndexedAndNotIndexedArgs(
f"0xdEaD{'00' * 17}02",
102,
f"0xbeeF{'00' * 17}02",
202,
"This is the SECOND string arg.",
).transact()

# get all logs
logs_no_filter = emitter.events.LogIndexedAndNotIndexed.get_logs(fromBlock=1)
assert len(logs_no_filter) == 2

# filter for the second log by each of the indexed args and compare
logs_filter_indexed_address = emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={"indexedAddress": f"0xdEaD{'00' * 17}02"},
)
logs_filter_indexed_uint256 = emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={"indexedUint256": 102},
)
assert len(logs_filter_indexed_address) == len(logs_filter_indexed_uint256) == 1
assert (
logs_filter_indexed_address[0]
== logs_filter_indexed_uint256[0]
== logs_no_filter[1]
)

# filter for the first log by non-indexed address
logs_filter_non_indexed_address = emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={"nonIndexedAddress": f"0xBEEf{'00' * 17}01"},
)
assert len(logs_filter_non_indexed_address) == 1
assert logs_filter_non_indexed_address[0] == logs_no_filter[0]

# filter for the second log by non-indexed uint256 and string separately
logs_filter_non_indexed_uint256 = emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={"nonIndexedUint256": 202},
)
logs_filter_non_indexed_string = emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={"nonIndexedString": "SECOND"},
)
assert len(logs_filter_non_indexed_uint256) == 1
assert len(logs_filter_non_indexed_string) == 1
assert (
logs_filter_non_indexed_uint256[0]
== logs_filter_non_indexed_string[0]
== logs_no_filter[1]
)

# filter by both string and uint256, non-indexed
logs_filter_non_indexed_uint256_and_string = (
emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={
"nonIndexedUint256": 201,
"nonIndexedString": "FIRST",
},
)
)
assert len(logs_filter_non_indexed_uint256_and_string) == 1
assert logs_filter_non_indexed_uint256_and_string[0] == logs_no_filter[0]


def test_get_logs_argument_filters_key_validation(
emitter,
):
with pytest.raises(
Web3ValidationError,
match="all argument names must be present in the contract's event ABI",
):
emitter.events.LogIndexedAndNotIndexed.get_logs(
argument_filters={"nonExistentKey": "Value shouldn't matter"},
)


# --- async --- #


Expand All @@ -96,7 +190,7 @@ def test_async_contract_get_available_events(
"""We can iterate over available contract events"""
contract = async_emitter
events = list(contract.events)
assert len(events) == 19
assert len(events) == len(EMITTER_ENUM)


@pytest.mark.asyncio
Expand Down Expand Up @@ -195,3 +289,106 @@ async def test_async_contract_get_logs_argument_filter(
argument_filters={"arg0": 1},
)
assert len(partial_logs) == 4


@pytest.mark.asyncio
async def test_async_get_logs_argument_filters_indexed_and_non_indexed_args(
async_emitter,
):
await async_emitter.functions.logIndexedAndNotIndexedArgs(
f"0xdead{'00' * 17}01", # indexed address
101, # indexed uint256
f"0xBEEf{'00' * 17}01", # non-indexed address
201, # non-indexed uint256
"This is the FIRST string arg.", # non-indexed string
).transact()
await async_emitter.functions.logIndexedAndNotIndexedArgs(
f"0xdEaD{'00' * 17}02",
102,
f"0xbeeF{'00' * 17}02",
202,
"This is the SECOND string arg.",
).transact()

# get all logs
logs_no_filter = await async_emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1
)
assert len(logs_no_filter) == 2

# filter for the second log by each of the indexed args and compare
logs_filter_indexed_address = (
await async_emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={"indexedAddress": f"0xdEaD{'00' * 17}02"},
)
)
logs_filter_indexed_uint256 = (
await async_emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={"indexedUint256": 102},
)
)
assert len(logs_filter_indexed_address) == len(logs_filter_indexed_uint256) == 1
assert (
logs_filter_indexed_address[0]
== logs_filter_indexed_uint256[0]
== logs_no_filter[1]
)

# filter for the first log by non-indexed address
logs_filter_non_indexed_address = (
await async_emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={"nonIndexedAddress": f"0xBEEf{'00' * 17}01"},
)
)
assert len(logs_filter_non_indexed_address) == 1
assert logs_filter_non_indexed_address[0] == logs_no_filter[0]

# filter for the second log by non-indexed uint256 and string separately
logs_filter_non_indexed_uint256 = (
await async_emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={"nonIndexedUint256": 202},
)
)
logs_filter_non_indexed_string = (
await async_emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={"nonIndexedString": "SECOND"},
)
)
assert len(logs_filter_non_indexed_uint256) == 1
assert len(logs_filter_non_indexed_string) == 1
assert (
logs_filter_non_indexed_uint256[0]
== logs_filter_non_indexed_string[0]
== logs_no_filter[1]
)

# filter by both string and uint256, non-indexed
logs_filter_non_indexed_uint256_and_string = (
await async_emitter.events.LogIndexedAndNotIndexed.get_logs(
fromBlock=1,
argument_filters={
"nonIndexedUint256": 201,
"nonIndexedString": "FIRST",
},
)
)
assert len(logs_filter_non_indexed_uint256_and_string) == 1
assert logs_filter_non_indexed_uint256_and_string[0] == logs_no_filter[0]


@pytest.mark.asyncio
async def test_async_get_logs_argument_filters_key_validation(
async_emitter,
):
with pytest.raises(
Web3ValidationError,
match="all argument names must be present in the contract's event ABI",
):
await async_emitter.events.LogIndexedAndNotIndexed.get_logs(
argument_filters={"nonExistentKey": "Value shouldn't matter"},
)
51 changes: 38 additions & 13 deletions web3/_utils/contract_sources/EmitterContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ contract EmitterContract {
}
event LogStructArgs(uint arg0, TestTuple arg1);

event LogIndexedAndNotIndexed(
address indexed indexedAddress,
uint256 indexed indexedUint256,
address nonIndexedAddress,
uint256 nonIndexedUint256,
string nonIndexedString
);

enum WhichEvent {
LogAnonymous,
LogNoArguments,
Expand All @@ -50,67 +58,84 @@ contract EmitterContract {
LogListArgs,
LogAddressIndexed,
LogAddressNotIndexed,
LogStructArgs
LogStructArgs,
LogIndexedAndNotIndexed
}

function logNoArgs(WhichEvent which) public {
function logNoArgs(WhichEvent which) external {
if (which == WhichEvent.LogNoArguments) emit LogNoArguments();
else if (which == WhichEvent.LogAnonymous) emit LogAnonymous();
else revert("Didn't match any allowable event index");
}

function logSingle(WhichEvent which, uint arg0) public {
function logSingle(WhichEvent which, uint arg0) external {
if (which == WhichEvent.LogSingleArg) emit LogSingleArg(arg0);
else if (which == WhichEvent.LogSingleWithIndex) emit LogSingleWithIndex(arg0);
else if (which == WhichEvent.LogSingleAnonymous) emit LogSingleAnonymous(arg0);
else revert("Didn't match any allowable event index");
}

function logDouble(WhichEvent which, uint arg0, uint arg1) public {
function logDouble(WhichEvent which, uint arg0, uint arg1) external {
if (which == WhichEvent.LogDoubleArg) emit LogDoubleArg(arg0, arg1);
else if (which == WhichEvent.LogDoubleWithIndex) emit LogDoubleWithIndex(arg0, arg1);
else if (which == WhichEvent.LogDoubleAnonymous) emit LogDoubleAnonymous(arg0, arg1);
else revert("Didn't match any allowable event index");
}

function logTriple(WhichEvent which, uint arg0, uint arg1, uint arg2) public {
function logTriple(WhichEvent which, uint arg0, uint arg1, uint arg2) external {
if (which == WhichEvent.LogTripleArg) emit LogTripleArg(arg0, arg1, arg2);
else if (which == WhichEvent.LogTripleWithIndex) emit LogTripleWithIndex(arg0, arg1, arg2);
else revert("Didn't match any allowable event index");
}

function logQuadruple(WhichEvent which, uint arg0, uint arg1, uint arg2, uint arg3) public {
function logQuadruple(WhichEvent which, uint arg0, uint arg1, uint arg2, uint arg3) external {
if (which == WhichEvent.LogQuadrupleArg) emit LogQuadrupleArg(arg0, arg1, arg2, arg3);
else if (which == WhichEvent.LogQuadrupleWithIndex) emit LogQuadrupleWithIndex(arg0, arg1, arg2, arg3);
else revert("Didn't match any allowable event index");
}

function logDynamicArgs(string memory arg0, string memory arg1) public {
function logDynamicArgs(string memory arg0, string memory arg1) external {
emit LogDynamicArgs(arg0, arg1);
}

function logListArgs(bytes2[] memory arg0, bytes2[] memory arg1) public {
function logListArgs(bytes2[] memory arg0, bytes2[] memory arg1) external {
emit LogListArgs(arg0, arg1);
}

function logAddressIndexedArgs(address arg0, address arg1) public {
function logAddressIndexedArgs(address arg0, address arg1) external {
emit LogAddressIndexed(arg0, arg1);
}

function logAddressNotIndexedArgs(address arg0, address arg1) public {
function logAddressNotIndexedArgs(address arg0, address arg1) external {
emit LogAddressNotIndexed(arg0, arg1);
}

function logBytes(bytes memory v) public {
function logBytes(bytes memory v) external {
emit LogBytes(v);
}

function logString(string memory v) public {
function logString(string memory v) external {
emit LogString(v);
}

function logStruct(uint arg0, TestTuple memory arg1) public {
function logStruct(uint arg0, TestTuple memory arg1) external {
emit LogStructArgs(arg0, arg1);
}

function logIndexedAndNotIndexedArgs(
address indexedAddress,
uint256 indexedUint256,
address nonIndexedAddress,
uint256 nonIndexedUint256,
string memory nonIndexedString
) external {
emit LogIndexedAndNotIndexed(
indexedAddress,
indexedUint256,
nonIndexedAddress,
nonIndexedUint256,
nonIndexedString
);
}
}