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

Initialize contracts having function or event overrides #3476

Open
wants to merge 30 commits into
base: main
Choose a base branch
from

Conversation

reedsa
Copy link
Contributor

@reedsa reedsa commented Sep 4, 2024

What was wrong?

Closes #1704, #3482

The Contract class cannot initialize if the contract contains overloaded functions or events. Currently only the function/event name is used to filter ABI events from the contract ABI, so an exception is raised. Currently, events raise a ValueError when _get_event_abi is called on the Contract or ContractEvent class. Functions raise a MismatchedABI error.

Example:
BaseContractEvent._get_event_abi() calls eth-utils get_event_abi() and expects to return the ABI for the event class. The event classes are not created for every overloaded function. When there are contract events with the same name, only one is ever created and used.

How was it fixed?

Initialize the ContractFunctions or ContractEvents class with every ABI found in the contract, storing the abi signature in the class name. Along with the signature, the abi property is set for each class within the __init__ method. This ensures the correct instance is found and used for subsequent functions like get_logs().

get_abi_element now handles both functions and events using the ABI signature to find a unique ABI. The signature is represented by a string that includes it's name and list of input types within parentheses. name(arg1Type,arg2Type,...)

Exceptions are now shared between functions and events for consistency.

Todo:

  • Clean up commit history
  • Add or update documentation related to these changes
  • Add entry to the release notes

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->

@reedsa reedsa force-pushed the get-event-abi-ambiguity branch 4 times, most recently from bdc2282 to 4b2adad Compare September 5, 2024 21:07
reedsa added a commit to reedsa/web3.py that referenced this pull request Sep 5, 2024
@reedsa reedsa marked this pull request as ready for review September 5, 2024 22:36
Copy link
Collaborator

@kclowes kclowes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Just left a few nits!

@@ -760,3 +785,4 @@ def async_call(request):
@pytest.fixture
def async_estimate_gas(request):
return async_partial(async_invoke_contract, api_call_desig="estimate_gas")
return async_partial(async_invoke_contract, api_call_desig="estimate_gas")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy pasta?

assert event_abi == ABI_EVENT_DEPOSIT


def test_get_abi_element_with_signature_for_amibguous_tuple_events() -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
def test_get_abi_element_with_signature_for_amibguous_tuple_events() -> None:
def test_get_abi_element_with_signature_for_amibiguous_tuple_events() -> None:

