Skip to content

Commit

Permalink
feat: adds .abi property to ContractLog (#2504)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Feb 13, 2025
1 parent f86b325 commit cdcddf5
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 18 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ repos:
- id: check-yaml

- repo: https://github.com/PyCQA/isort
rev: 5.13.2
rev: 6.0.0
hooks:
- id: isort

- repo: https://github.com/psf/black
rev: 24.10.0
rev: 25.1.0
hooks:
- id: black
name: black
Expand All @@ -22,7 +22,7 @@ repos:
additional_dependencies: [flake8-breakpoint, flake8-print, flake8-pydantic, flake8-type-checking]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.14.1
rev: v1.15.0
hooks:
- id: mypy
additional_dependencies: [
Expand All @@ -37,7 +37,7 @@ repos:
]

- repo: https://github.com/executablebooks/mdformat
rev: 0.7.19
rev: 0.7.22
hooks:
- id: mdformat
additional_dependencies: [mdformat-gfm, mdformat-frontmatter, mdformat-pyproject]
Expand Down
8 changes: 4 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"hypothesis-jsonschema==0.19.0", # JSON Schema fuzzer extension
],
"lint": [
"black>=24.10.0,<25", # Auto-formatter and linter
"mypy>=1.14.1,<1.15.0", # Static type analyzer
"black>=25.1.0,<26", # Auto-formatter and linter
"mypy>=1.15.0,<1.16.0", # Static type analyzer
"types-PyYAML", # Needed due to mypy typeshed
"types-requests", # Needed due to mypy typeshed
"types-setuptools", # Needed due to mypy typeshed
Expand All @@ -36,8 +36,8 @@
"flake8-print>=4.0.1,<5", # Detect print statements left in code
"flake8-pydantic", # For detecting issues with Pydantic models
"flake8-type-checking", # Detect imports to move in/out of type-checking blocks
"isort>=5.13.2,<6", # Import sorting linter
"mdformat>=0.7.19", # Auto-formatter for markdown
"isort>=6.0.0,<7", # Import sorting linter
"mdformat>=0.7.22", # Auto-formatter for markdown
"mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown
"mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates
"mdformat-pyproject>=0.0.2", # Allows configuring in pyproject.toml
Expand Down
2 changes: 1 addition & 1 deletion src/ape/managers/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

@contextlib.contextmanager
def _use_sender(
account: Union[AccountAPI, TestAccountAPI]
account: Union[AccountAPI, TestAccountAPI],
) -> "Generator[AccountAPI, TestAccountAPI, None]":
try:
_DEFAULT_SENDERS.append(account)
Expand Down
8 changes: 4 additions & 4 deletions src/ape/plugins/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class EcosystemPlugin(PluginType):
what is required to implement an ecosystem plugin.
"""

@hookspec # type: ignore[empty-body]
def ecosystems(self) -> Iterator[type["EcosystemAPI"]]:
@hookspec
def ecosystems(self) -> Iterator[type["EcosystemAPI"]]: # type: ignore[empty-body]
"""
A hook that must return an iterator of :class:`ape.api.networks.EcosystemAPI`
subclasses.
Expand All @@ -40,8 +40,8 @@ class NetworkPlugin(PluginType):
networks.
"""

@hookspec # type: ignore[empty-body]
def networks(self) -> Iterator[tuple[str, str, type["NetworkAPI"]]]:
@hookspec
def networks(self) -> Iterator[tuple[str, str, type["NetworkAPI"]]]: # type: ignore[empty-body]
"""
A hook that must return an iterator of tuples of:
Expand Down
4 changes: 2 additions & 2 deletions src/ape/plugins/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class ProjectPlugin(PluginType):
via ``.gitmodules``.
"""

@hookspec # type: ignore[empty-body]
def projects(self) -> Iterator[type["ProjectAPI"]]:
@hookspec
def projects(self) -> Iterator[type["ProjectAPI"]]: # type: ignore[empty-body]
"""
A hook that returns a :class:`~ape.api.projects.ProjectAPI` subclass type.
Expand Down
4 changes: 2 additions & 2 deletions src/ape/plugins/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class QueryPlugin(PluginType):
A plugin for querying chains.
"""

@hookspec # type: ignore[empty-body]
def query_engines(self) -> Iterator[type["QueryAPI"]]:
@hookspec
def query_engines(self) -> Iterator[type["QueryAPI"]]: # type: ignore[empty-body]
"""
A hook that returns an iterator of types of a ``QueryAPI`` subclasses
Expand Down
40 changes: 40 additions & 0 deletions src/ape/types/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from pydantic import BaseModel, field_serializer, field_validator, model_validator
from web3.types import FilterParams

from ape.exceptions import ContractNotFoundError
from ape.types.address import AddressType
from ape.types.basic import HexInt
from ape.utils.abi import LogInputABICollection
from ape.utils.basemodel import BaseInterfaceModel, ExtraAttributesMixin, ExtraModelAttributes
from ape.utils.misc import ZERO_ADDRESS, log_instead_of_fail

Expand Down Expand Up @@ -175,6 +177,14 @@ class ContractLog(ExtraAttributesMixin, BaseContractLog):
An instance of a log from a contract.
"""

def __init__(self, *args, **kwargs):
abi = kwargs.pop("_abi", None)
super().__init__(*args, **kwargs)
if isinstance(abi, LogInputABICollection):
abi = abi.abi

self._abi = abi

transaction_hash: Any
"""The hash of the transaction containing this log."""

Expand Down Expand Up @@ -216,6 +226,36 @@ def _serialize_hashes(self, value, info):
def block(self) -> "BlockAPI":
return self.chain_manager.blocks[self.block_number]

@property
def abi(self) -> EventABI:
"""
An ABI describing the event.
"""
if abi := self._abi:
return abi

try:
contract = self.chain_manager.contracts[self.contract_address]
except (ContractNotFoundError, KeyError):
pass

else:
for event_abi in contract.events:
if event_abi.name == self.event_name and len(event_abi.inputs) == len(
self.event_arguments
):
abi = event_abi

if abi is None:
# Last case scenario, try to calculate it.
# NOTE: This is a rare edge case that shouldn't really happen,
# so this is a lower priorty.
# TODO: Handle inputs here.
abi = EventABI(name=self.event_name)

self._abi = abi
return abi

@property
def timestamp(self) -> int:
"""
Expand Down
1 change: 1 addition & 0 deletions src/ape_ethereum/ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,7 @@ def get_abi(_topic: HexStr) -> Optional[LogInputABICollection]:
converted_arguments[key] = value

yield ContractLog(
_abi=abi,
block_hash=log.get("blockHash") or log.get("block_hash") or "",
block_number=log.get("blockNumber") or log.get("block_number") or 0,
contract_address=self.decode_address(log["address"]),
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/test_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def test_sign_message_with_prompts(runner, keyfile_account, message):

def test_sign_raw_hash(runner, keyfile_account):
# NOTE: `message` is a 32 byte raw hash, which is treated specially
message = b"\xAB" * 32
message = b"\xab" * 32

# "y\na\ny": yes sign raw hash, password, yes keep unlocked
with runner.isolation(input=f"y\n{PASSPHRASE}\ny"):
Expand Down
5 changes: 5 additions & 0 deletions tests/functional/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ def test_get_contract_logs_single_log(chain, contract_instance, owner, eth_teste
logs = [log for log in eth_tester_provider.get_contract_logs(log_filter)]
assert len(logs) == 1
assert logs[0]["foo"] == 0
assert logs[0].abi == contract_instance.FooHappened.abi

# Show it looks up the ABI when not cached not anymore.
logs[0]._abi = None
assert logs[0].abi == contract_instance.FooHappened.abi


def test_get_contract_logs_single_log_query_multiple_values(
Expand Down
5 changes: 5 additions & 0 deletions tests/functional/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ def test_contract_log_access(log):
assert log.bar == log["bar"] == log.get("bar") == 1


def test_contract_log_abi(log):
# The log fixture is very basic class usage.
assert log.abi.name == "MyEvent"


def test_topic_filter_encoding():
event_abi = EventABI.model_validate_json(RAW_EVENT_ABI)
log_filter = LogFilter.from_event(
Expand Down

0 comments on commit cdcddf5

Please sign in to comment.