Skip to content

Commit

Permalink
feat: implement address normalization in resolved form (#124)
Browse files Browse the repository at this point in the history
* feat: implement address normalization in resolved form

* fix submodule
  • Loading branch information
jnicoulaud-ledger authored Oct 28, 2024
1 parent fa72361 commit 4a3b347
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 4 additions & 6 deletions src/erc7730/convert/ledger/eip712/convert_erc7730_to_eip712.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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],
Expand Down
69 changes: 61 additions & 8 deletions src/erc7730/convert/resolved/convert_erc7730_input_to_resolved.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
5 changes: 2 additions & 3 deletions src/erc7730/generate/generate.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(),
Expand Down
57 changes: 0 additions & 57 deletions src/erc7730/model/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
)
68 changes: 62 additions & 6 deletions src/erc7730/model/input/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -27,22 +83,22 @@ 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"
"factory.",
)


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.",
Expand Down
4 changes: 2 additions & 2 deletions src/erc7730/model/input/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 "
Expand Down
Loading

0 comments on commit 4a3b347

Please sign in to comment.