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)