Skip to content

Commit

Permalink
Merge pull request #8 from ajrgrubbs/perfectionist-cleanup
Browse files Browse the repository at this point in the history
Improve docs for 1.0.0 release.
  • Loading branch information
ajrgrubbs authored Jun 8, 2019
2 parents a6aa5be + 6320aca commit 5ce0261
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 19 deletions.
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ Say we want to represent the following struct, convert it to a message and sign
```text
struct MyStruct {
string some_string;
uint some_number;
uint256 some_number;
}
```

With this module:
With this module, that would look like:
```python
# Make a unique domain
from eip712_structs import make_domain
domain = make_domain(name='Some name', version='1.0.0')
domain = make_domain(name='Some name', version='1.0.0') # Make a Domain Separator

# Define your struct type
from eip712_structs import EIP712Struct, String, Uint
Expand Down Expand Up @@ -175,10 +175,21 @@ struct_array = Array(MyStruct, 10) # MyStruct[10] - again, don't instantiate s
Contributions always welcome.

Install dependencies:
- `pip install -r requirements.txt && pip install -r test_requirements.txt`
- `pip install -r requirements.txt -r test_requirements.txt`

Run tests:
- `python setup.py test`
- Some tests expect an active local ganache chain. Compile contracts and start the chain using docker:
- `docker-compose up -d`
- If the chain is not detected, then they are skipped.
- Some tests expect an active local ganache chain on http://localhost:8545. Docker will compile the contracts and start the chain for you.
- Docker is optional, but useful to test the whole suite. If no chain is detected, chain tests are skipped.
- Usage:
- `docker-compose up -d` (Starts containers in the background)
- Note: Contracts are compiled when you run `up`, but won't be deployed until the test is run.
- Cleanup containers when you're done: `docker-compose down`

Deploying a new version:
- Set the version number in `eip712_structs/__init__.py`
- Make a release tag on the master branch in Github. Travis should handle the rest.


## Shameless Plug
Written by [ConsenSys](https://consensys.net) for ourselves and the community! :heart:
73 changes: 62 additions & 11 deletions eip712_structs/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,28 @@

class EIP712Type:
"""The base type for members of a struct.
Generally you wouldn't use this - instead, see the subclasses below. Or you may want an EIP712Struct instead.
"""
def __init__(self, type_name: str, none_val: Any):
self.type_name = type_name
self.none_val = none_val

def encode_value(self, value) -> bytes:
"""Given a value, verify it and convert into the format required by the spec.
:param value: A correct input value for the implemented type.
:return: A 32-byte object containing encoded data
"""
if value is None:
return self._encode_value(self.none_val)
else:
return self._encode_value(value)

def _encode_value(self, value) -> bytes:
"""Given a value, verify it and convert into the format required by the spec.
"""Must be implemented by subclasses, handles value encoding on a case-by-case basis.
:param value: A correct input value for the implemented type.
:return: A 32-byte object containing encoded data
Don't call this directly - use ``.encode_value(value)`` instead.
"""
pass

Expand All @@ -38,6 +44,13 @@ def __hash__(self):

class Array(EIP712Type):
def __init__(self, member_type: Union[EIP712Type, Type[EIP712Type]], fixed_length: int = 0):
"""Represents an array member type.
Example:
a1 = Array(String()) # string[] a1
a2 = Array(String(), 8) # string[8] a2
a3 = Array(MyStruct) # MyStruct[] a3
"""
fixed_length = int(fixed_length)
if fixed_length == 0:
type_name = f'{member_type.type_name}[]'
Expand All @@ -48,31 +61,37 @@ def __init__(self, member_type: Union[EIP712Type, Type[EIP712Type]], fixed_lengt
super(Array, self).__init__(type_name, [])

def _encode_value(self, value):
"""Arrays are encoded by concatenating their encoded contents, and taking the keccak256 hash."""
encoder = self.member_type
encoded_values = [encoder.encode_value(v) for v in value]
return keccak(b''.join(encoded_values))


class Address(EIP712Type):
def __init__(self):
"""Represents an ``address`` type."""
super(Address, self).__init__('address', 0)

def _encode_value(self, value):
# Some smart conversions - need to get an address as an int
"""Addresses are encoded like Uint160 numbers."""

# Some smart conversions - need to get the address to a numeric before we encode it
if isinstance(value, bytes):
v = to_int(value)
elif isinstance(value, str):
v = to_int(hexstr=value)
else:
v = value
v = value # Fallback, just use it as-is.
return Uint(160).encode_value(v)


class Boolean(EIP712Type):
def __init__(self):
"""Represents a ``bool`` type."""
super(Boolean, self).__init__('bool', False)

def _encode_value(self, value):
"""Booleans are encoded like the uint256 values of 0 and 1."""
if value is False:
return Uint(256).encode_value(0)
elif value is True:
Expand All @@ -83,6 +102,15 @@ def _encode_value(self, value):

class Bytes(EIP712Type):
def __init__(self, length: int = 0):
"""Represents a solidity bytes type.
Length may be used to specify a static ``bytesN`` type. Or 0 for a dynamic ``bytes`` type.
Example:
b1 = Bytes() # bytes b1
b2 = Bytes(10) # bytes10 b2
``length`` MUST be between 0 and 32, or a ValueError is raised.
"""
length = int(length)
if length == 0:
# Special case: Length of 0 means a dynamic bytes type
Expand All @@ -95,6 +123,7 @@ def __init__(self, length: int = 0):
super(Bytes, self).__init__(type_name, b'')

def _encode_value(self, value):
"""Static bytesN types are encoded by right-padding to 32 bytes. Dynamic bytes types are keccak256 hashed."""
if self.length == 0:
return keccak(value)
else:
Expand All @@ -105,39 +134,58 @@ def _encode_value(self, value):


class Int(EIP712Type):
def __init__(self, length: int):
def __init__(self, length: int = 256):
"""Represents a signed int type. Length may be given to specify the int length in bits. Default length is 256
Example:
i1 = Int(256) # int256 i1
i2 = Int() # int256 i2
i3 = Int(128) # int128 i3
"""
length = int(length)
if length < 8 or length > 256 or length % 8 != 0:
raise ValueError(f'Int length must be a multiple of 8, between 8 and 256. Got: {length}')
self.length = length
super(Int, self).__init__(f'int{length}', 0)

def _encode_value(self, value: int):
"""Ints are encoded by padding them to 256-bit representations."""
value.to_bytes(self.length // 8, byteorder='big', signed=True) # For validation
return value.to_bytes(32, byteorder='big', signed=True)


class String(EIP712Type):
def __init__(self):
"""Represents a string type."""
super(String, self).__init__('string', '')

def _encode_value(self, value):
"""Strings are encoded by taking the keccak256 hash of their contents."""
return keccak(text=value)


class Uint(EIP712Type):
def __init__(self, length: int):
def __init__(self, length: int = 256):
"""Represents an unsigned int type. Length may be given to specify the int length in bits. Default length is 256
Example:
ui1 = Uint(256) # uint256 ui1
ui2 = Uint() # uint256 ui2
ui3 = Uint(128) # uint128 ui3
"""
length = int(length)
if length < 8 or length > 256 or length % 8 != 0:
raise ValueError(f'Uint length must be a multiple of 8, between 8 and 256. Got: {length}')
self.length = length
super(Uint, self).__init__(f'uint{length}', 0)

def _encode_value(self, value: int):
"""Uints are encoded by padding them to 256-bit representations."""
value.to_bytes(self.length // 8, byteorder='big', signed=False) # For validation
return value.to_bytes(32, byteorder='big', signed=False)


# This helper dict maps solidity's type names to our EIP712Type classes
solidity_type_map = {
'address': Address,
'bool': Boolean,
Expand All @@ -156,21 +204,24 @@ def from_solidity_type(solidity_type: str):
if match is None:
return None

type_name = match.group(1)
opt_len = match.group(2)
is_array = match.group(3)
array_len = match.group(4)
type_name = match.group(1) # The type name, like the "bytes" in "bytes32"
opt_len = match.group(2) # An optional length spec, like the "32" in "bytes32"
is_array = match.group(3) # Basically just checks for square brackets
array_len = match.group(4) # For fixed length arrays only, this is the length

if type_name not in solidity_type_map:
# Only supporting basic types here - return None if we don't recognize it.
return None

# Construct the basic type
base_type = solidity_type_map[type_name]
if opt_len:
type_instance = base_type(int(opt_len))
else:
type_instance = base_type()

if is_array:
# Nest the aforementioned basic type into an Array.
if array_len:
result = Array(type_instance, int(array_len))
else:
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,18 @@ def run_tests(self):
setup(
name=name,
version=version,
author='AJ Grubbs',
packages=find_packages(),
install_requires=install_requirements,
tests_require=test_requirements,
cmdclass={
"test": PyTest,
"coveralls": CoverallsCommand,
},
description='A python library for EIP712 objects',
long_description=long_description,
long_description_content_type='text/markdown',
license='MIT',
keywords='ethereum eip712',
keywords='ethereum eip712 solidity',
url='https://github.com/ajrgrubbs/py-eip712-structs',
)

0 comments on commit 5ce0261

Please sign in to comment.