Source code for erc7730.convert.convert_erc7730_to_eip712

from typing import assert_never, final, override

from eip712 import (
    EIP712ContractDescriptor as LegacyEIP712ContractDescriptor,
)
from eip712 import (
    EIP712DAppDescriptor as LegacyEIP712DAppDescriptor,
)
from eip712 import (
    EIP712Field as LegacyEIP712Field,
)
from eip712 import (
    EIP712Format as LegacyEIP712Format,
)
from eip712 import (
    EIP712Mapper as LegacyEIP712Mapper,
)
from eip712 import (
    EIP712MessageDescriptor as LegacyEIP712MessageDescriptor,
)

from erc7730.common.ledger import ledger_network_id
from erc7730.common.output import OutputAdder
from erc7730.convert import ERC7730Converter
from erc7730.model.context import Deployment, EIP712Field, EIP712JsonSchema
from erc7730.model.display import (
    FieldFormat,
    TokenAmountParameters,
)
from erc7730.model.resolved.context import ResolvedEIP712Context
from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor
from erc7730.model.resolved.display import (
    ResolvedField,
    ResolvedFieldDescription,
    ResolvedNestedFields,
)


[docs] @final class ERC7730toEIP712Converter(ERC7730Converter[ResolvedERC7730Descriptor, LegacyEIP712DAppDescriptor]): """ Converts ERC-7730 descriptor to Ledger legacy EIP-712 descriptor. Generates 1 output EIP712DAppDescriptor per chain id, as EIP-712 descriptors are chain-specific. """
[docs] @override def convert( self, descriptor: ResolvedERC7730Descriptor, out: OutputAdder ) -> dict[str, LegacyEIP712DAppDescriptor] | None: # FIXME model_construct() needs to be used here due to bad conception of EIP-712 library, # which adds computed fields on validation context = descriptor.context if not isinstance(context, ResolvedEIP712Context): return out.error("context is not EIP712") if (domain := context.eip712.domain) is None or (dapp_name := domain.name) is None: return out.error("EIP712 domain is not defined") if (contract_name := descriptor.metadata.owner) is None: return out.error("metadata.owner is not defined") messages: list[LegacyEIP712MessageDescriptor] = [] for primary_type, format in descriptor.display.formats.items(): schema = self._get_schema(primary_type, context.eip712.schemas, out) if schema is None: return out.error(f"EIP-712 schema for {primary_type} is missing") label = format.intent if isinstance(format.intent, str) else primary_type messages.append( LegacyEIP712MessageDescriptor.model_construct( schema=schema, mapper=LegacyEIP712Mapper.model_construct( label=label, fields=[ out_field for in_field in format.fields for out_field in self.convert_field(in_field, prefix=None) ], ), ) ) descriptors: dict[str, LegacyEIP712DAppDescriptor] = {} for deployment in context.eip712.deployments: output_descriptor = self._build_network_descriptor(deployment, dapp_name, contract_name, messages, out) if output_descriptor is not None: descriptors[str(deployment.chainId)] = output_descriptor return descriptors
@classmethod def _build_network_descriptor( cls, deployment: Deployment, dapp_name: str, contract_name: str, messages: list[LegacyEIP712MessageDescriptor], out: OutputAdder, ) -> LegacyEIP712DAppDescriptor | None: if (network := ledger_network_id(deployment.chainId)) is None: return out.error(f"network id {deployment.chainId} not supported") return LegacyEIP712DAppDescriptor.model_construct( blockchainName=network, chainId=deployment.chainId, name=dapp_name, contracts=[ LegacyEIP712ContractDescriptor.model_construct( address=deployment.address.lower(), contractName=contract_name, messages=messages ) ], ) @classmethod def _get_schema( cls, primary_type: str, schemas: list[EIP712JsonSchema], out: OutputAdder ) -> dict[str, list[EIP712Field]] | None: for schema in schemas: if schema.primaryType == primary_type: return schema.types return out.error(f"schema for type {primary_type} not found")
[docs] @classmethod def convert_field(cls, field: ResolvedField, prefix: str | None) -> list[LegacyEIP712Field]: if isinstance(field, ResolvedNestedFields): field_prefix = field.path if prefix is None else f"{prefix}.{field.path}" return [out_field for in_field in field.fields for out_field in cls.convert_field(in_field, field_prefix)] return [cls.convert_field_description(field, prefix)]
[docs] @classmethod def convert_field_description(cls, field: ResolvedFieldDescription, prefix: str | None) -> LegacyEIP712Field: asset_path: str | None = None field_format: LegacyEIP712Format | None = None match field.format: case FieldFormat.TOKEN_AMOUNT: if field.params is not None and isinstance(field.params, TokenAmountParameters): asset_path = field.params.tokenPath if prefix is None else f"{prefix}.{field.params.tokenPath}" # FIXME edge case for referencing verifyingContract, this will be handled cleanly in #65 if asset_path == "@.to": asset_path = None field_format = LegacyEIP712Format.AMOUNT case FieldFormat.AMOUNT: field_format = LegacyEIP712Format.AMOUNT case FieldFormat.DATE: field_format = LegacyEIP712Format.DATETIME case FieldFormat.ADDRESS_NAME: field_format = LegacyEIP712Format.RAW case FieldFormat.ENUM: field_format = LegacyEIP712Format.RAW case FieldFormat.UNIT: field_format = LegacyEIP712Format.RAW case FieldFormat.DURATION: field_format = LegacyEIP712Format.RAW case FieldFormat.NFT_NAME: field_format = LegacyEIP712Format.RAW case FieldFormat.CALL_DATA: field_format = LegacyEIP712Format.RAW case FieldFormat.RAW: field_format = LegacyEIP712Format.RAW case None: field_format = None case _: assert_never(field.format) return LegacyEIP712Field( path=field.path if prefix is None else f"{prefix}.{field.path}", label=field.label, assetPath=asset_path, format=field_format, )