Skip to content

Commit cf5015f

Browse files
committed
Support anonymous fields/arguments
1 parent 4d97dae commit cf5015f

15 files changed

Lines changed: 592 additions & 348 deletions

docs/api.rst

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,17 @@ Contract ABI
151151
.. autoclass:: Receive
152152
:members:
153153

154+
.. autoclass:: Fields
155+
:members:
156+
157+
.. autoclass:: EventFields
158+
:members:
159+
:show-inheritance:
160+
161+
.. autoclass:: FieldValues
162+
:members:
163+
:special-members: __getitem__, __getattr__
164+
154165

155166
Testing utilities
156167
-----------------
@@ -255,12 +266,6 @@ Utility classes
255266

256267
Generic method type parameter.
257268

258-
.. autoclass:: pons._contract_abi.Signature()
259-
:members: canonical_form
260-
261-
.. autoclass:: pons._contract_abi.Method
262-
:members:
263-
264269

265270
Utility methods
266271
---------------

docs/changelog.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,31 @@ Changelog
22
---------
33

44

5-
0.10.1 (in development)
5+
0.11.0 (in development)
66
~~~~~~~~~~~~~~~~~~~~~~~
77

8+
Changed
9+
^^^^^^^
10+
11+
- ``ContractABI`` interface modified to allow for methods, errors, and events with arbitrary anonymous parameters and fields. Instead of returning ``dict[str, Any]`` as a parsed struct we now return a ``FieldValues`` object. Signatures of methods and fields of errors are exposed as ``Fields``, fields of events are exposed as ``EventFields`` objects. (PR_92_)
12+
13+
814
Added
915
^^^^^
1016

1117
- Support for contract errors with anonymous fields. (PR_90_)
1218
- Support for partially named method arguments. (PR_91_)
1319

1420

21+
Fixed
22+
^^^^^
23+
24+
- Name disambiguation for method parameters and struct fields. (PR_92_)
25+
26+
1527
.. _PR_90: https://github.com/fjarri-eth/pons/pull/90
1628
.. _PR_91: https://github.com/fjarri-eth/pons/pull/91
29+
.. _PR_92: https://github.com/fjarri-eth/pons/pull/92
1730

1831

1932
0.10.0 (2025-10-19)

pons/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@
3535
Either,
3636
Error,
3737
Event,
38+
EventFields,
3839
EventFilter,
3940
Fallback,
41+
Fields,
42+
FieldValues,
4043
Method,
4144
MethodCall,
4245
MultiMethod,
@@ -94,11 +97,14 @@
9497
"Either",
9598
"Error",
9699
"Event",
100+
"EventFields",
97101
"EventFilter",
98102
"Fallback",
99103
"FallbackProvider",
100104
"FallbackStrategy",
101105
"FallbackStrategyFactory",
106+
"FieldValues",
107+
"Fields",
102108
"HTTPError",
103109
"HTTPProvider",
104110
"InvalidResponse",

pons/_abi_types.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -506,23 +506,13 @@ def dispatch_type(abi_entry: ABI_JSON) -> Type:
506506
return type_from_abi_string(element_type_name)
507507

508508

509-
def dispatch_types(abi_entry: ABI_JSON) -> list[Type] | dict[str, Type]:
509+
def dispatch_parameter_types(abi_entry: ABI_JSON) -> list[tuple[str | None, Type]]:
510510
# TODO (#83): use proper validation
511511
abi_entry_typed = cast("Iterable[dict[str, Any]]", abi_entry)
512-
513-
names = [entry["name"] for entry in abi_entry_typed]
514-
515-
# In Solidity, it is possible to have have an argument list like `(address x, uint256)`
516-
# (that is, only some of the arguments are unnamed).
517-
# This cannot be mapped to Python function signatures,
518-
# so we have to treat this as if all the arguments were unnamed.
519-
if names and any(not name for name in names):
520-
return [dispatch_type(entry) for entry in abi_entry_typed]
521-
522-
# Since we are returning a dictionary, need to be sure we don't silently merge entries
523-
if len(names) != len(set(names)):
524-
raise ValueError("All ABI entries must have distinct names")
525-
return {entry["name"]: dispatch_type(entry) for entry in abi_entry_typed}
512+
return [
513+
(entry["name"] if entry["name"] != "" else None, dispatch_type(entry))
514+
for entry in abi_entry_typed
515+
]
526516

