UNPKG

eth-eip712-util-browser

Version:

(Pure JS fork) Generate hashes of typed ethereum data for signing

259 lines (235 loc) 8.37 kB
const util = require('./util') const abi = require('./abi') const Buffer = require('buffer/').Buffer const TYPED_MESSAGE_SCHEMA = { type: 'object', properties: { types: { type: 'object', additionalProperties: { type: 'array', items: { type: 'object', properties: { name: {type: 'string'}, type: {type: 'string'}, }, required: ['name', 'type'], }, }, }, primaryType: {type: 'string'}, domain: {type: 'object'}, message: {type: 'object'}, }, required: ['types', 'primaryType', 'domain', 'message'], } /** * A collection of utility functions used for signing typed data */ const TypedDataUtils = { /** * Encodes an object by encoding and concatenating each of its members * * @param {string} primaryType - Root type * @param {Object} data - Object to encode * @param {Object} types - Type definitions * @returns {string} - Encoded representation of an object */ encodeData (primaryType, data, types, useV4 = true) { const encodedTypes = ['bytes32'] const encodedValues = [this.hashType(primaryType, types)] if(useV4) { const encodeField = (name, type, value) => { if (types[type] !== undefined) { return ['bytes32', value == null ? '0x0000000000000000000000000000000000000000000000000000000000000000' : util.keccak(this.encodeData(type, value, types, useV4))] } if(value === undefined) throw new Error(`missing value for field ${name} of type ${type}`) if (type === 'bytes') { return ['bytes32', util.keccak(value)] } if (type === 'string') { // convert string to buffer - prevents ethUtil from interpreting strings like '0xabcd' as hex if (typeof value === 'string') { value = Buffer.from(value, 'utf8') } return ['bytes32', util.keccak(value)] } if (type.lastIndexOf(']') === type.length - 1) { const parsedType = type.slice(0, type.lastIndexOf('[')) const typeValuePairs = value.map(item => encodeField(name, parsedType, item)) return ['bytes32', util.keccak(abi.rawEncode( typeValuePairs.map(([type]) => type), typeValuePairs.map(([, value]) => value), ))] } return [type, value] } for (const field of types[primaryType]) { const [type, value] = encodeField(field.name, field.type, data[field.name]) encodedTypes.push(type) encodedValues.push(value) } } else { for (const field of types[primaryType]) { let value = data[field.name] if (value !== undefined) { if (field.type === 'bytes') { encodedTypes.push('bytes32') value = util.keccak(value) encodedValues.push(value) } else if (field.type === 'string') { encodedTypes.push('bytes32') // convert string to buffer - prevents ethUtil from interpreting strings like '0xabcd' as hex if (typeof value === 'string') { value = Buffer.from(value, 'utf8') } value = util.keccak(value) encodedValues.push(value) } else if (types[field.type] !== undefined) { encodedTypes.push('bytes32') value = util.keccak(this.encodeData(field.type, value, types, useV4)) encodedValues.push(value) } else if (field.type.lastIndexOf(']') === field.type.length - 1) { throw new Error('Arrays currently unimplemented in encodeData') } else { encodedTypes.push(field.type) encodedValues.push(value) } } } } return abi.rawEncode(encodedTypes, encodedValues) }, /** * Encodes the type of an object by encoding a comma delimited list of its members * * @param {string} primaryType - Root type to encode * @param {Object} types - Type definitions * @returns {string} - Encoded representation of the type of an object */ encodeType (primaryType, types) { let result = '' let deps = this.findTypeDependencies(primaryType, types).filter(dep => dep !== primaryType) deps = [primaryType].concat(deps.sort()) for (const type of deps) { const children = types[type] if (!children) { throw new Error('No type definition specified: ' + type) } result += type + '(' + types[type].map(({ name, type }) => type + ' ' + name).join(',') + ')' } return result }, /** * Finds all types within a type defintion object * * @param {string} primaryType - Root type * @param {Object} types - Type definitions * @param {Array} results - current set of accumulated types * @returns {Array} - Set of all types found in the type definition */ findTypeDependencies (primaryType, types, results = []) { primaryType = primaryType.match(/^\w*/)[0] if (results.includes(primaryType) || types[primaryType] === undefined) { return results } results.push(primaryType) for (const field of types[primaryType]) { for (const dep of this.findTypeDependencies(field.type, types, results)) { !results.includes(dep) && results.push(dep) } } return results }, /** * Hashes an object * * @param {string} primaryType - Root type * @param {Object} data - Object to hash * @param {Object} types - Type definitions * @returns {string} - Hash of an object */ hashStruct (primaryType, data, types, useV4 = true) { return util.keccak(this.encodeData(primaryType, data, types, useV4)) }, /** * Hashes the type of an object * * @param {string} primaryType - Root type to hash * @param {Object} types - Type definitions * @returns {string} - Hash of an object */ hashType (primaryType, types) { return util.keccak(this.encodeType(primaryType, types)) }, /** * Removes properties from a message object that are not defined per EIP-712 * * @param {Object} data - typed message object * @returns {Object} - typed message object with only allowed fields */ sanitizeData (data) { const sanitizedData = {} for (const key in TYPED_MESSAGE_SCHEMA.properties) { data[key] && (sanitizedData[key] = data[key]) } if (sanitizedData.types) { sanitizedData.types = Object.assign({ EIP712Domain: [] }, sanitizedData.types) } return sanitizedData }, /** * Returns the hash of a typed message as per EIP-712 for signing * * @param {Object} typedData - Types message data to sign * @returns {string} - sha3 hash for signing */ hash (typedData, useV4 = true) { const sanitizedData = this.sanitizeData(typedData) const parts = [Buffer.from('1901', 'hex')] parts.push(this.hashStruct('EIP712Domain', sanitizedData.domain, sanitizedData.types, useV4)) if (sanitizedData.primaryType !== 'EIP712Domain') { parts.push(this.hashStruct(sanitizedData.primaryType, sanitizedData.message, sanitizedData.types, useV4)) } return util.keccak(Buffer.concat(parts)) }, } module.exports = { TYPED_MESSAGE_SCHEMA, TypedDataUtils, hashForSignTypedDataLegacy: function (msgParams) { return typedSignatureHashLegacy(msgParams.data) }, hashForSignTypedData_v3: function (msgParams) { return TypedDataUtils.hash(msgParams.data, false) }, hashForSignTypedData_v4: function (msgParams) { return TypedDataUtils.hash(msgParams.data) }, } /** * @param typedData - Array of data along with types, as per EIP712. * @returns Buffer */ function typedSignatureHashLegacy(typedData) { const error = new Error('Expect argument to be non-empty array') if (typeof typedData !== 'object' || !typedData.length) throw error const data = typedData.map(function (e) { return e.type === 'bytes' ? util.toBuffer(e.value) : e.value }) const types = typedData.map(function (e) { return e.type }) const schema = typedData.map(function (e) { if (!e.name) throw error return e.type + ' ' + e.name }) return abi.soliditySHA3( ['bytes32', 'bytes32'], [ abi.soliditySHA3(new Array(typedData.length).fill('string'), schema), abi.soliditySHA3(types, data) ] ) }