diff --git a/src/erc7730/common/abi.py b/src/erc7730/common/abi.py index 919b2568..fe5fdc46 100644 --- a/src/erc7730/common/abi.py +++ b/src/erc7730/common/abi.py @@ -22,7 +22,7 @@ named_param: type identifier? named_tuple: tuple array* identifier? - array: "[]" + array: "[]" | "[" /[0-9]+/ "]" identifier: /[a-zA-Z$_][a-zA-Z0-9$_]*/ type: identifier array* @@ -61,8 +61,8 @@ def named_tuple(self, ast: Any) -> Component: # Separate arrays from name # Arrays are "[]", name is anything else - arrays = [elem for elem in ast[1:] if elem == "[]"] - names = [elem for elem in ast[1:] if elem != "[]"] + arrays = [elem for elem in ast[1:] if isinstance(elem, str) and elem.startswith("[")] + names = [elem for elem in ast[1:] if not (isinstance(elem, str) and elem.startswith("["))] # Build type with array suffixes type_str = "tuple" + "".join(arrays) @@ -73,6 +73,8 @@ def named_tuple(self, ast: Any) -> Component: return Component(name=name, type=type_str, components=components) def array(self, ast: Any) -> str: + if ast: + return f"[{ast[0]}]" return "[]" def identifier(self, ast: Any) -> str: diff --git a/src/erc7730/model/paths/path_schemas.py b/src/erc7730/model/paths/path_schemas.py index 862704df..78958a16 100644 --- a/src/erc7730/model/paths/path_schemas.py +++ b/src/erc7730/model/paths/path_schemas.py @@ -89,6 +89,34 @@ def compute_abi_schema_paths(abi: Function) -> set[DataPath]: """ paths: set[DataPath] = set() + def split_array_dimensions(type_name: str) -> tuple[str, int, int]: + """ + Return ``(base_type, total_dims, trailing_dynamic_dims)`` for an ABI type. + + ``total_dims`` counts all array suffixes, including fixed-size ones like ``[11]``. + ``trailing_dynamic_dims`` preserves the historical behavior that only considered + trailing ``[]`` dimensions when building ABI schema paths. + """ + + trailing_dynamic_dims = 0 + base_type = type_name + while base_type.endswith("[]"): + trailing_dynamic_dims += 1 + base_type = base_type[:-2] + + total_dims = trailing_dynamic_dims + while base_type.endswith("]"): + left_bracket = base_type.rfind("[") + if left_bracket < 0: + break + dimension = base_type[left_bracket + 1 : -1] + if not dimension.isdigit(): + break + total_dims += 1 + base_type = base_type[:left_bracket] + + return base_type, total_dims, trailing_dynamic_dims + def append_paths(path: DataPath, params: list[InputOutput] | list[Component] | None) -> None: if not params: return None @@ -98,31 +126,34 @@ def append_paths(path: DataPath, params: list[InputOutput] | list[Component] | N sub_path = data_path_append(path, Field(identifier=param.name)) - # Determine base type and array dimensions - full_type = param.type - dims = 0 - while full_type.endswith("[]"): - dims += 1 - full_type = full_type[:-2] - - param_base_type = full_type + param_base_type, total_dims, trailing_dynamic_dims = split_array_dimensions(param.type) # If the (non-array) base type is bytes, allow indexing into the byte sequence if param_base_type == "bytes": paths.add(data_path_append(sub_path, Array())) - # TODO: For now there is no use case that requires paths for intermediate array levels - # So we only add the final array level if any - if dims > 0: - for _ in range(dims): - sub_path = data_path_append(sub_path, Array()) - paths.add(sub_path) + legacy_path = sub_path + if trailing_dynamic_dims > 0: + for _ in range(trailing_dynamic_dims): + legacy_path = data_path_append(legacy_path, Array()) + paths.add(legacy_path) + + full_array_path = sub_path + if total_dims > 0: + for _ in range(total_dims): + full_array_path = data_path_append(full_array_path, Array()) + if total_dims > trailing_dynamic_dims: + paths.add(full_array_path) # Recurse into tuple/components if present, otherwise add the final path if param.components: - append_paths(sub_path, param.components) # type: ignore + if total_dims > 0 and trailing_dynamic_dims == 0: + paths.add(sub_path) + append_paths(full_array_path if total_dims > 0 else sub_path, param.components) # type: ignore[arg-type] else: - paths.add(sub_path) + paths.add(legacy_path if trailing_dynamic_dims > 0 else sub_path) + if total_dims > trailing_dynamic_dims: + paths.add(full_array_path) append_paths(ROOT_DATA_PATH, abi.inputs) diff --git a/tests/common/test_abi.py b/tests/common/test_abi.py index f74ff065..26dde7b1 100644 --- a/tests/common/test_abi.py +++ b/tests/common/test_abi.py @@ -67,6 +67,31 @@ "mixed(bytes32[][] data, (address a,uint256 b)[][] _tuples, string name)", "mixed(bytes32[][],(address,uint256)[][],string)", ), + # fixed-size arrays + ( + "exchange(address[11] _route, uint256[5][5] _swap_params, uint256 _amount, uint256 _min_dy)", + "exchange(address[11],uint256[5][5],uint256,uint256)", + ), + # fixed-size array single dimension + ( + "foo(uint256[3] values)", + "foo(uint256[3])", + ), + # mixed fixed and dynamic arrays + ( + "bar(address[5] addrs, uint256[] amounts, bytes32[2][] pairs)", + "bar(address[5],uint256[],bytes32[2][])", + ), + # fixed-size array of tuples + ( + "baz((uint256,address)[3] items)", + "baz((uint256,address)[3])", + ), + # higher-dimensional fixed-size arrays + ( + "multi(uint256[2][3][4] cube)", + "multi(uint256[2][3][4])", + ), ], ) def test_reduce_signature(signature: str, expected: str) -> None: diff --git a/tests/model/paths/test_path_schemas.py b/tests/model/paths/test_path_schemas.py index 7ab39225..5027c335 100644 --- a/tests/model/paths/test_path_schemas.py +++ b/tests/model/paths/test_path_schemas.py @@ -92,6 +92,51 @@ def test_compute_abi_paths_multidimensional_tuple() -> None: assert compute_abi_schema_paths(abi) == expected +def test_compute_abi_paths_with_fixed_size_arrays() -> None: + abi = Function( + name="exchange", + inputs=[ + InputOutput(name="_route", type="address[11]"), + InputOutput(name="_swap_params", type="uint256[5][5]"), + InputOutput(name="_amount", type="uint256"), + InputOutput(name="_pools", type="address[5]"), + ], + ) + expected = { + to_path("#._route"), + to_path("#._route.[]"), + to_path("#._swap_params"), + to_path("#._swap_params.[].[]"), + to_path("#._amount"), + to_path("#._pools"), + to_path("#._pools.[]"), + } + assert compute_abi_schema_paths(abi) == expected + + +def test_compute_abi_paths_with_fixed_size_array_of_tuples() -> None: + abi = Function( + name="batch", + inputs=[ + InputOutput( + name="items", + type="tuple[3]", + components=[ + Component(name="to", type="address"), + Component(name="amount", type="uint256"), + ], + ) + ], + ) + expected = { + to_path("#.items"), + to_path("#.items.[]"), + to_path("#.items.[].to"), + to_path("#.items.[].amount"), + } + assert compute_abi_schema_paths(abi) == expected + + def test_compute_eip712_paths_with_slicable_params() -> None: schema = EIP712Schema( primaryType="Foo",