|
| 1 | +import functools |
| 2 | +import json |
| 3 | +import operator |
1 | 4 | import re |
2 | 5 | from collections import OrderedDict, defaultdict |
3 | 6 | from typing import List, Tuple, NamedTuple |
4 | 7 |
|
5 | 8 | from eth_utils.crypto import keccak |
6 | 9 |
|
7 | 10 | import eip712_structs |
8 | | -from eip712_structs.types import Array, EIP712Type, from_solidity_type |
| 11 | +from eip712_structs.types import Array, EIP712Type, from_solidity_type, BytesJSONEncoder |
9 | 12 |
|
10 | 13 |
|
11 | 14 | class OrderedAttributesMeta(type): |
@@ -180,6 +183,10 @@ def to_message(self, domain: 'EIP712Struct' = None) -> dict: |
180 | 183 |
|
181 | 184 | return result |
182 | 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 | + |
183 | 190 | def signable_bytes(self, domain: 'EIP712Struct' = None) -> bytes: |
184 | 191 | """Return a ``bytes`` object suitable for signing, as specified for EIP712. |
185 | 192 |
|
@@ -251,6 +258,63 @@ def from_message(cls, message_dict: dict) -> 'StructTuple': |
251 | 258 |
|
252 | 259 | return result |
253 | 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 | + |
254 | 318 |
|
255 | 319 | class StructTuple(NamedTuple): |
256 | 320 | message: EIP712Struct |
|
0 commit comments