Repository: rmeissner/py-eth-sig-utils

Issue:
Recover Signed Messages
First off - thanks a ton for your work on this! Getting tooling around EIP-712 is going to be critical for it to be adopted so I applaud this effort!

I wanted to see if you had any intention of implementing recovery / verification for EIP-712, so that you can verify signed messages, e.g. where they are generated on the client and sent to a backend.


Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..9f4cc33
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,15 @@
+language: python
+cache: pip
+python:
+  - "3.6"
+dist: trusty
+install:
+  - pip install -r requirements.txt
+script:
+  - python -m unittest
+deploy:
+  provider: script
+  script: bash scripts/deploy.sh
+  skip_cleanup: true
+  on:
+    tags: true
\ No newline at end of file
diff --git a/README.md b/README.md
index b2f4298..33a9c0f 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
 # Python Ethereum Signing Utils
 
+[![Build Status](https://travis-ci.org/rmeissner/py-eth-sig-utils.svg?branch=master)](https://travis-ci.org/rmeissner/py-eth-sig-utils)
+[![PyPI version](https://badge.fury.io/py/py-eth-sig-utils.svg)](https://badge.fury.io/py/py-eth-sig-utils)
+
 ### Type Data Hashes
 
 This utils contain methods to generate hashes of typed data based on [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md).
@@ -8,4 +11,14 @@ This utils contain methods to generate hashes of typed data based on [EIP-712](h
 
 1. `virtualenv env -p python3`
 1. `pip install -r requirements.txt`
-1. `python test.py`
\ No newline at end of file
+1. `python -m unittest`
+
+### Deploy
+
+Library is automatically deployed if a tag is created.
+
+Manual deployment can be peformed with:
+```bash
+python setup.py sdist bdist_wheel
+twine upload dist/*
+```
\ No newline at end of file
diff --git a/py_eth_sig_utils/__init__.py b/py_eth_sig_utils/__init__.py
index 9d67973..5dfa209 100644
--- a/py_eth_sig_utils/__init__.py
+++ b/py_eth_sig_utils/__init__.py
@@ -1,5 +1,5 @@
 name = "py_eth_sig_utils"
 
-from .main import (  # NOQA
-    encode_typed_data,
+from . import (  # NOQA
+    signing,
 )
\ No newline at end of file
diff --git a/py_eth_sig_utils/eip712/__init__.py b/py_eth_sig_utils/eip712/__init__.py
new file mode 100644
index 0000000..f1adeb4
--- /dev/null
+++ b/py_eth_sig_utils/eip712/__init__.py
@@ -0,0 +1,5 @@
+name = "eip712"
+
+from .encoding import (  # NOQA
+    encode_typed_data,
+)
\ No newline at end of file
diff --git a/py_eth_sig_utils/main.py b/py_eth_sig_utils/eip712/encoding.py
similarity index 96%
rename from py_eth_sig_utils/main.py
rename to py_eth_sig_utils/eip712/encoding.py
index 6c4be08..1f73e36 100644
--- a/py_eth_sig_utils/main.py
+++ b/py_eth_sig_utils/eip712/encoding.py
@@ -63,4 +63,4 @@ def encode_typed_data(data):
 
     domainHash = create_struct_hash("EIP712Domain", domain, types)
     messageHash = create_struct_hash(primaryType, message, types)
-    return utils.encode_hex(utils.sha3(bytes.fromhex('19') + bytes.fromhex('01') + domainHash + messageHash))
\ No newline at end of file
+    return utils.sha3(bytes.fromhex('19') + bytes.fromhex('01') + domainHash + messageHash)
\ No newline at end of file
diff --git a/py_eth_sig_utils/signing.py b/py_eth_sig_utils/signing.py
new file mode 100644
index 0000000..87fb627
--- /dev/null
+++ b/py_eth_sig_utils/signing.py
@@ -0,0 +1,22 @@
+from ethereum import utils
+from eth_utils import big_endian_to_int
+from .eip712 import encode_typed_data
+
+def signature_to_v_r_s(signature):
+    v = utils.safe_ord(signature[64])
+    r = big_endian_to_int(signature[0:32])
+    s = big_endian_to_int(signature[32:64])
+    return v, r, s
+
+def v_r_s_to_signature(v, r, s):
+    return r.to_bytes(32, 'big') + s.to_bytes(32, 'big') + v.to_bytes(1, 'big')
+
+def sign_typed_data(data, private_key):
+    msg_hash = encode_typed_data(data)
+    return utils.ecsign(msg_hash, private_key)
+
+def recover_typed_data(data, v, r, s):
+    msg_hash = encode_typed_data(data)
+    public_key = utils.ecrecover_to_pub(msg_hash, v, r, s)
+    address_bytes = utils.sha3(public_key)[-20:]
+    return utils.checksum_encode(address_bytes)
\ No newline at end of file
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
new file mode 100644
index 0000000..377337a
--- /dev/null
+++ b/scripts/deploy.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+# fail if any commands fails
+set -e
+
+python -m pip install --user --upgrade setuptools wheel
+python -m pip install --user --upgrade twine
+
+python setup.py sdist bdist_wheel
+twine upload -u $PYPI_USER -p $PYPI_PASS dist/*
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 55f2d46..3f56e5e 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
 
 setuptools.setup(
     name="py_eth_sig_utils",
-    version="0.2.0",
+    version="0.3.0",
     author="Richard Meissner",
     author_email="msc.meissner@gmail.com",
     description="Python Ethereum Signing Utils",


Test patch:
diff --git a/py_eth_sig_utils/test.py b/py_eth_sig_utils/test.py
deleted file mode 100644
index a43e290..0000000
--- a/py_eth_sig_utils/test.py
+++ /dev/null
@@ -1,222 +0,0 @@
-from ethereum import utils
-from main import *
-
-def testSignTypeData():
-    print("Test Simple")
-    data = { 
-        "types": { 
-            "EIP712Domain": [ 
-                { "name": 'name', "type": 'string' },
-                { "name": 'version', "type": 'string' },
-                { "name": 'chainId', "type": 'uint256' },
-                { "name": 'verifyingContract', "type": 'address' },
-            ],
-            "Person": [
-                { "name": 'name', "type": 'string' },
-                { "name": 'wallet', "type": 'address' }
-            ],
-            "Mail": [
-                { "name": 'from', "type": 'Person' },
-                { "name": 'to', "type": 'Person' },
-                { "name": 'contents', "type": 'string' }
-            ]
-        },
-        "primaryType": 'Mail',
-        "domain": {
-            "name": 'Ether Mail',
-            "version": '1',
-            "chainId": 1,
-            "verifyingContract": '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
-        },
-        "message": {
-            "from": {
-                "name": 'Cow',
-                "wallet": '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
-            },
-            "to": {
-                "name": 'Bob',
-                "wallet": '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
-            },
-            "contents": 'Hello, Bob!',
-        }, 
-    }
-    #print(create_schema("Mail", data["types"]))
-    assert 'Mail(Person from,Person to,string contents)Person(string name,address wallet)' == create_schema("Mail", data["types"])
-    #print(utils.encode_hex(create_schema_hash("Mail", data["types"])))
-    assert 'a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2' == utils.encode_hex(create_schema_hash("Mail", data["types"]))
-    #print(utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
-    assert 'a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8' == utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"]))
-    #print(utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
-    assert 'c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e' == utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"]))
-    #print(utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
-    assert 'f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f' == utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"]))
-    #print(encode_typed_data(data))
-    assert 'be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2' == encode_typed_data(data)
-
-def testSignTypeDataArray():
-    print("Test Arrays")
-    data = { 
-        "types": { 
-            "EIP712Domain": [ 
-                { "name": 'name', "type": 'string' },
-                { "name": 'version', "type": 'string' },
-                { "name": 'chainId', "type": 'uint256' },
-                { "name": 'verifyingContract', "type": 'address' },
-            ],
-            "Person": [
-                { "name": 'name', "type": 'string' },
-                { "name": 'wallet', "type": 'address' }
-            ],
-            "Mail": [
-                { "name": 'from', "type": 'Person' },
-                { "name": 'to', "type": 'Person[]' },
-                { "name": 'contents', "type": 'string' }
-            ]
-        },
-        "primaryType": 'Mail',
-        "domain": {
-            "name": 'Ether Mail',
-            "version": '1',
-            "chainId": 1,
-            "verifyingContract": '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
-        },
-        "message": {
-            "from": {
-                "name": 'Cow',
-                "wallet": '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
-            },
-            "to": [{
-                "name": 'Bob',
-                "wallet": '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
-            }],
-            "contents": 'Hello, Bob!',
-        }, 
-    }
-
-    #print(create_schema("Mail", data["types"]))
-    assert 'Mail(Person from,Person[] to,string contents)Person(string name,address wallet)' == create_schema("Mail", data["types"])
-    #print(utils.encode_hex(create_schema_hash("Mail", data["types"])))
-    assert 'dd57d9596af52b430ced3d5b52d4e3d5dccfdf3e0572db1dcf526baad311fbd1' == utils.encode_hex(create_schema_hash("Mail", data["types"]))
-    #print(utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
-    assert 'dd57d9596af52b430ced3d5b52d4e3d5dccfdf3e0572db1dcf526baad311fbd1fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8' == utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"]))
-    #print(utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
-    assert '25192142931f380985072cdd991e37f65cf8253ba7a0e675b54163a1d133b8ca' == utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"]))
-    #print(utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
-    assert 'f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f' == utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"]))
-    #print(encode_typed_data(data))
-    assert '0659335d0565297a1855731b0382d06cda439ea8e352de57c6f6499436a2b84e' == encode_typed_data(data)
-
-def testSignTypeDataArrayBytes():
-    print("Test Arrays and Bytes")
-    data = { 
-        "types": { 
-            "EIP712Domain": [ 
-                { "name": 'name', "type": 'string' },
-                { "name": 'version', "type": 'string' },
-                { "name": 'chainId', "type": 'uint256' },
-                { "name": 'verifyingContract', "type": 'address' },
-            ],
-            "Person": [
-                { "name": 'name', "type": 'string' },
-                { "name": 'wallet', "type": 'address' }
-            ],
-            "Mail": [
-                { "name": 'from', "type": 'Person' },
-                { "name": 'to', "type": 'Person[]' },
-                { "name": 'contents', "type": 'string' },
-                { "name": 'payload', "type": 'bytes' }
-            ]
-        },
-        "primaryType": 'Mail',
-        "domain": {
-            "name": 'Ether Mail',
-            "version": '1',
-            "chainId": 1,
-            "verifyingContract": '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
-        },
-        "message": {
-            "from": {
-                "name": 'Cow',
-                "wallet": '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
-            },
-            "to": [{
-                "name": 'Bob',
-                "wallet": '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
-            }],
-            "contents": 'Hello, Bob!',
-            "payload": '0x25192142931f380985072cdd991e37f65cf8253ba7a0e675b54163a1d133b8ca'
-        }, 
-    }
-
-    #print(create_schema("Mail", data["types"]))
-    assert 'Mail(Person from,Person[] to,string contents,bytes payload)Person(string name,address wallet)' == create_schema("Mail", data["types"])
-    #print(utils.encode_hex(create_schema_hash("Mail", data["types"])))
-    assert '3dddc94d13b9ebab8e68f1428610e81839fcd751bdee402b12d2b3de3aace1fd' == utils.encode_hex(create_schema_hash("Mail", data["types"]))
-    #print(utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
-    assert '3dddc94d13b9ebab8e68f1428610e81839fcd751bdee402b12d2b3de3aace1fdfc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8fac776d21ae071a32c362d4c20ba6586779708a56cad3a78d01b37ecb5744298' == utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"]))
-    #print(utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
-    assert '813d832c9580a8bb0d4e2f5e85eb4466ef05a6ddaae7c020800da77fb573fe4e' == utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"]))
-    #print(utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
-    assert 'f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f' == utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"]))
-    #print(encode_typed_data(data))
-    assert '78b9817f84906558ebb57f26f079e66887f444978b98d09e68a3468d48492f85' == encode_typed_data(data)
-
-def testSignTypeDataBytes():
-    print("Test Bytes")
-    data = { 
-        "types": { 
-            "EIP712Domain": [ 
-                { "name": 'name', "type": 'string' },
-                { "name": 'version', "type": 'string' },
-                { "name": 'chainId', "type": 'uint256' },
-                { "name": 'verifyingContract', "type": 'address' },
-            ],
-            "Person": [
-                { "name": 'name', "type": 'string' },
-                { "name": 'wallet', "type": 'address' }
-            ],
-            "Mail": [
-                { "name": 'from', "type": 'Person' },
-                { "name": 'to', "type": 'Person' },
-                { "name": 'contents', "type": 'string' },
-                { "name": 'payload', "type": 'bytes' }
-            ]
-        },
-        "primaryType": 'Mail',
-        "domain": {
-            "name": 'Ether Mail',
-            "version": '1',
-            "chainId": 1,
-            "verifyingContract": '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
-        },
-        "message": {
-            "from": {
-                "name": 'Cow',
-                "wallet": '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
-            },
-            "to": {
-                "name": 'Bob',
-                "wallet": '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
-            },
-            "contents": 'Hello, Bob!',
-            "payload": '0x25192142931f380985072cdd991e37f65cf8253ba7a0e675b54163a1d133b8ca'
-        }, 
-    }
-
-    #print(create_schema("Mail", data["types"]))
-    assert 'Mail(Person from,Person to,string contents,bytes payload)Person(string name,address wallet)' == create_schema("Mail", data["types"])
-    #print(utils.encode_hex(create_schema_hash("Mail", data["types"])))
-    assert '43999c52db673245777eb64b0330105de064e52179581a340a9856c32372528e' == utils.encode_hex(create_schema_hash("Mail", data["types"]))
-    #print(utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
-    assert '43999c52db673245777eb64b0330105de064e52179581a340a9856c32372528efc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8fac776d21ae071a32c362d4c20ba6586779708a56cad3a78d01b37ecb5744298' == utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"]))
-    #print(utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
-    assert 'e004bdc1ca57ba9ad5ea8c81e54dcbdb3bfce2d1d5ad92113f0871fb2a6eb052' == utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"]))
-    #print(utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
-    assert 'f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f' == utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"]))
-    #print(encode_typed_data(data))
-    assert 'b4aaf457227fec401db772ec22d2095d1235ee5d0833f56f59108c9ffc90fb4b' == encode_typed_data(data)
-
-testSignTypeData()
-testSignTypeDataArray()
-testSignTypeDataBytes()
-testSignTypeDataArrayBytes()
\ No newline at end of file
diff --git a/py_eth_sig_utils/tests/__init__.py b/py_eth_sig_utils/tests/__init__.py
new file mode 100644
index 0000000..f0321e9
--- /dev/null
+++ b/py_eth_sig_utils/tests/__init__.py
@@ -0,0 +1,5 @@
+name = "tests"
+
+from . import (  # NOQA
+    test_eip712_encode,
+)
\ No newline at end of file
diff --git a/py_eth_sig_utils/tests/test_eip712_encode.py b/py_eth_sig_utils/tests/test_eip712_encode.py
new file mode 100644
index 0000000..6d7a329
--- /dev/null
+++ b/py_eth_sig_utils/tests/test_eip712_encode.py
@@ -0,0 +1,223 @@
+import unittest
+from ethereum import utils
+from ..eip712.encoding import *
+
+class TestEIP712(unittest.TestCase):
+
+    def test_encode_type_data(self):
+        #print("Test Simple")
+        data = { 
+            "types": { 
+                "EIP712Domain": [ 
+                    { "name": 'name', "type": 'string' },
+                    { "name": 'version', "type": 'string' },
+                    { "name": 'chainId', "type": 'uint256' },
+                    { "name": 'verifyingContract', "type": 'address' },
+                ],
+                "Person": [
+                    { "name": 'name', "type": 'string' },
+                    { "name": 'wallet', "type": 'address' }
+                ],
+                "Mail": [
+                    { "name": 'from', "type": 'Person' },
+                    { "name": 'to', "type": 'Person' },
+                    { "name": 'contents', "type": 'string' }
+                ]
+            },
+            "primaryType": 'Mail',
+            "domain": {
+                "name": 'Ether Mail',
+                "version": '1',
+                "chainId": 1,
+                "verifyingContract": '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+            },
+            "message": {
+                "from": {
+                    "name": 'Cow',
+                    "wallet": '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+                },
+                "to": {
+                    "name": 'Bob',
+                    "wallet": '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+                },
+                "contents": 'Hello, Bob!',
+            }, 
+        }
+        #print(create_schema("Mail", data["types"]))
+        self.assertEqual('Mail(Person from,Person to,string contents)Person(string name,address wallet)', create_schema("Mail", data["types"]))
+        #print(utils.encode_hex(create_schema_hash("Mail", data["types"])))
+        self.assertEqual('a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2', utils.encode_hex(create_schema_hash("Mail", data["types"])))
+        #print(utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
+        self.assertEqual('a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8', utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
+        #print(utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
+        self.assertEqual('c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e', utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
+        #print(utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
+        self.assertEqual('f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f', utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
+        #print(encode_typed_data(data))
+        self.assertEqual('be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2', encode_typed_data(data).hex())
+
+    def test_encode_type_data_array(self):
+        #print("Test Arrays")
+        data = { 
+            "types": { 
+                "EIP712Domain": [ 
+                    { "name": 'name', "type": 'string' },
+                    { "name": 'version', "type": 'string' },
+                    { "name": 'chainId', "type": 'uint256' },
+                    { "name": 'verifyingContract', "type": 'address' },
+                ],
+                "Person": [
+                    { "name": 'name', "type": 'string' },
+                    { "name": 'wallet', "type": 'address' }
+                ],
+                "Mail": [
+                    { "name": 'from', "type": 'Person' },
+                    { "name": 'to', "type": 'Person[]' },
+                    { "name": 'contents', "type": 'string' }
+                ]
+            },
+            "primaryType": 'Mail',
+            "domain": {
+                "name": 'Ether Mail',
+                "version": '1',
+                "chainId": 1,
+                "verifyingContract": '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+            },
+            "message": {
+                "from": {
+                    "name": 'Cow',
+                    "wallet": '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+                },
+                "to": [{
+                    "name": 'Bob',
+                    "wallet": '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+                }],
+                "contents": 'Hello, Bob!',
+            }, 
+        }
+
+        #print(create_schema("Mail", data["types"]))
+        self.assertEqual('Mail(Person from,Person[] to,string contents)Person(string name,address wallet)', create_schema("Mail", data["types"]))
+        #print(utils.encode_hex(create_schema_hash("Mail", data["types"])))
+        self.assertEqual('dd57d9596af52b430ced3d5b52d4e3d5dccfdf3e0572db1dcf526baad311fbd1', utils.encode_hex(create_schema_hash("Mail", data["types"])))
+        #print(utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
+        self.assertEqual('dd57d9596af52b430ced3d5b52d4e3d5dccfdf3e0572db1dcf526baad311fbd1fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8', utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
+        #print(utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
+        self.assertEqual('25192142931f380985072cdd991e37f65cf8253ba7a0e675b54163a1d133b8ca', utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
+        #print(utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
+        self.assertEqual('f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f', utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
+        #print(encode_typed_data(data))
+        self.assertEqual('0659335d0565297a1855731b0382d06cda439ea8e352de57c6f6499436a2b84e', encode_typed_data(data).hex())
+
+    def test_encode_type_data_array_bytes(self):
+        #print("Test Arrays and Bytes")
+        data = { 
+            "types": { 
+                "EIP712Domain": [ 
+                    { "name": 'name', "type": 'string' },
+                    { "name": 'version', "type": 'string' },
+                    { "name": 'chainId', "type": 'uint256' },
+                    { "name": 'verifyingContract', "type": 'address' },
+                ],
+                "Person": [
+                    { "name": 'name', "type": 'string' },
+                    { "name": 'wallet', "type": 'address' }
+                ],
+                "Mail": [
+                    { "name": 'from', "type": 'Person' },
+                    { "name": 'to', "type": 'Person[]' },
+                    { "name": 'contents', "type": 'string' },
+                    { "name": 'payload', "type": 'bytes' }
+                ]
+            },
+            "primaryType": 'Mail',
+            "domain": {
+                "name": 'Ether Mail',
+                "version": '1',
+                "chainId": 1,
+                "verifyingContract": '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+            },
+            "message": {
+                "from": {
+                    "name": 'Cow',
+                    "wallet": '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+                },
+                "to": [{
+                    "name": 'Bob',
+                    "wallet": '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+                }],
+                "contents": 'Hello, Bob!',
+                "payload": '0x25192142931f380985072cdd991e37f65cf8253ba7a0e675b54163a1d133b8ca'
+            }, 
+        }
+
+        #print(create_schema("Mail", data["types"]))
+        self.assertEqual('Mail(Person from,Person[] to,string contents,bytes payload)Person(string name,address wallet)', create_schema("Mail", data["types"]))
+        #print(utils.encode_hex(create_schema_hash("Mail", data["types"])))
+        self.assertEqual('3dddc94d13b9ebab8e68f1428610e81839fcd751bdee402b12d2b3de3aace1fd', utils.encode_hex(create_schema_hash("Mail", data["types"])))
+        #print(utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
+        self.assertEqual('3dddc94d13b9ebab8e68f1428610e81839fcd751bdee402b12d2b3de3aace1fdfc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8fac776d21ae071a32c362d4c20ba6586779708a56cad3a78d01b37ecb5744298', utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
+        #print(utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
+        self.assertEqual('813d832c9580a8bb0d4e2f5e85eb4466ef05a6ddaae7c020800da77fb573fe4e', utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
+        #print(utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
+        self.assertEqual('f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f', utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
+        #print(encode_typed_data(data))
+        self.assertEqual('78b9817f84906558ebb57f26f079e66887f444978b98d09e68a3468d48492f85', encode_typed_data(data).hex())
+
+    def test_sign_type_data_bytes(self):
+        #print("Test Bytes")
+        data = { 
+            "types": { 
+                "EIP712Domain": [ 
+                    { "name": 'name', "type": 'string' },
+                    { "name": 'version', "type": 'string' },
+                    { "name": 'chainId', "type": 'uint256' },
+                    { "name": 'verifyingContract', "type": 'address' },
+                ],
+                "Person": [
+                    { "name": 'name', "type": 'string' },
+                    { "name": 'wallet', "type": 'address' }
+                ],
+                "Mail": [
+                    { "name": 'from', "type": 'Person' },
+                    { "name": 'to', "type": 'Person' },
+                    { "name": 'contents', "type": 'string' },
+                    { "name": 'payload', "type": 'bytes' }
+                ]
+            },
+            "primaryType": 'Mail',
+            "domain": {
+                "name": 'Ether Mail',
+                "version": '1',
+                "chainId": 1,
+                "verifyingContract": '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+            },
+            "message": {
+                "from": {
+                    "name": 'Cow',
+                    "wallet": '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+                },
+                "to": {
+                    "name": 'Bob',
+                    "wallet": '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+                },
+                "contents": 'Hello, Bob!',
+                "payload": '0x25192142931f380985072cdd991e37f65cf8253ba7a0e675b54163a1d133b8ca'
+            }, 
+        }
+
+        #print(create_schema("Mail", data["types"]))
+        self.assertEqual('Mail(Person from,Person to,string contents,bytes payload)Person(string name,address wallet)', create_schema("Mail", data["types"]))
+        #print(utils.encode_hex(create_schema_hash("Mail", data["types"])))
+        self.assertEqual('43999c52db673245777eb64b0330105de064e52179581a340a9856c32372528e', utils.encode_hex(create_schema_hash("Mail", data["types"])))
+        #print(utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
+        self.assertEqual('43999c52db673245777eb64b0330105de064e52179581a340a9856c32372528efc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8fac776d21ae071a32c362d4c20ba6586779708a56cad3a78d01b37ecb5744298', utils.encode_hex(encode_data(data["primaryType"], data["message"], data["types"])))
+        #print(utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
+        self.assertEqual('e004bdc1ca57ba9ad5ea8c81e54dcbdb3bfce2d1d5ad92113f0871fb2a6eb052', utils.encode_hex(create_struct_hash(data["primaryType"], data["message"], data["types"])))
+        #print(utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
+        self.assertEqual('f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f', utils.encode_hex(create_struct_hash('EIP712Domain', data["domain"], data["types"])))
+        #print(encode_typed_data(data))
+        self.assertEqual('b4aaf457227fec401db772ec22d2095d1235ee5d0833f56f59108c9ffc90fb4b', encode_typed_data(data).hex())
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file
diff --git a/py_eth_sig_utils/tests/test_sign_typed_data.py b/py_eth_sig_utils/tests/test_sign_typed_data.py
new file mode 100644
index 0000000..9f9ff46
--- /dev/null
+++ b/py_eth_sig_utils/tests/test_sign_typed_data.py
@@ -0,0 +1,55 @@
+import unittest
+from ethereum import utils
+from ..signing import *
+
+class TestSignTypedData(unittest.TestCase):
+
+    data = { 
+        "types": { 
+            "EIP712Domain": [ 
+                { "name": 'name', "type": 'string' },
+                { "name": 'version', "type": 'string' },
+                { "name": 'chainId', "type": 'uint256' },
+                { "name": 'verifyingContract', "type": 'address' },
+            ],
+            "Person": [
+                { "name": 'name', "type": 'string' },
+                { "name": 'wallet', "type": 'address' }
+            ],
+            "Mail": [
+                { "name": 'from', "type": 'Person' },
+                { "name": 'to', "type": 'Person' },
+                { "name": 'contents', "type": 'string' }
+            ]
+        },
+        "primaryType": 'Mail',
+        "domain": {
+            "name": 'Ether Mail',
+            "version": '1',
+            "chainId": 1,
+            "verifyingContract": '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+        },
+        "message": {
+            "from": {
+                "name": 'Cow',
+                "wallet": '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
+            },
+            "to": {
+                "name": 'Bob',
+                "wallet": '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
+            },
+            "contents": 'Hello, Bob!',
+        }, 
+    }
+
+    def test_sign(self):
+        private_key = utils.sha3('cow')
+        signature = v_r_s_to_signature(*sign_typed_data(self.data, private_key)).hex()
+        self.assertEqual(signature, '4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c')
+
+    def test_recover(self):
+        signer_address = recover_typed_data(self.data, *signature_to_v_r_s(bytes.fromhex('4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c')))
+        self.assertEqual('0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', signer_address)
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file