Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/erc7730/common/abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*

Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down
63 changes: 47 additions & 16 deletions src/erc7730/model/paths/path_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
25 changes: 25 additions & 0 deletions tests/common/test_abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
45 changes: 45 additions & 0 deletions tests/model/paths/test_path_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading