Skip to content

Commit faf1e39

Browse files
authored
Merge pull request #4 from ajrgrubbs/prep-1.0.0-api
Prep 1.0.0 api - big refactor to make this correct and usable - Significant test improvements, including parity tests w/ Docker+Ganache - Code coverage integration w/ TravisCI+Coveralls - Several bugfixes
2 parents 016372e + 79275b6 commit faf1e39

15 files changed

+498
-136
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
.eggs/
12
*.egg-info/
23
dist/
34
build/
45
.pytest_cache/
6+
7+
tests/contracts/build

.travis.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,10 @@ python:
55
- "3.6"
66
- "3.7"
77
script:
8-
- python setup.py test
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

API.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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.

README.md

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# EIP-712 Structs [![Build Status](https://travis-ci.org/ajrgrubbs/py-eip712-structs.svg?branch=master)](https://travis-ci.org/ajrgrubbs/py-eip712-structs)
1+
# EIP-712 Structs [![Build Status](https://travis-ci.org/ajrgrubbs/py-eip712-structs.svg?branch=master)](https://travis-ci.org/ajrgrubbs/py-eip712-structs) [![Coverage Status](https://coveralls.io/repos/github/ajrgrubbs/py-eip712-structs/badge.svg?branch=master)](https://coveralls.io/github/ajrgrubbs/py-eip712-structs?branch=master)
22

33
A python interface for simple EIP-712 struct construction.
44

