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)