From 214078942587887a5e2282a623a2078412d977cd Mon Sep 17 00:00:00 2001 From: Pierre Aoun Date: Fri, 9 Oct 2026 14:27:09 +0200 Subject: [PATCH] fix(calldata): always emit DECIMALS in PARAM_UNIT app-ethereum >= 1.22.4 rejects PARAM_UNIT structs without DECIMALS (6a80), although its spec documents the tag as optional with a default of 0. Unit fields without decimals (e.g. opencover "Cover duration") therefore fall back to blind signing on released devices. Always serialize DECIMALS, defaulting to 0, which is byte-for-byte the value the app is specified to assume. Co-Authored-By: Claude Opus 5.5 --- src/erc7730/convert/calldata/v1/tlv.py | 4 ++-- .../convert/calldata/test_convert_fields.py | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/erc7730/convert/calldata/v1/tlv.py b/src/erc7730/convert/calldata/v1/tlv.py index ab63bf19..672220c6 100644 --- a/src/erc7730/convert/calldata/v1/tlv.py +++ b/src/erc7730/convert/calldata/v1/tlv.py @@ -473,8 +473,8 @@ def tlv_param_unit(obj: CalldataDescriptorParamUnitV1) -> bytes: out += tlv(CalldataDescriptorParamUnitTag.VALUE, tlv_value(obj.value)) out += tlv(CalldataDescriptorParamUnitTag.BASE, obj.base) - if (decimals := obj.decimals) is not None: - out += tlv(CalldataDescriptorParamUnitTag.DECIMALS, decimals.to_bytes(1)) + # always emit DECIMALS (defaulting to 0): app-ethereum >= 1.22.4 rejects PARAM_UNIT structs without it + out += tlv(CalldataDescriptorParamUnitTag.DECIMALS, (obj.decimals or 0).to_bytes(1)) if (prefix := obj.prefix) is not None: out += tlv(CalldataDescriptorParamUnitTag.PREFIX, prefix.to_bytes(1)) diff --git a/tests/v2/convert/calldata/test_convert_fields.py b/tests/v2/convert/calldata/test_convert_fields.py index 694c86e6..7e83bc98 100644 --- a/tests/v2/convert/calldata/test_convert_fields.py +++ b/tests/v2/convert/calldata/test_convert_fields.py @@ -8,7 +8,12 @@ from erc7730.convert.calldata.convert_erc7730_v2_input_to_calldata import ( erc7730_v2_descriptor_to_calldata_descriptors, ) -from erc7730.convert.calldata.v1.tlv import CalldataDescriptorFieldTag, tlv_field +from erc7730.convert.calldata.v1.tlv import ( + CalldataDescriptorFieldTag, + CalldataDescriptorParamUnitTag, + tlv_field, + tlv_param_unit, +) from erc7730.model.calldata.v1.instruction import ( CalldataDescriptorFieldVisibilityV1, CalldataDescriptorInstructionFieldV1, @@ -204,6 +209,18 @@ def test_convert_unit_params() -> None: assert field.param.prefix is False +@pytest.mark.parametrize(("decimals", "expected"), [pytest.param(None, 0, id="unset"), pytest.param(4, 4, id="set")]) +def test_convert_unit_always_serializes_decimals(decimals: int | None, expected: int) -> None: + params: dict[str, Any] = {"base": "s"} if decimals is None else {"base": "s", "decimals": decimals} + field = convert_field( + "setDuration(uint256 duration)", {"path": "duration", "label": "Duration", "format": "unit", "params": params} + ) + + assert isinstance(field.param, CalldataDescriptorParamUnitV1) + values = dict(parse_tlv(tlv_param_unit(field.param))) + assert values[CalldataDescriptorParamUnitTag.DECIMALS] == bytes([expected]) + + def test_convert_token_ticker_has_no_native_currencies() -> None: field = convert_field( "getTokenTicker(address token)",