eth-typed-data
Version:
A library to simplifiy interacting with and signing EIP712 typed data
234 lines (207 loc) • 8.7 kB
JavaScript
import abi from 'ethereumjs-abi'
import { keccak256 } from 'js-sha3'
import AbstractType from './AbstractType'
import Type from './Type'
import { validate as validatePrimitive, isArrayType, isPrimitiveType, getElementaryType, isNotStructureType } from './primitives'
// The set of properties that a EIP712Domain MAY implement
export const EIP712DomainProperties = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
{ name: "salt", type: "bytes32" },
]
/**
* A constructor/factory function which constructs EIP712 Domains as types,
* then returns a new instance of the domain. Since an instantiated EIP712
* Domain needs to share many static methods with the types it contains
* (e.g. encodeType, typeHash), but does not require that all properties
* are present in a given instance, we define a new prototype for each Domain
* which specifies the provided properties as its type definition, and then
* instantiates that prototype with the provided values.
* @param {Object} def The definition of the EIP712 domain
* @returns {Object} An instantiated EIP712Domain type with the specified properties
*/
export default function EIP712Domain(def) {
const vals = {}
// Extract the EIP712 domain properties that were provided
const properties = EIP712DomainProperties.reduce((props, {name, type}) => {
// Skip unused EIP712 types
if (!(name in def)) return props
// Validate primitive types
vals[name] = validatePrimitive[type](def[name])
// Include property in type definition
return [...props, {name, type}]
}, [])
// Throw an error if extra properties were provided
if (Object.keys(vals).length !== Object.keys(def).length) {
throw new Error('Extra key in EIP712Domain definition')
} else if (Object.keys(def).length === 0) {
throw new Error('Must supply at least one EIP712Domain property')
}
/**
* @classdesc
* A domain is a scope in which we can define multiple Types which can
* reference each other. A domain behaves similarly to a Type, with the
* primary difference that it also includes a set of types that can
* reference each other within it. The primary method of a
* domain is `createType()` which will return a new constructor
* for a type that lives within this domain.
*/
class Domain extends AbstractType {
static name = 'EIP712Domain'
static properties = properties
static dependencies = []
constructor(vals) {
super()
this.vals = { ...vals }
// The types object maps String names to the type prototypes that exist
// within this domain. Prototypes are appendended to this.types for every
// call to this.createType()
this.types = {}
// Precompute the domainSeparator for use with signing types in this domain
this.domainSeparator = this.hashStruct()
/**
* Construct a new type that will be associated with this domain
* @returns {Function} the constructor for the new type class
*/
this.createType = Type.bind(this)
}
/**
* Validate that a particular object conforms to a valid type definition in this domain,
* and return a standardized version of input value. In particular, structure types will
* be coerced to an instance of the corresponding structure class, array types will validate
* each item according to the base type of the array, and primitive types will be validated
* by the appropriate validator in `validatePrimitive`
*
* @param {String} type the string name of the type of the value being validated
* @param {Any} val the candidate value of the type to be validated/standardized
* @returns {Any} the standardized/validated representation of this
*
* @throws {Error} if the input is an invalid instance of the given type
*/
validate(type, val) {
if (isArrayType(type)) {
// Apply the validator to each item in an array, using the base type
return val.map(item => this.validate(getElementaryType(type), item))
} else if (isPrimitiveType(type)) {
return validatePrimitive[type](val)
} else if (type in this.types) {
const StructType = this.types[type]
return (val instanceof StructType) ? val : new StructType(val)
} else {
throw new Error(`Type ${type} not recognized in this domain`)
}
}
/**
* Recursively expand an object containing instances of structure type classes,
* and return a bare javascript object with the same hierarchical structure
* Conceptually the opposite of @see this.validate
* @param {String} type the string name of the type of value being serialized
* @param {Any} val the type instance or primitive literal being serialized
* @returns {Object}
* @throws {Error}
*/
serialize(type, val) {
if (type in this.types) {
// Recursively expand nested structure types
return val.toObject()
} else if (isArrayType(type)) {
// Map serializer to array types
return val.map(item => this.serialize(getElementaryType(type), item))
} else if (isPrimitiveType(type)) {
return val
} else {
throw new Error(`Type ${type} is not a valid type in this domain`)
}
}
/**
* Return an object mapping the names of types contained by this domain
* to their list-style type definitions
* @returns {Object} Mapping from type name -> type definition
*/
listTypes() {
return Object.keys(this.types)
.reduce((obj, t) => ({...obj, [t]: this.types[t].typeDef()}), {})
}
/**
* Concatenate the type definition for this domain, with
* the definition of all the types that it contains
* @returns {Object}
*/
toDomainDef() {
return {
[this.constructor.name]: this.constructor.typeDef(),
...this.listTypes()
}
}
/**
* @override
* A simplified encodeData function that only needs to handle string
* and atomic types. Still defers to abi.rawEncode.
* @returns {String} encoding of the definition of this Domain
*/
encodeData() {
const types = this.constructor.properties.map(({type}) =>
type === 'string' ? 'bytes32' : type)
const values = this.constructor.properties.map(({name, type}) =>
type === 'string' ? Buffer.from(keccak256(this.vals[name]), 'hex') : this.vals[name])
return abi.rawEncode(
['bytes32', ...types],
[Buffer.from(this.constructor.typeHash(), 'hex'), ...values]
)
}
/**
* @override
*/
toObject() {
return {...this.vals}
}
}
return new Domain(vals)
}
/**
* Create a message object and domain from a raw signature request object.
* @param {Object} request An object representing a signature request
* @returns {Object} the constructed {domain} and {message} instances
* @throws {Error} if signature request contains cyclic dependencies
*/
EIP712Domain.fromSignatureRequest = function fromSignatureRequest(request) {
const { types, message: rawMessage, primaryType, domain: rawDomain } = request
// Create the domain instance
const domain = new EIP712Domain(rawDomain)
// Perform a (reverse) topological sort for dependency resolution, keeping track of depth first search postorder
const postorder = []
// Keep track of already visited types, as well as potential cycles
const marked = new Set(), cyclecheck = new Set()
// Define recursive depth-first search with cycle detection
const dfs = type => {
for (const {type: subtype} of types[type]) {
if (marked.has(subtype) || isNotStructureType(subtype)) continue
if (cyclecheck.has(subtype)) {
throw new Error('Cannot construct domain from signature request with cyclic dependencies')
}
cyclecheck.add(subtype)
dfs(subtype)
}
postorder.push(type)
marked.add(type)
}
// Perform the search
for (const type of Object.keys(types)) {
if (type !== 'EIP712Domain' && !marked.has(type)) {
dfs(type)
}
}
// Create all necessary structure types in this domain
// Iterate in postorder to guarantee dependencies are satisfied
for (const name of postorder) {
if (name !== 'EIP712Domain') {
domain.createType(name, types[name])
}
}
// Create the message instance
const MessageType = domain.types[primaryType]
const message = new MessageType(rawMessage)
return { domain, message }
}