Skip to content

Commit 40cc3f9

Browse files
author
Stuart Reed
committed
Contract utils for get_event_by_signature, get_event_by_selector, get_event_by_topic
1 parent 2639e7b commit 40cc3f9

File tree

4 files changed

+237
-7
lines changed

4 files changed

+237
-7
lines changed

docs/web3.contract.rst

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,18 @@ Each Contract Factory exposes the following methods.
345345
[<Event Transfer(uint256,bool)>, <Event Transfer(int256,bool)>]
346346
347347
348+
.. py:classmethod:: Contract.get_event_by_signature(signature)
349+
350+
Searches for a distinct event with matching signature. Returns an instance of
351+
:py:class:`ContractEvent` upon finding a match. Raises ``Web3ValueError`` if no
352+
match is found.
353+
354+
.. code-block:: python
355+
356+
>>> contract.get_event_by_signature('Transfer(uint256,bool)')
357+
<Event Transfer(uint256,bool)>
358+
359+
348360
.. py:classmethod:: Contract.find_events_by_name(name)
349361
350362
Searches for all events matching the provided name. Returns a list of matching
@@ -368,6 +380,55 @@ Each Contract Factory exposes the following methods.
368380
>>> contract.get_event_by_name('UniqueEvent')
369381
<Event UniqueEvent(uint256)>
370382
383+
.. py:classmethod:: Contract.find_events_by_selector(selector)
384+
385+
Searches for all events matching the provided selector. Returns a list of matching
386+
events where every event is an instance of :py:class:`ContractEvent`. Returns an
387+
empty list when no match is found.
388+
389+
.. code-block:: python
390+
391+
>>> contract.find_events_by_selector('0xf70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb15')
392+
[<Event LogSingleWithIndex(uint256)>]
393+
394+
.. py:classmethod:: Contract.get_event_by_selector(selector)
395+
396+
Searches for a distinct event with matching selector.
397+
The selector can be a hexadecimal string, bytes or int.
398+
Returns an instance of :py:class:`ContractEvent` upon finding a match.
399+
Raises ``Web3ValueError`` if no match is found.
400+
401+
.. code-block:: python
402+
403+
>>> contract.get_event_by_selector('0xf70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb15')
404+
<Event LogSingleWithIndex(uint256)>
405+
>>> contract.get_event_by_selector(b"\xf7\x0f\xe6\x89\xe2\x90\xd8\xce+*8\x8a\xc2\x8d\xb3o\xbb\x0e\x16\xa6\xd8\x9ch\x04\xc4a\xf6Z\x1b@\xbb\x15")
406+
<Event LogSingleWithIndex(uint256)>
407+
>>> contract.get_event_by_selector(0xf70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb15)
408+
<Event LogSingleWithIndex(uint256)>
409+
410+
.. py:classmethod:: Contract.find_events_by_topic(topic)
411+
412+
Searches for all events matching the provided topic. Returns a list of matching
413+
events where every event is an instance of :py:class:`ContractEvent`. Returns an
414+
empty list when no match is found.
415+
416+
.. code-block:: python
417+
418+
>>> contract.find_events_by_topic('0xf70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb15')
419+
[<Event LogSingleWithIndex(uint256)>]
420+
421+
.. py:classmethod:: Contract.get_event_by_topic(topic)
422+
423+
Searches for a distinct event with matching topic.
424+
The topic is a hexadecimal string.
425+
Returns an instance of :py:class:`ContractEvent` upon finding a match.
426+
Raises ``Web3ValueError`` if no match is found.
427+
428+
.. code-block:: python
429+
430+
>>> contract.get_event_by_topic('0xf70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb15')
431+
<Event LogSingleWithIndex(uint256)>
371432
372433
.. py:classmethod:: Contract.all_functions()
373434

