eth-typed-data
Version:
A library to simplifiy interacting with and signing EIP712 typed data
403 lines (347 loc) • 14.5 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Type;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
var _objectSpread3 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _ethereumjsAbi = _interopRequireDefault(require("ethereumjs-abi"));
var _jsSha = require("js-sha3");
var _AbstractType2 = _interopRequireDefault(require("./AbstractType"));
var _verify = require("./verify");
var _primitives = require("./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
*/
function Type(primaryType, defs) {
// Ensure that domain is defined
var 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
var properties = Array.isArray(defs) ? defs.map(function (_ref) {
var name = _ref.name,
type = _ref.type;
return validateTypeDefinition({
name: name,
type: type
}, domain);
}) : Object.keys(defs).map(function (name) {
return validateTypeDefinition({
name: 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
var 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.
*/
var StructureType =
/*#__PURE__*/
function (_AbstractType) {
(0, _inherits2.default)(StructureType, _AbstractType);
function StructureType(vals) {
var _this;
(0, _classCallCheck2.default)(this, StructureType);
_this = (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(StructureType).call(this));
_this.name = primaryType;
_this.properties = properties; // Save values for a type instance privately
_this._object = {};
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
var _loop = function _loop() {
var prop = _step.value;
if (!(prop.name in vals)) {
throw new Error("Type ".concat(_this.name, " missing required property ").concat(prop.name));
} // Expose getters and setters for this.prop, validating on set
Object.defineProperty((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), prop.name, {
get: function get() {
return _this._object[prop.name];
},
set: function set(val) {
return _this._object[prop.name] = domain.validate(prop.type, val);
}
});
_this._object[prop.name] = domain.validate(prop.type, vals[prop.name]);
};
for (var _iterator = properties[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
_loop();
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return != null) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
return _this;
}
/**
* @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
*/
(0, _createClass2.default)(StructureType, [{
key: "toObject",
/**
* @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
*/
value: function toObject() {
var _this2 = this;
// Generate a new bare object, with each complex item decomposed into regular javascript objects and arrays
return properties.reduce(function (obj, _ref2) {
var name = _ref2.name,
type = _ref2.type;
return (0, _objectSpread3.default)({}, obj, (0, _defineProperty2.default)({}, name, domain.serialize(type, _this2._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
*/
}, {
key: "toSignatureRequest",
value: function 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
*/
}, {
key: "encodeData",
value: function encodeData() {
// Build parallel lists of types and values, to be passed to abi.encode
var types = ['bytes32'];
var values = [Buffer.from(this.constructor.typeHash(), 'hex')];
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = this.constructor.properties[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var _ref4 = _step2.value;
var type = _ref4.type,
name = _ref4.name;
if ((0, _primitives.isDynamicType)(type)) {
// Dynamic types are hashed
types.push('bytes32');
values.push(Buffer.from((0, _jsSha.keccak256)(this[name]), 'hex'));
} else if (type in domain.types) {
// Structure Types are recursively encoded and hashed
types.push('bytes32');
values.push(Buffer.from((0, _jsSha.keccak256)(this[name].encodeData()), 'hex'));
} else if ((0, _primitives.isArrayType)(type)) {
// TODO: Figure out the spec for encoding array types
throw new Error('[DEV] Array types not yet supported');
} else if ((0, _primitives.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: ".concat(type));
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
return _ethereumjsAbi.default.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
*/
}, {
key: "encode",
value: function 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
*/
}, {
key: "signHash",
value: function signHash() {
return Buffer.from((0, _jsSha.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
*/
}, {
key: "sign",
value: function 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
*/
}, {
key: "verifySignature",
value: function verifySignature(signature, address) {
var hash = this.signHash();
return (0, _verify.verifyRawSignatureFromAddress)(hash, signature, address);
}
}], [{
key: "encodeDependentTypes",
value: function encodeDependentTypes() {
return this.dependencies.map(function (t) {
return domain.types[t].encodeTypeFragment();
});
}
}]);
return StructureType;
}(_AbstractType2.default); // Save the new StructureType to the domain
(0, _defineProperty2.default)(StructureType, "name", primaryType);
(0, _defineProperty2.default)(StructureType, "properties", properties);
(0, _defineProperty2.default)(StructureType, "dependencies", dependencies);
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(_ref5, domain) {
var name = _ref5.name,
type = _ref5.type;
if (!name || !type) {
throw new Error('Invalid type definition: all entries must specify name and type');
}
if ((0, _typeof2.default)(type) === 'object') {
// TODO: Allow recursive type defintions?
throw new Error('Nested type definitions not supported');
} else if (!(0, _primitives.isPrimitiveType)(type) && !(type in domain.types)) {
// Refuse undefined, non-primitive types
throw new Error("Type ".concat(type, " is undefined in this domain"));
}
return {
name: name,
type: 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) {
var found = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = props[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var _ref7 = _step3.value;
var type = _ref7.type;
if ((0, _primitives.isPrimitiveType)(type)) continue; // Merge the found array with new dependencies of
found = found.concat([type].concat((0, _toConsumableArray2.default)(findDependencies(domain.types[type].properties, domain, found))).filter(function (t) {
return !found.includes(t);
}));
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
return found;
}