Skip to content

Repository files navigation

EIP-712 Structs Build StatusCoverage Status

A python interface for simple EIP-712 struct construction.

In this module, a "struct" is structured data as defined in the standard. It is not the same as the Python Standard Library's struct (e.g., import struct).

Read the proposal:
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md

Supported Python Versions

  • 3.6
  • 3.7

Install

pip install eip712-structs

Usage

See API.md for a succinct summary of available methods.

Examples/Details below.

Quickstart

Say we want to represent the following struct, convert it to a message and sign it:

struct MyStruct {
string some_string;
uint256 some_number;
}

With this module, that would look like:

# Make a unique domainfromeip712_structsimportmake_domaindomain=make_domain(name='Some name', version='1.0.0') # Make a Domain Separator# Define your struct typefromeip712_structsimportEIP712Struct, String, UintclassMyStruct(EIP712Struct):
some_string=String()
some_number=Uint(256)
# Create an instance with some datamine=MyStruct(some_string='hello world', some_number=1234)
# Values can be get/set dictionary-style:mine['some_number'] =4567assertmine['some_string'] =='hello world'assertmine['some_number'] ==4567# Into a message dict - domain requiredmy_msg=mine.to_message(domain)
# Into message JSON - domain required.# This method converts bytes types for you, which the default JSON encoder won't handle.my_msg_json=mine.to_message_json(domain)
# Into signable bytes - domain requiredmy_bytes=mine.signable_bytes(domain)

See Member Types for more information on supported types.

Dynamic construction

Attributes may be added dynamically as well. This may be necessary if you want to use a reserved keyword like from.

fromeip712_structsimportEIP712Struct, AddressclassMessage(EIP712Struct):
passMessage.to=Address()
setattr(Message, 'from', Address())
# At this point, Message is equivalent to `struct Message { address to; address from; }`

The domain separator

EIP-712 specifies a domain struct, to differentiate between identical structs that may be unrelated. A helper method exists for this purpose. All values to the make_domain() function are optional - but at least one must be defined. If omitted, the resulting domain struct's definition leaves out the parameter entirely.

The full signature:
make_domain(name: string, version: string, chainId: uint256, verifyingContract: address, salt: bytes32)

Setting a default domain

Constantly providing the same domain can be cumbersome. You can optionally set a default, and then forget it. It is automatically used by .to_message() and .signable_bytes()

importeip712_structsfoo=SomeStruct()
my_domain=eip712_structs.make_domain(name='hello world')
eip712_structs.default_domain=my_domainassertfoo.to_message() ==foo.to_message(my_domain)
assertfoo.signable_bytes() ==foo.signable_bytes(my_domain)

Member Types

Basic types

EIP712's basic types map directly to solidity types.

fromeip712_structsimportAddress, Boolean, Bytes, Int, String, UintAddress() # Solidity's 'address'Boolean() # 'bool'Bytes() # 'bytes'Bytes(N) # 'bytesN' - N must be an int from 1 through 32Int(N) # 'intN' - N must be a multiple of 8, from 8 to 256String() # 'string'Uint(N) # 'uintN' - N must be a multiple of 8, from 8 to 256

Use like:

fromeip712_structsimportEIP712Struct, Address, BytesclassFoo(EIP712Struct):
member_name_0=Address()
member_name_1=Bytes(5)
# ...etc

Struct references

In addition to holding basic types, EIP712 structs may also hold other structs! Usage is almost the same - the difference is you don't "instantiate" the class.

Example:

fromeip712_structsimportEIP712Struct, StringclassDog(EIP712Struct):
name=String()
breed=String()
classPerson(EIP712Struct):
name=String()
dog=Dog# Take note - no parentheses!# Dog "stands alone"Dog.encode_type() # Dog(string name,string breed)# But Person knows how to include DogPerson.encode_type() # Person(string name,Dog dog)Dog(string name,string breed)

Instantiating the structs with nested values may be done a couple different ways:

# Method one: set it to a structdog=Dog(name='Mochi', breed='Corgi')
person=Person(name='E.M.', dog=dog)
# Method two: set it to a dict - the underlying struct is built for youperson=Person(
name='E.M.',
dog={
'name': 'Mochi',
'breed': 'Corgi',
}
)

Arrays

Arrays are also supported for the standard.

array_member=Array(<item_type>[, <optional_length>])
  • <item_type> - The basic type or struct that will live in the array
  • <optional_length> - If given, the array is set to that length.

For example:

dynamic_array=Array(String()) # String[] dynamic_arraystatic_array=Array(String(), 10) # String[10] static_arraystruct_array=Array(MyStruct, 10) # MyStruct[10] - again, don't instantiate structs like the basic types

Development

Contributions always welcome.

Install dependencies:

  • pip install -r requirements.txt

Run tests:

  • python setup.py test
  • 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:

  • Bump the version number in setup.py, commit it into master.
  • Make a release tag on the master branch in Github. Travis should handle the rest.

Shameless Plug

Written by ConsenSys for the world! ❤️

About

EIP712 data structure management for python

Topics

Resources

Stars

35 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages