Skip to content

Commit 524d2c0

Browse files
committed
Support contract error structs with anonymous fields
1 parent d5891e9 commit 524d2c0

7 files changed

Lines changed: 59 additions & 22 deletions

File tree

docs/changelog.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ Changelog
22
---------
33

44

5+
0.10.1 (in development)
6+
~~~~~~~~~~~~~~~~~~~~~~~
7+
8+
Added
9+
^^^^^
10+
11+
- Support for contract errors with anonymous fields. (PR_90_)
12+
13+
14+
.. _PR_90: https://github.com/fjarri-eth/pons/pull/90
15+
16+
517
0.10.0 (2025-10-19)
618
~~~~~~~~~~~~~~~~~~~
719

pons/_client.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ class ContractError(Exception):
156156
error: Error
157157
"""The recognized ABI Error object."""
158158

159-
data: dict[str, Any]
159+
data: dict[str, Any] | tuple[Any, ...]
160160
"""The unpacked error data, corresponding to the ABI."""
161161

162-
def __init__(self, error: Error, decoded_data: dict[str, Any]):
162+
def __init__(self, error: Error, decoded_data: dict[str, Any] | tuple[Any, ...]):
163163
super().__init__(error, decoded_data)
164164
self.error = error
165165
self.data = decoded_data
@@ -189,10 +189,15 @@ def decode_contract_error(
189189
except UnknownError:
190190
return ProviderError(exc)
191191

192+
# These errors have named fields, so `decoded_data` is expected to be a dictionary.
193+
# The assertions are there for `mypy`'s sake.
192194
if error == PANIC_ERROR:
195+
assert isinstance(decoded_data, dict) # noqa: S101
193196
return ContractPanic.from_code(decoded_data["code"])
194197
if error == LEGACY_ERROR:
198+
assert isinstance(decoded_data, dict) # noqa: S101
195199
return ContractLegacyError(decoded_data["message"])
200+
196201
return ContractError(error, decoded_data)
197202
return ProviderError(exc)
198203

pons/_contract_abi.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -583,27 +583,28 @@ def from_json(cls, error_entry: ABI_JSON) -> "Error":
583583

584584
name = error_entry_typed["name"]
585585
fields = dispatch_types(error_entry_typed["inputs"])
586-
if isinstance(fields, list):
587-
raise TypeError("Error fields must be named")
588586

589587
return cls(name=name, fields=fields)
590588

591589
def __init__(
592590
self,
593591
name: str,
594-
fields: Mapping[str, Type],
592+
fields: Mapping[str, Type] | Sequence[Type],
595593
):
596594
self.name = name
595+
self._named_fields = isinstance(fields, Mapping)
597596
self.fields = Signature(fields)
598597

599598
@cached_property
600599
def selector(self) -> bytes:
601600
"""Error's selector."""
602601
return keccak(self.name.encode() + self.fields.canonical_form.encode())[:SELECTOR_LENGTH]
603602

604-
def decode_fields(self, data_bytes: bytes) -> dict[str, Any]:
603+
def decode_fields(self, data_bytes: bytes) -> dict[str, Any] | tuple[Any, ...]:
605604
"""Decodes the error fields from the given packed data."""
606-
return self.fields.decode_into_dict(data_bytes)
605+
if self._named_fields:
606+
return self.fields.decode_into_dict(data_bytes)
607+
return self.fields.decode_into_tuple(data_bytes)
607608

608609
def __str__(self) -> str:
609610
return f"error {self.name}{self.fields}"
@@ -843,7 +844,7 @@ def __init__(
843844
error.selector: error for error in chain([PANIC_ERROR, LEGACY_ERROR], self.error)
844845
}
845846

846-
def resolve_error(self, error_data: bytes) -> tuple[Error, dict[str, Any]]:
847+
def resolve_error(self, error_data: bytes) -> tuple[Error, dict[str, Any] | tuple[Any, ...]]:
847848
"""
848849
Given the packed error data, attempts to find the error in the ABI
849850
and decode the data into its fields.

tests/TestClient.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ contract PayableConstructor {
104104
contract TestErrors {
105105
error CustomError(uint256 x);
106106

107+
error AnonymousFieldError(uint256);
108+
107109
uint256 state;
108110

109111
constructor(uint256 x) {
@@ -152,4 +154,8 @@ contract TestErrors {
152154
function transactPanic(uint256 x) public {
153155
state = raisePanic(x);
154156
}
157+
158+
function raiseAnonymousFieldError(uint256 x) public view returns (uint256) {
159+
revert AnonymousFieldError(x);
160+
}
155161
}

tests/TestContractFunctionality.sol

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,4 @@ contract Test {
9898
ByteInner[2] memory inner_arr = [inner1, inner2];
9999
emit Complicated("aaaa", bytestring33len2, foo, inner_arr);
100100
}
101-
102-
error MyError(address sender);
103101
}

tests/test_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,20 @@ async def test_call_decoding_error(
208208
await session.call(wrong_contract.method.getState(456))
209209

210210

211+
async def test_error_with_anonymous_fields(
212+
session: ClientSession,
213+
compiled_contracts: dict[str, CompiledContract],
214+
root_signer: AccountSigner,
215+
) -> None:
216+
compiled_contract = compiled_contracts["TestErrors"]
217+
deployed_contract = await session.deploy(root_signer, compiled_contract.constructor(123))
218+
219+
with pytest.raises(ContractError) as exc:
220+
await session.call(deployed_contract.method.raiseAnonymousFieldError(4))
221+
assert exc.value.error == deployed_contract.error.AnonymousFieldError
222+
assert exc.value.data == (4,)
223+
224+
211225
async def test_estimate_deploy(
212226
session: ClientSession,
213227
compiled_contracts: dict[str, CompiledContract],

tests/test_contract_abi.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -762,18 +762,6 @@ def test_error_from_json() -> None:
762762
):
763763
Error.from_json(dict(type="constructor"))
764764

765-
with pytest.raises(TypeError, match="Error fields must be named"):
766-
Error.from_json(
767-
dict(
768-
inputs=[
769-
dict(internalType="address", name="", type="address"),
770-
dict(internalType="bytes", name="", type="bytes"),
771-
],
772-
name="Foo",
773-
type="error",
774-
)
775-
)
776-
777765

778766
def test_error_init() -> None:
779767
error = Error(
@@ -785,6 +773,8 @@ def test_error_init() -> None:
785773

786774

787775
def test_error_decode() -> None:
776+
# Named fields
777+
788778
error = Error(
789779
"Foo",
790780
dict(foo=abi.bytes(), bar=abi.uint(8)),
@@ -794,6 +784,17 @@ def test_error_decode() -> None:
794784
decoded = error.decode_fields(encoded_bytes)
795785
assert decoded == dict(foo=b"12345", bar=9)
796786

787+
# Anonymous fields
788+
789+
error = Error(
790+
"Foo",
791+
[abi.bytes(), abi.uint(8)],
792+
)
793+
794+
encoded_bytes = encode_args((abi.bytes(), b"12345"), (abi.uint(8), 9))
795+
decoded = error.decode_fields(encoded_bytes)
796+
assert decoded == (b"12345", 9)
797+
797798

798799
def test_resolve_error() -> None:
799800
error1 = Error("Error1", dict(foo=abi.bytes(), bar=abi.uint(8)))

0 commit comments

Comments
 (0)