eth-typed-data
Version:
A library to simplifiy interacting with and signing EIP712 typed data
257 lines (230 loc) • 9.19 kB
JavaScript
import abi from 'ethereumjs-abi'
import { keccak256 } from 'js-sha3'
import AbstractType from './AbstractType'
import { verifyRawSignatureFromAddress } from './verify'
import {
isArrayType, isPrimitiveType,
isDynamicType, isAtomicType,
} from './primitives'
/**
* A factory function which returns a class representing an EIP712 Type
* The returned Type can be instantiated and validated as an instance
* of a particular type
*
* There are two acceptable formats for the type definitions, a list of
* {
* name1: 'string',
* name2: 'string',
* }
*
* or
*
* [{
* name: 'name1',
* type: 'string'
* }, {
* name: 'name2',
* type: 'string'
* }]
*
* @this {Domain} The domain object to which the returned type should be associated *
* @param {String} name A String to define the type
* @param {Object|Object[]} defs The definition of the type's members and their types
* @returns {Function} the constructor for the new StructureType
*/
export default function Type (primaryType, defs) {
// Ensure that domain is defined
const domain = this || { types: {} }
if (!domain.types) domain.types = {}
// Process the type definition into an array of {name, type} pairs,
// keeping track of all dependent types
const properties = Array.isArray(defs)
? defs.map(({name, type}) => validateTypeDefinition({name, type}, domain))
: Object.keys(defs).map((name) => validateTypeDefinition({name, type: defs[name]}, domain))
// Descend through the properties list to come up with a list of
// structure types that this type includes, sorted alphabetically
const dependencies = findDependencies(properties, domain, []).sort()
/**
* @classdesc
* This is the dynamically created class that represents a particular Struct type in
* the EIP712 scheme. This can be instantiated with particular values that match the
* type's definition, and provides methods to encode the type in various formats, and
* sign it with a provided signer.
*/
class StructureType extends AbstractType {
static name = primaryType
static properties = properties
static dependencies = dependencies
constructor (vals) {
super()
this.name = primaryType
this.properties = properties
// Save values for a type instance privately
this._object = {}
for (const prop of properties) {
if (!(prop.name in vals)) {
throw new Error(`Type ${this.name} missing required property ${prop.name}`)
}
// Expose getters and setters for this.prop, validating on set
Object.defineProperty(this, prop.name, {
get: () => this._object[prop.name],
set: (val) => this._object[prop.name] = domain.validate(prop.type, val)
})
this._object[prop.name] = domain.validate(prop.type, vals[prop.name])
}
}
/**
* @override @static
* Return the part of the type encoding that consists of the dependent types
* i.e. the nested Structure types contained by this type.
* @returns {String} A string encoding all types upon which this type depends
*/
static encodeDependentTypes() {
return this.dependencies.map(t => domain.types[t].encodeTypeFragment())
}
/**
* @override
* Return a bare object representation of this instance (as a new object)
*
* @returns {Object} new object containing same key-value pairs of this instance
*/
toObject() {
// Generate a new bare object, with each complex item decomposed into regular javascript objects and arrays
return properties.reduce((obj, {name, type}) =>
({...obj, [name]: domain.serialize(type, this._object[name])}), {})
}
/**
* Encode this object along with its type and domain as a full EIP712
* signature request, defining the types, domain, primaryType, and message
* to be signed. The output is suitable for use with web3.eth.signTypedData
* @returns {Object} the signature request encoding of this instance
*/
toSignatureRequest() {
return {
types: domain.toDomainDef(),
domain: domain.toObject(),
primaryType: this.name,
message: this.toObject()
}
}
/**
* @override
* Return the EIP712 data encoding of this instance, padding each member
* to 32 bytes and hashing the result. The ethereumjs-abi module provides
* an encode() function which does most of the heavy lifting here, and the
* structure of this function significantly inspired by the sample code
* provided in the original EIP712 proposal.
* @returns {String} the encoded data of this instance, ready for use with hashStruct
*/
encodeData() {
// Build parallel lists of types and values, to be passed to abi.encode
let types = ['bytes32']
let values = [Buffer.from(this.constructor.typeHash(), 'hex')]
for (const {type, name} of this.constructor.properties) {
if (isDynamicType(type)) {
// Dynamic types are hashed
types.push('bytes32')
values.push(Buffer.from(keccak256(this[name]), 'hex'))
} else if (type in domain.types) {
// Structure Types are recursively encoded and hashed
types.push('bytes32')
values.push(Buffer.from(keccak256(this[name].encodeData()), 'hex'))
} else if (isArrayType(type)) {
// TODO: Figure out the spec for encoding array types
throw new Error('[DEV] Array types not yet supported')
} else if (isAtomicType(type)) {
// Atomic types have their encoding defined by the solidity ABI
types.push(type)
values.push(this[name])
} else {
throw new Error(`Unknown type: ${type}`)
}
}
return abi.rawEncode(types, values)
}
/**
* ABI encode this type according to the EIP712 spec. The encoding returned
* is compatible with solidity, and ready to be hashed and signed.
* @returns {String} The abi encoding of this instance, in an appropriate format to be hashed and signed
*/
encode() {
// \x19\x01 is the specified prefix for a typedData message
return Buffer.concat([
Buffer.from([0x19, 0x01]),
domain.domainSeparator,
this.hashStruct()
])
}
/**
* Return the hash to be signed, simply the Keccak256 of the encoding
* @returns {String} The hash, ready to be signed
*/
signHash() {
return Buffer.from(keccak256(this.encode()), 'hex')
}
/**
* Sign the fully encoded version of the current instance with a provided
* signer. This is equivalent to the `web3.eth.signTypedData` function.
* @param {Object} signer The signer function, which takes a buffer and return a signature
* @returns {String} the signed, encoded piece of data
*/
sign(signer) {
if (typeof signer !== 'function') {
throw new Error('Must provide a signer function')
}
return signer(this.signHash())
}
/**
* Verify a signature made by an address over this object
* @param {Object} signature
* @param {String} address
* @returns {Boolean} whether the given signature is valid over this object
*/
verifySignature(signature, address) {
const hash = this.signHash()
return verifyRawSignatureFromAddress(hash, signature, address)
}
}
// Save the new StructureType to the domain
domain.types[primaryType] = StructureType
return StructureType
}
/**
* Check the validity of a particular name/type pair within a given domain,
* and return an object containing the name and type.
*
* @param {Object} item the item being validated, must contain `name` and `type` keys
* @param {Object} domain the domain in which the item should be validated
*/
function validateTypeDefinition({name, type}, domain) {
if (!name || !type) {
throw new Error('Invalid type definition: all entries must specify name and type')
}
if (typeof type === 'object') {
// TODO: Allow recursive type defintions?
throw new Error('Nested type definitions not supported')
} else if (!isPrimitiveType(type) && !(type in domain.types)) {
// Refuse undefined, non-primitive types
throw new Error(`Type ${type} is undefined in this domain`)
}
return {name, type}
}
/**
* Recursively search a list of properties to uncover a list of all dependent types
* Each non-primitive type delegates to the dependencies of that type
*
* @param {Object[]} props The properties of a particular
* @param {Object|Domain} domain The domain object in which the dependencies are being resolved
* @param {String[]} found A list of structure type names found so far
*/
function findDependencies(props, domain, found=[]) {
for (let {type} of props) {
if (isPrimitiveType(type)) continue
// Merge the found array with new dependencies of
found = found.concat(
[type, ...findDependencies(domain.types[type].properties, domain, found)]
.filter(t => !found.includes(t))
)
}
return found
}