tests/core/utilities/test_abi.py Show resolved Hide resolved
def __init__(
self,
*argument_names: Tuple[str],
abi: ABIEvent = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit?

Suggested change
abi: ABIEvent = None,
abi: Optional[ABIEvent] = None,


def __getattr__(self, event_name: str) -> Type["BaseContractEvent"]:
if "_events" not in self.__dict__:
raise NoABIEventsFound(
"The abi for this contract contains no event definitions. ",
"Are you sure you provided the correct contract abi?",
)
elif event_name not in self.__dict__["_events"]:
elif event_name.split("(")[0] not in [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this event_name.split... may deserve to be its own method since you're using it so much in here. Feel free to take or leave :)

@@ -0,0 +1 @@
Contracts which overloaded functions or events are now supported. The Contract initializes functions and events using an identifier to distinguish between them. The identifier is the function or event signature, which consists of the name and the parameter types.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
Contracts which overloaded functions or events are now supported. The Contract initializes functions and events using an identifier to distinguish between them. The identifier is the function or event signature, which consists of the name and the parameter types.
Contracts with overloaded functions or events are now supported. The Contract initializes functions and events using an identifier to distinguish between them. The identifier is the function or event signature, which consists of the name and the parameter types.

*self.args,
abi_codec=self.w3.codec,
**self.kwargs,
cls.contract_abi, get_abi_element_identifier(abi_element_identifier)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need the codec passed too?

@reedsa
Copy link
Contributor Author

reedsa commented Sep 6, 2024

The ambiguity with functions is also an issue unfortunately. Taking a look at how to disambiguate since right now it uses the "first" one.

reedsa added a commit to reedsa/web3.py that referenced this pull request Sep 10, 2024
@reedsa reedsa force-pushed the get-event-abi-ambiguity branch 2 times, most recently from 7b15976 to 4354b1a Compare September 11, 2024 01:03
@reedsa reedsa requested review from kclowes, fselmo and pacrob and removed request for fselmo, pacrob and kclowes September 11, 2024 19:51
@reedsa reedsa changed the title Initialize contracts with ambiguous events Initialize contracts having function or event overrides Sep 11, 2024
@@ -173,14 +173,14 @@ def test_offchain_resolver_function_call_raises_with_ccip_read_disabled(
with pytest.raises(OffchainLookup):
offchain_resolver.functions.resolve(
ens_encode_name("offchainexample.eth"),
ENCODED_ADDR_CALLDATA,
ENCODED_ADDR_CALLDATA.encode(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the added encode here? Test passes for me with or without it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this after all, good callout.


def __iter__(self) -> Iterable["ContractEvent"]:
"""
Iterate over supported
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - docstring should state what the function does (not sure if it's necessary for a magic method?). Also should match for the same method in AsyncContractEvents.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, there's no practical information I provided there anyway. Removing!

) -> ABIElement:
"""
Return the interface for an ``ABIElement`` which matches the provided identifier
and arguments.
Return the interface for an ``ABIElement`` from the `abi` that matches the provided
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Return the interface for an ``ABIElement`` from the `abi` that matches the provided
Return the interface for an ``ABIElement`` from the ``abi`` that matches the provided

def get_event_abi(
abi: ABI,
event_name: str,
argument_names: Optional[Sequence[str]] = None,
) -> ABIEvent:
"""
.. warning::
This function is deprecated. It is unable to distinguish between
overloaded events. Use `get_abi_element` instead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
overloaded events. Use `get_abi_element` instead.
overloaded events. Use ``get_abi_element`` instead.

__getattr__ now uses the ABI to look up the function class by signature
Additional fixes for ens
Refactoring and docs update for ``get_abi_element`` and ``_build_filters``

Rename ``get_abi_element_identifier`` -> ``get_abi_element_signature``
…ith_name``

Further cleanup and refactoring
Remove docs from events iter
Fix docstring formatting
filter is a partial function that takes a contract ABI and returns a filtered list.
Each parameter is checked before applying the relevant filter.

When the `abi_element_identifier` is a function name or signature and no arguments
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
When the `abi_element_identifier` is a function name or signature and no arguments
When the ``abi_element_identifier`` is a function name or signature and no arguments

filters include the function name, argument count, argument name, argument type,
and argument encodability.

`constructor`, `fallback`, and `receive` ABI elements are handled only with a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`constructor`, `fallback`, and `receive` ABI elements are handled only with a
``constructor``, ``fallback``, and ``receive`` ABI elements are handled only with a

:param abi_element_identifier: Find an element ABI with matching identifier.
:param abi_element_identifier: Find an element ABI with matching identifier. The \
identifier may be a function name, signature, or ``FallbackFn`` or ``ReceiveFn``. \
A function signature is in the form `name(arg1Type,arg2Type,...)`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A function signature is in the form `name(arg1Type,arg2Type,...)`.
A function signature is in the form ``name(arg1Type,arg2Type,...)``.

Copy link
Contributor

@pacrob pacrob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐑

Copy link
Collaborator

@kclowes kclowes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet! I didn't see anything major, just left a few small comments.

function_name_tester_contract_abi,
function_name_tester_contract,
):
# test `w3` function name does not throw when creating the contract factory
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# test `w3` function name does not throw when creating the contract factory
# test `abi` function name does not throw when creating the contract factory

# re-instantiate the contract
contract = contract_factory(function_name_tester_contract.address)

# Contract w3 function should not override web3 instance
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something to test that contract.abi doesn't get overridden here instead?


assert contract.abi == function_name_tester_contract_abi

# assert the `w3` function returns true when called
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# assert the `w3` function returns true when called
# assert the `abi` function returns true when called

function_name_tester_contract_abi,
async_function_name_tester_contract,
):
# test `w3` function name does not throw when creating the contract factory
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# test `w3` function name does not throw when creating the contract factory
# test `abi` function name does not throw when creating the contract factory

Comment on lines 80 to 82
# Contract w3 function should not override web3 instance
with pytest.raises(AttributeError):
contract.functions.abi.get_block("latest")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a test to make sure abi doesn't get overridden here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a slight adjustment to the order of the assertion/exception cases and updated the inline comments to be accurate. Let me know if there's further clarity needed there!


assert contract.abi == function_name_tester_contract_abi

# assert the `w3` function returns true when called
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# assert the `w3` function returns true when called
# assert the `abi` function returns true when called

@@ -486,23 +516,49 @@ class BaseContractFunction:
args: Any = None
kwargs: Any = None

def __init__(self, abi: Optional[ABIFunction] = None) -> None:
def __init__(self, *arguments: Any, abi: Optional[ABIFunction] = None) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change, since I believe now abi has to be passed in as a kwarg. Does that change anything for the UI?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was originally intended to bring the same interface to BaseContractFunction and BaseContractEvent, but the arugments are never actually used.

I've also taken further steps to eliminate extraneous arguments passed to the factory function. Passing the signature is enough to find the ABI that is needed later on. I've made updates to the async/sync contracts so initialization of the ABI still works as intended.

abi: Optional[ABIEvent] = None,
abi_element_identifier: Optional[ABIElementIdentifier] = None,
) -> None:
# event was referenced with parens, juts return the event
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
# event was referenced with parens, juts return the event
# event was referenced with parens, just return the event

@@ -119,6 +122,34 @@ def format_contract_call_return_data_curried(
return normalized_data[0] if len(normalized_data) == 1 else normalized_data


def copy_contract_function(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we gain anything by exposing this publicly. Consider moving these two methods to web3/_utils/contracts.py

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, moved!


event_get_abi_result = ambiguous_event_contract.events[
"LogSingleArg(uint256)"
]._get_event_abi()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a stylistic nitpick, but it seems a little smelly to me to test a private method. It looks like _get_event_abi is used to assign the abi attribute on ContractEvent. Would it be possible to do something like:

Suggested change
]._get_event_abi()
event_get_abi_result = ambiguous_event_contract.events[
"LogSingleArg(uint256)"
].abi

Feel free to take or leave if that doesn't make sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely better as .abi 👍

Copy link
Contributor

@pacrob pacrob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐑

Copy link
Collaborator

@kclowes kclowes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Since this is such a core component of our library, it's probably worth some manual testing to make sure old functionality still works as expected. I did some myself and have fairly high confidence in the tests, but it's probably worth one more run through!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Events cannot retrieve their ABI if multiple events have the same name
3 participants