@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
JavaScript
"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;