diff --git a/.github/workflows/jobs.yml b/.github/workflows/jobs.yml new file mode 100644 index 0000000..36c42c0 --- /dev/null +++ b/.github/workflows/jobs.yml @@ -0,0 +1,71 @@ +name: lint + +on: + push: + branches: + - main + - update + pull_request: + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + with: + token: ${{github.token}} + + - name: set up python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + token: ${{github.token}} + + - name: install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: install project + run: | + uv venv .venv -p 3.12 + source .venv/bin/activate + uv pip install -e .[all] + + - name: lint with ruff + run: | + source .venv/bin/activate + python -m ruff check . + test: + name: test + runs-on: ubuntu-latest + needs: lint + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + steps: + - name: checkout + uses: actions/checkout@v4 + with: + token: ${{github.token}} + + - name: set up python + uses: actions/setup-python@v5 + with: + python-version: ${{matrix.python-version}} + token: ${{github.token}} + + - name: install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: install project + run: | + uv venv .venv -p ${{matrix.python-version}} + source .venv/bin/activate + uv pip install -e .[all] + + - name: test + run: | + source .venv/bin/activate + docker-compose up -d + python -m pytest . diff --git a/.gitignore b/.gitignore index 6a472f4..a710368 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ build/ .coverage tests/contracts/build +__pycache__ +.vscode diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9e8d1d1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -dist: xenial -language: python -python: - - "3.6" - - "3.7" -script: - - python setup.py test -a --cov=eip712_structs -services: - - docker -before_install: - - docker-compose up -d -after_success: - - python setup.py coveralls -deploy: - provider: pypi - user: ajrgrubbs - password: - secure: USn2HOarmeVduJM/8SauQf3L/n+cj+QWhUD7uYVj69SvZsm3/NQkhGCdKX4qNhy/QOhGuIJ1qo2RCDLijyPgOCmee3Cz1SRxP4jkN3BdCCqlb5iVRZCSmu4/gp3d7qZyLfidyKMx63Ra7+DiWij6xKTSdPsRZ/3DZMApIrvUkQBqnsOsA+1ycIs8ASkTRymq7kVbTOn17uzPf3jBWHZFBynCY+qe5lC0Aa1N3y4l4jrzH6zGTIFnXjirpvvBRDoKBEIj5S/X25xwcKkJtFwAzigtM0EgVcU2FqVBplniNuLh8qZrmcqqIVNC+KbWCVLfD+dYT6yhFobOjjjalhZ13LITBLVO9YPQWHFHiEcDe1jz3+aPxuDOUmWqD/1e55QfypNIwJNGdcQ8WaqxxwzT1qClYCae5FFAS03Zct8AfxlMkPpfMudaD5642gzWcGVTBZg32GlW3Q6TEbTIlr+/CIyXjxjCndlr2A463kxl06FY4XsNcMC0A40p0Ygwq3Q09FQhk+ObIY2V8Zej8Mbat4b3EEO2bK4jmTK1V3r3EqUCPI+/JbsoCxAZNdTnoQfwlwgHCzclM02OKTFSTQuXs+b8zCn/fXCJivqnfWPzqQgoD1zAMwEN4DAY53dukPzcKdgTJ1TCcwKaIcxFu4UAjzYiCFx6nLetvLkpndu2cyE= - on: - tags: true - skip_existing: true diff --git a/API.md b/API.md deleted file mode 100644 index 299c4e5..0000000 --- a/API.md +++ /dev/null @@ -1,17 +0,0 @@ -# API - -### `class EIP712Struct` -#### Important methods -- `.to_message(domain: EIP712Struct)` - Convert the struct (and given domain struct) into the standard EIP-712 message structure. -- `.signable_bytes(domain: EIP712Struct)` - Get the standard EIP-712 bytes hash, suitable for signing. -- `.from_message(message_dict: dict)` **(Class method)** - Given a standard EIP-712 message dictionary (such as produced from `.to_message`), returns a NamedTuple containing the `message` and `domain` EIP712Structs. - -#### Other stuff -- `.encode_value()` - Returns a `bytes` object containing the ordered concatenation of each members bytes32 representation. -- `.encode_type()` **(Class method)** - Gets the "signature" of the struct class. Includes nested structs too! -- `.type_hash()` **(Class method)** - The keccak256 hash of the result of `.encode_type()`. -- `.hash_struct()` - Gets the keccak256 hash of the concatenation of `.type_hash()` and `.encode_value()` -- `.get_data_value(member_name: str)` - Get the value of the given struct member -- `.set_data_value(member_name: str, value: Any)` - Set the value of the given struct member -- `.data_dict()` - Returns a dictionary with all data in this struct. Includes nested struct data, if exists. -- `.get_members()` **(Class method)** - Returns a dictionary mapping each data member's name to it's type. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 1e2ef23..9c33ee4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2019 ConsenSys +Copyright (c) 2024 Mihai Cosma Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7565c81..cf68db4 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,25 @@ Read the proposal:
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md #### Supported Python Versions -- `3.6` -- `3.7` - -## Install -```bash -pip install eip712-structs -``` +- 3.10 and up, tested up to 3.12 ## Usage -See [API.md](API.md) for a succinct summary of available methods. + +### `class EIP712Struct` +#### Important methods +- `.to_message(domain: EIP712Struct)` - Convert the struct (and given domain struct) into the standard EIP-712 message structure. +- `.signable_bytes(domain: EIP712Struct)` - Get the standard EIP-712 bytes hash, suitable for signing. +- `.from_message(message_dict: dict)` **(Class method)** - Given a standard EIP-712 message dictionary (such as produced from `.to_message`), returns a NamedTuple containing the `message` and `domain` EIP712Structs. + +#### Other stuff +- `.encode_value()` - Returns a `bytes` object containing the ordered concatenation of each members bytes32 representation. +- `.encode_type()` **(Class method)** - Gets the "signature" of the struct class. Includes nested structs too! +- `.type_hash()` **(Class method)** - The keccak256 hash of the result of `.encode_type()`. +- `.hash_struct()` - Gets the keccak256 hash of the concatenation of `.type_hash()` and `.encode_value()` +- `.get_data_value(member_name: str)` - Get the value of the given struct member +- `.set_data_value(member_name: str, value: Any)` - Set the value of the given struct member +- `.data_dict()` - Returns a dictionary with all data in this struct. Includes nested struct data, if exists. +- `.get_members()` **(Class method)** - Returns a dictionary mapping each data member's name to it's type. Examples/Details below. @@ -193,12 +202,12 @@ struct_array = Array(MyStruct, 10) # MyStruct[10] - again, don't instantiate s ## Development Contributions always welcome. -Install dependencies: -- `pip install -r requirements.txt` +Install test dependencies: +- `uv pip install -e ".[test]"` Run tests: -- `python setup.py test` -- Some tests expect an active local ganache chain on http://localhost:8545. Docker will compile the contracts and start the chain for you. +- `pytest` +- Some tests expect an active local anvil chain on http://localhost:11111. Docker will compile the contracts and start the chain for you. - Docker is optional, but useful to test the whole suite. If no chain is detected, chain tests are skipped. - Usage: - `docker-compose up -d` (Starts containers in the background) @@ -206,9 +215,19 @@ Run tests: - Cleanup containers when you're done: `docker-compose down` Deploying a new version: -- Bump the version number in `setup.py`, commit it into master. +- Bump the version number in `pyproject.toml`, commit it into master. - Make a release tag on the master branch in Github. Travis should handle the rest. +### Changes in 1.2 +- Switch from ganache to anvil +- Remove pysha3 dependency +- Remove python2 style super() call +- Remove OrderedAttributesMeta. From version 3.7 onward, dictionaries maintain the insertion order of their items. +- Require python >= 3.10 as the lowest version to install with [uv](https://github.com/astral-sh/uv) +- Switch from Sphinx to Google docstring format for readability +- Lint with [ruff](https://github.com/astral-sh/ruff) +- Add Github workflows (lint and test) +- Add pyproject.toml ## Shameless Plug Written by [ConsenSys](https://consensys.net) for the world! :heart: diff --git a/docker-compose.yml b/docker-compose.yml index 9bcb3de..5f52b95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,14 @@ ---- -version: "3" services: - ganache: - image: "trufflesuite/ganache-cli:latest" - command: "--account=\"0x3660582119566511de16f4dcf397fa324b27bd6247f653cf0298a0993f3432ed,100000000000000000000\"" + anvil: + image: "ghcr.io/foundry-rs/foundry:latest" + command: "'anvil --host 0.0.0.0 --port=11111'" ports: - - "8545:8545" + - "11111:11111" depends_on: - compiler labels: net.consensys.description: > - Starts an instance of ganache-cli for chain parity tests. - Unlocks address 0xD68D840e1e971750E6d45049ff579925456d5893" + Starts an instance of anvil on port 11111 for chain parity tests. compiler: image: "ethereum/solc:stable" diff --git a/eip712_structs/__init__.py b/eip712_structs/__init__.py index 4b25f03..95cfad1 100644 --- a/eip712_structs/__init__.py +++ b/eip712_structs/__init__.py @@ -1,5 +1,10 @@ +"""EIP712 data structure management for python.""" + +# required before imports to avoid circular dependency +# pylint: disable=wrong-import-position +# pylint: disable=invalid-name +default_domain = None + from eip712_structs.domain_separator import make_domain from eip712_structs.struct import EIP712Struct from eip712_structs.types import Address, Array, Boolean, Bytes, Int, String, Uint - -default_domain = None diff --git a/eip712_structs/domain_separator.py b/eip712_structs/domain_separator.py index 3111132..62e73ca 100644 --- a/eip712_structs/domain_separator.py +++ b/eip712_structs/domain_separator.py @@ -1,33 +1,39 @@ +"""EIP-712 Domain Separator.""" + import eip712_structs +# allow camelCase +# ruff: noqa: N803 +# pylint: disable=invalid-name +# allow missing class docstring +# pylint: disable=missing-class-docstring def make_domain(name=None, version=None, chainId=None, verifyingContract=None, salt=None): - """Helper method to create the standard EIP712Domain struct for you. + """Create the standard EIP712Domain struct. Per the standard, if a value is not used then the parameter is omitted from the struct entirely. """ - if all(i is None for i in [name, version, chainId, verifyingContract, salt]): - raise ValueError('At least one argument must be given.') + raise ValueError("At least one argument must be given.") class EIP712Domain(eip712_structs.EIP712Struct): pass - kwargs = dict() + kwargs = {} if name is not None: - EIP712Domain.name = eip712_structs.String() - kwargs['name'] = str(name) + EIP712Domain.name = eip712_structs.String() # type: ignore + kwargs["name"] = str(name) if version is not None: - EIP712Domain.version = eip712_structs.String() - kwargs['version'] = str(version) + EIP712Domain.version = eip712_structs.String() # type: ignore + kwargs["version"] = str(version) if chainId is not None: - EIP712Domain.chainId = eip712_structs.Uint(256) - kwargs['chainId'] = int(chainId) + EIP712Domain.chainId = eip712_structs.Uint(256) # type: ignore + kwargs["chainId"] = int(chainId) if verifyingContract is not None: - EIP712Domain.verifyingContract = eip712_structs.Address() - kwargs['verifyingContract'] = verifyingContract + EIP712Domain.verifyingContract = eip712_structs.Address() # type: ignore + kwargs["verifyingContract"] = verifyingContract if salt is not None: - EIP712Domain.salt = eip712_structs.Bytes(32) - kwargs['salt'] = salt + EIP712Domain.salt = eip712_structs.Bytes(32) # type: ignore + kwargs["salt"] = salt return EIP712Domain(**kwargs) diff --git a/eip712_structs/struct.py b/eip712_structs/struct.py index b754987..b5b952f 100644 --- a/eip712_structs/struct.py +++ b/eip712_structs/struct.py @@ -1,28 +1,22 @@ +"""EIP-712 Structs.""" + import functools import json import operator import re -from collections import OrderedDict, defaultdict -from typing import List, Tuple, NamedTuple +from collections import defaultdict +from typing import List, NamedTuple, Tuple from eth_utils.crypto import keccak import eip712_structs -from eip712_structs.types import Array, EIP712Type, from_solidity_type, BytesJSONEncoder - +from eip712_structs.types import Array, BytesJSONEncoder, EIP712Type, from_solidity_type -class OrderedAttributesMeta(type): - """Metaclass to ensure struct attribute order is preserved. - """ - @classmethod - def __prepare__(mcs, name, bases): - return OrderedDict() +class EIP712Struct(EIP712Type): + """Represent an EIP712 struct. Subclass it to use it. -class EIP712Struct(EIP712Type, metaclass=OrderedAttributesMeta): - """A representation of an EIP712 struct. Subclass it to use it. - - Example: + Examples: from eip712_structs import EIP712Struct, String class MyStruct(EIP712Struct): @@ -30,48 +24,53 @@ class MyStruct(EIP712Struct): struct_instance = MyStruct(some_param='some_value') """ + def __init__(self, **kwargs): - super(EIP712Struct, self).__init__(self.type_name, None) - members = self.get_members() - self.values = dict() - for name, typ in members: + """Initialize the struct.""" + super().__init__(type_name=self.type_name, none_val=None) + self.values = {} + for name, typ in self.get_members(): value = kwargs.get(name) if isinstance(value, dict): - value = typ(**value) + # check if it's callable + if callable(typ): + value = typ(**value) + else: + raise TypeError(f"Cannot create {typ.__class__.__name__} from {value}") self.values[name] = value @classmethod def __init_subclass__(cls, **kwargs): + """Initialize the subclass.""" super().__init_subclass__(**kwargs) cls.type_name = cls.__name__ - def encode_value(self, value=None): - """Returns the struct's encoded value. + def _encode_value(self, value=None): + """Return the struct's encoded value. A struct's encoded value is a concatenation of the bytes32 representation of each member of the struct. - Order is preserved. - :param value: This parameter is not used for structs. + Args: + value (Any): This parameter is not used for structs. """ - encoded_values = list() + encoded_values = [] for name, typ in self.get_members(): if isinstance(typ, type) and issubclass(typ, EIP712Struct): # Nested structs are recursively hashed, with the resulting 32-byte hash appended to the list of values sub_struct = self.get_data_value(name) + assert sub_struct is not None, f"Value for {name} not set" encoded_values.append(sub_struct.hash_struct()) else: # Regular types are encoded as normal encoded_values.append(typ.encode_value(self.values[name])) - return b''.join(encoded_values) + return b"".join(encoded_values) def get_data_value(self, name): - """Get the value of the given struct parameter. - """ + """Get the value of the given struct parameter.""" return self.values.get(name) def set_data_value(self, name, value): - """Set the value of the given struct parameter. - """ + """Set the value of the given struct parameter.""" if name in self.values: self.values[name] = value @@ -80,36 +79,32 @@ def data_dict(self): Nested structs instances are also converted to dict form. """ - result = dict() - for k, v in self.values.items(): - if isinstance(v, EIP712Struct): - result[k] = v.data_dict() - else: - result[k] = v - return result + return {k: v.data_dict() if isinstance(v, EIP712Struct) else v for k, v in self.values.items()} @classmethod def _encode_type(cls, resolve_references: bool) -> str: - member_sigs = [f'{typ.type_name} {name}' for name, typ in cls.get_members()] + member_sigs = [f"{typ.type_name} {name}" for name, typ in cls.get_members()] struct_sig = f'{cls.type_name}({",".join(member_sigs)})' if resolve_references: reference_structs = set() cls._gather_reference_structs(reference_structs) - sorted_structs = sorted(list(s for s in reference_structs if s != cls), key=lambda s: s.type_name) + sorted_structs = sorted( + [s for s in reference_structs if s != cls], + key=lambda s: s.type_name, + ) for struct in sorted_structs: - struct_sig += struct._encode_type(resolve_references=False) + struct_sig += struct._encode_type(resolve_references=False) # pylint: disable=protected-access return struct_sig @classmethod def _gather_reference_structs(cls, struct_set): - """Finds reference structs defined in this struct type, and inserts them into the given set. - """ + """Find reference structs defined in this struct type, and inserts them into the given set.""" structs = [m[1] for m in cls.get_members() if isinstance(m[1], type) and issubclass(m[1], EIP712Struct)] for struct in structs: if struct not in struct_set: struct_set.add(struct) - struct._gather_reference_structs(struct_set) + struct._gather_reference_structs(struct_set) # pylint: disable=protected-access @classmethod def encode_type(cls): @@ -117,7 +112,7 @@ def encode_type(cls): Nested structs are also encoded, and appended in alphabetical order. """ - return cls._encode_type(True) + return cls._encode_type(resolve_references=True) @classmethod def type_hash(cls) -> bytes: @@ -125,138 +120,147 @@ def type_hash(cls) -> bytes: return keccak(text=cls.encode_type()) def hash_struct(self) -> bytes: - """The hash of the struct. + """Return the hash of the struct. hash_struct => keccak(type_hash || encode_data) """ - return keccak(b''.join([self.type_hash(), self.encode_value()])) + return keccak(b"".join([self.type_hash(), self.encode_value()])) @classmethod - def get_members(cls) -> List[Tuple[str, EIP712Type]]: - """A list of tuples of supported parameters. + def get_members(cls) -> List[Tuple[str, EIP712Type | type[EIP712Type]]]: + """Return a list of tuples of supported parameters. - Each tuple is (, ). The list's order is determined by definition order. + Each tuple is (, ). """ - members = [m for m in cls.__dict__.items() if isinstance(m[1], EIP712Type) - or (isinstance(m[1], type) and issubclass(m[1], EIP712Struct))] - return members + return [ + (name, attr) + for name, attr in cls.__dict__.items() + if isinstance(attr, EIP712Type) or isinstance(attr, type) and issubclass(attr, EIP712Type) + ] @staticmethod - def _assert_domain(domain): - result = domain or eip712_structs.default_domain - if not result: - raise ValueError('Domain must be provided, or eip712_structs.default_domain must be set.') - return result + def _assert_domain(domain: "EIP712Struct | None") -> "EIP712Struct": + if result := domain or eip712_structs.default_domain: + return result + raise ValueError("Domain must be provided, or eip712_structs.default_domain must be set.") - def to_message(self, domain: 'EIP712Struct' = None) -> dict: + def to_message(self, domain: "EIP712Struct | None" = eip712_structs.default_domain) -> dict: """Convert a struct into a dictionary suitable for messaging. - Dictionary is of the form: - { - 'primaryType': Name of the primary type, - 'types': Definition of each included struct type (including the domain type) - 'domain': Values for the domain struct, - 'message': Values for the message struct, - } + Dictionary is of the form: + { + 'primaryType': Name of the primary type, + 'types': Definition of each included struct type (including the domain type) + 'domain': Values for the domain struct, + 'message': Values for the message struct, + } + + Args: + domain (EIP712Struct | None, optional): The domain struct to include in the message. + Use `eip712_structs.default_domain` if None. - :returns: This struct + the domain in dict form, structured as specified for EIP712 messages. - """ + Returns: + dict: This struct + the domain in dict form, structured as specified for EIP712 messages. + """ domain = self._assert_domain(domain) structs = {domain, self} self._gather_reference_structs(structs) # Build type dictionary - types = dict() + types = {} for struct in structs: - members_json = [{ - 'name': m[0], - 'type': m[1].type_name, - } for m in struct.get_members()] + members_json = [ + { + "name": m[0], + "type": m[1].type_name, + } + for m in struct.get_members() + ] types[struct.type_name] = members_json - result = { - 'primaryType': self.type_name, - 'types': types, - 'domain': domain.data_dict(), - 'message': self.data_dict(), + return { + "primaryType": self.type_name, + "types": types, + "domain": domain.data_dict(), + "message": self.data_dict(), } - return result + def to_message_json(self, domain: "EIP712Struct | None" = None) -> str: + """Convert a struct into a JSON string suitable for messaging. - def to_message_json(self, domain: 'EIP712Struct' = None) -> str: + Returns: + str: This struct + the domain in JSON form, structured as specified for EIP712 messages. + """ message = self.to_message(domain) return json.dumps(message, cls=BytesJSONEncoder) - def signable_bytes(self, domain: 'EIP712Struct' = None) -> bytes: - """Return a ``bytes`` object suitable for signing, as specified for EIP712. + def signable_bytes(self, domain: "EIP712Struct | None" = None) -> bytes: + r"""Construct a byte object suitable for signing based on the EIP712 spec. + + This method prefixes the byte string with `b'\x19\x01'` and appends hashes of the + domain and structure, which are used to produce the final signable byte object. - As per the spec, bytes are constructed as follows: - ``b'\x19\x01' + domain_hash_bytes + struct_hash_bytes`` + Args: + domain (EIP712Struct | None, optional): The domain to include in the hash bytes. + Use `eip712_structs.default_domain` if None. - :param domain: The domain to include in the hash bytes. If None, uses ``eip712_structs.default_domain`` - :return: The bytes object + Returns: + bytes: A 32-byte object containing the encoded data suitable for signing. """ domain = self._assert_domain(domain) - result = b'\x19\x01' + domain.hash_struct() + self.hash_struct() - return result + return b"\x19\x01" + domain.hash_struct() + self.hash_struct() @classmethod - def from_message(cls, message_dict: dict) -> 'StructTuple': + def from_message(cls, message_dict: dict) -> "StructTuple": """Convert a message dictionary into two EIP712Struct objects - one for domain, another for the message struct. Returned as a StructTuple, which has the attributes ``message`` and ``domain``. - Example: + Examples: my_msg = { .. } deserialized = EIP712Struct.from_message(my_msg) msg_struct = deserialized.message domain_struct = deserialized.domain - :param message_dict: The dictionary, such as what is produced by EIP712Struct.to_message. - :return: A StructTuple object, containing the message and domain structs. + Args: + message_dict (dict): The dictionary, such as what is produced by EIP712Struct.to_message. + + Returns: + StructTuple: A StructTuple object, containing the message and domain structs. """ - structs = dict() + structs = {} unfulfilled_struct_params = defaultdict(list) - for type_name in message_dict['types']: + for type_name in message_dict["types"]: # Dynamically construct struct class from dict representation - StructFromJSON = type(type_name, (EIP712Struct,), {}) - - for member in message_dict['types'][type_name]: + struct_from_json = type(type_name, (EIP712Struct,), {}) + for member in message_dict["types"][type_name]: # Either a basic solidity type is set, or None if referring to a reference struct (we'll fill it later) - member_name = member['name'] - member_sol_type = from_solidity_type(member['type']) - setattr(StructFromJSON, member_name, member_sol_type) - if member_sol_type is None: + setattr(struct_from_json, member["name"], from_solidity_type(member["type"])) + if getattr(struct_from_json, member["name"]) is None: # Track the refs we'll need to set later. - unfulfilled_struct_params[type_name].append((member_name, member['type'])) + unfulfilled_struct_params[type_name].append((member["name"], member["type"])) + structs[type_name] = struct_from_json - structs[type_name] = StructFromJSON + regex_pattern = r"([a-zA-Z0-9_]+)(\[(\d+)?\])?" # Now that custom structs have been parsed, pass through again to set the references for struct_name, unfulfilled_member_names in unfulfilled_struct_params.items(): - regex_pattern = r'([a-zA-Z0-9_]+)(\[(\d+)?\])?' - - struct_class = structs[struct_name] for name, type_name in unfulfilled_member_names: match = re.match(regex_pattern, type_name) - base_type_name = match.group(1) - ref_struct = structs[base_type_name] - if match.group(2): + assert match is not None, f'"{type_name}" is not a valid type name.' + ref_struct = structs[match[1]] + if match[2]: # The type is an array of the struct - arr_len = match.group(3) or 0 # length of 0 means the array is dynamically sized - setattr(struct_class, name, Array(ref_struct, arr_len)) + arr_len = match[3] or 0 + setattr(structs[struct_name], name, Array(ref_struct, int(arr_len))) else: - setattr(struct_class, name, ref_struct) + setattr(structs[struct_name], name, ref_struct) - primary_struct = structs[message_dict['primaryType']] - domain_struct = structs['EIP712Domain'] - - primary_result = primary_struct(**message_dict['message']) - domain_result = domain_struct(**message_dict['domain']) - result = StructTuple(message=primary_result, domain=domain_result) - - return result + return StructTuple( + message=structs[message_dict["primaryType"]](**message_dict["message"]), + domain=structs["EIP712Domain"](**message_dict["domain"]), + ) @classmethod def _assert_key_is_member(cls, key): @@ -266,38 +270,41 @@ def _assert_key_is_member(cls, key): @classmethod def _assert_property_type(cls, key, value): - """Eagerly check for a correct member type""" + """Eagerly check for a correct member type.""" members = dict(cls.get_members()) typ = members[key] if isinstance(typ, type) and issubclass(typ, EIP712Struct): # We expect an EIP712Struct instance. Assert that's true, and check the struct signature too. - if not isinstance(value, EIP712Struct) or value._encode_type(False) != typ._encode_type(False): - raise ValueError(f'Given value is of type {type(value)}, but we expected {typ}') + if not isinstance(value, EIP712Struct) or value._encode_type(False) != typ._encode_type(False): # pylint: disable=protected-access + raise ValueError(f"Given value is of type {type(value)}, but we expected {typ}") else: # Since it isn't a nested struct, its an EIP712Type try: typ.encode_value(value) - except Exception as e: - raise ValueError(f'The python type {type(value)} does not appear ' - f'to be supported for data type {typ}.') from e + except Exception as exc: + raise ValueError( + f"The python type {type(value)} does not appear " f"to be supported for data type {typ}." + ) from exc def __getitem__(self, key): - """Provide access directly to the underlying value dictionary""" + """Return the underlying value dictionary.""" self._assert_key_is_member(key) return self.values.__getitem__(key) def __setitem__(self, key, value): - """Provide access directly to the underlying value dictionary""" + """Set the underlying value dictionary.""" self._assert_key_is_member(key) self._assert_property_type(key, value) return self.values.__setitem__(key, value) def __delitem__(self, _): - raise TypeError('Deleting entries from an EIP712Struct is not allowed.') + """Disallow deleting an entry.""" + raise TypeError("Deleting entries from an EIP712Struct is not allowed.") def __eq__(self, other): + """Equality is determined by type equality and value equality.""" if not other: # Null check return False @@ -312,10 +319,13 @@ def __eq__(self, other): return self.encode_type() == other.encode_type() and self.encode_value() == other.encode_value() def __hash__(self): + """Hash is determined by the type name and value hash.""" value_hashes = [hash(k) ^ hash(v) for k, v in self.values.items()] return functools.reduce(operator.xor, value_hashes, hash(self.type_name)) class StructTuple(NamedTuple): + """A tuple containing an EIP712Struct and an EIP712Struct.""" + message: EIP712Struct domain: EIP712Struct diff --git a/eip712_structs/types.py b/eip712_structs/types.py index 0ab615a..d6badc4 100644 --- a/eip712_structs/types.py +++ b/eip712_structs/types.py @@ -1,9 +1,14 @@ +"""EIP-712 Types.""" + import re from json import JSONEncoder -from typing import Any, Union, Type +from typing import Any, Type, Union -from eth_utils.crypto import keccak from eth_utils.conversions import to_bytes, to_hex, to_int +from eth_utils.crypto import keccak + +# allow magic value comparison +# ruff: noqa: PLR2004 class EIP712Type: @@ -11,71 +16,88 @@ class EIP712Type: Generally you wouldn't use this - instead, see the subclasses below. Or you may want an EIP712Struct instead. """ + + type_name = None + def __init__(self, type_name: str, none_val: Any): + """Initialize the type.""" self.type_name = type_name self.none_val = none_val - def encode_value(self, value) -> bytes: + def encode_value(self, value=None) -> bytes: """Given a value, verify it and convert into the format required by the spec. - :param value: A correct input value for the implemented type. - :return: A 32-byte object containing encoded data + Args: + value (Any): A correct input value for the implemented type. + + Returns: + bytes: A 32-byte object containing encoded data """ if value is None: return self._encode_value(self.none_val) - else: - return self._encode_value(value) + return self._encode_value(value) def _encode_value(self, value) -> bytes: """Must be implemented by subclasses, handles value encoding on a case-by-case basis. - Don't call this directly - use ``.encode_value(value)`` instead. + Don't call this directly - use .encode_value(value) instead. """ - pass + raise NotImplementedError("Subclasses must implement this method.") def __eq__(self, other): - self_type = getattr(self, 'type_name') - other_type = getattr(other, 'type_name') + """Equality is determined by type equality.""" + self_type = getattr(self, "type_name") + other_type = getattr(other, "type_name") return self_type is not None and self_type == other_type def __hash__(self): + """Hash is determined by the type name.""" return hash(self.type_name) class Array(EIP712Type): - def __init__(self, member_type: Union[EIP712Type, Type[EIP712Type]], fixed_length: int = 0): - """Represents an array member type. + """Represent an array member type. - Example: - a1 = Array(String()) # string[] a1 - a2 = Array(String(), 8) # string[8] a2 - a3 = Array(MyStruct) # MyStruct[] a3 - """ - fixed_length = int(fixed_length) + This class can represent both fixed and dynamic arrays of a specific member type. + + Args: + member_type (Union[EIP712Type, Type[EIP712Type]]): The type of the array members. This can be any subclass of EIP712Type. + fixed_length (int, optional): The number of elements in the array if it is a fixed-length array. Defaults to 0, which represents a dynamic array. + + Examples: + a1 = Array(String()) # string[] a1 + a2 = Array(String(), 8) # string[8] a2 + a3 = Array(MyStruct) # MyStruct[] a3 + """ + + def __init__(self, member_type: Union[EIP712Type, Type[EIP712Type]], fixed_length: int = 0): + """Initialize an instance of the Array class representing an array member type.""" + fixed_length = assert_int(fixed_length) if fixed_length == 0: - type_name = f'{member_type.type_name}[]' + type_name = f"{member_type.type_name}[]" else: - type_name = f'{member_type.type_name}[{fixed_length}]' + type_name = f"{member_type.type_name}[{fixed_length}]" self.member_type = member_type self.fixed_length = fixed_length - super(Array, self).__init__(type_name, []) + super().__init__(type_name, []) def _encode_value(self, value): - """Arrays are encoded by concatenating their encoded contents, and taking the keccak256 hash.""" + """Encode an array by concatenating its encoded contents, and taking the keccak256 hash.""" encoder = self.member_type encoded_values = [encoder.encode_value(v) for v in value] - return keccak(b''.join(encoded_values)) + return keccak(b"".join(encoded_values)) class Address(EIP712Type): + """Represent an address type.""" + def __init__(self): - """Represents an ``address`` type.""" - super(Address, self).__init__('address', 0) + """Initialize an address type.""" + super().__init__("address", 0) def _encode_value(self, value): - """Addresses are encoded like Uint160 numbers.""" - + """Encode addresses like Uint160 numbers.""" # Some smart conversions - need to get the address to a numeric before we encode it if isinstance(value, bytes): v = to_int(value) @@ -87,82 +109,89 @@ def _encode_value(self, value): class Boolean(EIP712Type): + """Represent a bool type.""" + def __init__(self): - """Represents a ``bool`` type.""" - super(Boolean, self).__init__('bool', False) + """Initialize a bool type.""" + super().__init__("bool", False) def _encode_value(self, value): """Booleans are encoded like the uint256 values of 0 and 1.""" if value is False: return Uint(256).encode_value(0) - elif value is True: + if value is True: return Uint(256).encode_value(1) - else: - raise ValueError(f'Must be True or False. Got: {value}') + raise ValueError(f"Must be True or False. Got: {value}") class Bytes(EIP712Type): - def __init__(self, length: int = 0): - """Represents a solidity bytes type. + """Represent a solidity bytes type. - Length may be used to specify a static ``bytesN`` type. Or 0 for a dynamic ``bytes`` type. - Example: - b1 = Bytes() # bytes b1 - b2 = Bytes(10) # bytes10 b2 + Length may be used to specify a static bytesN type. Or 0 for a dynamic bytes type. + Length MUST be between 0 and 32, or a ValueError is raised. - ``length`` MUST be between 0 and 32, or a ValueError is raised. - """ - length = int(length) + Examples: + b1 = Bytes() # bytes b1 + b2 = Bytes(10) # bytes10 b2 + """ + + def __init__(self, length: int = 0): + """Initialize a bytes type.""" + length = assert_int(length) if length == 0: # Special case: Length of 0 means a dynamic bytes type - type_name = 'bytes' + type_name = "bytes" elif 1 <= length <= 32: - type_name = f'bytes{length}' + type_name = f"bytes{length}" else: - raise ValueError(f'Byte length must be between 1 or 32. Got: {length}') + raise ValueError(f"Byte length must be between 1 or 32. Got: {length}") self.length = length - super(Bytes, self).__init__(type_name, b'') + super().__init__(type_name, b"") def _encode_value(self, value): - """Static bytesN types are encoded by right-padding to 32 bytes. Dynamic bytes types are keccak256 hashed.""" + """Encode static bytesN types by right-padding to 32 bytes. Dynamic bytes types are keccak256 hashed.""" if isinstance(value, str): # Try converting to a bytestring, assuming that it's been given as hex value = to_bytes(hexstr=value) if self.length == 0: return keccak(value) - else: - if len(value) > self.length: - raise ValueError(f'{self.type_name} was given bytes with length {len(value)}') - padding = bytes(32 - len(value)) - return value + padding + if len(value) > self.length: + raise ValueError(f"{self.type_name} was given bytes with length {len(value)}") + return value + bytes(32 - len(value)) class Int(EIP712Type): - def __init__(self, length: int = 256): - """Represents a signed int type. Length may be given to specify the int length in bits. Default length is 256 + """Represent a signed int type. - Example: - i1 = Int(256) # int256 i1 - i2 = Int() # int256 i2 - i3 = Int(128) # int128 i3 - """ - length = int(length) + Length may be given to specify the int length in bits. Default length is 256. + + Examples: + i1 = Int(256) # int256 i1 + i2 = Int() # int256 i2 + i3 = Int(128) # int128 i3 + """ + + def __init__(self, length: int = 256): + """Initialize an int type.""" + length = assert_int(length) if length < 8 or length > 256 or length % 8 != 0: - raise ValueError(f'Int length must be a multiple of 8, between 8 and 256. Got: {length}') + raise ValueError(f"Int length must be a multiple of 8, between 8 and 256. Got: {length}") self.length = length - super(Int, self).__init__(f'int{length}', 0) + super().__init__(f"int{length}", 0) def _encode_value(self, value: int): """Ints are encoded by padding them to 256-bit representations.""" - value.to_bytes(self.length // 8, byteorder='big', signed=True) # For validation - return value.to_bytes(32, byteorder='big', signed=True) + value.to_bytes(self.length // 8, byteorder="big", signed=True) # For validation + return value.to_bytes(32, byteorder="big", signed=True) class String(EIP712Type): + """Represent a string type.""" + def __init__(self): - """Represents a string type.""" - super(String, self).__init__('string', '') + """Initialize a string type.""" + super().__init__("string", "") def _encode_value(self, value): """Strings are encoded by taking the keccak256 hash of their contents.""" @@ -170,49 +199,62 @@ def _encode_value(self, value): class Uint(EIP712Type): - def __init__(self, length: int = 256): - """Represents an unsigned int type. Length may be given to specify the int length in bits. Default length is 256 + """Represent an unsigned int type. - Example: - ui1 = Uint(256) # uint256 ui1 - ui2 = Uint() # uint256 ui2 - ui3 = Uint(128) # uint128 ui3 - """ - length = int(length) + Length may be given to specify the int length in bits. Default length is 256. + + Examples: + ui1 = Uint(256) # uint256 ui1 + ui2 = Uint() # uint256 ui2 + ui3 = Uint(128) # uint128 ui3 + """ + + def __init__(self, length: int = 256): + """Initialize a uint type.""" + length = assert_int(length) if length < 8 or length > 256 or length % 8 != 0: - raise ValueError(f'Uint length must be a multiple of 8, between 8 and 256. Got: {length}') + raise ValueError(f"Uint length must be a multiple of 8, between 8 and 256. Got: {length}") self.length = length - super(Uint, self).__init__(f'uint{length}', 0) + super().__init__(f"uint{length}", 0) def _encode_value(self, value: int): """Uints are encoded by padding them to 256-bit representations.""" - value.to_bytes(self.length // 8, byteorder='big', signed=False) # For validation - return value.to_bytes(32, byteorder='big', signed=False) + value.to_bytes(self.length // 8, byteorder="big", signed=False) # For validation + return value.to_bytes(32, byteorder="big", signed=False) + + +def assert_int(value) -> int: + """Convert to int, raising an error if unable.""" + try: + return int(value) + except ValueError as exc: + raise ValueError(f"Expected an int, got {value}") from exc # This helper dict maps solidity's type names to our EIP712Type classes solidity_type_map = { - 'address': Address, - 'bool': Boolean, - 'bytes': Bytes, - 'int': Int, - 'string': String, - 'uint': Uint, + "address": Address, + "bool": Boolean, + "bytes": Bytes, + "int": Int, + "string": String, + "uint": Uint, } -def from_solidity_type(solidity_type: str): +def from_solidity_type(solidity_type: str) -> EIP712Type | None: """Convert a string into the EIP712Type implementation. Basic types only.""" - pattern = r'([a-z]+)(\d+)?(\[(\d+)?\])?' + pattern = r"([a-z]+)(\d+)?(\[(\d+)?\])?" match = re.match(pattern, solidity_type) if match is None: return None - type_name = match.group(1) # The type name, like the "bytes" in "bytes32" - opt_len = match.group(2) # An optional length spec, like the "32" in "bytes32" - is_array = match.group(3) # Basically just checks for square brackets - array_len = match.group(4) # For fixed length arrays only, this is the length + # type_name # The type name, like the "bytes" in "bytes32" + # opt_len # An optional length spec, like the "32" in "bytes32" + # is_array # Basically just checks for square brackets + # array_len # For fixed length arrays only, this is the length + type_name, opt_len, is_array, array_len = match.groups() if type_name not in solidity_type_map: # Only supporting basic types here - return None if we don't recognize it. @@ -220,25 +262,15 @@ def from_solidity_type(solidity_type: str): # Construct the basic type base_type = solidity_type_map[type_name] - if opt_len: - type_instance = base_type(int(opt_len)) - else: - type_instance = base_type() - + type_instance = base_type(int(opt_len)) if opt_len else base_type() if is_array: - # Nest the aforementioned basic type into an Array. - if array_len: - result = Array(type_instance, int(array_len)) - else: - result = Array(type_instance) - return result - else: - return type_instance + return Array(type_instance, int(array_len)) if array_len else Array(type_instance) + return type_instance class BytesJSONEncoder(JSONEncoder): + """Custom JSON encoder for bytes.""" + def default(self, o): - if isinstance(o, bytes): - return to_hex(o) - else: - return super(BytesJSONEncoder, self).default(o) + """Encode bytes as hex strings.""" + return to_hex(o) if isinstance(o, bytes) else super().default(o) diff --git a/pyproject.toml b/pyproject.toml new file mode 100755 index 0000000..3d7999a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[project] +name = "eip712-structs" +version = "1.2.0" +authors = [ + { name = "AJ Grubbs" }, + { name = "Mihai Cosma", email = "mcosma@gmail.com" } +] +requires-python = ">= 3.10" +dependencies = ["eth-utils"] +description = "A python library for EIP712 objects" +keywords = ["ethereum", "eip712", "solidity"] +license = {text = "MIT License"} + +[project.urls] +Homepage = "https://github.com/wakamex/py-eip712-structs" +Repository = "https://github.com/wakamex/py-eip712-structs.git" + +[project.optional-dependencies] +test = [ + "pytest", + "web3", +] +dev = [ + "ruff", +] +all = [ + "eip712-structs[test, dev]", +] + +[tool.pylint.format] +max-line-length = "120" + +[tool.ruff] +# Assume Python 3.12 +target-version = "py312" +line-length = 120 + +[tool.ruff.lint] +# Default is: pycodestyle (E) and Pyflakes (F) +# We add flake8-builtins (A), pydocstyle (D), isort (I), pep8-naming (N), and pylint (PL). +# We remove pycodestyle (E) since it throws erroneous line too long errors. +# We remove Pyflakes (F) since it complains about `import *` which we need. +select = ["A", "D", "I", "N", "PL"] + +# We ignore the following rules: +# D100: Missing docstring in public module +# D103: Missing docstring in public function +# D203: 1 blank line required before class docstring (incompatible with D211: no blank lines before class docstring) +# D213: multi-line-summary-second-line (incompatible with D212: multi-line summary should start at the first line) +# D406: Section name should end with a newline +# D407: Missing dashed underline after section (not needed for Google docstring format) +# D413: Missing blank line after last section +ignore = ["D100", "D103", "D203", "D213", "D406", "D407", "D413"] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +fixable = ["A", "D", "I", "N", "PL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..2b2b58e --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -rA --verbosity=2 --log-cli-level=INFO diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b793b34..0000000 --- a/requirements.txt +++ /dev/null @@ -1,40 +0,0 @@ -atomicwrites==1.3.0 -attrdict==2.0.1 -attrs==19.1.0 -certifi==2019.3.9 -chardet==3.0.4 -coverage==4.5.3 -coveralls==1.8.0 -cytoolz==0.9.0.1 -docopt==0.6.2 -eth-abi==1.3.0 -eth-account==0.3.0 -eth-hash==0.2.0 -eth-keyfile==0.5.1 -eth-keys==0.2.3 -eth-rlp==0.1.2 -eth-typing==2.1.0 -eth-utils==1.6.0 -hexbytes==0.2.0 -idna==2.8 -importlib-metadata==0.17 -lru-dict==1.1.6 -more-itertools==7.0.0 -packaging==19.0 -parsimonious==0.8.1 -pluggy==0.12.0 -py==1.8.0 -pycryptodome==3.8.2 -pyparsing==2.4.0 -pysha3==1.0.2 -pytest==4.6.2 -pytest-cov==2.7.1 -requests==2.22.0 -rlp==1.1.0 -six==1.12.0 -toolz==0.9.0 -urllib3==1.25.3 -wcwidth==0.1.7 -web3==4.9.2 -websockets==6.0 -zipp==0.5.1 diff --git a/setup.py b/setup.py deleted file mode 100644 index c35978c..0000000 --- a/setup.py +++ /dev/null @@ -1,83 +0,0 @@ -import shlex -import sys -from pathlib import Path - -from setuptools import setup, find_packages -from setuptools.command.test import test as TestCommand - - -NAME = 'eip712-structs' -VERSION = '1.1.0' - -install_requirements = [ - 'eth-utils>=1.4.0', - 'pysha3>=1.0.2', -] - -test_requirements = [ - 'coveralls==1.8.0', - 'pytest==4.6.2', - 'pytest-cov==2.7.1', - 'web3==4.9.2', -] - - -def get_file_text(filename): - file_path = Path(__file__).parent / filename - if not file_path.exists(): - return '' - else: - file_text = file_path.read_text().strip() - return file_text - - -long_description = get_file_text('README.md') - - -class PyTest(TestCommand): - user_options = [("pytest-args=", "a", "Arguments to pass to pytest")] - - def initialize_options(self): - TestCommand.initialize_options(self) - self.pytest_args = "" - - def run_tests(self): - # import here, cause outside the eggs aren't loaded - import pytest - - errno = pytest.main(shlex.split(self.pytest_args)) - sys.exit(errno) - - -class CoverallsCommand(TestCommand): - description = 'Run the coveralls command' - user_options = [("coveralls-args=", "a", "Arguments to pass to coveralls")] - - def initialize_options(self): - TestCommand.initialize_options(self) - self.coveralls_args = "" - - def run_tests(self): - import coveralls.cli - errno = coveralls.cli.main(shlex.split(self.coveralls_args)) - sys.exit(errno) - - -setup( - name=NAME, - version=VERSION, - author='AJ Grubbs', - packages=find_packages(), - install_requires=install_requirements, - tests_require=test_requirements, - cmdclass={ - "test": PyTest, - "coveralls": CoverallsCommand, - }, - description='A python library for EIP712 objects', - long_description=long_description, - long_description_content_type='text/markdown', - license='MIT', - keywords='ethereum eip712 solidity', - url='https://github.com/ConsenSys/py-eip712-structs', -) diff --git a/tests/contracts/hash_test_contract.sol b/tests/contracts/hash_test_contract.sol index b020bbd..81559a4 100644 --- a/tests/contracts/hash_test_contract.sol +++ b/tests/contracts/hash_test_contract.sol @@ -1,4 +1,4 @@ -pragma solidity >=0.5.0 <0.6.0; +pragma solidity >=0.5.0; pragma experimental ABIEncoderV2; diff --git a/tests/test_chain_parity.py b/tests/test_chain_parity.py index 5022f13..0e5be3d 100644 --- a/tests/test_chain_parity.py +++ b/tests/test_chain_parity.py @@ -1,44 +1,60 @@ +"""Test that chain parity works.""" + import os -import pytest +import pytest from requests.exceptions import ConnectionError from web3 import HTTPProvider, Web3 -from eip712_structs import EIP712Struct, String, Uint, Int, Address, Boolean, Bytes, Array - - -@pytest.fixture(scope='module') +from eip712_structs import Address, Array, Boolean, Bytes, EIP712Struct, Int, String, Uint + +# allow redefining ConnectionError +# pylint: disable=redefined-builtin +# allow lots of function arguments +# ruff: noqa: PLR0913 +# pylint: disable=too-many-arguments +# allow lots of local variables +# pylint: disable=too-many-locals +# allow redefining outer name for fixtures +# pylint: disable=redefined-outer-name +# allow classes without docstrings +# ruff: noqa: D101 +# pylint: disable=missing-class-docstring +# allow classes with no methods +# pylint: disable=too-few-public-methods + + +@pytest.fixture(scope="module") def w3(): """Provide a Web3 client to interact with a local chain.""" - client = Web3(HTTPProvider('http://localhost:8545')) - client.eth.defaultAccount = client.eth.accounts[0] + client = Web3(HTTPProvider("http://localhost:11111")) + client.eth.default_account = client.eth.accounts[0] return client -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def contract(w3): """Deploys the test contract to the local chain, and returns a Web3.py Contract to interact with it. Note this expects the contract to be compiled already. This project's docker-compose config pulls a solc container to do this for you. """ - base_path = 'tests/contracts/build/TestContract' - with open(f'{base_path}.abi', 'r') as f: + base_path = "tests/contracts/build/TestContract" + with open(f"{base_path}.abi", "r", encoding="utf-8") as f: abi = f.read() - with open(f'{base_path}.bin', 'r') as f: + with open(f"{base_path}.bin", "r", encoding="utf-8") as f: bytecode = f.read() tmp_contract = w3.eth.contract(abi=abi, bytecode=bytecode) deploy_hash = tmp_contract.constructor().transact() - deploy_receipt = w3.eth.waitForTransactionReceipt(deploy_hash) + deploy_receipt = w3.eth.wait_for_transaction_receipt(deploy_hash) - deployed_contract = w3.eth.contract(abi=abi, address=deploy_receipt.contractAddress) - return deployed_contract + return w3.eth.contract(abi=abi, address=deploy_receipt.contractAddress) def skip_this_module(): """If we can't reach a local chain, then all tests in this module are skipped.""" - client = Web3(HTTPProvider('http://localhost:8545')) + client = Web3(HTTPProvider("http://localhost:11111")) try: client.eth.accounts except ConnectionError: @@ -47,7 +63,7 @@ def skip_this_module(): # Implicitly adds this ``skipif`` mark to the tests below. -pytestmark = pytest.mark.skipif(skip_this_module(), reason='No accessible test chain.') +pytestmark = pytest.mark.skipif(skip_this_module(), reason="No accessible test chain.") # These structs must match the struct in tests/contracts/hash_test_contract.sol @@ -69,9 +85,8 @@ class Foo(EIP712Struct): def get_chain_hash(contract, s, u_i, s_i, a, b, bytes_30, dyn_bytes, bar_uint, arr) -> bytes: - """Uses the contract to create and hash a Foo struct with the given parameters.""" - result = contract.functions.hashFooStructFromParams(s, u_i, s_i, a, b, bytes_30, dyn_bytes, bar_uint, arr).call() - return result + """Use the contract to create and hash a Foo struct with the given parameters.""" + return contract.functions.hashFooStructFromParams(s, u_i, s_i, a, b, bytes_30, dyn_bytes, bar_uint, arr).call() def test_encoded_types(contract): @@ -101,12 +116,11 @@ def test_encoded_types(contract): def test_chain_hash_matches(contract): """Assert that the hashes we derive locally match the hashes derived on-chain.""" - # Initialize basic values - s = 'some string' + s = "some string" u_i = 1234 s_i = -7 - a = Web3.toChecksumAddress(f'0x{os.urandom(20).hex()}') + a = Web3.to_checksum_address(f"0x{os.urandom(20).hex()}") b = True bytes_30 = os.urandom(30) dyn_bytes = os.urandom(50) diff --git a/tests/test_domain_separator.py b/tests/test_domain_separator.py index 0c4549d..1a14080 100644 --- a/tests/test_domain_separator.py +++ b/tests/test_domain_separator.py @@ -1,10 +1,29 @@ +"""Test domain separator.""" + import os import pytest from eth_utils.crypto import keccak import eip712_structs -from eip712_structs import make_domain, EIP712Struct, String +from eip712_structs import EIP712Struct, String, make_domain + +# allow lots of function arguments +# ruff: noqa: PLR0913 +# pylint: disable=too-many-arguments +# allow lots of local variables +# pylint: disable=too-many-locals +# allow redefining outer name for fixtures +# pylint: disable=redefined-outer-name +# allow classes without docstrings +# ruff: noqa: D101 +# pylint: disable=missing-class-docstring +# allow functions without docstrings +# pylint: disable=missing-function-docstring +# allow classes with no methods +# pylint: disable=too-few-public-methods +# allow unused arguments +# pylint: disable=unused-argument @pytest.fixture @@ -18,15 +37,15 @@ def default_domain_manager(): def test_domain_sep_create(): salt = os.urandom(32) - domain_struct = make_domain(name='name', salt=salt) + domain_struct = make_domain(name="name", salt=salt) - expected_result = 'EIP712Domain(string name,bytes32 salt)' + expected_result = "EIP712Domain(string name,bytes32 salt)" assert domain_struct.encode_type() == expected_result - expected_data = b''.join([keccak(text='name'), salt]) + expected_data = b"".join([keccak(text="name"), salt]) assert domain_struct.encode_value() == expected_data - with pytest.raises(ValueError, match='At least one argument must be given'): + with pytest.raises(ValueError, match="At least one argument must be given"): make_domain() @@ -34,16 +53,20 @@ def test_domain_sep_types(): salt = os.urandom(32) contract = os.urandom(20) - domain_struct = make_domain(name='name', version='version', chainId=1, - verifyingContract=contract, salt=salt) + domain_struct = make_domain(name="name", version="version", chainId=1, verifyingContract=contract, salt=salt) - encoded_data = [keccak(text='name'), keccak(text='version'), int(1).to_bytes(32, 'big', signed=False), - bytes(12) + contract, salt] + encoded_data = [ + keccak(text="name"), + keccak(text="version"), + int(1).to_bytes(32, "big", signed=False), + bytes(12) + contract, + salt, + ] - expected_result = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)' + expected_result = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" assert domain_struct.encode_type() == expected_result - expected_data = b''.join(encoded_data) + expected_data = b"".join(encoded_data) assert domain_struct.encode_value() == expected_data @@ -52,15 +75,16 @@ def test_default_domain(default_domain_manager): class Foo(EIP712Struct): s = String() - foo = Foo(s='hello world') - domain = make_domain(name='domain') - other_domain = make_domain(name='other domain') + foo = Foo(s="hello world") + + domain = make_domain(name="domain") + other_domain = make_domain(name="other domain") # When neither methods provide a domain, expect a ValueError - with pytest.raises(ValueError, match='Domain must be provided'): + with pytest.raises(ValueError, match="Domain must be provided"): foo.to_message() - with pytest.raises(ValueError, match='Domain must be provided'): + with pytest.raises(ValueError, match="Domain must be provided"): foo.signable_bytes() # But we can still provide a domain explicitly diff --git a/tests/test_encode_data.py b/tests/test_encode_data.py index 943f716..22f137d 100644 --- a/tests/test_encode_data.py +++ b/tests/test_encode_data.py @@ -1,11 +1,30 @@ +"""Test encoding data.""" + import os import random import string -from eth_utils.crypto import keccak import pytest +from eth_utils.crypto import keccak -from eip712_structs import Address, Array, Boolean, Bytes, Int, String, Uint, EIP712Struct, make_domain +from eip712_structs import Address, Array, Boolean, Bytes, EIP712Struct, Int, String, Uint, make_domain + +# allow magic value comparison +# ruff: noqa: PLR2004 +# allow lots of function arguments +# ruff: noqa: PLR0913 +# pylint: disable=too-many-arguments +# allow lots of local variables +# pylint: disable=too-many-locals +# allow redefining outer name for fixtures +# pylint: disable=redefined-outer-name +# allow classes without docstrings +# ruff: noqa: D101 +# pylint: disable=missing-class-docstring +# allow functions without docstrings +# pylint: disable=missing-function-docstring +# allow classes with no methods +# pylint: disable=too-few-public-methods def signed_min_max(bits): @@ -31,37 +50,37 @@ class TestStruct(EIP712Struct): uint_32 = Uint(32) uint_256 = Uint(256) - values = dict() - values['address'] = os.urandom(20) - values['boolean'] = False - values['dyn_bytes'] = os.urandom(random.choice(range(33, 100))) - values['bytes_1'] = os.urandom(1) - values['bytes_32'] = os.urandom(32) - values['int_32'] = random.randint(*signed_min_max(32)) - values['int_256'] = random.randint(*signed_min_max(256)) - values['string'] = ''.join([random.choice(string.ascii_letters) for _ in range(100)]) - values['uint_32'] = random.randint(0, unsigned_max(32)) - values['uint_256'] = random.randint(0, unsigned_max(256)) - - expected_data = list() - expected_data.append(bytes(12) + values['address']) + values = {} + values["address"] = os.urandom(20) + values["boolean"] = False + values["dyn_bytes"] = os.urandom(random.choice(range(33, 100))) + values["bytes_1"] = os.urandom(1) + values["bytes_32"] = os.urandom(32) + values["int_32"] = random.randint(*signed_min_max(32)) + values["int_256"] = random.randint(*signed_min_max(256)) + values["string"] = "".join([random.choice(string.ascii_letters) for _ in range(100)]) + values["uint_32"] = random.randint(0, unsigned_max(32)) + values["uint_256"] = random.randint(0, unsigned_max(256)) + + expected_data = [] + expected_data.append(bytes(12) + values["address"]) expected_data.append(bytes(32)) - expected_data.append(keccak(values['dyn_bytes'])) - expected_data.append(values['bytes_1'] + bytes(31)) - expected_data.append(values['bytes_32']) - expected_data.append(values['int_32'].to_bytes(32, byteorder='big', signed=True)) - expected_data.append(values['int_256'].to_bytes(32, byteorder='big', signed=True)) - expected_data.append(keccak(text=values['string'])) - expected_data.append(values['uint_32'].to_bytes(32, byteorder='big', signed=False)) - expected_data.append(values['uint_256'].to_bytes(32, byteorder='big', signed=False)) + expected_data.append(keccak(values["dyn_bytes"])) + expected_data.append(values["bytes_1"] + bytes(31)) + expected_data.append(values["bytes_32"]) + expected_data.append(values["int_32"].to_bytes(32, byteorder="big", signed=True)) + expected_data.append(values["int_256"].to_bytes(32, byteorder="big", signed=True)) + expected_data.append(keccak(text=values["string"])) + expected_data.append(values["uint_32"].to_bytes(32, byteorder="big", signed=False)) + expected_data.append(values["uint_256"].to_bytes(32, byteorder="big", signed=False)) s = TestStruct(**values) encoded_data = s.encode_value() - encoded_bytes = list() + encoded_bytes = [] # Compare each byte range itself to find offenders for i in range(0, len(encoded_data), 32): - encoded_bytes.append(encoded_data[i:i + 32]) + encoded_bytes.append(encoded_data[i : i + 32]) assert encoded_bytes == expected_data @@ -73,7 +92,7 @@ class TestStruct(EIP712Struct): byte_array = [os.urandom(32) for _ in range(4)] s = TestStruct(byte_array=byte_array) - assert s.encode_value() == keccak(b''.join(byte_array)) + assert s.encode_value() == keccak(b"".join(byte_array)) def test_encode_nested_structs(): @@ -85,9 +104,9 @@ class MainStruct(EIP712Struct): sub_2 = String() sub_3 = SubStruct - s1 = 'foo' - s2 = 'bar' - s3 = 'baz' + s1 = "foo" + s2 = "bar" + s3 = "baz" sub_1 = SubStruct(s=s1) sub_3 = SubStruct(s=s3) @@ -98,7 +117,7 @@ class MainStruct(EIP712Struct): sub_3=sub_3, ) - expected_encoded_vals = b''.join([sub_1.hash_struct(), keccak(text=s2), sub_3.hash_struct()]) + expected_encoded_vals = b"".join([sub_1.hash_struct(), keccak(text=s2), sub_3.hash_struct()]) assert s.encode_value() == expected_encoded_vals @@ -113,18 +132,18 @@ class Bar(EIP712Struct): bar = Bar( foo=Foo( - s='hello', + s="hello", i=100, ), - b=b'\xff' + b=b"\xff", ) expected_result = { - 'foo': { - 's': 'hello', - 'i': 100, + "foo": { + "s": "hello", + "i": 100, }, - 'b': b'\xff' + "b": b"\xff", } assert bar.data_dict() == expected_result @@ -134,15 +153,15 @@ class Foo(EIP712Struct): s = String() i = Int(256) - domain = make_domain(name='hello') - foo = Foo(s='hello', i=1234) + domain = make_domain(name="hello") + foo = Foo(s="hello", i=1234) - start_bytes = b'\x19\x01' + start_bytes = b"\x19\x01" exp_domain_bytes = keccak(domain.type_hash() + domain.encode_value()) exp_struct_bytes = keccak(foo.type_hash() + foo.encode_value()) sign_bytes = foo.signable_bytes(domain) - assert sign_bytes[0:2] == start_bytes + assert sign_bytes[:2] == start_bytes assert sign_bytes[2:34] == exp_domain_bytes assert sign_bytes[34:] == exp_struct_bytes @@ -156,49 +175,49 @@ class Foo(EIP712Struct): encoded_val = foo.encode_value() assert len(encoded_val) == 64 - empty_string_hash = keccak(text='') - assert encoded_val[0:32] == empty_string_hash + empty_string_hash = keccak(text="") + assert encoded_val[:32] == empty_string_hash assert encoded_val[32:] == bytes(32) def test_validation_errors(): bytes_type = Bytes(10) - int_type = Int(8) # -128 <= i < 128 + int_type = Int(8) # -128 <= i < 128 uint_type = Uint(8) # 0 <= i < 256 bool_type = Boolean() - with pytest.raises(ValueError, match='bytes10 was given bytes with length 11'): + with pytest.raises(ValueError, match="bytes10 was given bytes with length 11"): bytes_type.encode_value(os.urandom(11)) - with pytest.raises(OverflowError, match='too big'): + with pytest.raises(OverflowError, match="too big"): int_type.encode_value(128) - with pytest.raises(OverflowError, match='too big'): + with pytest.raises(OverflowError, match="too big"): int_type.encode_value(-129) - with pytest.raises(OverflowError, match='too big'): + with pytest.raises(OverflowError, match="too big"): uint_type.encode_value(256) assert uint_type.encode_value(0) == bytes(32) - with pytest.raises(OverflowError, match='negative int to unsigned'): + with pytest.raises(OverflowError, match="negative int to unsigned"): uint_type.encode_value(-1) - assert bool_type.encode_value(True) == bytes(31) + b'\x01' + assert bool_type.encode_value(True) == bytes(31) + b"\x01" assert bool_type.encode_value(False) == bytes(32) - with pytest.raises(ValueError, match='Must be True or False.'): + with pytest.raises(ValueError, match="Must be True or False."): bool_type.encode_value(0) - with pytest.raises(ValueError, match='Must be True or False.'): + with pytest.raises(ValueError, match="Must be True or False."): bool_type.encode_value(1) def test_struct_eq(): class Foo(EIP712Struct): s = String() - foo = Foo(s='hello world') - foo_copy = Foo(s='hello world') - foo_2 = Foo(s='blah') - assert foo != None - assert foo != 'unrelated type' - assert foo == foo + foo = Foo(s="hello world") + foo_copy = Foo(s="hello world") + foo_2 = Foo(s="blah") + + assert foo is not None + assert foo != "unrelated type" assert foo is not foo_copy assert foo == foo_copy assert foo != foo_2 @@ -207,28 +226,31 @@ def make_different_foo(): # We want another struct defined with the same name but different member types class Foo(EIP712Struct): b = Bytes() + return Foo def make_same_foo(): # For good measure, recreate the exact same class and ensure they can still compare class Foo(EIP712Struct): s = String() + return Foo - OtherFooClass = make_different_foo() - wrong_type = OtherFooClass(b=b'hello world') + other_foo_class = make_different_foo() + wrong_type = other_foo_class(b=b"hello world") assert wrong_type != foo - assert OtherFooClass != Foo + assert other_foo_class != Foo - SameFooClass = make_same_foo() - right_type = SameFooClass(s='hello world') + same_foo_class = make_same_foo() + right_type = same_foo_class(s="hello world") assert right_type == foo - assert SameFooClass != Foo + assert same_foo_class != Foo # Different name, same members class Bar(EIP712Struct): s = String() - bar = Bar(s='hello world') + + bar = Bar(s="hello world") assert bar != foo @@ -237,35 +259,35 @@ class Foo(EIP712Struct): s = String() b = Bytes(32) - test_str = 'hello world' + test_str = "hello world" test_bytes = os.urandom(32) foo = Foo(s=test_str, b=test_bytes) - assert foo['s'] == test_str - assert foo['b'] == test_bytes + assert foo["s"] == test_str + assert foo["b"] == test_bytes test_bytes_2 = os.urandom(32) - foo['b'] = test_bytes_2 + foo["b"] = test_bytes_2 - assert foo['b'] == test_bytes_2 + assert foo["b"] == test_bytes_2 with pytest.raises(KeyError): - foo['x'] = 'unacceptable' + foo["x"] = "unacceptable" # Check behavior when accessing a member that wasn't defined for the struct. with pytest.raises(KeyError): - foo['x'] + foo["x"] # Lets cheat a lil bit for robustness- add an invalid 'x' member to the value dict, and check the error still raises - foo.values['x'] = 'test' + foo.values["x"] = "test" with pytest.raises(KeyError): - foo['x'] - foo.values.pop('x') + foo["x"] + foo.values.pop("x") with pytest.raises(ValueError): - foo['s'] = b'unacceptable' + foo["s"] = b"unacceptable" with pytest.raises(ValueError): # Bytes do accept strings, but it has to be hex formatted. - foo['b'] = 'unacceptable' + foo["b"] = "unacceptable" # Test behavior when attempting to set nested structs as values class Bar(EIP712Struct): @@ -274,15 +296,16 @@ class Bar(EIP712Struct): class Baz(EIP712Struct): s = String() + baz = Baz(s=test_str) bar = Bar(s=test_str) - bar['f'] = foo - assert bar['f'] == foo + bar["f"] = foo + assert bar["f"] == foo with pytest.raises(ValueError): # Expects a Foo type, so should throw an error - bar['f'] = baz + bar["f"] = baz with pytest.raises(TypeError): - del foo['s'] + del foo["s"] diff --git a/tests/test_encode_type.py b/tests/test_encode_type.py index 649d346..61f868e 100644 --- a/tests/test_encode_type.py +++ b/tests/test_encode_type.py @@ -1,11 +1,28 @@ +"""Test encoding types.""" + from eip712_structs import Address, Array, EIP712Struct, Int, String, Uint +# allow lots of function arguments +# ruff: noqa: PLR0913 +# pylint: disable=too-many-arguments +# allow lots of local variables +# pylint: disable=too-many-locals +# allow redefining outer name for fixtures +# pylint: disable=redefined-outer-name +# allow classes without docstrings +# ruff: noqa: D101 +# pylint: disable=missing-class-docstring +# allow functions without docstrings +# pylint: disable=missing-function-docstring +# allow classes with no methods +# pylint: disable=too-few-public-methods + def test_empty_struct(): class Empty(EIP712Struct): pass - assert Empty.encode_type() == 'Empty()' + assert Empty.encode_type() == "Empty()" def test_simple_struct(): @@ -13,9 +30,9 @@ class Person(EIP712Struct): name = String() addr = Address() numbers = Array(Int(256)) - moreNumbers = Array(Uint(256), 8) + moreNumbers = Array(Uint(256), 8) # noqa: N815 - expected_result = 'Person(string name,address addr,int256[] numbers,uint256[8] moreNumbers)' + expected_result = "Person(string name,address addr,int256[] numbers,uint256[8] moreNumbers)" assert Person.encode_type() == expected_result @@ -29,7 +46,7 @@ class Mail(EIP712Struct): dest = Person content = String() - expected_result = 'Mail(Person source,Person dest,string content)Person(string name,address addr)' + expected_result = "Mail(Person source,Person dest,string content)Person(string name,address addr)" assert Mail.encode_type() == expected_result @@ -39,7 +56,7 @@ class Person(EIP712Struct): Person.parent = Person - expected_result = 'Person(string name,Person parent)' + expected_result = "Person(string name,Person parent)" assert Person.encode_type() == expected_result @@ -55,7 +72,7 @@ class A(EIP712Struct): s = String() b = B - expected_result = 'A(string s,B b)B(string s,C c)C(string s)' + expected_result = "A(string s,B b)B(string s,C c)C(string s)" assert A.encode_type() == expected_result @@ -72,14 +89,14 @@ class A(EIP712Struct): s = String() c = C - expected_result = 'A(string s,C c)B(string s)C(string s,B b)' + expected_result = "A(string s,C c)B(string s)C(string s,B b)" assert A.encode_type() == expected_result class Z(EIP712Struct): s = String() a = A - expected_result = 'Z(string s,A a)' + expected_result + expected_result = "Z(string s,A a)" + expected_result assert Z.encode_type() == expected_result @@ -96,13 +113,13 @@ class A(EIP712Struct): C.a = A - a_sig = 'A(B b)' - b_sig = 'B(C c)' - c_sig = 'C(A a)' + a_sig = "A(B b)" + b_sig = "B(C c)" + c_sig = "C(A a)" - expected_result_a = f'{a_sig}{b_sig}{c_sig}' - expected_result_b = f'{b_sig}{a_sig}{c_sig}' - expected_result_c = f'{c_sig}{a_sig}{b_sig}' + expected_result_a = f"{a_sig}{b_sig}{c_sig}" + expected_result_b = f"{b_sig}{a_sig}{c_sig}" + expected_result_c = f"{c_sig}{a_sig}{b_sig}" assert A.encode_type() == expected_result_a assert B.encode_type() == expected_result_b diff --git a/tests/test_message_json.py b/tests/test_message_json.py index d422bd5..3fc214e 100644 --- a/tests/test_message_json.py +++ b/tests/test_message_json.py @@ -1,36 +1,57 @@ +"""Test message JSON.""" + import json import os import pytest -from eip712_structs import EIP712Struct, String, make_domain, Bytes +from eip712_structs import Bytes, EIP712Struct, String, make_domain + +# allow magic value comparison +# ruff: noqa: PLR2004 +# allow lots of function arguments +# ruff: noqa: PLR0913 +# pylint: disable=too-many-arguments +# allow lots of local variables +# pylint: disable=too-many-locals +# allow redefining outer name for fixtures +# pylint: disable=redefined-outer-name +# allow classes without docstrings +# ruff: noqa: D101 +# pylint: disable=missing-class-docstring +# allow functions without docstrings +# pylint: disable=missing-function-docstring +# allow classes with no methods +# pylint: disable=too-few-public-methods def test_flat_struct_to_message(): class Foo(EIP712Struct): s = String() - domain = make_domain(name='domain') - foo = Foo(s='foobar') + domain = make_domain(name="domain") + foo = Foo(s="foobar") expected_result = { - 'primaryType': 'Foo', - 'types': { - 'EIP712Domain': [{ - 'name': 'name', - 'type': 'string', - }], - 'Foo': [{ - 'name': 's', - 'type': 'string', - }] + "primaryType": "Foo", + "types": { + "EIP712Domain": [ + { + "name": "name", + "type": "string", + } + ], + "Foo": [ + { + "name": "s", + "type": "string", + } + ], }, - 'domain': { - 'name': 'domain', + "domain": { + "name": "domain", }, - 'message': { - 's': 'foobar' - } + "message": {"s": "foobar"}, } message = foo.to_message(domain) @@ -38,14 +59,14 @@ class Foo(EIP712Struct): # Now test in reverse... new_struct, domain = EIP712Struct.from_message(expected_result) - assert new_struct.type_name == 'Foo' + assert new_struct.type_name == "Foo" members_list = new_struct.get_members() assert len(members_list) == 1 - assert members_list[0][0] == 's' - assert members_list[0][1].type_name == 'string' + assert members_list[0][0] == "s" + assert members_list[0][1].type_name == "string" - assert new_struct.get_data_value('s') == 'foobar' + assert new_struct.get_data_value("s") == "foobar" def test_nested_struct_to_message(): @@ -56,41 +77,45 @@ class Foo(EIP712Struct): s = String() bar = Bar - domain = make_domain(name='domain') + domain = make_domain(name="domain") - foo = Foo( - s="foo", - bar=Bar(s="bar") - ) + foo = Foo(s="foo", bar=Bar(s="bar")) expected_result = { - 'primaryType': 'Foo', - 'types': { - 'EIP712Domain': [{ - 'name': 'name', - 'type': 'string', - }], - 'Foo': [{ - 'name': 's', - 'type': 'string', - }, { - 'name': 'bar', - 'type': 'Bar', - }], - 'Bar': [{ - 'name': 's', - 'type': 'string', - }] + "primaryType": "Foo", + "types": { + "EIP712Domain": [ + { + "name": "name", + "type": "string", + } + ], + "Foo": [ + { + "name": "s", + "type": "string", + }, + { + "name": "bar", + "type": "Bar", + }, + ], + "Bar": [ + { + "name": "s", + "type": "string", + } + ], + }, + "domain": { + "name": "domain", }, - 'domain': { - 'name': 'domain', + "message": { + "s": "foo", + "bar": { + "s": "bar", + }, }, - 'message': { - 's': 'foo', - 'bar': { - 's': 'bar', - } - } } message = foo.to_message(domain) @@ -98,16 +123,16 @@ class Foo(EIP712Struct): # And test in reverse... new_struct, new_domain = EIP712Struct.from_message(expected_result) - assert new_struct.type_name == 'Foo' + assert new_struct.type_name == "Foo" members = new_struct.get_members() assert len(members) == 2 - assert members[0][0] == 's' and members[0][1].type_name == 'string' - assert members[1][0] == 'bar' and members[1][1].type_name == 'Bar' + assert members[0][0] == "s" and members[0][1].type_name == "string" + assert members[1][0] == "bar" and members[1][1].type_name == "Bar" - bar_val = new_struct.get_data_value('bar') - assert bar_val.type_name == 'Bar' - assert bar_val.get_data_value('s') == 'bar' + bar_val = new_struct.get_data_value("bar") + assert bar_val.type_name == "Bar" + assert bar_val.get_data_value("s") == "bar" assert foo.hash_struct() == new_struct.hash_struct() @@ -115,7 +140,8 @@ class Foo(EIP712Struct): def test_bytes_json_encoder(): class Foo(EIP712Struct): b = Bytes(32) - domain = make_domain(name='domain') + + domain = make_domain(name="domain") bytes_val = os.urandom(32) foo = Foo(b=bytes_val) @@ -130,9 +156,10 @@ class Foo(EIP712Struct): class UnserializableObject: pass + obj = UnserializableObject() # Fabricate this failure case to test that the custom json encoder's fallback path works as expected. - foo.values['b'] = obj - with pytest.raises(TypeError, match='not JSON serializable'): + foo.values["b"] = obj + with pytest.raises(TypeError, match="not JSON serializable"): foo.to_message_json(domain) diff --git a/tests/test_types.py b/tests/test_types.py index 9cb7a65..cc50922 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,19 +1,38 @@ +"""Test EIP-712 types.""" + import pytest -from eip712_structs import Address, Array, Boolean, Bytes, Int, String, Uint, EIP712Struct +from eip712_structs import Address, Array, Boolean, Bytes, EIP712Struct, Int, String, Uint from eip712_structs.types import from_solidity_type +# allow magic value comparison +# ruff: noqa: PLR2004 +# allow lots of function arguments +# ruff: noqa: PLR0913 +# pylint: disable=too-many-arguments +# allow lots of local variables +# pylint: disable=too-many-locals +# allow redefining outer name for fixtures +# pylint: disable=redefined-outer-name +# allow classes without docstrings +# ruff: noqa: D101 +# pylint: disable=missing-class-docstring +# allow functions without docstrings +# pylint: disable=missing-function-docstring +# allow classes with no methods +# pylint: disable=too-few-public-methods + def test_bytes_validation(): bytes0 = Bytes() - assert bytes0.type_name == 'bytes' + assert bytes0.type_name == "bytes" bytes0 = Bytes(0) - assert bytes0.type_name == 'bytes' + assert bytes0.type_name == "bytes" for n in range(1, 33): bytes_n = Bytes(n) - assert bytes_n.type_name == f'bytes{n}' + assert bytes_n.type_name == f"bytes{n}" with pytest.raises(ValueError): Bytes(33) @@ -23,7 +42,7 @@ def run_int_test(clazz, base_name): for n in range(7, 258): if n % 8 == 0: int_n = clazz(n) - assert int_n.type_name == f'{base_name}{n}' + assert int_n.type_name == f"{base_name}{n}" else: with pytest.raises(ValueError): clazz(n) @@ -34,54 +53,54 @@ def run_int_test(clazz, base_name): def test_int_validation(): - run_int_test(Int, 'int') + run_int_test(Int, "int") def test_uint_validation(): - run_int_test(Uint, 'uint') + run_int_test(Uint, "uint") def test_arrays(): - assert Array(String()).type_name == 'string[]' - assert Array(String(), 4).type_name == 'string[4]' + assert Array(String()).type_name == "string[]" + assert Array(String(), 4).type_name == "string[4]" - assert Array(Bytes(17)).type_name == 'bytes17[]' - assert Array(Bytes(17), 10).type_name == 'bytes17[10]' + assert Array(Bytes(17)).type_name == "bytes17[]" + assert Array(Bytes(17), 10).type_name == "bytes17[10]" - assert Array(Array(Uint(160))).type_name == 'uint160[][]' + assert Array(Array(Uint(160))).type_name == "uint160[][]" def test_struct_arrays(): class Foo(EIP712Struct): s = String() - assert Array(Foo).type_name == 'Foo[]' - assert Array(Foo, 10).type_name == 'Foo[10]' + assert Array(Foo).type_name == "Foo[]" + assert Array(Foo, 10).type_name == "Foo[10]" def test_length_str_typing(): # Ensure that if length is given as a string, it's typecast to int - assert Array(String(), '5').fixed_length == 5 - assert Bytes('10').length == 10 - assert Int('128').length == 128 - assert Uint('128').length == 128 + assert Array(String(), "5").fixed_length == 5 + assert Bytes("10").length == 10 + assert Int("128").length == 128 + assert Uint("128").length == 128 def test_from_solidity_type(): - assert from_solidity_type('address') == Address() - assert from_solidity_type('bool') == Boolean() - assert from_solidity_type('bytes') == Bytes() - assert from_solidity_type('bytes32') == Bytes(32) - assert from_solidity_type('int128') == Int(128) - assert from_solidity_type('string') == String() - assert from_solidity_type('uint256') == Uint(256) - - assert from_solidity_type('address[]') == Array(Address()) - assert from_solidity_type('address[10]') == Array(Address(), 10) - assert from_solidity_type('bytes16[32]') == Array(Bytes(16), 32) + assert from_solidity_type("address") == Address() + assert from_solidity_type("bool") == Boolean() + assert from_solidity_type("bytes") == Bytes() + assert from_solidity_type("bytes32") == Bytes(32) + assert from_solidity_type("int128") == Int(128) + assert from_solidity_type("string") == String() + assert from_solidity_type("uint256") == Uint(256) + + assert from_solidity_type("address[]") == Array(Address()) + assert from_solidity_type("address[10]") == Array(Address(), 10) + assert from_solidity_type("bytes16[32]") == Array(Bytes(16), 32) # Sanity check that equivalency is working as expected - assert from_solidity_type('bytes32') != Bytes(31) - assert from_solidity_type('bytes16[32]') != Array(Bytes(16), 31) - assert from_solidity_type('bytes16[32]') != Array(Bytes(), 32) - assert from_solidity_type('bytes16[32]') != Array(Bytes(8), 32) + assert from_solidity_type("bytes32") != Bytes(31) + assert from_solidity_type("bytes16[32]") != Array(Bytes(16), 31) + assert from_solidity_type("bytes16[32]") != Array(Bytes(), 32) + assert from_solidity_type("bytes16[32]") != Array(Bytes(8), 32)