Skip to content

Commit

Permalink
feat(BACK-8213): EIP-712 trusted name support
Browse files Browse the repository at this point in the history
  • Loading branch information
fsamier committed Jan 30, 2025
1 parent 669a6e2 commit 90a1569
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 27 deletions.
62 changes: 59 additions & 3 deletions src/erc7730/convert/ledger/eip712/convert_eip712_to_erc7730.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@
from eip712.model.resolved.descriptor import ResolvedEIP712DAppDescriptor
from eip712.model.resolved.message import ResolvedEIP712MapperField
from eip712.model.schema import EIP712SchemaField, EIP712Type
from eip712.model.types import EIP712Format
from eip712.model.types import EIP712Format, EIP712NameSource, EIP712NameType
from eip712.utils import MissingRootTypeError, MultipleRootTypesError, get_primary_type
from pydantic_string_url import HttpUrl

from erc7730.common.output import ExceptionsToOutput, OutputAdder
from erc7730.convert import ERC7730Converter
from erc7730.model.context import EIP712Schema
from erc7730.model.display import (
AddressNameType,
DateEncoding,
FieldFormat,
)
from erc7730.model.input.context import InputDeployment, InputDomain, InputEIP712, InputEIP712Context
from erc7730.model.input.descriptor import InputERC7730Descriptor
from erc7730.model.input.display import (
InputAddressNameParameters,
InputDateParameters,
InputDisplay,
InputFieldDescription,
Expand Down Expand Up @@ -56,7 +58,7 @@ def convert(

formats[primary_type] = InputFormat(
intent=message.mapper.label,
fields=[self._convert_field(field) for field in message.mapper.fields],
fields=[self._convert_field(field, out) for field in message.mapper.fields],
required=None,
screens=None,
)
Expand Down Expand Up @@ -91,7 +93,7 @@ def convert(

@classmethod
def _convert_field(
cls, field: ResolvedEIP712MapperField
cls, field: ResolvedEIP712MapperField, out: OutputAdder
) -> InputFieldDescription | InputReference | InputNestedFields:
# FIXME must generate nested fields for arrays
match field.format:
Expand All @@ -118,6 +120,19 @@ def _convert_field(
format=FieldFormat.DATE,
params=InputDateParameters(encoding=DateEncoding.TIMESTAMP),
)
case EIP712Format.TRUSTED_NAME:
field_format = (
FieldFormat.NFT_NAME if EIP712NameType.COLLECTION in field.nameTypes else FieldFormat.ADDRESS_NAME
)
return InputFieldDescription(
path=field.path,
label=field.label,
format=field_format,
params=InputAddressNameParameters(
types=cls.convert_trusted_names_types(field.nameTypes, out),
sources=cls.convert_trusted_names_sources(field.nameSources),
),
)
case _:
assert_never(field.format)

Expand All @@ -139,3 +154,44 @@ def _get_primary_type(
message="Primary type could not be determined on EIP-712 schema, as several types are not"
"referenced by any other type. Please make sure your schema has a single root type.",
)

@classmethod
def convert_trusted_names_types(
cls, types: list[EIP712NameType] | None, out: OutputAdder
) -> list[AddressNameType] | None:
if types is None:
return None

name_types: list[AddressNameType] = []
for name_type in types:
match name_type:
case EIP712NameType.WALLET:
name_types.append(AddressNameType.WALLET)
case EIP712NameType.EOA:
name_types.append(AddressNameType.EOA)
case EIP712NameType.SMART_CONTRACT:
name_types.append(AddressNameType.CONTRACT)
case EIP712NameType.TOKEN:
name_types.append(AddressNameType.TOKEN)
case EIP712NameType.COLLECTION:
name_types.append(AddressNameType.COLLECTION)
case EIP712NameType.CONTEXT_ADDRESS:
return out.error("EIP712 context_address trusted name type is not supported in ERC-7730")
case _:
assert_never(name_type)

return name_types

@classmethod
def convert_trusted_names_sources(cls, sources: list[EIP712NameSource] | None) -> list[str] | None:
if sources is None:
return None
name_sources: list[str] = []

for name_source in sources:
if name_source == EIP712NameSource.LOCAL_ADDRESS_BOOK:
# ERC-7730 specs defines "local" as an example
name_sources.append("local")
else:
name_sources.append(name_source.value)
return name_sources
61 changes: 57 additions & 4 deletions src/erc7730/convert/ledger/eip712/convert_erc7730_to_eip712.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@
from eip712.model.input.descriptor import InputEIP712DAppDescriptor
from eip712.model.input.message import InputEIP712Mapper, InputEIP712MapperField, InputEIP712Message
from eip712.model.schema import EIP712SchemaField
from eip712.model.types import EIP712Format
from eip712.model.types import EIP712Format, EIP712NameSource, EIP712NameType

from erc7730.common.ledger import ledger_network_id
from erc7730.common.output import ExceptionsToOutput, OutputAdder
from erc7730.convert import ERC7730Converter
from erc7730.model.context import EIP712Schema
from erc7730.model.display import FieldFormat
from erc7730.model.display import AddressNameType, FieldFormat
from erc7730.model.paths import Array, ContainerField, ContainerPath, DataPath
from erc7730.model.paths.path_ops import data_path_concat, to_relative
from erc7730.model.resolved.context import ResolvedDeployment, ResolvedEIP712Context
from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor
from erc7730.model.resolved.display import (
ResolvedAddressNameParameters,
ResolvedField,
ResolvedFieldDescription,
ResolvedNestedFields,
Expand Down Expand Up @@ -164,7 +165,7 @@ def convert_field_description(
case None:
field_format = None
case FieldFormat.ADDRESS_NAME:
field_format = EIP712Format.RAW
field_format = EIP712Format.TRUSTED_NAME
case FieldFormat.RAW:
field_format = EIP712Format.RAW
case FieldFormat.ENUM:
Expand All @@ -174,7 +175,7 @@ def convert_field_description(
case FieldFormat.DURATION:
field_format = EIP712Format.RAW
case FieldFormat.NFT_NAME:
field_format = EIP712Format.RAW
field_format = EIP712Format.TRUSTED_NAME
case FieldFormat.CALL_DATA:
field_format = EIP712Format.RAW
case FieldFormat.DATE:
Expand Down Expand Up @@ -214,9 +215,61 @@ def convert_field_description(
case _:
assert_never(field.format)

name_types: list[EIP712NameType] | None = None
name_sources: list[EIP712NameSource] | None = None

if (
field_format == EIP712Format.TRUSTED_NAME
and field.params is not None
and isinstance(field.params, ResolvedAddressNameParameters)
):
name_types = cls.convert_trusted_names_types(field.params.types)
name_sources = cls.convert_trusted_names_sources(field.params.sources)

return InputEIP712MapperField(
path=str(to_relative(field_path)),
label=field.label,
assetPath=None if asset_path is None else str(to_relative(asset_path)),
format=field_format,
nameTypes=name_types,
nameSources=name_sources,
)

@classmethod
def convert_trusted_names_types(cls, types: list[AddressNameType] | None) -> list[EIP712NameType] | None:
if types is None:
return None

name_types: list[EIP712NameType] = []
for name_type in types:
match name_type:
case AddressNameType.WALLET:
name_types.append(EIP712NameType.WALLET)
case AddressNameType.EOA:
name_types.append(EIP712NameType.EOA)
case AddressNameType.CONTRACT:
name_types.append(EIP712NameType.SMART_CONTRACT)
case AddressNameType.TOKEN:
name_types.append(EIP712NameType.TOKEN)
case AddressNameType.COLLECTION:
name_types.append(EIP712NameType.COLLECTION)
case _:
assert_never(name_type)

return name_types

_VALID_EIP712_SOURCES = tuple(s.value for s in EIP712NameSource)

@classmethod
def convert_trusted_names_sources(cls, sources: list[str] | None) -> list[EIP712NameSource] | None:
if sources is None:
return None
name_sources: list[EIP712NameSource] = []

for name_source in sources:
if name_source == "local": # ERC-7730 specs defines "local" as an example
name_sources.append(EIP712NameSource.LOCAL_ADDRESS_BOOK)
elif name_source in cls._VALID_EIP712_SOURCES:
name_sources.append(EIP712NameSource(name_source))
# else ignore
return name_sources
4 changes: 2 additions & 2 deletions src/erc7730/model/paths/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class DataPath(Model):

@override
def __str__(self) -> str:
return f'{"#." if self.absolute else ""}{".".join(str(e) for e in self.elements)}'
return f"{'#.' if self.absolute else ''}{'.'.join(str(e) for e in self.elements)}"

@override
def __hash__(self) -> int:
Expand Down Expand Up @@ -226,7 +226,7 @@ class DescriptorPath(Model):

@override
def __str__(self) -> str:
return f'$.{".".join(str(e) for e in self.elements)}'
return f"$.{'.'.join(str(e) for e in self.elements)}"

@override
def __hash__(self) -> int:
Expand Down
18 changes: 16 additions & 2 deletions tests/convert/ledger/eip712/data/eip712-UniswapX-DutchOrder.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,14 @@
{
"path": "spender",
"label": "Approve to spender",
"format": "raw"
"format": "trusted-name",
"nameTypes": [
"smart_contract"
],
"nameSources": [
"local_address_book",
"ens"
]
},
{
"path": "permitted.amount",
Expand All @@ -158,7 +165,14 @@
{
"path": "witness.outputs.[].recipient",
"label": "On Addresses",
"format": "raw"
"format": "trusted-name",
"nameTypes": [
"smart_contract"
],
"nameSources": [
"local_address_book",
"ens"
]
},
{
"path": "deadline",
Expand Down
23 changes: 9 additions & 14 deletions tests/convert/ledger/eip712/test_convert_eip712_round_trip.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,15 @@ def _cleanup_formats(formats: dict[str, Any]) -> Any:
del_by_path(message, "screens")
if "fields" in message:
for field in message["fields"]:
if "format" in field:
format = field["format"]

# Address name format parameters cannot be preserved
if format == "addressName":
del_by_path(field, "params")

# Other formats are always converted to RAW
if format not in (
FieldFormat.AMOUNT,
FieldFormat.TOKEN_AMOUNT,
FieldFormat.DATE,
):
field["format"] = "raw"
# Other formats are always converted to RAW
if "format" in field and field["format"] not in (
FieldFormat.AMOUNT,
FieldFormat.TOKEN_AMOUNT,
FieldFormat.DATE,
FieldFormat.ADDRESS_NAME,
FieldFormat.NFT_NAME,
):
field["format"] = "raw"

return formats

Expand Down
4 changes: 2 additions & 2 deletions tests/convert/ledger/eip712/test_convert_erc7730_to_eip712.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from tests.assertions import assert_dict_equals
from tests.cases import path_id
from tests.files import ERC7730_EIP712_DESCRIPTORS
from tests.schemas import assert_valid_legacy_eip_712
from tests.skip import single_or_first, single_or_skip

DATA = Path(__file__).resolve().parent / "data"
Expand All @@ -32,7 +31,8 @@ def test_erc7730_registry_files(input_file: Path) -> None:
resolved_erc7730_descriptor = single_or_skip(resolved_erc7730_descriptor)
output_descriptor = convert_and_print_errors(resolved_erc7730_descriptor, ERC7730toEIP712Converter())
output_descriptor = single_or_skip(output_descriptor)
assert_valid_legacy_eip_712(output_descriptor)
# schema validation skipped as schemas have not been updated with trusted names support
# assert_valid_legacy_eip_712(output_descriptor)


@pytest.mark.parametrize("input_file", ERC7730_EIP712_DESCRIPTORS, ids=path_id)
Expand Down

0 comments on commit 90a1569

Please sign in to comment.