527517

528518
def encode_args(*types_and_args: tuple[Type, Any]) -> bytes:

pons/_client.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
ContractABI,
3737
Error,
3838
EventFilter,
39+
FieldValues,
3940
UnknownError,
4041
)
4142
from ._provider import Provider, ProviderError, ProviderSession
@@ -156,10 +157,10 @@ class ContractError(Exception):
156157
error: Error
157158
"""The recognized ABI Error object."""
158159

159-
data: dict[str, Any] | tuple[Any, ...]
160+
data: FieldValues
160161
"""The unpacked error data, corresponding to the ABI."""
161162

162-
def __init__(self, error: Error, decoded_data: dict[str, Any] | tuple[Any, ...]):
163+
def __init__(self, error: Error, decoded_data: FieldValues):
163164
super().__init__(error, decoded_data)
164165
self.error = error
165166
self.data = decoded_data
@@ -189,14 +190,16 @@ def decode_contract_error(
189190
except UnknownError:
190191
return ProviderError(exc)
191192

192-
# These errors have named fields, so `decoded_data` is expected to be a dictionary.
193-
# The assertions are there for `mypy`'s sake.
194193
if error == PANIC_ERROR:
195-
assert isinstance(decoded_data, dict) # noqa: S101
196-
return ContractPanic.from_code(decoded_data["code"])
194+
# If `resolve_error()` finished successfully,
195+
# `code` will be present in `decoded_data`
196+
# since it is declared as a field in the PANIC_ERROR
197+
return ContractPanic.from_code(decoded_data.as_dict["code"])
197198
if error == LEGACY_ERROR:
198-
assert isinstance(decoded_data, dict) # noqa: S101
199-
return ContractLegacyError(decoded_data["message"])
199+
# If `resolve_error()` finished successfully,
200+
# `message` will be present in `decoded_data`
201+
# since it is declared as a field in the PANIC_ERROR
202+
return ContractLegacyError(decoded_data.as_dict["message"])
200203

201204
return ContractError(error, decoded_data)
202205
return ProviderError(exc)
@@ -519,7 +522,7 @@ async def transact(
519522
amount: None | Amount = None,
520523
gas: None | int = None,
521524
return_events: None | Sequence[BoundEvent] = None,
522-
) -> dict[BoundEvent, list[dict[str, Any]]]:
525+
) -> dict[BoundEvent, list[FieldValues]]:
523526
"""
524527
Transacts with the contract using a prepared method call.
525528
If ``gas`` is ``None``, the required amount of gas is estimated first,
@@ -594,7 +597,7 @@ async def iter_events(
594597
poll_interval: int = 1,
595598
from_block: Block = BlockLabel.LATEST,
596599
to_block: Block = BlockLabel.LATEST,
597-
) -> AsyncIterator[dict[str, Any]]:
600+
) -> AsyncIterator[FieldValues]:
598601
"""
599602
Yields decoded log entries produced by the filter.
600603
The fields that were hashed when converted to topics (that is, fields of reference types)

pons/_contract.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Error,
1010
Event,
1111
EventFilter,
12+
FieldValues,
1213
Method,
1314
Methods,
1415
MultiMethod,
@@ -169,7 +170,7 @@ def __init__(self, contract_address: Address, event: Event, event_filter: EventF
169170
self.topics = event_filter.topics
170171
self._event = event
171172

172-
def decode_log_entry(self, log_entry: LogEntry) -> dict[str, Any]:
173+
def decode_log_entry(self, log_entry: LogEntry) -> FieldValues:
173174
if log_entry.address != self.contract_address:
174175
raise ValueError("Log entry originates from a different contract")
175176
return self._event.decode_log_entry(log_entry)

0 commit comments

Comments
 (0)