diff --git a/src/erc7730/convert/ledger/eip712/convert_eip712_to_erc7730.py b/src/erc7730/convert/ledger/eip712/convert_eip712_to_erc7730.py index d302f93..aadc301 100644 --- a/src/erc7730/convert/ledger/eip712/convert_eip712_to_erc7730.py +++ b/src/erc7730/convert/ledger/eip712/convert_eip712_to_erc7730.py @@ -9,12 +9,12 @@ from erc7730.common.output import OutputAdder from erc7730.convert import ERC7730Converter -from erc7730.model.context import Deployment, Domain, EIP712JsonSchema +from erc7730.model.context import EIP712JsonSchema from erc7730.model.display import ( DateEncoding, FieldFormat, ) -from erc7730.model.input.context import InputEIP712, InputEIP712Context +from erc7730.model.input.context import InputDeployment, InputDomain, InputEIP712, InputEIP712Context from erc7730.model.input.descriptor import InputERC7730Descriptor from erc7730.model.input.display import ( InputDateParameters, @@ -63,14 +63,14 @@ def convert( descriptors[contract.address] = InputERC7730Descriptor( context=InputEIP712Context( eip712=InputEIP712( - domain=Domain( + domain=InputDomain( name=descriptor.name, version=None, chainId=descriptor.chainId, verifyingContract=contract.address, ), schemas=schemas, - deployments=[Deployment(chainId=descriptor.chainId, address=contract.address)], + deployments=[InputDeployment(chainId=descriptor.chainId, address=contract.address)], ) ), metadata=InputMetadata( diff --git a/src/erc7730/convert/ledger/eip712/convert_erc7730_to_eip712.py b/src/erc7730/convert/ledger/eip712/convert_erc7730_to_eip712.py index e1fb931..1c3443f 100644 --- a/src/erc7730/convert/ledger/eip712/convert_erc7730_to_eip712.py +++ b/src/erc7730/convert/ledger/eip712/convert_erc7730_to_eip712.py @@ -9,13 +9,11 @@ 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, EIP712JsonSchema -from erc7730.model.display import ( - FieldFormat, -) +from erc7730.model.context import EIP712JsonSchema +from erc7730.model.display import FieldFormat from erc7730.model.paths import ContainerField, ContainerPath, DataPath from erc7730.model.paths.path_ops import data_path_concat, to_relative -from erc7730.model.resolved.context import ResolvedEIP712Context +from erc7730.model.resolved.context import ResolvedDeployment, ResolvedEIP712Context from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor from erc7730.model.resolved.display import ( ResolvedField, @@ -76,7 +74,7 @@ def convert( @classmethod def _build_network_descriptor( cls, - deployment: Deployment, + deployment: ResolvedDeployment, dapp_name: str, contract_name: str, messages: list[InputEIP712Message], diff --git a/src/erc7730/convert/resolved/convert_erc7730_input_to_resolved.py b/src/erc7730/convert/resolved/convert_erc7730_input_to_resolved.py index 95998ac..022e43d 100644 --- a/src/erc7730/convert/resolved/convert_erc7730_input_to_resolved.py +++ b/src/erc7730/convert/resolved/convert_erc7730_input_to_resolved.py @@ -15,7 +15,15 @@ from erc7730.model.display import ( FieldFormat, ) -from erc7730.model.input.context import InputContract, InputContractContext, InputEIP712, InputEIP712Context +from erc7730.model.input.context import ( + InputContract, + InputContractContext, + InputDeployment, + InputDomain, + InputEIP712, + InputEIP712Context, + InputFactory, +) from erc7730.model.input.descriptor import InputERC7730Descriptor from erc7730.model.input.display import ( InputDisplay, @@ -33,8 +41,11 @@ from erc7730.model.resolved.context import ( ResolvedContract, ResolvedContractContext, + ResolvedDeployment, + ResolvedDomain, ResolvedEIP712, ResolvedEIP712Context, + ResolvedFactory, ) from erc7730.model.resolved.descriptor import ResolvedERC7730Descriptor from erc7730.model.resolved.display import ( @@ -45,7 +56,7 @@ ResolvedNestedFields, ) from erc7730.model.resolved.metadata import ResolvedMetadata -from erc7730.model.types import Id, Selector +from erc7730.model.types import Address, Id, Selector @final @@ -55,7 +66,7 @@ class ERC7730InputToResolved(ERC7730Converter[InputERC7730Descriptor, ResolvedER After conversion, the descriptor is in resolved form: - URLs have been fetched - - Contract addresses have been normalized to lowercase (TODO not implemented) + - Contract addresses have been normalized to lowercase - References have been inlined - Constants have been inlined - Field definitions have been inlined @@ -133,11 +144,39 @@ def _resolve_context_contract( def _resolve_contract(cls, contract: InputContract, out: OutputAdder) -> ResolvedContract | None: if (abi := cls._resolve_abis(contract.abi, out)) is None: return None + if (deployments := cls._resolve_deployments(contract.deployments, out)) is None: + return None + + if contract.factory is None: + factory = None + elif (factory := cls._resolve_factory(contract.factory, out)) is None: + return None return ResolvedContract( - abi=abi, deployments=contract.deployments, addressMatcher=contract.addressMatcher, factory=contract.factory + abi=abi, deployments=deployments, addressMatcher=contract.addressMatcher, factory=factory ) + @classmethod + def _resolve_deployments( + cls, deployments: list[InputDeployment], out: OutputAdder + ) -> list[ResolvedDeployment] | None: + resolved_deployments = [] + for deployment in deployments: + if (resolved_deployment := cls._resolve_deployment(deployment, out)) is not None: + resolved_deployments.append(resolved_deployment) + return resolved_deployments + + @classmethod + def _resolve_deployment(cls, deployment: InputDeployment, out: OutputAdder) -> ResolvedDeployment | None: + return ResolvedDeployment(chainId=deployment.chainId, address=Address(deployment.address)) + + @classmethod + def _resolve_factory(cls, factory: InputFactory, out: OutputAdder) -> ResolvedFactory | None: + if (deployments := cls._resolve_deployments(factory.deployments, out)) is None: + return None + + return ResolvedFactory(deployments=deployments, deployEvent=factory.deployEvent) + @classmethod def _resolve_abis(cls, abis: list[ABI] | HttpUrl, out: OutputAdder) -> list[ABI] | None: match abis: @@ -163,16 +202,30 @@ def _resolve_context_eip712(cls, context: InputEIP712Context, out: OutputAdder) @classmethod def _resolve_eip712(cls, eip712: InputEIP712, out: OutputAdder) -> ResolvedEIP712 | None: - schemas = cls._resolve_schemas(eip712.schemas, out) + if eip712.domain is None: + domain = None + elif (domain := cls._resolve_domain(eip712.domain, out)) is None: + return None - if schemas is None: + if (schemas := cls._resolve_schemas(eip712.schemas, out)) is None: + return None + if (deployments := cls._resolve_deployments(eip712.deployments, out)) is None: return None return ResolvedEIP712( - domain=eip712.domain, + domain=domain, schemas=schemas, domainSeparator=eip712.domainSeparator, - deployments=eip712.deployments, + deployments=deployments, + ) + + @classmethod + def _resolve_domain(cls, domain: InputDomain, out: OutputAdder) -> ResolvedDomain | None: + return ResolvedDomain( + name=domain.name, + version=domain.version, + chainId=domain.chainId, + verifyingContract=None if domain.verifyingContract is None else Address(domain.verifyingContract), ) @classmethod diff --git a/src/erc7730/generate/generate.py b/src/erc7730/generate/generate.py index 72d13a0..7f0c829 100644 --- a/src/erc7730/generate/generate.py +++ b/src/erc7730/generate/generate.py @@ -1,9 +1,8 @@ from erc7730.common.abi import compute_signature from erc7730.common.client import get_contract_abis from erc7730.model.abi import Function, InputOutput -from erc7730.model.context import Deployment from erc7730.model.display import FieldFormat -from erc7730.model.input.context import InputContract, InputContractContext +from erc7730.model.input.context import InputContract, InputContractContext, InputDeployment from erc7730.model.input.descriptor import InputERC7730Descriptor from erc7730.model.input.display import InputDisplay, InputField, InputFieldDescription, InputFormat from erc7730.model.input.metadata import InputMetadata @@ -26,7 +25,7 @@ def generate_contract(chain_id: int, contract_address: Address) -> InputERC7730D context=InputContractContext( contract=InputContract( abi=abis, - deployments=[Deployment(chainId=chain_id, address=contract_address)], + deployments=[InputDeployment(chainId=chain_id, address=contract_address)], ) ), metadata=InputMetadata(), diff --git a/src/erc7730/model/context.py b/src/erc7730/model/context.py index c41a178..aa6c131 100644 --- a/src/erc7730/model/context.py +++ b/src/erc7730/model/context.py @@ -3,7 +3,6 @@ from pydantic_string_url import HttpUrl from erc7730.model.base import Model -from erc7730.model.types import Address # ruff: noqa: N815 - camel case field names are tolerated to match schema @@ -28,59 +27,3 @@ class EIP712Schema(Model): eip712Schema: HttpUrl | EIP712JsonSchema = Field( title="EIP-712 message schema", description="The EIP-712 message schema." ) - - -class Domain(Model): - """ - EIP 712 Domain Binding constraint. - - Each value of the domain constraint MUST match the corresponding eip 712 message domain value. - """ - - name: str | None = Field(default=None, title="Name", description="The EIP-712 domain name.") - - version: str | None = Field(default=None, title="Version", description="The EIP-712 version.") - - chainId: int | None = Field(default=None, title="Chain ID", description="The EIP-155 chain id.") - - verifyingContract: Address | None = Field( - default=None, title="Verifying Contract", description="The EIP-712 verifying contract address." - ) - - -class Deployment(Model): - """ - A deployment describing where the contract is deployed. - - The target contract (Tx to or factory) MUST match one of those deployments. - """ - - chainId: int = Field(title="Chain ID", description="The deployment EIP-155 chain id.") - - address: Address = Field(title="Contract Address", description="The deployment contract address.") - - -class Factory(Model): - """ - A factory constraint is used to check whether the target contract is deployed by a specified factory. - """ - - deployments: list[Deployment] = Field( - title="Deployments", - description="An array of deployments describing where the contract is deployed. The target contract (Tx to or" - "factory) MUST match one of those deployments.", - ) - - deployEvent: str = Field( - title="Deploy Event signature", - description="The event signature that the factory emits when deploying a new contract.", - ) - - -class BindingContext(Model): - deployments: list[Deployment] = Field( - title="Deployments", - description="An array of deployments describing where the contract is deployed. The target contract (Tx to or" - "factory) MUST match one of those deployments.", - min_length=1, - ) diff --git a/src/erc7730/model/input/context.py b/src/erc7730/model/input/context.py index bade181..910d892 100644 --- a/src/erc7730/model/input/context.py +++ b/src/erc7730/model/input/context.py @@ -3,13 +3,69 @@ from erc7730.model.abi import ABI from erc7730.model.base import Model -from erc7730.model.context import BindingContext, Domain, EIP712JsonSchema, Factory -from erc7730.model.types import Id +from erc7730.model.context import EIP712JsonSchema +from erc7730.model.types import Id, MixedCaseAddress # ruff: noqa: N815 - camel case field names are tolerated to match schema -class InputContract(BindingContext): +class InputDomain(Model): + """ + EIP 712 Domain Binding constraint. + + Each value of the domain constraint MUST match the corresponding eip 712 message domain value. + """ + + name: str | None = Field(default=None, title="Name", description="The EIP-712 domain name.") + + version: str | None = Field(default=None, title="Version", description="The EIP-712 version.") + + chainId: int | None = Field(default=None, title="Chain ID", description="The EIP-155 chain id.") + + verifyingContract: MixedCaseAddress | None = Field( + default=None, title="Verifying Contract", description="The EIP-712 verifying contract address." + ) + + +class InputDeployment(Model): + """ + A deployment describing where the contract is deployed. + + The target contract (Tx to or factory) MUST match one of those deployments. + """ + + chainId: int = Field(title="Chain ID", description="The deployment EIP-155 chain id.") + + address: MixedCaseAddress = Field(title="Contract Address", description="The deployment contract address.") + + +class InputFactory(Model): + """ + A factory constraint is used to check whether the target contract is deployed by a specified factory. + """ + + deployments: list[InputDeployment] = Field( + title="Deployments", + description="An array of deployments describing where the contract is deployed. The target contract (Tx to or" + "factory) MUST match one of those deployments.", + ) + + deployEvent: str = Field( + title="Deploy Event signature", + description="The event signature that the factory emits when deploying a new contract.", + ) + + +class InputBindingContext(Model): + deployments: list[InputDeployment] = Field( + title="Deployments", + description="An array of deployments describing where the contract is deployed. The target contract (Tx to or" + "factory) MUST match one of those deployments.", + min_length=1, + ) + + +class InputContract(InputBindingContext): """ The contract binding context is a set constraints that are used to bind the ERC7730 file to a specific smart contract. @@ -27,7 +83,7 @@ class InputContract(BindingContext): description="An URL of a contract address matcher that should be used to match the contract address.", ) - factory: Factory | None = Field( + factory: InputFactory | None = Field( None, title="Factory Constraint", description="A factory constraint is used to check whether the target contract is deployed by a specified" @@ -35,14 +91,14 @@ class InputContract(BindingContext): ) -class InputEIP712(BindingContext): +class InputEIP712(InputBindingContext): """ EIP 712 Binding. The EIP-712 binding context is a set of constraints that must be verified by the message being signed. """ - domain: Domain | None = Field( + domain: InputDomain | None = Field( default=None, title="EIP 712 Domain Binding constraint", description="Each value of the domain constraint MUST match the corresponding eip 712 message domain value.", diff --git a/src/erc7730/model/input/display.py b/src/erc7730/model/input/display.py index 77a1b7a..858a775 100644 --- a/src/erc7730/model/input/display.py +++ b/src/erc7730/model/input/display.py @@ -11,7 +11,7 @@ FormatBase, ) from erc7730.model.input.path import ContainerPathStr, DataPathStr, DescriptorPathStr -from erc7730.model.types import Address, HexStr, Id +from erc7730.model.types import HexStr, Id, MixedCaseAddress from erc7730.model.unions import field_discriminator, field_parameters_discriminator # ruff: noqa: N815 - camel case field names are tolerated to match schema @@ -71,7 +71,7 @@ class InputTokenAmountParameters(Model): '"Unknown token" warning.', ) - nativeCurrencyAddress: DescriptorPathStr | Address | list[Address] | None = Field( + nativeCurrencyAddress: DescriptorPathStr | list[MixedCaseAddress] | MixedCaseAddress | None = Field( default=None, title="Native Currency Address", description="An address or array of addresses, any of which are interpreted as an amount in native currency " diff --git a/src/erc7730/model/input/lenses.py b/src/erc7730/model/input/lenses.py index 4f5727a..056dd6a 100644 --- a/src/erc7730/model/input/lenses.py +++ b/src/erc7730/model/input/lenses.py @@ -2,8 +2,7 @@ Utilities for accessing ERC-7730 input descriptors nested fields. """ -from erc7730.model.context import BindingContext, Deployment -from erc7730.model.input.context import InputContractContext, InputEIP712Context +from erc7730.model.input.context import InputBindingContext, InputContractContext, InputDeployment, InputEIP712Context from erc7730.model.input.descriptor import InputERC7730Descriptor @@ -12,12 +11,12 @@ def get_chain_ids(descriptor: InputERC7730Descriptor) -> set[int]: return {d.chainId for d in get_deployments(descriptor)} -def get_deployments(descriptor: InputERC7730Descriptor) -> list[Deployment]: +def get_deployments(descriptor: InputERC7730Descriptor) -> list[InputDeployment]: """Get deployments section for a descriptor.""" return get_binding_context(descriptor).deployments -def get_binding_context(descriptor: InputERC7730Descriptor) -> BindingContext: +def get_binding_context(descriptor: InputERC7730Descriptor) -> InputBindingContext: """Get binding context for a descriptor.""" if isinstance(context := descriptor.context, InputEIP712Context): return context.eip712 diff --git a/src/erc7730/model/resolved/context.py b/src/erc7730/model/resolved/context.py index 9ba986b..cf60e82 100644 --- a/src/erc7730/model/resolved/context.py +++ b/src/erc7730/model/resolved/context.py @@ -3,13 +3,69 @@ from erc7730.model.abi import ABI from erc7730.model.base import Model -from erc7730.model.context import BindingContext, Domain, EIP712JsonSchema, Factory -from erc7730.model.types import Id +from erc7730.model.context import EIP712JsonSchema +from erc7730.model.types import Address, Id # ruff: noqa: N815 - camel case field names are tolerated to match schema -class ResolvedContract(BindingContext): +class ResolvedDomain(Model): + """ + EIP 712 Domain Binding constraint. + + Each value of the domain constraint MUST match the corresponding eip 712 message domain value. + """ + + name: str | None = Field(default=None, title="Name", description="The EIP-712 domain name.") + + version: str | None = Field(default=None, title="Version", description="The EIP-712 version.") + + chainId: int | None = Field(default=None, title="Chain ID", description="The EIP-155 chain id.") + + verifyingContract: Address | None = Field( + default=None, title="Verifying Contract", description="The EIP-712 verifying contract address." + ) + + +class ResolvedDeployment(Model): + """ + A deployment describing where the contract is deployed. + + The target contract (Tx to or factory) MUST match one of those deployments. + """ + + chainId: int = Field(title="Chain ID", description="The deployment EIP-155 chain id.") + + address: Address = Field(title="Contract Address", description="The deployment contract address.") + + +class ResolvedFactory(Model): + """ + A factory constraint is used to check whether the target contract is deployed by a specified factory. + """ + + deployments: list[ResolvedDeployment] = Field( + title="Deployments", + description="An array of deployments describing where the contract is deployed. The target contract (Tx to or" + "factory) MUST match one of those deployments.", + ) + + deployEvent: str = Field( + title="Deploy Event signature", + description="The event signature that the factory emits when deploying a new contract.", + ) + + +class ResolvedBindingContext(Model): + deployments: list[ResolvedDeployment] = Field( + title="Deployments", + description="An array of deployments describing where the contract is deployed. The target contract (Tx to or" + "factory) MUST match one of those deployments.", + min_length=1, + ) + + +class ResolvedContract(ResolvedBindingContext): """ Contract Binding Context. @@ -28,7 +84,7 @@ class ResolvedContract(BindingContext): description="An URL of a contract address matcher that should be used to match the contract address.", ) - factory: Factory | None = Field( + factory: ResolvedFactory | None = Field( default=None, title="Factory Constraint", description="A factory constraint is used to check whether the target contract is deployed by a specified" @@ -36,14 +92,14 @@ class ResolvedContract(BindingContext): ) -class ResolvedEIP712(BindingContext): +class ResolvedEIP712(ResolvedBindingContext): """ EIP 712 Binding. The EIP-712 binding context is a set of constraints that must be verified by the message being signed. """ - domain: Domain | None = Field( + domain: ResolvedDomain | None = Field( default=None, title="EIP 712 Domain Binding constraint", description="Each value of the domain constraint MUST match the corresponding eip 712 message domain value.", diff --git a/src/erc7730/model/types.py b/src/erc7730/model/types.py index bc746cf..853978b 100644 --- a/src/erc7730/model/types.py +++ b/src/erc7730/model/types.py @@ -19,15 +19,27 @@ ), ] +MixedCaseAddress = Annotated[ + str, + Field( + title="Contract Address", + description="An Ethereum contract address, can be lowercase or EIP-55.", + min_length=42, + max_length=42, + pattern=r"^0x[a-fA-F0-9]+$", + ), +] + Address = Annotated[ str, Field( title="Contract Address", - description="An Ethereum contract address.", + description="An Ethereum contract address (normalized to lowercase).", min_length=42, max_length=42, - pattern=r"^0x[a-zA-Z0-9_\-]+$", + pattern=r"^0x[a-f0-9]+$", ), + BeforeValidator(lambda v: v.lower()), ] Selector = Annotated[ diff --git a/tests/convert/resolved/data/minimal_contract_input.json b/tests/convert/resolved/data/minimal_contract_input.json index d3734ea..a927cb8 100644 --- a/tests/convert/resolved/data/minimal_contract_input.json +++ b/tests/convert/resolved/data/minimal_contract_input.json @@ -5,7 +5,7 @@ "deployments": [ { "chainId": 1, - "address": "0x0000000000000000000000000000000000000000" + "address": "0x0000000000000000000000000000000000000aAa" } ], "abi": [ diff --git a/tests/convert/resolved/data/minimal_contract_resolved.json b/tests/convert/resolved/data/minimal_contract_resolved.json index c64faed..3d0c19d 100644 --- a/tests/convert/resolved/data/minimal_contract_resolved.json +++ b/tests/convert/resolved/data/minimal_contract_resolved.json @@ -1,7 +1,7 @@ { "context": { "contract": { - "deployments": [{ "chainId": 1, "address": "0x0000000000000000000000000000000000000000" }], + "deployments": [{ "chainId": 1, "address": "0x0000000000000000000000000000000000000aaa" }], "abi": [ { "type": "function", diff --git a/tests/convert/resolved/test_constants.py b/tests/convert/resolved/test_constants.py index 35c855d..69822a8 100644 --- a/tests/convert/resolved/test_constants.py +++ b/tests/convert/resolved/test_constants.py @@ -4,8 +4,7 @@ from erc7730.common.output import RaisingOutputAdder from erc7730.convert.resolved.constants import DefaultConstantProvider -from erc7730.model.context import Deployment -from erc7730.model.input.context import InputContract, InputContractContext +from erc7730.model.input.context import InputContract, InputContractContext, InputDeployment from erc7730.model.input.descriptor import InputERC7730Descriptor from erc7730.model.input.display import InputDisplay, InputFieldDescription, InputFormat from erc7730.model.input.metadata import InputMetadata @@ -21,8 +20,8 @@ def _provider(**constants: str | int | bool | float | None) -> DefaultConstantPr contract=InputContract( abi=HttpUrl("https://example.net/abi.json"), deployments=[ - Deployment(chainId=1, address="0x1111111111111111111111111111111111111111"), - Deployment(chainId=42, address="0x4242424242424242424242424242424242424242"), + InputDeployment(chainId=1, address="0x1111111111111111111111111111111111111111"), + InputDeployment(chainId=42, address="0x4242424242424242424242424242424242424242"), ], ) ),