@@ -17,47 +17,43 @@ https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md
1717
pip install eip712-structs
1818
```
1919

20-
## Basic Usage
21-
22-
Our desired struct:
23-
```
24-
struct Message {
25-
address to;
26-
string contents;
20+
## Quickstart
21+
Say we want to represent the following struct, convert it to a message and sign it:
22+
```text
23+
struct MyStruct {
24+
string some_string;
25+
uint some_number;
2726
}
2827
```
2928

30-
Python representation:
29+
With this module:
3130
```python
32-
from eip712_structs import EIP712Struct, Address, String, make_domain, struct_to_message
33-
34-
class Message(EIP712Struct):
35-
to = Address()
36-
contents = String()
37-
38-
enctyp = Message.encode_type() # 'Mail(address to,string contents)'
31+
# Make a unique domain
32+
from eip712_structs import make_domain
33+
domain = make_domain(name='Some name', version='1.0.0')
3934

40-
msg = Message(to='0xdead...beef', contents='hello world')
41-
msg.encode_data() # The struct's data in encoded form
35+
# Define your struct type
36+
from eip712_structs import EIP712Struct, String, Uint
37+
class MyStruct(EIP712Struct):
38+
some_string = String()
39+
some_number = Uint(256)
4240

43-
domain = make_domain(name='example')
44-
msg_body, signable_bytes = struct_to_message(msg, domain)
41+
# Create an instance with some data
42+
mine = MyStruct(some_string='hello world', some_number=1234)
4543

46-
# msg_body is a standardized dict with keys primaryType, types, domain, and message
47-
# suitable for converting to JSON for making requests
44+
# Into a message dict (serializable to JSON) - domain required
45+
my_msg = mine.to_message(domain)
4846

49-
# `signable bytes` is a deterministic bytes representation of the struct
50-
# Suitable for hashing or signing
51-
# bytes 0-1: b'\x19\x01'
52-
# bytes 2-33: domain separator (hash of domain type and data)
53-
# bytes 34-65: hash of struct type and data
47+
# Into signable bytes - domain required
48+
my_bytes = mine.signable_bytes(domain)
5449
```
5550

5651
#### Dynamic construction
5752
Attributes may be added dynamically as well. This may be necessary if you
5853
want to use a reserved keyword like `from`.
5954

6055
```python
56+
from eip712_structs import EIP712Struct, Address
6157
class Message(EIP712Struct):
6258
pass
6359

@@ -66,27 +62,15 @@ setattr(Message, 'from', Address())
6662
```
6763

6864
#### The domain separator
69-
Messages also require a domain struct. A helper method exists for this purpose.
65+
EIP-712 specifies a domain struct, to differentiate between identical structs that may be unrelated.
66+
A helper method exists for this purpose.
7067
All values to the `make_domain()`
7168
function are optional - but at least one must be defined. If omitted, the resulting
7269
domain struct's definition leaves out the parameter entirely.
7370

7471
The full signature: <br/>
7572
`make_domain(name: string, version: string, chainId: uint256, verifyingContract: address, salt: bytes32)`
7673

77-
```python
78-
from eip712_structs import EIP712Struct, String, make_domain, struct_to_message
79-
80-
domain = make_domain(name='my_domain')
81-
82-
class Foo(EIP712Struct):
83-
bar = String()
84-
85-
foo = Foo(bar='baz')
86-
87-
message_dict, message_hash = struct_to_message(foo, domain)
88-
```
89-
9074

9175
## Member Types
9276

@@ -107,6 +91,8 @@ Uint(N) # 'uintN' - N must be a multiple of 8, from 8 to 256
10791

10892
Use like:
10993
```python
94+
from eip712_structs import EIP712Struct, Address, Bytes
95+
11096
class Foo(EIP712Struct):
11197
member_name_0 = Address()
11298
member_name_1 = Bytes(5)
@@ -119,6 +105,8 @@ Usage is almost the same - the difference is you don't "instantiate" the class.
119105

120106
Example:
121107
```python
108+
from eip712_structs import EIP712Struct, String
109+
122110
class Dog(EIP712Struct):
123111
name = String()
124112
breed = String()
@@ -127,20 +115,23 @@ class Person(EIP712Struct):
127115
name = String()
128116
dog = Dog # Take note - no parentheses!
129117

118+
# Dog "stands alone"
130119
Dog.encode_type() # Dog(string name,string breed)
120+
121+
# But Person knows how to include Dog
131122
Person.encode_type() # Person(string name,Dog dog)Dog(string name,string breed)
132123
```
133124

134-
Instantiating the structs with nested values may be done like:
125+
Instantiating the structs with nested values may be done a couple different ways:
135126

136127
```python
137128
# Method one: set it to a struct
138129
dog = Dog(name='Mochi', breed='Corgi')
139-
person = Person(name='Ed', dog=dog)
130+
person = Person(name='E.M.', dog=dog)
140131

141132
# Method two: set it to a dict - the underlying struct is built for you
142133
person = Person(
143-
name='Ed',
134+
name='E.M.',
144135
dog={
145136
'name': 'Mochi',
146137
'breed': 'Corgi',
@@ -173,3 +164,6 @@ Install dependencies:
173164

174165
Run tests:
175166
- `python setup.py test`
167+
- Some tests expect an active local ganache chain. Compile contracts and start the chain using docker:
168+
- `docker-compose up -d`
169+
- If the chain is not detected, then they are skipped.

docker-compose.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
version: "3"
3+
services:
4+
ganache:
5+
image: "trufflesuite/ganache-cli:latest"
6+
# Corresponding account address: 0xD68D840e1e971750E6d45049ff579925456d5893
7+
command: "--account=\"0x3660582119566511de16f4dcf397fa324b27bd6247f653cf0298a0993f3432ed,100000000000000000000\""
8+
ports:
9+
- "8545:8545"
10+
depends_on:
11+
- compiler
12+
13+
compiler:
14+
image: "ethereum/solc:stable"
15+
command: "--abi --bin -o /tmp/contracts/build --overwrite /tmp/contracts/hash_test_contract.sol"
16+
volumes:
17+
- ./tests/contracts:/tmp/contracts

eip712_structs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from eip712_structs.domain_separator import make_domain
2-
from eip712_structs.struct import EIP712Struct, struct_from_message, struct_to_message
2+
from eip712_structs.struct import EIP712Struct
33
from eip712_structs.types import Address, Array, Boolean, Bytes, Int, String, Uint
44

55
name = 'eip712-structs'

eip712_structs/domain_separator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def make_domain(name=None, version=None, chainId=None, verifyingContract=None, s
88
"""
99

1010
if all(i is None for i in [name, version, chainId, verifyingContract, salt]):
11-
raise ValueError('At least on argument must be given.')
11+
raise ValueError('At least one argument must be given.')
1212

1313
class EIP712Domain(eip712_structs.EIP712Struct):
1414
pass

0 commit comments

Comments
 (0)