Skip to content

Commit

Permalink
Merge pull request #10 from ajrgrubbs/json-dumps
Browse files Browse the repository at this point in the history
Add helper method to get a struct message as JSON
  • Loading branch information
ajrgrubbs authored Jun 13, 2019
2 parents 2ae3c3a + afaec95 commit 9bf0955
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 3 deletions.
27 changes: 26 additions & 1 deletion eip712_structs/struct.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import functools
import json
import operator
import re
from collections import OrderedDict, defaultdict
from typing import List, Tuple, NamedTuple

from eth_utils.crypto import keccak

import eip712_structs
from eip712_structs.types import Array, EIP712Type, from_solidity_type
from eip712_structs.types import Array, EIP712Type, from_solidity_type, BytesJSONEncoder


class OrderedAttributesMeta(type):
Expand Down Expand Up @@ -180,6 +183,10 @@ def to_message(self, domain: 'EIP712Struct' = None) -> dict:

return result

def to_message_json(self, domain: 'EIP712Struct' = None) -> str:
message = self.to_message(domain)
return json.dumps(message, cls=BytesJSONEncoder)

def signable_bytes(self, domain: 'EIP712Struct' = None) -> bytes:
"""Return a ``bytes`` object suitable for signing, as specified for EIP712.
Expand Down Expand Up @@ -290,6 +297,24 @@ def __setitem__(self, key, value):
def __delitem__(self, _):
raise TypeError('Deleting entries from an EIP712Struct is not allowed.')

def __eq__(self, other):
if not other:
# Null check
return False
if self is other:
# Check identity
return True
if not isinstance(other, EIP712Struct):
# Check class
return False
# Our structs are considered equal if their type signature and encoded value signature match.
# E.g., like computing signable bytes but without a domain separator
return self.encode_type() == other.encode_type() and self.encode_value() == other.encode_value()

def __hash__(self):
value_hashes = [hash(k) ^ hash(v) for k, v in self.values.items()]
return functools.reduce(operator.xor, value_hashes, hash(self.type_name))


class StructTuple(NamedTuple):
message: EIP712Struct
Expand Down
15 changes: 14 additions & 1 deletion eip712_structs/types.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import re
from json import JSONEncoder
from typing import Any, Union, Type

from eth_utils.crypto import keccak
from eth_utils.conversions import to_int
from eth_utils.conversions import to_bytes, to_hex, to_int


class EIP712Type:
Expand Down Expand Up @@ -124,6 +125,10 @@ def __init__(self, length: int = 0):

def _encode_value(self, value):
"""Static bytesN types are encoded by right-padding to 32 bytes. Dynamic bytes types are keccak256 hashed."""
if isinstance(value, str):
# Try converting to a bytestring, assuming that it's been given as hex
value = to_bytes(hexstr=value)

if self.length == 0:
return keccak(value)
else:
Expand Down Expand Up @@ -229,3 +234,11 @@ def from_solidity_type(solidity_type: str):
return result
else:
return type_instance


class BytesJSONEncoder(JSONEncoder):
def default(self, o):
if isinstance(o, bytes):
return to_hex(o)
else:
return super(BytesJSONEncoder, self).default(o)
43 changes: 43 additions & 0 deletions tests/test_encode_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,49 @@ def test_validation_errors():
bool_type.encode_value(1)


def test_struct_eq():
class Foo(EIP712Struct):
s = String()
foo = Foo(s='hello world')
foo_copy = Foo(s='hello world')
foo_2 = Foo(s='blah')

assert foo != None
assert foo != 'unrelated type'
assert foo == foo
assert foo is not foo_copy
assert foo == foo_copy
assert foo != foo_2

def make_different_foo():
# We want another struct defined with the same name but different member types
class Foo(EIP712Struct):
b = Bytes()
return Foo

def make_same_foo():
# For good measure, recreate the exact same class and ensure they can still compare
class Foo(EIP712Struct):
s = String()
return Foo

OtherFooClass = make_different_foo()
wrong_type = OtherFooClass(b=b'hello world')
assert wrong_type != foo
assert OtherFooClass != Foo

SameFooClass = make_same_foo()
right_type = SameFooClass(s='hello world')
assert right_type == foo
assert SameFooClass != Foo

# Different name, same members
class Bar(EIP712Struct):
s = String()
bar = Bar(s='hello world')
assert bar != foo


def test_value_access():
class Foo(EIP712Struct):
s = String()
Expand Down
33 changes: 32 additions & 1 deletion tests/test_message_json.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from eip712_structs import EIP712Struct, String, make_domain
import json
import os

import pytest

from eip712_structs import EIP712Struct, String, make_domain, Bytes


def test_flat_struct_to_message():
Expand Down Expand Up @@ -105,3 +110,29 @@ class Foo(EIP712Struct):
assert bar_val.get_data_value('s') == 'bar'

assert foo.hash_struct() == new_struct.hash_struct()


def test_bytes_json_encoder():
class Foo(EIP712Struct):
b = Bytes(32)
domain = make_domain(name='domain')

bytes_val = os.urandom(32)
foo = Foo(b=bytes_val)
result = foo.to_message_json(domain)

expected_substring = f'"b": "0x{bytes_val.hex()}"'
assert expected_substring in result

reconstructed = EIP712Struct.from_message(json.loads(result))
assert reconstructed.domain == domain
assert reconstructed.message == foo

class UnserializableObject:
pass
obj = UnserializableObject()

# Fabricate this failure case to test that the custom json encoder's fallback path works as expected.
foo.values['b'] = obj
with pytest.raises(TypeError, match='not JSON serializable'):
foo.to_message_json(domain)

0 comments on commit 9bf0955

Please sign in to comment.