From 2626f910485f95e01083220fb89183465921fe91 Mon Sep 17 00:00:00 2001 From: venugopalanvip <102638505+venugopalanvip@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:33:34 +0530 Subject: [PATCH] fix(calldata): accept boolean enum keys instead of failing the descriptor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An enum key is the field value it matches, and a bool holds 0 or 1 in calldata, so `true` and `false` are the keys a descriptor writes for a bool field. JSON has no boolean object key, so they arrive as text. `convert_enums` ran `int(ordinal)` on them. That raises, and the exception is caught only at the top of the conversion, so one bool enum discards every format and every deployment of the descriptor and returns an empty list. Two descriptors in the clear-signing registry produce nothing today: registry/flyingtulip/calldata-PftNft.json {"True": …, "False": …} 0 -> 6 registry/ekubo/calldata-MEVCaptureRouter.json {"false": …, "true": …} 0 -> 168 The casing differs because nothing constrains it: the v2 schema says "enumeration keys are the field values" and allows any string. Boolean literals now map to the value the bool holds, case-insensitively. A key that is neither decimal nor boolean still raises, because silently mapping it to a number would build an entry the device can never match. Co-Authored-By: Claude Opus 5 --- src/erc7730/convert/calldata/v1/enum.py | 22 +++++++++- .../convert/calldata/v1/test_enum_ordinal.py | 40 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tests/convert/calldata/v1/test_enum_ordinal.py diff --git a/src/erc7730/convert/calldata/v1/enum.py b/src/erc7730/convert/calldata/v1/enum.py index 3b1bfe84..3d78f6ab 100644 --- a/src/erc7730/convert/calldata/v1/enum.py +++ b/src/erc7730/convert/calldata/v1/enum.py @@ -9,6 +9,26 @@ from erc7730.model.resolved.context import ResolvedDeployment from erc7730.model.types import Id, Selector +# A bool holds 0 or 1 in calldata, and an enum key is the field value it matches, so these are +# the keys a descriptor writes for a bool field. JSON has no bool object key, so they arrive as +# text, and the casing follows whoever wrote the descriptor rather than any rule. +BOOLEAN_ORDINALS = {"true": 1, "false": 0} + + +def enum_ordinal(ordinal: str) -> int: + """Value the device compares an enum entry against. + + :param ordinal: enum key, as written in the descriptor + :return: the numeric field value it stands for + :raises ValueError: if the key is neither decimal nor a boolean literal + """ + try: + return int(ordinal) + except ValueError: + if (value := BOOLEAN_ORDINALS.get(ordinal.strip().lower())) is not None: + return value + raise + def convert_enums( deployment: ResolvedDeployment, selector: Selector, enums: dict[Id, EnumDefinition] | None @@ -29,7 +49,7 @@ def convert_enums( selector=selector, id=i, enum_id=enum_id, - value=int(ordinal), + value=enum_ordinal(ordinal), name=name, ) for i, (enum_id, enum) in enumerate(enums.items()) diff --git a/tests/convert/calldata/v1/test_enum_ordinal.py b/tests/convert/calldata/v1/test_enum_ordinal.py new file mode 100644 index 00000000..ef830a04 --- /dev/null +++ b/tests/convert/calldata/v1/test_enum_ordinal.py @@ -0,0 +1,40 @@ +import pytest + +from erc7730.convert.calldata.v1.enum import enum_ordinal + + +@pytest.mark.parametrize( + "ordinal,expected", + [ + pytest.param("0", 0, id="zero"), + pytest.param("1", 1, id="one"), + pytest.param("42", 42, id="larger"), + pytest.param("-1", -1, id="negative"), + ], +) +def test_a_decimal_key_is_its_own_value(ordinal: str, expected: int) -> None: + assert enum_ordinal(ordinal) == expected + + +@pytest.mark.parametrize( + "ordinal,expected", + [ + pytest.param("false", 0, id="false"), + pytest.param("true", 1, id="true"), + pytest.param("False", 0, id="False"), + pytest.param("True", 1, id="True"), + pytest.param("FALSE", 0, id="FALSE"), + pytest.param("TRUE", 1, id="TRUE"), + pytest.param(" true ", 1, id="padded"), + ], +) +def test_a_boolean_key_is_the_value_the_bool_holds_in_calldata(ordinal: str, expected: int) -> None: + """A bool holds 0 or 1, and the casing follows whoever wrote the descriptor.""" + assert enum_ordinal(ordinal) == expected + + +@pytest.mark.parametrize("ordinal", ["maybe", "", "0x1", "1.0", "yes"]) +def test_a_key_that_is_neither_still_raises(ordinal: str) -> None: + """Silently mapping an unrecognised key to a number would build an entry that never matches.""" + with pytest.raises(ValueError): + enum_ordinal(ordinal)