UNPKG

@colony/purser-core

Version:

A collection of helpers, utils, validators and normalizers to assist the individual purser modules

524 lines (448 loc) 18.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.getChainDefinition = exports.messageOrDataValidator = exports.userInputValidator = exports.messageVerificationObjectValidator = exports.transactionObjectValidator = exports.verifyMessageSignature = exports.recoverPublicKey = exports.derivationPathSerializer = void 0; var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); var _ethereumjsUtil = require("ethereumjs-util"); var _ethereumjsCommon = _interopRequireDefault(require("ethereumjs-common")); var _validators = require("./validators"); var _normalizers = require("./normalizers"); var _utils = require("./utils"); var _messages = require("./messages"); var _defaults = require("./defaults"); /** * Serialize an derivation path object's props into it's string counterpart * * @method derivationPathSerializer * * @param {number} purpose path purpose * @param {number} coinType path coin type (and network) * @param {number} account path account number * @param {number} change path change number * @param {number} addressIndex address index (no default since it should be manually added) * * See the defaults file for some more information regarding the format of the * Ethereum deviation path. * * All the above params are sent in as props of an {DerivationPathObjectType} object. * * @return {string} The serialized path */ var derivationPathSerializer = function derivationPathSerializer() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref$purpose = _ref.purpose, purpose = _ref$purpose === void 0 ? _defaults.PATH.PURPOSE : _ref$purpose, _ref$coinType = _ref.coinType, coinType = _ref$coinType === void 0 ? _defaults.PATH.COIN_MAINNET : _ref$coinType, _ref$account = _ref.account, account = _ref$account === void 0 ? _defaults.PATH.ACCOUNT : _ref$account, change = _ref.change, addressIndex = _ref.addressIndex; var DELIMITER = _defaults.PATH.DELIMITER; var hasChange = change || change === 0; var hasAddressIndex = addressIndex || addressIndex === 0; return ( /* * It's using a template in the last spot, eslint just donesn't recognizes it... */ /* eslint-disable-next-line prefer-template */ "".concat(_defaults.PATH.HEADER_KEY, "/").concat(purpose) + "".concat(DELIMITER).concat(coinType) + "".concat(DELIMITER).concat(account) + "".concat(DELIMITER) + /* * We're already checking if the change and address index has a value, so * we're not coercing `undefined`. * * Flow is overreacting again... */ /* $FlowFixMe */ "".concat(hasChange ? change : '') + ( /* $FlowFixMe */ hasChange && hasAddressIndex ? "/".concat(addressIndex) : '') ); }; /** * Recover a public key from a message and the signature of that message. * * @NOTE Further optimization * * This can be further optimized by writing our own recovery mechanism since we already * do most of the cleanup, checking and coversions. * * All that is left to do is to use `secp256k1` to convert and recover the public key * from the signature points (components). * * But since most of our dependencies already use `ethereumjs-util` under the hood anyway, * it's easier just use it as well. * * @method recoverPublicKey * * @param {string} message The message string to hash for the signature verification procedure * @param {string} signature The signature to recover the private key from, as a `hex` string * * All the above params are sent in as props of an {MessageVerificationObjectType} object. * * @return {String} The recovered public key. */ exports.derivationPathSerializer = derivationPathSerializer; var recoverPublicKey = function recoverPublicKey() { var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, message = _ref2.message, signature = _ref2.signature; var messages = _messages.helpers.verifyMessageSignature; var signatureBuffer = Buffer.from( /* $FlowFixMe */ (0, _normalizers.hexSequenceNormalizer)(signature.toLowerCase(), false), _defaults.HEX_HASH_TYPE); /* * It should be 65 bits in legth: * - 32 for the (R) point (component) * - 32 for the (S) point (component) * - 1 for the reco(V)ery param */ if (signatureBuffer.length !== 65) { throw new Error(messages.wrongLength); } /* * The recovery param is the the 64th bit of the signature Buffer */ var recoveryParam = (0, _normalizers.recoveryParamNormalizer)(signatureBuffer[64]); var rComponent = signatureBuffer.slice(0, 32); var sComponent = signatureBuffer.slice(32, 64); var messageHash = (0, _ethereumjsUtil.hashPersonalMessage)(Buffer.from(message)); /* * Elliptic curve recovery. * * @NOTE `ecrecover` is just a helper method * Around `secp256k1`'s `recover()` and `publicKeyConvert()` methods * * This is to what the function description comment block note is referring to */ var recoveredPublicKeyBuffer = (0, _ethereumjsUtil.ecrecover)(messageHash, recoveryParam, rComponent, sComponent); /* * Normalize and return the recovered public key */ return (0, _normalizers.hexSequenceNormalizer)(recoveredPublicKeyBuffer.toString(_defaults.HEX_HASH_TYPE)); }; /** * Verify a signed message. * By extracting it's public key from the signature and comparing it with a provided one. * * @method verifyMessageSignature * * @param {string} publicKey Public key to check against, as a 'hex' string * @param {string} message The message string to hash for the signature verification procedure * @param {string} signature The signature to recover the private key from, as a `hex` string * * All the above params are sent in as props of an {MessageVerificationObjectType} object. * * @return {boolean} true or false depending if the signature is valid or not * */ exports.recoverPublicKey = recoverPublicKey; var verifyMessageSignature = function verifyMessageSignature(_ref3) { var publicKey = _ref3.publicKey, message = _ref3.message, signature = _ref3.signature; var messages = _messages.helpers.verifyMessageSignature; try { /* * Normalize the recovered public key by removing the `0x` preifx */ var recoveredPublicKey = (0, _normalizers.hexSequenceNormalizer)( /* * We need this little go-around trick to mock just one export of * the module, while leaving the rest of the module intact so we can test it * * See: https://github.com/facebook/jest/issues/936 */ /* eslint-disable-next-line no-use-before-define */ coreHelpers.recoverPublicKey({ message: message, signature: signature }), false); /* * Remove the prefix (0x) and the header (first two bits) from the public key we * want to test against */ var normalizedPublicKey = (0, _normalizers.hexSequenceNormalizer)(publicKey, false).slice(2); /* * Last 64 bits of the private should match the first 64 bits of the recovered public key */ return !!recoveredPublicKey.includes(normalizedPublicKey); } catch (caughtError) { (0, _utils.warning)("".concat(messages.somethingWentWrong, ". Error: ").concat(caughtError.message), { level: 'high' }); return false; } }; /** * Validate an transaction object * * @NOTE We can only validate here, we can't also normalize. This is because different * wallet types expect different value formats so we must normalize them on a case by case basis. * * @method transactionObjectValidator * * @param {bigNumber} gasPrice gas price for the transaction in WEI (as an instance of bigNumber), defaults to 9000000000 (9 GWEI) * @param {bigNumber} gasLimit gas limit for the transaction (as an instance of bigNumber), defaults to 21000 * @param {number} chainId the id of the chain for which this transaction is intended. Defaults to 1 * @param {number} nonce the nonce to use for the transaction (as a number) * @param {string} to the address to which to the transaction is sent * @param {bigNumber} value the value of the transaction in WEI (as an instance of bigNumber), defaults to 1 * @param {string} inputData data appended to the transaction (as a `hex` string) * * All the above params are sent in as props of an {TransactionObjectType} object. * * @return {TransactionObjectType} The validated transaction object containing the exact passed in values */ exports.verifyMessageSignature = verifyMessageSignature; var transactionObjectValidator = function transactionObjectValidator() { var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref4$gasPrice = _ref4.gasPrice, gasPrice = _ref4$gasPrice === void 0 ? (0, _utils.bigNumber)(_defaults.TRANSACTION.GAS_PRICE) : _ref4$gasPrice, _ref4$gasLimit = _ref4.gasLimit, gasLimit = _ref4$gasLimit === void 0 ? (0, _utils.bigNumber)(_defaults.TRANSACTION.GAS_LIMIT) : _ref4$gasLimit, _ref4$chainId = _ref4.chainId, chainId = _ref4$chainId === void 0 ? _defaults.TRANSACTION.CHAIN_ID : _ref4$chainId, _ref4$nonce = _ref4.nonce, nonce = _ref4$nonce === void 0 ? _defaults.TRANSACTION.NONCE : _ref4$nonce, to = _ref4.to, _ref4$value = _ref4.value, value = _ref4$value === void 0 ? (0, _utils.bigNumber)(_defaults.TRANSACTION.VALUE) : _ref4$value, _ref4$inputData = _ref4.inputData, inputData = _ref4$inputData === void 0 ? _defaults.TRANSACTION.INPUT_DATA : _ref4$inputData; /* * Check that the gas price is a big number */ (0, _validators.bigNumberValidator)(gasPrice); /* * Check that the gas limit is a big number */ (0, _validators.bigNumberValidator)(gasLimit); /* * Check if the chain id value is valid (a positive, safe integer) */ (0, _validators.safeIntegerValidator)(chainId); /* * Check if the nonce value is valid (a positive, safe integer) */ (0, _validators.safeIntegerValidator)(nonce); /* * Only check if the address (`to` prop) is in the correct * format, if one was provided in the initial transaction object */ if (to) { (0, _validators.addressValidator)(to); } /* * Check that the value is a big number */ (0, _validators.bigNumberValidator)(value); /* * Check that the input data prop is a valid hex string sequence */ (0, _validators.hexSequenceValidator)(inputData); /* * Normalize the values and return them */ return { gasPrice: gasPrice, gasLimit: gasLimit, chainId: chainId, nonce: nonce, to: to, value: value, inputData: inputData }; }; /** * Validate a signature verification message object * * @method messageVerificationObjectValidator * * @param {string} message The message string to check the signature against * @param {string} signature The signature of the message. * * All the above params are sent in as props of an {MessageVerificationObjectType} object. * * @return {Object} The validated signature object containing the exact passed in values */ exports.transactionObjectValidator = transactionObjectValidator; var messageVerificationObjectValidator = function messageVerificationObjectValidator() { var _ref5 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, message = _ref5.message, signature = _ref5.signature; /* * Check if the messages is in the correct format */ (0, _validators.messageValidator)(message); /* * Check if the signature is in the correct format */ (0, _validators.hexSequenceValidator)(signature); return { message: message, /* * Ensure the signature has the hex `0x` prefix */ signature: (0, _normalizers.hexSequenceNormalizer)(signature) }; }; /** * Check if the user provided input is in the form of an Object and it's required props * * @method userInputValidator * * @param {Object} firstArgument The argument to validate that it's indeed an object, and that it has the required props * @param {Array} requiredEither Array of strings representing prop names of which at least one is required. * @param {Array} requiredAll Array of strings representing prop names of which all are required. * * All the above params are sent in as props of an object. */ exports.messageVerificationObjectValidator = messageVerificationObjectValidator; var userInputValidator = function userInputValidator() { var _ref6 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref6$firstArgument = _ref6.firstArgument, firstArgument = _ref6$firstArgument === void 0 ? {} : _ref6$firstArgument, _ref6$requiredEither = _ref6.requiredEither, requiredEither = _ref6$requiredEither === void 0 ? [] : _ref6$requiredEither, _ref6$requiredAll = _ref6.requiredAll, requiredAll = _ref6$requiredAll === void 0 ? [] : _ref6$requiredAll, _ref6$requiredOr = _ref6.requiredOr, requiredOr = _ref6$requiredOr === void 0 ? [] : _ref6$requiredOr; var messages = _messages.helpers.userInputValidator; /* * First we check if the argument is an Object (also, not an Array) */ if ((0, _typeof2.default)(firstArgument) !== 'object' || Array.isArray(firstArgument)) { /* * Explain the arguments format (if we're in dev mode), then throw the Error */ (0, _utils.warning)(messages.argumentsFormatExplanation); throw new Error(messages.notObject); } /* * Check if some of the required props are available * Fail if none are available. */ if (requiredEither.length) { var availableProps = requiredEither.map(function (propName) { return Object.prototype.hasOwnProperty.call(firstArgument, propName); }); if (!availableProps.some(function (propExists) { return propExists === true; })) { /* * Explain the arguments format (if we're in dev mode), then throw the Error */ (0, _utils.warning)(messages.argumentsFormatExplanation); throw new Error("".concat(messages.notSomeProps, ": { '").concat(requiredEither.join("', '"), "' }")); } } /* * Check if all required props are present. * Fail after the first one missing. */ requiredAll.map(function (propName) { if (!Object.prototype.hasOwnProperty.call(firstArgument, propName)) { /* * Explain the arguments format (if we're in dev mode), then throw the Error */ (0, _utils.warning)(messages.argumentsFormatExplanation); throw new Error("".concat(messages.notAllProps, ": { '").concat(requiredAll.join("', '"), "' }")); } return propName; }); /* * Check if exactly one of the required props is present. * Fail if multiple are present. */ if (requiredOr.length && requiredOr.reduce(function (acc, propName) { return Object.prototype.hasOwnProperty.call(firstArgument, propName) ? acc + 1 : acc; }, 0) !== 1) { (0, _utils.warning)(messages.argumentsFormatExplanation); throw new Error(); } }; exports.userInputValidator = userInputValidator; var messageOrDataValidator = function messageOrDataValidator(_ref7) { var message = _ref7.message, messageData = _ref7.messageData; if (message) { (0, _validators.messageValidator)(message); return message; } (0, _validators.messageDataValidator)(messageData); return typeof messageData === 'string' ? new Uint8Array(Buffer.from((0, _normalizers.hexSequenceNormalizer)(messageData, false), 'hex')) : messageData; }; /** * In order to support EIP-155, it's necessary to specify various * definitions for a given chain (e.g. the chain ID, network ID, hardforks). * * Given a chain ID, this function returns a chain definition in the format * expected by `ethereumjs-tx`. * * @param {number} chainId The given chain ID (as defined in EIP-155) * @return {Object} The common chain definition */ exports.messageOrDataValidator = messageOrDataValidator; var getChainDefinition = function getChainDefinition(chainId) { var baseChain = function () { switch (chainId) { /* * Ganache's default chain ID is 1337, and is also the standard for * private chains. The assumption is taken here that this inherits * all of the other properties from mainnet, but that might not be * the case. * * @TODO Provide a means to specify all chain properties for transactions */ case _defaults.CHAIN_IDS.HOMESTEAD: case _defaults.CHAIN_IDS.LOCAL: return _defaults.NETWORK_NAMES.MAINNET; case _defaults.CHAIN_IDS.GOERLI: return _defaults.NETWORK_NAMES.GOERLI; /* * The following (or other) chain IDs _may_ cause validation errors * in `ethereumjs-common` */ case _defaults.CHAIN_IDS.KOVAN: return _defaults.NETWORK_NAMES.KOVAN; case _defaults.CHAIN_IDS.ROPSTEN: return _defaults.NETWORK_NAMES.ROPSTEN; case _defaults.CHAIN_IDS.RINKEBY: return _defaults.NETWORK_NAMES.RINKEBY; default: return chainId; } }(); return { common: _ethereumjsCommon.default.forCustomChain(baseChain, { chainId: chainId }, /* * `ethereumjs-common` requires a hardfork to be defined, so we are * using the current default for this property. This is also an * assumption, and this should be made configurable. */ _defaults.HARDFORKS.PETERSBURG) }; }; /* * This default export is only here to help us with testing, otherwise * it wound't be needed */ exports.getChainDefinition = getChainDefinition; var coreHelpers = { getChainDefinition: getChainDefinition, derivationPathSerializer: derivationPathSerializer, recoverPublicKey: recoverPublicKey, verifyMessageSignature: verifyMessageSignature, transactionObjectValidator: transactionObjectValidator, messageVerificationObjectValidator: messageVerificationObjectValidator, userInputValidator: userInputValidator, messageOrDataValidator: messageOrDataValidator }; var _default = coreHelpers; exports.default = _default;