├── .gitignore ├── .travis.yml ├── API.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── eip712_structs ├── __init__.py ├── domain_separator.py ├── struct.py └── types.py ├── requirements.txt ├── setup.py └── tests ├── contracts └── hash_test_contract.sol ├── test_chain_parity.py ├── test_domain_separator.py ├── test_encode_data.py ├── test_encode_type.py ├── test_message_json.py └── test_types.py /.gitignore: -------------------------------------------------------------------------------- 1 | .eggs/ 2 | *.egg-info/ 3 | dist/ 4 | build/ 5 | .pytest_cache/ 6 | .coverage 7 | 8 | tests/contracts/build 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dist: xenial 3 | language: python 4 | python: 5 | - "3.6" 6 | - "3.7" 7 | script: 8 | - python setup.py test -a --cov=eip712_structs 9 | services: 10 | - docker 11 | before_install: 12 | - docker-compose up -d 13 | after_success: 14 | - python setup.py coveralls 15 | deploy: 16 | provider: pypi 17 | user: ajrgrubbs 18 | password: 19 | secure: USn2HOarmeVduJM/8SauQf3L/n+cj+QWhUD7uYVj69SvZsm3/NQkhGCdKX4qNhy/QOhGuIJ1qo2RCDLijyPgOCmee3Cz1SRxP4jkN3BdCCqlb5iVRZCSmu4/gp3d7qZyLfidyKMx63Ra7+DiWij6xKTSdPsRZ/3DZMApIrvUkQBqnsOsA+1ycIs8ASkTRymq7kVbTOn17uzPf3jBWHZFBynCY+qe5lC0Aa1N3y4l4jrzH6zGTIFnXjirpvvBRDoKBEIj5S/X25xwcKkJtFwAzigtM0EgVcU2FqVBplniNuLh8qZrmcqqIVNC+KbWCVLfD+dYT6yhFobOjjjalhZ13LITBLVO9YPQWHFHiEcDe1jz3+aPxuDOUmWqD/1e55QfypNIwJNGdcQ8WaqxxwzT1qClYCae5FFAS03Zct8AfxlMkPpfMudaD5642gzWcGVTBZg32GlW3Q6TEbTIlr+/CIyXjxjCndlr2A463kxl06FY4XsNcMC0A40p0Ygwq3Q09FQhk+ObIY2V8Zej8Mbat4b3EEO2bK4jmTK1V3r3EqUCPI+/JbsoCxAZNdTnoQfwlwgHCzclM02OKTFSTQuXs+b8zCn/fXCJivqnfWPzqQgoD1zAMwEN4DAY53dukPzcKdgTJ1TCcwKaIcxFu4UAjzYiCFx6nLetvLkpndu2cyE= 20 | on: 21 | tags: true 22 | skip_existing: true 23 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ### `class EIP712Struct` 4 | #### Important methods 5 | - `.to_message(domain: EIP712Struct)` - Convert the struct (and given domain struct) into the standard EIP-712 message structure. 6 | - `.signable_bytes(domain: EIP712Struct)` - Get the standard EIP-712 bytes hash, suitable for signing. 7 | - `.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. 8 | 9 | #### Other stuff 10 | - `.encode_value()` - Returns a `bytes` object containing the ordered concatenation of each members bytes32 representation. 11 | - `.encode_type()` **(Class method)** - Gets the "signature" of the struct class. Includes nested structs too! 12 | - `.type_hash()` **(Class method)** - The keccak256 hash of the result of `.encode_type()`. 13 | - `.hash_struct()` - Gets the keccak256 hash of the concatenation of `.type_hash()` and `.encode_value()` 14 | - `.get_data_value(member_name: str)` - Get the value of the given struct member 15 | - `.set_data_value(member_name: str, value: Any)` - Set the value of the given struct member 16 | - `.data_dict()` - Returns a dictionary with all data in this struct. Includes nested struct data, if exists. 17 | - `.get_members()` **(Class method)** - Returns a dictionary mapping each data member's name to it's type. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ConsenSys 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EIP-712 Structs [![Build Status](https://travis-ci.org/ConsenSys/py-eip712-structs.svg?branch=master)](https://travis-ci.org/ConsenSys/py-eip712-structs) [![Coverage Status](https://coveralls.io/repos/github/ConsenSys/py-eip712-structs/badge.svg?branch=master)](https://coveralls.io/github/ConsenSys/py-eip712-structs?branch=master) 2 | 3 | A python interface for simple EIP-712 struct construction. 4 | 5 | In this module, a "struct" is structured data as defined in the standard. 6 | It is not the same as the Python Standard Library's struct (e.g., `import struct`). 7 | 8 | Read the proposal:
9 | https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md 10 | 11 | #### Supported Python Versions 12 | - `3.6` 13 | - `3.7` 14 | 15 | ## Install 16 | ```bash 17 | pip install eip712-structs 18 | ``` 19 | 20 | ## Usage 21 | See [API.md](API.md) for a succinct summary of available methods. 22 | 23 | Examples/Details below. 24 | 25 | #### Quickstart 26 | Say we want to represent the following struct, convert it to a message and sign it: 27 | ```text 28 | struct MyStruct { 29 | string some_string; 30 | uint256 some_number; 31 | } 32 | ``` 33 | 34 | With this module, that would look like: 35 | ```python 36 | # Make a unique domain 37 | from eip712_structs import make_domain 38 | domain = make_domain(name='Some name', version='1.0.0') # Make a Domain Separator 39 | 40 | # Define your struct type 41 | from eip712_structs import EIP712Struct, String, Uint 42 | class MyStruct(EIP712Struct): 43 | some_string = String() 44 | some_number = Uint(256) 45 | 46 | # Create an instance with some data 47 | mine = MyStruct(some_string='hello world', some_number=1234) 48 | 49 | # Values can be get/set dictionary-style: 50 | mine['some_number'] = 4567 51 | assert mine['some_string'] == 'hello world' 52 | assert mine['some_number'] == 4567 53 | 54 | # Into a message dict - domain required 55 | my_msg = mine.to_message(domain) 56 | 57 | # Into message JSON - domain required. 58 | # This method converts bytes types for you, which the default JSON encoder won't handle. 59 | my_msg_json = mine.to_message_json(domain) 60 | 61 | # Into signable bytes - domain required 62 | my_bytes = mine.signable_bytes(domain) 63 | ``` 64 | 65 | See [Member Types](#member-types) for more information on supported types. 66 | 67 | #### Dynamic construction 68 | Attributes may be added dynamically as well. This may be necessary if you 69 | want to use a reserved keyword like `from`. 70 | 71 | ```python 72 | from eip712_structs import EIP712Struct, Address 73 | class Message(EIP712Struct): 74 | pass 75 | 76 | Message.to = Address() 77 | setattr(Message, 'from', Address()) 78 | 79 | # At this point, Message is equivalent to `struct Message { address to; address from; }` 80 | 81 | ``` 82 | 83 | #### The domain separator 84 | EIP-712 specifies a domain struct, to differentiate between identical structs that may be unrelated. 85 | A helper method exists for this purpose. 86 | All values to the `make_domain()` 87 | function are optional - but at least one must be defined. If omitted, the resulting 88 | domain struct's definition leaves out the parameter entirely. 89 | 90 | The full signature:
91 | `make_domain(name: string, version: string, chainId: uint256, verifyingContract: address, salt: bytes32)` 92 | 93 | ##### Setting a default domain 94 | Constantly providing the same domain can be cumbersome. You can optionally set a default, and then forget it. 95 | It is automatically used by `.to_message()` and `.signable_bytes()` 96 | 97 | ```python 98 | import eip712_structs 99 | 100 | foo = SomeStruct() 101 | 102 | my_domain = eip712_structs.make_domain(name='hello world') 103 | eip712_structs.default_domain = my_domain 104 | 105 | assert foo.to_message() == foo.to_message(my_domain) 106 | assert foo.signable_bytes() == foo.signable_bytes(my_domain) 107 | ``` 108 | 109 | ## Member Types 110 | 111 | ### Basic types 112 | EIP712's basic types map directly to solidity types. 113 | 114 | ```python 115 | from eip712_structs import Address, Boolean, Bytes, Int, String, Uint 116 | 117 | Address() # Solidity's 'address' 118 | Boolean() # 'bool' 119 | Bytes() # 'bytes' 120 | Bytes(N) # 'bytesN' - N must be an int from 1 through 32 121 | Int(N) # 'intN' - N must be a multiple of 8, from 8 to 256 122 | String() # 'string' 123 | Uint(N) # 'uintN' - N must be a multiple of 8, from 8 to 256 124 | ``` 125 | 126 | Use like: 127 | ```python 128 | from eip712_structs import EIP712Struct, Address, Bytes 129 | 130 | class Foo(EIP712Struct): 131 | member_name_0 = Address() 132 | member_name_1 = Bytes(5) 133 | # ...etc 134 | ``` 135 | 136 | ### Struct references 137 | In addition to holding basic types, EIP712 structs may also hold other structs! 138 | Usage is almost the same - the difference is you don't "instantiate" the class. 139 | 140 | Example: 141 | ```python 142 | from eip712_structs import EIP712Struct, String 143 | 144 | class Dog(EIP712Struct): 145 | name = String() 146 | breed = String() 147 | 148 | class Person(EIP712Struct): 149 | name = String() 150 | dog = Dog # Take note - no parentheses! 151 | 152 | # Dog "stands alone" 153 | Dog.encode_type() # Dog(string name,string breed) 154 | 155 | # But Person knows how to include Dog 156 | Person.encode_type() # Person(string name,Dog dog)Dog(string name,string breed) 157 | ``` 158 | 159 | Instantiating the structs with nested values may be done a couple different ways: 160 | 161 | ```python 162 | # Method one: set it to a struct 163 | dog = Dog(name='Mochi', breed='Corgi') 164 | person = Person(name='E.M.', dog=dog) 165 | 166 | # Method two: set it to a dict - the underlying struct is built for you 167 | person = Person( 168 | name='E.M.', 169 | dog={ 170 | 'name': 'Mochi', 171 | 'breed': 'Corgi', 172 | } 173 | ) 174 | ``` 175 | 176 | ### Arrays 177 | Arrays are also supported for the standard. 178 | 179 | ```python 180 | array_member = Array([, ]) 181 | ``` 182 | 183 | - `` - The basic type or struct that will live in the array 184 | - `` - If given, the array is set to that length. 185 | 186 | For example: 187 | ```python 188 | dynamic_array = Array(String()) # String[] dynamic_array 189 | static_array = Array(String(), 10) # String[10] static_array 190 | struct_array = Array(MyStruct, 10) # MyStruct[10] - again, don't instantiate structs like the basic types 191 | ``` 192 | 193 | ## Development 194 | Contributions always welcome. 195 | 196 | Install dependencies: 197 | - `pip install -r requirements.txt` 198 | 199 | Run tests: 200 | - `python setup.py test` 201 | - Some tests expect an active local ganache chain on http://localhost:8545. Docker will compile the contracts and start the chain for you. 202 | - Docker is optional, but useful to test the whole suite. If no chain is detected, chain tests are skipped. 203 | - Usage: 204 | - `docker-compose up -d` (Starts containers in the background) 205 | - Note: Contracts are compiled when you run `up`, but won't be deployed until the test is run. 206 | - Cleanup containers when you're done: `docker-compose down` 207 | 208 | Deploying a new version: 209 | - Bump the version number in `setup.py`, commit it into master. 210 | - Make a release tag on the master branch in Github. Travis should handle the rest. 211 | 212 | 213 | ## Shameless Plug 214 | Written by [ConsenSys](https://consensys.net) for the world! :heart: 215 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3" 3 | services: 4 | ganache: 5 | image: "trufflesuite/ganache-cli:latest" 6 | command: "--account=\"0x3660582119566511de16f4dcf397fa324b27bd6247f653cf0298a0993f3432ed,100000000000000000000\"" 7 | ports: 8 | - "8545:8545" 9 | depends_on: 10 | - compiler 11 | labels: 12 | net.consensys.description: > 13 | Starts an instance of ganache-cli for chain parity tests. 14 | Unlocks address 0xD68D840e1e971750E6d45049ff579925456d5893" 15 | 16 | compiler: 17 | image: "ethereum/solc:stable" 18 | command: "--abi --bin -o /tmp/contracts/build --overwrite /tmp/contracts/hash_test_contract.sol" 19 | volumes: 20 | - ./tests/contracts:/tmp/contracts 21 | labels: 22 | net.consensys.description: > 23 | Compiles ./tests/contracts/hash_test_contract.sol. Places generated files in ./tests/contracts/build/. 24 | Generates JSON ABI and Bytecode hex: TestContract.abi, TestContract.bin. 25 | See ./tests/test_chain_parity.py::contract to see how this is deployed. 26 | -------------------------------------------------------------------------------- /eip712_structs/__init__.py: -------------------------------------------------------------------------------- 1 | from eip712_structs.domain_separator import make_domain 2 | from eip712_structs.struct import EIP712Struct 3 | from eip712_structs.types import Address, Array, Boolean, Bytes, Int, String, Uint 4 | 5 | default_domain = None 6 | -------------------------------------------------------------------------------- /eip712_structs/domain_separator.py: -------------------------------------------------------------------------------- 1 | import eip712_structs 2 | 3 | 4 | def make_domain(name=None, version=None, chainId=None, verifyingContract=None, salt=None): 5 | """Helper method to create the standard EIP712Domain struct for you. 6 | 7 | Per the standard, if a value is not used then the parameter is omitted from the struct entirely. 8 | """ 9 | 10 | if all(i is None for i in [name, version, chainId, verifyingContract, salt]): 11 | raise ValueError('At least one argument must be given.') 12 | 13 | class EIP712Domain(eip712_structs.EIP712Struct): 14 | pass 15 | 16 | kwargs = dict() 17 | if name is not None: 18 | EIP712Domain.name = eip712_structs.String() 19 | kwargs['name'] = str(name) 20 | if version is not None: 21 | EIP712Domain.version = eip712_structs.String() 22 | kwargs['version'] = str(version) 23 | if chainId is not None: 24 | EIP712Domain.chainId = eip712_structs.Uint(256) 25 | kwargs['chainId'] = int(chainId) 26 | if verifyingContract is not None: 27 | EIP712Domain.verifyingContract = eip712_structs.Address() 28 | kwargs['verifyingContract'] = verifyingContract 29 | if salt is not None: 30 | EIP712Domain.salt = eip712_structs.Bytes(32) 31 | kwargs['salt'] = salt 32 | 33 | return EIP712Domain(**kwargs) 34 | -------------------------------------------------------------------------------- /eip712_structs/struct.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | import operator 4 | import re 5 | from collections import OrderedDict, defaultdict 6 | from typing import List, Tuple, NamedTuple 7 | 8 | from eth_utils.crypto import keccak 9 | 10 | import eip712_structs 11 | from eip712_structs.types import Array, EIP712Type, from_solidity_type, BytesJSONEncoder 12 | 13 | 14 | class OrderedAttributesMeta(type): 15 | """Metaclass to ensure struct attribute order is preserved. 16 | """ 17 | @classmethod 18 | def __prepare__(mcs, name, bases): 19 | return OrderedDict() 20 | 21 | 22 | class EIP712Struct(EIP712Type, metaclass=OrderedAttributesMeta): 23 | """A representation of an EIP712 struct. Subclass it to use it. 24 | 25 | Example: 26 | from eip712_structs import EIP712Struct, String 27 | 28 | class MyStruct(EIP712Struct): 29 | some_param = String() 30 | 31 | struct_instance = MyStruct(some_param='some_value') 32 | """ 33 | def __init__(self, **kwargs): 34 | super(EIP712Struct, self).__init__(self.type_name, None) 35 | members = self.get_members() 36 | self.values = dict() 37 | for name, typ in members: 38 | value = kwargs.get(name) 39 | if isinstance(value, dict): 40 | value = typ(**value) 41 | self.values[name] = value 42 | 43 | @classmethod 44 | def __init_subclass__(cls, **kwargs): 45 | super().__init_subclass__(**kwargs) 46 | cls.type_name = cls.__name__ 47 | 48 | def encode_value(self, value=None): 49 | """Returns the struct's encoded value. 50 | 51 | A struct's encoded value is a concatenation of the bytes32 representation of each member of the struct. 52 | Order is preserved. 53 | 54 | :param value: This parameter is not used for structs. 55 | """ 56 | encoded_values = list() 57 | for name, typ in self.get_members(): 58 | if isinstance(typ, type) and issubclass(typ, EIP712Struct): 59 | # Nested structs are recursively hashed, with the resulting 32-byte hash appended to the list of values 60 | sub_struct = self.get_data_value(name) 61 | encoded_values.append(sub_struct.hash_struct()) 62 | else: 63 | # Regular types are encoded as normal 64 | encoded_values.append(typ.encode_value(self.values[name])) 65 | return b''.join(encoded_values) 66 | 67 | def get_data_value(self, name): 68 | """Get the value of the given struct parameter. 69 | """ 70 | return self.values.get(name) 71 | 72 | def set_data_value(self, name, value): 73 | """Set the value of the given struct parameter. 74 | """ 75 | if name in self.values: 76 | self.values[name] = value 77 | 78 | def data_dict(self): 79 | """Provide the entire data dictionary representing the struct. 80 | 81 | Nested structs instances are also converted to dict form. 82 | """ 83 | result = dict() 84 | for k, v in self.values.items(): 85 | if isinstance(v, EIP712Struct): 86 | result[k] = v.data_dict() 87 | else: 88 | result[k] = v 89 | return result 90 | 91 | @classmethod 92 | def _encode_type(cls, resolve_references: bool) -> str: 93 | member_sigs = [f'{typ.type_name} {name}' for name, typ in cls.get_members()] 94 | struct_sig = f'{cls.type_name}({",".join(member_sigs)})' 95 | 96 | if resolve_references: 97 | reference_structs = set() 98 | cls._gather_reference_structs(reference_structs) 99 | sorted_structs = sorted(list(s for s in reference_structs if s != cls), key=lambda s: s.type_name) 100 | for struct in sorted_structs: 101 | struct_sig += struct._encode_type(resolve_references=False) 102 | return struct_sig 103 | 104 | @classmethod 105 | def _gather_reference_structs(cls, struct_set): 106 | """Finds reference structs defined in this struct type, and inserts them into the given set. 107 | """ 108 | structs = [m[1] for m in cls.get_members() if isinstance(m[1], type) and issubclass(m[1], EIP712Struct)] 109 | for struct in structs: 110 | if struct not in struct_set: 111 | struct_set.add(struct) 112 | struct._gather_reference_structs(struct_set) 113 | 114 | @classmethod 115 | def encode_type(cls): 116 | """Get the encoded type signature of the struct. 117 | 118 | Nested structs are also encoded, and appended in alphabetical order. 119 | """ 120 | return cls._encode_type(True) 121 | 122 | @classmethod 123 | def type_hash(cls) -> bytes: 124 | """Get the keccak hash of the struct's encoded type.""" 125 | return keccak(text=cls.encode_type()) 126 | 127 | def hash_struct(self) -> bytes: 128 | """The hash of the struct. 129 | 130 | hash_struct => keccak(type_hash || encode_data) 131 | """ 132 | return keccak(b''.join([self.type_hash(), self.encode_value()])) 133 | 134 | @classmethod 135 | def get_members(cls) -> List[Tuple[str, EIP712Type]]: 136 | """A list of tuples of supported parameters. 137 | 138 | Each tuple is (, ). The list's order is determined by definition order. 139 | """ 140 | members = [m for m in cls.__dict__.items() if isinstance(m[1], EIP712Type) 141 | or (isinstance(m[1], type) and issubclass(m[1], EIP712Struct))] 142 | return members 143 | 144 | @staticmethod 145 | def _assert_domain(domain): 146 | result = domain or eip712_structs.default_domain 147 | if not result: 148 | raise ValueError('Domain must be provided, or eip712_structs.default_domain must be set.') 149 | return result 150 | 151 | def to_message(self, domain: 'EIP712Struct' = None) -> dict: 152 | """Convert a struct into a dictionary suitable for messaging. 153 | 154 | Dictionary is of the form: 155 | { 156 | 'primaryType': Name of the primary type, 157 | 'types': Definition of each included struct type (including the domain type) 158 | 'domain': Values for the domain struct, 159 | 'message': Values for the message struct, 160 | } 161 | 162 | :returns: This struct + the domain in dict form, structured as specified for EIP712 messages. 163 | """ 164 | domain = self._assert_domain(domain) 165 | structs = {domain, self} 166 | self._gather_reference_structs(structs) 167 | 168 | # Build type dictionary 169 | types = dict() 170 | for struct in structs: 171 | members_json = [{ 172 | 'name': m[0], 173 | 'type': m[1].type_name, 174 | } for m in struct.get_members()] 175 | types[struct.type_name] = members_json 176 | 177 | result = { 178 | 'primaryType': self.type_name, 179 | 'types': types, 180 | 'domain': domain.data_dict(), 181 | 'message': self.data_dict(), 182 | } 183 | 184 | return result 185 | 186 | def to_message_json(self, domain: 'EIP712Struct' = None) -> str: 187 | message = self.to_message(domain) 188 | return json.dumps(message, cls=BytesJSONEncoder) 189 | 190 | def signable_bytes(self, domain: 'EIP712Struct' = None) -> bytes: 191 | """Return a ``bytes`` object suitable for signing, as specified for EIP712. 192 | 193 | As per the spec, bytes are constructed as follows: 194 | ``b'\x19\x01' + domain_hash_bytes + struct_hash_bytes`` 195 | 196 | :param domain: The domain to include in the hash bytes. If None, uses ``eip712_structs.default_domain`` 197 | :return: The bytes object 198 | """ 199 | domain = self._assert_domain(domain) 200 | result = b'\x19\x01' + domain.hash_struct() + self.hash_struct() 201 | return result 202 | 203 | @classmethod 204 | def from_message(cls, message_dict: dict) -> 'StructTuple': 205 | """Convert a message dictionary into two EIP712Struct objects - one for domain, another for the message struct. 206 | 207 | Returned as a StructTuple, which has the attributes ``message`` and ``domain``. 208 | 209 | Example: 210 | my_msg = { .. } 211 | deserialized = EIP712Struct.from_message(my_msg) 212 | msg_struct = deserialized.message 213 | domain_struct = deserialized.domain 214 | 215 | :param message_dict: The dictionary, such as what is produced by EIP712Struct.to_message. 216 | :return: A StructTuple object, containing the message and domain structs. 217 | """ 218 | structs = dict() 219 | unfulfilled_struct_params = defaultdict(list) 220 | 221 | for type_name in message_dict['types']: 222 | # Dynamically construct struct class from dict representation 223 | StructFromJSON = type(type_name, (EIP712Struct,), {}) 224 | 225 | for member in message_dict['types'][type_name]: 226 | # Either a basic solidity type is set, or None if referring to a reference struct (we'll fill it later) 227 | member_name = member['name'] 228 | member_sol_type = from_solidity_type(member['type']) 229 | setattr(StructFromJSON, member_name, member_sol_type) 230 | if member_sol_type is None: 231 | # Track the refs we'll need to set later. 232 | unfulfilled_struct_params[type_name].append((member_name, member['type'])) 233 | 234 | structs[type_name] = StructFromJSON 235 | 236 | # Now that custom structs have been parsed, pass through again to set the references 237 | for struct_name, unfulfilled_member_names in unfulfilled_struct_params.items(): 238 | regex_pattern = r'([a-zA-Z0-9_]+)(\[(\d+)?\])?' 239 | 240 | struct_class = structs[struct_name] 241 | for name, type_name in unfulfilled_member_names: 242 | match = re.match(regex_pattern, type_name) 243 | base_type_name = match.group(1) 244 | ref_struct = structs[base_type_name] 245 | if match.group(2): 246 | # The type is an array of the struct 247 | arr_len = match.group(3) or 0 # length of 0 means the array is dynamically sized 248 | setattr(struct_class, name, Array(ref_struct, arr_len)) 249 | else: 250 | setattr(struct_class, name, ref_struct) 251 | 252 | primary_struct = structs[message_dict['primaryType']] 253 | domain_struct = structs['EIP712Domain'] 254 | 255 | primary_result = primary_struct(**message_dict['message']) 256 | domain_result = domain_struct(**message_dict['domain']) 257 | result = StructTuple(message=primary_result, domain=domain_result) 258 | 259 | return result 260 | 261 | @classmethod 262 | def _assert_key_is_member(cls, key): 263 | member_names = {tup[0] for tup in cls.get_members()} 264 | if key not in member_names: 265 | raise KeyError(f'"{key}" is not defined for this struct.') 266 | 267 | @classmethod 268 | def _assert_property_type(cls, key, value): 269 | """Eagerly check for a correct member type""" 270 | members = dict(cls.get_members()) 271 | typ = members[key] 272 | 273 | if isinstance(typ, type) and issubclass(typ, EIP712Struct): 274 | # We expect an EIP712Struct instance. Assert that's true, and check the struct signature too. 275 | if not isinstance(value, EIP712Struct) or value._encode_type(False) != typ._encode_type(False): 276 | raise ValueError(f'Given value is of type {type(value)}, but we expected {typ}') 277 | else: 278 | # Since it isn't a nested struct, its an EIP712Type 279 | try: 280 | typ.encode_value(value) 281 | except Exception as e: 282 | raise ValueError(f'The python type {type(value)} does not appear ' 283 | f'to be supported for data type {typ}.') from e 284 | 285 | def __getitem__(self, key): 286 | """Provide access directly to the underlying value dictionary""" 287 | self._assert_key_is_member(key) 288 | return self.values.__getitem__(key) 289 | 290 | def __setitem__(self, key, value): 291 | """Provide access directly to the underlying value dictionary""" 292 | self._assert_key_is_member(key) 293 | self._assert_property_type(key, value) 294 | 295 | return self.values.__setitem__(key, value) 296 | 297 | def __delitem__(self, _): 298 | raise TypeError('Deleting entries from an EIP712Struct is not allowed.') 299 | 300 | def __eq__(self, other): 301 | if not other: 302 | # Null check 303 | return False 304 | if self is other: 305 | # Check identity 306 | return True 307 | if not isinstance(other, EIP712Struct): 308 | # Check class 309 | return False 310 | # Our structs are considered equal if their type signature and encoded value signature match. 311 | # E.g., like computing signable bytes but without a domain separator 312 | return self.encode_type() == other.encode_type() and self.encode_value() == other.encode_value() 313 | 314 | def __hash__(self): 315 | value_hashes = [hash(k) ^ hash(v) for k, v in self.values.items()] 316 | return functools.reduce(operator.xor, value_hashes, hash(self.type_name)) 317 | 318 | 319 | class StructTuple(NamedTuple): 320 | message: EIP712Struct 321 | domain: EIP712Struct 322 | -------------------------------------------------------------------------------- /eip712_structs/types.py: -------------------------------------------------------------------------------- 1 | import re 2 | from json import JSONEncoder 3 | from typing import Any, Union, Type 4 | 5 | from eth_utils.crypto import keccak 6 | from eth_utils.conversions import to_bytes, to_hex, to_int 7 | 8 | 9 | class EIP712Type: 10 | """The base type for members of a struct. 11 | 12 | Generally you wouldn't use this - instead, see the subclasses below. Or you may want an EIP712Struct instead. 13 | """ 14 | def __init__(self, type_name: str, none_val: Any): 15 | self.type_name = type_name 16 | self.none_val = none_val 17 | 18 | def encode_value(self, value) -> bytes: 19 | """Given a value, verify it and convert into the format required by the spec. 20 | 21 | :param value: A correct input value for the implemented type. 22 | :return: A 32-byte object containing encoded data 23 | """ 24 | if value is None: 25 | return self._encode_value(self.none_val) 26 | else: 27 | return self._encode_value(value) 28 | 29 | def _encode_value(self, value) -> bytes: 30 | """Must be implemented by subclasses, handles value encoding on a case-by-case basis. 31 | 32 | Don't call this directly - use ``.encode_value(value)`` instead. 33 | """ 34 | pass 35 | 36 | def __eq__(self, other): 37 | self_type = getattr(self, 'type_name') 38 | other_type = getattr(other, 'type_name') 39 | 40 | return self_type is not None and self_type == other_type 41 | 42 | def __hash__(self): 43 | return hash(self.type_name) 44 | 45 | 46 | class Array(EIP712Type): 47 | def __init__(self, member_type: Union[EIP712Type, Type[EIP712Type]], fixed_length: int = 0): 48 | """Represents an array member type. 49 | 50 | Example: 51 | a1 = Array(String()) # string[] a1 52 | a2 = Array(String(), 8) # string[8] a2 53 | a3 = Array(MyStruct) # MyStruct[] a3 54 | """ 55 | fixed_length = int(fixed_length) 56 | if fixed_length == 0: 57 | type_name = f'{member_type.type_name}[]' 58 | else: 59 | type_name = f'{member_type.type_name}[{fixed_length}]' 60 | self.member_type = member_type 61 | self.fixed_length = fixed_length 62 | super(Array, self).__init__(type_name, []) 63 | 64 | def _encode_value(self, value): 65 | """Arrays are encoded by concatenating their encoded contents, and taking the keccak256 hash.""" 66 | encoder = self.member_type 67 | encoded_values = [encoder.encode_value(v) for v in value] 68 | return keccak(b''.join(encoded_values)) 69 | 70 | 71 | class Address(EIP712Type): 72 | def __init__(self): 73 | """Represents an ``address`` type.""" 74 | super(Address, self).__init__('address', 0) 75 | 76 | def _encode_value(self, value): 77 | """Addresses are encoded like Uint160 numbers.""" 78 | 79 | # Some smart conversions - need to get the address to a numeric before we encode it 80 | if isinstance(value, bytes): 81 | v = to_int(value) 82 | elif isinstance(value, str): 83 | v = to_int(hexstr=value) 84 | else: 85 | v = value # Fallback, just use it as-is. 86 | return Uint(160).encode_value(v) 87 | 88 | 89 | class Boolean(EIP712Type): 90 | def __init__(self): 91 | """Represents a ``bool`` type.""" 92 | super(Boolean, self).__init__('bool', False) 93 | 94 | def _encode_value(self, value): 95 | """Booleans are encoded like the uint256 values of 0 and 1.""" 96 | if value is False: 97 | return Uint(256).encode_value(0) 98 | elif value is True: 99 | return Uint(256).encode_value(1) 100 | else: 101 | raise ValueError(f'Must be True or False. Got: {value}') 102 | 103 | 104 | class Bytes(EIP712Type): 105 | def __init__(self, length: int = 0): 106 | """Represents a solidity bytes type. 107 | 108 | Length may be used to specify a static ``bytesN`` type. Or 0 for a dynamic ``bytes`` type. 109 | Example: 110 | b1 = Bytes() # bytes b1 111 | b2 = Bytes(10) # bytes10 b2 112 | 113 | ``length`` MUST be between 0 and 32, or a ValueError is raised. 114 | """ 115 | length = int(length) 116 | if length == 0: 117 | # Special case: Length of 0 means a dynamic bytes type 118 | type_name = 'bytes' 119 | elif 1 <= length <= 32: 120 | type_name = f'bytes{length}' 121 | else: 122 | raise ValueError(f'Byte length must be between 1 or 32. Got: {length}') 123 | self.length = length 124 | super(Bytes, self).__init__(type_name, b'') 125 | 126 | def _encode_value(self, value): 127 | """Static bytesN types are encoded by right-padding to 32 bytes. Dynamic bytes types are keccak256 hashed.""" 128 | if isinstance(value, str): 129 | # Try converting to a bytestring, assuming that it's been given as hex 130 | value = to_bytes(hexstr=value) 131 | 132 | if self.length == 0: 133 | return keccak(value) 134 | else: 135 | if len(value) > self.length: 136 | raise ValueError(f'{self.type_name} was given bytes with length {len(value)}') 137 | padding = bytes(32 - len(value)) 138 | return value + padding 139 | 140 | 141 | class Int(EIP712Type): 142 | def __init__(self, length: int = 256): 143 | """Represents a signed int type. Length may be given to specify the int length in bits. Default length is 256 144 | 145 | Example: 146 | i1 = Int(256) # int256 i1 147 | i2 = Int() # int256 i2 148 | i3 = Int(128) # int128 i3 149 | """ 150 | length = int(length) 151 | if length < 8 or length > 256 or length % 8 != 0: 152 | raise ValueError(f'Int length must be a multiple of 8, between 8 and 256. Got: {length}') 153 | self.length = length 154 | super(Int, self).__init__(f'int{length}', 0) 155 | 156 | def _encode_value(self, value: int): 157 | """Ints are encoded by padding them to 256-bit representations.""" 158 | value.to_bytes(self.length // 8, byteorder='big', signed=True) # For validation 159 | return value.to_bytes(32, byteorder='big', signed=True) 160 | 161 | 162 | class String(EIP712Type): 163 | def __init__(self): 164 | """Represents a string type.""" 165 | super(String, self).__init__('string', '') 166 | 167 | def _encode_value(self, value): 168 | """Strings are encoded by taking the keccak256 hash of their contents.""" 169 | return keccak(text=value) 170 | 171 | 172 | class Uint(EIP712Type): 173 | def __init__(self, length: int = 256): 174 | """Represents an unsigned int type. Length may be given to specify the int length in bits. Default length is 256 175 | 176 | Example: 177 | ui1 = Uint(256) # uint256 ui1 178 | ui2 = Uint() # uint256 ui2 179 | ui3 = Uint(128) # uint128 ui3 180 | """ 181 | length = int(length) 182 | if length < 8 or length > 256 or length % 8 != 0: 183 | raise ValueError(f'Uint length must be a multiple of 8, between 8 and 256. Got: {length}') 184 | self.length = length 185 | super(Uint, self).__init__(f'uint{length}', 0) 186 | 187 | def _encode_value(self, value: int): 188 | """Uints are encoded by padding them to 256-bit representations.""" 189 | value.to_bytes(self.length // 8, byteorder='big', signed=False) # For validation 190 | return value.to_bytes(32, byteorder='big', signed=False) 191 | 192 | 193 | # This helper dict maps solidity's type names to our EIP712Type classes 194 | solidity_type_map = { 195 | 'address': Address, 196 | 'bool': Boolean, 197 | 'bytes': Bytes, 198 | 'int': Int, 199 | 'string': String, 200 | 'uint': Uint, 201 | } 202 | 203 | 204 | def from_solidity_type(solidity_type: str): 205 | """Convert a string into the EIP712Type implementation. Basic types only.""" 206 | pattern = r'([a-z]+)(\d+)?(\[(\d+)?\])?' 207 | match = re.match(pattern, solidity_type) 208 | 209 | if match is None: 210 | return None 211 | 212 | type_name = match.group(1) # The type name, like the "bytes" in "bytes32" 213 | opt_len = match.group(2) # An optional length spec, like the "32" in "bytes32" 214 | is_array = match.group(3) # Basically just checks for square brackets 215 | array_len = match.group(4) # For fixed length arrays only, this is the length 216 | 217 | if type_name not in solidity_type_map: 218 | # Only supporting basic types here - return None if we don't recognize it. 219 | return None 220 | 221 | # Construct the basic type 222 | base_type = solidity_type_map[type_name] 223 | if opt_len: 224 | type_instance = base_type(int(opt_len)) 225 | else: 226 | type_instance = base_type() 227 | 228 | if is_array: 229 | # Nest the aforementioned basic type into an Array. 230 | if array_len: 231 | result = Array(type_instance, int(array_len)) 232 | else: 233 | result = Array(type_instance) 234 | return result 235 | else: 236 | return type_instance 237 | 238 | 239 | class BytesJSONEncoder(JSONEncoder): 240 | def default(self, o): 241 | if isinstance(o, bytes): 242 | return to_hex(o) 243 | else: 244 | return super(BytesJSONEncoder, self).default(o) 245 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | atomicwrites==1.3.0 2 | attrdict==2.0.1 3 | attrs==19.1.0 4 | certifi==2019.3.9 5 | chardet==3.0.4 6 | coverage==4.5.3 7 | coveralls==1.8.0 8 | cytoolz==0.9.0.1 9 | docopt==0.6.2 10 | eth-abi==1.3.0 11 | eth-account==0.3.0 12 | eth-hash==0.2.0 13 | eth-keyfile==0.5.1 14 | eth-keys==0.2.3 15 | eth-rlp==0.1.2 16 | eth-typing==2.1.0 17 | eth-utils==1.6.0 18 | hexbytes==0.2.0 19 | idna==2.8 20 | importlib-metadata==0.17 21 | lru-dict==1.1.6 22 | more-itertools==7.0.0 23 | packaging==19.0 24 | parsimonious==0.8.1 25 | pluggy==0.12.0 26 | py==1.8.0 27 | pycryptodome==3.8.2 28 | pyparsing==2.4.0 29 | pysha3==1.0.2 30 | pytest==4.6.2 31 | pytest-cov==2.7.1 32 | requests==2.22.0 33 | rlp==1.1.0 34 | six==1.12.0 35 | toolz==0.9.0 36 | urllib3==1.25.3 37 | wcwidth==0.1.7 38 | web3==4.9.2 39 | websockets==6.0 40 | zipp==0.5.1 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | import sys 3 | from pathlib import Path 4 | 5 | from setuptools import setup, find_packages 6 | from setuptools.command.test import test as TestCommand 7 | 8 | 9 | NAME = 'eip712-structs' 10 | VERSION = '1.1.0' 11 | 12 | install_requirements = [ 13 | 'eth-utils>=1.4.0', 14 | 'pysha3>=1.0.2', 15 | ] 16 | 17 | test_requirements = [ 18 | 'coveralls==1.8.0', 19 | 'pytest==4.6.2', 20 | 'pytest-cov==2.7.1', 21 | 'web3==4.9.2', 22 | ] 23 | 24 | 25 | def get_file_text(filename): 26 | file_path = Path(__file__).parent / filename 27 | if not file_path.exists(): 28 | return '' 29 | else: 30 | file_text = file_path.read_text().strip() 31 | return file_text 32 | 33 | 34 | long_description = get_file_text('README.md') 35 | 36 | 37 | class PyTest(TestCommand): 38 | user_options = [("pytest-args=", "a", "Arguments to pass to pytest")] 39 | 40 | def initialize_options(self): 41 | TestCommand.initialize_options(self) 42 | self.pytest_args = "" 43 | 44 | def run_tests(self): 45 | # import here, cause outside the eggs aren't loaded 46 | import pytest 47 | 48 | errno = pytest.main(shlex.split(self.pytest_args)) 49 | sys.exit(errno) 50 | 51 | 52 | class CoverallsCommand(TestCommand): 53 | description = 'Run the coveralls command' 54 | user_options = [("coveralls-args=", "a", "Arguments to pass to coveralls")] 55 | 56 | def initialize_options(self): 57 | TestCommand.initialize_options(self) 58 | self.coveralls_args = "" 59 | 60 | def run_tests(self): 61 | import coveralls.cli 62 | errno = coveralls.cli.main(shlex.split(self.coveralls_args)) 63 | sys.exit(errno) 64 | 65 | 66 | setup( 67 | name=NAME, 68 | version=VERSION, 69 | author='AJ Grubbs', 70 | packages=find_packages(), 71 | install_requires=install_requirements, 72 | tests_require=test_requirements, 73 | cmdclass={ 74 | "test": PyTest, 75 | "coveralls": CoverallsCommand, 76 | }, 77 | description='A python library for EIP712 objects', 78 | long_description=long_description, 79 | long_description_content_type='text/markdown', 80 | license='MIT', 81 | keywords='ethereum eip712 solidity', 82 | url='https://github.com/ConsenSys/py-eip712-structs', 83 | ) 84 | -------------------------------------------------------------------------------- /tests/contracts/hash_test_contract.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0 <0.6.0; 2 | pragma experimental ABIEncoderV2; 3 | 4 | 5 | contract TestContract { 6 | /******************** 7 | * Constant Members * 8 | ********************/ 9 | struct Bar { 10 | uint256 bar_uint; 11 | } 12 | 13 | struct Foo { 14 | string s; 15 | uint256 u_i; 16 | int8 s_i; 17 | address a; 18 | bool b; 19 | bytes30 bytes_30; 20 | bytes dyn_bytes; 21 | Bar bar; 22 | bytes1[] arr; 23 | } 24 | 25 | string constant public BarSig = "Bar(uint256 bar_uint)"; 26 | string constant public FooSig = "Foo(string s,uint256 u_i,int8 s_i,address a,bool b,bytes30 bytes_30,bytes dyn_bytes,Bar bar,bytes1[] arr)Bar(uint256 bar_uint)"; 27 | 28 | bytes32 constant public Bar_TYPEHASH = keccak256( 29 | abi.encodePacked("Bar(uint256 bar_uint)") 30 | ); 31 | bytes32 constant public Foo_TYPEHASH = keccak256( 32 | abi.encodePacked("Foo(string s,uint256 u_i,int8 s_i,address a,bool b,bytes30 bytes_30,bytes dyn_bytes,Bar bar,bytes1[] arr)Bar(uint256 bar_uint)") 33 | ); 34 | 35 | /******************/ 36 | /* Hash Functions */ 37 | /******************/ 38 | function encodeBytes1Array(bytes1[] memory arr) public pure returns (bytes32) { 39 | uint256 len = arr.length; 40 | bytes32[] memory padded = new bytes32[](len); 41 | for (uint256 i = 0; i < len; i++) { 42 | padded[i] = bytes32(arr[i]); 43 | } 44 | return keccak256( 45 | abi.encodePacked(padded) 46 | ); 47 | } 48 | 49 | function hashBarStruct(Bar memory bar) public pure returns (bytes32) { 50 | return keccak256(abi.encode( 51 | Bar_TYPEHASH, 52 | bar.bar_uint 53 | )); 54 | } 55 | 56 | function hashFooStruct(Foo memory foo) public pure returns (bytes32) { 57 | return keccak256(abi.encode( 58 | Foo_TYPEHASH, 59 | keccak256(abi.encodePacked(foo.s)), 60 | foo.u_i, 61 | foo.s_i, 62 | foo.a, 63 | foo.b, 64 | foo.bytes_30, 65 | keccak256(abi.encodePacked(foo.dyn_bytes)), 66 | hashBarStruct(foo.bar), 67 | encodeBytes1Array(foo.arr) 68 | )); 69 | } 70 | 71 | function hashBarStructFromParams( 72 | uint256 bar_uint 73 | ) public pure returns (bytes32) { 74 | Bar memory bar; 75 | bar.bar_uint = bar_uint; 76 | return hashBarStruct(bar); 77 | } 78 | 79 | function hashFooStructFromParams( 80 | string memory s, 81 | uint256 u_i, 82 | int8 s_i, 83 | address a, 84 | bool b, 85 | bytes30 bytes_30, 86 | bytes memory dyn_bytes, 87 | uint256 bar_uint, 88 | bytes1[] memory arr 89 | ) public pure returns (bytes32) { 90 | // Construct Foo struct with basic types 91 | Foo memory foo; 92 | foo.s = s; 93 | foo.u_i = u_i; 94 | foo.s_i = s_i; 95 | foo.a = a; 96 | foo.b = b; 97 | foo.bytes_30 = bytes_30; 98 | foo.dyn_bytes = dyn_bytes; 99 | foo.arr = arr; 100 | 101 | // Construct Bar struct and add it to Foo 102 | Bar memory bar; 103 | bar.bar_uint = bar_uint; 104 | foo.bar = bar; 105 | 106 | return hashFooStruct(foo); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/test_chain_parity.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from requests.exceptions import ConnectionError 5 | from web3 import HTTPProvider, Web3 6 | 7 | from eip712_structs import EIP712Struct, String, Uint, Int, Address, Boolean, Bytes, Array 8 | 9 | 10 | @pytest.fixture(scope='module') 11 | def w3(): 12 | """Provide a Web3 client to interact with a local chain.""" 13 | client = Web3(HTTPProvider('http://localhost:8545')) 14 | client.eth.defaultAccount = client.eth.accounts[0] 15 | return client 16 | 17 | 18 | @pytest.fixture(scope='module') 19 | def contract(w3): 20 | """Deploys the test contract to the local chain, and returns a Web3.py Contract to interact with it. 21 | 22 | Note this expects the contract to be compiled already. 23 | This project's docker-compose config pulls a solc container to do this for you. 24 | """ 25 | base_path = 'tests/contracts/build/TestContract' 26 | with open(f'{base_path}.abi', 'r') as f: 27 | abi = f.read() 28 | with open(f'{base_path}.bin', 'r') as f: 29 | bytecode = f.read() 30 | 31 | tmp_contract = w3.eth.contract(abi=abi, bytecode=bytecode) 32 | deploy_hash = tmp_contract.constructor().transact() 33 | deploy_receipt = w3.eth.waitForTransactionReceipt(deploy_hash) 34 | 35 | deployed_contract = w3.eth.contract(abi=abi, address=deploy_receipt.contractAddress) 36 | return deployed_contract 37 | 38 | 39 | def skip_this_module(): 40 | """If we can't reach a local chain, then all tests in this module are skipped.""" 41 | client = Web3(HTTPProvider('http://localhost:8545')) 42 | try: 43 | client.eth.accounts 44 | except ConnectionError: 45 | return True 46 | return False 47 | 48 | 49 | # Implicitly adds this ``skipif`` mark to the tests below. 50 | pytestmark = pytest.mark.skipif(skip_this_module(), reason='No accessible test chain.') 51 | 52 | 53 | # These structs must match the struct in tests/contracts/hash_test_contract.sol 54 | class Bar(EIP712Struct): 55 | bar_uint = Uint(256) 56 | 57 | 58 | # TODO Add Array type (w/ appropriate test updates) to this struct. 59 | class Foo(EIP712Struct): 60 | s = String() 61 | u_i = Uint(256) 62 | s_i = Int(8) 63 | a = Address() 64 | b = Boolean() 65 | bytes_30 = Bytes(30) 66 | dyn_bytes = Bytes() 67 | bar = Bar 68 | arr = Array(Bytes(1)) 69 | 70 | 71 | def get_chain_hash(contract, s, u_i, s_i, a, b, bytes_30, dyn_bytes, bar_uint, arr) -> bytes: 72 | """Uses the contract to create and hash a Foo struct with the given parameters.""" 73 | result = contract.functions.hashFooStructFromParams(s, u_i, s_i, a, b, bytes_30, dyn_bytes, bar_uint, arr).call() 74 | return result 75 | 76 | 77 | def test_encoded_types(contract): 78 | """Checks that the encoded types (and the respective hashes) of our structs match.""" 79 | local_bar_sig = Bar.encode_type() 80 | remote_bar_sig = contract.functions.BarSig().call() 81 | assert local_bar_sig == remote_bar_sig 82 | 83 | local_foo_sig = Foo.encode_type() 84 | remote_foo_sig = contract.functions.FooSig().call() 85 | assert local_foo_sig == remote_foo_sig 86 | 87 | local_bar_hash = Bar.type_hash() 88 | remote_bar_hash = contract.functions.Bar_TYPEHASH().call() 89 | assert local_bar_hash == remote_bar_hash 90 | 91 | local_foo_hash = Foo.type_hash() 92 | remote_foo_hash = contract.functions.Foo_TYPEHASH().call() 93 | assert local_foo_hash == remote_foo_hash 94 | 95 | array_type = Array(Bytes(1)) 96 | bytes_array = [os.urandom(1) for _ in range(5)] 97 | local_encoded_array = array_type.encode_value(bytes_array) 98 | remote_encoded_array = contract.functions.encodeBytes1Array(bytes_array).call() 99 | assert local_encoded_array == remote_encoded_array 100 | 101 | 102 | def test_chain_hash_matches(contract): 103 | """Assert that the hashes we derive locally match the hashes derived on-chain.""" 104 | 105 | # Initialize basic values 106 | s = 'some string' 107 | u_i = 1234 108 | s_i = -7 109 | a = Web3.toChecksumAddress(f'0x{os.urandom(20).hex()}') 110 | b = True 111 | bytes_30 = os.urandom(30) 112 | dyn_bytes = os.urandom(50) 113 | arr = [os.urandom(1) for _ in range(5)] 114 | 115 | # Initialize a Bar struct, and check it standalone 116 | bar_uint = 1337 117 | bar_struct = Bar(bar_uint=bar_uint) 118 | local_bar_hash = bar_struct.hash_struct() 119 | remote_bar_hash = contract.functions.hashBarStructFromParams(bar_uint).call() 120 | assert local_bar_hash == remote_bar_hash 121 | 122 | # Initialize a Foo struct (including the Bar struct above) and check the hashes 123 | foo_struct = Foo(s=s, u_i=u_i, s_i=s_i, a=a, b=b, bytes_30=bytes_30, dyn_bytes=dyn_bytes, bar=bar_struct, arr=arr) 124 | local_foo_hash = foo_struct.hash_struct() 125 | remote_foo_hash = get_chain_hash(contract, s, u_i, s_i, a, b, bytes_30, dyn_bytes, bar_uint, arr) 126 | assert local_foo_hash == remote_foo_hash 127 | -------------------------------------------------------------------------------- /tests/test_domain_separator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from eth_utils.crypto import keccak 5 | 6 | import eip712_structs 7 | from eip712_structs import make_domain, EIP712Struct, String 8 | 9 | 10 | @pytest.fixture 11 | def default_domain_manager(): 12 | # This fixture can be called to ensure we cleanup our default domain var before/after tests 13 | current_value = eip712_structs.default_domain 14 | eip712_structs.default_domain = None 15 | yield 16 | eip712_structs.default_domain = current_value 17 | 18 | 19 | def test_domain_sep_create(): 20 | salt = os.urandom(32) 21 | domain_struct = make_domain(name='name', salt=salt) 22 | 23 | expected_result = 'EIP712Domain(string name,bytes32 salt)' 24 | assert domain_struct.encode_type() == expected_result 25 | 26 | expected_data = b''.join([keccak(text='name'), salt]) 27 | assert domain_struct.encode_value() == expected_data 28 | 29 | with pytest.raises(ValueError, match='At least one argument must be given'): 30 | make_domain() 31 | 32 | 33 | def test_domain_sep_types(): 34 | salt = os.urandom(32) 35 | contract = os.urandom(20) 36 | 37 | domain_struct = make_domain(name='name', version='version', chainId=1, 38 | verifyingContract=contract, salt=salt) 39 | 40 | encoded_data = [keccak(text='name'), keccak(text='version'), int(1).to_bytes(32, 'big', signed=False), 41 | bytes(12) + contract, salt] 42 | 43 | expected_result = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)' 44 | assert domain_struct.encode_type() == expected_result 45 | 46 | expected_data = b''.join(encoded_data) 47 | assert domain_struct.encode_value() == expected_data 48 | 49 | 50 | def test_default_domain(default_domain_manager): 51 | assert eip712_structs.default_domain is None 52 | 53 | class Foo(EIP712Struct): 54 | s = String() 55 | foo = Foo(s='hello world') 56 | 57 | domain = make_domain(name='domain') 58 | other_domain = make_domain(name='other domain') 59 | 60 | # When neither methods provide a domain, expect a ValueError 61 | with pytest.raises(ValueError, match='Domain must be provided'): 62 | foo.to_message() 63 | with pytest.raises(ValueError, match='Domain must be provided'): 64 | foo.signable_bytes() 65 | 66 | # But we can still provide a domain explicitly 67 | explicit_msg = foo.to_message(domain) 68 | explicit_bytes = foo.signable_bytes(domain) 69 | 70 | # Setting it lets us forgo providing it 71 | eip712_structs.default_domain = domain 72 | implicit_msg = foo.to_message() 73 | implicit_bytes = foo.signable_bytes() 74 | 75 | # Either method should produce the same result 76 | assert implicit_msg == explicit_msg 77 | assert implicit_bytes == explicit_bytes 78 | 79 | # Using a different domain should not use any current default domain 80 | assert implicit_msg != foo.to_message(other_domain) 81 | assert implicit_bytes != foo.signable_bytes(other_domain) 82 | -------------------------------------------------------------------------------- /tests/test_encode_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import string 4 | 5 | from eth_utils.crypto import keccak 6 | import pytest 7 | 8 | from eip712_structs import Address, Array, Boolean, Bytes, Int, String, Uint, EIP712Struct, make_domain 9 | 10 | 11 | def signed_min_max(bits): 12 | min_val = (pow(2, bits) // 2) * -1 13 | max_val = (pow(2, bits) // 2) - 1 14 | return min_val, max_val 15 | 16 | 17 | def unsigned_max(bits): 18 | return pow(2, bits) - 1 19 | 20 | 21 | def test_encode_basic_types(): 22 | class TestStruct(EIP712Struct): 23 | address = Address() 24 | boolean = Boolean() 25 | dyn_bytes = Bytes() 26 | bytes_1 = Bytes(1) 27 | bytes_32 = Bytes(32) 28 | int_32 = Int(32) 29 | int_256 = Int(256) 30 | string = String() 31 | uint_32 = Uint(32) 32 | uint_256 = Uint(256) 33 | 34 | values = dict() 35 | values['address'] = os.urandom(20) 36 | values['boolean'] = False 37 | values['dyn_bytes'] = os.urandom(random.choice(range(33, 100))) 38 | values['bytes_1'] = os.urandom(1) 39 | values['bytes_32'] = os.urandom(32) 40 | values['int_32'] = random.randint(*signed_min_max(32)) 41 | values['int_256'] = random.randint(*signed_min_max(256)) 42 | values['string'] = ''.join([random.choice(string.ascii_letters) for _ in range(100)]) 43 | values['uint_32'] = random.randint(0, unsigned_max(32)) 44 | values['uint_256'] = random.randint(0, unsigned_max(256)) 45 | 46 | expected_data = list() 47 | expected_data.append(bytes(12) + values['address']) 48 | expected_data.append(bytes(32)) 49 | expected_data.append(keccak(values['dyn_bytes'])) 50 | expected_data.append(values['bytes_1'] + bytes(31)) 51 | expected_data.append(values['bytes_32']) 52 | expected_data.append(values['int_32'].to_bytes(32, byteorder='big', signed=True)) 53 | expected_data.append(values['int_256'].to_bytes(32, byteorder='big', signed=True)) 54 | expected_data.append(keccak(text=values['string'])) 55 | expected_data.append(values['uint_32'].to_bytes(32, byteorder='big', signed=False)) 56 | expected_data.append(values['uint_256'].to_bytes(32, byteorder='big', signed=False)) 57 | 58 | s = TestStruct(**values) 59 | encoded_data = s.encode_value() 60 | encoded_bytes = list() 61 | 62 | # Compare each byte range itself to find offenders 63 | for i in range(0, len(encoded_data), 32): 64 | encoded_bytes.append(encoded_data[i:i + 32]) 65 | 66 | assert encoded_bytes == expected_data 67 | 68 | 69 | def test_encode_array(): 70 | class TestStruct(EIP712Struct): 71 | byte_array = Array(Bytes(32), 4) 72 | 73 | byte_array = [os.urandom(32) for _ in range(4)] 74 | 75 | s = TestStruct(byte_array=byte_array) 76 | assert s.encode_value() == keccak(b''.join(byte_array)) 77 | 78 | 79 | def test_encode_nested_structs(): 80 | class SubStruct(EIP712Struct): 81 | s = String() 82 | 83 | class MainStruct(EIP712Struct): 84 | sub_1 = SubStruct 85 | sub_2 = String() 86 | sub_3 = SubStruct 87 | 88 | s1 = 'foo' 89 | s2 = 'bar' 90 | s3 = 'baz' 91 | 92 | sub_1 = SubStruct(s=s1) 93 | sub_3 = SubStruct(s=s3) 94 | 95 | s = MainStruct( 96 | sub_1=sub_1, 97 | sub_2=s2, 98 | sub_3=sub_3, 99 | ) 100 | 101 | expected_encoded_vals = b''.join([sub_1.hash_struct(), keccak(text=s2), sub_3.hash_struct()]) 102 | assert s.encode_value() == expected_encoded_vals 103 | 104 | 105 | def test_data_dicts(): 106 | class Foo(EIP712Struct): 107 | s = String() 108 | i = Int(256) 109 | 110 | class Bar(EIP712Struct): 111 | foo = Foo 112 | b = Bytes(1) 113 | 114 | bar = Bar( 115 | foo=Foo( 116 | s='hello', 117 | i=100, 118 | ), 119 | b=b'\xff' 120 | ) 121 | 122 | expected_result = { 123 | 'foo': { 124 | 's': 'hello', 125 | 'i': 100, 126 | }, 127 | 'b': b'\xff' 128 | } 129 | assert bar.data_dict() == expected_result 130 | 131 | 132 | def test_signable_bytes(): 133 | class Foo(EIP712Struct): 134 | s = String() 135 | i = Int(256) 136 | 137 | domain = make_domain(name='hello') 138 | foo = Foo(s='hello', i=1234) 139 | 140 | start_bytes = b'\x19\x01' 141 | exp_domain_bytes = keccak(domain.type_hash() + domain.encode_value()) 142 | exp_struct_bytes = keccak(foo.type_hash() + foo.encode_value()) 143 | 144 | sign_bytes = foo.signable_bytes(domain) 145 | assert sign_bytes[0:2] == start_bytes 146 | assert sign_bytes[2:34] == exp_domain_bytes 147 | assert sign_bytes[34:] == exp_struct_bytes 148 | 149 | 150 | def test_none_replacement(): 151 | class Foo(EIP712Struct): 152 | s = String() 153 | i = Int(256) 154 | 155 | foo = Foo(**{}) 156 | encoded_val = foo.encode_value() 157 | assert len(encoded_val) == 64 158 | 159 | empty_string_hash = keccak(text='') 160 | assert encoded_val[0:32] == empty_string_hash 161 | assert encoded_val[32:] == bytes(32) 162 | 163 | 164 | def test_validation_errors(): 165 | bytes_type = Bytes(10) 166 | int_type = Int(8) # -128 <= i < 128 167 | uint_type = Uint(8) # 0 <= i < 256 168 | bool_type = Boolean() 169 | 170 | with pytest.raises(ValueError, match='bytes10 was given bytes with length 11'): 171 | bytes_type.encode_value(os.urandom(11)) 172 | 173 | with pytest.raises(OverflowError, match='too big'): 174 | int_type.encode_value(128) 175 | with pytest.raises(OverflowError, match='too big'): 176 | int_type.encode_value(-129) 177 | 178 | with pytest.raises(OverflowError, match='too big'): 179 | uint_type.encode_value(256) 180 | assert uint_type.encode_value(0) == bytes(32) 181 | with pytest.raises(OverflowError, match='negative int to unsigned'): 182 | uint_type.encode_value(-1) 183 | 184 | assert bool_type.encode_value(True) == bytes(31) + b'\x01' 185 | assert bool_type.encode_value(False) == bytes(32) 186 | with pytest.raises(ValueError, match='Must be True or False.'): 187 | bool_type.encode_value(0) 188 | with pytest.raises(ValueError, match='Must be True or False.'): 189 | bool_type.encode_value(1) 190 | 191 | 192 | def test_struct_eq(): 193 | class Foo(EIP712Struct): 194 | s = String() 195 | foo = Foo(s='hello world') 196 | foo_copy = Foo(s='hello world') 197 | foo_2 = Foo(s='blah') 198 | 199 | assert foo != None 200 | assert foo != 'unrelated type' 201 | assert foo == foo 202 | assert foo is not foo_copy 203 | assert foo == foo_copy 204 | assert foo != foo_2 205 | 206 | def make_different_foo(): 207 | # We want another struct defined with the same name but different member types 208 | class Foo(EIP712Struct): 209 | b = Bytes() 210 | return Foo 211 | 212 | def make_same_foo(): 213 | # For good measure, recreate the exact same class and ensure they can still compare 214 | class Foo(EIP712Struct): 215 | s = String() 216 | return Foo 217 | 218 | OtherFooClass = make_different_foo() 219 | wrong_type = OtherFooClass(b=b'hello world') 220 | assert wrong_type != foo 221 | assert OtherFooClass != Foo 222 | 223 | SameFooClass = make_same_foo() 224 | right_type = SameFooClass(s='hello world') 225 | assert right_type == foo 226 | assert SameFooClass != Foo 227 | 228 | # Different name, same members 229 | class Bar(EIP712Struct): 230 | s = String() 231 | bar = Bar(s='hello world') 232 | assert bar != foo 233 | 234 | 235 | def test_value_access(): 236 | class Foo(EIP712Struct): 237 | s = String() 238 | b = Bytes(32) 239 | 240 | test_str = 'hello world' 241 | test_bytes = os.urandom(32) 242 | foo = Foo(s=test_str, b=test_bytes) 243 | 244 | assert foo['s'] == test_str 245 | assert foo['b'] == test_bytes 246 | 247 | test_bytes_2 = os.urandom(32) 248 | foo['b'] = test_bytes_2 249 | 250 | assert foo['b'] == test_bytes_2 251 | 252 | with pytest.raises(KeyError): 253 | foo['x'] = 'unacceptable' 254 | 255 | # Check behavior when accessing a member that wasn't defined for the struct. 256 | with pytest.raises(KeyError): 257 | foo['x'] 258 | # Lets cheat a lil bit for robustness- add an invalid 'x' member to the value dict, and check the error still raises 259 | foo.values['x'] = 'test' 260 | with pytest.raises(KeyError): 261 | foo['x'] 262 | foo.values.pop('x') 263 | 264 | with pytest.raises(ValueError): 265 | foo['s'] = b'unacceptable' 266 | with pytest.raises(ValueError): 267 | # Bytes do accept strings, but it has to be hex formatted. 268 | foo['b'] = 'unacceptable' 269 | 270 | # Test behavior when attempting to set nested structs as values 271 | class Bar(EIP712Struct): 272 | s = String() 273 | f = Foo 274 | 275 | class Baz(EIP712Struct): 276 | s = String() 277 | baz = Baz(s=test_str) 278 | 279 | bar = Bar(s=test_str) 280 | bar['f'] = foo 281 | assert bar['f'] == foo 282 | 283 | with pytest.raises(ValueError): 284 | # Expects a Foo type, so should throw an error 285 | bar['f'] = baz 286 | 287 | with pytest.raises(TypeError): 288 | del foo['s'] 289 | -------------------------------------------------------------------------------- /tests/test_encode_type.py: -------------------------------------------------------------------------------- 1 | from eip712_structs import Address, Array, EIP712Struct, Int, String, Uint 2 | 3 | 4 | def test_empty_struct(): 5 | class Empty(EIP712Struct): 6 | pass 7 | 8 | assert Empty.encode_type() == 'Empty()' 9 | 10 | 11 | def test_simple_struct(): 12 | class Person(EIP712Struct): 13 | name = String() 14 | addr = Address() 15 | numbers = Array(Int(256)) 16 | moreNumbers = Array(Uint(256), 8) 17 | 18 | expected_result = 'Person(string name,address addr,int256[] numbers,uint256[8] moreNumbers)' 19 | assert Person.encode_type() == expected_result 20 | 21 | 22 | def test_struct_with_reference(): 23 | class Person(EIP712Struct): 24 | name = String() 25 | addr = Address() 26 | 27 | class Mail(EIP712Struct): 28 | source = Person 29 | dest = Person 30 | content = String() 31 | 32 | expected_result = 'Mail(Person source,Person dest,string content)Person(string name,address addr)' 33 | assert Mail.encode_type() == expected_result 34 | 35 | 36 | def test_self_reference(): 37 | class Person(EIP712Struct): 38 | name = String() 39 | 40 | Person.parent = Person 41 | 42 | expected_result = 'Person(string name,Person parent)' 43 | assert Person.encode_type() == expected_result 44 | 45 | 46 | def test_nested_reference(): 47 | class C(EIP712Struct): 48 | s = String() 49 | 50 | class B(EIP712Struct): 51 | s = String() 52 | c = C 53 | 54 | class A(EIP712Struct): 55 | s = String() 56 | b = B 57 | 58 | expected_result = 'A(string s,B b)B(string s,C c)C(string s)' 59 | assert A.encode_type() == expected_result 60 | 61 | 62 | def test_reference_ordering(): 63 | # The "main" struct is always first. Then the rest are ordered alphabetically. 64 | class B(EIP712Struct): 65 | s = String() 66 | 67 | class C(EIP712Struct): 68 | s = String() 69 | b = B 70 | 71 | class A(EIP712Struct): 72 | s = String() 73 | c = C 74 | 75 | expected_result = 'A(string s,C c)B(string s)C(string s,B b)' 76 | assert A.encode_type() == expected_result 77 | 78 | class Z(EIP712Struct): 79 | s = String() 80 | a = A 81 | 82 | expected_result = 'Z(string s,A a)' + expected_result 83 | assert Z.encode_type() == expected_result 84 | 85 | 86 | def test_circular_reference(): 87 | class C(EIP712Struct): 88 | # Must define A before we can reference it 89 | pass 90 | 91 | class B(EIP712Struct): 92 | c = C 93 | 94 | class A(EIP712Struct): 95 | b = B 96 | 97 | C.a = A 98 | 99 | a_sig = 'A(B b)' 100 | b_sig = 'B(C c)' 101 | c_sig = 'C(A a)' 102 | 103 | expected_result_a = f'{a_sig}{b_sig}{c_sig}' 104 | expected_result_b = f'{b_sig}{a_sig}{c_sig}' 105 | expected_result_c = f'{c_sig}{a_sig}{b_sig}' 106 | 107 | assert A.encode_type() == expected_result_a 108 | assert B.encode_type() == expected_result_b 109 | assert C.encode_type() == expected_result_c 110 | -------------------------------------------------------------------------------- /tests/test_message_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import pytest 5 | 6 | from eip712_structs import EIP712Struct, String, make_domain, Bytes 7 | 8 | 9 | def test_flat_struct_to_message(): 10 | class Foo(EIP712Struct): 11 | s = String() 12 | 13 | domain = make_domain(name='domain') 14 | foo = Foo(s='foobar') 15 | 16 | expected_result = { 17 | 'primaryType': 'Foo', 18 | 'types': { 19 | 'EIP712Domain': [{ 20 | 'name': 'name', 21 | 'type': 'string', 22 | }], 23 | 'Foo': [{ 24 | 'name': 's', 25 | 'type': 'string', 26 | }] 27 | }, 28 | 'domain': { 29 | 'name': 'domain', 30 | }, 31 | 'message': { 32 | 's': 'foobar' 33 | } 34 | } 35 | 36 | message = foo.to_message(domain) 37 | assert message == expected_result 38 | 39 | # Now test in reverse... 40 | new_struct, domain = EIP712Struct.from_message(expected_result) 41 | assert new_struct.type_name == 'Foo' 42 | 43 | members_list = new_struct.get_members() 44 | assert len(members_list) == 1 45 | assert members_list[0][0] == 's' 46 | assert members_list[0][1].type_name == 'string' 47 | 48 | assert new_struct.get_data_value('s') == 'foobar' 49 | 50 | 51 | def test_nested_struct_to_message(): 52 | class Bar(EIP712Struct): 53 | s = String() 54 | 55 | class Foo(EIP712Struct): 56 | s = String() 57 | bar = Bar 58 | 59 | domain = make_domain(name='domain') 60 | 61 | foo = Foo( 62 | s="foo", 63 | bar=Bar(s="bar") 64 | ) 65 | 66 | expected_result = { 67 | 'primaryType': 'Foo', 68 | 'types': { 69 | 'EIP712Domain': [{ 70 | 'name': 'name', 71 | 'type': 'string', 72 | }], 73 | 'Foo': [{ 74 | 'name': 's', 75 | 'type': 'string', 76 | }, { 77 | 'name': 'bar', 78 | 'type': 'Bar', 79 | }], 80 | 'Bar': [{ 81 | 'name': 's', 82 | 'type': 'string', 83 | }] 84 | }, 85 | 'domain': { 86 | 'name': 'domain', 87 | }, 88 | 'message': { 89 | 's': 'foo', 90 | 'bar': { 91 | 's': 'bar', 92 | } 93 | } 94 | } 95 | 96 | message = foo.to_message(domain) 97 | assert message == expected_result 98 | 99 | # And test in reverse... 100 | new_struct, new_domain = EIP712Struct.from_message(expected_result) 101 | assert new_struct.type_name == 'Foo' 102 | 103 | members = new_struct.get_members() 104 | assert len(members) == 2 105 | assert members[0][0] == 's' and members[0][1].type_name == 'string' 106 | assert members[1][0] == 'bar' and members[1][1].type_name == 'Bar' 107 | 108 | bar_val = new_struct.get_data_value('bar') 109 | assert bar_val.type_name == 'Bar' 110 | assert bar_val.get_data_value('s') == 'bar' 111 | 112 | assert foo.hash_struct() == new_struct.hash_struct() 113 | 114 | 115 | def test_bytes_json_encoder(): 116 | class Foo(EIP712Struct): 117 | b = Bytes(32) 118 | domain = make_domain(name='domain') 119 | 120 | bytes_val = os.urandom(32) 121 | foo = Foo(b=bytes_val) 122 | result = foo.to_message_json(domain) 123 | 124 | expected_substring = f'"b": "0x{bytes_val.hex()}"' 125 | assert expected_substring in result 126 | 127 | reconstructed = EIP712Struct.from_message(json.loads(result)) 128 | assert reconstructed.domain == domain 129 | assert reconstructed.message == foo 130 | 131 | class UnserializableObject: 132 | pass 133 | obj = UnserializableObject() 134 | 135 | # Fabricate this failure case to test that the custom json encoder's fallback path works as expected. 136 | foo.values['b'] = obj 137 | with pytest.raises(TypeError, match='not JSON serializable'): 138 | foo.to_message_json(domain) 139 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from eip712_structs import Address, Array, Boolean, Bytes, Int, String, Uint, EIP712Struct 4 | from eip712_structs.types import from_solidity_type 5 | 6 | 7 | def test_bytes_validation(): 8 | bytes0 = Bytes() 9 | assert bytes0.type_name == 'bytes' 10 | 11 | bytes0 = Bytes(0) 12 | assert bytes0.type_name == 'bytes' 13 | 14 | for n in range(1, 33): 15 | bytes_n = Bytes(n) 16 | assert bytes_n.type_name == f'bytes{n}' 17 | 18 | with pytest.raises(ValueError): 19 | Bytes(33) 20 | 21 | 22 | def run_int_test(clazz, base_name): 23 | for n in range(7, 258): 24 | if n % 8 == 0: 25 | int_n = clazz(n) 26 | assert int_n.type_name == f'{base_name}{n}' 27 | else: 28 | with pytest.raises(ValueError): 29 | clazz(n) 30 | 31 | for n in [-8, 0, 264]: 32 | with pytest.raises(ValueError): 33 | clazz(n) 34 | 35 | 36 | def test_int_validation(): 37 | run_int_test(Int, 'int') 38 | 39 | 40 | def test_uint_validation(): 41 | run_int_test(Uint, 'uint') 42 | 43 | 44 | def test_arrays(): 45 | assert Array(String()).type_name == 'string[]' 46 | assert Array(String(), 4).type_name == 'string[4]' 47 | 48 | assert Array(Bytes(17)).type_name == 'bytes17[]' 49 | assert Array(Bytes(17), 10).type_name == 'bytes17[10]' 50 | 51 | assert Array(Array(Uint(160))).type_name == 'uint160[][]' 52 | 53 | 54 | def test_struct_arrays(): 55 | class Foo(EIP712Struct): 56 | s = String() 57 | 58 | assert Array(Foo).type_name == 'Foo[]' 59 | assert Array(Foo, 10).type_name == 'Foo[10]' 60 | 61 | 62 | def test_length_str_typing(): 63 | # Ensure that if length is given as a string, it's typecast to int 64 | assert Array(String(), '5').fixed_length == 5 65 | assert Bytes('10').length == 10 66 | assert Int('128').length == 128 67 | assert Uint('128').length == 128 68 | 69 | 70 | def test_from_solidity_type(): 71 | assert from_solidity_type('address') == Address() 72 | assert from_solidity_type('bool') == Boolean() 73 | assert from_solidity_type('bytes') == Bytes() 74 | assert from_solidity_type('bytes32') == Bytes(32) 75 | assert from_solidity_type('int128') == Int(128) 76 | assert from_solidity_type('string') == String() 77 | assert from_solidity_type('uint256') == Uint(256) 78 | 79 | assert from_solidity_type('address[]') == Array(Address()) 80 | assert from_solidity_type('address[10]') == Array(Address(), 10) 81 | assert from_solidity_type('bytes16[32]') == Array(Bytes(16), 32) 82 | 83 | # Sanity check that equivalency is working as expected 84 | assert from_solidity_type('bytes32') != Bytes(31) 85 | assert from_solidity_type('bytes16[32]') != Array(Bytes(16), 31) 86 | assert from_solidity_type('bytes16[32]') != Array(Bytes(), 32) 87 | assert from_solidity_type('bytes16[32]') != Array(Bytes(8), 32) 88 | --------------------------------------------------------------------------------