Skip to content

Commit 5f67156

Browse files
committed
Cleanup time - update readme, docs, methods for humans
1 parent 712066f commit 5f67156

File tree

8 files changed

+153
-36
lines changed

8 files changed

+153
-36
lines changed

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ struct Message {
2525

2626
Python representation:
2727
```python
28-
from eip712_structs import EIP712Struct, Address, String, make_domain_separator, create_message
28+
from eip712_structs import EIP712Struct, Address, String, make_domain, struct_to_dict
2929

3030
class Message(EIP712Struct):
3131
to = Address()
@@ -36,8 +36,36 @@ enctyp = Message.encode_type() # 'Mail(address to,string contents)'
3636
msg = Message(to='0xdead...beef', contents='hello world')
3737
msg.encode_data() # The struct's data in encoded form
3838

39-
domain = make_domain_separator(name='example')
40-
msg_body, msg_hash = create_message(domain, msg)
39+
domain = make_domain(name='example')
40+
msg_body, msg_hash = struct_to_dict(msg, domain)
41+
```
42+
43+
#### Dynamic construction
44+
Attributes may be added dynamically as well. This may be necessary if you
45+
want to use a reserved keyword like `from`.
46+
47+
```python
48+
class Message(EIP712Struct):
49+
pass
50+
51+
Message.to = Address()
52+
setattr(Message, 'from', Address())
53+
```
54+
55+
#### Creating Messages and Hashing
56+
Messages also require a domain struct. A helper method exists for this purpose.
57+
58+
```python
59+
from eip712_structs import EIP712Struct, String, make_domain, struct_to_dict
60+
61+
domain = make_domain(name='my_domain') # Also accepts kwargs: version, chainId, verifyingContract, salt
62+
63+
class Foo(EIP712Struct):
64+
bar = String()
65+
66+
foo = Foo(bar='baz')
67+
68+
message_dict, message_hash = struct_to_dict(foo, domain)
4169
```
4270

4371
## Member Types

eip712_structs/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from eip712_structs.domain_separator import make_domain_separator
2-
from eip712_structs.struct import EIP712Struct, struct_to_json, struct_from_json
1+
from eip712_structs.domain_separator import make_domain
2+
from eip712_structs.struct import EIP712Struct, struct_to_dict, struct_from_dict
33
from eip712_structs.types import Address, Array, Boolean, Bytes, Int, String, Uint
44

55
name = 'eip712-structs'
6-
version = '0.1.1'
6+
version = '0.1.2'

eip712_structs/domain_separator.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import eip712_structs
22

33

4-
def make_domain_separator(name=None, version=None, chainId=None, verifyingContract=None, salt=None):
4+
def make_domain(name=None, version=None, chainId=None, verifyingContract=None, salt=None):
55
"""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.
68
"""
9+
10+
if all(i is None for i in [name, version, chainId, verifyingContract, salt]):
11+
raise ValueError('At least on argument must be given.')
12+
713
class EIP712Domain(eip712_structs.EIP712Struct):
814
pass
915

1016
kwargs = dict()
1117
if name is not None:
1218
EIP712Domain.name = eip712_structs.String()
13-
kwargs['name'] = name
19+
kwargs['name'] = str(name)
1420
if version is not None:
1521
EIP712Domain.version = eip712_structs.String()
16-
kwargs['version'] = version
22+
kwargs['version'] = str(version)
1723
if chainId is not None:
1824
EIP712Domain.chainId = eip712_structs.Uint(256)
19-
kwargs['chainId'] = chainId
25+
kwargs['chainId'] = int(chainId)
2026
if verifyingContract is not None:
2127
EIP712Domain.verifyingContract = eip712_structs.Address()
2228
kwargs['verifyingContract'] = verifyingContract

eip712_structs/struct.py

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import re
12
from collections import OrderedDict, defaultdict
23
from typing import List, Tuple
34

45
from eth_utils.crypto import keccak
56

6-
from eip712_structs.types import EIP712Type, from_solidity_type
7+
from eip712_structs.types import Array, EIP712Type, from_solidity_type
78

89

910
class OrderedAttributesMeta(type):
@@ -25,6 +26,16 @@ def __init_subclass__(cls, **kwargs):
2526

2627

2728
class EIP712Struct(_EIP712StructTypeHelper):
29+
"""A representation of an EIP712 struct. Subclass it to use it.
30+
31+
Example:
32+
from eip712_structs import EIP712Struct, String
33+
34+
class MyStruct(EIP712Struct):
35+
some_param = String()
36+
37+
struct_instance = MyStruct(some_param='some_value')
38+
"""
2839
def __init__(self, **kwargs):
2940
super(EIP712Struct, self).__init__(self.type_name)
3041
members = self.get_members()
@@ -36,20 +47,32 @@ def __init__(self, **kwargs):
3647
self.values[name] = value
3748

3849
def encode_value(self, value=None):
50+
"""Returns the struct's encoded value.
51+
52+
A struct's encoded value is a concatenation of the bytes32 representation of each member of the struct.
53+
Order is preserved.
54+
55+
:param value: This parameter is not used for structs.
56+
"""
3957
encoded_values = [typ.encode_value(self.values[name]) for name, typ in self.get_members()]
4058
return b''.join(encoded_values)
4159

42-
def encode_data(self):
43-
return self.encode_value()
44-
4560
def get_data_value(self, name):
61+
"""Get the value of the given struct parameter.
62+
"""
4663
return self.values.get(name)
4764

4865
def set_data_value(self, name, value):
66+
"""Set the value of the given struct parameter.
67+
"""
4968
if name in self.values:
5069
self.values[name] = value
5170

5271
def data_dict(self):
72+
"""Provide the entire data dictionary representing the struct.
73+
74+
Nested structs instances are also converted to dict form.
75+
"""
5376
result = dict()
5477
for k, v in self.values.items():
5578
if isinstance(v, EIP712Struct):
@@ -81,23 +104,51 @@ def _gather_reference_structs(cls, struct_set):
81104

82105
@classmethod
83106
def encode_type(cls):
107+
"""Get the encoded type signature of the struct.
108+
109+
Nested structs are also encoded, and appended in alphabetical order.
110+
"""
84111
return cls._encode_type(True)
85112

86113
@classmethod
87114
def type_hash(cls):
115+
"""Get the keccak hash of the struct's encoded type."""
88116
return keccak(text=cls.encode_type())
89117

90118
def hash_struct(self):
119+
"""The hash of the struct.
120+
121+
hash_struct => keccak(type_hash || encode_data)
122+
"""
91123
return keccak(b''.join([self.type_hash(), self.encode_data()]))
92124

93125
@classmethod
94126
def get_members(cls) -> List[Tuple[str, EIP712Type]]:
127+
"""A list of tuples of supported parameters.
128+
129+
Each tuple is (<parameter_name>, <parameter_type>). The list's order is determined by definition order.
130+
"""
95131
members = [m for m in cls.__dict__.items() if isinstance(m[1], EIP712Type)
96132
or (isinstance(m[1], type) and issubclass(m[1], EIP712Struct))]
97133
return members
98134

99135

100-
def struct_to_json(domain: EIP712Struct, primary_struct: EIP712Struct):
136+
def struct_to_dict(primary_struct: EIP712Struct, domain: EIP712Struct):
137+
"""Convert a struct into a dictionary suitable for messaging.
138+
139+
Dictionary is of the form:
140+
{
141+
'primaryType': Name of the primary type,
142+
'types': Definition of each included struct type (including the domain type)
143+
'domain': Values for the domain struct,
144+
'message': Values for the message struct,
145+
}
146+
147+
The hash is constructed as:
148+
`` b'\\x19\\x01' + domain_type_hash + struct_type_hash ``
149+
150+
:returns: A tuple in the form of: (message_dict, encoded_message_hash>)
151+
"""
101152
structs = {domain, primary_struct}
102153
primary_struct._gather_reference_structs(structs)
103154

@@ -122,29 +173,46 @@ def struct_to_json(domain: EIP712Struct, primary_struct: EIP712Struct):
122173
return result, typed_data_hash
123174

124175

125-
def struct_from_json(json):
176+
def struct_from_dict(message_dict):
177+
"""Return the EIP712Struct object of the message and domain structs.
178+
179+
:returns: A tuple in the form of: (<primary struct>, <domain struct>)
180+
"""
126181
structs = dict()
127182
unfulfilled_struct_params = defaultdict(list)
128183

129-
for type_name in json['types']:
130-
# Dynamically construct struct class from JSON
184+
for type_name in message_dict['types']:
185+
# Dynamically construct struct class from dict representation
131186
StructFromJSON = type(type_name, (EIP712Struct,), {})
132187

133-
for member in json['types'][type_name]:
188+
for member in message_dict['types'][type_name]:
134189
# Either a basic solidity type is set, or None if referring to a reference struct (we'll fill that later)
135190
member_name = member['name']
136191
member_sol_type = from_solidity_type(member['type'])
137192
setattr(StructFromJSON, member_name, member_sol_type)
138193
if member_sol_type is None:
194+
# Track the refs we'll need to set later.
139195
unfulfilled_struct_params[type_name].append((member_name, member['type']))
140196

141197
structs[type_name] = StructFromJSON
142198

199+
# Now that custom structs have been parsed, pass through again to set the references
143200
for struct_name, unfulfilled_member_names in unfulfilled_struct_params.items():
201+
regex_pattern = r'([a-zA-Z0-9_]+)(\[(\d+)?\])?'
202+
144203
struct_class = structs[struct_name]
145204
for name, type_name in unfulfilled_member_names:
146-
ref_struct = structs[type_name]
147-
setattr(struct_class, name, ref_struct)
205+
match = re.match(regex_pattern, type_name)
206+
base_type_name = match.group(1)
207+
ref_struct = structs[base_type_name]
208+
if match.group(2):
209+
# The type is an array of the struct
210+
arr_len = match.group(3) or 0 # length of 0 means the array is dynamically sized
211+
setattr(struct_class, name, Array(ref_struct, arr_len))
212+
else:
213+
setattr(struct_class, name, ref_struct)
214+
215+
primary_struct = structs[message_dict['primaryType']]
216+
domain_struct = structs['EIP712Domain']
148217

149-
primary_struct = structs[json['primaryType']]
150-
return primary_struct(**json['message'])
218+
return primary_struct(**message_dict['message']), domain_struct(**message_dict['domain'])

eip712_structs/types.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import re
2+
from typing import Union, Type
23

34
from eth_utils.crypto import keccak
45
from eth_utils.conversions import to_int
56

67

78
class EIP712Type:
9+
"""The base type for members of a struct.
10+
"""
811
def __init__(self, type_name: str):
912
self.type_name = type_name
1013

@@ -18,7 +21,7 @@ def encode_value(self, value) -> bytes:
1821

1922

2023
class Array(EIP712Type):
21-
def __init__(self, member_type: EIP712Type, fixed_length: int = 0):
24+
def __init__(self, member_type: Union[EIP712Type, Type[EIP712Type]], fixed_length: int = 0):
2225
if fixed_length == 0:
2326
type_name = f'{member_type.type_name}[]'
2427
else:
@@ -125,6 +128,7 @@ def encode_value(self, value: int):
125128

126129

127130
def from_solidity_type(solidity_type: str):
131+
"""Convert a string into the EIP712Type implementation. Basic types only."""
128132
pattern = r'([a-z]+)(\d+)?(\[(\d+)?\])?'
129133
match = re.match(pattern, solidity_type)
130134

@@ -136,6 +140,9 @@ def from_solidity_type(solidity_type: str):
136140
is_array = match.group(3)
137141
array_len = match.group(4)
138142

143+
if type_name not in solidity_type_map:
144+
return None
145+
139146
base_type = solidity_type_map[type_name]
140147
if opt_len:
141148
type_instance = base_type(opt_len)

tests/test_domain_separator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import os
2-
from eip712_structs import make_domain_separator
2+
from eip712_structs import make_domain
33
from eth_utils.crypto import keccak
44

55

66
def test_domain_sep_create():
77
salt = os.urandom(32)
8-
domain_struct = make_domain_separator(name='name', salt=salt)
8+
domain_struct = make_domain(name='name', salt=salt)
99

1010
expected_result = 'EIP712Domain(string name,bytes32 salt)'
1111
assert domain_struct.encode_type() == expected_result
@@ -18,8 +18,8 @@ def test_domain_sep_types():
1818
salt = os.urandom(32)
1919
contract = os.urandom(20)
2020

21-
domain_struct = make_domain_separator(name='name', version='version', chainId=1,
22-
verifyingContract=contract, salt=salt)
21+
domain_struct = make_domain(name='name', version='version', chainId=1,
22+
verifyingContract=contract, salt=salt)
2323

2424
encoded_data = [keccak(text='name'), keccak(text='version'), int(1).to_bytes(32, 'big', signed=False),
2525
bytes(12) + contract, salt]

tests/test_message_json.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from eip712_structs import EIP712Struct, String, make_domain_separator, struct_to_json, struct_from_json
1+
from eip712_structs import EIP712Struct, String, make_domain, struct_to_dict, struct_from_dict
22

33

44
def test_flat_struct_to_message():
55
class Foo(EIP712Struct):
66
s = String()
77

8-
domain = make_domain_separator(name='domain')
8+
domain = make_domain(name='domain')
99
foo = Foo(s='foobar')
1010

1111
expected_result = {
@@ -28,11 +28,11 @@ class Foo(EIP712Struct):
2828
}
2929
}
3030

31-
message, _ = struct_to_json(domain, foo)
31+
message, _ = struct_to_dict(foo, domain)
3232
assert message == expected_result
3333

3434
# Now test in reverse...
35-
new_struct = struct_from_json(expected_result)
35+
new_struct, domain = struct_from_dict(expected_result)
3636
assert new_struct.type_name == 'Foo'
3737

3838
members_list = new_struct.get_members()
@@ -51,7 +51,7 @@ class Foo(EIP712Struct):
5151
s = String()
5252
bar = Bar
5353

54-
domain = make_domain_separator(name='domain')
54+
domain = make_domain(name='domain')
5555

5656
foo = Foo(
5757
s="foo",
@@ -88,11 +88,11 @@ class Foo(EIP712Struct):
8888
}
8989
}
9090

91-
message, _ = struct_to_json(domain, foo)
91+
message, _ = struct_to_dict(foo, domain)
9292
assert message == expected_result
9393

9494
# And test in reverse...
95-
new_struct = struct_from_json(expected_result)
95+
new_struct, new_domain = struct_from_dict(expected_result)
9696
assert new_struct.type_name == 'Foo'
9797

9898
members = new_struct.get_members()

0 commit comments

Comments
 (0)