├── .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 [](https://travis-ci.org/ConsenSys/py-eip712-structs) [](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 |
--------------------------------------------------------------------------------