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

contract.events.X.getLogs() support without creating filters #1192

Merged
merged 5 commits into from
Jan 17, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/filters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ Filtering

.. py:module:: web3.utils.filters


The :meth:`web3.eth.Eth.filter` method can be used to setup filters for:

* Pending Transactions: ``web3.eth.filter('pending')``
Expand Down Expand Up @@ -32,6 +31,12 @@ The :meth:`web3.eth.Eth.filter` method can be used to setup filters for:
from web3.auto import w3
existing_filter = web3.eth.filter(filter_id="0x0")

.. note ::

Creating event filters requires that your Ethereum node has an API support enabled for filters.
It does not work with Infura nodes. To get event logs on Infura or other
stateless nodes please see :class:`web3.contract.ContractEvents`.


Filter Class
------------
Expand Down Expand Up @@ -160,6 +165,13 @@ methods:
Provides a means to filter on the log data, in other words the ability to filter on values from
un-indexed event arguments. The parameter ``data_filter_set`` should be a list or set of 32-byte hex encoded values.

Getting events without setting up a filter
------------------------------------------

You can query Ethereum node for direct fetch of events, without creating a filter first.
This works on all node types, including Infura.

For examples see :meth:`web3.contract.ContractEvents.getLogs`.

Examples: Listening For Events
------------------------------
Expand Down
95 changes: 95 additions & 0 deletions tests/core/filtering/test_contract_getLogs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@


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


def test_contract_getLogs_all(
web3,
emitter,
wait_for_transaction,
emitter_event_ids,
):
contract = emitter
event_id = emitter_event_ids.LogNoArguments

txn_hash = contract.functions.logNoArgs(event_id).transact()
wait_for_transaction(web3, txn_hash)

log_entries = list(contract.events.LogNoArguments.getLogs())
assert len(log_entries) == 1
assert log_entries[0]['transactionHash'] == txn_hash


def test_contract_getLogs_range(
web3,
emitter,
wait_for_transaction,
emitter_event_ids,
):
contract = emitter
event_id = emitter_event_ids.LogNoArguments

assert web3.eth.blockNumber == 2
txn_hash = contract.functions.logNoArgs(event_id).transact()
# Mined as block 3
wait_for_transaction(web3, txn_hash)
assert web3.eth.blockNumber == 3

log_entries = list(contract.events.LogNoArguments.getLogs())
assert len(log_entries) == 1

log_entries = list(contract.events.LogNoArguments.getLogs(fromBlock=2, toBlock=3))
assert len(log_entries) == 1

log_entries = list(contract.events.LogNoArguments.getLogs(fromBlock=1, toBlock=2))
assert len(log_entries) == 0


def test_contract_getLogs_argument_filter(
web3,
emitter,
wait_for_transaction,
emitter_event_ids):

contract = emitter

txn_hashes = []
event_id = emitter_event_ids.LogTripleWithIndex
# 1 = arg0
# 4 = arg1
# 1 = arg2
txn_hashes.append(
emitter.functions.logTriple(event_id, 1, 4, 1).transact()
)
txn_hashes.append(
emitter.functions.logTriple(event_id, 1, 1, 2).transact()
)
txn_hashes.append(
emitter.functions.logTriple(event_id, 1, 2, 2).transact()
)
txn_hashes.append(
emitter.functions.logTriple(event_id, 1, 3, 1).transact()
)
for txn_hash in txn_hashes:
wait_for_transaction(web3, txn_hash)

all_logs = contract.events.LogTripleWithIndex.getLogs()
assert len(all_logs) == 4

# Filter all entries where arg2 in (1, 2)
kclowes marked this conversation as resolved.
Show resolved Hide resolved
partial_logs = contract.events.LogTripleWithIndex.getLogs(
argument_filters={'arg1': [1, 2]},
)
assert len(partial_logs) == 2

# Filter all entries where arg0 == 1
partial_logs = contract.events.LogTripleWithIndex.getLogs(
argument_filters={'arg0': 1},
)
assert len(partial_logs) == 4
113 changes: 113 additions & 0 deletions web3/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,23 @@ def __getitem__(self, function_name):

class ContractEvents:
"""Class containing contract event objects

This is available via:

.. code-block:: python

>>> mycontract.events
<web3.contract.ContractEvents object at 0x108afde10>

To get list of all supported events in the contract ABI.
This allows you to iterate over :class:`ContractEvent` proxy classes.

.. code-block:: python

>>> for e in mycontract.events: print(e)
<class 'web3._utils.datatypes.LogAnonymous'>
...

"""

def __init__(self, abi, web3, address=None):
Expand Down Expand Up @@ -181,6 +198,14 @@ def __getattr__(self, event_name):
def __getitem__(self, event_name):
return getattr(self, event_name)

def __iter__(self):
"""Iterate over supported

:return: Iterable of :class:`ContractEvent`
"""
for event in self._events:
yield self[event['name']]


class Contract:
"""Base class for Contract proxy classes.
Expand Down Expand Up @@ -216,6 +241,8 @@ class Contract:
clone_bin = None

functions = None

#: Instance of :class:`ContractEvents` presenting available Event ABIs
events = None

dev_doc = None
Expand Down Expand Up @@ -1323,6 +1350,92 @@ def build_filter(self):
builder.address = self.address
return builder

@combomethod
def getLogs(self,
argument_filters=None,
fromBlock=1,
toBlock="latest"):
"""Get events for this contract instance using eth_getLogs API.

This is a stateless method, as opposite to createFilter.
It can be safely called against nodes which do not provide
eth_newFilter API, like Infura nodes.

If no block range is provided and there are many events,
like ``Transfer`` events for a popular token,
the Ethereum node might be overload and timeout
on underlying JSON-RPC call.

Example - how to get all ERC-20 token transactions
for the latest 10 blocks:

.. code-block:: python

f = max(mycontract.web3.eth.blockNumber - 10, 1)
t= mycontract.web3.eth.blockNumber

events = mycontract.events.Transfer.getLogs(fromBlock=f, toBlock=t)

for e in events:
print(e["args"]["from"],
e["args"]["to"],
e["args"]["value"])

The returned processed log values will look like:

.. code-block:: python

(
AttributeDict({
'args': AttributeDict({}),
'event': 'LogNoArguments',
'logIndex': 0,
'transactionIndex': 0,
'transactionHash': HexBytes('...'),
'address': '0xF2E246BB76DF876Cef8b38ae84130F4F55De395b',
'blockHash': HexBytes('...'),
'blockNumber': 3
}),
AttributeDict(...),
...
)

See also: :func:`web3.middleware.filter.local_filter_middleware`.

:param argument_filters: TODO
kclowes marked this conversation as resolved.
Show resolved Hide resolved
:param fromBlock: block number, defaults to 1
:param toBlock: "block number or "latest", defaults to "latest"
:yield: Tuple of :class:`AttributeDict` instances
"""

if not self.address:
raise TypeError("This method can be only called on "
"an instated contract with an address")

abi = self._get_event_abi()

if argument_filters is None:
argument_filters = dict()

_filters = dict(**argument_filters)

# 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(
abi,
contract_address=self.address,
argument_filters=_filters,
fromBlock=fromBlock,
toBlock=toBlock,
address=self.address,
)

# Call JSON-RPC API
logs = self.web3.eth.getLogs(event_filter_params)

# Convert raw binary data to Python proxy objects as described by ABI
return tuple(get_event_data(abi, entry) for entry in logs)

@classmethod
def factory(cls, class_name, **kwargs):
return PropertyCheckingFactory(class_name, (cls,), kwargs)
Expand Down