From 155bfd372f1377387a7043e78ca66080301e7a37 Mon Sep 17 00:00:00 2001 From: Mikko Ohtamaa Date: Tue, 8 Jan 2019 23:17:16 +0100 Subject: [PATCH 1/5] Initial draft for contract.events.X.getLogs() --- tests/core/filtering/test_contract_getLogs.py | 47 ++++++++++ web3/contract.py | 93 +++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 tests/core/filtering/test_contract_getLogs.py diff --git a/tests/core/filtering/test_contract_getLogs.py b/tests/core/filtering/test_contract_getLogs.py new file mode 100644 index 0000000000..be04646e81 --- /dev/null +++ b/tests/core/filtering/test_contract_getLogs.py @@ -0,0 +1,47 @@ +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, +): + contract = emitter + event_id = 1 + + 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, +): + contract = emitter + event_id = 1 + + 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 diff --git a/web3/contract.py b/web3/contract.py index 6e68203183..e98aefda82 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -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 + + + 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) + + ... + """ def __init__(self, abi, web3, address=None): @@ -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. @@ -216,6 +241,8 @@ class Contract: clone_bin = None functions = None + + #: Instance of :class:`ContractEvents` presenting available Event ABIs events = None dev_doc = None @@ -1323,6 +1350,72 @@ 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"]) + + :param argument_filters: TODO + :param fromBlock: block number, defaults to 1 + :param toBlock: "block number or "latest", defaults to "latest" + :yield: Iterable of dictionatries + """ + + if not self.address: + raise TypeError("This method can be only called on " + "an instiated contract with an adress") + + 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 + for entry in logs: + yield get_event_data(abi, entry) + @classmethod def factory(cls, class_name, **kwargs): return PropertyCheckingFactory(class_name, (cls,), kwargs) From cd556685850b810659c323caaf4cf1286a1227c7 Mon Sep 17 00:00:00 2001 From: Mikko Ohtamaa Date: Mon, 14 Jan 2019 22:13:35 +0000 Subject: [PATCH 2/5] Fix lint, spelling, more API doc, return tuples instead of iterator --- tests/core/filtering/test_contract_getLogs.py | 2 ++ web3/contract.py | 28 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/core/filtering/test_contract_getLogs.py b/tests/core/filtering/test_contract_getLogs.py index be04646e81..97b322be58 100644 --- a/tests/core/filtering/test_contract_getLogs.py +++ b/tests/core/filtering/test_contract_getLogs.py @@ -1,3 +1,5 @@ + + def test_contract_get_available_events( emitter, ): diff --git a/web3/contract.py b/web3/contract.py index e98aefda82..3024bac06a 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -1381,15 +1381,36 @@ def getLogs(self, 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 :param fromBlock: block number, defaults to 1 :param toBlock: "block number or "latest", defaults to "latest" - :yield: Iterable of dictionatries + :yield: Tuple of :class:`AttributeDict` instances """ if not self.address: raise TypeError("This method can be only called on " - "an instiated contract with an adress") + "an instated contract with an address") abi = self._get_event_abi() @@ -1413,8 +1434,7 @@ def getLogs(self, logs = self.web3.eth.getLogs(event_filter_params) # Convert raw binary data to Python proxy objects as described by ABI - for entry in logs: - yield get_event_data(abi, entry) + return (get_event_data(abi, entry) for entry in logs) @classmethod def factory(cls, class_name, **kwargs): From bc3e52f8da7275d25a98127588229e324654d9b7 Mon Sep 17 00:00:00 2001 From: Mikko Ohtamaa Date: Mon, 14 Jan 2019 22:25:58 +0000 Subject: [PATCH 3/5] Added argument_filters test --- tests/core/filtering/test_contract_getLogs.py | 50 ++++++++++++++++++- web3/contract.py | 2 +- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/tests/core/filtering/test_contract_getLogs.py b/tests/core/filtering/test_contract_getLogs.py index 97b322be58..b4229d1523 100644 --- a/tests/core/filtering/test_contract_getLogs.py +++ b/tests/core/filtering/test_contract_getLogs.py @@ -13,9 +13,10 @@ def test_contract_getLogs_all( web3, emitter, wait_for_transaction, + emitter_event_ids, ): contract = emitter - event_id = 1 + event_id = emitter_event_ids.LogNoArguments txn_hash = contract.functions.logNoArgs(event_id).transact() wait_for_transaction(web3, txn_hash) @@ -29,9 +30,10 @@ def test_contract_getLogs_range( web3, emitter, wait_for_transaction, + emitter_event_ids, ): contract = emitter - event_id = 1 + event_id = emitter_event_ids.LogNoArguments assert web3.eth.blockNumber == 2 txn_hash = contract.functions.logNoArgs(event_id).transact() @@ -47,3 +49,47 @@ def test_contract_getLogs_range( 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) + 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 diff --git a/web3/contract.py b/web3/contract.py index 3024bac06a..972663ba37 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -1434,7 +1434,7 @@ def getLogs(self, logs = self.web3.eth.getLogs(event_filter_params) # Convert raw binary data to Python proxy objects as described by ABI - return (get_event_data(abi, entry) for entry in logs) + return tuple(get_event_data(abi, entry) for entry in logs) @classmethod def factory(cls, class_name, **kwargs): From 052e50ccc6362f9889550f6f98f5de4ac016bb5d Mon Sep 17 00:00:00 2001 From: Mikko Ohtamaa Date: Mon, 14 Jan 2019 22:39:53 +0000 Subject: [PATCH 4/5] Doc updates --- docs/filters.rst | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/filters.rst b/docs/filters.rst index 47d504ca19..5da081ef1a 100644 --- a/docs/filters.rst +++ b/docs/filters.rst @@ -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')`` @@ -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 ------------ @@ -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 ------------------------------ From 518e38eddbdc8442e498b918135cac7ca88758bb Mon Sep 17 00:00:00 2001 From: Keri Date: Thu, 17 Jan 2019 10:43:10 -0700 Subject: [PATCH 5/5] Fix nitpicks --- docs/filters.rst | 2 +- tests/core/filtering/test_contract_getLogs.py | 2 +- web3/contract.py | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/filters.rst b/docs/filters.rst index 5da081ef1a..fd169dbccc 100644 --- a/docs/filters.rst +++ b/docs/filters.rst @@ -168,7 +168,7 @@ un-indexed event arguments. The parameter ``data_filter_set`` should be a list o Getting events without setting up a filter ------------------------------------------ -You can query Ethereum node for direct fetch of events, without creating a filter first. +You can query an 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`. diff --git a/tests/core/filtering/test_contract_getLogs.py b/tests/core/filtering/test_contract_getLogs.py index b4229d1523..ecf053d3d9 100644 --- a/tests/core/filtering/test_contract_getLogs.py +++ b/tests/core/filtering/test_contract_getLogs.py @@ -82,7 +82,7 @@ def test_contract_getLogs_argument_filter( all_logs = contract.events.LogTripleWithIndex.getLogs() assert len(all_logs) == 4 - # Filter all entries where arg2 in (1, 2) + # Filter all entries where arg1 in (1, 2) partial_logs = contract.events.LogTripleWithIndex.getLogs( argument_filters={'arg1': [1, 2]}, ) diff --git a/web3/contract.py b/web3/contract.py index 972663ba37..c6fc5fa67e 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -1357,24 +1357,24 @@ def getLogs(self, toBlock="latest"): """Get events for this contract instance using eth_getLogs API. - This is a stateless method, as opposite to createFilter. + This is a stateless method, as opposed 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. + the Ethereum node might be overloaded and timeout + on the 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 + from = max(mycontract.web3.eth.blockNumber - 10, 1) + to = mycontract.web3.eth.blockNumber - events = mycontract.events.Transfer.getLogs(fromBlock=f, toBlock=t) + events = mycontract.events.Transfer.getLogs(fromBlock=from, toBlock=to) for e in events: print(e["args"]["from"], @@ -1402,9 +1402,9 @@ def getLogs(self, See also: :func:`web3.middleware.filter.local_filter_middleware`. - :param argument_filters: TODO + :param argument_filters: :param fromBlock: block number, defaults to 1 - :param toBlock: "block number or "latest", defaults to "latest" + :param toBlock: block number or "latest". Defaults to "latest" :yield: Tuple of :class:`AttributeDict` instances """