tests/core/contracts/test_contract_events.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939
"<Event LogSingleWithIndex(uint256)>",
4040
],
4141
),
42+
(
43+
"get_event_by_signature",
44+
("LogSingleArg(uint256)",),
45+
repr,
46+
"<Event LogSingleArg(uint256)>",
47+
),
4248
(
4349
"find_events_by_name",
4450
("LogSingleArg",),
@@ -53,6 +59,42 @@
5359
repr,
5460
"<Event LogSingleArg(uint256)>",
5561
),
62+
(
63+
"find_events_by_selector",
64+
(
65+
b"\xf7\x0f\xe6\x89\xe2\x90\xd8\xce+*8\x8a\xc2\x8d\xb3o\xbb\x0e\x16\xa6\xd8\x9ch\x04\xc4a\xf6Z\x1b@\xbb\x15", # noqa: E501
66+
),
67+
map_repr,
68+
[
69+
"<Event LogSingleWithIndex(uint256)>",
70+
],
71+
),
72+
(
73+
"get_event_by_selector",
74+
(
75+
b"\xf7\x0f\xe6\x89\xe2\x90\xd8\xce+*8\x8a\xc2\x8d\xb3o\xbb\x0e\x16\xa6\xd8\x9ch\x04\xc4a\xf6Z\x1b@\xbb\x15", # noqa: E501
76+
),
77+
repr,
78+
"<Event LogSingleWithIndex(uint256)>",
79+
),
80+
(
81+
"get_event_by_selector",
82+
(0xF70FE689E290D8CE2B2A388AC28DB36FBB0E16A6D89C6804C461F65A1B40BB15,),
83+
repr,
84+
"<Event LogSingleWithIndex(uint256)>",
85+
),
86+
(
87+
"get_event_by_selector",
88+
("0xf70fe689e290d8ce2b2a388ac28db36fbb0e16a6d89c6804c461f65a1b40bb15",),
89+
repr,
90+
"<Event LogSingleWithIndex(uint256)>",
91+
),
92+
(
93+
"get_event_by_topic",
94+
("0x56d2ef3c5228bf5d88573621e325a4672ab50e033749a601e4f4a5e1dce905d4",),
95+
repr,
96+
"<Event LogSingleArg(uint256)>",
97+
),
5698
),
5799
)
58100
def test_find_or_get_events_by_type(
@@ -64,8 +106,8 @@ def test_find_or_get_events_by_type(
64106
event_contract: "Contract",
65107
) -> None:
66108
contract = w3.eth.contract(abi=event_contract.abi)
67-
contract_function = getattr(contract, method)(*args)
68-
assert repr_func(contract_function) == expected
109+
contract_event = getattr(contract, method)(*args)
110+
assert repr_func(contract_event) == expected
69111

70112

71113
def test_find_events_by_identifier(w3: "Web3", event_contract: "Contract") -> None:
@@ -85,7 +127,7 @@ def test_get_event_by_identifier(w3: "Web3", event_contract: "Contract") -> None
85127
assert repr(contract_event) == "<Event LogSingleArg(uint256)>"
86128

87129

88-
def test_functions_iterator(math_contract):
130+
def test_events_iterator(math_contract):
89131
all_events = math_contract.all_events()
90132
events_iter = math_contract.events
91133

@@ -94,7 +136,7 @@ def test_functions_iterator(math_contract):
94136
assert event.name == expected_event.name
95137

96138

97-
def test_async_functions_iterator(async_math_contract):
139+
def test_async_events_iterator(async_math_contract):
98140
all_events = async_math_contract.all_events()
99141
events_iter = async_math_contract.events
100142

web3/contract/base_contract.py

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
get_normalized_abi_inputs,
4040
is_list_like,
4141
is_text,
42+
keccak,
43+
to_bytes,
4244
to_tuple,
4345
)
4446
from hexbytes import (
@@ -67,6 +69,7 @@
6769
empty,
6870
)
6971
from web3._utils.encoding import (
72+
hexstr_if_str,
7073
to_4byte_hex,
7174
to_hex,
7275
)
@@ -784,7 +787,7 @@ def encode_abi(
784787
) -> HexStr:
785788
"""
786789
Encodes the arguments using the Ethereum ABI for the contract function
787-
that matches the given name and arguments..
790+
that matches the given name and arguments.
788791
789792
:param data: defaults to function selector
790793
"""
@@ -811,12 +814,20 @@ def encode_abi(
811814
def all_functions(
812815
self,
813816
) -> "BaseContractFunction":
817+
"""
818+
Return all functions in the contract.
819+
"""
814820
return self.find_functions_by_identifier(
815821
self.abi, self.w3, self.address, lambda _: True
816822
)
817823

818824
@combomethod
819825
def get_function_by_signature(self, signature: str) -> "BaseContractFunction":
826+
"""
827+
Return a distinct function with matching signature.
828+
Raises a Web3ValueError if the signature is invalid or if there is no match or
829+
more than one is found.
830+
"""
820831
if " " in signature:
821832
raise Web3ValueError(
822833
"Function signature should not contain any spaces. "
@@ -833,6 +844,11 @@ def callable_check(fn_abi: ABIFunction) -> bool:
833844

834845
@combomethod
835846
def find_functions_by_name(self, fn_name: str) -> "BaseContractFunction":
847+
"""
848+
Return all functions with matching name.
849+
Raises a Web3ValueError if there is no match or more than one is found.
850+
"""
851+
836852
def callable_check(fn_abi: ABIFunction) -> bool:
837853
return fn_abi["name"] == fn_name
838854

@@ -842,13 +858,22 @@ def callable_check(fn_abi: ABIFunction) -> bool:
842858

843859
@combomethod
844860
def get_function_by_name(self, fn_name: str) -> "BaseContractFunction":
861+
"""
862+
Return a distinct function with matching name.
863+
Raises a Web3ValueError if there is no match or more than one is found.
864+
"""
845865
fns = self.find_functions_by_name(fn_name)
846866
return self.get_function_by_identifier(fns, "name")
847867

848868
@combomethod
849869
def get_function_by_selector(
850870
self, selector: Union[bytes, int, HexStr]
851871
) -> "BaseContractFunction":
872+
"""
873+
Return a distinct function with matching 4byte selector.
874+
Raises a Web3ValueError if there is no match or more than one is found.
875+
"""
876+
852877
def callable_check(fn_abi: ABIFunction) -> bool:
853878
return encode_hex(function_abi_to_4byte_selector(fn_abi)) == to_4byte_hex(
854879
selector
@@ -863,6 +888,9 @@ def callable_check(fn_abi: ABIFunction) -> bool:
863888
def decode_function_input(
864889
self, data: HexStr
865890
) -> Tuple["BaseContractFunction", Dict[str, Any]]:
891+
"""
892+
Return a Tuple of the function selector and decoded arguments.
893+
"""
866894
func = self.get_function_by_selector(HexBytes(data)[:4])
867895
arguments = decode_transaction_data(
868896
func.abi, data, normalizers=BASE_RETURN_NORMALIZERS
@@ -871,6 +899,11 @@ def decode_function_input(
871899

872900
@combomethod
873901
def find_functions_by_args(self, *args: Any) -> "BaseContractFunction":
902+
"""
903+
Return all functions with matching args, checking each argument can be encoded
904+
with the type.
905+
"""
906+
874907
def callable_check(fn_abi: ABIFunction) -> bool:
875908
return check_if_arguments_can_be_encoded(
876909
fn_abi,
@@ -885,20 +918,54 @@ def callable_check(fn_abi: ABIFunction) -> bool:
885918

886919
@combomethod
887920
def get_function_by_args(self, *args: Any) -> "BaseContractFunction":
921+
"""
922+
Return a distinct function with matching args, checking each argument can be
923+
encoded with the type.
924+
Raises a Web3ValueError if there is no match or more than one is found.
925+
"""
888926
fns = self.find_functions_by_args(*args)
889927
return self.get_function_by_identifier(fns, "args")
890928

891929
#
892930
# Events API
893931
#
894932
@combomethod
895-
def all_events(self) -> "BaseContractEvent":
933+
def all_events(self) -> List["BaseContractEvent"]:
934+
"""
935+
Return all events in the contract.
936+
"""
896937
return self.find_events_by_identifier(
897938
self.abi, self.w3, self.address, lambda _: True
898939
)
899940

900941
@combomethod
901-
def find_events_by_name(self, event_name: str) -> "BaseContractEvent":
942+
def get_event_by_signature(self, signature: str) -> "BaseContractEvent":
943+
"""
944+
Return a distinct event with matching signature.
945+
Raises a Web3ValueError if the signature is invalid or if there is no match or
946+
more than one is found.
947+
"""
948+
if " " in signature:
949+
raise Web3ValueError(
950+
"Event signature should not contain any spaces. "
951+
f"Found spaces in input: {signature}"
952+
)
953+
954+
def callable_check(event_abi: ABIEvent) -> bool:
955+
return abi_to_signature(event_abi) == signature
956+
957+
events = self.find_events_by_identifier(
958+
self.abi, self.w3, self.address, callable_check
959+
)
960+
return self.get_event_by_identifier(events, "signature")
961+
962+
@combomethod
963+
def find_events_by_name(self, event_name: str) -> List["BaseContractEvent"]:
964+
"""
965+
Return all events with matching name.
966+
Raises a Web3ValueError if there is no match or more than one is found.
967+
"""
968+
902969
def callable_check(fn_abi: ABIFunction) -> bool:
903970
return fn_abi["name"] == event_name
904971

@@ -908,9 +975,68 @@ def callable_check(fn_abi: ABIFunction) -> bool:
908975

909976
@combomethod
910977
def get_event_by_name(self, event_name: str) -> "BaseContractEvent":
978+
"""
979+
Return a distinct event with matching name.
980+
Raises a Web3ValueError if there is no match or more than one is found.
981+
"""
911982
events = self.find_events_by_name(event_name)
912983
return self.get_event_by_identifier(events, "name")
913984

985+
@combomethod
986+
def find_events_by_selector(
987+
self, selector: Union[bytes, int, HexStr]
988+
) -> List["BaseContractEvent"]:
989+
"""
990+
Return all events with matching selector.
991+
Raises a Web3ValueError if there is no match or more than one is found.
992+
"""
993+
994+
def callable_check(event_abi: ABIEvent) -> bool:
995+
return encode_hex(
996+
keccak(text=abi_to_signature(event_abi).replace(" ", ""))
997+
) == encode_hex(hexstr_if_str(to_bytes, selector))
998+
999+
return self.find_events_by_identifier(
1000+
self.abi, self.w3, self.address, callable_check
1001+
)
1002+
1003+
@combomethod
1004+
def get_event_by_selector(
1005+
self, selector: Union[bytes, int, HexStr]
1006+
) -> "BaseContractEvent":
1007+
"""
1008+
Return a distinct event with matching keccak selector.
1009+
Raises a Web3ValueError if there is no match or more than one is found.
1010+
"""
1011+
events = self.find_events_by_selector(selector)
1012+
return self.get_event_by_identifier(events, "selector")
1013+
1014+
@combomethod
1015+
def find_events_by_topic(self, topic: HexStr) -> List["BaseContractEvent"]:
1016+
"""
1017+
Return all events with matching topic.
1018+
Raises a Web3ValueError if there is no match or more than one is found.
1019+
"""
1020+
1021+
def callable_check(event_abi: ABIEvent) -> bool:
1022+
return (
1023+
encode_hex(keccak(text=abi_to_signature(event_abi).replace(" ", "")))
1024+
== topic
1025+
)
1026+
1027+
return self.find_events_by_identifier(
1028+
self.abi, self.w3, self.address, callable_check
1029+
)
1030+
1031+
@combomethod
1032+
def get_event_by_topic(self, topic: HexStr) -> "BaseContractEvent":
1033+
"""
1034+
Return a distinct event with matching topic.
1035+
Raises a Web3ValueError if there is no match or more than one is found.
1036+
"""
1037+
events = self.find_events_by_topic(topic)
1038+
return self.get_event_by_identifier(events, "topic")
1039+
9141040
#
9151041
# Private Helpers
9161042
#

web3/contract/contract.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,7 @@ def find_events_by_identifier(
589589
def get_event_by_identifier(
590590
cls, events: Sequence["ContractEvent"], identifier: str
591591
) -> "ContractEvent":
592+
print("there", events)
592593
return get_event_by_identifier(events, identifier)
593594

594595

0 commit comments

Comments
 (0)