Skip to content

Commit

Permalink
feat: implement resolution of selectors (#122)
Browse files Browse the repository at this point in the history
* feat: implement resolution of selectors

* UPDATE_REFERENCES = False

* update linter
  • Loading branch information
jnicoulaud-ledger authored Oct 28, 2024
1 parent 51af91e commit fa72361
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 33 deletions.
55 changes: 48 additions & 7 deletions src/erc7730/convert/resolved/convert_erc7730_input_to_resolved.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import assert_never, final, override

from eip712.model.schema import EIP712Type
from pydantic_string_url import HttpUrl

from erc7730.common import client
from erc7730.common.abi import reduce_signature, signature_to_selector
from erc7730.common.output import OutputAdder
from erc7730.convert import ERC7730Converter
from erc7730.convert.resolved.constants import ConstantProvider, DefaultConstantProvider
Expand Down Expand Up @@ -43,7 +45,7 @@
ResolvedNestedFields,
)
from erc7730.model.resolved.metadata import ResolvedMetadata
from erc7730.model.types import Id
from erc7730.model.types import Id, Selector


@final
Expand All @@ -57,7 +59,7 @@ class ERC7730InputToResolved(ERC7730Converter[InputERC7730Descriptor, ResolvedER
- References have been inlined
- Constants have been inlined
- Field definitions have been inlined
- Selectors have been converted to 4 bytes form (TODO not implemented)
- Selectors have been converted to 4 bytes form
"""

@override
Expand All @@ -68,7 +70,9 @@ def convert(self, descriptor: InputERC7730Descriptor, out: OutputAdder) -> Resol
return None
if (metadata := self._resolve_metadata(descriptor.metadata, out)) is None:
return None
if (display := self._resolve_display(descriptor.display, metadata.enums or {}, constants, out)) is None:
if (
display := self._resolve_display(descriptor.display, context, metadata.enums or {}, constants, out)
) is None:
return None

return ResolvedERC7730Descriptor(context=context, metadata=metadata, display=display)
Expand Down Expand Up @@ -199,14 +203,27 @@ def _resolve_schema(cls, schema: EIP712JsonSchema | HttpUrl, out: OutputAdder) -

@classmethod
def _resolve_display(
cls, display: InputDisplay, enums: dict[Id, EnumDefinition], constants: ConstantProvider, out: OutputAdder
cls,
display: InputDisplay,
context: ResolvedContractContext | ResolvedEIP712Context,
enums: dict[Id, EnumDefinition],
constants: ConstantProvider,
out: OutputAdder,
) -> ResolvedDisplay | None:
formats = {}
for format_key, format in display.formats.items():
for format_id, format in display.formats.items():
if (resolved_format_id := cls._resolve_format_id(format_id, context, out)) is None:
return None
if (
resolved_format := cls._resolve_format(format, display.definitions or {}, enums, constants, out)
) is not None:
formats[format_key] = resolved_format
) is None:
return None
if resolved_format_id in formats:
return out.error(
title="Duplicate format",
message=f"Descriptor contains 2 formats sections for {resolved_format_id}",
)
formats[resolved_format_id] = resolved_format

return ResolvedDisplay(formats=formats)

Expand Down Expand Up @@ -254,6 +271,30 @@ def _resolve_field_description(
}
)

@classmethod
def _resolve_format_id(
cls,
format_id: str,
context: ResolvedContractContext | ResolvedEIP712Context,
out: OutputAdder,
) -> EIP712Type | Selector | None:
match context:
case ResolvedContractContext():
if format_id.startswith("0x"):
return Selector(format_id)

if (reduced_signature := reduce_signature(format_id)) is not None:
return Selector(signature_to_selector(reduced_signature))

return out.error(
title="Invalid selector",
message=f""""{format_id}" is not a valid function signature or selector.""",
)
case ResolvedEIP712Context():
return format_id
case _:
assert_never(context)

@classmethod
def _resolve_format(
cls,
Expand Down
30 changes: 7 additions & 23 deletions src/erc7730/lint/lint_validate_display_fields.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import final, override

from erc7730.common.abi import function_to_selector, reduce_signature, signature_to_selector
from erc7730.common.abi import function_to_selector
from erc7730.common.output import OutputAdder
from erc7730.lint import ERC7730Linter
from erc7730.model.paths import DataPath, Field
Expand Down Expand Up @@ -99,10 +99,6 @@ def _validate_eip712_paths(cls, descriptor: ResolvedERC7730Descriptor, out: Outp
f"valid according to the EIP-712 schema.",
)

@classmethod
def _display(cls, selector: str, keccak: str) -> str:
return selector if selector == keccak else f"`{keccak}/{selector}`"

@classmethod
def _validate_abi_paths(cls, descriptor: ResolvedERC7730Descriptor, out: OutputAdder) -> None:
if isinstance(descriptor.context, ResolvedContractContext):
Expand All @@ -112,50 +108,38 @@ def _validate_abi_paths(cls, descriptor: ResolvedERC7730Descriptor, out: OutputA
abi_paths_by_selector[function_to_selector(abi)] = compute_abi_schema_paths(abi)

for selector, fmt in descriptor.display.formats.items():
keccak = selector
if not selector.startswith("0x"):
if (reduced_signature := reduce_signature(selector)) is not None:
keccak = signature_to_selector(reduced_signature)
else:
out.error(
title="Invalid selector",
message=f"Selector {cls._display(selector, keccak)} is not a valid function signature.",
)
continue
if keccak not in abi_paths_by_selector:
if selector not in abi_paths_by_selector:
out.error(
title="Invalid selector",
message=f"Selector {cls._display(selector, keccak)} not found in ABI.",
message=f"Selector {selector} not found in ABI.",
)
continue
format_paths = compute_format_schema_paths(fmt).data_paths
abi_paths = abi_paths_by_selector[keccak]
abi_paths = abi_paths_by_selector[selector]

if (excluded := fmt.excluded) is not None:
excluded_paths = [to_absolute(path) for path in excluded]
else:
excluded_paths = []

function = cls._display(selector, keccak)

for path in abi_paths - format_paths:
if any(path_starts_with(path, excluded_path) for excluded_path in excluded_paths):
continue

if any(data_path_ends_with(path, allowed) for allowed in AUTHORIZED_MISSING_DISPLAY_FIELDS):
out.debug(
title="Optional Display field missing",
message=f"Display field for path `{path}` is missing for selector {function}. If "
message=f"Display field for path `{path}` is missing for selector {selector}. If "
f"intentionally excluded, please add it to `excluded` list to avoid this warning.",
)
else:
out.warning(
title="Missing Display field",
message=f"Display field for path `{path}` is missing for selector {function}. If "
message=f"Display field for path `{path}` is missing for selector {selector}. If "
f"intentionally excluded, please add it to `excluded` list to avoid this warning.",
)
for path in format_paths - abi_paths:
out.error(
title="Invalid Display field",
message=f"Display field for path `{path}` is not in selector {function}.",
message=f"Display field for path `{path}` is not in selector {selector}.",
)
5 changes: 3 additions & 2 deletions src/erc7730/model/resolved/display.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Annotated, ForwardRef

from eip712.model.schema import EIP712Type
from pydantic import Discriminator, Field, Tag

from erc7730.model.base import Model
Expand All @@ -12,7 +13,7 @@
)
from erc7730.model.paths import ContainerPath, DataPath
from erc7730.model.resolved.path import ResolvedPath
from erc7730.model.types import Address, HexStr, Id
from erc7730.model.types import Address, HexStr, Id, Selector
from erc7730.model.unions import field_discriminator, field_parameters_discriminator

# ruff: noqa: N815 - camel case field names are tolerated to match schema
Expand Down Expand Up @@ -252,7 +253,7 @@ class ResolvedDisplay(Model):
Display Formatting Info Section.
"""

formats: dict[str, ResolvedFormat] = Field(
formats: dict[EIP712Type | Selector, ResolvedFormat] = Field(
title="List of field formats",
description="The list includes formatting info for each field of a structure. This list is indexed by a key"
"identifying uniquely the message's type in the abi. For smartcontracts, it is the selector of the"
Expand Down
11 changes: 11 additions & 0 deletions src/erc7730/model/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@
),
]

Selector = Annotated[
str,
Field(
title="Selector",
description="An Ethereum contract function identifier, in 4 bytes, hex encoded form.",
min_length=10,
max_length=10,
pattern=r"^0x[a-z0-9]+$",
),
]

HexStr = Annotated[
str,
Field(
Expand Down
2 changes: 1 addition & 1 deletion tests/convert/resolved/data/minimal_contract_resolved.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"metadata": { "enums": {} },
"display": {
"formats": {
"function1(bytes4)": {
"0x5ca8f297": {
"fields": [
{
"path": { "type": "data", "absolute": true, "elements": [{ "type": "field", "identifier": "param1" }] },
Expand Down

0 comments on commit fa72361

Please sign in to comment.